mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-12 13:23:48 -05:00
Compare commits
1778 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
30577b0c17 | ||
|
|
212de25409 | ||
|
|
5da8bbf45a | ||
|
|
aa0e10ead7 | ||
|
|
a9906c7c29 | ||
|
|
9fefe08b3f | ||
|
|
684bfff713 | ||
|
|
3db6b88d82 | ||
|
|
8ae96774df | ||
|
|
f68017d21e | ||
|
|
2b725a4db5 | ||
|
|
af1a5f130b | ||
|
|
86e3994e87 | ||
|
|
1e6ac5590e | ||
|
|
5e42b1c9f8 | ||
|
|
9d842630c9 | ||
|
|
77cb906dfe | ||
|
|
a59e846f74 | ||
|
|
6e6340a0c9 | ||
|
|
357e82e51b | ||
|
|
394d8cfd18 | ||
|
|
ef80bd401f | ||
|
|
f51d61d57a | ||
|
|
1dd256a68a | ||
|
|
85644aa3fb | ||
|
|
effbc258bb | ||
|
|
e615600ff1 | ||
|
|
60465c4664 | ||
|
|
c03c058bd5 | ||
|
|
7238c8944d | ||
|
|
9a41fd5327 | ||
|
|
b471042037 | ||
|
|
2886f06977 | ||
|
|
d630484eeb | ||
|
|
e24299239e | ||
|
|
d2fa470165 | ||
|
|
168453da71 | ||
|
|
23a06d63ac | ||
|
|
751aa1944a | ||
|
|
05b5f3f845 | ||
|
|
16fc6862a8 | ||
|
|
7e1c0f39e7 | ||
|
|
deccf20a35 | ||
|
|
73c0a645e0 | ||
|
|
e975bd0c8d | ||
|
|
78da928727 | ||
|
|
11962dabba | ||
|
|
dceb5d09cd | ||
|
|
b4cccf23d4 | ||
|
|
b911af200c | ||
|
|
68683c444f | ||
|
|
a185593d65 | ||
|
|
525040238e | ||
|
|
33f89a08f3 | ||
|
|
11645e1fac | ||
|
|
6390140539 | ||
|
|
072066c49c | ||
|
|
a2e9366c84 | ||
|
|
391669a451 | ||
|
|
0c6c76e081 | ||
|
|
f1520bdde6 | ||
|
|
3089880f18 | ||
|
|
ab11b74be4 | ||
|
|
a5a97be017 | ||
|
|
80b5bc1b68 | ||
|
|
5c7dcaffe8 | ||
|
|
5095899245 | ||
|
|
4800e5d2ae | ||
|
|
3b1e37f718 | ||
|
|
6577388250 | ||
|
|
3b9dbd4146 | ||
|
|
a1260feeed | ||
|
|
7322504ad0 | ||
|
|
de569f0052 | ||
|
|
e7097a9d25 | ||
|
|
c1dbc800e5 | ||
|
|
951746297e | ||
|
|
984304568d | ||
|
|
723217bdea | ||
|
|
0fdb71f7e4 | ||
|
|
12ce76b56a | ||
|
|
0030d18448 | ||
|
|
0e3e6ac442 | ||
|
|
430e8193e0 | ||
|
|
03e8ed4d88 | ||
|
|
ef492f6178 | ||
|
|
8eea45ef50 | ||
|
|
ff951341c9 | ||
|
|
df570afd52 | ||
|
|
07d755df11 | ||
|
|
37585bd5a5 | ||
|
|
89e24bf8f2 | ||
|
|
8d2fcd3518 | ||
|
|
f39ab3875e | ||
|
|
82efe6c60d | ||
|
|
75972d59a8 | ||
|
|
e7d60aac9c | ||
|
|
a0bfbdd49c | ||
|
|
ba594982f0 | ||
|
|
2157f4f193 | ||
|
|
309bae423c | ||
|
|
4f8bf2ae78 | ||
|
|
85c1f8a9e0 | ||
|
|
e00e7e1e56 | ||
|
|
1a6defdbcc | ||
|
|
ef577a6509 | ||
|
|
b7c6838e45 | ||
|
|
91d04cec5c | ||
|
|
3bd8441079 | ||
|
|
8cf45a5197 | ||
|
|
8dc1377efb | ||
|
|
6c32148f90 | ||
|
|
315e568de0 | ||
|
|
5d16b28869 | ||
|
|
5624a89231 | ||
|
|
63c42b14f2 | ||
|
|
6f1eaa9b39 | ||
|
|
ca42e5e00a | ||
|
|
61feee690c | ||
|
|
d4ed955aee | ||
|
|
b46227dcb6 | ||
|
|
fd8d371ac7 | ||
|
|
0e06e298d4 | ||
|
|
72df905902 | ||
|
|
0d748a0699 | ||
|
|
27c40dc6b0 | ||
|
|
8e34e6fbb4 | ||
|
|
3bc98ed623 | ||
|
|
70a92a858a | ||
|
|
49d04374a4 | ||
|
|
8540902a35 | ||
|
|
8c6fcee3ca | ||
|
|
13803d0dbb | ||
|
|
423986996a | ||
|
|
1c9e7b7ea6 | ||
|
|
6de1ad9d3d | ||
|
|
5004ae3457 | ||
|
|
e67cc75063 | ||
|
|
0edbcbdf19 | ||
|
|
f0fe79dd3b | ||
|
|
daa1958f86 | ||
|
|
2c26f02f5c | ||
|
|
af87650bc4 | ||
|
|
2b19c0bc68 | ||
|
|
76a2dcb5a9 | ||
|
|
68ec3d1c10 | ||
|
|
2ff19084ca | ||
|
|
62f062ecfa | ||
|
|
cce17ad0a0 | ||
|
|
b8296a91b9 | ||
|
|
6e9452b06e | ||
|
|
888fd35689 | ||
|
|
1fb0fbca58 | ||
|
|
ddd2a109e4 | ||
|
|
87504a528e | ||
|
|
6eac4af7db | ||
|
|
89de1340af | ||
|
|
9e753a0d44 | ||
|
|
f57920ad90 | ||
|
|
7dbbbef51a | ||
|
|
7add75126d | ||
|
|
d207672bd5 | ||
|
|
851fa38251 | ||
|
|
43345fb642 | ||
|
|
9ff33814ea | ||
|
|
21b94d2de5 | ||
|
|
24236860c8 | ||
|
|
3f868fd792 | ||
|
|
417bca03df | ||
|
|
cce6aef557 | ||
|
|
eb3afc03b5 | ||
|
|
7f0caf0683 | ||
|
|
7f606665cb | ||
|
|
202872c2dc | ||
|
|
93aeae1985 | ||
|
|
5c34ab6692 | ||
|
|
390b49653b | ||
|
|
b877c385f0 | ||
|
|
9c47739c0e | ||
|
|
04aa2992e7 | ||
|
|
2f1edeff78 | ||
|
|
306d51cdcf | ||
|
|
54a026525a | ||
|
|
d6588fc835 | ||
|
|
5a7b41a2cf | ||
|
|
338a73d764 | ||
|
|
c20954f020 | ||
|
|
1e8e1d3c9d | ||
|
|
f6b1962056 | ||
|
|
b3b101a89c | ||
|
|
9615c4edf1 | ||
|
|
85a75ee035 | ||
|
|
1e5bd55672 | ||
|
|
37d4015d56 | ||
|
|
6b27554cdb | ||
|
|
fc1b119159 | ||
|
|
2cd0d4a9f7 | ||
|
|
fd03aabeb2 | ||
|
|
8068c975c2 | ||
|
|
a6d2ab3360 | ||
|
|
fe7b91dfd9 | ||
|
|
5784101bea | ||
|
|
eaf6eb8978 | ||
|
|
3af63bcf1f | ||
|
|
80a21f7a75 | ||
|
|
0b33dc6ce1 | ||
|
|
64a6ced62e | ||
|
|
438f6c96cd | ||
|
|
6ae085f974 | ||
|
|
cb8e97274e | ||
|
|
c4185e81e8 | ||
|
|
0580fe9046 | ||
|
|
1b1bc9ea36 | ||
|
|
c2614467cf | ||
|
|
077ae51f05 | ||
|
|
ee40212e97 | ||
|
|
7f5f6efbac | ||
|
|
45d4c57d91 | ||
|
|
41e0208335 | ||
|
|
2f8238342b | ||
|
|
e1582b8323 | ||
|
|
7cfa6f0265 | ||
|
|
e3973c74e7 | ||
|
|
a8deca2dd9 | ||
|
|
a78ade1771 | ||
|
|
79d2ef4616 | ||
|
|
5edc3f755c | ||
|
|
288976310b | ||
|
|
58b5be8ab6 | ||
|
|
26d7896877 | ||
|
|
fd6bc7308f | ||
|
|
6c41c95f28 | ||
|
|
446e04469d | ||
|
|
5097e563df | ||
|
|
c7ad97c641 | ||
|
|
9516fe3324 | ||
|
|
20cdbac8c3 | ||
|
|
e3e7b3360c | ||
|
|
655dfb8328 | ||
|
|
9b9c67b768 | ||
|
|
5b7457ff08 | ||
|
|
48adad5454 | ||
|
|
b27dc3eb17 | ||
|
|
e89eebb7ba | ||
|
|
fee404399a | ||
|
|
6b4805ca1a | ||
|
|
159699b5d7 | ||
|
|
af809c9661 | ||
|
|
329de8f416 | ||
|
|
e825b07e85 | ||
|
|
71fdb99a07 | ||
|
|
55ee4186aa | ||
|
|
941b0a0ff7 | ||
|
|
6aae12288e | ||
|
|
302cc552ef | ||
|
|
a2a4df0886 | ||
|
|
3399e39968 | ||
|
|
87874bba88 | ||
|
|
c304fc4333 | ||
|
|
6977cf268f | ||
|
|
931c78a70c | ||
|
|
8d23646fe6 | ||
|
|
656963e018 | ||
|
|
644277faf1 | ||
|
|
0558dfee79 | ||
|
|
487c8fe88f | ||
|
|
0d171ba1d8 | ||
|
|
2069bbc8b5 | ||
|
|
053d628b53 | ||
|
|
6bc592e6c9 | ||
|
|
6c76d8cd1c | ||
|
|
a09e411936 | ||
|
|
02a7b96f33 | ||
|
|
e55e029ae8 | ||
|
|
6b18b144cf | ||
|
|
6d53089cc1 | ||
|
|
e85a8a68d0 | ||
|
|
dc55e68524 | ||
|
|
462c68b625 | ||
|
|
999d374f0c | ||
|
|
b208aa675e | ||
|
|
2b98fee136 | ||
|
|
e5e75efebc | ||
|
|
4a4fef2daf | ||
|
|
ecb6b234cc | ||
|
|
39dbc8acdb | ||
|
|
a56489bc7f | ||
|
|
99927c7071 | ||
|
|
3e28403978 | ||
|
|
37370f057f | ||
|
|
f4b46fad27 | ||
|
|
9d2c6a95f4 | ||
|
|
376a76d1d3 | ||
|
|
1fcc07e54e | ||
|
|
8db3345c2f | ||
|
|
69aa2fea68 | ||
|
|
298749bfcd | ||
|
|
f1f31baae1 | ||
|
|
e1c8f19e8f | ||
|
|
5e302c70e9 | ||
|
|
4c5a679066 | ||
|
|
41f0b2c354 | ||
|
|
a0a3c349c9 | ||
|
|
bc3983181d | ||
|
|
980b58ef5a | ||
|
|
a2604c0963 | ||
|
|
6dbc108da2 | ||
|
|
bd98f988f0 | ||
|
|
06301c7847 | ||
|
|
18a1aeaa91 | ||
|
|
c9f16b6430 | ||
|
|
bc9d2abdb6 | ||
|
|
28810c178f | ||
|
|
a9e64efe45 | ||
|
|
6b5886c034 | ||
|
|
7727ad43af | ||
|
|
bbe10f4f77 | ||
|
|
5e72709613 | ||
|
|
9e85cba0d0 | ||
|
|
4b59ced08f | ||
|
|
8dbdd55730 | ||
|
|
6725151a99 | ||
|
|
d4f3d5a164 | ||
|
|
7b5ccc45bc | ||
|
|
940214a1a2 | ||
|
|
68bd410159 | ||
|
|
b13fcfd831 | ||
|
|
07ef2b051c | ||
|
|
3fc795340d | ||
|
|
70cfa6af13 | ||
|
|
dbcaec59ae | ||
|
|
faedae708e | ||
|
|
0c66521b23 | ||
|
|
bf92862459 | ||
|
|
1a68698d76 | ||
|
|
842a73357c | ||
|
|
5efdeccdbb | ||
|
|
050777b8c4 | ||
|
|
a4d78e2200 | ||
|
|
b49f22cdf9 | ||
|
|
7e483b0c25 | ||
|
|
3cf9ae04c7 | ||
|
|
bf0cb4bfe2 | ||
|
|
773133c4ce | ||
|
|
ca0b3b6fd7 | ||
|
|
f4731c0514 | ||
|
|
34f16e5b7d | ||
|
|
83e9af6601 | ||
|
|
8bbf9335e1 | ||
|
|
159f30b37f | ||
|
|
2e3dc75425 | ||
|
|
7d3575b362 | ||
|
|
35d407021c | ||
|
|
076f49d447 | ||
|
|
0665fe0413 | ||
|
|
669a6fee40 | ||
|
|
8aab0fc189 | ||
|
|
5d6eb5bfd6 | ||
|
|
cf4711d878 | ||
|
|
21d664d670 | ||
|
|
ab182e276b | ||
|
|
96a3250152 | ||
|
|
f5746002fd | ||
|
|
e1e3339770 | ||
|
|
3a5086796d | ||
|
|
11300913a4 | ||
|
|
e65f14cbed | ||
|
|
6898849e3e | ||
|
|
2d61691bb2 | ||
|
|
eba9e04e2e | ||
|
|
33f32de690 | ||
|
|
93b8f61551 | ||
|
|
7f17a9d1b4 | ||
|
|
d34e4cf698 | ||
|
|
6b592137b9 | ||
|
|
d5e72bf55d | ||
|
|
5677e5e133 | ||
|
|
7a11a06cbd | ||
|
|
a5862d4b9c | ||
|
|
a50909e806 | ||
|
|
7c8f7d3f20 | ||
|
|
9078197446 | ||
|
|
0fe07cf9fe | ||
|
|
2216169ca1 | ||
|
|
50e989ca85 | ||
|
|
fa1fc3d855 | ||
|
|
bbe696e925 | ||
|
|
5d12f523a3 | ||
|
|
d295d20dc4 | ||
|
|
2ba10071c9 | ||
|
|
505dc0491b | ||
|
|
54a4ab0f26 | ||
|
|
e03e91477b | ||
|
|
88ac397158 | ||
|
|
6fd4be580b | ||
|
|
53348feb89 | ||
|
|
337cdbb37c | ||
|
|
05fdf91fc5 | ||
|
|
c387689d1c | ||
|
|
cb9238dc4e | ||
|
|
a484811f78 | ||
|
|
111d1934c4 | ||
|
|
972fb1a29d | ||
|
|
3a6af27586 | ||
|
|
c89ac341e4 | ||
|
|
cd59e5d07b | ||
|
|
0b940e4b2b | ||
|
|
b29375c844 | ||
|
|
76d3f6d248 | ||
|
|
e87a85a179 | ||
|
|
11407bf656 | ||
|
|
c82fb3c9b9 | ||
|
|
309e1d8619 | ||
|
|
c2db67c1c0 | ||
|
|
9526594905 | ||
|
|
3d74d277aa | ||
|
|
fc274c2ba4 | ||
|
|
ce43ea9f42 | ||
|
|
21da02fac2 | ||
|
|
19569bd5c5 | ||
|
|
afa25d8c57 | ||
|
|
1ba7acf4bd | ||
|
|
a847fe8754 | ||
|
|
5bb18b6441 | ||
|
|
876c233a26 | ||
|
|
ee5aeb80a4 | ||
|
|
02ceae15a2 | ||
|
|
e514739280 | ||
|
|
72265298f9 | ||
|
|
4b700192c1 | ||
|
|
fe83589ade | ||
|
|
fcf63c74f1 | ||
|
|
c95bb109c8 | ||
|
|
bd9c46ee34 | ||
|
|
736aeaa1d3 | ||
|
|
dd1f26522c | ||
|
|
712b7b2188 | ||
|
|
5b749e2d5c | ||
|
|
d85a69a709 | ||
|
|
a425e96fb2 | ||
|
|
7763fdf6ba | ||
|
|
dd156b59fc | ||
|
|
36dceecd58 | ||
|
|
421b9b271a | ||
|
|
ed57dcb924 | ||
|
|
95c77bfb98 | ||
|
|
2e3e721344 | ||
|
|
da2c28d5c2 | ||
|
|
dbddee9de9 | ||
|
|
8731d75607 | ||
|
|
f2ce233a6d | ||
|
|
6a75e30941 | ||
|
|
a3244c4892 | ||
|
|
a5ad8fd3bd | ||
|
|
deccdb1ec5 | ||
|
|
12a43b5e62 | ||
|
|
e1291aa6d2 | ||
|
|
bb26f32ac7 | ||
|
|
4d928001b8 | ||
|
|
c4baa6a10c | ||
|
|
71dec3dc5e | ||
|
|
e5017c0431 | ||
|
|
cbb5134874 | ||
|
|
ff248d566d | ||
|
|
6ccc12c332 | ||
|
|
2a669e9a17 | ||
|
|
5130abe76f | ||
|
|
fa7c8977a8 | ||
|
|
24fa183297 | ||
|
|
131aa5dd15 | ||
|
|
a06ccc928f | ||
|
|
d09ad13208 | ||
|
|
8ac37d5927 | ||
|
|
7ef0e50507 | ||
|
|
62ab8ece5e | ||
|
|
8e2e63f9b9 | ||
|
|
f96173cbe4 | ||
|
|
11015df52f | ||
|
|
05ed57a9f0 | ||
|
|
4bece04207 | ||
|
|
ede7bfb901 | ||
|
|
44d3faa048 | ||
|
|
e0036b5ad2 | ||
|
|
208d4f2173 | ||
|
|
dc3957ce79 | ||
|
|
4ecb7f3a16 | ||
|
|
03f5ef08c8 | ||
|
|
2720816266 | ||
|
|
1896aa1748 | ||
|
|
5b68027bee | ||
|
|
48863ac55c | ||
|
|
d64828ce6d | ||
|
|
2aa739be81 | ||
|
|
9977a3e9fc | ||
|
|
f8082bc53a | ||
|
|
996dcb14a3 | ||
|
|
0c127cfdc1 | ||
|
|
ae274158de | ||
|
|
340af463cd | ||
|
|
78a3f81972 | ||
|
|
d18b8e0d2c | ||
|
|
6c6c0a4778 | ||
|
|
a16d8f66a9 | ||
|
|
45793d75c2 | ||
|
|
9d545f9578 | ||
|
|
a30999a785 | ||
|
|
1a50f1eca1 | ||
|
|
1448d631a7 | ||
|
|
fd137a9e87 | ||
|
|
3670273719 | ||
|
|
6c0fd7f9ca | ||
|
|
42a2371d26 | ||
|
|
45faad7e04 | ||
|
|
73eacf1137 | ||
|
|
7b0d9e1e07 | ||
|
|
c7b0764002 | ||
|
|
847c512539 | ||
|
|
97330ee8fc | ||
|
|
0508e70f9b | ||
|
|
8a502af4c1 | ||
|
|
c60bfb2b0f | ||
|
|
16b5902aa2 | ||
|
|
a442fe0fd0 | ||
|
|
ab9ae4f643 | ||
|
|
d9a51030ea | ||
|
|
67026718c1 | ||
|
|
a71c471405 | ||
|
|
3858086047 | ||
|
|
dffef3d9f3 | ||
|
|
de1c6b8727 | ||
|
|
6f17f412ba | ||
|
|
746961bf43 | ||
|
|
182a6d99fd | ||
|
|
af31088481 | ||
|
|
43425158f4 | ||
|
|
8524ea7441 | ||
|
|
6a65006f55 | ||
|
|
d75ed841a9 | ||
|
|
3cd2547e91 | ||
|
|
8c661d4e8c | ||
|
|
4b332d831e | ||
|
|
22487810ba | ||
|
|
c49e65d926 | ||
|
|
2e8814bb57 | ||
|
|
dc557c0d4c | ||
|
|
a2beb159f1 | ||
|
|
7ce427ff47 | ||
|
|
a221c672fb | ||
|
|
f87d382ec8 | ||
|
|
3dfc020fac | ||
|
|
2d87896939 | ||
|
|
2192d8d816 | ||
|
|
d206949f62 | ||
|
|
4accc69022 | ||
|
|
898d8d94c8 | ||
|
|
26895da969 | ||
|
|
0c573b3dff | ||
|
|
2cff00dce2 | ||
|
|
06a6ad8bca | ||
|
|
02c6ad0e59 | ||
|
|
9f321cbe13 | ||
|
|
9f30ca2923 | ||
|
|
37f2d8f795 | ||
|
|
400e443a0a | ||
|
|
0a8d2996dc | ||
|
|
cfdb00b971 | ||
|
|
9b9ad39143 | ||
|
|
0541c0dbcf | ||
|
|
47b11cb8b4 | ||
|
|
d3da310b92 | ||
|
|
93e0a6a9de | ||
|
|
ac549a853a | ||
|
|
053af9a1c8 | ||
|
|
60112def02 | ||
|
|
2134c0c8a9 | ||
|
|
3222d62ddf | ||
|
|
aeb957a285 | ||
|
|
154cf22ffa | ||
|
|
51f532697e | ||
|
|
01b88539ba | ||
|
|
3066b206af | ||
|
|
04492bab10 | ||
|
|
8b0d0342d4 | ||
|
|
957c12e7d7 | ||
|
|
3b5ae0f8a2 | ||
|
|
1fc5659842 | ||
|
|
1acd2adce2 | ||
|
|
1bc223d4b3 | ||
|
|
bef405bfa5 | ||
|
|
0612074abe | ||
|
|
3bf51d8362 | ||
|
|
2c8479a7c5 | ||
|
|
8c8b5b313e | ||
|
|
66d55fd893 | ||
|
|
7fa5e6c861 | ||
|
|
00f96aae76 | ||
|
|
a749e6bd16 | ||
|
|
791076d366 | ||
|
|
37f43fbb35 | ||
|
|
401a5fd5ff | ||
|
|
1854922f0c | ||
|
|
2fc7c18747 | ||
|
|
8ef2420677 | ||
|
|
cf6f4d74c4 | ||
|
|
f44d40f6b4 | ||
|
|
1c81a58127 | ||
|
|
9baf7c4874 | ||
|
|
22b089e47e | ||
|
|
b166f18220 | ||
|
|
68600f6ecf | ||
|
|
4d4447779f | ||
|
|
639de4c27b | ||
|
|
d87390934e | ||
|
|
411ec2e557 | ||
|
|
f025602841 | ||
|
|
f958c9daf5 | ||
|
|
b86838c2b0 | ||
|
|
1f7d1f9b15 | ||
|
|
f8fdf9618a | ||
|
|
827a83efbc | ||
|
|
3e88849386 | ||
|
|
608c416207 | ||
|
|
62f6ff9d6c | ||
|
|
37dc273148 | ||
|
|
f7f01d109e | ||
|
|
01ee335521 | ||
|
|
0e0de29b87 | ||
|
|
babf877fd6 | ||
|
|
935272824e | ||
|
|
3a9532c8fd | ||
|
|
c4c92142a6 | ||
|
|
d4b6338102 | ||
|
|
8df7d962e6 | ||
|
|
41e916a511 | ||
|
|
d9c8a9a880 | ||
|
|
ddc7bb9064 | ||
|
|
1d4057c209 | ||
|
|
822b86942c | ||
|
|
1e74dbb937 | ||
|
|
7cef92fffe | ||
|
|
42e4992f06 | ||
|
|
a6066175c6 | ||
|
|
27444d6b1e | ||
|
|
d6a99c0391 | ||
|
|
f787f7e651 | ||
|
|
a7c9c08371 | ||
|
|
fccc93176b | ||
|
|
6439a138fe | ||
|
|
a9a29dff4f | ||
|
|
6a52f8b8dd | ||
|
|
a1049328d6 | ||
|
|
5c2b96bd00 | ||
|
|
c36413fdf6 | ||
|
|
52cf5af91c | ||
|
|
3a4e053af7 | ||
|
|
049bc9ec68 | ||
|
|
b461a555b8 | ||
|
|
0f87b2d1e1 | ||
|
|
0fb5b76c0d | ||
|
|
0c918dd87a | ||
|
|
05299a0fee | ||
|
|
b36b0a91f5 | ||
|
|
6081eac58a | ||
|
|
942ba749c7 | ||
|
|
f941012687 | ||
|
|
fed5e5d5af | ||
|
|
b864885753 | ||
|
|
64747c2324 | ||
|
|
34965edcda | ||
|
|
bd4377084d | ||
|
|
38a2076b89 | ||
|
|
5759d50d4a | ||
|
|
e455836cc9 | ||
|
|
8a90f26c8a | ||
|
|
24e1fabf2e | ||
|
|
c39c039e15 | ||
|
|
07f176f426 | ||
|
|
19339e3a6d | ||
|
|
3e1d6a7bcf | ||
|
|
2bbc12063c | ||
|
|
b8737b724b | ||
|
|
d91c3a2f5e | ||
|
|
fe5db5aadc | ||
|
|
cf9c957c66 | ||
|
|
68b60c6d19 | ||
|
|
3a644b16a4 | ||
|
|
95b34de339 | ||
|
|
6a431cbf49 | ||
|
|
56fb2f00b3 | ||
|
|
1c86aaf342 | ||
|
|
cfc0b18eaa | ||
|
|
412c211655 | ||
|
|
923feb69ab | ||
|
|
92dba7035a | ||
|
|
b8a3ba16a2 | ||
|
|
cd5e4d9402 | ||
|
|
f074709fc9 | ||
|
|
e0b29e437b | ||
|
|
bdb94fba7d | ||
|
|
2f364c62f4 | ||
|
|
7ed9f83662 | ||
|
|
f498a9b3fb | ||
|
|
13330738b8 | ||
|
|
e53535cc61 | ||
|
|
c62fc5e75c | ||
|
|
70245ad98c | ||
|
|
6d235bceee | ||
|
|
4adebfc856 | ||
|
|
faccc0a410 | ||
|
|
9078688baf | ||
|
|
9bd8b1d25f | ||
|
|
dd4be1da38 | ||
|
|
66f86e1870 | ||
|
|
4ab75b68dc | ||
|
|
73cb70dbb3 | ||
|
|
d082cccb6d | ||
|
|
88a80e3c2c | ||
|
|
24516bcf4d | ||
|
|
b4c4a642ed | ||
|
|
0231617857 | ||
|
|
7f64fba80f | ||
|
|
633aec38f5 | ||
|
|
d1b402a23c | ||
|
|
35a9aff8e1 | ||
|
|
988c9bd9be | ||
|
|
095f31b316 | ||
|
|
d86cee2a69 | ||
|
|
e986f20a85 | ||
|
|
c727ba1d99 | ||
|
|
bb70923cd8 | ||
|
|
772fa42dcb | ||
|
|
85ef3263fc | ||
|
|
4bde8de63f | ||
|
|
654a7df9b0 | ||
|
|
c3aa836ec0 | ||
|
|
95764bef6f | ||
|
|
63dbf48546 | ||
|
|
e2401350a3 | ||
|
|
e867355b2a | ||
|
|
b28c14b93a | ||
|
|
879ead210f | ||
|
|
2f6d23b91e | ||
|
|
5f63a7b587 | ||
|
|
d9ce797d88 | ||
|
|
12230f8043 | ||
|
|
0c8de1ca44 | ||
|
|
89687105f4 | ||
|
|
74d1694be9 | ||
|
|
935e986be5 | ||
|
|
e5ac2ebd7c | ||
|
|
8d6e13bf94 | ||
|
|
2ca704405a | ||
|
|
802c1c2937 | ||
|
|
3cb5fef6b6 | ||
|
|
6da2e0aa1e | ||
|
|
24f3ec7f33 | ||
|
|
a57b375b41 | ||
|
|
6cc9d53978 | ||
|
|
df32c05833 | ||
|
|
c0652adf4c | ||
|
|
6ea760a336 | ||
|
|
f704b94603 | ||
|
|
444a67cafa | ||
|
|
f91cbd5688 | ||
|
|
3073ca3e5a | ||
|
|
b47ab633e2 | ||
|
|
09a2ab39fe | ||
|
|
6cf54833f7 | ||
|
|
2ccdf21a1f | ||
|
|
cf8afc527e | ||
|
|
1d6f05f974 | ||
|
|
85751966e9 | ||
|
|
a7bc9d5351 | ||
|
|
42c006d07c | ||
|
|
1b9ca314b8 | ||
|
|
e72a360337 | ||
|
|
45108ddd53 | ||
|
|
e3401a0645 | ||
|
|
26b9100709 | ||
|
|
a568120e42 | ||
|
|
e57182c658 | ||
|
|
6354dbbbdf | ||
|
|
2b3e740569 | ||
|
|
40d934e378 | ||
|
|
e95d82748f | ||
|
|
30bd0b53db | ||
|
|
1893eca41a | ||
|
|
82067463b8 | ||
|
|
ce9c51d399 | ||
|
|
96176476f3 | ||
|
|
68c84264af | ||
|
|
69438a55ca | ||
|
|
8695b5e319 | ||
|
|
95970164ad | ||
|
|
f6c6e59a50 | ||
|
|
45143f9541 | ||
|
|
23244bb410 | ||
|
|
edb647667e | ||
|
|
8d3a302a17 | ||
|
|
1d2d32c847 | ||
|
|
d635b3fd3c | ||
|
|
0f281ef894 | ||
|
|
b18db4733c | ||
|
|
6e08fe337c | ||
|
|
2a2c0a0957 | ||
|
|
4230b6f3c9 | ||
|
|
aa171b45cb | ||
|
|
661d06c90a | ||
|
|
a9aa263d3a | ||
|
|
6208fc9cfd | ||
|
|
e1dd798482 | ||
|
|
c8a3f6f06a | ||
|
|
3b9984379c | ||
|
|
a1b60b1d42 | ||
|
|
b5850ebd4c | ||
|
|
ac0a62e494 | ||
|
|
54b4b0c56f | ||
|
|
033afde3b5 | ||
|
|
a07944a5bb | ||
|
|
32010055e1 | ||
|
|
971ea2217c | ||
|
|
d513a210c6 | ||
|
|
a1db64e7b1 | ||
|
|
0b9c4e1e74 | ||
|
|
248320fa55 | ||
|
|
d4e26707c7 | ||
|
|
99ea1056ac | ||
|
|
7bcf4effa5 | ||
|
|
e1df876b61 | ||
|
|
28ffb9638d | ||
|
|
1c20255504 | ||
|
|
1fd884b34f | ||
|
|
701687faab | ||
|
|
bbc3055feb | ||
|
|
95c69083c7 | ||
|
|
57a37b5832 | ||
|
|
d29ae1c462 | ||
|
|
df468fc482 | ||
|
|
31278bcc68 | ||
|
|
e7e86b68f4 | ||
|
|
a89d8995c3 | ||
|
|
dbc854d5f4 | ||
|
|
f1cd0e2daf | ||
|
|
90d32bd756 | ||
|
|
e99731ea85 | ||
|
|
15659ac6e6 | ||
|
|
3ef41845a9 | ||
|
|
c84e681581 | ||
|
|
c3cf3427b1 | ||
|
|
2c4f71d85b | ||
|
|
c6328affae | ||
|
|
aaef18295d | ||
|
|
14f0d2035e | ||
|
|
64afff6b9a | ||
|
|
6bddffbca4 | ||
|
|
81a88693c1 | ||
|
|
68541e66b7 | ||
|
|
672b593634 | ||
|
|
5769d3867d | ||
|
|
724ffa3756 | ||
|
|
5694b5ed30 | ||
|
|
a1184ceb4e | ||
|
|
02203c7739 | ||
|
|
4d709e0dd2 | ||
|
|
ae04f56dbd | ||
|
|
f80ff8c917 | ||
|
|
b4ce89bbf5 | ||
|
|
486b87d821 | ||
|
|
b3010a4624 | ||
|
|
7d53051ec8 | ||
|
|
ed893c5f47 | ||
|
|
a4eb3323da | ||
|
|
1da065e50e | ||
|
|
86bc9d506f | ||
|
|
eee45a9578 | ||
|
|
659f49a09a | ||
|
|
8fa9e85980 | ||
|
|
92a75c9563 | ||
|
|
7c7a30c472 | ||
|
|
ea271cd4e2 | ||
|
|
6a38d07a4c | ||
|
|
c4e5ee63bb | ||
|
|
862da2c0b1 | ||
|
|
545370d2b3 | ||
|
|
59220c63a6 | ||
|
|
86306dd45a | ||
|
|
98d2bfa0db | ||
|
|
aec48f159b | ||
|
|
ad7e433a7d | ||
|
|
5a60aa5050 | ||
|
|
ebea470875 | ||
|
|
d980e00961 | ||
|
|
987799f8fb | ||
|
|
d2f3604c1d | ||
|
|
72cc558fdc | ||
|
|
6bc3fe6e67 | ||
|
|
9398878048 | ||
|
|
ca19762e58 | ||
|
|
8764be07e2 | ||
|
|
2022a3ad96 | ||
|
|
65d9d416b4 | ||
|
|
fa2f9f1f21 | ||
|
|
c656cfbdce | ||
|
|
de829c0938 | ||
|
|
64443221aa | ||
|
|
9017e29741 | ||
|
|
0a22142d88 | ||
|
|
ac160f98a8 | ||
|
|
62e01a2a62 | ||
|
|
5660cebaf6 | ||
|
|
a7e588ceac | ||
|
|
5baf1c5536 | ||
|
|
9a2d9ad947 | ||
|
|
90b0cd44ac | ||
|
|
698e8008df | ||
|
|
1de4cc3ba8 | ||
|
|
0d66ad23c6 | ||
|
|
7f7741099b | ||
|
|
5a72dc6922 | ||
|
|
80ed02e72e | ||
|
|
8fb31e1b4d | ||
|
|
148f21415a | ||
|
|
1c31e07d34 | ||
|
|
55d566b72f | ||
|
|
60336c7423 | ||
|
|
7ae877bd3a | ||
|
|
c601fc6437 | ||
|
|
e5fec408c4 | ||
|
|
8156e9894e | ||
|
|
cacc212f12 | ||
|
|
d0f2c00f9f | ||
|
|
766427de0c | ||
|
|
a7b75c99a5 | ||
|
|
bae10a6582 | ||
|
|
c4cf90a3d2 | ||
|
|
15c49a3e08 | ||
|
|
ae87f6548a | ||
|
|
7833fa7396 | ||
|
|
9278f3acd2 | ||
|
|
e83ae34a3b | ||
|
|
e13bafc1ab | ||
|
|
0ea66329b8 | ||
|
|
634670e3ea | ||
|
|
dea60b11bc | ||
|
|
5e90f0a57b | ||
|
|
0b4542fcdf | ||
|
|
02bd2d2adf | ||
|
|
dce6fe6f2d | ||
|
|
fcae99f09b | ||
|
|
fb1b026d3d | ||
|
|
9f953fc944 | ||
|
|
909ea1a698 | ||
|
|
7231acd442 | ||
|
|
7814371a9a | ||
|
|
6166e2dd80 | ||
|
|
ee0c8a2635 | ||
|
|
2bebddefc0 | ||
|
|
fdbf3d3fec | ||
|
|
f9136cffe6 | ||
|
|
51d84b1869 | ||
|
|
13e040baee | ||
|
|
cc0d5539ba | ||
|
|
b53f61fc59 | ||
|
|
4e0e03403e | ||
|
|
928fccc15b | ||
|
|
bbaa3ab8bd | ||
|
|
5e3cb3a4ea | ||
|
|
f71ea5f3ea | ||
|
|
f469c25730 | ||
|
|
18469b6954 | ||
|
|
d01db4862b | ||
|
|
8b2adba8d6 | ||
|
|
d459e9abce | ||
|
|
c9abe1b1ff | ||
|
|
a0e6147bb5 | ||
|
|
b0f491d3c3 | ||
|
|
392da53f53 | ||
|
|
ae72b0fb70 | ||
|
|
a79d080ea8 | ||
|
|
ec85fd552d | ||
|
|
11db046fc7 | ||
|
|
938151a834 | ||
|
|
14e3b84073 | ||
|
|
56100f0fa7 | ||
|
|
5254ee2e2a | ||
|
|
355d004895 | ||
|
|
a336494f5d | ||
|
|
8270f7f0ca | ||
|
|
638a956a9e | ||
|
|
d395ebd28f | ||
|
|
c0d3faa84f | ||
|
|
3492c8b780 | ||
|
|
a8b2c257cd | ||
|
|
5e8d8dab82 | ||
|
|
b504c6eb39 | ||
|
|
d261c36cde | ||
|
|
fe4e452d68 | ||
|
|
d54a4fa223 | ||
|
|
45bd323cab | ||
|
|
8677dbded1 | ||
|
|
794ad5785d | ||
|
|
fa5b58968e | ||
|
|
e720f56ea8 | ||
|
|
7db53e6459 | ||
|
|
e287bd7f04 | ||
|
|
022435a90a | ||
|
|
6c99cc1700 | ||
|
|
fe5b190a7d | ||
|
|
77bab51696 | ||
|
|
77048f3e3b | ||
|
|
8b618f7439 | ||
|
|
8973207bb4 | ||
|
|
6ad1736832 | ||
|
|
9fca611c4a | ||
|
|
8e7164553f | ||
|
|
3b52811796 | ||
|
|
2e84b1db64 | ||
|
|
9f33068ab3 | ||
|
|
eaa3c67a5e | ||
|
|
1b9b1d15bc | ||
|
|
97f433a274 | ||
|
|
45a3655eaf | ||
|
|
81ffde92fb | ||
|
|
0be4cead20 | ||
|
|
f6dd32046e | ||
|
|
443a80f254 | ||
|
|
8017635a71 | ||
|
|
98f62b191a | ||
|
|
52771a6226 | ||
|
|
b00bcf506e | ||
|
|
fdbfe36c0b | ||
|
|
446e822723 | ||
|
|
b68e59a24b | ||
|
|
4e0e492427 | ||
|
|
8f99f8fcc6 | ||
|
|
3cdf71801e | ||
|
|
801cf9ac62 | ||
|
|
34946b72a5 | ||
|
|
1592bedbe8 | ||
|
|
15099eb13b | ||
|
|
c511b45ff6 | ||
|
|
40761b11b1 | ||
|
|
cca543d0cd | ||
|
|
34e5e2dd82 | ||
|
|
2b7c3df66b | ||
|
|
f766531e74 | ||
|
|
7f59b42b05 | ||
|
|
f41de932d6 | ||
|
|
b4a05ff27e | ||
|
|
3b91467941 | ||
|
|
26d2af5ee8 | ||
|
|
2b61dc6557 | ||
|
|
0b770cd48a | ||
|
|
c14aa99ef6 | ||
|
|
8f371ee81c | ||
|
|
3b63b39810 | ||
|
|
0cd238700c | ||
|
|
14fbe06d9e | ||
|
|
64949bf467 | ||
|
|
732f133940 | ||
|
|
5dc4df9570 | ||
|
|
7dde8dbbd9 | ||
|
|
01405ad92e | ||
|
|
683abb86ef | ||
|
|
207aa07891 | ||
|
|
26a141c6a6 | ||
|
|
dc64568c83 | ||
|
|
f4a595eedd | ||
|
|
5a17a6323a | ||
|
|
2b8e445321 | ||
|
|
315499b1d4 | ||
|
|
65a2bdb01d | ||
|
|
ed8202efc6 | ||
|
|
0937bd6c16 | ||
|
|
3d26b5336c | ||
|
|
c8f208b96b | ||
|
|
2e339e49b8 | ||
|
|
5d9107fd15 | ||
|
|
4b7c571575 | ||
|
|
5502b68a1d | ||
|
|
5794fd42df | ||
|
|
9c6e46ab15 | ||
|
|
09d0ac0347 | ||
|
|
22ae7adac8 | ||
|
|
36924d0b1c | ||
|
|
6ed9de9051 | ||
|
|
857619995e | ||
|
|
9310ae28ab | ||
|
|
27e26bd1ea | ||
|
|
305ec3b3ce | ||
|
|
f4fe93338b | ||
|
|
3b84c80d56 | ||
|
|
5e120e7ab5 | ||
|
|
a4cf5510e3 | ||
|
|
edb5ab5622 | ||
|
|
06b4f75680 | ||
|
|
318edc8c35 | ||
|
|
651a8f8cc2 | ||
|
|
9f64a00549 | ||
|
|
a88bf87e2a | ||
|
|
e82eb27787 | ||
|
|
3f0e6a5806 | ||
|
|
917b1759b0 | ||
|
|
16ca9c688b | ||
|
|
7b6a27cb5e | ||
|
|
869a234938 | ||
|
|
537d07c1e5 | ||
|
|
d091a2c4bb | ||
|
|
d2f95d69fb | ||
|
|
1169cc8653 | ||
|
|
f66d94c6b0 | ||
|
|
2fe1e28220 | ||
|
|
288131ac5a | ||
|
|
3610acec5a | ||
|
|
cc67d2e1cf | ||
|
|
f77ed0fb07 | ||
|
|
a30908c66a | ||
|
|
f9225f98e7 | ||
|
|
2db2feea37 | ||
|
|
d1d59272a2 | ||
|
|
d08542ce5d | ||
|
|
b8904a8c3e | ||
|
|
48ab87294b | ||
|
|
3e1e75fe08 | ||
|
|
120cc0aadd | ||
|
|
853012ceef | ||
|
|
2add45fe2f | ||
|
|
b882de87ab | ||
|
|
2d68cb8639 | ||
|
|
3a9d1df026 | ||
|
|
5c25984ea0 | ||
|
|
319d6ced80 | ||
|
|
51a19a2804 | ||
|
|
a7cb1a78df | ||
|
|
d4daece76b | ||
|
|
3ec83babac | ||
|
|
91fc6c984b | ||
|
|
a4f3d09704 | ||
|
|
40180c18ac | ||
|
|
82bea6758a | ||
|
|
348731fc3b | ||
|
|
797f42ecc6 | ||
|
|
8385a55bda | ||
|
|
8406cedf2d | ||
|
|
f22b83db6c | ||
|
|
1481304d3b | ||
|
|
2cec5c0f30 | ||
|
|
4760bb7743 | ||
|
|
c1adf0cd3d | ||
|
|
622e69ff54 | ||
|
|
68503d32df | ||
|
|
57319f8c58 | ||
|
|
dd4d465305 | ||
|
|
467a22dd36 | ||
|
|
50292adacb | ||
|
|
84a7499ae3 | ||
|
|
39d7177bd3 | ||
|
|
1c65139888 | ||
|
|
8a4db3c004 | ||
|
|
cef93f700b | ||
|
|
0a3d3460b1 | ||
|
|
d988f3fa50 | ||
|
|
e865144ace | ||
|
|
2a167aa030 | ||
|
|
9cfecf7f0b | ||
|
|
f9d6b83f5e | ||
|
|
ce7d4a1c53 | ||
|
|
9bba6bd172 | ||
|
|
4ad92e3a0b | ||
|
|
c4bf820dc3 | ||
|
|
39f43587d0 | ||
|
|
fdaa4e9b18 | ||
|
|
91876e98cd | ||
|
|
eb8fef0031 | ||
|
|
87447ddd6d | ||
|
|
9d138173be | ||
|
|
eae53576bd | ||
|
|
f8c49effd4 | ||
|
|
618706a5f5 | ||
|
|
9ffcd26d50 | ||
|
|
b431e227da | ||
|
|
d94dfe0876 | ||
|
|
6130026786 | ||
|
|
a723977b9f | ||
|
|
3dddbfd8fa | ||
|
|
e70a2a5817 | ||
|
|
d80a41bb6d | ||
|
|
2bebd5cdb4 | ||
|
|
7bb75b0213 | ||
|
|
bc2e82efc1 | ||
|
|
c04e8de9b0 | ||
|
|
4977174def | ||
|
|
5eef0acea1 | ||
|
|
3935aa84d8 | ||
|
|
dd6138a655 | ||
|
|
68c5bea3f8 | ||
|
|
0f474d541d | ||
|
|
c4d59aeec4 | ||
|
|
b2c423d1ff | ||
|
|
49c752b1f7 | ||
|
|
daa79a6df2 | ||
|
|
48e0c1e721 | ||
|
|
12d81e212f | ||
|
|
c22e729d9c | ||
|
|
2b8a1c0d70 | ||
|
|
e4b56b9702 | ||
|
|
789a474b28 | ||
|
|
fb2959c514 | ||
|
|
62a28468a7 | ||
|
|
23dba99eda | ||
|
|
5f62d224b0 | ||
|
|
6728870071 | ||
|
|
87c71a3ea6 | ||
|
|
06ab399497 | ||
|
|
f7b52d2541 | ||
|
|
c111af0ed2 | ||
|
|
07e2bd673e | ||
|
|
e4ce64d10b | ||
|
|
5f3326a888 | ||
|
|
1304428003 | ||
|
|
55828f389a | ||
|
|
7e2c18a1f6 | ||
|
|
79c147ed78 | ||
|
|
d4b41c5e03 | ||
|
|
b15a0e9650 | ||
|
|
fe09559ee9 | ||
|
|
94e8e6419f | ||
|
|
4d2d18649c | ||
|
|
c1aa5c5f33 | ||
|
|
4a1752d3fc | ||
|
|
b9b1eeffce | ||
|
|
5667667d1f | ||
|
|
f5b034095a | ||
|
|
95e5beb34e | ||
|
|
e808151c28 | ||
|
|
d760b790b3 | ||
|
|
1b5599972a | ||
|
|
6c2ce28d0d | ||
|
|
ff09c275d4 | ||
|
|
93dcd932e8 | ||
|
|
e6a0de4094 | ||
|
|
9f39671e65 | ||
|
|
423317b82a | ||
|
|
47201c2c4d | ||
|
|
53d5d9d162 | ||
|
|
9cb0cdb4ac | ||
|
|
448132c46c | ||
|
|
1476fc7f3b | ||
|
|
71a7b3a26f | ||
|
|
a47c06cb61 | ||
|
|
48e16edb47 | ||
|
|
c35d98dc42 | ||
|
|
8bead4ae34 | ||
|
|
1b6cb3532d | ||
|
|
0a0955755a | ||
|
|
a3101120fd | ||
|
|
30f9651f99 | ||
|
|
4dcc0f10b8 | ||
|
|
3d39ab5ded | ||
|
|
c3a198d0c7 | ||
|
|
be5c17612a | ||
|
|
fe89ac8a89 | ||
|
|
4c3ae847b6 | ||
|
|
5c0dc79ffa | ||
|
|
0a83705d21 | ||
|
|
ea22292d2c | ||
|
|
1990f3c992 | ||
|
|
c0b432f7b4 | ||
|
|
ae3180f919 | ||
|
|
62acb9adc4 | ||
|
|
0b5fa56444 | ||
|
|
789f26b1a5 | ||
|
|
a3068a33d5 | ||
|
|
b8c4b35415 | ||
|
|
209a6d36ad | ||
|
|
5c491d573a | ||
|
|
2c86e728b5 | ||
|
|
cd847affb7 | ||
|
|
7a2bc2cada | ||
|
|
9dbf6b02d2 | ||
|
|
1db68a3976 | ||
|
|
1c31352675 | ||
|
|
6c3489087c | ||
|
|
313578a1a0 | ||
|
|
bd7331ecf5 | ||
|
|
e293cd4d08 | ||
|
|
ca4bdfb4bd | ||
|
|
4f40314433 | ||
|
|
f670f4f076 | ||
|
|
6e86fee588 | ||
|
|
2d9b38b93e | ||
|
|
b8a9861f95 | ||
|
|
188c90bf25 | ||
|
|
8b02ae650c | ||
|
|
b7bb100810 | ||
|
|
aa05bf5206 | ||
|
|
d303c5b3eb | ||
|
|
f401c42f9c | ||
|
|
efec9acd6f | ||
|
|
3ed86445e1 | ||
|
|
23f27f3ce5 | ||
|
|
f99f66570b | ||
|
|
3e129ac68c | ||
|
|
8a0ab20a70 | ||
|
|
b277f5ae6f | ||
|
|
6109a0fe44 | ||
|
|
383f908cf7 | ||
|
|
3e6c950e12 | ||
|
|
ee2ee02599 | ||
|
|
b42dcdb7a7 | ||
|
|
82156d34cc | ||
|
|
4a5142c60b | ||
|
|
ea25e9674f | ||
|
|
dee0909d2b | ||
|
|
8e5ecf6b38 | ||
|
|
7557737569 | ||
|
|
53bce0581e | ||
|
|
f9f9b671c5 | ||
|
|
606d33e77e | ||
|
|
d2f7acbc69 | ||
|
|
0dd024a09f | ||
|
|
0a6cb62169 | ||
|
|
9930a1d4d9 | ||
|
|
40d0a6347c | ||
|
|
baad26a0fd | ||
|
|
f3177305d5 | ||
|
|
4ceb520c1d | ||
|
|
d761ea5158 | ||
|
|
7ba93d9f83 | ||
|
|
b34f93f307 | ||
|
|
ec040d82dd | ||
|
|
00190677d4 | ||
|
|
d38f7a5eb5 | ||
|
|
ee433ef6e9 | ||
|
|
d89c9e94ba | ||
|
|
7e2dfef930 | ||
|
|
0296fcb5cd | ||
|
|
80819f3c44 | ||
|
|
7571baadb4 | ||
|
|
da03a66e69 | ||
|
|
3c47b7fa5f | ||
|
|
ba9365c438 | ||
|
|
db37e67575 | ||
|
|
76a3ef8c37 | ||
|
|
6fd6fff3a6 | ||
|
|
d1387bf512 | ||
|
|
4c923a2d19 | ||
|
|
4ee85f11e8 | ||
|
|
829c7f909c | ||
|
|
990fa00660 | ||
|
|
77592825f0 | ||
|
|
ce53b9b2a5 | ||
|
|
175fe158ed | ||
|
|
80efafcceb | ||
|
|
b241409e4b | ||
|
|
11967be017 | ||
|
|
6ee811ea03 | ||
|
|
d5e7303a25 | ||
|
|
2924fd3e23 | ||
|
|
75b44aac13 | ||
|
|
86c73105ee | ||
|
|
2d00abc7cb | ||
|
|
1e07b3b1c2 | ||
|
|
4313c1c25c | ||
|
|
cc9938d4c9 | ||
|
|
a54784cd53 | ||
|
|
22989b0488 | ||
|
|
892aa1e78b | ||
|
|
b9ab7d2413 | ||
|
|
69b2a0a733 | ||
|
|
13cd4ed546 | ||
|
|
7261d3afcd | ||
|
|
84fc73ad9c | ||
|
|
4103f5c3cc | ||
|
|
5390616694 | ||
|
|
daf08f801f | ||
|
|
4e2a1fe5c8 | ||
|
|
03f155484c | ||
|
|
89298a8d23 | ||
|
|
3b14c5230c | ||
|
|
91401514ab | ||
|
|
91d986b6c0 | ||
|
|
4d72bd098a | ||
|
|
502973ff75 | ||
|
|
3e91c189ae | ||
|
|
b0f80b686c | ||
|
|
b824928b0b | ||
|
|
ccca34f9f7 | ||
|
|
b5350b24ff | ||
|
|
56ace10a37 | ||
|
|
72ec0a3408 | ||
|
|
05118cc440 | ||
|
|
e392da20e8 | ||
|
|
6e69339f6b | ||
|
|
30cdc06bcd | ||
|
|
9ce43d46f6 | ||
|
|
de09656197 | ||
|
|
3827a1b09e | ||
|
|
61ba8d5a11 | ||
|
|
4a3a5ee70d | ||
|
|
f58a53a001 | ||
|
|
65c1b53275 | ||
|
|
0b43f988c7 | ||
|
|
f8e357fa19 | ||
|
|
c3a4e4cd23 | ||
|
|
9dac12cb32 | ||
|
|
d76a3646b7 | ||
|
|
d7c734acd6 | ||
|
|
ed13fc8618 | ||
|
|
edcd7c6aa6 | ||
|
|
b0fdd6db99 | ||
|
|
edf27f47f2 | ||
|
|
3b218b77eb | ||
|
|
1e02471940 | ||
|
|
1b9dadb3d3 | ||
|
|
c3827dea10 | ||
|
|
6a1b916598 | ||
|
|
a2c7b001d5 | ||
|
|
3c6e938bb1 | ||
|
|
5a0afc5fea | ||
|
|
f37be006c3 | ||
|
|
459c332351 | ||
|
|
153a87d84a | ||
|
|
05da892cd2 | ||
|
|
f6b1a6278f | ||
|
|
db58182483 | ||
|
|
6e9f0882da | ||
|
|
7ed18579dc | ||
|
|
f250fc8f86 | ||
|
|
6eea9603c2 | ||
|
|
20915529b7 | ||
|
|
b3efccca81 | ||
|
|
809d465de5 | ||
|
|
7d15071c63 | ||
|
|
89eb1575e7 | ||
|
|
5d6ed935a4 | ||
|
|
0528435386 | ||
|
|
fe22213b51 | ||
|
|
aab42eaaba | ||
|
|
16031b0d54 | ||
|
|
ded184daaf | ||
|
|
ecf90bd25b | ||
|
|
d82e38adc1 | ||
|
|
af677e7e35 | ||
|
|
6ad38bdad3 | ||
|
|
8b80136a87 | ||
|
|
97de919152 | ||
|
|
0eafa725b9 | ||
|
|
fa212efe5f | ||
|
|
a9056ce90c | ||
|
|
16682a3f92 | ||
|
|
02c01c81a0 | ||
|
|
22d3929ae3 | ||
|
|
ab9fbf1967 | ||
|
|
608ec2b806 | ||
|
|
e5ae4f0ef6 | ||
|
|
67ba87d390 | ||
|
|
77d45cb173 | ||
|
|
d83febea46 | ||
|
|
546a315884 | ||
|
|
af616457e3 | ||
|
|
1a100a2919 | ||
|
|
a85bb93b69 | ||
|
|
057eda060c | ||
|
|
48f9ee6763 | ||
|
|
52b74abb99 | ||
|
|
ec4b8a59fa | ||
|
|
cf8dbf8047 | ||
|
|
995d380200 | ||
|
|
ae86cdf09a | ||
|
|
2b346659a0 | ||
|
|
49081711a9 | ||
|
|
e7439ce193 | ||
|
|
b8e438b6be | ||
|
|
678e950b6d | ||
|
|
9ea651f1cd | ||
|
|
bd98a08b89 | ||
|
|
f02bb4fdac | ||
|
|
0a8352a5cd | ||
|
|
737423995d | ||
|
|
2916bf7ee4 | ||
|
|
fa54c5d9b0 | ||
|
|
693b6651b4 | ||
|
|
5c71ecb267 | ||
|
|
1ba50eba98 | ||
|
|
2c8a256b13 | ||
|
|
f4c5aa03d7 | ||
|
|
c6acb2a639 | ||
|
|
2296013174 | ||
|
|
8a3e8c2d81 | ||
|
|
ae84d8c7a4 | ||
|
|
dbd627c38a | ||
|
|
d172c3ce03 | ||
|
|
9904f5354e | ||
|
|
f345bf7983 | ||
|
|
875f9b6534 | ||
|
|
871dfb709d | ||
|
|
19e24bd644 | ||
|
|
457a240457 | ||
|
|
bbf4567dd8 | ||
|
|
27d3b52843 | ||
|
|
dcb4694ec1 | ||
|
|
2fb8ae010f | ||
|
|
65ae6cabb5 | ||
|
|
86a66da04d | ||
|
|
d66b02b0cd | ||
|
|
b3182c3304 | ||
|
|
2dbca00bfb | ||
|
|
b22fd6de6d | ||
|
|
245ee42763 | ||
|
|
98bef4600c | ||
|
|
f5d53b94fe | ||
|
|
00c8a68430 | ||
|
|
c1be834ff9 | ||
|
|
2c0dc2f3b1 | ||
|
|
1c94fef720 | ||
|
|
b711d76b8e | ||
|
|
4396ab7548 | ||
|
|
2b8c2b9f2a | ||
|
|
426284c87e | ||
|
|
089691faaf | ||
|
|
301290663d | ||
|
|
1155da7e1c | ||
|
|
eca0a99fb4 | ||
|
|
96215c4619 | ||
|
|
b2d2be55ef | ||
|
|
7280e8ebc2 | ||
|
|
c7e86ad4f1 | ||
|
|
f2b2c022be | ||
|
|
7747daa9ec | ||
|
|
c2943e7681 | ||
|
|
d5fc03d867 | ||
|
|
b0eca20dc2 | ||
|
|
aad335475c | ||
|
|
c3676bf986 | ||
|
|
6fb4b6d097 | ||
|
|
6aa168833b | ||
|
|
0d83cae2ec | ||
|
|
773d9976a0 | ||
|
|
3723829b0a | ||
|
|
13cb198b5c | ||
|
|
79f645aa6c | ||
|
|
42d479d071 | ||
|
|
d7f50b1e41 | ||
|
|
39eb85596c | ||
|
|
bff7e9edf5 | ||
|
|
98ccc03a21 | ||
|
|
3b668ed448 | ||
|
|
33b28be941 | ||
|
|
76fe23b928 | ||
|
|
622c54f4a3 | ||
|
|
e09993f919 | ||
|
|
7ee6fd1f6d | ||
|
|
2dca6f0cb2 | ||
|
|
159dd7f069 | ||
|
|
b30f21e074 | ||
|
|
636c86cf6f | ||
|
|
5483e41b2a | ||
|
|
1c89994c94 | ||
|
|
e1bc4b983e | ||
|
|
cb3645ea95 | ||
|
|
04ebaddf5e | ||
|
|
45e1f1ae57 | ||
|
|
c1d5f7cef7 | ||
|
|
df663c4e41 | ||
|
|
d3742782f3 | ||
|
|
faff17b2a9 | ||
|
|
9a3cddc92e | ||
|
|
bd2763d863 | ||
|
|
b2bb22d883 | ||
|
|
ad8ec7f387 | ||
|
|
cf0ca8578c | ||
|
|
07aee79bd8 | ||
|
|
344b57fe33 | ||
|
|
18a2fbf54a | ||
|
|
39af56cf8f | ||
|
|
2d3a0a1034 | ||
|
|
655fa5d9aa | ||
|
|
9a49a29c7f | ||
|
|
89ae45cda4 | ||
|
|
f660ad35b2 | ||
|
|
c61738ae43 | ||
|
|
c4dec4d34b | ||
|
|
a797604255 | ||
|
|
25840d3bc7 | ||
|
|
4745d50931 | ||
|
|
04bf3abe99 | ||
|
|
57f7963eee | ||
|
|
2fa21e5dd6 | ||
|
|
9c4c37aa36 | ||
|
|
2540c9062f | ||
|
|
f28274109f | ||
|
|
724724bd8c | ||
|
|
64541cb5f8 | ||
|
|
179b00ed6c | ||
|
|
a9fd496691 | ||
|
|
b14c57e656 | ||
|
|
fa5617e076 | ||
|
|
e52a1d5fad |
22
.github/ISSUE_TEMPLATE.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
<!-- 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
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,2 +1,14 @@
|
||||
bin/fzf
|
||||
bin/fzf.exe
|
||||
dist
|
||||
target
|
||||
pkg
|
||||
Gemfile.lock
|
||||
.DS_Store
|
||||
doc/tags
|
||||
vendor
|
||||
gopath
|
||||
*.zwc
|
||||
fzf
|
||||
tmp
|
||||
*.patch
|
||||
|
||||
11
.gon.hcl
Normal file
11
.gon.hcl
Normal file
@@ -0,0 +1,11 @@
|
||||
source = ["./dist/fzf-macos_darwin_amd64/fzf"]
|
||||
bundle_id = "kr.junegunn.fzf"
|
||||
|
||||
apple_id {
|
||||
username = "junegunn.c@gmail.com"
|
||||
password = "@env:AC_PASSWORD"
|
||||
}
|
||||
|
||||
sign {
|
||||
application_identity = "Apple Development: junegunn.c@gmail.com"
|
||||
}
|
||||
69
.goreleaser.yml
Normal file
69
.goreleaser.yml
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
project_name: fzf
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- go mod download
|
||||
|
||||
builds:
|
||||
- id: fzf-macos
|
||||
binary: fzf
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
ldflags:
|
||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||
hooks:
|
||||
post: gon .gon.hcl
|
||||
|
||||
- goos:
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
- openbsd
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
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
|
||||
|
||||
archives:
|
||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
files:
|
||||
- non-existent*
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: junegunn
|
||||
name: fzf
|
||||
prerelease: auto
|
||||
name_template: '{{ .Tag }}'
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-devel"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- README
|
||||
- test
|
||||
28
.rubocop.yml
Normal file
28
.rubocop.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
Layout/LineLength:
|
||||
Enabled: false
|
||||
Metrics:
|
||||
Enabled: false
|
||||
Lint/ShadowingOuterLocalVariable:
|
||||
Enabled: false
|
||||
Style/MethodCallWithArgsParentheses:
|
||||
Enabled: true
|
||||
IgnoredMethods:
|
||||
- assert
|
||||
- exit
|
||||
- paste
|
||||
- puts
|
||||
- raise
|
||||
- refute
|
||||
- require
|
||||
- send_keys
|
||||
IgnoredPatterns:
|
||||
- ^assert_
|
||||
- ^refute_
|
||||
Style/NumericPredicate:
|
||||
Enabled: false
|
||||
Style/StringConcatenation:
|
||||
Enabled: false
|
||||
Style/OptionalBooleanParameter:
|
||||
Enabled: false
|
||||
Style/WordArray:
|
||||
MinSize: 1
|
||||
28
.travis.yml
Normal file
28
.travis.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
language: go
|
||||
go:
|
||||
- "1.14"
|
||||
env: GO111MODULE=on
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
dist: bionic
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- fish
|
||||
- zsh
|
||||
sources:
|
||||
sourceline: ppa:fish-shell/release-3
|
||||
homebrew:
|
||||
packages:
|
||||
- fish
|
||||
- tmux
|
||||
update: true
|
||||
install: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
|
||||
script:
|
||||
- make test
|
||||
# LC_ALL=C to avoid escape codes in
|
||||
# printf %q $'\355\205\214\354\212\244\355\212\270' on macOS. Bash on
|
||||
# macOS is built without HANDLE_MULTIBYTE?
|
||||
- make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose
|
||||
- rubocop --require rubocop-minitest --require rubocop-performance
|
||||
51
BUILD.md
Normal file
51
BUILD.md
Normal file
@@ -0,0 +1,51 @@
|
||||
Building fzf
|
||||
============
|
||||
|
||||
Build instructions
|
||||
------------------
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.11 or above
|
||||
|
||||
### Using Makefile
|
||||
|
||||
```sh
|
||||
# Build fzf binary for your platform in target
|
||||
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
|
||||
|
||||
# Make release archives for all supported platforms in target
|
||||
make release-all
|
||||
```
|
||||
|
||||
### Using `go get`
|
||||
|
||||
Alternatively, you can build fzf directly with `go get` command without
|
||||
manually cloning the repository.
|
||||
|
||||
```sh
|
||||
go get -u github.com/junegunn/fzf
|
||||
```
|
||||
|
||||
Third-party libraries used
|
||||
--------------------------
|
||||
|
||||
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [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)
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
[MIT](LICENSE)
|
||||
1005
CHANGELOG.md
Normal file
1005
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM archlinux/base:latest
|
||||
RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc
|
||||
RUN gem install --no-document -v 5.14.2 minitest
|
||||
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
|
||||
RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
||||
|
||||
# Do not set default PS1
|
||||
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 ]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2020 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.
|
||||
129
Makefile
Normal file
129
Makefile
Normal file
@@ -0,0 +1,129 @@
|
||||
GO ?= go
|
||||
GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version))))
|
||||
|
||||
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
|
||||
ROOT_DIR := $(shell dirname $(MAKEFILE))
|
||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(MAKEFILE)
|
||||
|
||||
VERSION := $(shell git describe --abbrev=0)
|
||||
VERSION_TRIM := $(shell sed "s/-.*//" <<< $(VERSION))
|
||||
VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
|
||||
REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES))
|
||||
BUILD_FLAGS := -a -ldflags "-X main.version=$(VERSION) -X main.revision=$(REVISION) -w '-extldflags=$(LDFLAGS)'" -tags "$(TAGS)"
|
||||
|
||||
BINARY64 := fzf-$(GOOS)_amd64
|
||||
BINARYARM5 := fzf-$(GOOS)_arm5
|
||||
BINARYARM6 := fzf-$(GOOS)_arm6
|
||||
BINARYARM7 := fzf-$(GOOS)_arm7
|
||||
BINARYARM8 := fzf-$(GOOS)_arm8
|
||||
BINARYPPC64LE := fzf-$(GOOS)_ppc64le
|
||||
|
||||
# https://en.wikipedia.org/wiki/Uname
|
||||
UNAME_M := $(shell uname -m)
|
||||
ifeq ($(UNAME_M),x86_64)
|
||||
BINARY := $(BINARY64)
|
||||
else ifeq ($(UNAME_M),amd64)
|
||||
BINARY := $(BINARY64)
|
||||
else ifeq ($(UNAME_M),armv5l)
|
||||
BINARY := $(BINARYARM5)
|
||||
else ifeq ($(UNAME_M),armv6l)
|
||||
BINARY := $(BINARYARM6)
|
||||
else ifeq ($(UNAME_M),armv7l)
|
||||
BINARY := $(BINARYARM7)
|
||||
else ifeq ($(UNAME_M),armv8l)
|
||||
BINARY := $(BINARYARM8)
|
||||
else ifeq ($(UNAME_M),aarch64)
|
||||
BINARY := $(BINARYARM8)
|
||||
else ifeq ($(UNAME_M),ppc64le)
|
||||
BINARY := $(BINARYPPC64LE)
|
||||
else
|
||||
$(error Build on $(UNAME_M) is not supported, yet.)
|
||||
endif
|
||||
|
||||
all: target/$(BINARY)
|
||||
|
||||
test: $(SOURCES)
|
||||
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
|
||||
github.com/junegunn/fzf/src \
|
||||
github.com/junegunn/fzf/src/algo \
|
||||
github.com/junegunn/fzf/src/tui \
|
||||
github.com/junegunn/fzf/src/util
|
||||
|
||||
install: bin/fzf
|
||||
|
||||
build:
|
||||
goreleaser --rm-dist --snapshot
|
||||
|
||||
release:
|
||||
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
|
||||
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 --rm-dist --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 dist target
|
||||
|
||||
target/$(BINARY64): $(SOURCES)
|
||||
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
# https://github.com/golang/go/wiki/GoArm
|
||||
target/$(BINARYARM5): $(SOURCES)
|
||||
GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
target/$(BINARYARM6): $(SOURCES)
|
||||
GOARCH=arm GOARM=6 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
target/$(BINARYARM7): $(SOURCES)
|
||||
GOARCH=arm GOARM=7 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
target/$(BINARYARM8): $(SOURCES)
|
||||
GOARCH=arm64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
target/$(BINARYPPC64LE): $(SOURCES)
|
||||
GOARCH=ppc64le $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
bin/fzf: target/$(BINARY) | bin
|
||||
cp -f target/$(BINARY) bin/fzf
|
||||
|
||||
docker:
|
||||
docker build -t fzf-arch .
|
||||
docker run -it fzf-arch tmux
|
||||
|
||||
docker-test:
|
||||
docker build -t fzf-arch .
|
||||
docker run -it fzf-arch
|
||||
|
||||
update:
|
||||
$(GO) get -u
|
||||
$(GO) mod tidy
|
||||
|
||||
.PHONY: all build release test install clean docker docker-test update
|
||||
449
README-VIM.md
Normal file
449
README-VIM.md
Normal file
@@ -0,0 +1,449 @@
|
||||
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 git
|
||||
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 git
|
||||
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
|
||||
-------
|
||||
|
||||
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])`**
|
||||
- Starts fzf inside Vim with the given spec
|
||||
- `: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'})`
|
||||
- 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]`**
|
||||
- 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](https://github.com/junegunn/fzf.vim) 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.
|
||||
|
||||
`:FZF[!]`
|
||||
---------
|
||||
|
||||
```vim
|
||||
" Look for files under current directory
|
||||
:FZF
|
||||
|
||||
" Look for files under your home directory
|
||||
:FZF ~
|
||||
|
||||
" With fzf command-line options
|
||||
:FZF --reverse --info=inline /tmp
|
||||
|
||||
" Bang version starts fzf in fullscreen mode
|
||||
:FZF!
|
||||
```
|
||||
|
||||
Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), 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.
|
||||
|
||||
### Configuration
|
||||
|
||||
- `g:fzf_action`
|
||||
- Customizable extra key bindings for opening selected files in different ways
|
||||
- `g:fzf_layout`
|
||||
- Determines the size and position of fzf window
|
||||
- `g:fzf_colors`
|
||||
- Customizes fzf colors to match the current color scheme
|
||||
- `g:fzf_history_dir`
|
||||
- Enables history feature
|
||||
|
||||
#### Examples
|
||||
|
||||
```vim
|
||||
" This is the default extra key bindings
|
||||
let g:fzf_action = {
|
||||
\ 'ctrl-t': 'tab split',
|
||||
\ 'ctrl-x': 'split',
|
||||
\ 'ctrl-v': 'vsplit' }
|
||||
|
||||
" 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 }'))
|
||||
copen
|
||||
cc
|
||||
endfunction
|
||||
|
||||
let g:fzf_action = {
|
||||
\ 'ctrl-q': function('s:build_quickfix_list'),
|
||||
\ 'ctrl-t': 'tab split',
|
||||
\ 'ctrl-x': 'split',
|
||||
\ 'ctrl-v': 'vsplit' }
|
||||
|
||||
" Default fzf layout
|
||||
" - Popup window
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
|
||||
" - 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' }
|
||||
|
||||
" Customize fzf colors to match your color scheme
|
||||
" - fzf#wrap translates this to a set of `--color` options
|
||||
let g:fzf_colors =
|
||||
\ { 'fg': ['fg', 'Normal'],
|
||||
\ 'bg': ['bg', 'Normal'],
|
||||
\ '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'] }
|
||||
|
||||
" Enable per-command history
|
||||
" - History files will be stored in the specified directory
|
||||
" - When set, CTRL-N and CTRL-P will be bound to 'next-history' and
|
||||
" 'previous-history' instead of 'down' and 'up'.
|
||||
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) |
|
||||
| `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 |
|
||||
| `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:
|
||||
|
||||
```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`
|
||||
---------
|
||||
|
||||
`fzf#run()` function is the core of Vim integration. It takes a single
|
||||
dictionary argument, *a spec*, and starts fzf process accordingly. At the very
|
||||
least, specify `sink` option to tell what it should do with the selected
|
||||
entry.
|
||||
|
||||
```vim
|
||||
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.
|
||||
|
||||
```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.
|
||||
|
||||
```vim
|
||||
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
|
||||
```
|
||||
|
||||
fzf options can be specified as `options` entry in spec dictionary.
|
||||
|
||||
```vim
|
||||
call fzf#run({'sink': 'tabedit', 'options': '--multi --reverse'})
|
||||
```
|
||||
|
||||
You can also pass a layout option if you don't want fzf window to take up the
|
||||
entire screen.
|
||||
|
||||
```vim
|
||||
" up / down / left / right / window are allowed
|
||||
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'left': '40%'})
|
||||
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'window': '30vnew'})
|
||||
```
|
||||
|
||||
`source` doesn't have to be an external shell command, you can pass a Vim
|
||||
array as the source. In the next example, we pass the names of color
|
||||
schemes as the source to implement a color scheme selector.
|
||||
|
||||
```vim
|
||||
call fzf#run({'source': map(split(globpath(&rtp, 'colors/*.vim')),
|
||||
\ 'fnamemodify(v:val, ":t:r")'),
|
||||
\ 'sink': 'colo', 'left': '25%'})
|
||||
```
|
||||
|
||||
The following table summarizes the available options.
|
||||
|
||||
| Option name | Type | Description |
|
||||
| -------------------------- | ------------- | ---------------------------------------------------------------- |
|
||||
| `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 |
|
||||
| `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) fzf-tmux options (e.g. `-p90%,60%`) |
|
||||
| `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.
|
||||
|
||||
```vim
|
||||
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]]
|
||||
- `border` [string default `rounded`]: Border style
|
||||
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `none`
|
||||
|
||||
`fzf#wrap`
|
||||
----------
|
||||
|
||||
We have seen that several aspects of `:FZF` command can be configured with
|
||||
a set of global option variables; different ways to open files
|
||||
(`g:fzf_action`), window position and size (`g:fzf_layout`), color palette
|
||||
(`g:fzf_colors`), etc.
|
||||
|
||||
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)`**
|
||||
- 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` is not defined.
|
||||
- `fullscreen` can be either `0` or `1` (default: 0).
|
||||
|
||||
`fzf#wrap` takes a spec and returns an extended version of it (also
|
||||
a dictionary) with additional options for addressing global preferences. You
|
||||
can examine the return value of it like so:
|
||||
|
||||
```vim
|
||||
echo fzf#wrap({'source': 'ls'})
|
||||
```
|
||||
|
||||
After we *"wrap"* our spec, we pass it to `fzf#run`.
|
||||
|
||||
```vim
|
||||
call fzf#run(fzf#wrap({'source': 'ls'}))
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```vim
|
||||
command! LS call fzf#run(fzf#wrap({'source': 'ls'}))
|
||||
```
|
||||
|
||||
Type `:LS` and see how it works.
|
||||
|
||||
We would like to make `:LS!` (bang version) open fzf in fullscreen, just like
|
||||
`:FZF!`. Add `-bang` to command definition, and use `<bang>` value to set
|
||||
the last `fullscreen` argument of `fzf#wrap` (see `:help <bang>`).
|
||||
|
||||
```vim
|
||||
" On :LS!, <bang> evaluates to '!', and '!0' becomes 1
|
||||
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.
|
||||
|
||||
```vim
|
||||
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 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
|
||||
\ 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 `sink*`) 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 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]]
|
||||
" - highlight [string default 'Comment']: Highlight group for border
|
||||
" - 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 fzf-tmux options in `tmux` key.
|
||||
|
||||
```vim
|
||||
" See `man fzf-tmux` for available options
|
||||
if exists('$TMUX')
|
||||
let g:fzf_layout = { 'tmux': '-p90%,60%' }
|
||||
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 a non-popup layout (e.g. `{'down': '40%'}`) on Neovim,
|
||||
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
|
||||
```
|
||||
|
||||
[License](LICENSE)
|
||||
------------------
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2020 Junegunn Choi
|
||||
833
README.md
833
README.md
@@ -1,85 +1,190 @@
|
||||
fzf - Fuzzy finder for your shell
|
||||
=================================
|
||||
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf.png" height="170" alt="fzf - a command-line fuzzy finder"> [](https://travis-ci.org/junegunn/fzf)
|
||||
===
|
||||
|
||||
fzf is a general-purpose fuzzy finder for your shell.
|
||||
fzf is a general-purpose command-line fuzzy finder.
|
||||
|
||||

|
||||
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-preview.png" width=640>
|
||||
|
||||
It was heavily inspired by [ctrlp.vim](https://github.com/kien/ctrlp.vim) and
|
||||
the likes.
|
||||
It's an interactive Unix filter for command-line that can be used with any
|
||||
list; files, command history, processes, hostnames, bookmarks, git commits,
|
||||
etc.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
Pros
|
||||
----
|
||||
|
||||
fzf requires Ruby (>= 1.8.5).
|
||||
- Portable, no dependencies
|
||||
- Blazingly fast
|
||||
- The most comprehensive feature set
|
||||
- Flexible layout
|
||||
- Batteries included
|
||||
- Vim/Neovim plugin, key bindings and fuzzy auto-completion
|
||||
|
||||
Table of Contents
|
||||
-----------------
|
||||
|
||||
<!-- vim-markdown-toc GFM -->
|
||||
|
||||
* [Installation](#installation)
|
||||
* [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew)
|
||||
* [Using git](#using-git)
|
||||
* [Using Linux package managers](#using-linux-package-managers)
|
||||
* [Windows](#windows)
|
||||
* [As Vim plugin](#as-vim-plugin)
|
||||
* [Upgrading fzf](#upgrading-fzf)
|
||||
* [Building fzf](#building-fzf)
|
||||
* [Usage](#usage)
|
||||
* [Using the finder](#using-the-finder)
|
||||
* [Layout](#layout)
|
||||
* [Search syntax](#search-syntax)
|
||||
* [Environment variables](#environment-variables)
|
||||
* [Options](#options)
|
||||
* [Demo](#demo)
|
||||
* [Examples](#examples)
|
||||
* [`fzf-tmux` script](#fzf-tmux-script)
|
||||
* [Key bindings for command-line](#key-bindings-for-command-line)
|
||||
* [Fuzzy completion for bash and zsh](#fuzzy-completion-for-bash-and-zsh)
|
||||
* [Files and directories](#files-and-directories)
|
||||
* [Process IDs](#process-ids)
|
||||
* [Host names](#host-names)
|
||||
* [Environment variables / Aliases](#environment-variables--aliases)
|
||||
* [Settings](#settings)
|
||||
* [Supported commands](#supported-commands)
|
||||
* [Custom fuzzy completion](#custom-fuzzy-completion)
|
||||
* [Vim plugin](#vim-plugin)
|
||||
* [Advanced topics](#advanced-topics)
|
||||
* [Performance](#performance)
|
||||
* [Executing external programs](#executing-external-programs)
|
||||
* [Reloading the candidate list](#reloading-the-candidate-list)
|
||||
* [1. Update the list of processes by pressing CTRL-R](#1-update-the-list-of-processes-by-pressing-ctrl-r)
|
||||
* [2. Switch between sources by pressing CTRL-D or CTRL-F](#2-switch-between-sources-by-pressing-ctrl-d-or-ctrl-f)
|
||||
* [3. Interactive ripgrep integration](#3-interactive-ripgrep-integration)
|
||||
* [Preview window](#preview-window)
|
||||
* [Tips](#tips)
|
||||
* [Respecting `.gitignore`](#respecting-gitignore)
|
||||
* [Fish shell](#fish-shell)
|
||||
* [Related projects](#related-projects)
|
||||
* [License](#license)
|
||||
|
||||
<!-- vim-markdown-toc -->
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Download [fzf executable](https://raw.github.com/junegunn/fzf/master/fzf) and
|
||||
put it somewhere in your search $PATH.
|
||||
fzf project consists of the following components:
|
||||
|
||||
- `fzf` executable
|
||||
- `fzf-tmux` script for launching fzf in a tmux pane
|
||||
- Shell extensions
|
||||
- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
|
||||
- Fuzzy auto-completion (bash, zsh)
|
||||
- Vim/Neovim plugin
|
||||
|
||||
You can [download fzf executable][bin] alone if you don't need the extra
|
||||
stuff.
|
||||
|
||||
[bin]: https://github.com/junegunn/fzf/releases
|
||||
|
||||
### Using Homebrew or Linuxbrew
|
||||
|
||||
You can use [Homebrew](http://brew.sh/) or [Linuxbrew](http://linuxbrew.sh/)
|
||||
to install fzf.
|
||||
|
||||
```sh
|
||||
mkdir -p ~/bin
|
||||
wget https://raw.github.com/junegunn/fzf/master/fzf -O ~/bin/fzf
|
||||
chmod +x ~/bin/fzf
|
||||
brew install fzf
|
||||
|
||||
# To install useful key bindings and fuzzy completion:
|
||||
$(brew --prefix)/opt/fzf/install
|
||||
```
|
||||
|
||||
Or you can just clone this repository and run
|
||||
fzf is also available [via MacPorts][portfile]: `sudo port install fzf`
|
||||
|
||||
[portfile]: https://github.com/macports/macports-ports/blob/master/sysutils/fzf/Portfile
|
||||
|
||||
### Using git
|
||||
|
||||
Alternatively, you can "git clone" this repository to any directory and run
|
||||
[install](https://github.com/junegunn/fzf/blob/master/install) script.
|
||||
|
||||
```sh
|
||||
git clone https://github.com/junegunn/fzf.git
|
||||
fzf/install
|
||||
git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
|
||||
~/.fzf/install
|
||||
```
|
||||
|
||||
Make sure that ~/bin is included in $PATH.
|
||||
### Using Linux package managers
|
||||
|
||||
```sh
|
||||
export PATH=$PATH:~/bin
|
||||
| Package Manager | Linux Distribution | Command |
|
||||
| --- | --- | --- |
|
||||
| APK | Alpine Linux | `sudo apk add fzf` |
|
||||
| APT | Debian 9+/Ubuntu 19.10+ | `sudo apt-get install fzf` |
|
||||
| Conda | | `conda install -c conda-forge fzf` |
|
||||
| DNF | Fedora | `sudo dnf install fzf` |
|
||||
| Nix | NixOS, etc. | `nix-env -iA nixpkgs.fzf` |
|
||||
| Pacman | Arch Linux | `sudo pacman -S fzf` |
|
||||
| pkg | FreeBSD | `pkg install fzf` |
|
||||
| pkg_add | OpenBSD | `pkg_add fzf` |
|
||||
| XBPS | Void Linux | `sudo xbps-install -S fzf` |
|
||||
| Zypper | openSUSE | `sudo zypper install fzf` |
|
||||
|
||||
> :warning: **Key bindings (CTRL-T / CTRL-R / ALT-C) and fuzzy auto-completion
|
||||
> may not be enabled by default.**
|
||||
>
|
||||
> Refer to the package documentation for more information. (e.g. `apt-cache show fzf`)
|
||||
|
||||
### Windows
|
||||
|
||||
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
|
||||
available via [Chocolatey][choco] and [Scoop][scoop]:
|
||||
|
||||
| Package manager | Command |
|
||||
| --- | --- |
|
||||
| Chocolatey | `choco install fzf` |
|
||||
| Scoop | `scoop install fzf` |
|
||||
|
||||
[choco]: https://chocolatey.org/packages/fzf
|
||||
[scoop]: https://github.com/ScoopInstaller/Main/blob/master/bucket/fzf.json
|
||||
|
||||
Known issues and limitations on Windows can be found on [the wiki
|
||||
page][windows-wiki].
|
||||
|
||||
[windows-wiki]: https://github.com/junegunn/fzf/wiki/Windows
|
||||
|
||||
### As Vim plugin
|
||||
|
||||
If you use
|
||||
[vim-plug](https://github.com/junegunn/vim-plug), add this line to your Vim
|
||||
configuration file:
|
||||
|
||||
```vim
|
||||
Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
|
||||
```
|
||||
|
||||
### Install as Ruby gem
|
||||
`fzf#install()` makes sure that you have the latest binary, but it's optional,
|
||||
so you can omit it if you use a plugin manager that doesn't support hooks.
|
||||
|
||||
fzf can be installed as a Ruby gem
|
||||
For more installation options, see [README-VIM.md](README-VIM.md).
|
||||
|
||||
```
|
||||
gem install fzf
|
||||
```
|
||||
Upgrading fzf
|
||||
-------------
|
||||
|
||||
It's a bit easier to install and update the script but the Ruby gem version
|
||||
takes slightly longer to start.
|
||||
fzf is being actively developed and you might want to upgrade it once in a
|
||||
while. Please follow the instruction below depending on the installation
|
||||
method used.
|
||||
|
||||
### Install as Vim plugin
|
||||
- git: `cd ~/.fzf && git pull && ./install`
|
||||
- brew: `brew update; brew reinstall fzf`
|
||||
- macports: `sudo port upgrade fzf`
|
||||
- chocolatey: `choco upgrade fzf`
|
||||
- vim-plug: `:PlugUpdate fzf`
|
||||
|
||||
You can use any Vim plugin manager to install fzf for Vim. If you don't use one,
|
||||
I recommend you try [vim-plug](https://github.com/junegunn/vim-plug).
|
||||
Building fzf
|
||||
------------
|
||||
|
||||
1. [Install vim-plug](https://github.com/junegunn/vim-plug#usage)
|
||||
2. Edit your .vimrc
|
||||
|
||||
call plug#begin()
|
||||
Plug 'junegunn/fzf'
|
||||
" ...
|
||||
call plug#end()
|
||||
|
||||
3. Run `:PlugInstall`
|
||||
See [BUILD.md](BUILD.md).
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
```
|
||||
usage: fzf [options]
|
||||
|
||||
-m, --multi Enable multi-select
|
||||
-x, --extended Extended-search mode
|
||||
-s, --sort=MAX Maximum number of matched items to sort. Default: 1000
|
||||
+s, --no-sort Do not sort the result. Keep the sequence unchanged.
|
||||
+i Case-sensitive match
|
||||
+c, --no-color Disable colors
|
||||
```
|
||||
|
||||
fzf will launch curses-based finder, read the list from STDIN, and write the
|
||||
fzf will launch interactive finder, read the list from STDIN, and write the
|
||||
selected item to STDOUT.
|
||||
|
||||
```sh
|
||||
@@ -94,187 +199,503 @@ files excluding hidden ones. (You can override the default command with
|
||||
vim $(fzf)
|
||||
```
|
||||
|
||||
If you want to preserve the exact sequence of the input, provide `--no-sort` (or
|
||||
`+s`) option.
|
||||
#### Using the finder
|
||||
|
||||
- `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P`) to move cursor up and down
|
||||
- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit
|
||||
- On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items
|
||||
- Emacs style key bindings
|
||||
- Mouse: scroll, click, double-click; shift-click and shift-scroll on
|
||||
multi-select mode
|
||||
|
||||
#### Layout
|
||||
|
||||
fzf by default starts in fullscreen mode, but you can make it start below the
|
||||
cursor with `--height` option.
|
||||
|
||||
```sh
|
||||
history | fzf +s
|
||||
vim $(fzf --height 40%)
|
||||
```
|
||||
|
||||
### Key binding
|
||||
|
||||
Use CTRL-J and CTRL-K (or CTRL-N and CTRL-P) to change the selection, press
|
||||
enter key to select the item. CTRL-C will terminate the finder.
|
||||
|
||||
The following readline key bindings should also work as expected.
|
||||
|
||||
- CTRL-A / CTRL-E
|
||||
- CTRL-B / CTRL-F
|
||||
- CTRL-W / CTRL-U
|
||||
- ALT-B / ALT-F
|
||||
|
||||
If you enable multi-select mode with `-m` option, you can select multiple items
|
||||
with TAB or Shift-TAB key.
|
||||
|
||||
### Extended-search mode
|
||||
|
||||
With `-x` or `--extended` option, fzf will start in "extended-search mode".
|
||||
|
||||
In this mode, you can specify multiple patterns delimited by spaces,
|
||||
such as: `^music .mp3$ sbtrkt !rmx`
|
||||
|
||||
| Token | Description | Match type |
|
||||
| -------- | -------------------------------- | -------------------- |
|
||||
| `^music` | Items that start with `music` | prefix-exact-match |
|
||||
| `.mp3$` | Items that end with `.mp3` | suffix-exact-match |
|
||||
| `sbtrkt` | Items that match `sbtrkt` | fuzzy-match |
|
||||
| `!rmx` | Items that do not match `rmx` | inverse-fuzzy-match |
|
||||
| `'wild` | Items that include `wild` | exact-match (quoted) |
|
||||
| `!'fire` | Items that do not include `fire` | inverse-exact-match |
|
||||
|
||||
Usage as Vim plugin
|
||||
-------------------
|
||||
|
||||
If you install fzf as a Vim plugin, `:FZF` command will be added.
|
||||
|
||||
```vim
|
||||
:FZF
|
||||
:FZF --no-sort -m
|
||||
```
|
||||
|
||||
You can override the source command which produces input to fzf.
|
||||
|
||||
```vim
|
||||
let g:fzf_source = 'find . -type f'
|
||||
```
|
||||
|
||||
And you can predefine default options to fzf command.
|
||||
|
||||
```vim
|
||||
let g:fzf_options = '--no-color --extended'
|
||||
```
|
||||
|
||||
For more advanced uses, you can call `fzf#run` function as follows.
|
||||
|
||||
```vim
|
||||
:call fzf#run('tabedit', '-m +c')
|
||||
```
|
||||
|
||||
Most of the time, you will prefer native Vim plugins with better integration
|
||||
with Vim. The only reason one might consider using fzf in Vim is its speed. For
|
||||
a very large list of files, fzf is significantly faster and it does not block.
|
||||
|
||||
Useful bash examples
|
||||
--------------------
|
||||
Also check out `--reverse` and `--layout` options if you prefer
|
||||
"top-down" layout instead of the default "bottom-up" layout.
|
||||
|
||||
```sh
|
||||
# vimf - Open selected file in Vim
|
||||
vimf() {
|
||||
FILE=$(fzf) && vim "$FILE"
|
||||
}
|
||||
|
||||
# fd - cd to selected directory
|
||||
fd() {
|
||||
DIR=$(find ${1:-*} -path '*/\.*' -prune -o -type d -print 2> /dev/null | fzf) && cd "$DIR"
|
||||
}
|
||||
|
||||
# fda - including hidden directories
|
||||
fda() {
|
||||
DIR=$(find ${1:-*} -type d 2> /dev/null | fzf) && cd "$DIR"
|
||||
}
|
||||
|
||||
# fh - repeat history
|
||||
fh() {
|
||||
eval $(history | fzf +s | sed 's/ *[0-9]* *//')
|
||||
}
|
||||
|
||||
# fkill - kill process
|
||||
fkill() {
|
||||
ps -ef | sed 1d | fzf -m | awk '{print $2}' | xargs kill -${1:-9}
|
||||
}
|
||||
|
||||
# (Assuming you don't use the default CTRL-T and CTRL-R)
|
||||
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
bind '"\er": redraw-current-line'
|
||||
bind '"\C-t": " \C-u \C-a\C-k$(fzf)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er"'
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind '"\C-r": " \C-e\C-u$(history | fzf +s | sed \"s/ *[0-9]* *//\")\e\C-e\er"'
|
||||
vim $(fzf --height 40% --reverse)
|
||||
```
|
||||
|
||||
zsh widgets
|
||||
-----------
|
||||
You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by
|
||||
default. For example,
|
||||
|
||||
```sh
|
||||
# CTRL-T - Paste the selected file(s) path into the command line
|
||||
fzf-file-widget() {
|
||||
local FILES
|
||||
local IFS="
|
||||
"
|
||||
FILES=($(
|
||||
find * -path '*/\.*' -prune \
|
||||
-o -type f -print \
|
||||
-o -type l -print 2> /dev/null | fzf -m))
|
||||
unset IFS
|
||||
FILES=$FILES:q
|
||||
LBUFFER="${LBUFFER%% #} $FILES"
|
||||
zle redisplay
|
||||
}
|
||||
zle -N fzf-file-widget
|
||||
bindkey '^T' fzf-file-widget
|
||||
export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border'
|
||||
```
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
fzf-cd-widget() {
|
||||
cd "${$(find * -path '*/\.*' -prune \
|
||||
-o -type d -print 2> /dev/null | fzf):-.}"
|
||||
zle reset-prompt
|
||||
}
|
||||
zle -N fzf-cd-widget
|
||||
bindkey '\ec' fzf-cd-widget
|
||||
#### Search syntax
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
fzf-history-widget() {
|
||||
LBUFFER=$(history | fzf +s | sed "s/ *[0-9]* *//")
|
||||
zle redisplay
|
||||
Unless otherwise specified, fzf starts in "extended-search mode" where you can
|
||||
type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt
|
||||
!fire`
|
||||
|
||||
| Token | Match type | Description |
|
||||
| --------- | -------------------------- | ------------------------------------ |
|
||||
| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` |
|
||||
| `'wild` | exact-match (quoted) | Items that include `wild` |
|
||||
| `^music` | prefix-exact-match | Items that start with `music` |
|
||||
| `.mp3$` | suffix-exact-match | Items that end with `.mp3` |
|
||||
| `!fire` | inverse-exact-match | Items that do not include `fire` |
|
||||
| `!^music` | inverse-prefix-exact-match | Items that do not start with `music` |
|
||||
| `!.mp3$` | inverse-suffix-exact-match | Items that do not end with `.mp3` |
|
||||
|
||||
If you don't prefer fuzzy matching and do not wish to "quote" every word,
|
||||
start fzf with `-e` or `--exact` option. Note that when `--exact` is set,
|
||||
`'`-prefix "unquotes" the term.
|
||||
|
||||
A single bar character term acts as an OR operator. For example, the following
|
||||
query matches entries that start with `core` and end with either `go`, `rb`,
|
||||
or `py`.
|
||||
|
||||
```
|
||||
^core go$ | rb$ | py$
|
||||
```
|
||||
|
||||
#### Environment variables
|
||||
|
||||
- `FZF_DEFAULT_COMMAND`
|
||||
- Default command to use when input is tty
|
||||
- e.g. `export FZF_DEFAULT_COMMAND='fd --type f'`
|
||||
- `FZF_DEFAULT_OPTS`
|
||||
- Default options
|
||||
- e.g. `export FZF_DEFAULT_OPTS="--layout=reverse --inline-info"`
|
||||
|
||||
#### Options
|
||||
|
||||
See the man page (`man fzf`) for the full list of options.
|
||||
|
||||
#### Demo
|
||||
If you learn by watching videos, check out this screencast by [@samoshkin](https://github.com/samoshkin) to explore `fzf` features.
|
||||
|
||||
<a title="fzf - command-line fuzzy finder" href="https://www.youtube.com/watch?v=qgG5Jhi_Els">
|
||||
<img src="https://i.imgur.com/vtG8olE.png" width="640">
|
||||
</a>
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Many useful examples can be found on [the wiki
|
||||
page](https://github.com/junegunn/fzf/wiki/examples). Feel free to add your
|
||||
own as well.
|
||||
|
||||
`fzf-tmux` script
|
||||
-----------------
|
||||
|
||||
[fzf-tmux](bin/fzf-tmux) is a bash script that opens fzf in a tmux pane.
|
||||
|
||||
```sh
|
||||
# usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
||||
|
||||
# See available options
|
||||
fzf-tmux --help
|
||||
|
||||
# select git branches in horizontal split below (15 lines)
|
||||
git branch | fzf-tmux -d 15
|
||||
|
||||
# select multiple words in vertical split on the left (20% of screen width)
|
||||
cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse
|
||||
```
|
||||
|
||||
It will still work even when you're not on tmux, silently ignoring `-[pudlr]`
|
||||
options, so you can invariably use `fzf-tmux` in your scripts.
|
||||
|
||||
Alternatively, you can use `--height HEIGHT[%]` option not to start fzf in
|
||||
fullscreen mode.
|
||||
|
||||
```sh
|
||||
fzf --height 40%
|
||||
```
|
||||
|
||||
Key bindings for command-line
|
||||
-----------------------------
|
||||
|
||||
The install script will setup the following key bindings for bash, zsh, and
|
||||
fish.
|
||||
|
||||
- `CTRL-T` - Paste the selected files and directories onto the command-line
|
||||
- Set `FZF_CTRL_T_COMMAND` to override the default command
|
||||
- Set `FZF_CTRL_T_OPTS` to pass additional options
|
||||
- `CTRL-R` - Paste the selected command from history onto the command-line
|
||||
- If you want to see the commands in chronological order, press `CTRL-R`
|
||||
again which toggles sorting by relevance
|
||||
- Set `FZF_CTRL_R_OPTS` to pass additional options
|
||||
- `ALT-C` - cd into the selected directory
|
||||
- Set `FZF_ALT_C_COMMAND` to override the default command
|
||||
- Set `FZF_ALT_C_OPTS` to pass additional options
|
||||
|
||||
If you're on a tmux session, you can start fzf in a tmux split pane or in
|
||||
a tmux popup window by setting `FZF_TMUX_OPTS` (e.g. `-d 40%`).
|
||||
See `fzf-tmux --help` for available options.
|
||||
|
||||
More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings).
|
||||
|
||||
Fuzzy completion for bash and zsh
|
||||
---------------------------------
|
||||
|
||||
#### Files and directories
|
||||
|
||||
Fuzzy completion for files and directories can be triggered if the word before
|
||||
the cursor ends with the trigger sequence which is by default `**`.
|
||||
|
||||
- `COMMAND [DIRECTORY/][FUZZY_PATTERN]**<TAB>`
|
||||
|
||||
```sh
|
||||
# Files under current directory
|
||||
# - You can select multiple items with TAB key
|
||||
vim **<TAB>
|
||||
|
||||
# Files under parent directory
|
||||
vim ../**<TAB>
|
||||
|
||||
# Files under parent directory that match `fzf`
|
||||
vim ../fzf**<TAB>
|
||||
|
||||
# Files under your home directory
|
||||
vim ~/**<TAB>
|
||||
|
||||
|
||||
# Directories under current directory (single-selection)
|
||||
cd **<TAB>
|
||||
|
||||
# Directories under ~/github that match `fzf`
|
||||
cd ~/github/fzf**<TAB>
|
||||
```
|
||||
|
||||
#### Process IDs
|
||||
|
||||
Fuzzy completion for PIDs is provided for kill command. In this case,
|
||||
there is no trigger sequence, just press tab key after kill command.
|
||||
|
||||
```sh
|
||||
# Can select multiple processes with <TAB> or <Shift-TAB> keys
|
||||
kill -9 <TAB>
|
||||
```
|
||||
|
||||
#### Host names
|
||||
|
||||
For ssh and telnet commands, fuzzy completion for host names is provided. The
|
||||
names are extracted from /etc/hosts and ~/.ssh/config.
|
||||
|
||||
```sh
|
||||
ssh **<TAB>
|
||||
telnet **<TAB>
|
||||
```
|
||||
|
||||
#### Environment variables / Aliases
|
||||
|
||||
```sh
|
||||
unset **<TAB>
|
||||
export **<TAB>
|
||||
unalias **<TAB>
|
||||
```
|
||||
|
||||
#### Settings
|
||||
|
||||
```sh
|
||||
# Use ~~ as the trigger sequence instead of the default **
|
||||
export FZF_COMPLETION_TRIGGER='~~'
|
||||
|
||||
# Options to fzf command
|
||||
export FZF_COMPLETION_OPTS='+c -x'
|
||||
|
||||
# Use fd (https://github.com/sharkdp/fd) instead of the default find
|
||||
# command for listing path candidates.
|
||||
# - The first argument to the function ($1) is the base path to start traversal
|
||||
# - See the source code (completion.{bash,zsh}) for the details.
|
||||
_fzf_compgen_path() {
|
||||
fd --hidden --follow --exclude ".git" . "$1"
|
||||
}
|
||||
zle -N fzf-history-widget
|
||||
bindkey '^R' fzf-history-widget
|
||||
|
||||
# Use fd to generate the list for directory completion
|
||||
_fzf_compgen_dir() {
|
||||
fd --type d --hidden --follow --exclude ".git" . "$1"
|
||||
}
|
||||
|
||||
# (EXPERIMENTAL) Advanced customization of fzf options via _fzf_comprun function
|
||||
# - The first argument to the function is the name of the command.
|
||||
# - You should make sure to pass the rest of the arguments to fzf.
|
||||
_fzf_comprun() {
|
||||
local command=$1
|
||||
shift
|
||||
|
||||
case "$command" in
|
||||
cd) fzf "$@" --preview 'tree -C {} | head -200' ;;
|
||||
export|unset) fzf "$@" --preview "eval 'echo \$'{}" ;;
|
||||
ssh) fzf "$@" --preview 'dig {}' ;;
|
||||
*) fzf "$@" ;;
|
||||
esac
|
||||
}
|
||||
```
|
||||
|
||||
#### Supported commands
|
||||
|
||||
On bash, fuzzy completion is enabled only for a predefined set of commands
|
||||
(`complete | grep _fzf` to see the list). But you can enable it for other
|
||||
commands as well by using `_fzf_setup_completion` helper function.
|
||||
|
||||
```sh
|
||||
# usage: _fzf_setup_completion path|dir|var|alias|host COMMANDS...
|
||||
_fzf_setup_completion path ag git kubectl
|
||||
_fzf_setup_completion dir tree
|
||||
```
|
||||
|
||||
#### Custom fuzzy completion
|
||||
|
||||
_**(Custom completion API is experimental and subject to change)**_
|
||||
|
||||
For a command named _"COMMAND"_, define `_fzf_complete_COMMAND` function using
|
||||
`_fzf_complete` helper.
|
||||
|
||||
```sh
|
||||
# Custom fuzzy completion for "doge" command
|
||||
# e.g. doge **<TAB>
|
||||
_fzf_complete_doge() {
|
||||
_fzf_complete --multi --reverse --prompt="doge> " -- "$@" < <(
|
||||
echo very
|
||||
echo wow
|
||||
echo such
|
||||
echo doge
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
- The arguments before `--` are the options to fzf.
|
||||
- After `--`, simply pass the original completion arguments unchanged (`"$@"`).
|
||||
- Then write a set of commands that generates the completion candidates and
|
||||
feed its output to the function using process substitution (`< <(...)`).
|
||||
|
||||
zsh will automatically pick up the function using the naming convention but in
|
||||
bash you have to manually associate the function with the command using
|
||||
`complete` command.
|
||||
|
||||
```sh
|
||||
[ -n "$BASH" ] && complete -F _fzf_complete_doge -o default -o bashdefault doge
|
||||
```
|
||||
|
||||
If you need to post-process the output from fzf, define
|
||||
`_fzf_complete_COMMAND_post` as follows.
|
||||
|
||||
```sh
|
||||
_fzf_complete_foo() {
|
||||
_fzf_complete --multi --reverse --header-lines=3 -- "$@" < <(
|
||||
ls -al
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_foo_post() {
|
||||
awk '{print $NF}'
|
||||
}
|
||||
|
||||
[ -n "$BASH" ] && complete -F _fzf_complete_foo -o default -o bashdefault foo
|
||||
```
|
||||
|
||||
Vim plugin
|
||||
----------
|
||||
|
||||
See [README-VIM.md](README-VIM.md).
|
||||
|
||||
Advanced topics
|
||||
---------------
|
||||
|
||||
### Performance
|
||||
|
||||
fzf is fast and is [getting even faster][perf]. Performance should not be
|
||||
a problem in most use cases. However, you might want to be aware of the
|
||||
options that affect the performance.
|
||||
|
||||
- `--ansi` tells fzf to extract and parse ANSI color codes in the input and it
|
||||
makes the initial scanning slower. So it's not recommended that you add it
|
||||
to your `$FZF_DEFAULT_OPTS`.
|
||||
- `--nth` makes fzf slower as fzf has to tokenize each line.
|
||||
- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each
|
||||
line.
|
||||
- If you absolutely need better performance, you can consider using
|
||||
`--algo=v1` (the default being `v2`) to make fzf use a faster greedy
|
||||
algorithm. However, this algorithm is not guaranteed to find the optimal
|
||||
ordering of the matches and is not recommended.
|
||||
|
||||
[perf]: https://junegunn.kr/images/fzf-0.17.0.png
|
||||
|
||||
### Executing external programs
|
||||
|
||||
You can set up key bindings for starting external processes without leaving
|
||||
fzf (`execute`, `execute-silent`).
|
||||
|
||||
```bash
|
||||
# Press F1 to open the file with less without leaving fzf
|
||||
# Press CTRL-Y to copy the line to clipboard and aborts fzf (requires pbcopy)
|
||||
fzf --bind 'f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort'
|
||||
```
|
||||
|
||||
See *KEY BINDINGS* section of the man page for details.
|
||||
|
||||
### Reloading the candidate list
|
||||
|
||||
By binding `reload` action to a key or an event, you can make fzf dynamically
|
||||
reload the candidate list. See https://github.com/junegunn/fzf/issues/1750 for
|
||||
more details.
|
||||
|
||||
#### 1. Update the list of processes by pressing CTRL-R
|
||||
|
||||
```sh
|
||||
FZF_DEFAULT_COMMAND='ps -ef' \
|
||||
fzf --bind 'ctrl-r:reload($FZF_DEFAULT_COMMAND)' \
|
||||
--header 'Press CTRL-R to reload' --header-lines=1 \
|
||||
--height=50% --layout=reverse
|
||||
```
|
||||
|
||||
#### 2. Switch between sources by pressing CTRL-D or CTRL-F
|
||||
|
||||
```sh
|
||||
FZF_DEFAULT_COMMAND='find . -type f' \
|
||||
fzf --bind 'ctrl-d:reload(find . -type d),ctrl-f:reload($FZF_DEFAULT_COMMAND)' \
|
||||
--height=50% --layout=reverse
|
||||
```
|
||||
|
||||
#### 3. Interactive ripgrep integration
|
||||
|
||||
The following example uses fzf as the selector interface for ripgrep. We bound
|
||||
`reload` action to `change` event, so every time you type on fzf, ripgrep
|
||||
process will restart with the updated query string denoted by the placeholder
|
||||
expression `{q}`. Also, note that we used `--phony` option so that fzf doesn't
|
||||
perform any secondary filtering.
|
||||
|
||||
```sh
|
||||
INITIAL_QUERY=""
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \
|
||||
fzf --bind "change:reload:$RG_PREFIX {q} || true" \
|
||||
--ansi --phony --query "$INITIAL_QUERY" \
|
||||
--height=50% --layout=reverse
|
||||
```
|
||||
|
||||
If ripgrep doesn't find any matches, it will exit with a non-zero exit status,
|
||||
and fzf will warn you about it. To suppress the warning message, we added
|
||||
`|| true` to the command, so that it always exits with 0.
|
||||
|
||||
### Preview window
|
||||
|
||||
When the `--preview` option is set, fzf automatically starts an external process
|
||||
with the current line as the argument and shows the result in the split window.
|
||||
Your `$SHELL` is used to execute the command with `$SHELL -c COMMAND`.
|
||||
The window can be scrolled using the mouse or custom key bindings.
|
||||
|
||||
```bash
|
||||
# {} is replaced to the single-quoted string of the focused line
|
||||
fzf --preview 'cat {}'
|
||||
```
|
||||
|
||||
Preview window supports ANSI colors, so you can use any program that
|
||||
syntax-highlights the content of a file, such as
|
||||
[Bat](https://github.com/sharkdp/bat) or
|
||||
[Highlight](http://www.andre-simon.de/doku/highlight/en/highlight.php):
|
||||
|
||||
```bash
|
||||
fzf --preview 'bat --style=numbers --color=always --line-range :500 {}'
|
||||
```
|
||||
|
||||
You can customize the size, position, and border of the preview window using
|
||||
`--preview-window` option, and the foreground and background color of it with
|
||||
`--color` option. For example,
|
||||
|
||||
```bash
|
||||
fzf --height 40% --layout reverse --info inline --border \
|
||||
--preview 'file {}' --preview-window down:1:noborder \
|
||||
--color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899'
|
||||
```
|
||||
|
||||
See the man page (`man fzf`) for the full list of options.
|
||||
|
||||
For more advanced examples, see [Key bindings for git with fzf][fzf-git]
|
||||
([code](https://gist.github.com/junegunn/8b572b8d4b5eddd8b85e5f4d40f17236)).
|
||||
|
||||
[fzf-git]: https://junegunn.kr/2016/07/fzf-git/
|
||||
|
||||
----
|
||||
|
||||
Since fzf is a general-purpose text filter rather than a file finder, **it is
|
||||
not a good idea to add `--preview` option to your `$FZF_DEFAULT_OPTS`**.
|
||||
|
||||
```sh
|
||||
# *********************
|
||||
# ** DO NOT DO THIS! **
|
||||
# *********************
|
||||
export FZF_DEFAULT_OPTS='--preview "bat --style=numbers --color=always --line-range :500 {}"'
|
||||
|
||||
# bat doesn't work with any input other than the list of files
|
||||
ps -ef | fzf
|
||||
seq 100 | fzf
|
||||
history | fzf
|
||||
```
|
||||
|
||||
Tips
|
||||
----
|
||||
|
||||
### Faster startup with `--disable-gems` options
|
||||
#### Respecting `.gitignore`
|
||||
|
||||
If you're running Ruby 1.9 or above, you can improve the startup time with
|
||||
`--disable-gems` option to Ruby.
|
||||
|
||||
- `time ruby ~/bin/fzf -h`
|
||||
- 0.077 sec
|
||||
- `time ruby --disable-gems ~/bin/fzf -h`
|
||||
- 0.025 sec
|
||||
|
||||
Define fzf alias with the option as follows:
|
||||
You can use [fd](https://github.com/sharkdp/fd),
|
||||
[ripgrep](https://github.com/BurntSushi/ripgrep), or [the silver
|
||||
searcher](https://github.com/ggreer/the_silver_searcher) instead of the
|
||||
default find command to traverse the file system while respecting
|
||||
`.gitignore`.
|
||||
|
||||
```sh
|
||||
alias fzf='ruby --disable-gems ~/bin/fzf'
|
||||
# Feed the output of fd into fzf
|
||||
fd --type f | fzf
|
||||
|
||||
# Setting fd as the default source for fzf
|
||||
export FZF_DEFAULT_COMMAND='fd --type f'
|
||||
|
||||
# Now fzf (w/o pipe) will use fd instead of find
|
||||
fzf
|
||||
|
||||
# To apply the command to CTRL-T as well
|
||||
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
|
||||
```
|
||||
|
||||
### Incorrect display on Ruby 1.8
|
||||
If you want the command to follow symbolic links, and don't want it to exclude
|
||||
hidden files, use the following command:
|
||||
|
||||
It is reported that the output of fzf can become unreadable on some terminals
|
||||
when it's running on Ruby 1.8. If you experience the problem, upgrade your Ruby
|
||||
to 1.9 or above. Ruby 1.9 or above is also required for displaying Unicode
|
||||
characters.
|
||||
```sh
|
||||
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
#### Fish shell
|
||||
|
||||
MIT
|
||||
`CTRL-T` key binding of fish, unlike those of bash and zsh, will use the last
|
||||
token on the command-line as the root directory for the recursive search. For
|
||||
instance, hitting `CTRL-T` at the end of the following command-line
|
||||
|
||||
Author
|
||||
------
|
||||
```sh
|
||||
ls /var/
|
||||
```
|
||||
|
||||
Junegunn Choi
|
||||
will list all files and directories under `/var/`.
|
||||
|
||||
When using a custom `FZF_CTRL_T_COMMAND`, use the unexpanded `$dir` variable to
|
||||
make use of this feature. `$dir` defaults to `.` when the last token is not a
|
||||
valid directory. Example:
|
||||
|
||||
```sh
|
||||
set -g FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'"
|
||||
```
|
||||
|
||||
Related projects
|
||||
----------------
|
||||
|
||||
https://github.com/junegunn/fzf/wiki/Related-projects
|
||||
|
||||
[License](LICENSE)
|
||||
------------------
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2020 Junegunn Choi
|
||||
|
||||
8
Rakefile
8
Rakefile
@@ -1,8 +0,0 @@
|
||||
require "bundler/gem_tasks"
|
||||
require 'rake/testtask'
|
||||
|
||||
Rake::TestTask.new(:test) do |test|
|
||||
test.pattern = 'test/**/test_*.rb'
|
||||
test.verbose = true
|
||||
end
|
||||
|
||||
224
bin/fzf-tmux
Executable file
224
bin/fzf-tmux
Executable file
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env bash
|
||||
# fzf-tmux: starts fzf in a tmux pane
|
||||
# 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'
|
||||
|
||||
tmux_args=()
|
||||
args=()
|
||||
opt=""
|
||||
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}")
|
||||
|
||||
help() {
|
||||
>&2 echo 'usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
||||
|
||||
LAYOUT OPTIONS:
|
||||
(default layout: -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
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
arg="$1"
|
||||
shift
|
||||
[[ -z "$skip" ]] && case "$arg" in
|
||||
-)
|
||||
term=1
|
||||
;;
|
||||
--help)
|
||||
help
|
||||
;;
|
||||
--version)
|
||||
echo "fzf-tmux (with fzf $("$fzf" --version))"
|
||||
exit
|
||||
;;
|
||||
-p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*)
|
||||
if [[ "$arg" =~ ^-[pwhxy] ]]; then
|
||||
[[ "$opt" =~ "-K -E" ]] || opt="-K -E"
|
||||
elif [[ "$arg" =~ ^.[lr] ]]; then
|
||||
opt="-h"
|
||||
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
|
||||
opt="$opt -d"
|
||||
swap="; swap-pane -D ; select-pane -U"
|
||||
close="; tmux swap-pane -D"
|
||||
fi
|
||||
fi
|
||||
if [[ ${#arg} -gt 2 ]]; then
|
||||
size="${arg:2}"
|
||||
else
|
||||
if [[ "$1" =~ ^[0-9%,]+$ ]] || [[ "$1" =~ ^[A-Z]$ ]]; then
|
||||
size="$1"
|
||||
shift
|
||||
else
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
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 [[ -n "$swap" ]]; then
|
||||
opt="$opt -p $(( 100 - size ))"
|
||||
else
|
||||
opt="$opt -p $size"
|
||||
fi
|
||||
else
|
||||
if [[ -n "$swap" ]]; then
|
||||
if [[ "$arg" =~ ^.l ]]; then
|
||||
max=$columns
|
||||
else
|
||||
max=$lines
|
||||
fi
|
||||
size=$(( max - size ))
|
||||
[[ $size -lt 0 ]] && size=0
|
||||
opt="$opt -l $size"
|
||||
else
|
||||
opt="$opt -l $size"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
--)
|
||||
# "--" can be used to separate fzf-tmux options from fzf options to
|
||||
# avoid conflicts
|
||||
skip=1
|
||||
tmux_args=("${args[@]}")
|
||||
args=()
|
||||
continue
|
||||
;;
|
||||
*)
|
||||
args+=("$arg")
|
||||
;;
|
||||
esac
|
||||
[[ -n "$skip" ]] && args+=("$arg")
|
||||
done
|
||||
|
||||
if [[ -z "$TMUX" ]]; then
|
||||
"$fzf" "${args[@]}"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# --height option is not allowed
|
||||
args=("${args[@]}" "--no-height")
|
||||
|
||||
# Handle zoomed tmux pane without popup options by moving it to a temp window
|
||||
if [[ ! "$opt" =~ "-K -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
|
||||
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"
|
||||
tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') )
|
||||
cleanup() {
|
||||
\rm -f $argsf $fifo1 $fifo2 $fifo3
|
||||
|
||||
# Restore tmux window options
|
||||
if [[ "${#tmux_win_opts[@]}" -gt 0 ]]; then
|
||||
eval "tmux ${tmux_win_opts[*]}"
|
||||
fi
|
||||
|
||||
# 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 \; \
|
||||
kill-window -t $tmp_window \; \
|
||||
resize-pane -Z
|
||||
fi
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
trap - EXIT
|
||||
exit 130
|
||||
fi
|
||||
}
|
||||
trap 'cleanup 1' SIGUSR1
|
||||
trap 'cleanup' EXIT
|
||||
|
||||
envs="export TERM=$TERM "
|
||||
[[ "$opt" =~ "-K -E" ]] && FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||
[[ -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")"
|
||||
echo "$envs;" > "$argsf"
|
||||
|
||||
# Build arguments to fzf
|
||||
opts=$(printf "%q " "${args[@]}")
|
||||
|
||||
pppid=$$
|
||||
echo -n "trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM;" >> $argsf
|
||||
close="; trap - EXIT SIGINT SIGTERM $close"
|
||||
|
||||
export TMUX=$(cut -d , -f 1,2 <<< "$TMUX")
|
||||
mkfifo -m o+w $fifo2
|
||||
if [[ "$opt" =~ "-K -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
|
||||
tmux popup -d "$PWD" "${tmux_args[@]}" $opt -R "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
|
||||
else
|
||||
mkfifo $fifo1
|
||||
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf
|
||||
cat <&0 > $fifo1 &
|
||||
fi
|
||||
tmux set-window-option synchronize-panes off \;\
|
||||
set-window-option remain-on-exit off \;\
|
||||
split-window -c "$PWD" $opt "${tmux_args[@]}" "bash -c 'exec -a fzf bash $argsf'" $swap \
|
||||
> /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; }
|
||||
cat $fifo2
|
||||
exit "$(cat $fifo3)"
|
||||
464
doc/fzf.txt
Normal file
464
doc/fzf.txt
Normal file
@@ -0,0 +1,464 @@
|
||||
fzf.txt fzf Last change: October 18 2020
|
||||
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
|
||||
==============================================================================
|
||||
|
||||
FZF Vim integration
|
||||
Installation
|
||||
Summary
|
||||
:FZF[!]
|
||||
Configuration
|
||||
Examples
|
||||
Explanation of g:fzf_colors
|
||||
fzf#run
|
||||
fzf#wrap
|
||||
Global options supported by fzf#wrap
|
||||
Tips
|
||||
fzf inside terminal buffer
|
||||
Starting fzf in a popup window
|
||||
Hide statusline
|
||||
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 git
|
||||
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 git
|
||||
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])`
|
||||
- Starts fzf inside Vim with the given spec
|
||||
- `: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'})`
|
||||
- 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]`
|
||||
- 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}{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.
|
||||
|
||||
{2} https://github.com/junegunn/fzf.vim
|
||||
|
||||
|
||||
:FZF[!]
|
||||
==============================================================================
|
||||
|
||||
*:FZF*
|
||||
>
|
||||
" Look for files under current directory
|
||||
:FZF
|
||||
|
||||
" Look for files under your home directory
|
||||
:FZF ~
|
||||
|
||||
" With fzf command-line options
|
||||
:FZF --reverse --info=inline /tmp
|
||||
|
||||
" Bang version starts fzf in fullscreen mode
|
||||
:FZF!
|
||||
<
|
||||
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.
|
||||
|
||||
{3} https://github.com/kien/ctrlp.vim
|
||||
|
||||
|
||||
< Configuration >_____________________________________________________________~
|
||||
*fzf-configuration*
|
||||
|
||||
*g:fzf_action* *g:fzf_layout* *g:fzf_colors* *g:fzf_history_dir*
|
||||
|
||||
- `g:fzf_action`
|
||||
- Customizable extra key bindings for opening selected files in different
|
||||
ways
|
||||
- `g:fzf_layout`
|
||||
- Determines the size and position of fzf window
|
||||
- `g:fzf_colors`
|
||||
- Customizes fzf colors to match the current color scheme
|
||||
- `g:fzf_history_dir`
|
||||
- Enables history feature
|
||||
|
||||
|
||||
Examples~
|
||||
*fzf-examples*
|
||||
>
|
||||
" This is the default extra key bindings
|
||||
let g:fzf_action = {
|
||||
\ 'ctrl-t': 'tab split',
|
||||
\ 'ctrl-x': 'split',
|
||||
\ 'ctrl-v': 'vsplit' }
|
||||
|
||||
" 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 }'))
|
||||
copen
|
||||
cc
|
||||
endfunction
|
||||
|
||||
let g:fzf_action = {
|
||||
\ 'ctrl-q': function('s:build_quickfix_list'),
|
||||
\ 'ctrl-t': 'tab split',
|
||||
\ 'ctrl-x': 'split',
|
||||
\ 'ctrl-v': 'vsplit' }
|
||||
|
||||
" Default fzf layout
|
||||
" - Popup window
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
|
||||
" - 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' }
|
||||
|
||||
" Customize fzf colors to match your color scheme
|
||||
" - fzf#wrap translates this to a set of `--color` options
|
||||
let g:fzf_colors =
|
||||
\ { 'fg': ['fg', 'Normal'],
|
||||
\ 'bg': ['bg', 'Normal'],
|
||||
\ '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'] }
|
||||
|
||||
" Enable per-command history
|
||||
" - History files will be stored in the specified directory
|
||||
" - When set, CTRL-N and CTRL-P will be bound to 'next-history' and
|
||||
" 'previous-history' instead of 'down' and 'up'.
|
||||
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)
|
||||
`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
|
||||
`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 the setting by printing
|
||||
the result of `fzf#wrap()` function like so:
|
||||
>
|
||||
:echo fzf#wrap()
|
||||
<
|
||||
|
||||
FZF#RUN
|
||||
==============================================================================
|
||||
|
||||
*fzf#run*
|
||||
|
||||
`fzf#run()` function is the core of Vim integration. It takes a single
|
||||
dictionary argument, a spec, and starts fzf process accordingly. At the very
|
||||
least, specify `sink` option to tell what it should do with the selected
|
||||
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.
|
||||
>
|
||||
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.
|
||||
>
|
||||
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
|
||||
<
|
||||
fzf options can be specified as `options` entry in spec dictionary.
|
||||
>
|
||||
call fzf#run({'sink': 'tabedit', 'options': '--multi --reverse'})
|
||||
<
|
||||
You can also pass a layout option if you don't want fzf window to take up the
|
||||
entire screen.
|
||||
>
|
||||
" up / down / left / right / window are allowed
|
||||
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'left': '40%'})
|
||||
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'window': '30vnew'})
|
||||
<
|
||||
`source` doesn't have to be an external shell command, you can pass a Vim
|
||||
array as the source. In the next example, we pass the names of color schemes
|
||||
as the source to implement a color scheme selector.
|
||||
>
|
||||
call fzf#run({'source': map(split(globpath(&rtp, 'colors/*.vim')),
|
||||
\ 'fnamemodify(v:val, ":t:r")'),
|
||||
\ 'sink': 'colo', 'left': '25%'})
|
||||
<
|
||||
The following table summarizes the available options.
|
||||
|
||||
---------------------------+---------------+----------------------------------------------------------------------
|
||||
Option name | Type | Description ~
|
||||
---------------------------+---------------+----------------------------------------------------------------------
|
||||
`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
|
||||
`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) fzf-tmux options (e.g. `-p90%,60%` )
|
||||
`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.
|
||||
>
|
||||
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]]
|
||||
- `highlight` [string default `'Comment'`]: Highlight group for border
|
||||
- `border` [string default `rounded`]: Border style
|
||||
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right`
|
||||
|
||||
|
||||
FZF#WRAP
|
||||
==============================================================================
|
||||
|
||||
*fzf#wrap*
|
||||
|
||||
We have seen that several aspects of `:FZF` command can be configured with a
|
||||
set of global option variables; different ways to open files (`g:fzf_action`),
|
||||
window position and size (`g:fzf_layout`), color palette (`g:fzf_colors`),
|
||||
etc.
|
||||
|
||||
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)`
|
||||
- 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`
|
||||
is not defined.
|
||||
- `fullscreen` can be either `0` or `1` (default: 0).
|
||||
|
||||
`fzf#wrap` takes a spec and returns an extended version of it (also a
|
||||
dictionary) with additional options for addressing global preferences. You can
|
||||
examine the return value of it like so:
|
||||
>
|
||||
echo fzf#wrap({'source': 'ls'})
|
||||
<
|
||||
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 (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.
|
||||
>
|
||||
command! LS call fzf#run(fzf#wrap({'source': 'ls'}))
|
||||
<
|
||||
Type `:LS` and see how it works.
|
||||
|
||||
We would like to make `:LS!` (bang version) open fzf in fullscreen, just like
|
||||
`:FZF!`. Add `-bang` to command definition, and use <bang> value to set the
|
||||
last `fullscreen` argument of `fzf#wrap` (see :help <bang>).
|
||||
>
|
||||
" On :LS!, <bang> evaluates to '!', and '!0' becomes 1
|
||||
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.
|
||||
>
|
||||
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
|
||||
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
|
||||
\ 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 `sink*`) 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*
|
||||
==============================================================================
|
||||
|
||||
|
||||
< 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%'}`
|
||||
|
||||
|
||||
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]]
|
||||
" - highlight [string default 'Comment']: Highlight group for border
|
||||
" - 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 fzf-tmux options in `tmux` key.
|
||||
>
|
||||
" See `man fzf-tmux` for available options
|
||||
if exists('$TMUX')
|
||||
let g:fzf_layout = { 'tmux': '-p90%,60%' }
|
||||
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
|
||||
the window.
|
||||
|
||||
For example, if you use a non-popup layout (e.g. `{'down': '40%'}`) on Neovim,
|
||||
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
|
||||
<
|
||||
|
||||
LICENSE *fzf-license*
|
||||
==============================================================================
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2020 Junegunn Choi
|
||||
|
||||
==============================================================================
|
||||
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:
|
||||
858
fzf
858
fzf
@@ -1,858 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
# encoding: utf-8
|
||||
#
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ Fuzzy finder for your shell
|
||||
#
|
||||
# URL: https://github.com/junegunn/fzf
|
||||
# Author: Junegunn Choi
|
||||
# License: MIT
|
||||
# Last update: November 17, 2013
|
||||
#
|
||||
# Copyright (c) 2013 Junegunn Choi
|
||||
#
|
||||
# MIT License
|
||||
#
|
||||
# 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.
|
||||
|
||||
require 'thread'
|
||||
require 'curses'
|
||||
require 'set'
|
||||
|
||||
class FZF
|
||||
C = Curses
|
||||
attr_reader :rxflag, :sort, :color, :multi
|
||||
|
||||
class AtomicVar
|
||||
def initialize value
|
||||
@value = value
|
||||
@mutex = Mutex.new
|
||||
end
|
||||
|
||||
def get
|
||||
@mutex.synchronize { @value }
|
||||
end
|
||||
|
||||
def set value = nil
|
||||
if block_given?
|
||||
@mutex.synchronize { @value = yield @value }
|
||||
else
|
||||
@mutex.synchronize { @value = value }
|
||||
end
|
||||
end
|
||||
|
||||
def method_missing sym, *args, &blk
|
||||
@mutex.synchronize { @value.send(sym, *args, &blk) }
|
||||
end
|
||||
end
|
||||
|
||||
def initialize argv, source = $stdin
|
||||
usage 0 unless (%w[--help -h] & argv).empty?
|
||||
@rxflag = argv.delete('+i') ? 0 : Regexp::IGNORECASE
|
||||
@sort = %w[+s --no-sort].map { |e| argv.delete e }.compact.empty? ?
|
||||
ENV.fetch('FZF_DEFAULT_SORT', 1000).to_i : nil
|
||||
@color = %w[+c --no-color].map { |e| argv.delete e }.compact.empty?
|
||||
@multi = !%w[-m --multi].map { |e| argv.delete e }.compact.empty?
|
||||
@xmode = !%w[-x --extended].map { |e| argv.delete e }.compact.empty?
|
||||
rest = argv.join ' '
|
||||
if sort = rest.match(/(-s|--sort=?) ?([0-9]+)/)
|
||||
usage 1 unless @sort
|
||||
@sort = sort[2].to_i
|
||||
rest = rest.delete sort[0]
|
||||
end
|
||||
usage 1 unless rest.empty?
|
||||
|
||||
@source = source
|
||||
@mtx = Mutex.new
|
||||
@cv = ConditionVariable.new
|
||||
@events = {}
|
||||
@new = []
|
||||
@queue = Queue.new
|
||||
@cursor_x = AtomicVar.new(0)
|
||||
@query = AtomicVar.new('')
|
||||
@matches = AtomicVar.new([])
|
||||
@count = AtomicVar.new(0)
|
||||
@vcursor = AtomicVar.new(0)
|
||||
@vcursors = AtomicVar.new(Set.new)
|
||||
@spinner = AtomicVar.new('-\|/-\|/'.split(//))
|
||||
@selects = AtomicVar.new({}) # ordered >= 1.9
|
||||
@main = Thread.current
|
||||
@stdout = $stdout.clone
|
||||
@plcount = 0
|
||||
end
|
||||
|
||||
def start
|
||||
$stdout.reopen($stderr)
|
||||
|
||||
init_screen
|
||||
start_reader
|
||||
start_renderer
|
||||
start_search
|
||||
start_loop
|
||||
end
|
||||
|
||||
def usage x
|
||||
$stderr.puts %[usage: fzf [options]
|
||||
|
||||
-m, --multi Enable multi-select
|
||||
-x, --extended Extended-search mode
|
||||
-s, --sort=MAX Maximum number of matched items to sort. Default: 1000
|
||||
+s, --no-sort Do not sort the result. Keep the sequence unchanged.
|
||||
+i Case-sensitive match
|
||||
+c, --no-color Disable colors]
|
||||
exit x
|
||||
end
|
||||
|
||||
case RUBY_PLATFORM
|
||||
when /darwin/
|
||||
module UConv
|
||||
CHOSUNG = 0x1100
|
||||
JUNGSUNG = 0x1161
|
||||
JONGSUNG = 0x11A7
|
||||
CHOSUNGS = 19
|
||||
JUNGSUNGS = 21
|
||||
JONGSUNGS = 28
|
||||
JJCOUNT = JUNGSUNGS * JONGSUNGS
|
||||
NFC_BEGIN = 0xAC00
|
||||
NFC_END = NFC_BEGIN + CHOSUNGS * JUNGSUNGS * JONGSUNGS
|
||||
|
||||
def self.nfd str
|
||||
str.split(//).map do |c|
|
||||
cp = c.ord
|
||||
if cp >= NFC_BEGIN && cp < NFC_END
|
||||
chr = ''
|
||||
idx = cp - NFC_BEGIN
|
||||
cho = CHOSUNG + idx / JJCOUNT
|
||||
jung = JUNGSUNG + (idx % JJCOUNT) / JONGSUNGS
|
||||
jong = JONGSUNG + idx % JONGSUNGS
|
||||
chr << cho << jung
|
||||
chr << jong if jong != JONGSUNG
|
||||
chr
|
||||
else
|
||||
c
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.to_nfc arr
|
||||
[NFC_BEGIN + arr[0] * JJCOUNT +
|
||||
(arr[1] || 0) * JONGSUNGS +
|
||||
(arr[2] || 0)].pack('U*')
|
||||
end
|
||||
|
||||
if String.method_defined?(:each_char)
|
||||
def self.split str
|
||||
str.each_char.to_a
|
||||
end
|
||||
else
|
||||
def self.split str
|
||||
str.split('')
|
||||
end
|
||||
end
|
||||
|
||||
def self.nfc str, offsets = []
|
||||
ret = ''
|
||||
omap = []
|
||||
pend = []
|
||||
split(str).each_with_index do |c, idx|
|
||||
cp =
|
||||
begin
|
||||
c.ord
|
||||
rescue Exception
|
||||
next
|
||||
end
|
||||
omap << ret.length
|
||||
unless pend.empty?
|
||||
if cp >= JUNGSUNG && cp < JUNGSUNG + JUNGSUNGS
|
||||
pend << cp - JUNGSUNG
|
||||
next
|
||||
elsif cp >= JONGSUNG && cp < JONGSUNG + JONGSUNGS
|
||||
pend << cp - JONGSUNG
|
||||
next
|
||||
else
|
||||
omap[-1] = omap[-1] + 1
|
||||
ret << to_nfc(pend)
|
||||
pend.clear
|
||||
end
|
||||
end
|
||||
if cp >= CHOSUNG && cp < CHOSUNG + CHOSUNGS
|
||||
pend << cp - CHOSUNG
|
||||
else
|
||||
ret << c
|
||||
end
|
||||
end
|
||||
ret << to_nfc(pend) unless pend.empty?
|
||||
return [ret,
|
||||
offsets.map { |pair|
|
||||
b, e = pair
|
||||
[omap[b] || 0, omap[e] || ((omap.last || 0) + 1)] }]
|
||||
end
|
||||
end
|
||||
|
||||
def convert_item item
|
||||
UConv.nfc(*item)
|
||||
end
|
||||
|
||||
class Matcher
|
||||
def query_chars q
|
||||
UConv.nfd(q)
|
||||
end
|
||||
|
||||
def sanitize q
|
||||
UConv.nfd(q).join
|
||||
end
|
||||
end
|
||||
else
|
||||
def convert_item item
|
||||
item
|
||||
end
|
||||
|
||||
class Matcher
|
||||
def query_chars q
|
||||
q.split(//)
|
||||
end
|
||||
|
||||
def sanitize q
|
||||
q
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def emit event
|
||||
@mtx.synchronize do
|
||||
@events[event] = yield
|
||||
@cv.broadcast
|
||||
end
|
||||
end
|
||||
|
||||
def max_items; C.lines - 2; end
|
||||
def cursor_y; C.lines - 1; end
|
||||
def cprint str, col
|
||||
C.attron(col) do
|
||||
addstr_safe str
|
||||
end if str
|
||||
end
|
||||
def addstr_safe str
|
||||
C.addstr str.gsub("\0", '')
|
||||
end
|
||||
|
||||
def print_input
|
||||
C.setpos cursor_y, 0
|
||||
C.clrtoeol
|
||||
cprint '> ', color(:prompt, true)
|
||||
C.attron(C::A_BOLD) do
|
||||
C.addstr @query.get
|
||||
end
|
||||
end
|
||||
|
||||
def print_info msg = nil
|
||||
C.setpos cursor_y - 1, 0
|
||||
C.clrtoeol
|
||||
prefix =
|
||||
if spinner = @spinner.first
|
||||
cprint spinner, color(:spinner, true)
|
||||
' '
|
||||
else
|
||||
' '
|
||||
end
|
||||
C.attron color(:info, false) do
|
||||
C.addstr "#{prefix}#{@matches.length}/#{@count.get}"
|
||||
if (selected = @selects.length) > 0
|
||||
C.addstr " (#{selected})"
|
||||
end
|
||||
C.addstr msg if msg
|
||||
end
|
||||
end
|
||||
|
||||
def refresh
|
||||
C.setpos cursor_y, 2 + width(@query[0, @cursor_x.get])
|
||||
C.refresh
|
||||
end
|
||||
|
||||
def ctrl char
|
||||
char.to_s.ord - 'a'.ord + 1
|
||||
end
|
||||
|
||||
def format line, limit, offsets
|
||||
offsets ||= []
|
||||
maxe = offsets.map { |e| e.last }.max || 0
|
||||
|
||||
# Overflow
|
||||
if width(line) > limit
|
||||
ewidth = width(line[0...maxe])
|
||||
# Stri..
|
||||
if ewidth <= limit - 2
|
||||
line, _ = trim line, limit - 2, false
|
||||
line << '..'
|
||||
# ..ring
|
||||
else
|
||||
# ..ri..
|
||||
line = line[0...maxe] + '..' if ewidth < width(line) - 2
|
||||
line, diff = trim line, limit - 2, true
|
||||
offsets = offsets.map { |pair|
|
||||
b, e = pair
|
||||
b += 2 - diff
|
||||
e += 2 - diff
|
||||
b = [2, b].max
|
||||
[b, e]
|
||||
}
|
||||
line = '..' + line
|
||||
end
|
||||
end
|
||||
|
||||
tokens = []
|
||||
index = 0
|
||||
offsets.select { |pair| pair.first < pair.last }.
|
||||
sort_by { |pair| pair }.each do |pair|
|
||||
b, e = pair.map { |x| [index, x].max }
|
||||
tokens << [line[index...b], false]
|
||||
tokens << [line[b...e], true]
|
||||
index = e
|
||||
end
|
||||
tokens << [line[index..-1], false]
|
||||
tokens.reject { |pair| pair.first.empty? }
|
||||
end
|
||||
|
||||
def print_item row, tokens, chosen, selected
|
||||
# Cursor
|
||||
C.setpos row, 0
|
||||
C.clrtoeol
|
||||
cprint chosen ? '>' : ' ', color(:cursor, true)
|
||||
cprint selected ? '>' : ' ',
|
||||
chosen ? color(:chosen) : (selected ? color(:selected, true) : 0)
|
||||
|
||||
# Highlighted item
|
||||
C.attron color(:chosen, true) if chosen
|
||||
tokens.each do |pair|
|
||||
token, highlighted = pair
|
||||
|
||||
if highlighted
|
||||
cprint token, color(chosen ? :match! : :match, chosen)
|
||||
C.attron color(:chosen, true) if chosen
|
||||
else
|
||||
addstr_safe token
|
||||
end
|
||||
end
|
||||
C.attroff color(:chosen, true) if chosen
|
||||
end
|
||||
|
||||
def sort_by_rank list
|
||||
list.sort_by { |tuple|
|
||||
line, offsets = tuple
|
||||
matchlen = 0
|
||||
pe = nil
|
||||
offsets.sort.each do |pair|
|
||||
b, e = pair
|
||||
b = pe if pe && pe > b
|
||||
pe = e
|
||||
matchlen += e - b
|
||||
end
|
||||
[matchlen, line.length, line]
|
||||
}
|
||||
end
|
||||
|
||||
if RUBY_VERSION.split('.').map { |e| e.rjust(3, '0') }.join > '001009'
|
||||
@@wrx = Regexp.new '\p{Han}|\p{Katakana}|\p{Hiragana}|\p{Hangul}'
|
||||
def width str
|
||||
str.gsub(@@wrx, ' ').length
|
||||
end
|
||||
|
||||
def trim str, len, left
|
||||
width = width str
|
||||
diff = 0
|
||||
while width > len
|
||||
width -= (left ? str[0, 1] : str[-1, 1]) =~ @@wrx ? 2 : 1
|
||||
str = left ? str[1..-1] : str[0...-1]
|
||||
diff += 1
|
||||
end
|
||||
[str, diff]
|
||||
end
|
||||
else
|
||||
def width str
|
||||
str.length
|
||||
end
|
||||
|
||||
def trim str, len, left
|
||||
diff = str.length - len
|
||||
if diff > 0
|
||||
[left ? str[diff..-1] : str[0...-diff], diff]
|
||||
else
|
||||
[str, 0]
|
||||
end
|
||||
end
|
||||
|
||||
class ::String
|
||||
def ord
|
||||
self.unpack('c').first
|
||||
end
|
||||
end
|
||||
|
||||
class ::Fixnum
|
||||
def ord
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def init_screen
|
||||
C.init_screen
|
||||
C.start_color
|
||||
dbg =
|
||||
if C.respond_to?(:use_default_colors)
|
||||
C.use_default_colors
|
||||
-1
|
||||
else
|
||||
C::COLOR_BLACK
|
||||
end
|
||||
C.raw
|
||||
C.noecho
|
||||
|
||||
if @color
|
||||
if C.can_change_color?
|
||||
C.init_pair 1, 110, dbg
|
||||
C.init_pair 2, 108, dbg
|
||||
C.init_pair 3, 254, 236
|
||||
C.init_pair 4, 151, 236
|
||||
C.init_pair 5, 148, dbg
|
||||
C.init_pair 6, 144, dbg
|
||||
C.init_pair 7, 161, 236
|
||||
C.init_pair 8, 168, 236
|
||||
else
|
||||
C.init_pair 1, C::COLOR_BLUE, dbg
|
||||
C.init_pair 2, C::COLOR_GREEN, dbg
|
||||
C.init_pair 3, C::COLOR_YELLOW, C::COLOR_BLACK
|
||||
C.init_pair 4, C::COLOR_GREEN, C::COLOR_BLACK
|
||||
C.init_pair 5, C::COLOR_GREEN, dbg
|
||||
C.init_pair 6, C::COLOR_WHITE, dbg
|
||||
C.init_pair 7, C::COLOR_RED, C::COLOR_BLACK
|
||||
C.init_pair 8, C::COLOR_MAGENTA, C::COLOR_BLACK
|
||||
end
|
||||
|
||||
def self.color sym, bold = false
|
||||
C.color_pair([:prompt, :match, :chosen, :match!,
|
||||
:spinner, :info, :cursor, :selected].index(sym) + 1) |
|
||||
(bold ? C::A_BOLD : 0)
|
||||
end
|
||||
else
|
||||
def self.color sym, bold = false
|
||||
case sym
|
||||
when :chosen
|
||||
bold ? C::A_REVERSE : 0
|
||||
when :match
|
||||
C::A_UNDERLINE
|
||||
when :match!
|
||||
C::A_REVERSE | C::A_UNDERLINE
|
||||
else
|
||||
0
|
||||
end | (bold ? C::A_BOLD : 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def start_reader
|
||||
stream =
|
||||
if @source.tty?
|
||||
if default_command = ENV['FZF_DEFAULT_COMMAND']
|
||||
IO.popen(default_command)
|
||||
elsif !`which find`.empty?
|
||||
IO.popen("find * -path '*/\\.*' -prune -o -type f -print -o -type l -print 2> /dev/null")
|
||||
else
|
||||
exit 1
|
||||
end
|
||||
else
|
||||
@source
|
||||
end
|
||||
|
||||
Thread.new do
|
||||
while line = stream.gets
|
||||
emit(:new) { @new << line.chomp }
|
||||
end
|
||||
emit(:loaded) { true }
|
||||
@spinner.clear
|
||||
end
|
||||
end
|
||||
|
||||
def start_search
|
||||
matcher = (@xmode ? ExtendedFuzzyMatcher : FuzzyMatcher).new @rxflag
|
||||
searcher = Thread.new {
|
||||
lists = []
|
||||
events = {}
|
||||
fcache = {}
|
||||
q = ''
|
||||
delay = -5
|
||||
|
||||
begin
|
||||
while true
|
||||
@mtx.synchronize do
|
||||
while true
|
||||
events.merge! @events
|
||||
|
||||
if @events.empty? # No new events
|
||||
@cv.wait @mtx
|
||||
next
|
||||
end
|
||||
@events.clear
|
||||
break
|
||||
end
|
||||
|
||||
if events[:new]
|
||||
lists << @new
|
||||
@count.set { |c| c + @new.length }
|
||||
@spinner.set { |spinner|
|
||||
if e = spinner.shift
|
||||
spinner.push e
|
||||
end; spinner
|
||||
}
|
||||
@new = []
|
||||
fcache.clear
|
||||
end
|
||||
end#mtx
|
||||
|
||||
new_search = events[:key] || events.delete(:new)
|
||||
user_input = events[:key]
|
||||
progress = 0
|
||||
started_at = Time.now
|
||||
|
||||
if new_search && !lists.empty?
|
||||
q, cx = events.delete(:key) || [q, 0]
|
||||
empty = matcher.empty?(q)
|
||||
matches = fcache[q] ||=
|
||||
begin
|
||||
found = []
|
||||
skip = false
|
||||
cnt = 0
|
||||
lists.each do |list|
|
||||
cnt += list.length
|
||||
skip = @mtx.synchronize { @events[:key] }
|
||||
break if skip
|
||||
|
||||
if !empty && (progress = 100 * cnt / @count.get) < 100 && Time.now - started_at > 0.5
|
||||
render { print_info " (#{progress}%)" }
|
||||
end
|
||||
|
||||
found.concat(q.empty? ? list :
|
||||
matcher.match(list, q, q[0, cx], q[cx..-1]))
|
||||
end
|
||||
next if skip
|
||||
@sort ? found : found.reverse
|
||||
end
|
||||
|
||||
if !empty && @sort && matches.length <= @sort
|
||||
matches = sort_by_rank(matches)
|
||||
end
|
||||
|
||||
# Atomic update
|
||||
@matches.set matches
|
||||
end#new_search
|
||||
|
||||
# This small delay reduces the number of partial lists
|
||||
sleep((delay = [20, delay + 5].min) * 0.01) unless user_input
|
||||
|
||||
update_list new_search
|
||||
end#while
|
||||
rescue Exception => e
|
||||
@main.raise e
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def pick
|
||||
items = @matches[0, max_items]
|
||||
curr = [0, [@vcursor.get, items.length - 1].min].max
|
||||
[*items.fetch(curr, [])][0]
|
||||
end
|
||||
|
||||
def update_list wipe
|
||||
render do
|
||||
items = @matches[0, max_items]
|
||||
|
||||
# Wipe
|
||||
if items.length < @plcount
|
||||
@plcount.downto(items.length) do |idx|
|
||||
C.setpos cursor_y - idx - 2, 0
|
||||
C.clrtoeol
|
||||
end
|
||||
end
|
||||
@plcount = items.length
|
||||
|
||||
maxc = C.cols - 3
|
||||
vcursor = @vcursor.set { |v| [0, [v, items.length - 1].min].max }
|
||||
cleanse = Set[vcursor]
|
||||
@vcursors.set { |vs|
|
||||
cleanse.merge vs
|
||||
Set.new
|
||||
}
|
||||
items.each_with_index do |item, idx|
|
||||
next unless wipe || cleanse.include?(idx)
|
||||
row = cursor_y - idx - 2
|
||||
chosen = idx == vcursor
|
||||
selected = @selects.include?([*item][0])
|
||||
line, offsets = convert_item item
|
||||
tokens = format line, maxc, offsets
|
||||
print_item row, tokens, chosen, selected
|
||||
end
|
||||
print_info
|
||||
end
|
||||
end
|
||||
|
||||
def start_renderer
|
||||
Thread.new do
|
||||
begin
|
||||
while blk = @queue.shift
|
||||
blk.call
|
||||
refresh
|
||||
end
|
||||
rescue Exception => e
|
||||
@main.raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def render &blk
|
||||
@queue.push blk
|
||||
nil
|
||||
end
|
||||
|
||||
def start_loop
|
||||
got = nil
|
||||
begin
|
||||
tty = IO.open(IO.sysopen('/dev/tty'), 'r')
|
||||
input = ''
|
||||
cursor = 0
|
||||
backword = proc {
|
||||
cursor = (input[0, cursor].rindex(/\s\S/) || -1) + 1
|
||||
}
|
||||
actions = {
|
||||
:nop => proc { nil },
|
||||
ctrl(:c) => proc { exit 1 },
|
||||
ctrl(:d) => proc { exit 1 if input.empty? },
|
||||
ctrl(:m) => proc {
|
||||
got = pick
|
||||
exit 0
|
||||
},
|
||||
ctrl(:u) => proc { input = input[cursor..-1]; cursor = 0 },
|
||||
ctrl(:a) => proc { cursor = 0; nil },
|
||||
ctrl(:e) => proc { cursor = input.length; nil },
|
||||
ctrl(:j) => proc { @vcursor.set { |v| @vcursors << v; v - 1 }; update_list false },
|
||||
ctrl(:k) => proc { @vcursor.set { |v| @vcursors << v; v + 1 }; update_list false },
|
||||
ctrl(:w) => proc {
|
||||
pcursor = cursor
|
||||
backword.call
|
||||
input = input[0...cursor] + input[pcursor..-1]
|
||||
},
|
||||
127 => proc { input[cursor -= 1] = '' if cursor > 0 },
|
||||
9 => proc { |o|
|
||||
if @multi && sel = pick
|
||||
if @selects.has_key? sel
|
||||
@selects.delete sel
|
||||
else
|
||||
@selects[sel] = 1
|
||||
end
|
||||
@vcursor.set { |v|
|
||||
@vcursors << v
|
||||
v + (o == :stab ? 1 : -1)
|
||||
}
|
||||
update_list false
|
||||
end
|
||||
},
|
||||
:left => proc { cursor = [0, cursor - 1].max; nil },
|
||||
:right => proc { cursor = [input.length, cursor + 1].min; nil },
|
||||
:alt_b => proc { backword.call; nil },
|
||||
:alt_f => proc {
|
||||
cursor += (input[cursor..-1].index(/(\S\s)|(.$)/) || -1) + 1
|
||||
nil
|
||||
},
|
||||
}
|
||||
actions[ctrl(:b)] = actions[:left]
|
||||
actions[ctrl(:f)] = actions[:right]
|
||||
actions[ctrl(:h)] = actions[127]
|
||||
actions[ctrl(:n)] = actions[ctrl(:j)]
|
||||
actions[ctrl(:p)] = actions[ctrl(:k)]
|
||||
actions[:stab] = actions[9]
|
||||
|
||||
while true
|
||||
@cursor_x.set cursor
|
||||
render { print_input }
|
||||
|
||||
ord = tty.getc.ord
|
||||
ord =
|
||||
case ord = tty.getc.ord
|
||||
when 91
|
||||
case tty.getc.ord
|
||||
when 68 then :left
|
||||
when 67 then :right
|
||||
when 66 then ctrl(:j)
|
||||
when 65 then ctrl(:k)
|
||||
when 90 then :stab
|
||||
else :nop
|
||||
end
|
||||
when 'b'.ord
|
||||
:alt_b
|
||||
when 'f'.ord
|
||||
:alt_f
|
||||
else
|
||||
ord
|
||||
end if ord == 27
|
||||
|
||||
upd = actions.fetch(ord, proc { |ord|
|
||||
char = [ord].pack('U*')
|
||||
if char =~ /[[:print:]]/
|
||||
input.insert cursor, char
|
||||
cursor += 1
|
||||
end
|
||||
}).call(ord)
|
||||
|
||||
# Dispatch key event
|
||||
emit(:key) { [@query.set(input.dup), cursor] } if upd
|
||||
end
|
||||
ensure
|
||||
C.close_screen
|
||||
if got
|
||||
if @selects.empty?
|
||||
@stdout.puts got
|
||||
else
|
||||
@selects.each do |sel, _|
|
||||
@stdout.puts sel
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class FuzzyMatcher < Matcher
|
||||
attr_reader :caches, :rxflag
|
||||
|
||||
def initialize rxflag
|
||||
@caches = Hash.new { |h, k| h[k] = {} }
|
||||
@regexp = {}
|
||||
@rxflag = rxflag
|
||||
end
|
||||
|
||||
def empty? q
|
||||
q.empty?
|
||||
end
|
||||
|
||||
def fuzzy_regex q
|
||||
@regexp[q] ||= begin
|
||||
q = q.downcase if @rxflag != 0
|
||||
Regexp.new(query_chars(q).inject('') { |sum, e|
|
||||
e = Regexp.escape e
|
||||
sum << (e.length > 1 ? "(?:#{e}).*?" : # FIXME: not equivalent
|
||||
"#{e}[^#{e}]*?")
|
||||
}, @rxflag)
|
||||
end
|
||||
end
|
||||
|
||||
def match list, q, prefix, suffix
|
||||
regexp = fuzzy_regex q
|
||||
|
||||
cache = @caches[list.object_id]
|
||||
prefix_cache = nil
|
||||
(prefix.length - 1).downto(1) do |len|
|
||||
break if prefix_cache = cache[prefix[0, len]]
|
||||
end
|
||||
|
||||
suffix_cache = nil
|
||||
0.upto(suffix.length - 1) do |idx|
|
||||
break if suffix_cache = cache[suffix[idx..-1]]
|
||||
end unless suffix.empty?
|
||||
|
||||
partial_cache = [prefix_cache,
|
||||
suffix_cache].compact.sort_by { |e| e.length }.first
|
||||
cache[q] ||= (partial_cache ?
|
||||
partial_cache.map { |e| e.first } : list).map { |line|
|
||||
# Ignore errors: e.g. invalid byte sequence in UTF-8
|
||||
md = line.match(regexp) rescue nil
|
||||
md && [line, [md.offset(0)]]
|
||||
}.compact
|
||||
end
|
||||
end
|
||||
|
||||
class ExtendedFuzzyMatcher < FuzzyMatcher
|
||||
def initialize rxflag
|
||||
super
|
||||
@regexps = {}
|
||||
end
|
||||
|
||||
def empty? q
|
||||
parse(q).empty?
|
||||
end
|
||||
|
||||
def parse q
|
||||
q = q.strip
|
||||
@regexps[q] ||= q.split(/\s+/).map { |w|
|
||||
invert =
|
||||
if w =~ /^!/
|
||||
w = w[1..-1]
|
||||
true
|
||||
end
|
||||
|
||||
[ @regexp[w] ||=
|
||||
case w
|
||||
when ''
|
||||
nil
|
||||
when /^'/
|
||||
w.length > 1 ?
|
||||
Regexp.new(sanitize(Regexp.escape(w[1..-1])), rxflag) : nil
|
||||
when /^\^/
|
||||
w.length > 1 ?
|
||||
Regexp.new('^' << sanitize(Regexp.escape(w[1..-1])), rxflag) : nil
|
||||
when /\$$/
|
||||
w.length > 1 ?
|
||||
Regexp.new(sanitize(Regexp.escape(w[0..-2])) << '$', rxflag) : nil
|
||||
else
|
||||
fuzzy_regex w
|
||||
end, invert ]
|
||||
}.select { |pair| pair.first }
|
||||
end
|
||||
|
||||
def match list, q, prefix, suffix
|
||||
regexps = parse q
|
||||
# Look for prefix cache
|
||||
cache = @caches[list.object_id]
|
||||
prefix = prefix.strip.sub(/\$\S*$/, '').sub(/(^|\s)!\S*$/, '')
|
||||
prefix_cache = nil
|
||||
(prefix.length - 1).downto(1) do |len|
|
||||
break if prefix_cache = cache[Set[@regexps[prefix[0, len]]]]
|
||||
end
|
||||
|
||||
cache[Set[regexps]] ||= (prefix_cache ?
|
||||
prefix_cache.map { |e| e.first } :
|
||||
list).map { |line|
|
||||
offsets = []
|
||||
regexps.all? { |pair|
|
||||
regexp, invert = pair
|
||||
md = line.match(regexp) rescue nil
|
||||
if md && !invert
|
||||
offsets << md.offset(0)
|
||||
elsif !md && invert
|
||||
true
|
||||
end
|
||||
} && [line, offsets]
|
||||
}.select { |e| e }
|
||||
end
|
||||
end
|
||||
end#FZF
|
||||
|
||||
FZF.new(ARGV, $stdin).start if ENV.fetch('FZF_EXECUTABLE', '1') == '1'
|
||||
|
||||
15
fzf.gemspec
15
fzf.gemspec
@@ -1,15 +0,0 @@
|
||||
# coding: utf-8
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = 'fzf'
|
||||
spec.version = '0.4.2'
|
||||
spec.authors = ['Junegunn Choi']
|
||||
spec.email = ['junegunn.c@gmail.com']
|
||||
spec.description = %q{Fuzzy finder for your shell}
|
||||
spec.summary = %q{Fuzzy finder for your shell}
|
||||
spec.homepage = 'https://github.com/junegunn/fzf'
|
||||
spec.license = 'MIT'
|
||||
|
||||
spec.bindir = '.'
|
||||
spec.files = %w[fzf.gemspec]
|
||||
spec.executables = 'fzf'
|
||||
end
|
||||
15
go.mod
Normal file
15
go.mod
Normal file
@@ -0,0 +1,15 @@
|
||||
module github.com/junegunn/fzf
|
||||
|
||||
require (
|
||||
github.com/gdamore/tcell v1.4.0
|
||||
github.com/mattn/go-isatty v0.0.12
|
||||
github.com/mattn/go-runewidth v0.0.9
|
||||
github.com/mattn/go-shellwords v1.0.10
|
||||
github.com/saracen/walker v0.1.1
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect
|
||||
golang.org/x/sys v0.0.0-20201026173827-119d4633e4d1
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
)
|
||||
|
||||
go 1.13
|
||||
35
go.sum
Normal file
35
go.sum
Normal file
@@ -0,0 +1,35 @@
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
||||
github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
|
||||
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/saracen/walker v0.1.1 h1:Ou2QIKTWqo0QxhtuHVmtObbmhjMCEUyJ82xp0uV+MGI=
|
||||
github.com/saracen/walker v0.1.1/go.mod h1:0oKYMsKVhSJ+ful4p/XbjvXbMgLEkLITZaxozsl4CGE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201026173827-119d4633e4d1 h1:/DtoiOYKoQCcIFXQjz07RnWNPRCbqmSXSpgEzhC9ZHM=
|
||||
golang.org/x/sys v0.0.0-20201026173827-119d4633e4d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
384
install
384
install
@@ -1,7 +1,381 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cd `dirname $BASH_SOURCE`
|
||||
mkdir -p ~/bin
|
||||
ln -sf `pwd`/fzf ~/bin/fzf
|
||||
chmod +x ~/bin/fzf
|
||||
set -u
|
||||
|
||||
version=0.24.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
shells="bash zsh fish"
|
||||
prefix='~/.fzf'
|
||||
prefix_expand=~/.fzf
|
||||
fish_dir=${XDG_CONFIG_HOME:-$HOME/.config}/fish
|
||||
|
||||
help() {
|
||||
cat << EOF
|
||||
usage: $0 [OPTIONS]
|
||||
|
||||
--help Show this message
|
||||
--bin Download fzf binary only; Do not generate ~/.fzf.{bash,zsh}
|
||||
--all Download fzf binary and update configuration files
|
||||
to enable key bindings and fuzzy completion
|
||||
--xdg Generate files under \$XDG_CONFIG_HOME/fzf
|
||||
--[no-]key-bindings Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C)
|
||||
--[no-]completion Enable/disable fuzzy completion (bash & zsh)
|
||||
--[no-]update-rc Whether or not to update shell configuration files
|
||||
|
||||
--no-bash Do not set up bash configuration
|
||||
--no-zsh Do not set up zsh configuration
|
||||
--no-fish Do not set up fish configuration
|
||||
EOF
|
||||
}
|
||||
|
||||
for opt in "$@"; do
|
||||
case $opt in
|
||||
--help)
|
||||
help
|
||||
exit 0
|
||||
;;
|
||||
--all)
|
||||
auto_completion=1
|
||||
key_bindings=1
|
||||
update_config=1
|
||||
;;
|
||||
--xdg)
|
||||
prefix='"${XDG_CONFIG_HOME:-$HOME/.config}"/fzf/fzf'
|
||||
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 ;;
|
||||
--bin) ;;
|
||||
--no-bash) shells=${shells/bash/} ;;
|
||||
--no-zsh) shells=${shells/zsh/} ;;
|
||||
--no-fish) shells=${shells/fish/} ;;
|
||||
*)
|
||||
echo "unknown option: $opt"
|
||||
help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
fzf_base=$(pwd)
|
||||
fzf_base_esc=$(printf %q "$fzf_base")
|
||||
|
||||
ask() {
|
||||
while true; do
|
||||
read -p "$1 ([y]/n) " -r
|
||||
REPLY=${REPLY:-"y"}
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
return 1
|
||||
elif [[ $REPLY =~ ^[Nn]$ ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
check_binary() {
|
||||
echo -n " - Checking fzf executable ... "
|
||||
local output
|
||||
output=$("$fzf_base"/bin/fzf --version 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: $output"
|
||||
binary_error="Invalid binary"
|
||||
else
|
||||
output=${output/ */}
|
||||
if [ "$version" != "$output" ]; then
|
||||
echo "$output != $version"
|
||||
binary_error="Invalid version"
|
||||
else
|
||||
echo "$output"
|
||||
binary_error=""
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
rm -f "$fzf_base"/bin/fzf
|
||||
return 1
|
||||
}
|
||||
|
||||
link_fzf_in_path() {
|
||||
if which_fzf="$(command -v fzf)"; then
|
||||
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
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
try_curl() {
|
||||
command -v curl > /dev/null &&
|
||||
if [[ $1 =~ tar.gz$ ]]; then
|
||||
curl -fL $1 | tar -xzf -
|
||||
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 =~ tar.gz$ ]]; then
|
||||
wget -O - $1 | tar -xzf -
|
||||
else
|
||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||
fi
|
||||
}
|
||||
|
||||
download() {
|
||||
echo "Downloading bin/fzf ..."
|
||||
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"
|
||||
return
|
||||
fi
|
||||
|
||||
local url
|
||||
url=https://github.com/junegunn/fzf/releases/download/$version/${1}
|
||||
set -o pipefail
|
||||
if ! (try_curl $url || try_wget $url); then
|
||||
set +o pipefail
|
||||
binary_error="Failed to download with curl and wget"
|
||||
return
|
||||
fi
|
||||
set +o pipefail
|
||||
|
||||
if [ ! -f fzf ]; then
|
||||
binary_error="Failed to download ${1}"
|
||||
return
|
||||
fi
|
||||
|
||||
chmod +x fzf && check_binary
|
||||
}
|
||||
|
||||
# Try to download binary executable
|
||||
archi=$(uname -sm)
|
||||
binary_available=1
|
||||
binary_error=""
|
||||
case "$archi" in
|
||||
Darwin\ *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*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||
Linux\ *64) download fzf-$version-linux_amd64.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"
|
||||
if [ -n "$binary_error" ]; then
|
||||
if [ $binary_available -eq 0 ]; then
|
||||
echo "No prebuilt binary for $archi ..."
|
||||
else
|
||||
echo " - $binary_error !!!"
|
||||
fi
|
||||
if command -v go > /dev/null; then
|
||||
echo -n "Building binary (go get -u 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
|
||||
echo "OK"
|
||||
cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
|
||||
else
|
||||
echo "Failed to build binary. Installation failed."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "go executable not found. Installation failed."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ "$*" =~ "--bin" ]] && exit 0
|
||||
|
||||
for s in $shells; do
|
||||
if ! command -v "$s" > /dev/null; then
|
||||
shells=${shells/$s/}
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#shells} -lt 3 ]]; then
|
||||
echo "No shell configuration to be updated."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Auto-completion
|
||||
if [ -z "$auto_completion" ]; then
|
||||
ask "Do you want to enable fuzzy auto-completion?"
|
||||
auto_completion=$?
|
||||
fi
|
||||
|
||||
# Key-bindings
|
||||
if [ -z "$key_bindings" ]; then
|
||||
ask "Do you want to enable key bindings?"
|
||||
key_bindings=$?
|
||||
fi
|
||||
|
||||
echo
|
||||
for shell in $shells; do
|
||||
[[ "$shell" = fish ]] && 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
|
||||
|
||||
cat > "$src" << EOF
|
||||
# Setup fzf
|
||||
# ---------
|
||||
if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then
|
||||
export PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
|
||||
fi
|
||||
|
||||
# Auto-completion
|
||||
# ---------------
|
||||
$fzf_completion
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
$fzf_key_bindings
|
||||
EOF
|
||||
echo "OK"
|
||||
done
|
||||
|
||||
# fish
|
||||
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
|
||||
update="$1"
|
||||
line="$2"
|
||||
file="$3"
|
||||
pat="${4:-}"
|
||||
lno=""
|
||||
|
||||
echo "Update $file:"
|
||||
echo " - $line"
|
||||
if [ -f "$file" ]; then
|
||||
if [ $# -lt 4 ]; then
|
||||
lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
else
|
||||
lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
fi
|
||||
fi
|
||||
if [ -n "$lno" ]; then
|
||||
echo " - Already exists: line #$lno"
|
||||
else
|
||||
if [ $update -eq 1 ]; then
|
||||
[ -f "$file" ] && echo >> "$file"
|
||||
echo "$line" >> "$file"
|
||||
echo " + Added"
|
||||
else
|
||||
echo " ~ Skipped"
|
||||
fi
|
||||
fi
|
||||
echo
|
||||
set +e
|
||||
}
|
||||
|
||||
create_file() {
|
||||
local file="$1"
|
||||
shift
|
||||
echo "Create $file:"
|
||||
for line in "$@"; do
|
||||
echo " $line"
|
||||
echo "$line" >> "$file"
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
if [ $update_config -eq 2 ]; then
|
||||
echo
|
||||
ask "Do you want to update your shell configuration files?"
|
||||
update_config=$?
|
||||
fi
|
||||
echo
|
||||
for shell in $shells; do
|
||||
[[ "$shell" = fish ]] && 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
|
||||
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'
|
||||
else
|
||||
append_line $update_config "fzf_key_bindings" "$bind_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $update_config -eq 1 ]; then
|
||||
echo 'Finished. Restart your shell or reload config file.'
|
||||
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 ]] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish'
|
||||
echo
|
||||
echo 'Use uninstall script to remove fzf.'
|
||||
echo
|
||||
fi
|
||||
echo 'For more information, see: https://github.com/junegunn/fzf'
|
||||
|
||||
61
install.ps1
Normal file
61
install.ps1
Normal file
@@ -0,0 +1,61 @@
|
||||
$version="0.24.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/$version/$file"
|
||||
$temp=$env:TMP + "\fzf.zip"
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
(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
|
||||
}
|
||||
check_binary >$null
|
||||
}
|
||||
|
||||
download "fzf-$version-windows_amd64.zip"
|
||||
|
||||
Write-Host 'For more information, see: https://github.com/junegunn/fzf'
|
||||
14
main.go
Normal file
14
main.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/junegunn/fzf/src"
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version string
|
||||
var revision string
|
||||
|
||||
func main() {
|
||||
protector.Protect()
|
||||
fzf.Run(fzf.ParseOptions(), version, revision)
|
||||
}
|
||||
68
man/man1/fzf-tmux.1
Normal file
68
man/man1/fzf-tmux.1
Normal file
@@ -0,0 +1,68 @@
|
||||
.ig
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2020 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.
|
||||
..
|
||||
.TH fzf-tmux 1 "Oct 2020" "fzf 0.24.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf-tmux - open fzf in tmux split pane
|
||||
|
||||
.SH SYNOPSIS
|
||||
.B fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
||||
|
||||
.SH DESCRIPTION
|
||||
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 LAYOUT OPTIONS
|
||||
|
||||
(default layout: \fB-d 50%\fR)
|
||||
|
||||
.SS Popup window
|
||||
(requires tmux 3.2 or above)
|
||||
.TP
|
||||
.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[%]]"
|
||||
Split below (down)
|
||||
.TP
|
||||
.B "-l [width[%]]"
|
||||
Split left
|
||||
.TP
|
||||
.B "-r [width[%]]"
|
||||
Split right
|
||||
886
man/man1/fzf.1
Normal file
886
man/man1/fzf.1
Normal file
@@ -0,0 +1,886 @@
|
||||
.ig
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2020 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.
|
||||
..
|
||||
.TH fzf 1 "Oct 2020" "fzf 0.24.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
|
||||
.SH SYNOPSIS
|
||||
fzf [options]
|
||||
|
||||
.SH DESCRIPTION
|
||||
fzf is a general-purpose command-line fuzzy finder.
|
||||
|
||||
.SH OPTIONS
|
||||
.SS Search mode
|
||||
.TP
|
||||
.B "-x, --extended"
|
||||
Extended-search mode. Since 0.10.9, this is enabled by default. You can disable
|
||||
it with \fB+x\fR or \fB--no-extended\fR.
|
||||
.TP
|
||||
.B "-e, --exact"
|
||||
Enable exact-match
|
||||
.TP
|
||||
.B "-i"
|
||||
Case-insensitive match (default: smart-case match)
|
||||
.TP
|
||||
.B "+i"
|
||||
Case-sensitive match
|
||||
.TP
|
||||
.B "--literal"
|
||||
Do not normalize latin script letters for matching.
|
||||
.TP
|
||||
.BI "--algo=" TYPE
|
||||
Fuzzy matching algorithm (default: v2)
|
||||
|
||||
.br
|
||||
.BR v2 " Optimal scoring algorithm (quality)"
|
||||
.br
|
||||
.BR v1 " Faster but not guaranteed to find the optimal result (performance)"
|
||||
.br
|
||||
|
||||
.TP
|
||||
.BI "-n, --nth=" "N[,..]"
|
||||
Comma-separated list of field index expressions for limiting search scope.
|
||||
See \fBFIELD INDEX EXPRESSION\fR for the details.
|
||||
.TP
|
||||
.BI "--with-nth=" "N[,..]"
|
||||
Transform the presentation of each line using field index expressions
|
||||
.TP
|
||||
.BI "-d, --delimiter=" "STR"
|
||||
Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style)
|
||||
.TP
|
||||
.BI "--phony"
|
||||
Do not perform search. With this option, fzf becomes a simple selector
|
||||
interface rather than a "fuzzy finder".
|
||||
.SS Search result
|
||||
.TP
|
||||
.B "+s, --no-sort"
|
||||
Do not sort the result
|
||||
.TP
|
||||
.B "--tac"
|
||||
Reverse the order of the input
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
\fBhistory | fzf --tac --no-sort\fR
|
||||
.RE
|
||||
.TP
|
||||
.BI "--tiebreak=" "CRI[,..]"
|
||||
Comma-separated list of sort criteria to apply when the scores are tied.
|
||||
.br
|
||||
|
||||
.br
|
||||
.BR length " Prefers line with shorter length"
|
||||
.br
|
||||
.BR begin " Prefers line with matched substring closer to the beginning"
|
||||
.br
|
||||
.BR end " Prefers line with matched substring closer to the end"
|
||||
.br
|
||||
.BR index " Prefers line that appeared earlier in the input stream"
|
||||
.br
|
||||
|
||||
.br
|
||||
- Each criterion should appear only once in the list
|
||||
.br
|
||||
- \fBindex\fR is only allowed at the end of the list
|
||||
.br
|
||||
- \fBindex\fR is implicitly appended to the list when not specified
|
||||
.br
|
||||
- Default is \fBlength\fR (or equivalently \fBlength\fR,index)
|
||||
.br
|
||||
- If \fBend\fR is found in the list, fzf will scan each line backwards
|
||||
.SS Interface
|
||||
.TP
|
||||
.B "-m, --multi"
|
||||
Enable multi-select with tab/shift-tab. It optionally takes an integer argument
|
||||
which denotes the maximum number of items that can be selected.
|
||||
.TP
|
||||
.B "+m, --no-multi"
|
||||
Disable multi-select
|
||||
.TP
|
||||
.B "--no-mouse"
|
||||
Disable mouse
|
||||
.TP
|
||||
.BI "--bind=" "KEYBINDS"
|
||||
Comma-separated list of custom key bindings. See \fBKEY/EVENT BINDINGS\fR for
|
||||
the details.
|
||||
.TP
|
||||
.B "--cycle"
|
||||
Enable cyclic scroll
|
||||
.TP
|
||||
.B "--keep-right"
|
||||
Keep the right end of the line visible when it's too long. Effective only when
|
||||
the query string is empty.
|
||||
.TP
|
||||
.B "--no-hscroll"
|
||||
Disable horizontal scroll
|
||||
.TP
|
||||
.BI "--hscroll-off=" "COL"
|
||||
Number of screen columns to keep to the right of the highlighted substring
|
||||
(default: 10). Setting it to a large value will cause the text to be positioned
|
||||
on the center of the screen.
|
||||
.TP
|
||||
.B "--filepath-word"
|
||||
Make word-wise movements and actions respect path separators. The following
|
||||
actions are affected:
|
||||
|
||||
\fBbackward-kill-word\fR
|
||||
.br
|
||||
\fBbackward-word\fR
|
||||
.br
|
||||
\fBforward-word\fR
|
||||
.br
|
||||
\fBkill-word\fR
|
||||
.TP
|
||||
.BI "--jump-labels=" "CHARS"
|
||||
Label characters for \fBjump\fR and \fBjump-accept\fR
|
||||
.SS Layout
|
||||
.TP
|
||||
.BI "--height=" "HEIGHT[%]"
|
||||
Display fzf window below the cursor with the given height instead of using
|
||||
the full screen.
|
||||
.TP
|
||||
.BI "--min-height=" "HEIGHT"
|
||||
Minimum height when \fB--height\fR is given in percent (default: 10).
|
||||
Ignored when \fB--height\fR is not specified.
|
||||
.TP
|
||||
.BI "--layout=" "LAYOUT"
|
||||
Choose the layout (default: default)
|
||||
|
||||
.br
|
||||
.BR default " Display from the bottom of the screen"
|
||||
.br
|
||||
.BR reverse " Display from the top of the screen"
|
||||
.br
|
||||
.BR reverse-list " Display from the top of the screen, prompt at the bottom"
|
||||
.br
|
||||
|
||||
.TP
|
||||
.B "--reverse"
|
||||
A synonym for \fB--layout=reverse\fB
|
||||
|
||||
.TP
|
||||
.BI "--border" [=STYLE]
|
||||
Draw border around the finder
|
||||
|
||||
.br
|
||||
.BR rounded " Border with rounded corners (default)"
|
||||
.br
|
||||
.BR sharp " Border with sharp corners"
|
||||
.br
|
||||
.BR horizontal " Horizontal lines above and below the finder"
|
||||
.br
|
||||
.BR vertical " Vertical lines on each side of the finder"
|
||||
.br
|
||||
.BR top
|
||||
.br
|
||||
.BR bottom
|
||||
.br
|
||||
.BR left
|
||||
.br
|
||||
.BR right
|
||||
.br
|
||||
|
||||
.TP
|
||||
.B "--no-unicode"
|
||||
Use ASCII characters instead of Unicode box drawing characters to draw border
|
||||
|
||||
.TP
|
||||
.BI "--margin=" MARGIN
|
||||
Comma-separated expression for margins around the finder.
|
||||
.br
|
||||
|
||||
.br
|
||||
.RS
|
||||
.BR TRBL " Same margin for top, right, bottom, and left"
|
||||
.br
|
||||
.BR TB,RL " Vertical, horizontal margin"
|
||||
.br
|
||||
.BR T,RL,B " Top, horizontal, bottom margin"
|
||||
.br
|
||||
.BR T,R,B,L " Top, right, bottom, left margin"
|
||||
.br
|
||||
|
||||
.br
|
||||
Each part can be given in absolute number or in percentage relative to the
|
||||
terminal size with \fB%\fR suffix.
|
||||
.br
|
||||
|
||||
.br
|
||||
e.g.
|
||||
\fBfzf --margin 10%
|
||||
fzf --margin 1,5%\fR
|
||||
.RE
|
||||
.TP
|
||||
.BI "--info=" "STYLE"
|
||||
Determines the display style of finder info.
|
||||
|
||||
.br
|
||||
.BR default " Display on the next line to the prompt"
|
||||
.br
|
||||
.BR inline " Display on the same line"
|
||||
.br
|
||||
.BR hidden " Do not display finder info"
|
||||
.br
|
||||
|
||||
.TP
|
||||
.B "--no-info"
|
||||
A synonym for \fB--info=hidden\fB
|
||||
|
||||
.TP
|
||||
.BI "--prompt=" "STR"
|
||||
Input prompt (default: '> ')
|
||||
.TP
|
||||
.BI "--pointer=" "STR"
|
||||
Pointer to the current line (default: '>')
|
||||
.TP
|
||||
.BI "--marker=" "STR"
|
||||
Multi-select marker (default: '>')
|
||||
.TP
|
||||
.BI "--header=" "STR"
|
||||
The given string will be printed as the sticky header. The lines are displayed
|
||||
in the given order from top to bottom regardless of \fB--layout\fR option, and
|
||||
are not affected by \fB--with-nth\fR. ANSI color codes are processed even when
|
||||
\fB--ansi\fR is not set.
|
||||
.TP
|
||||
.BI "--header-lines=" "N"
|
||||
The first N lines of the input are treated as the sticky header. When
|
||||
\fB--with-nth\fR is set, the lines are transformed just like the other
|
||||
lines that follow.
|
||||
.SS Display
|
||||
.TP
|
||||
.B "--ansi"
|
||||
Enable processing of ANSI color codes
|
||||
.TP
|
||||
.BI "--tabstop=" SPACES
|
||||
Number of spaces for a tab character (default: 8)
|
||||
.TP
|
||||
.BI "--color=" "[BASE_SCHEME][,COLOR_NAME[:ANSI_COLOR][:ANSI_ATTRIBUTES]]..."
|
||||
Color configuration. The name of the base color scheme is followed by custom
|
||||
color mappings.
|
||||
|
||||
.RS
|
||||
.B BASE SCHEME:
|
||||
(default: dark on 256-color terminal, otherwise 16)
|
||||
|
||||
\fBdark \fRColor scheme for dark 256-color terminal
|
||||
\fBlight \fRColor scheme for light 256-color terminal
|
||||
\fB16 \fRColor scheme for 16-color terminal
|
||||
\fBbw \fRNo colors (equivalent to \fB--no-color\fR)
|
||||
|
||||
.B COLOR NAMES:
|
||||
\fBfg \fRText
|
||||
\fBbg \fRBackground
|
||||
\fBpreview-fg \fRPreview window text
|
||||
\fBpreview-bg \fRPreview window background
|
||||
\fBhl \fRHighlighted substrings
|
||||
\fBfg+ \fRText (current line)
|
||||
\fBbg+ \fRBackground (current line)
|
||||
\fBgutter \fRGutter on the left (defaults to \fBbg+\fR)
|
||||
\fBhl+ \fRHighlighted substrings (current line)
|
||||
\fBinfo \fRInfo line (match counters)
|
||||
\fBborder \fRBorder around the window (\fB--border\fR and \fB--preview\fR)
|
||||
\fBprompt \fRPrompt
|
||||
\fBpointer \fRPointer to the current line
|
||||
\fBmarker \fRMulti-select marker
|
||||
\fBspinner \fRStreaming input indicator
|
||||
\fBheader \fRHeader
|
||||
|
||||
.B ANSI COLORS:
|
||||
\fB-1 \fRDefault terminal foreground/background color
|
||||
\fB \fR(or the original color of the text)
|
||||
\fB0 ~ 15 \fR16 base colors
|
||||
\fB16 ~ 255 \fRANSI 256 colors
|
||||
\fB#rrggbb \fR24-bit colors
|
||||
|
||||
.B ANSI ATTRIBUTES: (Only applies to foreground colors)
|
||||
\fBregular \fRClears previously set attributes; should precede the other ones
|
||||
\fBbold\fR
|
||||
\fBunderline\fR
|
||||
\fBreverse\fR
|
||||
\fBdim\fR
|
||||
\fBitalic\fR
|
||||
|
||||
.B EXAMPLES:
|
||||
|
||||
\fB# Seoul256 theme with 8-bit colors
|
||||
# (https://github.com/junegunn/seoul256.vim)
|
||||
fzf --color='bg:237,bg+:236,info:143,border:240,spinner:108' \\
|
||||
--color='hl:65,fg:252,header:65,fg+:252' \\
|
||||
--color='pointer:161,marker:168,prompt:110,hl+:108'
|
||||
|
||||
# Seoul256 theme with 24-bit colors
|
||||
fzf --color='bg:#4B4B4B,bg+:#3F3F3F,info:#BDBB72,border:#6B6B6B,spinner:#98BC99' \\
|
||||
--color='hl:#719872,fg:#D9D9D9,header:#719872,fg+:#D9D9D9' \\
|
||||
--color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "--no-bold"
|
||||
Do not use bold text
|
||||
.TP
|
||||
.B "--black"
|
||||
Use black background
|
||||
.SS History
|
||||
.TP
|
||||
.BI "--history=" "HISTORY_FILE"
|
||||
Load search history from the specified file and update the file on completion.
|
||||
When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to
|
||||
\fBnext-history\fR and \fBprevious-history\fR.
|
||||
.TP
|
||||
.BI "--history-size=" "N"
|
||||
Maximum number of entries in the history file (default: 1000). The file is
|
||||
automatically truncated when the number of the lines exceeds the value.
|
||||
.SS Preview
|
||||
.TP
|
||||
.BI "--preview=" "COMMAND"
|
||||
Execute the given command for the current line and display the result on the
|
||||
preview window. \fB{}\fR in the command is the placeholder that is replaced to
|
||||
the single-quoted string of the current line. To transform the replacement
|
||||
string, specify field index expressions between the braces (See \fBFIELD INDEX
|
||||
EXPRESSION\fR for the details).
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
\fBfzf --preview='head -$LINES {}'
|
||||
ls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
|
||||
|
||||
fzf exports \fB$FZF_PREVIEW_LINES\fR and \fB$FZF_PREVIEW_COLUMNS\fR so that
|
||||
they represent the exact size of the preview window. (It also overrides
|
||||
\fB$LINES\fR and \fB$COLUMNS\fR with the same values but they can be reset
|
||||
by the default shell, so prefer to refer to the ones with \fBFZF_PREVIEW_\fR
|
||||
prefix.)
|
||||
|
||||
A placeholder expression starting with \fB+\fR flag will be replaced to the
|
||||
space-separated list of the selected lines (or the current line if no selection
|
||||
was made) individually quoted.
|
||||
|
||||
e.g.
|
||||
\fBfzf --multi --preview='head -10 {+}'
|
||||
git log --oneline | fzf --multi --preview 'git show {+1}'\fR
|
||||
|
||||
When using a field index expression, leading and trailing whitespace is stripped
|
||||
from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
|
||||
|
||||
Also, \fB{q}\fR is replaced to the current query string, and \fB{n}\fR is
|
||||
replaced to zero-based ordinal index of the line. Use \fB{+n}\fR if you want
|
||||
all index numbers when multiple lines are selected.
|
||||
|
||||
A placeholder expression with \fBf\fR flag is replaced to the path of
|
||||
a temporary file that holds the evaluated list. This is useful when you
|
||||
multi-select a large number of items and the length of the evaluated string may
|
||||
exceed \fBARG_MAX\fR.
|
||||
|
||||
e.g.
|
||||
\fB# Press CTRL-A to select 100K items and see the sum of all the numbers.
|
||||
# This won't work properly without 'f' flag due to ARG_MAX limit.
|
||||
seq 100000 | fzf --multi --bind ctrl-a:select-all \\
|
||||
--preview "awk '{sum+=\$1} END {print sum}' {+f}"\fR
|
||||
|
||||
Note that you can escape a placeholder pattern by prepending a backslash.
|
||||
|
||||
Preview window will be updated even when there is no match for the current
|
||||
query if any of the placeholder expressions evaluates to a non-empty string.
|
||||
|
||||
Since 0.24.0, fzf can render partial preview content before the preview command
|
||||
completes. ANSI escape sequence for clearing the display (\fBCSI 2 J\fR) is
|
||||
supported, so you can use it to implement preview window that is constantly
|
||||
updating.
|
||||
|
||||
e.g.
|
||||
\fBfzf --preview 'for i in $(seq 100000); do
|
||||
(( i % 200 == 0 )) && printf "\\033[2J"
|
||||
echo "$i"
|
||||
sleep 0.01
|
||||
done'\fR
|
||||
.RE
|
||||
.TP
|
||||
.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]cycle][:[no]hidden][:+SCROLL[-OFFSET]][:default]"
|
||||
|
||||
.RS
|
||||
.B POSITION: (default: right)
|
||||
\fBup
|
||||
\fBdown
|
||||
\fBleft
|
||||
\fBright
|
||||
|
||||
\fRDetermines the layout of the preview window. If the argument contains
|
||||
\fB:hidden\fR, the preview window will be hidden by default until
|
||||
\fBtoggle-preview\fR action is triggered. Long lines are truncated by default.
|
||||
Line wrap can be enabled with \fB:wrap\fR flag. Cyclic scrolling is enabled
|
||||
with \fB:cycle\fR flag.
|
||||
|
||||
If size is given as 0, preview window will not be visible, but fzf will still
|
||||
execute the command in the background.
|
||||
|
||||
To change the style of the border of the preview window, specify one of
|
||||
\fBrounded\fR (border with rounded edges, default), \fBsharp\fR (border with
|
||||
sharp edges), or \fBnoborder\fR (no border).
|
||||
|
||||
\fB+SCROLL[-OFFSET]\fR determines the initial scroll offset of the preview
|
||||
window. \fBSCROLL\fR can be either a numeric integer or a single-field index
|
||||
expression that refers to a numeric integer. The optional \fB-OFFSET\fR part is
|
||||
for adjusting the base offset so that you can see the text above it. It should
|
||||
be given as a numeric integer (\fB-INTEGER\fR), or as a denominator form
|
||||
(\fB-/INTEGER\fR) for specifying a fraction of the preview window height.
|
||||
|
||||
\fBdefault\fR resets all options previously set to the default.
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
\fB# Non-default scroll window positions and sizes
|
||||
fzf --preview="head {}" --preview-window=up:30%
|
||||
fzf --preview="file {}" --preview-window=down:1
|
||||
|
||||
# Initial scroll offset is set to the line number of each line of
|
||||
# git grep output *minus* 5 lines (-5)
|
||||
git grep --line-number '' |
|
||||
fzf --delimiter : --preview 'nl {1}' --preview-window +{2}-5
|
||||
|
||||
# Preview with bat, matching line in the middle of the window (-/2)
|
||||
git grep --line-number '' |
|
||||
fzf --delimiter : \\
|
||||
--preview 'bat --style=numbers --color=always --highlight-line {2} {1}' \\
|
||||
--preview-window +{2}-/2\fR
|
||||
|
||||
.RE
|
||||
|
||||
.SS Scripting
|
||||
.TP
|
||||
.BI "-q, --query=" "STR"
|
||||
Start the finder with the given query
|
||||
.TP
|
||||
.B "-1, --select-1"
|
||||
Automatically select the only match
|
||||
.TP
|
||||
.B "-0, --exit-0"
|
||||
Exit immediately when there's no match
|
||||
.TP
|
||||
.BI "-f, --filter=" "STR"
|
||||
Filter mode. Do not start interactive finder. When used with \fB--no-sort\fR,
|
||||
fzf becomes a fuzzy-version of grep.
|
||||
.TP
|
||||
.B "--print-query"
|
||||
Print query as the first line
|
||||
.TP
|
||||
.BI "--expect=" "KEY[,..]"
|
||||
Comma-separated list of keys that can be used to complete fzf in addition to
|
||||
the default enter key. When this option is set, fzf will print the name of the
|
||||
key pressed as the first line of its output (or as the second line if
|
||||
\fB--print-query\fR is also used). The line will be empty if fzf is completed
|
||||
with the default enter key. If \fB--expect\fR option is specified multiple
|
||||
times, fzf will expect the union of the keys. \fB--no-expect\fR will clear the
|
||||
list.
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
\fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "--read0"
|
||||
Read input delimited by ASCII NUL characters instead of newline characters
|
||||
.TP
|
||||
.B "--print0"
|
||||
Print output delimited by ASCII NUL characters instead of newline characters
|
||||
.TP
|
||||
.B "--no-clear"
|
||||
Do not clear finder interface on exit. If fzf was started in full screen mode,
|
||||
it will not switch back to the original screen, so you'll have to manually run
|
||||
\fBtput rmcup\fR to return. This option can be used to avoid flickering of the
|
||||
screen when your application needs to start fzf multiple times in order.
|
||||
.TP
|
||||
.B "--sync"
|
||||
Synchronous search for multi-staged filtering. If specified, fzf will launch
|
||||
ncurses finder only after the input stream is complete.
|
||||
|
||||
.RS
|
||||
e.g. \fBfzf --multi | fzf --sync\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "--version"
|
||||
Display version information and exit
|
||||
|
||||
.TP
|
||||
Note that most options have the opposite versions with \fB--no-\fR prefix.
|
||||
|
||||
.SH ENVIRONMENT VARIABLES
|
||||
.TP
|
||||
.B FZF_DEFAULT_COMMAND
|
||||
Default command to use when input is tty. On *nix systems, fzf runs the command
|
||||
with \fBsh -c\fR, so make sure that it's POSIX-compliant.
|
||||
.TP
|
||||
.B FZF_DEFAULT_OPTS
|
||||
Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR
|
||||
|
||||
.SH EXIT STATUS
|
||||
.BR 0 " Normal exit"
|
||||
.br
|
||||
.BR 1 " No match"
|
||||
.br
|
||||
.BR 2 " Error"
|
||||
.br
|
||||
.BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR"
|
||||
|
||||
.SH FIELD INDEX EXPRESSION
|
||||
|
||||
A field index expression can be a non-zero integer or a range expression
|
||||
([BEGIN]..[END]). \fB--nth\fR and \fB--with-nth\fR take a comma-separated list
|
||||
of field index expressions.
|
||||
|
||||
.SS Examples
|
||||
.BR 1 " The 1st field"
|
||||
.br
|
||||
.BR 2 " The 2nd field"
|
||||
.br
|
||||
.BR -1 " The last field"
|
||||
.br
|
||||
.BR -2 " The 2nd to last field"
|
||||
.br
|
||||
.BR 3..5 " From the 3rd field to the 5th field"
|
||||
.br
|
||||
.BR 2.. " From the 2nd field to the last field"
|
||||
.br
|
||||
.BR ..-3 " From the 1st field to the 3rd to the last field"
|
||||
.br
|
||||
.BR .. " All the fields"
|
||||
.br
|
||||
|
||||
.SH EXTENDED SEARCH MODE
|
||||
|
||||
Unless specified otherwise, fzf will start in "extended-search mode". In this
|
||||
mode, you can specify multiple patterns delimited by spaces, such as: \fB'wild
|
||||
^music .mp3$ sbtrkt !rmx\fR
|
||||
|
||||
You can prepend a backslash to a space (\fB\\ \fR) to match a literal space
|
||||
character.
|
||||
|
||||
.SS Exact-match (quoted)
|
||||
A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as
|
||||
an "exact-match" (or "non-fuzzy") term. fzf will search for the exact
|
||||
occurrences of the string.
|
||||
|
||||
.SS Anchored-match
|
||||
A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an
|
||||
anchored-match term. Then fzf will search for the lines that start with or end
|
||||
with the given string. An anchored-match term is also an exact-match term.
|
||||
|
||||
.SS Negation
|
||||
If a term is prefixed by \fB!\fR, fzf will exclude the lines that satisfy the
|
||||
term from the result. In this case, fzf performs exact match by default.
|
||||
|
||||
.SS Exact-match by default
|
||||
If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with
|
||||
\fB'\fR) every word, start fzf with \fB-e\fR or \fB--exact\fR option. Note that
|
||||
when \fB--exact\fR is set, \fB'\fR-prefix "unquotes" the term.
|
||||
|
||||
.SS OR operator
|
||||
A single bar character term acts as an OR operator. For example, the following
|
||||
query matches entries that start with \fBcore\fR and end with either \fBgo\fR,
|
||||
\fBrb\fR, or \fBpy\fR.
|
||||
|
||||
e.g. \fB^core go$ | rb$ | py$\fR
|
||||
|
||||
.SH KEY/EVENT BINDINGS
|
||||
\fB--bind\fR option allows you to bind \fBa key\fR or \fBan event\fR to one or
|
||||
more \fBactions\fR. You can use it to customize key bindings or implement
|
||||
dynamic behaviors.
|
||||
|
||||
\fB--bind\fR takes a comma-separated list of binding expressions. Each binding
|
||||
expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR.
|
||||
|
||||
e.g.
|
||||
\fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
|
||||
|
||||
.SS AVAILABLE KEYS: (SYNONYMS)
|
||||
\fIctrl-[a-z]\fR
|
||||
.br
|
||||
\fIctrl-space\fR
|
||||
.br
|
||||
\fIctrl-\\\fR
|
||||
.br
|
||||
\fIctrl-]\fR
|
||||
.br
|
||||
\fIctrl-^\fR (\fIctrl-6\fR)
|
||||
.br
|
||||
\fIctrl-/\fR (\fIctrl-_\fR)
|
||||
.br
|
||||
\fIctrl-alt-[a-z]\fR
|
||||
.br
|
||||
\fIalt-[a-z]\fR
|
||||
.br
|
||||
\fIalt-[0-9]\fR
|
||||
.br
|
||||
\fIf[1-12]\fR
|
||||
.br
|
||||
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
|
||||
.br
|
||||
\fIspace\fR
|
||||
.br
|
||||
\fIbspace\fR (\fIbs\fR)
|
||||
.br
|
||||
\fIalt-up\fR
|
||||
.br
|
||||
\fIalt-down\fR
|
||||
.br
|
||||
\fIalt-left\fR
|
||||
.br
|
||||
\fIalt-right\fR
|
||||
.br
|
||||
\fIalt-enter\fR
|
||||
.br
|
||||
\fIalt-space\fR
|
||||
.br
|
||||
\fIalt-bspace\fR (\fIalt-bs\fR)
|
||||
.br
|
||||
\fIalt-/\fR
|
||||
.br
|
||||
\fItab\fR
|
||||
.br
|
||||
\fIbtab\fR (\fIshift-tab\fR)
|
||||
.br
|
||||
\fIesc\fR
|
||||
.br
|
||||
\fIdel\fR
|
||||
.br
|
||||
\fIup\fR
|
||||
.br
|
||||
\fIdown\fR
|
||||
.br
|
||||
\fIleft\fR
|
||||
.br
|
||||
\fIright\fR
|
||||
.br
|
||||
\fIhome\fR
|
||||
.br
|
||||
\fIend\fR
|
||||
.br
|
||||
\fIinsert\fR
|
||||
.br
|
||||
\fIpgup\fR (\fIpage-up\fR)
|
||||
.br
|
||||
\fIpgdn\fR (\fIpage-down\fR)
|
||||
.br
|
||||
\fIshift-up\fR
|
||||
.br
|
||||
\fIshift-down\fR
|
||||
.br
|
||||
\fIshift-left\fR
|
||||
.br
|
||||
\fIshift-right\fR
|
||||
.br
|
||||
\fIleft-click\fR
|
||||
.br
|
||||
\fIright-click\fR
|
||||
.br
|
||||
\fIdouble-click\fR
|
||||
.br
|
||||
or any single character
|
||||
|
||||
.SS AVAILABLE EVENTS:
|
||||
\fIchange\fR
|
||||
.RS
|
||||
Triggered whenever the query string is changed
|
||||
|
||||
e.g.
|
||||
\fB# Moves cursor to the top (or bottom depending on --layout) whenever the query is changed
|
||||
fzf --bind change:top\fR
|
||||
.RE
|
||||
|
||||
\fIbackward-eof\fR
|
||||
.RS
|
||||
Triggered when the query string is already empty and you try to delete it
|
||||
backward.
|
||||
|
||||
e.g.
|
||||
\fBfzf --bind backward-eof:abort\fR
|
||||
.RE
|
||||
|
||||
.SS AVAILABLE ACTIONS:
|
||||
A key or an event can be bound to one or more of the following actions.
|
||||
|
||||
\fBACTION: DEFAULT BINDINGS (NOTES):
|
||||
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
|
||||
\fBaccept\fR \fIenter double-click\fR
|
||||
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
|
||||
\fBbackward-char\fR \fIctrl-b left\fR
|
||||
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
|
||||
\fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty)
|
||||
\fBbackward-kill-word\fR \fIalt-bs\fR
|
||||
\fBbackward-word\fR \fIalt-b shift-left\fR
|
||||
\fBbeginning-of-line\fR \fIctrl-a home\fR
|
||||
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
|
||||
\fBclear-screen\fR \fIctrl-l\fR
|
||||
\fBclear-selection\fR (clear multi-selection)
|
||||
\fBclear-query\fR (clear query string)
|
||||
\fBdelete-char\fR \fIdel\fR
|
||||
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
|
||||
\fBdeselect-all\fR (deselect all matches)
|
||||
\fBdown\fR \fIctrl-j ctrl-n down\fR
|
||||
\fBend-of-line\fR \fIctrl-e end\fR
|
||||
\fBexecute(...)\fR (see below for the details)
|
||||
\fBexecute-silent(...)\fR (see below for the details)
|
||||
\fBforward-char\fR \fIctrl-f right\fR
|
||||
\fBforward-word\fR \fIalt-f shift-right\fR
|
||||
\fBignore\fR
|
||||
\fBjump\fR (EasyMotion-like 2-keystroke movement)
|
||||
\fBjump-accept\fR (jump and accept)
|
||||
\fBkill-line\fR
|
||||
\fBkill-word\fR \fIalt-d\fR
|
||||
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
|
||||
\fBpage-down\fR \fIpgdn\fR
|
||||
\fBpage-up\fR \fIpgup\fR
|
||||
\fBhalf-page-down\fR
|
||||
\fBhalf-page-up\fR
|
||||
\fBpreview(...)\fR (see below for the details)
|
||||
\fBpreview-down\fR \fIshift-down\fR
|
||||
\fBpreview-up\fR \fIshift-up\fR
|
||||
\fBpreview-page-down\fR
|
||||
\fBpreview-page-up\fR
|
||||
\fBpreview-half-page-down\fR
|
||||
\fBpreview-half-page-up\fR
|
||||
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
|
||||
\fBprint-query\fR (print query and exit)
|
||||
\fBrefresh-preview\fR
|
||||
\fBreload(...)\fR (see below for the details)
|
||||
\fBreplace-query\fR (replace query string with the current selection)
|
||||
\fBselect-all\fR (select all matches)
|
||||
\fBtoggle\fR (\fIright-click\fR)
|
||||
\fBtoggle-all\fR (toggle all matches)
|
||||
\fBtoggle+down\fR \fIctrl-i (tab)\fR
|
||||
\fBtoggle-in\fR (\fB--layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
|
||||
\fBtoggle-out\fR (\fB--layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
|
||||
\fBtoggle-preview\fR
|
||||
\fBtoggle-preview-wrap\fR
|
||||
\fBtoggle-sort\fR
|
||||
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
|
||||
\fBtop\fR (move to the top result)
|
||||
\fBunix-line-discard\fR \fIctrl-u\fR
|
||||
\fBunix-word-rubout\fR \fIctrl-w\fR
|
||||
\fBup\fR \fIctrl-k ctrl-p up\fR
|
||||
\fByank\fR \fIctrl-y\fR
|
||||
|
||||
.SS ACTION COMPOSITION
|
||||
|
||||
Multiple actions can be chained using \fB+\fR separator.
|
||||
|
||||
e.g.
|
||||
\fBfzf --bind 'ctrl-a:select-all+accept'\fR
|
||||
|
||||
.SS COMMAND EXECUTION
|
||||
|
||||
With \fBexecute(...)\fR action, you can execute arbitrary commands without
|
||||
leaving fzf. For example, you can turn fzf into a simple file browser by
|
||||
binding \fBenter\fR key to \fBless\fR command like follows.
|
||||
|
||||
\fBfzf --bind "enter:execute(less {})"\fR
|
||||
|
||||
You can use the same placeholder expressions as in \fB--preview\fR.
|
||||
|
||||
If the command contains parentheses, fzf may fail to parse the expression. In
|
||||
that case, you can use any of the following alternative notations to avoid
|
||||
parse errors.
|
||||
|
||||
\fBexecute[...]\fR
|
||||
\fBexecute~...~\fR
|
||||
\fBexecute!...!\fR
|
||||
\fBexecute@...@\fR
|
||||
\fBexecute#...#\fR
|
||||
\fBexecute$...$\fR
|
||||
\fBexecute%...%\fR
|
||||
\fBexecute^...^\fR
|
||||
\fBexecute&...&\fR
|
||||
\fBexecute*...*\fR
|
||||
\fBexecute;...;\fR
|
||||
\fBexecute/.../\fR
|
||||
\fBexecute|...|\fR
|
||||
\fBexecute:...\fR
|
||||
.RS
|
||||
The last one is the special form that frees you from parse errors as it does
|
||||
not expect the closing character. The catch is that it should be the last one
|
||||
in the comma-separated list of key-action pairs.
|
||||
.RE
|
||||
|
||||
fzf switches to the alternate screen when executing a command. However, if the
|
||||
command is expected to complete quickly, and you are not interested in its
|
||||
output, you might want to use \fBexecute-silent\fR instead, which silently
|
||||
executes the command without the switching. Note that fzf will not be
|
||||
responsive until the command is complete. For asynchronous execution, start
|
||||
your command as a background process (i.e. appending \fB&\fR).
|
||||
|
||||
.SS RELOAD INPUT
|
||||
|
||||
\fBreload(...)\fR action is used to dynamically update the input list
|
||||
without restarting fzf. It takes the same command template with placeholder
|
||||
expressions as \fBexecute(...)\fR.
|
||||
|
||||
See \fIhttps://github.com/junegunn/fzf/issues/1750\fR for more info.
|
||||
|
||||
e.g.
|
||||
\fB# Update the list of processes by pressing CTRL-R
|
||||
ps -ef | fzf --bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \\
|
||||
--header-lines=1 --layout=reverse
|
||||
|
||||
# Integration with ripgrep
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="foobar"
|
||||
FZF_DEFAULT_COMMAND="$RG_PREFIX '$INITIAL_QUERY'" \\
|
||||
fzf --bind "change:reload:$RG_PREFIX {q} || true" \\
|
||||
--ansi --phony --query "$INITIAL_QUERY"\fR
|
||||
|
||||
.SS PREVIEW BINDING
|
||||
|
||||
With \fBpreview(...)\fR action, you can specify multiple different preview
|
||||
commands in addition to the default preview command given by \fB--preview\fR
|
||||
option.
|
||||
|
||||
e.g.
|
||||
|
||||
# Default preview command with an extra preview binding
|
||||
fzf --preview 'file {}' --bind '?:preview:cat {}'
|
||||
|
||||
# A preview binding with no default preview command
|
||||
# (Preview window is initially empty)
|
||||
fzf --bind '?:preview:cat {}'
|
||||
|
||||
# Preview window hidden by default, it appears when you first hit '?'
|
||||
fzf --bind '?:preview:cat {}' --preview-window hidden
|
||||
|
||||
.SH AUTHOR
|
||||
Junegunn Choi (\fIjunegunn.c@gmail.com\fR)
|
||||
|
||||
.SH SEE ALSO
|
||||
.B Project homepage:
|
||||
.RS
|
||||
.I https://github.com/junegunn/fzf
|
||||
.RE
|
||||
.br
|
||||
|
||||
.br
|
||||
.B Extra Vim plugin:
|
||||
.RS
|
||||
.I https://github.com/junegunn/fzf.vim
|
||||
.RE
|
||||
|
||||
.SH LICENSE
|
||||
MIT
|
||||
973
plugin/fzf.vim
973
plugin/fzf.vim
@@ -1,4 +1,4 @@
|
||||
" Copyright (c) 2013 Junegunn Choi
|
||||
" Copyright (c) 2017 Junegunn Choi
|
||||
"
|
||||
" MIT License
|
||||
"
|
||||
@@ -21,27 +21,964 @@
|
||||
" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
let s:exec = expand('<sfile>:h:h').'/fzf'
|
||||
if exists('g:loaded_fzf')
|
||||
finish
|
||||
endif
|
||||
let g:loaded_fzf = 1
|
||||
|
||||
function! fzf#run(command, args)
|
||||
try
|
||||
let tf = tempname()
|
||||
let prefix = exists('g:fzf_source') ? g:fzf_source.'|' : ''
|
||||
let fzf = executable(s:exec) ? s:exec : 'fzf'
|
||||
let options = empty(a:args) ? get(g:, 'fzf_options', '') : a:args
|
||||
execute "silent !".prefix.fzf.' '.options." > ".tf
|
||||
if !v:shell_error
|
||||
for line in readfile(tf)
|
||||
if !empty(line)
|
||||
execute a:command.' '.line
|
||||
endif
|
||||
endfor
|
||||
let s:is_win = has('win32') || has('win64')
|
||||
if s:is_win && &shellslash
|
||||
set noshellslash
|
||||
let s:base_dir = expand('<sfile>:h:h')
|
||||
set shellslash
|
||||
else
|
||||
let s:base_dir = expand('<sfile>:h:h')
|
||||
endif
|
||||
if s:is_win
|
||||
let s:term_marker = '&::FZF'
|
||||
|
||||
function! s:fzf_call(fn, ...)
|
||||
let shellslash = &shellslash
|
||||
try
|
||||
set noshellslash
|
||||
return call(a:fn, a:000)
|
||||
finally
|
||||
let &shellslash = shellslash
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
" Use utf-8 for fzf.vim commands
|
||||
" Return array of shell commands for cmd.exe
|
||||
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']
|
||||
\ + (has('gui_running') ? ['set TERM= > nul'] : [])
|
||||
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds])
|
||||
\ + ['endlocal'],
|
||||
\ '<SID>enc_to_cp(v:val."\r")')
|
||||
endfunction
|
||||
else
|
||||
let s:term_marker = ";#FZF"
|
||||
|
||||
function! s:fzf_call(fn, ...)
|
||||
return call(a:fn, a:000)
|
||||
endfunction
|
||||
|
||||
function! s:wrap_cmds(cmds)
|
||||
return a:cmds
|
||||
endfunction
|
||||
|
||||
function! s:enc_to_cp(str)
|
||||
return a:str
|
||||
endfunction
|
||||
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', '').'^"'
|
||||
endfunction
|
||||
|
||||
function! fzf#shellescape(arg, ...)
|
||||
let shell = get(a:000, 0, s:is_win ? 'cmd.exe' : 'sh')
|
||||
if shell =~# 'cmd.exe$'
|
||||
return s:shellesc_cmd(a:arg)
|
||||
endif
|
||||
return s:fzf_call('shellescape', a:arg)
|
||||
endfunction
|
||||
|
||||
function! s:fzf_getcwd()
|
||||
return s:fzf_call('getcwd')
|
||||
endfunction
|
||||
|
||||
function! s:fzf_fnamemodify(fname, mods)
|
||||
return s:fzf_call('fnamemodify', a:fname, a:mods)
|
||||
endfunction
|
||||
|
||||
function! s:fzf_expand(fmt)
|
||||
return s:fzf_call('expand', a:fmt, 1)
|
||||
endfunction
|
||||
|
||||
function! s:fzf_tempname()
|
||||
return s:fzf_call('tempname')
|
||||
endfunction
|
||||
|
||||
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:cpo_save = &cpo
|
||||
set cpo&vim
|
||||
|
||||
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 ' . 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
|
||||
|
||||
function! s:version_requirement(val, min)
|
||||
let val = split(a:val, '\.')
|
||||
let min = split(a:min, '\.')
|
||||
for idx in range(0, len(min) - 1)
|
||||
let v = get(val, idx, 0)
|
||||
if v < min[idx] | return 0
|
||||
elseif v > min[idx] | return 1
|
||||
endif
|
||||
endfor
|
||||
return 1
|
||||
endfunction
|
||||
|
||||
let s:checked = {}
|
||||
function! fzf#exec(...)
|
||||
if !exists('s:exec')
|
||||
if executable(s:fzf_go)
|
||||
let s:exec = s:fzf_go
|
||||
elseif executable('fzf')
|
||||
let s:exec = 'fzf'
|
||||
elseif 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
|
||||
endif
|
||||
|
||||
if a:0 && !has_key(s:checked, a:1)
|
||||
let command = s:exec . ' --version'
|
||||
let output = systemlist(command)
|
||||
if v:shell_error || empty(output)
|
||||
throw printf('Failed to run "%s": %s', command, output)
|
||||
endif
|
||||
let fzf_version = matchstr(output[0], '[0-9.]\+')
|
||||
if s:version_requirement(fzf_version, a:1)
|
||||
let s:checked[a:1] = 1
|
||||
return s:exec
|
||||
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', a:1, fzf_version)) =~? '^y'
|
||||
redraw
|
||||
call fzf#install()
|
||||
return fzf#exec(a:1, 1)
|
||||
else
|
||||
throw printf('You need to upgrade fzf (required: %s or above)', a:1)
|
||||
endif
|
||||
endif
|
||||
|
||||
return s:exec
|
||||
endfunction
|
||||
|
||||
function! s:tmux_enabled()
|
||||
if has('gui_running') || !exists('$TMUX')
|
||||
return 0
|
||||
endif
|
||||
|
||||
if exists('s:tmux')
|
||||
return s:tmux
|
||||
endif
|
||||
|
||||
let s:tmux = 0
|
||||
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
|
||||
|
||||
function! s:escape(path)
|
||||
let path = fnameescape(a:path)
|
||||
return s:is_win ? escape(path, '$') : path
|
||||
endfunction
|
||||
|
||||
function! s:error(msg)
|
||||
echohl ErrorMsg
|
||||
echom a:msg
|
||||
echohl None
|
||||
endfunction
|
||||
|
||||
function! s:warn(msg)
|
||||
echohl WarningMsg
|
||||
echom a:msg
|
||||
echohl None
|
||||
endfunction
|
||||
|
||||
function! s:has_any(dict, keys)
|
||||
for key in a:keys
|
||||
if has_key(a:dict, key)
|
||||
return 1
|
||||
endif
|
||||
endfor
|
||||
return 0
|
||||
endfunction
|
||||
|
||||
function! s:open(cmd, target)
|
||||
if stridx('edit', a:cmd) == 0 && s:fzf_fnamemodify(a:target, ':p') ==# s:fzf_expand('%:p')
|
||||
return
|
||||
endif
|
||||
execute a:cmd s:escape(a:target)
|
||||
endfunction
|
||||
|
||||
function! s:common_sink(action, lines) abort
|
||||
if len(a:lines) < 2
|
||||
return
|
||||
endif
|
||||
let key = remove(a:lines, 0)
|
||||
let Cmd = get(a:action, key, 'e')
|
||||
if type(Cmd) == type(function('call'))
|
||||
return Cmd(a:lines)
|
||||
endif
|
||||
if len(a:lines) > 1
|
||||
augroup fzf_swap
|
||||
autocmd SwapExists * let v:swapchoice='o'
|
||||
\| call s:warn('fzf: E325: swap file exists: '.s:fzf_expand('<afile>'))
|
||||
augroup END
|
||||
endif
|
||||
try
|
||||
let empty = empty(s:fzf_expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified
|
||||
" 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 item[0] != '~' && item !~ (s:is_win ? '^[A-Z]:\' : '^/')
|
||||
let item = join([cwd, item], (s:is_win ? '\' : '/'))
|
||||
endif
|
||||
if empty
|
||||
execute 'e' s:escape(item)
|
||||
let empty = 0
|
||||
else
|
||||
call s:open(Cmd, item)
|
||||
endif
|
||||
if !has('patch-8.0.0177') && !has('nvim-0.2') && exists('#BufEnter')
|
||||
\ && isdirectory(item)
|
||||
doautocmd BufEnter
|
||||
endif
|
||||
endfor
|
||||
catch /^Vim:Interrupt$/
|
||||
finally
|
||||
redraw!
|
||||
silent! call delete(tf)
|
||||
silent! autocmd! fzf_swap
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
command! -nargs=* FZF call fzf#run('silent e', <q-args>)
|
||||
function! s:get_color(attr, ...)
|
||||
let gui = !s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors
|
||||
let fam = gui ? 'gui' : 'cterm'
|
||||
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
|
||||
for group in a:000
|
||||
let code = synIDattr(synIDtrans(hlID(group)), a:attr, fam)
|
||||
if code =~? pat
|
||||
return code
|
||||
endif
|
||||
endfor
|
||||
return ''
|
||||
endfunction
|
||||
|
||||
function! s:defaults()
|
||||
let rules = copy(get(g:, 'fzf_colors', {}))
|
||||
let colors = join(map(items(filter(map(rules, 'call("s:get_color", v:val)'), '!empty(v:val)')), 'join(v:val, ":")'), ',')
|
||||
return empty(colors) ? '' : fzf#shellescape('--color='.colors)
|
||||
endfunction
|
||||
|
||||
function! s:validate_layout(layout)
|
||||
for key in keys(a:layout)
|
||||
if index(s:layout_keys, key) < 0
|
||||
throw printf('Invalid entry in g:fzf_layout: %s (allowed: %s)%s',
|
||||
\ key, join(s:layout_keys, ', '), key == 'options' ? '. Use $FZF_DEFAULT_OPTS.' : '')
|
||||
endif
|
||||
endfor
|
||||
return a:layout
|
||||
endfunction
|
||||
|
||||
function! s:evaluate_opts(options)
|
||||
return type(a:options) == type([]) ?
|
||||
\ join(map(copy(a:options), 'fzf#shellescape(v:val)')) : a:options
|
||||
endfunction
|
||||
|
||||
" [name string,] [opts dict,] [fullscreen boolean]
|
||||
function! fzf#wrap(...)
|
||||
let args = ['', {}, 0]
|
||||
let expects = map(copy(args), 'type(v:val)')
|
||||
let tidx = 0
|
||||
for arg in copy(a:000)
|
||||
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
|
||||
let args[tidx] = arg
|
||||
let tidx += 1
|
||||
unlet arg
|
||||
endfor
|
||||
let [name, opts, bang] = args
|
||||
|
||||
if len(name)
|
||||
let opts.name = name
|
||||
end
|
||||
|
||||
" Layout: g:fzf_layout (and deprecated g:fzf_height)
|
||||
if bang
|
||||
for key in s:layout_keys
|
||||
if has_key(opts, key)
|
||||
call remove(opts, key)
|
||||
endif
|
||||
endfor
|
||||
elseif !s:has_any(opts, s:layout_keys)
|
||||
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())))
|
||||
endif
|
||||
endif
|
||||
|
||||
" Colors: g:fzf_colors
|
||||
let opts.options = s:defaults() .' '. s:evaluate_opts(get(opts, 'options', ''))
|
||||
|
||||
" History: g:fzf_history_dir
|
||||
if len(name) && len(get(g:, 'fzf_history_dir', ''))
|
||||
let dir = s:fzf_expand(g:fzf_history_dir)
|
||||
if !isdirectory(dir)
|
||||
call mkdir(dir, 'p')
|
||||
endif
|
||||
let history = fzf#shellescape(dir.'/'.name)
|
||||
let opts.options = join(['--history', history, opts.options])
|
||||
endif
|
||||
|
||||
" Action: g:fzf_action
|
||||
if !s:has_any(opts, ['sink', 'sink*'])
|
||||
let opts._action = get(g:, 'fzf_action', s:default_action)
|
||||
let opts.options .= ' --expect='.join(keys(opts._action), ',')
|
||||
function! opts.sink(lines) abort
|
||||
return s:common_sink(self._action, a:lines)
|
||||
endfunction
|
||||
let opts['sink*'] = remove(opts, 'sink')
|
||||
endif
|
||||
|
||||
return opts
|
||||
endfunction
|
||||
|
||||
function! s:use_sh()
|
||||
let [shell, shellslash, shellcmdflag, shellxquote] = [&shell, &shellslash, &shellcmdflag, &shellxquote]
|
||||
if s:is_win
|
||||
set shell=cmd.exe
|
||||
set noshellslash
|
||||
let &shellcmdflag = has('nvim') ? '/s /c' : '/c'
|
||||
let &shellxquote = has('nvim') ? '"' : '('
|
||||
else
|
||||
set shell=sh
|
||||
endif
|
||||
return [shell, shellslash, shellcmdflag, shellxquote]
|
||||
endfunction
|
||||
|
||||
function! fzf#run(...) abort
|
||||
try
|
||||
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
|
||||
|
||||
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 = fzf#shellescape(fzf#exec())
|
||||
catch
|
||||
throw v:exception
|
||||
endtry
|
||||
|
||||
if !has_key(dict, 'dir')
|
||||
let dict.dir = s:fzf_getcwd()
|
||||
endif
|
||||
if has('win32unix') && has_key(dict, 'dir')
|
||||
let dict.dir = fnamemodify(dict.dir, ':p')
|
||||
endif
|
||||
|
||||
if has_key(dict, 'source')
|
||||
let source = dict.source
|
||||
let type = type(source)
|
||||
if type == 1
|
||||
let prefix = '( '.source.' )|'
|
||||
elseif type == 3
|
||||
let temps.input = s:fzf_tempname()
|
||||
call writefile(source, temps.input)
|
||||
let prefix = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input).'|'
|
||||
else
|
||||
throw 'Invalid source type'
|
||||
endif
|
||||
else
|
||||
let prefix = ''
|
||||
endif
|
||||
|
||||
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 = (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
|
||||
let height = s:calc_size(&lines, dict.down, dict)
|
||||
let optstr .= ' --height='.height
|
||||
elseif use_term
|
||||
let optstr .= ' --no-height'
|
||||
endif
|
||||
let optstr .= s:border_opt(get(dict, 'window', 0))
|
||||
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
|
||||
|
||||
if use_term
|
||||
return s:execute_term(dict, command, temps)
|
||||
endif
|
||||
|
||||
let lines = use_tmux ? s:execute_tmux(dict, command, temps)
|
||||
\ : s:execute(dict, command, use_height, temps)
|
||||
call s:callback(dict, lines)
|
||||
return lines
|
||||
finally
|
||||
let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote]
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
function! s:present(dict, ...)
|
||||
for key in a:000
|
||||
if !empty(get(a:dict, key, ''))
|
||||
return 1
|
||||
endif
|
||||
endfor
|
||||
return 0
|
||||
endfunction
|
||||
|
||||
function! s:fzf_tmux(dict)
|
||||
let size = get(a:dict, 'tmux', '')
|
||||
if empty(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, '^\~', '', ''))
|
||||
endif
|
||||
break
|
||||
endif
|
||||
endfor
|
||||
endif
|
||||
return printf('LINES=%d COLUMNS=%d %s %s %s --',
|
||||
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
|
||||
endfunction
|
||||
|
||||
function! s:splittable(dict)
|
||||
return s:present(a:dict, 'up', 'down') && &lines > 15 ||
|
||||
\ s:present(a:dict, 'left', 'right') && &columns > 40
|
||||
endfunction
|
||||
|
||||
function! s:pushd(dict)
|
||||
if s:present(a:dict, 'dir')
|
||||
let cwd = s:fzf_getcwd()
|
||||
let w:fzf_pushd = {
|
||||
\ 'command': haslocaldir() ? 'lcd' : (exists(':tcd') && haslocaldir(-1) ? 'tcd' : 'cd'),
|
||||
\ 'origin': cwd,
|
||||
\ 'bufname': bufname('')
|
||||
\ }
|
||||
execute 'lcd' s:escape(a:dict.dir)
|
||||
let cwd = s:fzf_getcwd()
|
||||
let w:fzf_pushd.dir = cwd
|
||||
let a:dict.pushd = w:fzf_pushd
|
||||
return cwd
|
||||
endif
|
||||
return ''
|
||||
endfunction
|
||||
|
||||
augroup fzf_popd
|
||||
autocmd!
|
||||
autocmd WinEnter * call s:dopopd()
|
||||
augroup END
|
||||
|
||||
function! s:dopopd()
|
||||
if !exists('w:fzf_pushd')
|
||||
return
|
||||
endif
|
||||
|
||||
" FIXME: We temporarily change the working directory to 'dir' entry
|
||||
" of options dictionary (set to the current working directory if not given)
|
||||
" before running fzf.
|
||||
"
|
||||
" e.g. call fzf#run({'dir': '/tmp', 'source': 'ls', 'sink': 'e'})
|
||||
"
|
||||
" After processing the sink function, we have to restore the current working
|
||||
" directory. But doing so may not be desirable if the function changed the
|
||||
" working directory on purpose.
|
||||
"
|
||||
" So how can we tell if we should do it or not? A simple heuristic we use
|
||||
" here is that we change directory only if the current working directory
|
||||
" matches 'dir' entry. However, it is possible that the sink function did
|
||||
" change the directory to 'dir'. In that case, the user will have an
|
||||
" unexpected result.
|
||||
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
|
||||
endfunction
|
||||
|
||||
function! s:xterm_launcher()
|
||||
let fmt = 'xterm -T "[fzf]" -bg "%s" -fg "%s" -geometry %dx%d+%d+%d -e bash -ic %%s'
|
||||
if has('gui_macvim')
|
||||
let fmt .= '&& osascript -e "tell application \"MacVim\" to activate"'
|
||||
endif
|
||||
return printf(fmt,
|
||||
\ escape(synIDattr(hlID("Normal"), "bg"), '#'), escape(synIDattr(hlID("Normal"), "fg"), '#'),
|
||||
\ &columns, &lines/2, getwinposx(), getwinposy())
|
||||
endfunction
|
||||
unlet! s:launcher
|
||||
if s:is_win || has('win32unix')
|
||||
let s:launcher = '%s'
|
||||
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
|
||||
call s:error('Error running ' . a:command)
|
||||
if !empty(a:000)
|
||||
sleep
|
||||
endif
|
||||
return 0
|
||||
endif
|
||||
return 1
|
||||
endfunction
|
||||
|
||||
function! s:execute(dict, command, use_height, temps) abort
|
||||
call s:pushd(a:dict)
|
||||
if has('unix') && !a:use_height
|
||||
silent! !clear 2> /dev/null
|
||||
endif
|
||||
let escaped = (a:use_height || s:is_win) ? a:command : escape(substitute(a:command, '\n', '\\n', 'g'), '%#!')
|
||||
if has('gui_running')
|
||||
let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher)))
|
||||
let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher
|
||||
if has('unix')
|
||||
let escaped = "'".substitute(escaped, "'", "'\"'\"'", 'g')."'"
|
||||
endif
|
||||
let command = printf(fmt, escaped)
|
||||
else
|
||||
let command = escaped
|
||||
endif
|
||||
if s:is_win
|
||||
let batchfile = s:fzf_tempname().'.bat'
|
||||
call writefile(s:wrap_cmds(command), batchfile)
|
||||
let command = batchfile
|
||||
let a:temps.batchfile = batchfile
|
||||
if has('nvim')
|
||||
let fzf = {}
|
||||
let fzf.dict = a:dict
|
||||
let fzf.temps = a:temps
|
||||
function! fzf.on_exit(job_id, exit_status, event) dict
|
||||
call s:pushd(self.dict)
|
||||
let lines = s:collect(self.temps)
|
||||
call s:callback(self.dict, lines)
|
||||
endfunction
|
||||
let cmd = 'start /wait cmd /c '.command
|
||||
call jobstart(cmd, fzf)
|
||||
return []
|
||||
endif
|
||||
elseif has('win32unix') && $TERM !=# 'cygwin'
|
||||
let shellscript = s:fzf_tempname()
|
||||
call writefile([command], shellscript)
|
||||
let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript)
|
||||
let a:temps.shellscript = shellscript
|
||||
endif
|
||||
if a:use_height
|
||||
let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty'
|
||||
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin))
|
||||
else
|
||||
execute 'silent !'.command
|
||||
endif
|
||||
let exit_status = v:shell_error
|
||||
redraw!
|
||||
return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
|
||||
endfunction
|
||||
|
||||
function! s:execute_tmux(dict, command, temps) abort
|
||||
let command = a:command
|
||||
let cwd = s:pushd(a:dict)
|
||||
if len(cwd)
|
||||
" -c '#{pane_current_path}' is only available on tmux 1.9 or above
|
||||
let command = join(['cd', fzf#shellescape(cwd), '&&', command])
|
||||
endif
|
||||
|
||||
call system(command)
|
||||
let exit_status = v:shell_error
|
||||
redraw!
|
||||
return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
|
||||
endfunction
|
||||
|
||||
function! s:calc_size(max, val, dict)
|
||||
let val = substitute(a:val, '^\~', '', '')
|
||||
if val =~ '%$'
|
||||
let size = a:max * str2nr(val[:-2]) / 100
|
||||
else
|
||||
let size = min([a:max, str2nr(val)])
|
||||
endif
|
||||
|
||||
let srcsz = -1
|
||||
if type(get(a:dict, 'source', 0)) == type([])
|
||||
let srcsz = len(a:dict.source)
|
||||
endif
|
||||
|
||||
let opts = $FZF_DEFAULT_OPTS.' '.s:evaluate_opts(get(a:dict, 'options', ''))
|
||||
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 += stridx(opts, '--border') > stridx(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(), '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', 'rounded'))
|
||||
if !has_key(a:window, 'border') && !get(a:window, 'rounded', 1)
|
||||
let style = 'sharp'
|
||||
endif
|
||||
if style == 'none'
|
||||
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)
|
||||
let directions = {
|
||||
\ 'up': ['topleft', 'resize', &lines],
|
||||
\ 'down': ['botright', 'resize', &lines],
|
||||
\ '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')
|
||||
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
|
||||
for [dir, triple] in items(directions)
|
||||
let val = get(a:dict, dir, '')
|
||||
if !empty(val)
|
||||
let [cmd, resz, max] = triple
|
||||
if (dir == 'up' || dir == 'down') && val[0] == '~'
|
||||
let sz = s:calc_size(max, val, a:dict)
|
||||
else
|
||||
let sz = s:calc_size(max, val, {})
|
||||
endif
|
||||
execute cmd sz.'new'
|
||||
execute resz sz
|
||||
return [ppos, {}, is_popup]
|
||||
endif
|
||||
endfor
|
||||
endif
|
||||
return [ppos, is_popup ? {} : { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh }, is_popup]
|
||||
finally
|
||||
if !is_popup
|
||||
setlocal winfixwidth winfixheight
|
||||
endif
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
function! s:execute_term(dict, command, temps) abort
|
||||
let winrest = winrestcmd()
|
||||
let pbuf = bufnr('')
|
||||
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,
|
||||
\ 'winopts': winopts, 'winrest': winrest, 'lines': &lines,
|
||||
\ 'columns': &columns, 'command': a:command }
|
||||
function! fzf.switch_back(inplace)
|
||||
if a:inplace && bufnr('') == self.buf
|
||||
if bufexists(self.pbuf)
|
||||
execute 'keepalt b' self.pbuf
|
||||
endif
|
||||
" No other listed buffer
|
||||
if bufnr('') == self.buf
|
||||
enew
|
||||
endif
|
||||
endif
|
||||
endfunction
|
||||
function! fzf.on_exit(id, code, ...)
|
||||
if s:getpos() == self.ppos " {'window': 'enew'}
|
||||
for [opt, val] in items(self.winopts)
|
||||
execute 'let' opt '=' val
|
||||
endfor
|
||||
call self.switch_back(1)
|
||||
else
|
||||
if bufnr('') == self.buf
|
||||
" We use close instead of bd! since Vim does not close the split when
|
||||
" there's no other listed buffer (nvim +'set nobuflisted')
|
||||
close
|
||||
endif
|
||||
silent! execute 'tabnext' self.ppos.tab
|
||||
silent! execute self.ppos.win.'wincmd w'
|
||||
endif
|
||||
|
||||
if bufexists(self.buf)
|
||||
execute 'bd!' self.buf
|
||||
endif
|
||||
|
||||
if &lines == self.lines && &columns == self.columns && s:getpos() == self.ppos
|
||||
execute self.winrest
|
||||
endif
|
||||
|
||||
if !s:exit_handler(a:code, self.command, 1)
|
||||
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)
|
||||
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)
|
||||
let command = fzf.temps.batchfile
|
||||
else
|
||||
let command = a:command
|
||||
endif
|
||||
let command .= s:term_marker
|
||||
if has('nvim')
|
||||
call termopen(command, fzf)
|
||||
else
|
||||
let term_opts = {'exit_cb': function(fzf.on_exit)}
|
||||
if is_popup
|
||||
let term_opts.hidden = 1
|
||||
else
|
||||
let term_opts.curwin = 1
|
||||
endif
|
||||
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
|
||||
finally
|
||||
call s:dopopd()
|
||||
endtry
|
||||
setlocal nospell bufhidden=wipe nobuflisted nonumber
|
||||
setf fzf
|
||||
startinsert
|
||||
return []
|
||||
endfunction
|
||||
|
||||
function! s:collect(temps) abort
|
||||
try
|
||||
return filereadable(a:temps.result) ? readfile(a:temps.result) : []
|
||||
finally
|
||||
for tf in values(a:temps)
|
||||
silent! call delete(tf)
|
||||
endfor
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
function! s:callback(dict, lines) abort
|
||||
let popd = has_key(a:dict, 'pushd')
|
||||
if popd
|
||||
let w:fzf_pushd = a:dict.pushd
|
||||
endif
|
||||
|
||||
try
|
||||
if has_key(a:dict, 'sink')
|
||||
for line in a:lines
|
||||
if type(a:dict.sink) == 2
|
||||
call a:dict.sink(line)
|
||||
else
|
||||
execute a:dict.sink s:escape(line)
|
||||
endif
|
||||
endfor
|
||||
endif
|
||||
if has_key(a:dict, 'sink*')
|
||||
call a:dict['sink*'](a:lines)
|
||||
endif
|
||||
catch
|
||||
if stridx(v:exception, ':E325:') < 0
|
||||
echoerr v:exception
|
||||
endif
|
||||
endtry
|
||||
|
||||
" We may have opened a new window or tab
|
||||
if popd
|
||||
let w:fzf_pushd = a:dict.pushd
|
||||
call s:dopopd()
|
||||
endif
|
||||
endfunction
|
||||
|
||||
if has('nvim')
|
||||
function s:create_popup(hl, opts) abort
|
||||
let buf = nvim_create_buf(v:false, v:true)
|
||||
let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
|
||||
let border = has_key(opts, 'border') ? remove(opts, 'border') : []
|
||||
let win = nvim_open_win(buf, v:true, opts)
|
||||
call setwinvar(win, '&winhighlight', 'NormalFloat:'..a:hl)
|
||||
call setwinvar(win, '&colorcolumn', '')
|
||||
if !empty(border)
|
||||
call nvim_buf_set_lines(buf, 0, -1, v:true, border)
|
||||
endif
|
||||
return buf
|
||||
endfunction
|
||||
else
|
||||
function! s:create_popup(hl, opts) abort
|
||||
let is_frame = has_key(a:opts, 'border')
|
||||
let s:popup_create = {buf -> popup_create(buf, #{
|
||||
\ line: a:opts.row,
|
||||
\ col: a:opts.col,
|
||||
\ minwidth: a:opts.width,
|
||||
\ minheight: a:opts.height,
|
||||
\ zindex: 50 - is_frame,
|
||||
\ })}
|
||||
if is_frame
|
||||
let id = s:popup_create('')
|
||||
call setwinvar(id, '&wincolor', a:hl)
|
||||
call setbufline(winbufnr(id), 1, a:opts.border)
|
||||
execute 'autocmd BufWipeout * ++once call popup_close('..id..')'
|
||||
return winbufnr(id)
|
||||
else
|
||||
autocmd TerminalOpen * ++once call s:popup_create(str2nr(expand('<abuf>')))
|
||||
endif
|
||||
endfunction
|
||||
endif
|
||||
|
||||
function! s:popup(opts) abort
|
||||
" Size and position
|
||||
let width = min([max([8, a:opts.width > 1 ? a:opts.width : float2nr(&columns * a:opts.width)]), &columns])
|
||||
let height = min([max([4, a:opts.height > 1 ? a:opts.height : float2nr(&lines * a:opts.height)]), &lines - has('nvim')])
|
||||
let row = float2nr(get(a:opts, 'yoffset', 0.5) * (&lines - height))
|
||||
let col = float2nr(get(a:opts, 'xoffset', 0.5) * (&columns - width))
|
||||
|
||||
" 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')
|
||||
|
||||
call s:create_popup('Normal', {
|
||||
\ 'row': row, 'col': col, 'width': width, 'height': height
|
||||
\ })
|
||||
endfunction
|
||||
|
||||
let s:default_action = {
|
||||
\ 'ctrl-t': 'tab split',
|
||||
\ 'ctrl-x': 'split',
|
||||
\ 'ctrl-v': 'vsplit' }
|
||||
|
||||
function! s:shortpath()
|
||||
let short = fnamemodify(getcwd(), ':~:.')
|
||||
if !has('win32unix')
|
||||
let short = pathshorten(short)
|
||||
endif
|
||||
let slash = (s:is_win && !&shellslash) ? '\' : '/'
|
||||
return empty(short) ? '~'.slash : short . (short =~ escape(slash, '\').'$' ? '' : slash)
|
||||
endfunction
|
||||
|
||||
function! s:cmd(bang, ...) abort
|
||||
let args = copy(a:000)
|
||||
let opts = { 'options': ['--multi'] }
|
||||
if len(args) && isdirectory(expand(args[-1]))
|
||||
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
||||
if s:is_win && !&shellslash
|
||||
let opts.dir = substitute(opts.dir, '/', '\\', 'g')
|
||||
endif
|
||||
let prompt = opts.dir
|
||||
else
|
||||
let prompt = s:shortpath()
|
||||
endif
|
||||
let prompt = strwidth(prompt) < &columns - 20 ? prompt : '> '
|
||||
call extend(opts.options, ['--prompt', prompt])
|
||||
call extend(opts.options, args)
|
||||
call fzf#run(fzf#wrap('FZF', opts, a:bang))
|
||||
endfunction
|
||||
|
||||
command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>)
|
||||
|
||||
let &cpo = s:cpo_save
|
||||
unlet s:cpo_save
|
||||
|
||||
372
shell/completion.bash
Normal file
372
shell/completion.bash
Normal file
@@ -0,0 +1,372 @@
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ completion.bash
|
||||
#
|
||||
# - $FZF_TMUX (default: 0)
|
||||
# - $FZF_TMUX_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_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 redraw line after fzf closes (printf '\e[5n')
|
||||
bind '"\e[0n": redraw-current-line'
|
||||
|
||||
__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_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_opts_completion() {
|
||||
local cur prev opts
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
opts="
|
||||
-x --extended
|
||||
-e --exact
|
||||
--algo
|
||||
-i +i
|
||||
-n --nth
|
||||
--with-nth
|
||||
-d --delimiter
|
||||
+s --no-sort
|
||||
--tac
|
||||
--tiebreak
|
||||
-m --multi
|
||||
--no-mouse
|
||||
--bind
|
||||
--cycle
|
||||
--no-hscroll
|
||||
--jump-labels
|
||||
--height
|
||||
--literal
|
||||
--reverse
|
||||
--margin
|
||||
--inline-info
|
||||
--prompt
|
||||
--pointer
|
||||
--marker
|
||||
--header
|
||||
--header-lines
|
||||
--ansi
|
||||
--tabstop
|
||||
--color
|
||||
--no-bold
|
||||
--history
|
||||
--history-size
|
||||
--preview
|
||||
--preview-window
|
||||
-q --query
|
||||
-1 --select-1
|
||||
-0 --exit-0
|
||||
-f --filter
|
||||
--print-query
|
||||
--expect
|
||||
--sync"
|
||||
|
||||
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
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$cur" =~ ^-|\+ ]]; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "$cur") )
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_fzf_handle_dynamic_completion() {
|
||||
local cmd orig_var orig ret 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
|
||||
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
|
||||
_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
|
||||
eval "${orig_complete/ -F / -o nospace -F }"
|
||||
else
|
||||
eval "$orig_complete"
|
||||
fi
|
||||
fi
|
||||
return $ret
|
||||
fi
|
||||
}
|
||||
|
||||
__fzf_generic_path_completion() {
|
||||
local cur base dir leftover matches trigger cmd
|
||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||
COMPREPLY=()
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]]; then
|
||||
base=${cur:0:${#cur}-${#trigger}}
|
||||
eval "base=$base"
|
||||
|
||||
[[ $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 $2" __fzf_comprun "$4" -q "$leftover" | while read -r item; do
|
||||
printf "%q$3 " "$item"
|
||||
done)
|
||||
matches=${matches% }
|
||||
[[ -z "$3" ]] && [[ "$__fzf_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||
if [ -n "$matches" ]; then
|
||||
COMPREPLY=( "$matches" )
|
||||
else
|
||||
COMPREPLY=( "$cur" )
|
||||
fi
|
||||
printf '\e[5n'
|
||||
return 0
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
[[ "$dir" =~ /$ ]] || dir="$dir"/
|
||||
done
|
||||
else
|
||||
shift
|
||||
shift
|
||||
shift
|
||||
_fzf_handle_dynamic_completion "$cmd" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
_fzf_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 | awk '{print $2}')_post"
|
||||
type -t "$post" > /dev/null 2>&1 || post=cat
|
||||
|
||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]]; then
|
||||
cur=${cur:0:${#cur}-${#trigger}}
|
||||
|
||||
selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | tr '\n' ' ')
|
||||
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
||||
if [ -n "$selected" ]; then
|
||||
COMPREPLY=("$selected")
|
||||
else
|
||||
COMPREPLY=("$cur")
|
||||
fi
|
||||
printf '\e[5n'
|
||||
return 0
|
||||
else
|
||||
_fzf_handle_dynamic_completion "$cmd" "${rest[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
_fzf_path_completion() {
|
||||
__fzf_generic_path_completion _fzf_compgen_path "-m" "" "$@"
|
||||
}
|
||||
|
||||
# Deprecated. No file only completion.
|
||||
_fzf_file_completion() {
|
||||
_fzf_path_completion "$@"
|
||||
}
|
||||
|
||||
_fzf_dir_completion() {
|
||||
__fzf_generic_path_completion _fzf_compgen_dir "" "/" "$@"
|
||||
}
|
||||
|
||||
_fzf_complete_kill() {
|
||||
local trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ -z "$cur" ]]; then
|
||||
COMP_WORDS[$COMP_CWORD]=$trigger
|
||||
elif [[ "$cur" != *"$trigger" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_fzf_proc_completion "$@"
|
||||
}
|
||||
|
||||
_fzf_proc_completion() {
|
||||
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -ef | sed 1d
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_proc_completion_post() {
|
||||
awk '{print $2}'
|
||||
}
|
||||
|
||||
_fzf_host_completion() {
|
||||
_fzf_complete +m -- "$@" < <(
|
||||
command cat <(command tail -n +1 ~/.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
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_var_completion() {
|
||||
_fzf_complete -m -- "$@" < <(
|
||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_alias_completion() {
|
||||
_fzf_complete -m -- "$@" < <(
|
||||
alias | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
# fzf options
|
||||
complete -o default -F _fzf_opts_completion fzf
|
||||
|
||||
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
|
||||
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"
|
||||
|
||||
# Preserve existing completion
|
||||
eval "$(complete |
|
||||
sed -E '/-F/!d; / _fzf/d; '"/ ($(echo $d_cmds $a_cmds | sed 's/ /|/g; s/+/\\+/g'))$/"'!d' |
|
||||
__fzf_orig_completion_filter)"
|
||||
|
||||
if type _completion_loader > /dev/null 2>&1; then
|
||||
_fzf_completion_loader=1
|
||||
fi
|
||||
|
||||
__fzf_defc() {
|
||||
local cmd func opts orig_var orig def
|
||||
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"
|
||||
else
|
||||
complete -F "$func" $opts "$cmd"
|
||||
fi
|
||||
}
|
||||
|
||||
# Anything
|
||||
for cmd in $a_cmds; do
|
||||
__fzf_defc "$cmd" _fzf_path_completion "-o default -o bashdefault"
|
||||
done
|
||||
|
||||
# Directory
|
||||
for cmd in $d_cmds; do
|
||||
__fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
|
||||
done
|
||||
|
||||
# Kill completion (supports empty completion trigger)
|
||||
complete -F _fzf_complete_kill -o default -o bashdefault kill
|
||||
|
||||
unset cmd d_cmds a_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|var|alias|host|proc COMMANDS..."
|
||||
return 1
|
||||
fi
|
||||
shift
|
||||
eval "$(complete -p "$@" 2> /dev/null | grep -v "$fn" | __fzf_orig_completion_filter)"
|
||||
for cmd in "$@"; do
|
||||
case "$kind" in
|
||||
dir) __fzf_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
|
||||
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
|
||||
}
|
||||
|
||||
# Environment variables / Aliases / Hosts
|
||||
_fzf_setup_completion 'var' export unset
|
||||
_fzf_setup_completion 'alias' unalias
|
||||
_fzf_setup_completion 'host' ssh telnet
|
||||
|
||||
fi
|
||||
329
shell/completion.zsh
Normal file
329
shell/completion.zsh
Normal file
@@ -0,0 +1,329 @@
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ completion.zsh
|
||||
#
|
||||
# - $FZF_TMUX (default: 0)
|
||||
# - $FZF_TMUX_OPTS (default: '-d 40%')
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
|
||||
# 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.
|
||||
'emulate' 'zsh' '-o' '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.
|
||||
{
|
||||
|
||||
# Bail out if not interactive shell.
|
||||
[[ -o interactive ]] || return 0
|
||||
|
||||
# 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_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. foo=1 bar baz**<tab>
|
||||
__fzf_extract_command() {
|
||||
local token tokens
|
||||
tokens=(${(z)1})
|
||||
for token in $tokens; do
|
||||
token=${(Q)token}
|
||||
if [[ "$token" =~ [[:alnum:]] && ! "$token" =~ "=" ]]; then
|
||||
echo "$token"
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo "${tokens[1]}"
|
||||
}
|
||||
|
||||
__fzf_generic_path_completion() {
|
||||
local base lbuf cmd compgen fzf_opts suffix tail dir leftover matches
|
||||
base=$1
|
||||
lbuf=$2
|
||||
cmd=$(__fzf_extract_command "$lbuf")
|
||||
compgen=$3
|
||||
fzf_opts=$4
|
||||
suffix=$5
|
||||
tail=$6
|
||||
|
||||
setopt localoptions nonomatch
|
||||
eval "base=$base"
|
||||
[[ $base = *"/"* ]] && dir="$base"
|
||||
while [ 1 ]; do
|
||||
if [[ -z "$dir" || -d ${dir} ]]; then
|
||||
leftover=${base/#"$dir"}
|
||||
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_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" | while read item; do
|
||||
echo -n "${(q)item}$suffix "
|
||||
done)
|
||||
matches=${matches% }
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$lbuf$matches$tail"
|
||||
fi
|
||||
zle reset-prompt
|
||||
break
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
dir=${dir%/}/
|
||||
done
|
||||
}
|
||||
|
||||
_fzf_path_completion() {
|
||||
__fzf_generic_path_completion "$1" "$2" _fzf_compgen_path \
|
||||
"-m" "" " "
|
||||
}
|
||||
|
||||
_fzf_dir_completion() {
|
||||
__fzf_generic_path_completion "$1" "$2" _fzf_compgen_dir \
|
||||
"" "/" ""
|
||||
}
|
||||
|
||||
_fzf_feed_fifo() (
|
||||
command rm -f "$1"
|
||||
mkfifo "$1"
|
||||
cat <&0 > "$1" &
|
||||
)
|
||||
|
||||
_fzf_complete() {
|
||||
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 cmd matches post
|
||||
fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
|
||||
lbuf=${rest[0]}
|
||||
cmd=$(__fzf_extract_command "$lbuf")
|
||||
post="${funcstack[1]}_post"
|
||||
type $post > /dev/null 2>&1 || post=cat
|
||||
|
||||
_fzf_feed_fifo "$fifo"
|
||||
matches=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $str_arg" __fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$lbuf$matches"
|
||||
fi
|
||||
zle reset-prompt
|
||||
command rm -f "$fifo"
|
||||
}
|
||||
|
||||
_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_ssh() {
|
||||
_fzf_complete +m -- "$@" < <(
|
||||
setopt localoptions nonomatch
|
||||
command cat <(command tail -n +1 ~/.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
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_export() {
|
||||
_fzf_complete -m -- "$@" < <(
|
||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_unset() {
|
||||
_fzf_complete -m -- "$@" < <(
|
||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_unalias() {
|
||||
_fzf_complete +m -- "$@" < <(
|
||||
alias | sed 's/=.*//'
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_kill() {
|
||||
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -ef | sed 1d
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_kill_post() {
|
||||
awk '{print $2}'
|
||||
}
|
||||
|
||||
fzf-completion() {
|
||||
local tokens cmd prefix trigger tail matches lbuf d_cmds
|
||||
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
|
||||
|
||||
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
|
||||
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
|
||||
tokens=(${(z)LBUFFER})
|
||||
if [ ${#tokens} -lt 1 ]; then
|
||||
zle ${fzf_default_completion:-expand-or-complete}
|
||||
return
|
||||
fi
|
||||
|
||||
cmd=$(__fzf_extract_command "$LBUFFER")
|
||||
|
||||
# Explicitly allow for empty trigger.
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
[ -z "$trigger" -a ${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]}"
|
||||
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
|
||||
tail=$trigger
|
||||
tokens+=$trigger
|
||||
lbuf="$lbuf$trigger"
|
||||
fi
|
||||
|
||||
# Trigger sequence given
|
||||
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
|
||||
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
|
||||
|
||||
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
|
||||
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
|
||||
|
||||
if eval "type _fzf_complete_${cmd} > /dev/null"; then
|
||||
prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf}
|
||||
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
|
||||
_fzf_dir_completion "$prefix" "$lbuf"
|
||||
else
|
||||
_fzf_path_completion "$prefix" "$lbuf"
|
||||
fi
|
||||
# Fall back to default completion
|
||||
else
|
||||
zle ${fzf_default_completion:-expand-or-complete}
|
||||
fi
|
||||
}
|
||||
|
||||
[ -z "$fzf_default_completion" ] && {
|
||||
binding=$(bindkey '^I')
|
||||
[[ $binding =~ 'undefined-key' ]] || fzf_default_completion=$binding[(s: :w)2]
|
||||
unset binding
|
||||
}
|
||||
|
||||
zle -N fzf-completion
|
||||
bindkey '^I' fzf-completion
|
||||
|
||||
} always {
|
||||
# Restore the original options.
|
||||
eval $__fzf_completion_options
|
||||
'unset' '__fzf_completion_options'
|
||||
}
|
||||
96
shell/key-bindings.bash
Normal file
96
shell/key-bindings.bash
Normal file
@@ -0,0 +1,96 @@
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ key-bindings.bash
|
||||
#
|
||||
# - $FZF_TMUX_OPTS
|
||||
# - $FZF_CTRL_T_COMMAND
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_OPTS
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
# 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" $(__fzfcmd) -m "$@" | while read -r item; do
|
||||
printf '%q ' "$item"
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
if [[ $- =~ i ]]; then
|
||||
|
||||
__fzfcmd() {
|
||||
[ -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() {
|
||||
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"
|
||||
}
|
||||
|
||||
__fzf_history__() {
|
||||
local output
|
||||
output=$(
|
||||
builtin fc -lnr -2147483648 |
|
||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e 'BEGIN { getc; $/ = "\n\t"; $HISTCMD = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCMD - $. . "\t$_" if !$seen{$_}++' |
|
||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m --read0" $(__fzfcmd) --query "$READLINE_LINE"
|
||||
) || return
|
||||
READLINE_LINE=${output#*$'\t'}
|
||||
if [ -z "$READLINE_POINT" ]; then
|
||||
echo "$READLINE_LINE"
|
||||
else
|
||||
READLINE_POINT=0x7fffffff
|
||||
fi
|
||||
}
|
||||
|
||||
# Required to refresh the prompt after fzf
|
||||
bind -m emacs-standard '"\er": 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]}" -lt 4 ]; then
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
|
||||
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
||||
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u"$(__fzf_history__)"\e\C-e\er"'
|
||||
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
|
||||
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
|
||||
else
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
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'
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
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
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
|
||||
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
|
||||
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
|
||||
|
||||
fi
|
||||
161
shell/key-bindings.fish
Normal file
161
shell/key-bindings.fish
Normal file
@@ -0,0 +1,161 @@
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ key-bindings.fish
|
||||
#
|
||||
# - $FZF_TMUX_OPTS
|
||||
# - $FZF_CTRL_T_COMMAND
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $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.
|
||||
test -n "$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@^\./@@'"
|
||||
|
||||
test -n "$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
|
||||
end
|
||||
|
||||
function fzf-history-widget -d "Show command history"
|
||||
test -n "$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]
|
||||
|
||||
test -n "$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@^\./@@'"
|
||||
test -n "$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
|
||||
end
|
||||
|
||||
function __fzfcmd
|
||||
test -n "$FZF_TMUX"; or set FZF_TMUX 0
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
if [ -n "$FZF_TMUX_OPTS" ]
|
||||
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||
else if [ $FZF_TMUX -eq 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
|
||||
|
||||
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
|
||||
|
||||
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))
|
||||
|
||||
if [ -z $commandline ]
|
||||
# Default to current directory with no --query
|
||||
set dir '.'
|
||||
set fzf_query ''
|
||||
else
|
||||
set dir (__fzf_get_dir $commandline)
|
||||
|
||||
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
|
||||
else
|
||||
# Also remove trailing slash after dir, to "split" input properly
|
||||
set fzf_query (string replace -r "^$dir/?" -- '' "$commandline")
|
||||
end
|
||||
end
|
||||
|
||||
echo $dir
|
||||
echo $fzf_query
|
||||
end
|
||||
|
||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
||||
set dir $argv
|
||||
|
||||
# Strip all trailing slashes. Ignore if $dir is root dir (/)
|
||||
if [ (string length -- $dir) -gt 1 ]
|
||||
set dir (string replace -r '/*$' -- '' $dir)
|
||||
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")
|
||||
end
|
||||
|
||||
echo $dir
|
||||
end
|
||||
|
||||
end
|
||||
127
shell/key-bindings.zsh
Normal file
127
shell/key-bindings.zsh
Normal file
@@ -0,0 +1,127 @@
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ key-bindings.zsh
|
||||
#
|
||||
# - $FZF_TMUX_OPTS
|
||||
# - $FZF_CTRL_T_COMMAND
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_OPTS
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
|
||||
# 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
|
||||
|
||||
'emulate' 'zsh' '-o' 'no_aliases'
|
||||
|
||||
{
|
||||
|
||||
[[ -o interactive ]] || return 0
|
||||
|
||||
# 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-"}"
|
||||
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} "
|
||||
done
|
||||
local ret=$?
|
||||
echo
|
||||
return $ret
|
||||
}
|
||||
|
||||
__fzfcmd() {
|
||||
[ -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)"
|
||||
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
|
||||
|
||||
# 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)"
|
||||
if [[ -z "$dir" ]]; then
|
||||
zle redisplay
|
||||
return 0
|
||||
fi
|
||||
if [ -z "$BUFFER" ]; then
|
||||
BUFFER="cd ${(q)dir}"
|
||||
zle accept-line
|
||||
else
|
||||
print -sr "cd ${(q)dir}"
|
||||
cd "$dir"
|
||||
fi
|
||||
local ret=$?
|
||||
unset dir # ensure this doesn't end up appearing in prompt expansion
|
||||
zle fzf-redraw-prompt
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-cd-widget
|
||||
bindkey '\ec' fzf-cd-widget
|
||||
|
||||
# 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 | perl -ne 'print if !$seen{(/^\s*[0-9]+\**\s+(.*)/, $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 ret=$?
|
||||
if [ -n "$selected" ]; then
|
||||
num=$selected[1]
|
||||
if [ -n "$num" ]; then
|
||||
zle vi-fetch-history -n $num
|
||||
fi
|
||||
fi
|
||||
zle reset-prompt
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-history-widget
|
||||
bindkey '^R' fzf-history-widget
|
||||
|
||||
} always {
|
||||
eval $__fzf_key_bindings_options
|
||||
'unset' '__fzf_key_bindings_options'
|
||||
}
|
||||
21
src/LICENSE
Normal file
21
src/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2020 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.
|
||||
884
src/algo/algo.go
Normal file
884
src/algo/algo.go
Normal file
@@ -0,0 +1,884 @@
|
||||
package algo
|
||||
|
||||
/*
|
||||
|
||||
Algorithm
|
||||
---------
|
||||
|
||||
FuzzyMatchV1 finds the first "fuzzy" occurrence of the pattern within the given
|
||||
text in O(n) time where n is the length of the text. Once the position of the
|
||||
last character is located, it traverses backwards to see if there's a shorter
|
||||
substring that matches the pattern.
|
||||
|
||||
a_____b___abc__ To find "abc"
|
||||
*-----*-----*> 1. Forward scan
|
||||
<*** 2. Backward scan
|
||||
|
||||
The algorithm is simple and fast, but as it only sees the first occurrence,
|
||||
it is not guaranteed to find the occurrence with the highest score.
|
||||
|
||||
a_____b__c__abc
|
||||
*-----*--* ***
|
||||
|
||||
FuzzyMatchV2 implements a modified version of Smith-Waterman algorithm to find
|
||||
the optimal solution (highest score) according to the scoring criteria. Unlike
|
||||
the original algorithm, omission or mismatch of a character in the pattern is
|
||||
not allowed.
|
||||
|
||||
Performance
|
||||
-----------
|
||||
|
||||
The new V2 algorithm is slower than V1 as it examines all occurrences of the
|
||||
pattern instead of stopping immediately after finding the first one. The time
|
||||
complexity of the algorithm is O(nm) if a match is found and O(n) otherwise
|
||||
where n is the length of the item and m is the length of the pattern. Thus, the
|
||||
performance overhead may not be noticeable for a query with high selectivity.
|
||||
However, if the performance is more important than the quality of the result,
|
||||
you can still choose v1 algorithm with --algo=v1.
|
||||
|
||||
Scoring criteria
|
||||
----------------
|
||||
|
||||
- We prefer matches at special positions, such as the start of a word, or
|
||||
uppercase character in camelCase words.
|
||||
|
||||
- That is, we prefer an occurrence of the pattern with more characters
|
||||
matching at special positions, even if the total match length is longer.
|
||||
e.g. "fuzzyfinder" vs. "fuzzy-finder" on "ff"
|
||||
````````````
|
||||
- Also, if the first character in the pattern appears at one of the special
|
||||
positions, the bonus point for the position is multiplied by a constant
|
||||
as it is extremely likely that the first character in the typed pattern
|
||||
has more significance than the rest.
|
||||
e.g. "fo-bar" vs. "foob-r" on "br"
|
||||
``````
|
||||
- But since fzf is still a fuzzy finder, not an acronym finder, we should also
|
||||
consider the total length of the matched substring. This is why we have the
|
||||
gap penalty. The gap penalty increases as the length of the gap (distance
|
||||
between the matching characters) increases, so the effect of the bonus is
|
||||
eventually cancelled at some point.
|
||||
e.g. "fuzzyfinder" vs. "fuzzy-blurry-finder" on "ff"
|
||||
```````````
|
||||
- Consequently, it is crucial to find the right balance between the bonus
|
||||
and the gap penalty. The parameters were chosen that the bonus is cancelled
|
||||
when the gap size increases beyond 8 characters.
|
||||
|
||||
- The bonus mechanism can have the undesirable side effect where consecutive
|
||||
matches are ranked lower than the ones with gaps.
|
||||
e.g. "foobar" vs. "foo-bar" on "foob"
|
||||
```````
|
||||
- To correct this anomaly, we also give extra bonus point to each character
|
||||
in a consecutive matching chunk.
|
||||
e.g. "foobar" vs. "foo-bar" on "foob"
|
||||
``````
|
||||
- The amount of consecutive bonus is primarily determined by the bonus of the
|
||||
first character in the chunk.
|
||||
e.g. "foobar" vs. "out-of-bound" on "oob"
|
||||
````````````
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
var DEBUG bool
|
||||
|
||||
func indexAt(index int, max int, forward bool) int {
|
||||
if forward {
|
||||
return index
|
||||
}
|
||||
return max - index - 1
|
||||
}
|
||||
|
||||
// Result contains the results of running a match function.
|
||||
type Result struct {
|
||||
// TODO int32 should suffice
|
||||
Start int
|
||||
End int
|
||||
Score int
|
||||
}
|
||||
|
||||
const (
|
||||
scoreMatch = 16
|
||||
scoreGapStart = -3
|
||||
scoreGapExtention = -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
|
||||
// shorter fuzzy matches. The bonus point here was specifically chosen that
|
||||
// the bonus is cancelled when the gap between the acronyms grows over
|
||||
// 8 characters, which is approximately the average length of the words found
|
||||
// in web2 dictionary and my file system.
|
||||
bonusBoundary = scoreMatch / 2
|
||||
|
||||
// Although bonus point for non-word characters is non-contextual, we need it
|
||||
// for computing bonus points for consecutive chunks starting with a non-word
|
||||
// character.
|
||||
bonusNonWord = scoreMatch / 2
|
||||
|
||||
// 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
|
||||
|
||||
// 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)
|
||||
|
||||
// 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".
|
||||
// The amount of the extra bonus should be limited so that the gap penalty is
|
||||
// still respected.
|
||||
bonusFirstCharMultiplier = 2
|
||||
)
|
||||
|
||||
type charClass int
|
||||
|
||||
const (
|
||||
charNonWord charClass = iota
|
||||
charLower
|
||||
charUpper
|
||||
charLetter
|
||||
charNumber
|
||||
)
|
||||
|
||||
func posArray(withPos bool, len int) *[]int {
|
||||
if withPos {
|
||||
pos := make([]int, 0, len)
|
||||
return &pos
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func alloc16(offset int, slab *util.Slab, size int) (int, []int16) {
|
||||
if slab != nil && cap(slab.I16) > offset+size {
|
||||
slice := slab.I16[offset : offset+size]
|
||||
return offset + size, slice
|
||||
}
|
||||
return offset, make([]int16, size)
|
||||
}
|
||||
|
||||
func alloc32(offset int, slab *util.Slab, size int) (int, []int32) {
|
||||
if slab != nil && cap(slab.I32) > offset+size {
|
||||
slice := slab.I32[offset : offset+size]
|
||||
return offset + size, slice
|
||||
}
|
||||
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
|
||||
} else if unicode.IsUpper(char) {
|
||||
return charUpper
|
||||
} else if unicode.IsNumber(char) {
|
||||
return charNumber
|
||||
} else if unicode.IsLetter(char) {
|
||||
return charLetter
|
||||
}
|
||||
return charNonWord
|
||||
}
|
||||
|
||||
func charClassOf(char rune) charClass {
|
||||
if char <= unicode.MaxASCII {
|
||||
return charClassOfAscii(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 ||
|
||||
prevClass != charNumber && class == charNumber {
|
||||
// camelCase letter123
|
||||
return bonusCamel123
|
||||
} else if class == charNonWord {
|
||||
return bonusNonWord
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func bonusAt(input *util.Chars, idx int) int16 {
|
||||
if idx == 0 {
|
||||
return bonusBoundary
|
||||
}
|
||||
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx)))
|
||||
}
|
||||
|
||||
func normalizeRune(r rune) rune {
|
||||
if r < 0x00C0 || r > 0x2184 {
|
||||
return r
|
||||
}
|
||||
|
||||
n := normalized[r]
|
||||
if n > 0 {
|
||||
return n
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Algo functions make two assumptions
|
||||
// 1. "pattern" is given in lowercase if "caseSensitive" is false
|
||||
// 2. "pattern" is already normalized if "normalize" is true
|
||||
type Algo func(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int)
|
||||
|
||||
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.
|
||||
if !caseSensitive && b >= 'a' && b <= 'z' {
|
||||
if idx > 0 {
|
||||
byteArray = byteArray[:idx]
|
||||
}
|
||||
uidx := bytes.IndexByte(byteArray, b-32)
|
||||
if uidx >= 0 {
|
||||
idx = uidx
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
return -1
|
||||
}
|
||||
return from + idx
|
||||
}
|
||||
|
||||
func isAscii(runes []rune) bool {
|
||||
for _, r := range runes {
|
||||
if r >= utf8.RuneSelf {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int {
|
||||
// Can't determine
|
||||
if !input.IsBytes() {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Not possible
|
||||
if !isAscii(pattern) {
|
||||
return -1
|
||||
}
|
||||
|
||||
firstIdx, idx := 0, 0
|
||||
for pidx := 0; pidx < len(pattern); pidx++ {
|
||||
idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx)
|
||||
if idx < 0 {
|
||||
return -1
|
||||
}
|
||||
if pidx == 0 && idx > 0 {
|
||||
// Step back to find the right bonus point
|
||||
firstIdx = idx - 1
|
||||
}
|
||||
idx++
|
||||
}
|
||||
return firstIdx
|
||||
}
|
||||
|
||||
func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) {
|
||||
width := lastIdx - int(F[0]) + 1
|
||||
|
||||
for i, f := range F {
|
||||
I := i * width
|
||||
if i == 0 {
|
||||
fmt.Print(" ")
|
||||
for j := int(f); j <= lastIdx; j++ {
|
||||
fmt.Printf(" " + string(T[j]) + " ")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
fmt.Print(string(pattern[i]) + " ")
|
||||
for idx := int(F[0]); idx < int(f); idx++ {
|
||||
fmt.Print(" 0 ")
|
||||
}
|
||||
for idx := int(f); idx <= lastIdx; idx++ {
|
||||
fmt.Printf("%2d ", H[i*width+idx-int(F[0])])
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
fmt.Print(" ")
|
||||
for idx, p := range C[I : I+width] {
|
||||
if idx+int(F[0]) < int(F[i]) {
|
||||
p = 0
|
||||
}
|
||||
if p > 0 {
|
||||
fmt.Printf("%2d ", p)
|
||||
} else {
|
||||
fmt.Print(" ")
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
// Assume that pattern is given in lowercase if case-insensitive.
|
||||
// First check if there's a match and calculate bonus for each position.
|
||||
// If the input string is too long, consider finding the matching chars in
|
||||
// this phase as well (non-optimal alignment).
|
||||
M := len(pattern)
|
||||
if M == 0 {
|
||||
return Result{0, 0, 0}, posArray(withPos, M)
|
||||
}
|
||||
N := input.Length()
|
||||
|
||||
// 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) {
|
||||
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab)
|
||||
}
|
||||
|
||||
// Phase 1. Optimized search for ASCII string
|
||||
idx := asciiFuzzyIndex(input, pattern, caseSensitive)
|
||||
if idx < 0 {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
|
||||
offset16 := 0
|
||||
offset32 := 0
|
||||
offset16, H0 := alloc16(offset16, slab, N)
|
||||
offset16, C0 := alloc16(offset16, slab, N)
|
||||
// Bonus point for each position
|
||||
offset16, B := alloc16(offset16, slab, N)
|
||||
// The first occurrence of each character in the pattern
|
||||
offset32, F := alloc32(offset32, slab, M)
|
||||
// Rune array
|
||||
_, T := alloc32(offset32, slab, N)
|
||||
input.CopyRunes(T)
|
||||
|
||||
// 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 {
|
||||
var class charClass
|
||||
if char <= unicode.MaxASCII {
|
||||
class = charClassOfAscii(char)
|
||||
if !caseSensitive && class == charUpper {
|
||||
char += 32
|
||||
}
|
||||
} else {
|
||||
class = charClassOfNonAscii(char)
|
||||
if !caseSensitive && class == charUpper {
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
}
|
||||
|
||||
Tsub[off] = char
|
||||
bonus := bonusFor(prevClass, class)
|
||||
Bsub[off] = bonus
|
||||
prevClass = class
|
||||
|
||||
if char == pchar {
|
||||
if pidx < M {
|
||||
F[pidx] = int32(idx + off)
|
||||
pidx++
|
||||
pchar = pattern[util.Min(pidx, M-1)]
|
||||
}
|
||||
lastIdx = idx + off
|
||||
}
|
||||
|
||||
if char == pchar0 {
|
||||
score := scoreMatch + bonus*bonusFirstCharMultiplier
|
||||
H0sub[off] = score
|
||||
C0sub[off] = 1
|
||||
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
|
||||
maxScore, maxScorePos = score, idx+off
|
||||
if forward && bonus == bonusBoundary {
|
||||
break
|
||||
}
|
||||
}
|
||||
inGap = false
|
||||
} else {
|
||||
if inGap {
|
||||
H0sub[off] = util.Max16(prevH0+scoreGapExtention, 0)
|
||||
} else {
|
||||
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0)
|
||||
}
|
||||
C0sub[off] = 0
|
||||
inGap = true
|
||||
}
|
||||
prevH0 = H0sub[off]
|
||||
}
|
||||
if pidx != M {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
if M == 1 {
|
||||
result := Result{maxScorePos, maxScorePos + 1, int(maxScore)}
|
||||
if !withPos {
|
||||
return result, nil
|
||||
}
|
||||
pos := []int{maxScorePos}
|
||||
return result, &pos
|
||||
}
|
||||
|
||||
// Phase 3. Fill in score matrix (H)
|
||||
// Unlike the original algorithm, we do not allow omission.
|
||||
f0 := int(F[0])
|
||||
width := lastIdx - f0 + 1
|
||||
offset16, H := alloc16(offset16, slab, width*M)
|
||||
copy(H, H0[f0:lastIdx+1])
|
||||
|
||||
// Possible length of consecutive chunk at each position.
|
||||
_, C := alloc16(offset16, slab, width*M)
|
||||
copy(C, C0[f0:lastIdx+1])
|
||||
|
||||
Fsub := F[1:]
|
||||
Psub := pattern[1:][:len(Fsub)]
|
||||
for off, f := range Fsub {
|
||||
f := int(f)
|
||||
pchar := Psub[off]
|
||||
pidx := off + 1
|
||||
row := pidx * width
|
||||
inGap := false
|
||||
Tsub := T[f : lastIdx+1]
|
||||
Bsub := B[f:][:len(Tsub)]
|
||||
Csub := C[row+f-f0:][:len(Tsub)]
|
||||
Cdiag := C[row+f-f0-1-width:][:len(Tsub)]
|
||||
Hsub := H[row+f-f0:][:len(Tsub)]
|
||||
Hdiag := H[row+f-f0-1-width:][:len(Tsub)]
|
||||
Hleft := H[row+f-f0-1:][:len(Tsub)]
|
||||
Hleft[0] = 0
|
||||
for off, char := range Tsub {
|
||||
col := off + f
|
||||
var s1, s2, consecutive int16
|
||||
|
||||
if inGap {
|
||||
s2 = Hleft[off] + scoreGapExtention
|
||||
} else {
|
||||
s2 = Hleft[off] + scoreGapStart
|
||||
}
|
||||
|
||||
if pchar == char {
|
||||
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 s1+b < s2 {
|
||||
s1 += Bsub[off]
|
||||
consecutive = 0
|
||||
} else {
|
||||
s1 += b
|
||||
}
|
||||
}
|
||||
Csub[off] = consecutive
|
||||
|
||||
inGap = s1 < s2
|
||||
score := util.Max16(util.Max16(s1, s2), 0)
|
||||
if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {
|
||||
maxScore, maxScorePos = score, col
|
||||
}
|
||||
Hsub[off] = score
|
||||
}
|
||||
}
|
||||
|
||||
if DEBUG {
|
||||
debugV2(T, pattern, F, lastIdx, H, C)
|
||||
}
|
||||
|
||||
// Phase 4. (Optional) Backtrace to find character positions
|
||||
pos := posArray(withPos, M)
|
||||
j := f0
|
||||
if withPos {
|
||||
i := M - 1
|
||||
j = maxScorePos
|
||||
preferMatch := true
|
||||
for {
|
||||
I := i * width
|
||||
j0 := j - f0
|
||||
s := H[I+j0]
|
||||
|
||||
var s1, s2 int16
|
||||
if i > 0 && j >= int(F[i]) {
|
||||
s1 = H[I-width+j0-1]
|
||||
}
|
||||
if j > int(F[i]) {
|
||||
s2 = H[I+j0-1]
|
||||
}
|
||||
|
||||
if s > s1 && (s > s2 || s == s2 && preferMatch) {
|
||||
*pos = append(*pos, j)
|
||||
if i == 0 {
|
||||
break
|
||||
}
|
||||
i--
|
||||
}
|
||||
preferMatch = C[I+j0] > 1 || I+width+j0+1 < len(C) && C[I+width+j0+1] > 0
|
||||
j--
|
||||
}
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
if sidx > 0 {
|
||||
prevClass = charClassOf(text.Get(sidx - 1))
|
||||
}
|
||||
for idx := sidx; idx < eidx; idx++ {
|
||||
char := text.Get(idx)
|
||||
class := charClassOf(char)
|
||||
if !caseSensitive {
|
||||
if char >= 'A' && char <= 'Z' {
|
||||
char += 32
|
||||
} else if char > unicode.MaxASCII {
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
}
|
||||
// pattern is already normalized
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
if char == pattern[pidx] {
|
||||
if withPos {
|
||||
*pos = append(*pos, idx)
|
||||
}
|
||||
score += scoreMatch
|
||||
bonus := bonusFor(prevClass, class)
|
||||
if consecutive == 0 {
|
||||
firstBonus = bonus
|
||||
} else {
|
||||
// Break consecutive chunk
|
||||
if bonus == bonusBoundary {
|
||||
firstBonus = bonus
|
||||
}
|
||||
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
|
||||
}
|
||||
if pidx == 0 {
|
||||
score += int(bonus * bonusFirstCharMultiplier)
|
||||
} else {
|
||||
score += int(bonus)
|
||||
}
|
||||
inGap = false
|
||||
consecutive++
|
||||
pidx++
|
||||
} else {
|
||||
if inGap {
|
||||
score += scoreGapExtention
|
||||
} else {
|
||||
score += scoreGapStart
|
||||
}
|
||||
inGap = true
|
||||
consecutive = 0
|
||||
firstBonus = 0
|
||||
}
|
||||
prevClass = class
|
||||
}
|
||||
return score, pos
|
||||
}
|
||||
|
||||
// FuzzyMatchV1 performs fuzzy-match
|
||||
func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
if len(pattern) == 0 {
|
||||
return Result{0, 0, 0}, nil
|
||||
}
|
||||
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
pidx := 0
|
||||
sidx := -1
|
||||
eidx := -1
|
||||
|
||||
lenRunes := text.Length()
|
||||
lenPattern := len(pattern)
|
||||
|
||||
for index := 0; index < lenRunes; index++ {
|
||||
char := text.Get(indexAt(index, lenRunes, forward))
|
||||
// This is considerably faster than blindly applying strings.ToLower to the
|
||||
// whole string
|
||||
if !caseSensitive {
|
||||
// Partially inlining `unicode.ToLower`. Ugly, but makes a noticeable
|
||||
// difference in CPU cost. (Measured on Go 1.4.1. Also note that the Go
|
||||
// compiler as of now does not inline non-leaf functions.)
|
||||
if char >= 'A' && char <= 'Z' {
|
||||
char += 32
|
||||
} else if char > unicode.MaxASCII {
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
}
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
pchar := pattern[indexAt(pidx, lenPattern, forward)]
|
||||
if char == pchar {
|
||||
if sidx < 0 {
|
||||
sidx = index
|
||||
}
|
||||
if pidx++; pidx == lenPattern {
|
||||
eidx = index + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sidx >= 0 && eidx >= 0 {
|
||||
pidx--
|
||||
for index := eidx - 1; index >= sidx; index-- {
|
||||
tidx := indexAt(index, lenRunes, forward)
|
||||
char := text.Get(tidx)
|
||||
if !caseSensitive {
|
||||
if char >= 'A' && char <= 'Z' {
|
||||
char += 32
|
||||
} else if char > unicode.MaxASCII {
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
}
|
||||
|
||||
pidx_ := indexAt(pidx, lenPattern, forward)
|
||||
pchar := pattern[pidx_]
|
||||
if char == pchar {
|
||||
if pidx--; pidx < 0 {
|
||||
sidx = index
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !forward {
|
||||
sidx, eidx = lenRunes-eidx, lenRunes-sidx
|
||||
}
|
||||
|
||||
score, pos := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, withPos)
|
||||
return Result{sidx, eidx, score}, pos
|
||||
}
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
// ExactMatchNaive is a basic string searching algorithm that handles case
|
||||
// sensitivity. Although naive, it still performs better than the combination
|
||||
// of strings.ToLower + strings.Index for typical fzf use cases where input
|
||||
// strings and patterns are not very long.
|
||||
//
|
||||
// Since 0.15.0, this function searches for the match with the highest
|
||||
// bonus point, instead of stopping immediately after finding the first match.
|
||||
// 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) {
|
||||
if len(pattern) == 0 {
|
||||
return Result{0, 0, 0}, nil
|
||||
}
|
||||
|
||||
lenRunes := text.Length()
|
||||
lenPattern := len(pattern)
|
||||
|
||||
if lenRunes < lenPattern {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
if asciiFuzzyIndex(text, pattern, caseSensitive) < 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)
|
||||
for index := 0; index < lenRunes; index++ {
|
||||
index_ := indexAt(index, lenRunes, forward)
|
||||
char := text.Get(index_)
|
||||
if !caseSensitive {
|
||||
if char >= 'A' && char <= 'Z' {
|
||||
char += 32
|
||||
} else if char > unicode.MaxASCII {
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
}
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
pidx_ := indexAt(pidx, lenPattern, forward)
|
||||
pchar := pattern[pidx_]
|
||||
if pchar == char {
|
||||
if pidx_ == 0 {
|
||||
bonus = bonusAt(text, index_)
|
||||
}
|
||||
pidx++
|
||||
if pidx == lenPattern {
|
||||
if bonus > bestBonus {
|
||||
bestPos, bestBonus = index, bonus
|
||||
}
|
||||
if bonus == bonusBoundary {
|
||||
break
|
||||
}
|
||||
index -= pidx - 1
|
||||
pidx, bonus = 0, 0
|
||||
}
|
||||
} else {
|
||||
index -= pidx
|
||||
pidx, bonus = 0, 0
|
||||
}
|
||||
}
|
||||
if bestPos >= 0 {
|
||||
var sidx, eidx int
|
||||
if forward {
|
||||
sidx = bestPos - lenPattern + 1
|
||||
eidx = bestPos + 1
|
||||
} else {
|
||||
sidx = lenRunes - (bestPos + 1)
|
||||
eidx = lenRunes - (bestPos - lenPattern + 1)
|
||||
}
|
||||
score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
||||
return Result{sidx, eidx, score}, nil
|
||||
}
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
// PrefixMatch performs prefix-match
|
||||
func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
if len(pattern) == 0 {
|
||||
return Result{0, 0, 0}, nil
|
||||
}
|
||||
|
||||
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(trimmedLen + index)
|
||||
if !caseSensitive {
|
||||
char = unicode.ToLower(char)
|
||||
}
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
if char != r {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
}
|
||||
lenPattern := len(pattern)
|
||||
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
|
||||
if len(pattern) == 0 || !unicode.IsSpace(pattern[len(pattern)-1]) {
|
||||
trimmedLen -= text.TrailingWhitespaces()
|
||||
}
|
||||
if len(pattern) == 0 {
|
||||
return Result{trimmedLen, trimmedLen, 0}, nil
|
||||
}
|
||||
diff := trimmedLen - len(pattern)
|
||||
if diff < 0 {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
for index, r := range pattern {
|
||||
char := text.Get(index + diff)
|
||||
if !caseSensitive {
|
||||
char = unicode.ToLower(char)
|
||||
}
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
if char != r {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
}
|
||||
lenPattern := len(pattern)
|
||||
sidx := trimmedLen - lenPattern
|
||||
eidx := trimmedLen
|
||||
score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
||||
return Result{sidx, eidx, score}, nil
|
||||
}
|
||||
|
||||
// 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 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[trimmedLen+idx]
|
||||
if !caseSensitive {
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
if normalizeRune(pchar) != normalizeRune(char) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
runes := text.ToRunes()
|
||||
runesStr := string(runes[trimmedLen : len(runes)-trimmedEndLen])
|
||||
if !caseSensitive {
|
||||
runesStr = strings.ToLower(runesStr)
|
||||
}
|
||||
match = runesStr == string(pattern)
|
||||
}
|
||||
if match {
|
||||
return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+bonusBoundary)*lenPattern +
|
||||
(bonusFirstCharMultiplier-1)*bonusBoundary}, nil
|
||||
}
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
197
src/algo/algo_test.go
Normal file
197
src/algo/algo_test.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package algo
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool, input, pattern string, sidx int, eidx int, score int) {
|
||||
if !caseSensitive {
|
||||
pattern = strings.ToLower(pattern)
|
||||
}
|
||||
chars := util.ToChars([]byte(input))
|
||||
res, pos := fun(caseSensitive, normalize, forward, &chars, []rune(pattern), true, nil)
|
||||
var start, end int
|
||||
if pos == nil || len(*pos) == 0 {
|
||||
start = res.Start
|
||||
end = res.End
|
||||
} else {
|
||||
sort.Ints(*pos)
|
||||
start = (*pos)[0]
|
||||
end = (*pos)[len(*pos)-1] + 1
|
||||
}
|
||||
if start != sidx {
|
||||
t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", start, sidx, input, pattern)
|
||||
}
|
||||
if end != eidx {
|
||||
t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", end, eidx, input, pattern)
|
||||
}
|
||||
if res.Score != score {
|
||||
t.Errorf("Invalid score: %d (expected: %d, %s / %s)", res.Score, score, input, pattern)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
|
||||
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)
|
||||
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart)
|
||||
assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10,
|
||||
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtention)
|
||||
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
|
||||
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtention)
|
||||
assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
|
||||
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtention)
|
||||
assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+
|
||||
scoreGapStart*2+scoreGapExtention*3)
|
||||
assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*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)
|
||||
assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9,
|
||||
scoreMatch*3+bonusBoundary*(bonusFirstCharMultiplier+2)+
|
||||
scoreGapStart*2+scoreGapExtention*4)
|
||||
assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+
|
||||
scoreGapStart*2+scoreGapExtention*2)
|
||||
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+
|
||||
util.Max(bonusCamel123, bonusBoundary))
|
||||
|
||||
// Consecutive bonus updated
|
||||
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,
|
||||
scoreMatch*4+bonusBoundary*3)
|
||||
|
||||
// Non-match
|
||||
assertMatch(t, fn, true, forward, "fooBarbaz", "oBZ", -1, -1, 0)
|
||||
assertMatch(t, fn, true, forward, "Foo Bar Baz", "fbb", -1, -1, 0)
|
||||
assertMatch(t, fn, true, forward, "fooBarbaz", "fooBarbazz", -1, -1, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuzzyMatchBackward(t *testing.T) {
|
||||
assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4,
|
||||
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+
|
||||
scoreGapStart+scoreGapExtention)
|
||||
assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9,
|
||||
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary)
|
||||
}
|
||||
|
||||
func TestExactMatchNaive(t *testing.T) {
|
||||
for _, dir := range []bool{true, false} {
|
||||
assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1, 0)
|
||||
assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1, 0)
|
||||
|
||||
assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5,
|
||||
scoreMatch*3+bonusCamel123+bonusConsecutive)
|
||||
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))
|
||||
assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13,
|
||||
scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+4))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExactMatchNaiveBackward(t *testing.T) {
|
||||
assertMatch(t, ExactMatchNaive, false, true, "foobar foob", "oo", 1, 3,
|
||||
scoreMatch*2+bonusConsecutive)
|
||||
assertMatch(t, ExactMatchNaive, false, false, "foobar foob", "oo", 8, 10,
|
||||
scoreMatch*2+bonusConsecutive)
|
||||
}
|
||||
|
||||
func TestPrefixMatch(t *testing.T) {
|
||||
score := (scoreMatch+bonusBoundary)*3 + bonusBoundary*(bonusFirstCharMultiplier-1)
|
||||
|
||||
for _, dir := range []bool{true, false} {
|
||||
assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0)
|
||||
assertMatch(t, PrefixMatch, false, dir, "fooBarBaz", "baz", -1, -1, 0)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuffixMatch(t *testing.T) {
|
||||
for _, dir := range []bool{true, false} {
|
||||
assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1, 0)
|
||||
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1, 0)
|
||||
|
||||
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9,
|
||||
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+bonusNonWord)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyPattern(t *testing.T) {
|
||||
for _, dir := range []bool{true, false} {
|
||||
assertMatch(t, FuzzyMatchV1, true, dir, "foobar", "", 0, 0, 0)
|
||||
assertMatch(t, FuzzyMatchV2, true, dir, "foobar", "", 0, 0, 0)
|
||||
assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0, 0)
|
||||
assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0, 0)
|
||||
assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalize(t *testing.T) {
|
||||
caseSensitive := false
|
||||
normalize := true
|
||||
forward := true
|
||||
test := func(input, pattern string, sidx, eidx, score int, funs ...Algo) {
|
||||
for _, fun := range funs {
|
||||
assertMatch2(t, fun, caseSensitive, normalize, forward,
|
||||
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)
|
||||
}
|
||||
|
||||
func TestLongString(t *testing.T) {
|
||||
bytes := make([]byte, math.MaxUint16*2)
|
||||
for i := range bytes {
|
||||
bytes[i] = 'x'
|
||||
}
|
||||
bytes[math.MaxUint16] = 'z'
|
||||
assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive)
|
||||
}
|
||||
492
src/algo/normalize.go
Normal file
492
src/algo/normalize.go
Normal file
@@ -0,0 +1,492 @@
|
||||
// Normalization of latin script letters
|
||||
// Reference: http://www.unicode.org/Public/UCD/latest/ucd/Index.txt
|
||||
|
||||
package algo
|
||||
|
||||
var normalized map[rune]rune = map[rune]rune{
|
||||
0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER
|
||||
0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER
|
||||
0x00E2: 'a', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x00E4: 'a', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x0227: 'a', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1EA1: 'a', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0201: 'a', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
|
||||
0x00E0: 'a', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x1EA3: 'a', // WITH HOOK ABOVE, LATIN SMALL LETTER
|
||||
0x0203: 'a', // WITH INVERTED BREVE, LATIN SMALL LETTER
|
||||
0x0101: 'a', // WITH MACRON, LATIN SMALL LETTER
|
||||
0x0105: 'a', // WITH OGONEK, LATIN SMALL LETTER
|
||||
0x1E9A: 'a', // WITH RIGHT HALF RING, LATIN SMALL LETTER
|
||||
0x00E5: 'a', // WITH RING ABOVE, LATIN SMALL LETTER
|
||||
0x1E01: 'a', // WITH RING BELOW, LATIN SMALL LETTER
|
||||
0x00E3: 'a', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x0363: 'a', // , COMBINING LATIN SMALL LETTER
|
||||
0x0250: 'a', // , LATIN SMALL LETTER TURNED
|
||||
0x1E03: 'b', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E05: 'b', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0253: 'b', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E07: 'b', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x0180: 'b', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x0183: 'b', // WITH TOPBAR, LATIN SMALL LETTER
|
||||
0x0107: 'c', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x010D: 'c', // WITH CARON, LATIN SMALL LETTER
|
||||
0x00E7: 'c', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x0109: 'c', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x0255: 'c', // WITH CURL, LATIN SMALL LETTER
|
||||
0x010B: 'c', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x0188: 'c', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x023C: 'c', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x0368: 'c', // , COMBINING LATIN SMALL LETTER
|
||||
0x0297: 'c', // , LATIN LETTER STRETCHED
|
||||
0x2184: 'c', // , LATIN SMALL LETTER REVERSED
|
||||
0x010F: 'd', // WITH CARON, LATIN SMALL LETTER
|
||||
0x1E11: 'd', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x1E13: 'd', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
|
||||
0x0221: 'd', // WITH CURL, LATIN SMALL LETTER
|
||||
0x1E0B: 'd', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E0D: 'd', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0257: 'd', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E0F: 'd', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x0111: 'd', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x0256: 'd', // WITH TAIL, LATIN SMALL LETTER
|
||||
0x018C: 'd', // WITH TOPBAR, LATIN SMALL LETTER
|
||||
0x0369: 'd', // , COMBINING LATIN SMALL LETTER
|
||||
0x00E9: 'e', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0115: 'e', // WITH BREVE, LATIN SMALL LETTER
|
||||
0x011B: 'e', // WITH CARON, LATIN SMALL LETTER
|
||||
0x0229: 'e', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x1E19: 'e', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
|
||||
0x00EA: 'e', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x00EB: 'e', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x0117: 'e', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1EB9: 'e', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0205: 'e', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
|
||||
0x00E8: 'e', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x1EBB: 'e', // WITH HOOK ABOVE, LATIN SMALL LETTER
|
||||
0x025D: 'e', // WITH HOOK, LATIN SMALL LETTER REVERSED OPEN
|
||||
0x0207: 'e', // WITH INVERTED BREVE, LATIN SMALL LETTER
|
||||
0x0113: 'e', // WITH MACRON, LATIN SMALL LETTER
|
||||
0x0119: 'e', // WITH OGONEK, LATIN SMALL LETTER
|
||||
0x0247: 'e', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x1E1B: 'e', // WITH TILDE BELOW, LATIN SMALL LETTER
|
||||
0x1EBD: 'e', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x0364: 'e', // , COMBINING LATIN SMALL LETTER
|
||||
0x029A: 'e', // , LATIN SMALL LETTER CLOSED OPEN
|
||||
0x025E: 'e', // , LATIN SMALL LETTER CLOSED REVERSED OPEN
|
||||
0x025B: 'e', // , LATIN SMALL LETTER OPEN
|
||||
0x0258: 'e', // , LATIN SMALL LETTER REVERSED
|
||||
0x025C: 'e', // , LATIN SMALL LETTER REVERSED OPEN
|
||||
0x01DD: 'e', // , LATIN SMALL LETTER TURNED
|
||||
0x1D08: 'e', // , LATIN SMALL LETTER TURNED OPEN
|
||||
0x1E1F: 'f', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x0192: 'f', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x01F5: 'g', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x011F: 'g', // WITH BREVE, LATIN SMALL LETTER
|
||||
0x01E7: 'g', // WITH CARON, LATIN SMALL LETTER
|
||||
0x0123: 'g', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x011D: 'g', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x0121: 'g', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x0260: 'g', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E21: 'g', // WITH MACRON, LATIN SMALL LETTER
|
||||
0x01E5: 'g', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x0261: 'g', // , LATIN SMALL LETTER SCRIPT
|
||||
0x1E2B: 'h', // WITH BREVE BELOW, LATIN SMALL LETTER
|
||||
0x021F: 'h', // WITH CARON, LATIN SMALL LETTER
|
||||
0x1E29: 'h', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x0125: 'h', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x1E27: 'h', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x1E23: 'h', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E25: 'h', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x02AE: 'h', // WITH FISHHOOK, LATIN SMALL LETTER TURNED
|
||||
0x0266: 'h', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E96: 'h', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x0127: 'h', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x036A: 'h', // , COMBINING LATIN SMALL LETTER
|
||||
0x0265: 'h', // , LATIN SMALL LETTER TURNED
|
||||
0x2095: 'h', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x00ED: 'i', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x012D: 'i', // WITH BREVE, LATIN SMALL LETTER
|
||||
0x01D0: 'i', // WITH CARON, LATIN SMALL LETTER
|
||||
0x00EE: 'i', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x00EF: 'i', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x1ECB: 'i', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0209: 'i', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
|
||||
0x00EC: 'i', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x1EC9: 'i', // WITH HOOK ABOVE, LATIN SMALL LETTER
|
||||
0x020B: 'i', // WITH INVERTED BREVE, LATIN SMALL LETTER
|
||||
0x012B: 'i', // WITH MACRON, LATIN SMALL LETTER
|
||||
0x012F: 'i', // WITH OGONEK, LATIN SMALL LETTER
|
||||
0x0268: 'i', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x1E2D: 'i', // WITH TILDE BELOW, LATIN SMALL LETTER
|
||||
0x0129: 'i', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x0365: 'i', // , COMBINING LATIN SMALL LETTER
|
||||
0x0131: 'i', // , LATIN SMALL LETTER DOTLESS
|
||||
0x1D09: 'i', // , LATIN SMALL LETTER TURNED
|
||||
0x1D62: 'i', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x2071: 'i', // , SUPERSCRIPT LATIN SMALL LETTER
|
||||
0x01F0: 'j', // WITH CARON, LATIN SMALL LETTER
|
||||
0x0135: 'j', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x029D: 'j', // WITH CROSSED-TAIL, LATIN SMALL LETTER
|
||||
0x0249: 'j', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x025F: 'j', // WITH STROKE, LATIN SMALL LETTER DOTLESS
|
||||
0x0237: 'j', // , LATIN SMALL LETTER DOTLESS
|
||||
0x1E31: 'k', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x01E9: 'k', // WITH CARON, LATIN SMALL LETTER
|
||||
0x0137: 'k', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x1E33: 'k', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0199: 'k', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E35: 'k', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x029E: 'k', // , LATIN SMALL LETTER TURNED
|
||||
0x2096: 'k', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x013A: 'l', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x019A: 'l', // WITH BAR, LATIN SMALL LETTER
|
||||
0x026C: 'l', // WITH BELT, LATIN SMALL LETTER
|
||||
0x013E: 'l', // WITH CARON, LATIN SMALL LETTER
|
||||
0x013C: 'l', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x1E3D: 'l', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
|
||||
0x0234: 'l', // WITH CURL, LATIN SMALL LETTER
|
||||
0x1E37: 'l', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x1E3B: 'l', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x0140: 'l', // WITH MIDDLE DOT, LATIN SMALL LETTER
|
||||
0x026B: 'l', // WITH MIDDLE TILDE, LATIN SMALL LETTER
|
||||
0x026D: 'l', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
|
||||
0x0142: 'l', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x2097: 'l', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x1E3F: 'm', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x1E41: 'm', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E43: 'm', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0271: 'm', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x0270: 'm', // WITH LONG LEG, LATIN SMALL LETTER TURNED
|
||||
0x036B: 'm', // , COMBINING LATIN SMALL LETTER
|
||||
0x1D1F: 'm', // , LATIN SMALL LETTER SIDEWAYS TURNED
|
||||
0x026F: 'm', // , LATIN SMALL LETTER TURNED
|
||||
0x2098: 'm', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x0144: 'n', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0148: 'n', // WITH CARON, LATIN SMALL LETTER
|
||||
0x0146: 'n', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x1E4B: 'n', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
|
||||
0x0235: 'n', // WITH CURL, LATIN SMALL LETTER
|
||||
0x1E45: 'n', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E47: 'n', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x01F9: 'n', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x0272: 'n', // WITH LEFT HOOK, LATIN SMALL LETTER
|
||||
0x1E49: 'n', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x019E: 'n', // WITH LONG RIGHT LEG, LATIN SMALL LETTER
|
||||
0x0273: 'n', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
|
||||
0x00F1: 'n', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x2099: 'n', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x00F3: 'o', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x014F: 'o', // WITH BREVE, LATIN SMALL LETTER
|
||||
0x01D2: 'o', // WITH CARON, LATIN SMALL LETTER
|
||||
0x00F4: 'o', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x00F6: 'o', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x022F: 'o', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1ECD: 'o', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0151: 'o', // WITH DOUBLE ACUTE, LATIN SMALL LETTER
|
||||
0x020D: 'o', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
|
||||
0x00F2: 'o', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x1ECF: 'o', // WITH HOOK ABOVE, LATIN SMALL LETTER
|
||||
0x01A1: 'o', // WITH HORN, LATIN SMALL LETTER
|
||||
0x020F: 'o', // WITH INVERTED BREVE, LATIN SMALL LETTER
|
||||
0x014D: 'o', // WITH MACRON, LATIN SMALL LETTER
|
||||
0x01EB: 'o', // WITH OGONEK, LATIN SMALL LETTER
|
||||
0x00F8: 'o', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x1D13: 'o', // WITH STROKE, LATIN SMALL LETTER SIDEWAYS
|
||||
0x00F5: 'o', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x0366: 'o', // , COMBINING LATIN SMALL LETTER
|
||||
0x0275: 'o', // , LATIN SMALL LETTER BARRED
|
||||
0x1D17: 'o', // , LATIN SMALL LETTER BOTTOM HALF
|
||||
0x0254: 'o', // , LATIN SMALL LETTER OPEN
|
||||
0x1D11: 'o', // , LATIN SMALL LETTER SIDEWAYS
|
||||
0x1D12: 'o', // , LATIN SMALL LETTER SIDEWAYS OPEN
|
||||
0x1D16: 'o', // , LATIN SMALL LETTER TOP HALF
|
||||
0x1E55: 'p', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x1E57: 'p', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x01A5: 'p', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x209A: 'p', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x024B: 'q', // WITH HOOK TAIL, LATIN SMALL LETTER
|
||||
0x02A0: 'q', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x0155: 'r', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0159: 'r', // WITH CARON, LATIN SMALL LETTER
|
||||
0x0157: 'r', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x1E59: 'r', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E5B: 'r', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0211: 'r', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
|
||||
0x027E: 'r', // WITH FISHHOOK, LATIN SMALL LETTER
|
||||
0x027F: 'r', // WITH FISHHOOK, LATIN SMALL LETTER REVERSED
|
||||
0x027B: 'r', // WITH HOOK, LATIN SMALL LETTER TURNED
|
||||
0x0213: 'r', // WITH INVERTED BREVE, LATIN SMALL LETTER
|
||||
0x1E5F: 'r', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x027C: 'r', // WITH LONG LEG, LATIN SMALL LETTER
|
||||
0x027A: 'r', // WITH LONG LEG, LATIN SMALL LETTER TURNED
|
||||
0x024D: 'r', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x027D: 'r', // WITH TAIL, LATIN SMALL LETTER
|
||||
0x036C: 'r', // , COMBINING LATIN SMALL LETTER
|
||||
0x0279: 'r', // , LATIN SMALL LETTER TURNED
|
||||
0x1D63: 'r', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x015B: 's', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0161: 's', // WITH CARON, LATIN SMALL LETTER
|
||||
0x015F: 's', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x015D: 's', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x0219: 's', // WITH COMMA BELOW, LATIN SMALL LETTER
|
||||
0x1E61: 's', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E9B: 's', // WITH DOT ABOVE, LATIN SMALL LETTER LONG
|
||||
0x1E63: 's', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0282: 's', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x023F: 's', // WITH SWASH TAIL, LATIN SMALL LETTER
|
||||
0x017F: 's', // , LATIN SMALL LETTER LONG
|
||||
0x00DF: 's', // , LATIN SMALL LETTER SHARP
|
||||
0x209B: 's', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x0165: 't', // WITH CARON, LATIN SMALL LETTER
|
||||
0x0163: 't', // WITH CEDILLA, LATIN SMALL LETTER
|
||||
0x1E71: 't', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
|
||||
0x021B: 't', // WITH COMMA BELOW, LATIN SMALL LETTER
|
||||
0x0236: 't', // WITH CURL, LATIN SMALL LETTER
|
||||
0x1E97: 't', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x1E6B: 't', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E6D: 't', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x01AD: 't', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E6F: 't', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x01AB: 't', // WITH PALATAL HOOK, LATIN SMALL LETTER
|
||||
0x0288: 't', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
|
||||
0x0167: 't', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x036D: 't', // , COMBINING LATIN SMALL LETTER
|
||||
0x0287: 't', // , LATIN SMALL LETTER TURNED
|
||||
0x209C: 't', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x0289: 'u', // BAR, LATIN SMALL LETTER
|
||||
0x00FA: 'u', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x016D: 'u', // WITH BREVE, LATIN SMALL LETTER
|
||||
0x01D4: 'u', // WITH CARON, LATIN SMALL LETTER
|
||||
0x1E77: 'u', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
|
||||
0x00FB: 'u', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x1E73: 'u', // WITH DIAERESIS BELOW, LATIN SMALL LETTER
|
||||
0x00FC: 'u', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x1EE5: 'u', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0171: 'u', // WITH DOUBLE ACUTE, LATIN SMALL LETTER
|
||||
0x0215: 'u', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
|
||||
0x00F9: 'u', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x1EE7: 'u', // WITH HOOK ABOVE, LATIN SMALL LETTER
|
||||
0x01B0: 'u', // WITH HORN, LATIN SMALL LETTER
|
||||
0x0217: 'u', // WITH INVERTED BREVE, LATIN SMALL LETTER
|
||||
0x016B: 'u', // WITH MACRON, LATIN SMALL LETTER
|
||||
0x0173: 'u', // WITH OGONEK, LATIN SMALL LETTER
|
||||
0x016F: 'u', // WITH RING ABOVE, LATIN SMALL LETTER
|
||||
0x1E75: 'u', // WITH TILDE BELOW, LATIN SMALL LETTER
|
||||
0x0169: 'u', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x0367: 'u', // , COMBINING LATIN SMALL LETTER
|
||||
0x1D1D: 'u', // , LATIN SMALL LETTER SIDEWAYS
|
||||
0x1D1E: 'u', // , LATIN SMALL LETTER SIDEWAYS DIAERESIZED
|
||||
0x1D64: 'u', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x1E7F: 'v', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x028B: 'v', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E7D: 'v', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x036E: 'v', // , COMBINING LATIN SMALL LETTER
|
||||
0x028C: 'v', // , LATIN SMALL LETTER TURNED
|
||||
0x1D65: 'v', // , LATIN SUBSCRIPT SMALL LETTER
|
||||
0x1E83: 'w', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0175: 'w', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x1E85: 'w', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x1E87: 'w', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E89: 'w', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x1E81: 'w', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x1E98: 'w', // WITH RING ABOVE, LATIN SMALL LETTER
|
||||
0x028D: 'w', // , LATIN SMALL LETTER TURNED
|
||||
0x1E8D: 'x', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x1E8B: 'x', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x036F: 'x', // , COMBINING LATIN SMALL LETTER
|
||||
0x00FD: 'y', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0177: 'y', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x00FF: 'y', // WITH DIAERESIS, LATIN SMALL LETTER
|
||||
0x1E8F: 'y', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1EF5: 'y', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x1EF3: 'y', // WITH GRAVE, LATIN SMALL LETTER
|
||||
0x1EF7: 'y', // WITH HOOK ABOVE, LATIN SMALL LETTER
|
||||
0x01B4: 'y', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x0233: 'y', // WITH MACRON, LATIN SMALL LETTER
|
||||
0x1E99: 'y', // WITH RING ABOVE, LATIN SMALL LETTER
|
||||
0x024F: 'y', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x1EF9: 'y', // WITH TILDE, LATIN SMALL LETTER
|
||||
0x028E: 'y', // , LATIN SMALL LETTER TURNED
|
||||
0x017A: 'z', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x017E: 'z', // WITH CARON, LATIN SMALL LETTER
|
||||
0x1E91: 'z', // WITH CIRCUMFLEX, LATIN SMALL LETTER
|
||||
0x0291: 'z', // WITH CURL, LATIN SMALL LETTER
|
||||
0x017C: 'z', // WITH DOT ABOVE, LATIN SMALL LETTER
|
||||
0x1E93: 'z', // WITH DOT BELOW, LATIN SMALL LETTER
|
||||
0x0225: 'z', // WITH HOOK, LATIN SMALL LETTER
|
||||
0x1E95: 'z', // WITH LINE BELOW, LATIN SMALL LETTER
|
||||
0x0290: 'z', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
|
||||
0x01B6: 'z', // WITH STROKE, LATIN SMALL LETTER
|
||||
0x0240: 'z', // WITH SWASH TAIL, LATIN SMALL LETTER
|
||||
0x0251: 'a', // , latin small letter script
|
||||
0x00C1: 'A', // WITH ACUTE, LATIN CAPITAL LETTER
|
||||
0x00C2: 'A', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
|
||||
0x00C4: 'A', // WITH DIAERESIS, LATIN CAPITAL LETTER
|
||||
0x00C0: 'A', // WITH GRAVE, LATIN CAPITAL LETTER
|
||||
0x00C5: 'A', // WITH RING ABOVE, LATIN CAPITAL LETTER
|
||||
0x023A: 'A', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x00C3: 'A', // WITH TILDE, LATIN CAPITAL LETTER
|
||||
0x1D00: 'A', // , LATIN LETTER SMALL CAPITAL
|
||||
0x0181: 'B', // WITH HOOK, LATIN CAPITAL LETTER
|
||||
0x0243: 'B', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x0299: 'B', // , LATIN LETTER SMALL CAPITAL
|
||||
0x1D03: 'B', // , LATIN LETTER SMALL CAPITAL BARRED
|
||||
0x00C7: 'C', // WITH CEDILLA, LATIN CAPITAL LETTER
|
||||
0x023B: 'C', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x1D04: 'C', // , LATIN LETTER SMALL CAPITAL
|
||||
0x018A: 'D', // WITH HOOK, LATIN CAPITAL LETTER
|
||||
0x0189: 'D', // , LATIN CAPITAL LETTER AFRICAN
|
||||
0x1D05: 'D', // , LATIN LETTER SMALL CAPITAL
|
||||
0x00C9: 'E', // WITH ACUTE, LATIN CAPITAL LETTER
|
||||
0x00CA: 'E', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
|
||||
0x00CB: 'E', // WITH DIAERESIS, LATIN CAPITAL LETTER
|
||||
0x00C8: 'E', // WITH GRAVE, LATIN CAPITAL LETTER
|
||||
0x0246: 'E', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x0190: 'E', // , LATIN CAPITAL LETTER OPEN
|
||||
0x018E: 'E', // , LATIN CAPITAL LETTER REVERSED
|
||||
0x1D07: 'E', // , LATIN LETTER SMALL CAPITAL
|
||||
0x0193: 'G', // WITH HOOK, LATIN CAPITAL LETTER
|
||||
0x029B: 'G', // WITH HOOK, LATIN LETTER SMALL CAPITAL
|
||||
0x0262: 'G', // , LATIN LETTER SMALL CAPITAL
|
||||
0x029C: 'H', // , LATIN LETTER SMALL CAPITAL
|
||||
0x00CD: 'I', // WITH ACUTE, LATIN CAPITAL LETTER
|
||||
0x00CE: 'I', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
|
||||
0x00CF: 'I', // WITH DIAERESIS, LATIN CAPITAL LETTER
|
||||
0x0130: 'I', // WITH DOT ABOVE, LATIN CAPITAL LETTER
|
||||
0x00CC: 'I', // WITH GRAVE, LATIN CAPITAL LETTER
|
||||
0x0197: 'I', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x026A: 'I', // , LATIN LETTER SMALL CAPITAL
|
||||
0x0248: 'J', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x1D0A: 'J', // , LATIN LETTER SMALL CAPITAL
|
||||
0x1D0B: 'K', // , LATIN LETTER SMALL CAPITAL
|
||||
0x023D: 'L', // WITH BAR, LATIN CAPITAL LETTER
|
||||
0x1D0C: 'L', // WITH STROKE, LATIN LETTER SMALL CAPITAL
|
||||
0x029F: 'L', // , LATIN LETTER SMALL CAPITAL
|
||||
0x019C: 'M', // , LATIN CAPITAL LETTER TURNED
|
||||
0x1D0D: 'M', // , LATIN LETTER SMALL CAPITAL
|
||||
0x019D: 'N', // WITH LEFT HOOK, LATIN CAPITAL LETTER
|
||||
0x0220: 'N', // WITH LONG RIGHT LEG, LATIN CAPITAL LETTER
|
||||
0x00D1: 'N', // WITH TILDE, LATIN CAPITAL LETTER
|
||||
0x0274: 'N', // , LATIN LETTER SMALL CAPITAL
|
||||
0x1D0E: 'N', // , LATIN LETTER SMALL CAPITAL REVERSED
|
||||
0x00D3: 'O', // WITH ACUTE, LATIN CAPITAL LETTER
|
||||
0x00D4: 'O', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
|
||||
0x00D6: 'O', // WITH DIAERESIS, LATIN CAPITAL LETTER
|
||||
0x00D2: 'O', // WITH GRAVE, LATIN CAPITAL LETTER
|
||||
0x019F: 'O', // WITH MIDDLE TILDE, LATIN CAPITAL LETTER
|
||||
0x00D8: 'O', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x00D5: 'O', // WITH TILDE, LATIN CAPITAL LETTER
|
||||
0x0186: 'O', // , LATIN CAPITAL LETTER OPEN
|
||||
0x1D0F: 'O', // , LATIN LETTER SMALL CAPITAL
|
||||
0x1D10: 'O', // , LATIN LETTER SMALL CAPITAL OPEN
|
||||
0x1D18: 'P', // , LATIN LETTER SMALL CAPITAL
|
||||
0x024A: 'Q', // WITH HOOK TAIL, LATIN CAPITAL LETTER SMALL
|
||||
0x024C: 'R', // WITH STROKE, LATIN CAPITAL LETTER
|
||||
0x0280: 'R', // , LATIN LETTER SMALL CAPITAL
|
||||
0x0281: 'R', // , LATIN LETTER SMALL CAPITAL INVERTED
|
||||
0x1D19: 'R', // , LATIN LETTER SMALL CAPITAL REVERSED
|
||||
0x1D1A: 'R', // , LATIN LETTER SMALL CAPITAL TURNED
|
||||
0x023E: 'T', // WITH DIAGONAL STROKE, LATIN CAPITAL LETTER
|
||||
0x01AE: 'T', // WITH RETROFLEX HOOK, LATIN CAPITAL LETTER
|
||||
0x1D1B: 'T', // , LATIN LETTER SMALL CAPITAL
|
||||
0x0244: 'U', // BAR, LATIN CAPITAL LETTER
|
||||
0x00DA: 'U', // WITH ACUTE, LATIN CAPITAL LETTER
|
||||
0x00DB: 'U', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
|
||||
0x00DC: 'U', // WITH DIAERESIS, LATIN CAPITAL LETTER
|
||||
0x00D9: 'U', // WITH GRAVE, LATIN CAPITAL LETTER
|
||||
0x1D1C: 'U', // , LATIN LETTER SMALL CAPITAL
|
||||
0x01B2: 'V', // WITH HOOK, LATIN CAPITAL LETTER
|
||||
0x0245: 'V', // , LATIN CAPITAL LETTER TURNED
|
||||
0x1D20: 'V', // , LATIN LETTER SMALL CAPITAL
|
||||
0x1D21: 'W', // , LATIN LETTER SMALL CAPITAL
|
||||
0x00DD: 'Y', // WITH ACUTE, LATIN CAPITAL LETTER
|
||||
0x0178: 'Y', // WITH DIAERESIS, LATIN CAPITAL LETTER
|
||||
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',
|
||||
}
|
||||
|
||||
// NormalizeRunes normalizes latin script letters
|
||||
func NormalizeRunes(runes []rune) []rune {
|
||||
ret := make([]rune, len(runes))
|
||||
copy(ret, runes)
|
||||
for idx, r := range runes {
|
||||
if r < 0x00C0 || r > 0x2184 {
|
||||
continue
|
||||
}
|
||||
n := normalized[r]
|
||||
if n > 0 {
|
||||
ret[idx] = normalized[r]
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
292
src/ansi.go
Normal file
292
src/ansi.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
)
|
||||
|
||||
type ansiOffset struct {
|
||||
offset [2]int32
|
||||
color ansiState
|
||||
}
|
||||
|
||||
type ansiState struct {
|
||||
fg tui.Color
|
||||
bg tui.Color
|
||||
attr tui.Attr
|
||||
}
|
||||
|
||||
func (s *ansiState) colored() bool {
|
||||
return s.fg != -1 || s.bg != -1 || s.attr > 0
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (s *ansiState) ToString() string {
|
||||
if !s.colored() {
|
||||
return ""
|
||||
}
|
||||
|
||||
ret := ""
|
||||
if s.attr&tui.Bold > 0 {
|
||||
ret += "1;"
|
||||
}
|
||||
if s.attr&tui.Dim > 0 {
|
||||
ret += "2;"
|
||||
}
|
||||
if s.attr&tui.Italic > 0 {
|
||||
ret += "3;"
|
||||
}
|
||||
if s.attr&tui.Underline > 0 {
|
||||
ret += "4;"
|
||||
}
|
||||
if s.attr&tui.Blink > 0 {
|
||||
ret += "5;"
|
||||
}
|
||||
if s.attr&tui.Reverse > 0 {
|
||||
ret += "7;"
|
||||
}
|
||||
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
|
||||
|
||||
return "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
|
||||
}
|
||||
|
||||
func toAnsiString(color tui.Color, offset int) string {
|
||||
col := int(color)
|
||||
ret := ""
|
||||
if col == -1 {
|
||||
ret += strconv.Itoa(offset + 9)
|
||||
} else if col < 8 {
|
||||
ret += strconv.Itoa(offset + col)
|
||||
} else if col < 16 {
|
||||
ret += strconv.Itoa(offset - 30 + 90 + col - 8)
|
||||
} else if col < 256 {
|
||||
ret += strconv.Itoa(offset+8) + ";5;" + strconv.Itoa(col)
|
||||
} else if col >= (1 << 24) {
|
||||
r := strconv.Itoa((col >> 16) & 0xff)
|
||||
g := strconv.Itoa((col >> 8) & 0xff)
|
||||
b := strconv.Itoa(col & 0xff)
|
||||
ret += strconv.Itoa(offset+8) + ";2;" + r + ";" + g + ";" + b
|
||||
}
|
||||
return ret + ";"
|
||||
}
|
||||
|
||||
var ansiRegex *regexp.Regexp
|
||||
|
||||
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
|
||||
- https://invisible-island.net/xterm/ctlseqs/ctlseqs.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][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
|
||||
var offsets []ansiOffset
|
||||
var output bytes.Buffer
|
||||
|
||||
if state != nil {
|
||||
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
|
||||
}
|
||||
|
||||
prevIdx := 0
|
||||
runeCount := 0
|
||||
for idx := 0; idx < len(str); {
|
||||
idx += findAnsiStart(str[idx:])
|
||||
if idx == len(str) {
|
||||
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]
|
||||
|
||||
// Check if we should continue
|
||||
prev := str[prevIdx:offset[0]]
|
||||
if proc != nil && !proc(prev, state) {
|
||||
return "", nil, nil
|
||||
}
|
||||
|
||||
prevIdx = offset[1]
|
||||
runeCount += utf8.RuneCountInString(prev)
|
||||
output.WriteString(prev)
|
||||
|
||||
newState := interpretCode(str[offset[0]:offset[1]], state)
|
||||
if !newState.equals(state) {
|
||||
if state != nil {
|
||||
// Update last offset
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||
}
|
||||
|
||||
if newState.colored() {
|
||||
// Append new offset
|
||||
state = newState
|
||||
offsets = append(offsets, ansiOffset{[2]int32{int32(runeCount), int32(runeCount)}, *state})
|
||||
} else {
|
||||
// Discard state
|
||||
state = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var rest string
|
||||
var trimmed string
|
||||
|
||||
if prevIdx == 0 {
|
||||
// No ANSI code found
|
||||
rest = str
|
||||
trimmed = str
|
||||
} else {
|
||||
rest = str[prevIdx:]
|
||||
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
|
||||
}
|
||||
return trimmed, &offsets, state
|
||||
}
|
||||
|
||||
func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
||||
// State
|
||||
var state *ansiState
|
||||
if prevState == nil {
|
||||
state = &ansiState{-1, -1, 0}
|
||||
} else {
|
||||
state = &ansiState{prevState.fg, prevState.bg, prevState.attr}
|
||||
}
|
||||
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||
return state
|
||||
}
|
||||
|
||||
ptr := &state.fg
|
||||
state256 := 0
|
||||
|
||||
init := func() {
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.attr = 0
|
||||
state256 = 0
|
||||
}
|
||||
|
||||
ansiCode = ansiCode[2 : len(ansiCode)-1]
|
||||
if len(ansiCode) == 0 {
|
||||
init()
|
||||
}
|
||||
for _, code := range strings.Split(ansiCode, ";") {
|
||||
if num, err := strconv.Atoi(code); err == nil {
|
||||
switch state256 {
|
||||
case 0:
|
||||
switch num {
|
||||
case 38:
|
||||
ptr = &state.fg
|
||||
state256++
|
||||
case 48:
|
||||
ptr = &state.bg
|
||||
state256++
|
||||
case 39:
|
||||
state.fg = -1
|
||||
case 49:
|
||||
state.bg = -1
|
||||
case 1:
|
||||
state.attr = state.attr | tui.Bold
|
||||
case 2:
|
||||
state.attr = state.attr | tui.Dim
|
||||
case 3:
|
||||
state.attr = state.attr | tui.Italic
|
||||
case 4:
|
||||
state.attr = state.attr | tui.Underline
|
||||
case 5:
|
||||
state.attr = state.attr | tui.Blink
|
||||
case 7:
|
||||
state.attr = state.attr | tui.Reverse
|
||||
case 23: // tput rmso
|
||||
state.attr = state.attr &^ tui.Italic
|
||||
case 24: // tput rmul
|
||||
state.attr = state.attr &^ tui.Underline
|
||||
case 0:
|
||||
init()
|
||||
default:
|
||||
if num >= 30 && num <= 37 {
|
||||
state.fg = tui.Color(num - 30)
|
||||
} else if num >= 40 && num <= 47 {
|
||||
state.bg = tui.Color(num - 40)
|
||||
} else if num >= 90 && num <= 97 {
|
||||
state.fg = tui.Color(num - 90 + 8)
|
||||
} else if num >= 100 && num <= 107 {
|
||||
state.bg = tui.Color(num - 100 + 8)
|
||||
}
|
||||
}
|
||||
case 1:
|
||||
switch num {
|
||||
case 2:
|
||||
state256 = 10 // MAGIC
|
||||
case 5:
|
||||
state256++
|
||||
default:
|
||||
state256 = 0
|
||||
}
|
||||
case 2:
|
||||
*ptr = tui.Color(num)
|
||||
state256 = 0
|
||||
case 10:
|
||||
*ptr = tui.Color(1<<24) | tui.Color(num<<16)
|
||||
state256++
|
||||
case 11:
|
||||
*ptr = *ptr | tui.Color(num<<8)
|
||||
state256++
|
||||
case 12:
|
||||
*ptr = *ptr | tui.Color(num)
|
||||
state256 = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
if state256 > 0 {
|
||||
*ptr = -1
|
||||
}
|
||||
return state
|
||||
}
|
||||
187
src/ansi_test.go
Normal file
187
src/ansi_test.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
)
|
||||
|
||||
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
|
||||
if bold {
|
||||
attr = tui.Bold
|
||||
}
|
||||
if offset.offset[0] != b || offset.offset[1] != e ||
|
||||
offset.color.fg != fg || offset.color.bg != bg || offset.color.attr != attr {
|
||||
t.Error(offset, b, e, fg, bg, attr)
|
||||
}
|
||||
}
|
||||
|
||||
src := "hello world"
|
||||
var state *ansiState
|
||||
clean := "\x1b[0m"
|
||||
check := func(assertion func(ansiOffsets *[]ansiOffset, state *ansiState)) {
|
||||
output, ansiOffsets, newState := extractColor(src, state, nil)
|
||||
state = newState
|
||||
if output != "hello world" {
|
||||
t.Errorf("Invalid output: %s %v", output, []rune(output))
|
||||
}
|
||||
fmt.Println(src, ansiOffsets, clean)
|
||||
assertion(ansiOffsets, state)
|
||||
}
|
||||
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if offsets != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "\x1b[0mhello world"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if offsets != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "\x1b[1mhello world"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert((*offsets)[0], 0, 11, -1, -1, true)
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert((*offsets)[0], 0, 6, -1, -1, true)
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "\x1b[1mhello \x1b[Kworld"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert((*offsets)[0], 0, 11, -1, -1, true)
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "hello \x1b[34;45;1mworld"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert((*offsets)[0], 6, 11, 4, 5, true)
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert((*offsets)[0], 6, 11, 4, 5, true)
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "hello \x1b[34;45;1mwor\x1b[0mld"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert((*offsets)[0], 6, 9, 4, 5, true)
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 3 {
|
||||
t.Fail()
|
||||
}
|
||||
assert((*offsets)[0], 6, 8, 4, 233, true)
|
||||
assert((*offsets)[1], 8, 9, 161, 233, true)
|
||||
assert((*offsets)[2], 10, 11, 161, -1, false)
|
||||
})
|
||||
|
||||
// {38,48};5;{38,48}
|
||||
state = nil
|
||||
src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 2 {
|
||||
t.Fail()
|
||||
}
|
||||
assert((*offsets)[0], 6, 9, 38, 48, true)
|
||||
assert((*offsets)[1], 9, 10, 48, 38, true)
|
||||
})
|
||||
|
||||
src = "hello \x1b[32;1mworld"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
if state.fg != 2 || state.bg != -1 || state.attr == 0 {
|
||||
t.Fail()
|
||||
}
|
||||
assert((*offsets)[0], 6, 11, 2, -1, true)
|
||||
})
|
||||
|
||||
src = "hello world"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
if state.fg != 2 || state.bg != -1 || state.attr == 0 {
|
||||
t.Fail()
|
||||
}
|
||||
assert((*offsets)[0], 0, 11, 2, -1, true)
|
||||
})
|
||||
|
||||
src = "hello \x1b[0;38;5;200;48;5;100mworld"
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 2 {
|
||||
t.Fail()
|
||||
}
|
||||
if state.fg != 200 || state.bg != 100 || state.attr > 0 {
|
||||
t.Fail()
|
||||
}
|
||||
assert((*offsets)[0], 0, 6, 2, -1, true)
|
||||
assert((*offsets)[1], 6, 11, 200, 100, false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnsiCodeStringConversion(t *testing.T) {
|
||||
assert := func(code string, prevState *ansiState, expected string) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
assert("\x1b[m", nil, "")
|
||||
assert("\x1b[m", &ansiState{attr: tui.Blink}, "")
|
||||
|
||||
assert("\x1b[31m", nil, "\x1b[31;49m")
|
||||
assert("\x1b[41m", nil, "\x1b[39;41m")
|
||||
|
||||
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[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},
|
||||
"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
|
||||
}
|
||||
81
src/cache.go
Normal file
81
src/cache.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package fzf
|
||||
|
||||
import "sync"
|
||||
|
||||
// queryCache associates strings to lists of items
|
||||
type queryCache map[string][]Result
|
||||
|
||||
// ChunkCache associates Chunk and query string to lists of items
|
||||
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)}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
|
||||
cc.mutex.Lock()
|
||||
defer cc.mutex.Unlock()
|
||||
|
||||
qc, ok := cc.cache[chunk]
|
||||
if !ok {
|
||||
cc.cache[chunk] = &queryCache{}
|
||||
qc = cc.cache[chunk]
|
||||
}
|
||||
(*qc)[key] = list
|
||||
}
|
||||
|
||||
// Lookup is called to lookup ChunkCache
|
||||
func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []Result {
|
||||
if len(key) == 0 || !chunk.IsFull() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cc.mutex.Lock()
|
||||
defer cc.mutex.Unlock()
|
||||
|
||||
qc, ok := cc.cache[chunk]
|
||||
if ok {
|
||||
list, ok := (*qc)[key]
|
||||
if ok {
|
||||
return list
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc *ChunkCache) Search(chunk *Chunk, key string) []Result {
|
||||
if len(key) == 0 || !chunk.IsFull() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cc.mutex.Lock()
|
||||
defer cc.mutex.Unlock()
|
||||
|
||||
qc, ok := cc.cache[chunk]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
for idx := 1; idx < len(key); idx++ {
|
||||
// [---------| ] | [ |---------]
|
||||
// [--------| ] | [ |--------]
|
||||
// [-------| ] | [ |-------]
|
||||
prefix := key[:len(key)-idx]
|
||||
suffix := key[idx:]
|
||||
for _, substr := range [2]string{prefix, suffix} {
|
||||
if cached, found := (*qc)[substr]; found {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
39
src/cache_test.go
Normal file
39
src/cache_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package fzf
|
||||
|
||||
import "testing"
|
||||
|
||||
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)
|
||||
|
||||
{ // chunk1 is not full
|
||||
cached := cache.Lookup(chunk1p, "foo")
|
||||
if cached != nil {
|
||||
t.Error("Cached disabled for non-empty chunks", cached)
|
||||
}
|
||||
}
|
||||
{
|
||||
cached := cache.Lookup(chunk2p, "foo")
|
||||
if cached == nil || len(cached) != 1 {
|
||||
t.Error("Expected 1 item cached", cached)
|
||||
}
|
||||
}
|
||||
{
|
||||
cached := cache.Lookup(chunk2p, "bar")
|
||||
if cached == nil || len(cached) != 2 {
|
||||
t.Error("Expected 2 items cached", cached)
|
||||
}
|
||||
}
|
||||
{
|
||||
cached := cache.Lookup(chunk1p, "foobar")
|
||||
if cached != nil {
|
||||
t.Error("Expected 0 item cached", cached)
|
||||
}
|
||||
}
|
||||
}
|
||||
89
src/chunklist.go
Normal file
89
src/chunklist.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package fzf
|
||||
|
||||
import "sync"
|
||||
|
||||
// Chunk is a list of Items whose size has the upper limit of chunkSize
|
||||
type Chunk struct {
|
||||
items [chunkSize]Item
|
||||
count int
|
||||
}
|
||||
|
||||
// ItemBuilder is a closure type that builds Item object from byte array
|
||||
type ItemBuilder func(*Item, []byte) bool
|
||||
|
||||
// ChunkList is a list of Chunks
|
||||
type ChunkList struct {
|
||||
chunks []*Chunk
|
||||
mutex sync.Mutex
|
||||
trans ItemBuilder
|
||||
}
|
||||
|
||||
// NewChunkList returns a new ChunkList
|
||||
func NewChunkList(trans ItemBuilder) *ChunkList {
|
||||
return &ChunkList{
|
||||
chunks: []*Chunk{},
|
||||
mutex: sync.Mutex{},
|
||||
trans: trans}
|
||||
}
|
||||
|
||||
func (c *Chunk) push(trans ItemBuilder, data []byte) bool {
|
||||
if trans(&c.items[c.count], data) {
|
||||
c.count++
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsFull returns true if the Chunk is full
|
||||
func (c *Chunk) IsFull() bool {
|
||||
return c.count == chunkSize
|
||||
}
|
||||
|
||||
func (cl *ChunkList) lastChunk() *Chunk {
|
||||
return cl.chunks[len(cl.chunks)-1]
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Push adds the item to the list
|
||||
func (cl *ChunkList) Push(data []byte) bool {
|
||||
cl.mutex.Lock()
|
||||
|
||||
if len(cl.chunks) == 0 || cl.lastChunk().IsFull() {
|
||||
cl.chunks = append(cl.chunks, &Chunk{})
|
||||
}
|
||||
|
||||
ret := cl.lastChunk().push(cl.trans, data)
|
||||
cl.mutex.Unlock()
|
||||
return ret
|
||||
}
|
||||
|
||||
// Clear clears the data
|
||||
func (cl *ChunkList) Clear() {
|
||||
cl.mutex.Lock()
|
||||
cl.chunks = nil
|
||||
cl.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Snapshot returns immutable snapshot of the ChunkList
|
||||
func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
|
||||
cl.mutex.Lock()
|
||||
|
||||
ret := make([]*Chunk, len(cl.chunks))
|
||||
copy(ret, cl.chunks)
|
||||
|
||||
// Duplicate the last chunk
|
||||
if cnt := len(ret); cnt > 0 {
|
||||
newChunk := *ret[cnt-1]
|
||||
ret[cnt-1] = &newChunk
|
||||
}
|
||||
|
||||
cl.mutex.Unlock()
|
||||
return ret, CountItems(ret)
|
||||
}
|
||||
80
src/chunklist_test.go
Normal file
80
src/chunklist_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func TestChunkList(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byScore, byLength}
|
||||
|
||||
cl := NewChunkList(func(item *Item, s []byte) bool {
|
||||
item.text = util.ToChars(s)
|
||||
return true
|
||||
})
|
||||
|
||||
// Snapshot
|
||||
snapshot, count := cl.Snapshot()
|
||||
if len(snapshot) > 0 || count > 0 {
|
||||
t.Error("Snapshot should be empty now")
|
||||
}
|
||||
|
||||
// Add some data
|
||||
cl.Push([]byte("hello"))
|
||||
cl.Push([]byte("world"))
|
||||
|
||||
// Previously created snapshot should remain the same
|
||||
if len(snapshot) > 0 {
|
||||
t.Error("Snapshot should not have changed")
|
||||
}
|
||||
|
||||
// But the new snapshot should contain the added items
|
||||
snapshot, count = cl.Snapshot()
|
||||
if len(snapshot) != 1 && count != 2 {
|
||||
t.Error("Snapshot should not be empty now")
|
||||
}
|
||||
|
||||
// Check the content of the ChunkList
|
||||
chunk1 := snapshot[0]
|
||||
if chunk1.count != 2 {
|
||||
t.Error("Snapshot should contain only two items")
|
||||
}
|
||||
if chunk1.items[0].text.ToString() != "hello" ||
|
||||
chunk1.items[1].text.ToString() != "world" {
|
||||
t.Error("Invalid data")
|
||||
}
|
||||
if chunk1.IsFull() {
|
||||
t.Error("Chunk should not have been marked full yet")
|
||||
}
|
||||
|
||||
// Add more data
|
||||
for i := 0; i < chunkSize*2; i++ {
|
||||
cl.Push([]byte(fmt.Sprintf("item %d", i)))
|
||||
}
|
||||
|
||||
// Previous snapshot should remain the same
|
||||
if len(snapshot) != 1 {
|
||||
t.Error("Snapshot should stay the same")
|
||||
}
|
||||
|
||||
// New snapshot
|
||||
snapshot, count = cl.Snapshot()
|
||||
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")
|
||||
}
|
||||
if snapshot[2].count != 2 {
|
||||
t.Error("Unexpected number of items")
|
||||
}
|
||||
|
||||
cl.Push([]byte("hello"))
|
||||
cl.Push([]byte("world"))
|
||||
|
||||
lastChunkCount := snapshot[len(snapshot)-1].count
|
||||
if lastChunkCount != 2 {
|
||||
t.Error("Unexpected number of items:", lastChunkCount)
|
||||
}
|
||||
}
|
||||
84
src/constants.go
Normal file
84
src/constants.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// Core
|
||||
coordinatorDelayMax time.Duration = 100 * time.Millisecond
|
||||
coordinatorDelayStep time.Duration = 10 * time.Millisecond
|
||||
|
||||
// Reader
|
||||
readerBufferSize = 64 * 1024
|
||||
readerPollIntervalMin = 10 * time.Millisecond
|
||||
readerPollIntervalStep = 5 * time.Millisecond
|
||||
readerPollIntervalMax = 50 * time.Millisecond
|
||||
|
||||
// Terminal
|
||||
initialDelay = 20 * time.Millisecond
|
||||
initialDelayTac = 100 * time.Millisecond
|
||||
spinnerDuration = 100 * time.Millisecond
|
||||
previewCancelWait = 500 * time.Millisecond
|
||||
previewChunkDelay = 100 * time.Millisecond
|
||||
previewDelayed = 500 * time.Millisecond
|
||||
maxPatternLength = 300
|
||||
maxMulti = math.MaxInt32
|
||||
|
||||
// Matcher
|
||||
numPartitionsMultiplier = 8
|
||||
maxPartitions = 32
|
||||
progressMinDuration = 200 * time.Millisecond
|
||||
|
||||
// Capacity of each chunk
|
||||
chunkSize int = 100
|
||||
|
||||
// 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
|
||||
|
||||
// Not to cache mergers with large lists
|
||||
mergerCacheMax int = 100000
|
||||
|
||||
// History
|
||||
defaultHistoryMax int = 1000
|
||||
|
||||
// Jump labels
|
||||
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-"`
|
||||
}
|
||||
}
|
||||
|
||||
// fzf events
|
||||
const (
|
||||
EvtReadNew util.EventType = iota
|
||||
EvtReadFin
|
||||
EvtSearchNew
|
||||
EvtSearchProgress
|
||||
EvtSearchFin
|
||||
EvtHeader
|
||||
EvtReady
|
||||
)
|
||||
|
||||
const (
|
||||
exitCancel = -1
|
||||
exitOk = 0
|
||||
exitNoMatch = 1
|
||||
exitError = 2
|
||||
exitInterrupt = 130
|
||||
)
|
||||
344
src/core.go
Normal file
344
src/core.go
Normal file
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
/*
|
||||
Reader -> EvtReadFin
|
||||
Reader -> EvtReadNew -> Matcher (restart)
|
||||
Terminal -> EvtSearchNew:bool -> Matcher (restart)
|
||||
Matcher -> EvtSearchProgress -> Terminal (update info)
|
||||
Matcher -> EvtSearchFin -> Terminal (update list)
|
||||
Matcher -> EvtHeader -> Terminal (update header)
|
||||
*/
|
||||
|
||||
// Run starts fzf
|
||||
func Run(opts *Options, version string, revision string) {
|
||||
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()
|
||||
|
||||
// ANSI code processor
|
||||
ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
return util.ToChars(data), nil
|
||||
}
|
||||
|
||||
var lineAnsiState, prevLineAnsiState *ansiState
|
||||
if opts.Ansi {
|
||||
if opts.Theme.Colored {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chunk list
|
||||
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
|
||||
}
|
||||
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.Colored && 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
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++
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Reader
|
||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
||||
var reader *Reader
|
||||
if !streamingFilter {
|
||||
reader = NewReader(func(data []byte) bool {
|
||||
return chunkList.Push(data)
|
||||
}, eventBox, opts.ReadZero, opts.Filter == nil)
|
||||
go reader.ReadSource()
|
||||
}
|
||||
|
||||
// Matcher
|
||||
forward := true
|
||||
for _, cri := range opts.Criteria[1:] {
|
||||
if cri == byEnd {
|
||||
forward = false
|
||||
break
|
||||
}
|
||||
if cri == byBegin {
|
||||
break
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
|
||||
|
||||
// Filtering mode
|
||||
if opts.Filter != nil {
|
||||
if opts.PrintQuery {
|
||||
opts.Printer(*opts.Filter)
|
||||
}
|
||||
|
||||
pattern := patternBuilder([]rune(*opts.Filter))
|
||||
matcher.sort = pattern.sortable
|
||||
|
||||
found := false
|
||||
if streamingFilter {
|
||||
slab := util.MakeSlab(slab16Size, slab32Size)
|
||||
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())
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, eventBox, opts.ReadZero, false)
|
||||
reader.ReadSource()
|
||||
} else {
|
||||
eventBox.Unwatch(EvtReadNew)
|
||||
eventBox.WaitFor(EvtReadFin)
|
||||
|
||||
snapshot, _ := chunkList.Snapshot()
|
||||
merger, _ := matcher.scan(MatchRequest{
|
||||
chunks: snapshot,
|
||||
pattern: pattern})
|
||||
for i := 0; i < merger.Length(); i++ {
|
||||
opts.Printer(merger.Get(i).item.AsString(opts.Ansi))
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if found {
|
||||
os.Exit(exitOk)
|
||||
}
|
||||
os.Exit(exitNoMatch)
|
||||
}
|
||||
|
||||
// Synchronous search
|
||||
if opts.Sync {
|
||||
eventBox.Unwatch(EvtReadNew)
|
||||
eventBox.WaitFor(EvtReadFin)
|
||||
}
|
||||
|
||||
// Go interactive
|
||||
go matcher.Loop()
|
||||
|
||||
// Terminal I/O
|
||||
terminal := NewTerminal(opts, eventBox)
|
||||
deferred := opts.Select1 || opts.Exit0
|
||||
go terminal.Loop()
|
||||
if !deferred {
|
||||
terminal.startChan <- true
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
eventBox.Watch(EvtReadNew)
|
||||
for {
|
||||
delay := true
|
||||
ticks++
|
||||
input := func() []rune {
|
||||
if opts.Phony {
|
||||
return []rune{}
|
||||
}
|
||||
return []rune(terminal.Input())
|
||||
}
|
||||
eventBox.Wait(func(events *util.Events) {
|
||||
if _, fin := (*events)[EvtReadFin]; fin {
|
||||
delete(*events, EvtReadNew)
|
||||
}
|
||||
for evt, value := range *events {
|
||||
switch evt {
|
||||
|
||||
case EvtReadNew, EvtReadFin:
|
||||
if evt == EvtReadFin && nextCommand != nil {
|
||||
restart(*nextCommand)
|
||||
nextCommand = 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)
|
||||
}
|
||||
matcher.Reset(snapshot, input(), false, !reading, sort, clearCache())
|
||||
|
||||
case EvtSearchNew:
|
||||
var command *string
|
||||
switch val := value.(type) {
|
||||
case searchRequest:
|
||||
sort = val.sort
|
||||
command = val.command
|
||||
}
|
||||
if command != nil {
|
||||
if reading {
|
||||
reader.terminate()
|
||||
nextCommand = command
|
||||
} else {
|
||||
restart(*command)
|
||||
}
|
||||
break
|
||||
}
|
||||
snapshot, _ := chunkList.Snapshot()
|
||||
matcher.Reset(snapshot, input(), true, !reading, sort, clearCache())
|
||||
delay = false
|
||||
|
||||
case EvtSearchProgress:
|
||||
switch val := value.(type) {
|
||||
case float32:
|
||||
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:
|
||||
if deferred {
|
||||
count := val.Length()
|
||||
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
|
||||
deferred = false
|
||||
terminal.startChan <- true
|
||||
} else if val.final {
|
||||
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
|
||||
if opts.PrintQuery {
|
||||
opts.Printer(opts.Query)
|
||||
}
|
||||
if len(opts.Expect) > 0 {
|
||||
opts.Printer("")
|
||||
}
|
||||
for i := 0; i < count; i++ {
|
||||
opts.Printer(val.Get(i).item.AsString(opts.Ansi))
|
||||
}
|
||||
if count > 0 {
|
||||
os.Exit(exitOk)
|
||||
}
|
||||
os.Exit(exitNoMatch)
|
||||
}
|
||||
deferred = false
|
||||
terminal.startChan <- true
|
||||
}
|
||||
}
|
||||
terminal.UpdateList(val, clearSelection())
|
||||
}
|
||||
}
|
||||
}
|
||||
events.Clear()
|
||||
})
|
||||
if delay && reading {
|
||||
dur := util.DurWithin(
|
||||
time.Duration(ticks)*coordinatorDelayStep,
|
||||
0, coordinatorDelayMax)
|
||||
time.Sleep(dur)
|
||||
}
|
||||
}
|
||||
}
|
||||
96
src/history.go
Normal file
96
src/history.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// History struct represents input history
|
||||
type History struct {
|
||||
path string
|
||||
lines []string
|
||||
modified map[int]string
|
||||
maxSize int
|
||||
cursor int
|
||||
}
|
||||
|
||||
// NewHistory returns the pointer to a new History struct
|
||||
func NewHistory(path string, maxSize int) (*History, error) {
|
||||
fmtError := func(e error) error {
|
||||
if os.IsPermission(e) {
|
||||
return errors.New("permission denied: " + path)
|
||||
}
|
||||
return errors.New("invalid history file: " + e.Error())
|
||||
}
|
||||
|
||||
// Read history file
|
||||
data, err := ioutil.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 {
|
||||
return nil, fmtError(err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmtError(err)
|
||||
}
|
||||
}
|
||||
// Split lines and limit the maximum number of lines
|
||||
lines := strings.Split(strings.Trim(string(data), "\n"), "\n")
|
||||
if len(lines[len(lines)-1]) > 0 {
|
||||
lines = append(lines, "")
|
||||
}
|
||||
return &History{
|
||||
path: path,
|
||||
maxSize: maxSize,
|
||||
lines: lines,
|
||||
modified: make(map[int]string),
|
||||
cursor: len(lines) - 1}, nil
|
||||
}
|
||||
|
||||
func (h *History) append(line string) error {
|
||||
// We don't append empty lines
|
||||
if len(line) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
lines := append(h.lines[:len(h.lines)-1], line)
|
||||
if len(lines) > h.maxSize {
|
||||
lines = lines[len(lines)-h.maxSize:]
|
||||
}
|
||||
h.lines = append(lines, "")
|
||||
return ioutil.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
|
||||
if h.cursor == len(h.lines)-1 {
|
||||
h.lines[h.cursor] = str
|
||||
} else if h.cursor < len(h.lines)-1 {
|
||||
h.modified[h.cursor] = str
|
||||
}
|
||||
}
|
||||
|
||||
func (h *History) current() string {
|
||||
if str, prs := h.modified[h.cursor]; prs {
|
||||
return str
|
||||
}
|
||||
return h.lines[h.cursor]
|
||||
}
|
||||
|
||||
func (h *History) previous() string {
|
||||
if h.cursor > 0 {
|
||||
h.cursor--
|
||||
}
|
||||
return h.current()
|
||||
}
|
||||
|
||||
func (h *History) next() string {
|
||||
if h.cursor < len(h.lines)-1 {
|
||||
h.cursor++
|
||||
}
|
||||
return h.current()
|
||||
}
|
||||
73
src/history_test.go
Normal file
73
src/history_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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 {
|
||||
if _, e := NewHistory(path, maxHistory); e == nil {
|
||||
t.Error("Error expected for: " + path)
|
||||
}
|
||||
}
|
||||
|
||||
f, _ := ioutil.TempFile("", "fzf-history")
|
||||
f.Close()
|
||||
|
||||
{ // Append lines
|
||||
h, _ := NewHistory(f.Name(), maxHistory)
|
||||
for i := 0; i < maxHistory+10; i++ {
|
||||
h.append("foobar")
|
||||
}
|
||||
}
|
||||
{ // Read lines
|
||||
h, _ := NewHistory(f.Name(), maxHistory)
|
||||
if len(h.lines) != maxHistory+1 {
|
||||
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
|
||||
}
|
||||
for i := 0; i < maxHistory; i++ {
|
||||
if h.lines[i] != "foobar" {
|
||||
t.Error("Expected: foobar, actual: " + h.lines[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
{ // Append lines
|
||||
h, _ := NewHistory(f.Name(), maxHistory)
|
||||
h.append("barfoo")
|
||||
h.append("")
|
||||
h.append("foobarbaz")
|
||||
}
|
||||
{ // Read lines again
|
||||
h, _ := NewHistory(f.Name(), maxHistory)
|
||||
if len(h.lines) != maxHistory+1 {
|
||||
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
|
||||
}
|
||||
compare := func(idx int, exp string) {
|
||||
if h.lines[idx] != exp {
|
||||
t.Errorf("Expected: %s, actual: %s\n", exp, h.lines[idx])
|
||||
}
|
||||
}
|
||||
compare(maxHistory-3, "foobar")
|
||||
compare(maxHistory-2, "barfoo")
|
||||
compare(maxHistory-1, "foobarbaz")
|
||||
}
|
||||
}
|
||||
44
src/item.go
Normal file
44
src/item.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
// Item represents each input line. 56 bytes.
|
||||
type Item struct {
|
||||
text util.Chars // 32 = 24 + 1 + 1 + 2 + 4
|
||||
transformed *[]Token // 8
|
||||
origText *[]byte // 8
|
||||
colors *[]ansiOffset // 8
|
||||
}
|
||||
|
||||
// Index returns ordinal index of the Item
|
||||
func (item *Item) Index() int32 {
|
||||
return item.text.Index
|
||||
}
|
||||
|
||||
var minItem = Item{text: util.Chars{Index: -1}}
|
||||
|
||||
func (item *Item) TrimLength() uint16 {
|
||||
return item.text.TrimLength()
|
||||
}
|
||||
|
||||
// Colors returns ansiOffsets of the Item
|
||||
func (item *Item) Colors() []ansiOffset {
|
||||
if item.colors == nil {
|
||||
return []ansiOffset{}
|
||||
}
|
||||
return *item.colors
|
||||
}
|
||||
|
||||
// AsString returns the original string
|
||||
func (item *Item) AsString(stripAnsi bool) string {
|
||||
if item.origText != nil {
|
||||
if stripAnsi {
|
||||
trimmed, _, _ := extractColor(string(*item.origText), nil, nil)
|
||||
return trimmed
|
||||
}
|
||||
return string(*item.origText)
|
||||
}
|
||||
return item.text.ToString()
|
||||
}
|
||||
23
src/item_test.go
Normal file
23
src/item_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func TestStringPtr(t *testing.T) {
|
||||
orig := []byte("\x1b[34mfoo")
|
||||
text := []byte("\x1b[34mbar")
|
||||
item := Item{origText: &orig, text: util.ToChars(text)}
|
||||
if item.AsString(true) != "foo" || item.AsString(false) != string(orig) {
|
||||
t.Fail()
|
||||
}
|
||||
if item.AsString(true) != "foo" {
|
||||
t.Fail()
|
||||
}
|
||||
item.origText = nil
|
||||
if item.AsString(true) != string(text) || item.AsString(false) != string(text) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
235
src/matcher.go
Normal file
235
src/matcher.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
// MatchRequest represents a search request
|
||||
type MatchRequest struct {
|
||||
chunks []*Chunk
|
||||
pattern *Pattern
|
||||
final bool
|
||||
sort bool
|
||||
clearCache bool
|
||||
}
|
||||
|
||||
// Matcher is responsible for performing search
|
||||
type Matcher struct {
|
||||
patternBuilder func([]rune) *Pattern
|
||||
sort bool
|
||||
tac bool
|
||||
eventBox *util.EventBox
|
||||
reqBox *util.EventBox
|
||||
partitions int
|
||||
slab []*util.Slab
|
||||
mergerCache map[string]*Merger
|
||||
}
|
||||
|
||||
const (
|
||||
reqRetry util.EventType = iota
|
||||
reqReset
|
||||
)
|
||||
|
||||
// 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)
|
||||
return &Matcher{
|
||||
patternBuilder: patternBuilder,
|
||||
sort: sort,
|
||||
tac: tac,
|
||||
eventBox: eventBox,
|
||||
reqBox: util.NewEventBox(),
|
||||
partitions: partitions,
|
||||
slab: make([]*util.Slab, partitions),
|
||||
mergerCache: make(map[string]*Merger)}
|
||||
}
|
||||
|
||||
// Loop puts Matcher in action
|
||||
func (m *Matcher) Loop() {
|
||||
prevCount := 0
|
||||
|
||||
for {
|
||||
var request MatchRequest
|
||||
|
||||
m.reqBox.Wait(func(events *util.Events) {
|
||||
for _, val := range *events {
|
||||
switch val := val.(type) {
|
||||
case MatchRequest:
|
||||
request = val
|
||||
default:
|
||||
panic(fmt.Sprintf("Unexpected type: %T", val))
|
||||
}
|
||||
}
|
||||
events.Clear()
|
||||
})
|
||||
|
||||
if request.sort != m.sort || request.clearCache {
|
||||
m.sort = request.sort
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
clearChunkCache()
|
||||
}
|
||||
|
||||
// Restart search
|
||||
patternString := request.pattern.AsString()
|
||||
var merger *Merger
|
||||
cancelled := false
|
||||
count := CountItems(request.chunks)
|
||||
|
||||
foundCache := false
|
||||
if count == prevCount {
|
||||
// Look up mergerCache
|
||||
if cached, found := m.mergerCache[patternString]; found {
|
||||
foundCache = true
|
||||
merger = cached
|
||||
}
|
||||
} else {
|
||||
// Invalidate mergerCache
|
||||
prevCount = count
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
}
|
||||
|
||||
if !foundCache {
|
||||
merger, cancelled = m.scan(request)
|
||||
}
|
||||
|
||||
if !cancelled {
|
||||
if merger.cacheable() {
|
||||
m.mergerCache[patternString] = merger
|
||||
}
|
||||
merger.final = request.final
|
||||
m.eventBox.Set(EvtSearchFin, merger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
startedAt := time.Now()
|
||||
|
||||
numChunks := len(request.chunks)
|
||||
if numChunks == 0 {
|
||||
return EmptyMerger, false
|
||||
}
|
||||
pattern := request.pattern
|
||||
if pattern.IsEmpty() {
|
||||
return PassMerger(&request.chunks, m.tac), false
|
||||
}
|
||||
|
||||
cancelled := util.NewAtomicBool(false)
|
||||
|
||||
slices := m.sliceChunks(request.chunks)
|
||||
numSlices := len(slices)
|
||||
resultChan := make(chan partialResult, numSlices)
|
||||
countChan := make(chan int, numChunks)
|
||||
waitGroup := sync.WaitGroup{}
|
||||
|
||||
for idx, chunks := range slices {
|
||||
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)
|
||||
if cancelled.Get() {
|
||||
return
|
||||
}
|
||||
countChan <- len(matches)
|
||||
}
|
||||
sliceMatches := make([]Result, 0, count)
|
||||
for _, matches := range allMatches {
|
||||
sliceMatches = append(sliceMatches, matches...)
|
||||
}
|
||||
if m.sort {
|
||||
if m.tac {
|
||||
sort.Sort(ByRelevanceTac(sliceMatches))
|
||||
} else {
|
||||
sort.Sort(ByRelevance(sliceMatches))
|
||||
}
|
||||
}
|
||||
resultChan <- partialResult{idx, sliceMatches}
|
||||
}(idx, m.slab[idx], chunks)
|
||||
}
|
||||
|
||||
wait := func() bool {
|
||||
cancelled.Set(true)
|
||||
waitGroup.Wait()
|
||||
return true
|
||||
}
|
||||
|
||||
count := 0
|
||||
matchCount := 0
|
||||
for matchesInChunk := range countChan {
|
||||
count++
|
||||
matchCount += matchesInChunk
|
||||
|
||||
if count == numChunks {
|
||||
break
|
||||
}
|
||||
|
||||
if m.reqBox.Peek(reqReset) {
|
||||
return nil, wait()
|
||||
}
|
||||
|
||||
if time.Since(startedAt) > progressMinDuration {
|
||||
m.eventBox.Set(EvtSearchProgress, float32(count)/float32(numChunks))
|
||||
}
|
||||
}
|
||||
|
||||
partialResults := make([][]Result, numSlices)
|
||||
for range slices {
|
||||
partialResult := <-resultChan
|
||||
partialResults[partialResult.index] = partialResult.matches
|
||||
}
|
||||
return NewMerger(pattern, partialResults, m.sort, m.tac), 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) {
|
||||
pattern := m.patternBuilder(patternRunes)
|
||||
|
||||
var event util.EventType
|
||||
if cancel {
|
||||
event = reqReset
|
||||
} else {
|
||||
event = reqRetry
|
||||
}
|
||||
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, clearCache})
|
||||
}
|
||||
121
src/merger.go
Normal file
121
src/merger.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package fzf
|
||||
|
||||
import "fmt"
|
||||
|
||||
// EmptyMerger is a Merger with no data
|
||||
var EmptyMerger = NewMerger(nil, [][]Result{}, false, false)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// PassMerger returns a new Merger that simply returns the items in the
|
||||
// original order
|
||||
func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
|
||||
mg := Merger{
|
||||
pattern: nil,
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0}
|
||||
|
||||
for _, chunk := range *mg.chunks {
|
||||
mg.count += chunk.count
|
||||
}
|
||||
return &mg
|
||||
}
|
||||
|
||||
// NewMerger returns a new Merger
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger {
|
||||
mg := Merger{
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
merged: []Result{},
|
||||
chunks: nil,
|
||||
cursors: make([]int, len(lists)),
|
||||
sorted: sorted,
|
||||
tac: tac,
|
||||
final: false,
|
||||
count: 0}
|
||||
|
||||
for _, list := range mg.lists {
|
||||
mg.count += len(list)
|
||||
}
|
||||
return &mg
|
||||
}
|
||||
|
||||
// Length returns the number of items
|
||||
func (mg *Merger) Length() int {
|
||||
return mg.count
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
chunk := (*mg.chunks)[idx/chunkSize]
|
||||
return Result{item: &chunk.items[idx%chunkSize]}
|
||||
}
|
||||
|
||||
if mg.sorted {
|
||||
return mg.mergedGet(idx)
|
||||
}
|
||||
|
||||
if mg.tac {
|
||||
idx = mg.count - idx - 1
|
||||
}
|
||||
for _, list := range mg.lists {
|
||||
numItems := len(list)
|
||||
if idx < numItems {
|
||||
return list[idx]
|
||||
}
|
||||
idx -= numItems
|
||||
}
|
||||
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
|
||||
}
|
||||
|
||||
func (mg *Merger) cacheable() bool {
|
||||
return mg.count < mergerCacheMax
|
||||
}
|
||||
|
||||
func (mg *Merger) mergedGet(idx int) Result {
|
||||
for i := len(mg.merged); i <= idx; i++ {
|
||||
minRank := minRank()
|
||||
minIdx := -1
|
||||
for listIdx, list := range mg.lists {
|
||||
cursor := mg.cursors[listIdx]
|
||||
if cursor < 0 || cursor == len(list) {
|
||||
mg.cursors[listIdx] = -1
|
||||
continue
|
||||
}
|
||||
if cursor >= 0 {
|
||||
rank := list[cursor]
|
||||
if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
|
||||
minRank = rank
|
||||
minIdx = listIdx
|
||||
}
|
||||
}
|
||||
mg.cursors[listIdx] = cursor
|
||||
}
|
||||
|
||||
if minIdx >= 0 {
|
||||
chosen := mg.lists[minIdx]
|
||||
mg.merged = append(mg.merged, chosen[mg.cursors[minIdx]])
|
||||
mg.cursors[minIdx]++
|
||||
} else {
|
||||
panic(fmt.Sprintf("Index out of bounds (sorted, %d/%d)", i, mg.count))
|
||||
}
|
||||
}
|
||||
return mg.merged[idx]
|
||||
}
|
||||
88
src/merger_test.go
Normal file
88
src/merger_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func assert(t *testing.T, cond bool, msg ...string) {
|
||||
if !cond {
|
||||
t.Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func randResult() Result {
|
||||
str := fmt.Sprintf("%d", rand.Uint32())
|
||||
chars := util.ToChars([]byte(str))
|
||||
chars.Index = rand.Int31()
|
||||
return Result{item: &Item{text: chars}}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
func buildLists(partiallySorted bool) ([][]Result, []Result) {
|
||||
numLists := 4
|
||||
lists := make([][]Result, numLists)
|
||||
cnt := 0
|
||||
for i := 0; i < numLists; i++ {
|
||||
numResults := rand.Int() % 20
|
||||
cnt += numResults
|
||||
lists[i] = make([]Result, numResults)
|
||||
for j := 0; j < numResults; j++ {
|
||||
item := randResult()
|
||||
lists[i][j] = item
|
||||
}
|
||||
if partiallySorted {
|
||||
sort.Sort(ByRelevance(lists[i]))
|
||||
}
|
||||
}
|
||||
items := []Result{}
|
||||
for _, list := range lists {
|
||||
items = append(items, list...)
|
||||
}
|
||||
return lists, items
|
||||
}
|
||||
|
||||
func TestMergerUnsorted(t *testing.T) {
|
||||
lists, items := buildLists(false)
|
||||
cnt := len(items)
|
||||
|
||||
// Not sorted: same order
|
||||
mg := NewMerger(nil, lists, false, false)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
for i := 0; i < cnt; i++ {
|
||||
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergerSorted(t *testing.T) {
|
||||
lists, items := buildLists(true)
|
||||
cnt := len(items)
|
||||
|
||||
// Sorted sorted order
|
||||
mg := NewMerger(nil, lists, true, false)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
sort.Sort(ByRelevance(items))
|
||||
for i := 0; i < cnt; i++ {
|
||||
if items[i] != mg.Get(i) {
|
||||
t.Error("Not sorted", items[i], mg.Get(i))
|
||||
}
|
||||
}
|
||||
|
||||
// Inverse order
|
||||
mg2 := NewMerger(nil, lists, true, false)
|
||||
for i := cnt - 1; i >= 0; i-- {
|
||||
if items[i] != mg2.Get(i) {
|
||||
t.Error("Not sorted", items[i], mg2.Get(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
1547
src/options.go
Normal file
1547
src/options.go
Normal file
File diff suppressed because it is too large
Load Diff
448
src/options_test.go
Normal file
448
src/options_test.go
Normal file
@@ -0,0 +1,448 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
)
|
||||
|
||||
func TestDelimiterRegex(t *testing.T) {
|
||||
// Valid regex
|
||||
delim := delimiterRegexp(".")
|
||||
if delim.regex == nil || delim.str != nil {
|
||||
t.Error(delim)
|
||||
}
|
||||
// Broken regex -> string
|
||||
delim = delimiterRegexp("[0-9")
|
||||
if delim.regex != nil || *delim.str != "[0-9" {
|
||||
t.Error(delim)
|
||||
}
|
||||
// Valid regex
|
||||
delim = delimiterRegexp("[0-9]")
|
||||
if delim.regex.String() != "[0-9]" || delim.str != nil {
|
||||
t.Error(delim)
|
||||
}
|
||||
// Tab character
|
||||
delim = delimiterRegexp("\t")
|
||||
if delim.regex != nil || *delim.str != "\t" {
|
||||
t.Error(delim)
|
||||
}
|
||||
// Tab expression
|
||||
delim = delimiterRegexp("\\t")
|
||||
if delim.regex != nil || *delim.str != "\t" {
|
||||
t.Error(delim)
|
||||
}
|
||||
// Tabs -> regex
|
||||
delim = delimiterRegexp("\t+")
|
||||
if delim.regex == nil || delim.str != nil {
|
||||
t.Error(delim)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelimiterRegexString(t *testing.T) {
|
||||
delim := delimiterRegexp("*")
|
||||
tokens := Tokenize("-*--*---**---", delim)
|
||||
if delim.regex != nil ||
|
||||
tokens[0].text.ToString() != "-*" ||
|
||||
tokens[1].text.ToString() != "--*" ||
|
||||
tokens[2].text.ToString() != "---*" ||
|
||||
tokens[3].text.ToString() != "*" ||
|
||||
tokens[4].text.ToString() != "---" {
|
||||
t.Errorf("%s %v %d", delim, tokens, len(tokens))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelimiterRegexRegex(t *testing.T) {
|
||||
delim := delimiterRegexp("--\\*")
|
||||
tokens := Tokenize("-*--*---**---", delim)
|
||||
if delim.str != nil ||
|
||||
tokens[0].text.ToString() != "-*--*" ||
|
||||
tokens[1].text.ToString() != "---*" ||
|
||||
tokens[2].text.ToString() != "*---" {
|
||||
t.Errorf("%s %d", tokens, len(tokens))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitNth(t *testing.T) {
|
||||
{
|
||||
ranges := splitNth("..")
|
||||
if len(ranges) != 1 ||
|
||||
ranges[0].begin != rangeEllipsis ||
|
||||
ranges[0].end != rangeEllipsis {
|
||||
t.Errorf("%v", ranges)
|
||||
}
|
||||
}
|
||||
{
|
||||
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 ||
|
||||
ranges[2].begin != 2 || ranges[2].end != 3 ||
|
||||
ranges[3].begin != 4 || ranges[3].end != rangeEllipsis ||
|
||||
ranges[4].begin != -3 || ranges[4].end != -2 ||
|
||||
ranges[5].begin != rangeEllipsis || ranges[5].end != rangeEllipsis ||
|
||||
ranges[6].begin != 2 || ranges[6].end != 2 ||
|
||||
ranges[7].begin != -2 || ranges[7].end != -2 ||
|
||||
ranges[8].begin != 2 || ranges[8].end != -2 ||
|
||||
ranges[9].begin != rangeEllipsis || ranges[9].end != rangeEllipsis {
|
||||
t.Errorf("%v", ranges)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIrrelevantNth(t *testing.T) {
|
||||
{
|
||||
opts := defaultOptions()
|
||||
words := []string{"--nth", "..", "-x"}
|
||||
parseOptions(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"}} {
|
||||
{
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
postProcessOptions(opts)
|
||||
if len(opts.Nth) != 0 {
|
||||
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||
}
|
||||
}
|
||||
{
|
||||
opts := defaultOptions()
|
||||
words = append(words, "-x")
|
||||
parseOptions(opts, words)
|
||||
postProcessOptions(opts)
|
||||
if len(opts.Nth) != 2 {
|
||||
t.Errorf("nth should not be empty: %v", opts.Nth)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
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")
|
||||
|
||||
// Synonyms
|
||||
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.Tab, "tab")
|
||||
check(tui.BTab, "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", "")
|
||||
if len(pairs) != 11 {
|
||||
t.Error(11)
|
||||
}
|
||||
check(tui.Tab, "Ctrl-I")
|
||||
check(tui.PgUp, "page-up")
|
||||
check(tui.PgDn, "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")
|
||||
}
|
||||
|
||||
func TestParseKeysWithComma(t *testing.T) {
|
||||
checkN := func(a int, b int) {
|
||||
if a != b {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
pairs := parseKeyChords(",", "")
|
||||
checkN(len(pairs), 1)
|
||||
check(pairs, tui.AltZ+',', ",")
|
||||
|
||||
pairs = parseKeyChords(",,a,b", "")
|
||||
checkN(len(pairs), 3)
|
||||
check(pairs, tui.AltZ+'a', "a")
|
||||
check(pairs, tui.AltZ+'b', "b")
|
||||
check(pairs, tui.AltZ+',', ",")
|
||||
|
||||
pairs = parseKeyChords("a,b,,", "")
|
||||
checkN(len(pairs), 3)
|
||||
check(pairs, tui.AltZ+'a', "a")
|
||||
check(pairs, tui.AltZ+'b', "b")
|
||||
check(pairs, tui.AltZ+',', ",")
|
||||
|
||||
pairs = parseKeyChords("a,,,b", "")
|
||||
checkN(len(pairs), 3)
|
||||
check(pairs, tui.AltZ+'a', "a")
|
||||
check(pairs, tui.AltZ+'b', "b")
|
||||
check(pairs, tui.AltZ+',', ",")
|
||||
|
||||
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+',', ",")
|
||||
|
||||
pairs = parseKeyChords(",,,", "")
|
||||
checkN(len(pairs), 1)
|
||||
check(pairs, tui.AltZ+',', ",")
|
||||
}
|
||||
|
||||
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]))
|
||||
return
|
||||
}
|
||||
for idx, action := range keymap[keyName] {
|
||||
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)
|
||||
}
|
||||
}
|
||||
check(tui.CtrlA, "", 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 {};,"+
|
||||
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
|
||||
"x:Execute(foo+bar),X:execute/bar+baz/"+
|
||||
",f1:+top,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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
parseKeymap(keymap, "f1:abort")
|
||||
check(tui.F1, "", actAbort)
|
||||
}
|
||||
|
||||
func TestColorSpec(t *testing.T) {
|
||||
theme := tui.Dark256
|
||||
dark := parseTheme(theme, "dark")
|
||||
if *dark != *theme {
|
||||
t.Errorf("colors should be equivalent")
|
||||
}
|
||||
if dark == theme {
|
||||
t.Errorf("point should not be equivalent")
|
||||
}
|
||||
|
||||
light := parseTheme(theme, "dark,light")
|
||||
if *light == *theme {
|
||||
t.Errorf("should not be equivalent")
|
||||
}
|
||||
if *light != *tui.Light256 {
|
||||
t.Errorf("colors should be equivalent")
|
||||
}
|
||||
if light == theme {
|
||||
t.Errorf("point should not be equivalent")
|
||||
}
|
||||
|
||||
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 {
|
||||
t.Errorf("colors should not be equivalent")
|
||||
}
|
||||
customized.Fg = tui.Dark256.Fg
|
||||
customized.Bg = tui.Dark256.Bg
|
||||
if *tui.Dark256 != *customized {
|
||||
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
|
||||
}
|
||||
|
||||
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 TestDefaultCtrlNP(t *testing.T) {
|
||||
check := func(words []string, key int, expected actionType) {
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
postProcessOptions(opts)
|
||||
if opts.Keymap[key][0].t != expected {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
check([]string{}, tui.CtrlN, actDown)
|
||||
check([]string{}, tui.CtrlP, actUp)
|
||||
|
||||
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
|
||||
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
||||
|
||||
f, _ := ioutil.TempFile("", "fzf-history")
|
||||
f.Close()
|
||||
hist := "--history=" + f.Name()
|
||||
check([]string{hist}, tui.CtrlN, actNextHistory)
|
||||
check([]string{hist}, tui.CtrlP, actPreviousHistory)
|
||||
|
||||
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-p:accept"}, tui.CtrlN, actNextHistory)
|
||||
check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
||||
}
|
||||
|
||||
func optsFor(words ...string) *Options {
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
postProcessOptions(opts)
|
||||
return opts
|
||||
}
|
||||
|
||||
func TestToggle(t *testing.T) {
|
||||
opts := optsFor()
|
||||
if opts.ToggleSort {
|
||||
t.Error()
|
||||
}
|
||||
|
||||
opts = optsFor("--bind=a:toggle-sort")
|
||||
if !opts.ToggleSort {
|
||||
t.Error()
|
||||
}
|
||||
|
||||
opts = optsFor("--bind=a:toggle-sort", "--bind=a:up")
|
||||
if opts.ToggleSort {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreviewOpts(t *testing.T) {
|
||||
opts := optsFor()
|
||||
if !(opts.Preview.command == "" &&
|
||||
opts.Preview.hidden == false &&
|
||||
opts.Preview.wrap == false &&
|
||||
opts.Preview.position == posRight &&
|
||||
opts.Preview.size.percent == true &&
|
||||
opts.Preview.size.size == 50) {
|
||||
t.Error()
|
||||
}
|
||||
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) {
|
||||
t.Error(opts.Preview)
|
||||
}
|
||||
opts = optsFor("--preview-window=up:15:wrap:hidden:+{1}-/2", "--preview-window=down", "--preview-window=cycle")
|
||||
if !(opts.Preview.command == "" &&
|
||||
opts.Preview.hidden == true &&
|
||||
opts.Preview.wrap == true &&
|
||||
opts.Preview.cycle == true &&
|
||||
opts.Preview.position == posDown &&
|
||||
opts.Preview.scroll == "{1}-/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 == "" &&
|
||||
opts.Preview.hidden == true &&
|
||||
opts.Preview.wrap == true &&
|
||||
opts.Preview.position == posUp &&
|
||||
opts.Preview.size.percent == false &&
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdditiveExpect(t *testing.T) {
|
||||
opts := optsFor("--expect=a", "--expect", "b", "--expect=c")
|
||||
if len(opts.Expect) != 3 {
|
||||
t.Error(opts.Expect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSign(t *testing.T) {
|
||||
testCases := []struct {
|
||||
inputSign string
|
||||
isValid bool
|
||||
}{
|
||||
{"> ", true},
|
||||
{"아", true},
|
||||
{"😀", true},
|
||||
{"", false},
|
||||
{">>>", false},
|
||||
{"\n", false},
|
||||
{"\t", false},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
err := validateSign(testCase.inputSign, "")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
425
src/pattern.go
Normal file
425
src/pattern.go
Normal file
@@ -0,0 +1,425 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
// fuzzy
|
||||
// 'exact
|
||||
// ^prefix-exact
|
||||
// suffix-exact$
|
||||
// !inverse-exact
|
||||
// !'inverse-fuzzy
|
||||
// !^inverse-prefix-exact
|
||||
// !inverse-suffix-exact$
|
||||
|
||||
type termType int
|
||||
|
||||
const (
|
||||
termFuzzy termType = iota
|
||||
termExact
|
||||
termPrefix
|
||||
termSuffix
|
||||
termEqual
|
||||
)
|
||||
|
||||
type term struct {
|
||||
typ termType
|
||||
inv bool
|
||||
text []rune
|
||||
caseSensitive bool
|
||||
normalize bool
|
||||
}
|
||||
|
||||
// String returns the string representation of a term.
|
||||
func (t term) String() string {
|
||||
return fmt.Sprintf("term{typ: %d, inv: %v, text: []rune(%q), caseSensitive: %v}", t.typ, t.inv, string(t.text), t.caseSensitive)
|
||||
}
|
||||
|
||||
type termSet []term
|
||||
|
||||
// Pattern represents search pattern
|
||||
type Pattern struct {
|
||||
fuzzy bool
|
||||
fuzzyAlgo algo.Algo
|
||||
extended bool
|
||||
caseSensitive bool
|
||||
normalize bool
|
||||
forward bool
|
||||
text []rune
|
||||
termSets []termSet
|
||||
sortable bool
|
||||
cacheable bool
|
||||
cacheKey string
|
||||
delimiter Delimiter
|
||||
nth []Range
|
||||
procFun map[termType]algo.Algo
|
||||
}
|
||||
|
||||
var (
|
||||
_patternCache map[string]*Pattern
|
||||
_splitRegex *regexp.Regexp
|
||||
_cache ChunkCache
|
||||
)
|
||||
|
||||
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 {
|
||||
|
||||
var asString string
|
||||
if extended {
|
||||
asString = strings.TrimLeft(string(runes), " ")
|
||||
for strings.HasSuffix(asString, " ") && !strings.HasSuffix(asString, "\\ ") {
|
||||
asString = asString[:len(asString)-1]
|
||||
}
|
||||
} else {
|
||||
asString = string(runes)
|
||||
}
|
||||
|
||||
cached, found := _patternCache[asString]
|
||||
if found {
|
||||
return cached
|
||||
}
|
||||
|
||||
caseSensitive := true
|
||||
sortable := true
|
||||
termSets := []termSet{}
|
||||
|
||||
if extended {
|
||||
termSets = parseTerms(fuzzy, caseMode, normalize, asString)
|
||||
// We should not sort the result if there are only inverse search terms
|
||||
sortable = false
|
||||
Loop:
|
||||
for _, termSet := range termSets {
|
||||
for idx, term := range termSet {
|
||||
if !term.inv {
|
||||
sortable = true
|
||||
}
|
||||
// If the query contains inverse search terms or OR operators,
|
||||
// we cannot cache the search scope
|
||||
if !cacheable || idx > 0 || term.inv || fuzzy && term.typ != termFuzzy || !fuzzy && term.typ != termExact {
|
||||
cacheable = false
|
||||
if sortable {
|
||||
// Can't break until we see at least one non-inverse term
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lowerString := strings.ToLower(asString)
|
||||
normalize = normalize &&
|
||||
lowerString == string(algo.NormalizeRunes([]rune(lowerString)))
|
||||
caseSensitive = caseMode == CaseRespect ||
|
||||
caseMode == CaseSmart && lowerString != asString
|
||||
if !caseSensitive {
|
||||
asString = lowerString
|
||||
}
|
||||
}
|
||||
|
||||
ptr := &Pattern{
|
||||
fuzzy: fuzzy,
|
||||
fuzzyAlgo: fuzzyAlgo,
|
||||
extended: extended,
|
||||
caseSensitive: caseSensitive,
|
||||
normalize: normalize,
|
||||
forward: forward,
|
||||
text: []rune(asString),
|
||||
termSets: termSets,
|
||||
sortable: sortable,
|
||||
cacheable: cacheable,
|
||||
nth: nth,
|
||||
delimiter: delimiter,
|
||||
procFun: make(map[termType]algo.Algo)}
|
||||
|
||||
ptr.cacheKey = ptr.buildCacheKey()
|
||||
ptr.procFun[termFuzzy] = fuzzyAlgo
|
||||
ptr.procFun[termEqual] = algo.EqualMatch
|
||||
ptr.procFun[termExact] = algo.ExactMatchNaive
|
||||
ptr.procFun[termPrefix] = algo.PrefixMatch
|
||||
ptr.procFun[termSuffix] = algo.SuffixMatch
|
||||
|
||||
_patternCache[asString] = ptr
|
||||
return ptr
|
||||
}
|
||||
|
||||
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
|
||||
str = strings.Replace(str, "\\ ", "\t", -1)
|
||||
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)
|
||||
lowerText := strings.ToLower(text)
|
||||
caseSensitive := caseMode == CaseRespect ||
|
||||
caseMode == CaseSmart && text != lowerText
|
||||
normalizeTerm := normalize &&
|
||||
lowerText == string(algo.NormalizeRunes([]rune(lowerText)))
|
||||
if !caseSensitive {
|
||||
text = lowerText
|
||||
}
|
||||
if !fuzzy {
|
||||
typ = termExact
|
||||
}
|
||||
|
||||
if len(set) > 0 && !afterBar && text == "|" {
|
||||
switchSet = false
|
||||
afterBar = true
|
||||
continue
|
||||
}
|
||||
afterBar = false
|
||||
|
||||
if strings.HasPrefix(text, "!") {
|
||||
inv = true
|
||||
typ = termExact
|
||||
text = text[1:]
|
||||
}
|
||||
|
||||
if text != "$" && strings.HasSuffix(text, "$") {
|
||||
typ = termSuffix
|
||||
text = text[:len(text)-1]
|
||||
}
|
||||
|
||||
if strings.HasPrefix(text, "'") {
|
||||
// Flip exactness
|
||||
if fuzzy && !inv {
|
||||
typ = termExact
|
||||
text = text[1:]
|
||||
} else {
|
||||
typ = termFuzzy
|
||||
text = text[1:]
|
||||
}
|
||||
} else if strings.HasPrefix(text, "^") {
|
||||
if typ == termSuffix {
|
||||
typ = termEqual
|
||||
} else {
|
||||
typ = termPrefix
|
||||
}
|
||||
text = text[1:]
|
||||
}
|
||||
|
||||
if len(text) > 0 {
|
||||
if switchSet {
|
||||
sets = append(sets, set)
|
||||
set = termSet{}
|
||||
}
|
||||
textRunes := []rune(text)
|
||||
if normalizeTerm {
|
||||
textRunes = algo.NormalizeRunes(textRunes)
|
||||
}
|
||||
set = append(set, term{
|
||||
typ: typ,
|
||||
inv: inv,
|
||||
text: textRunes,
|
||||
caseSensitive: caseSensitive,
|
||||
normalize: normalizeTerm})
|
||||
switchSet = true
|
||||
}
|
||||
}
|
||||
if len(set) > 0 {
|
||||
sets = append(sets, set)
|
||||
}
|
||||
return sets
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the pattern is effectively empty
|
||||
func (p *Pattern) IsEmpty() bool {
|
||||
if !p.extended {
|
||||
return len(p.text) == 0
|
||||
}
|
||||
return len(p.termSets) == 0
|
||||
}
|
||||
|
||||
// AsString returns the search query in string type
|
||||
func (p *Pattern) AsString() string {
|
||||
return string(p.text)
|
||||
}
|
||||
|
||||
func (p *Pattern) buildCacheKey() string {
|
||||
if !p.extended {
|
||||
return p.AsString()
|
||||
}
|
||||
cacheableTerms := []string{}
|
||||
for _, termSet := range p.termSets {
|
||||
if len(termSet) == 1 && !termSet[0].inv && (p.fuzzy || termSet[0].typ == termExact) {
|
||||
cacheableTerms = append(cacheableTerms, string(termSet[0].text))
|
||||
}
|
||||
}
|
||||
return strings.Join(cacheableTerms, "\t")
|
||||
}
|
||||
|
||||
// CacheKey is used to build string to be used as the key of result cache
|
||||
func (p *Pattern) CacheKey() string {
|
||||
return p.cacheKey
|
||||
}
|
||||
|
||||
// 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()
|
||||
if p.cacheable {
|
||||
if cached := _cache.Lookup(chunk, cacheKey); cached != nil {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix/suffix cache
|
||||
space := _cache.Search(chunk, cacheKey)
|
||||
|
||||
matches := p.matchChunk(chunk, space, slab)
|
||||
|
||||
if p.cacheable {
|
||||
_cache.Add(chunk, cacheKey, matches)
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||
matches := []Result{}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
// MatchItem returns true if the Item is a 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 nil, 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 nil, 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}}
|
||||
} else {
|
||||
input = p.transformInput(item)
|
||||
}
|
||||
if p.fuzzy {
|
||||
return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
|
||||
}
|
||||
return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
|
||||
}
|
||||
|
||||
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}}
|
||||
} else {
|
||||
input = p.transformInput(item)
|
||||
}
|
||||
offsets := []Offset{}
|
||||
var totalScore int
|
||||
var allPos *[]int
|
||||
if withPos {
|
||||
allPos = &[]int{}
|
||||
}
|
||||
for _, termSet := range p.termSets {
|
||||
var offset Offset
|
||||
var currentScore int
|
||||
matched := false
|
||||
for _, term := range termSet {
|
||||
pfun := p.procFun[term.typ]
|
||||
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
|
||||
}
|
||||
offset, currentScore = off, score
|
||||
matched = true
|
||||
if withPos {
|
||||
if pos != nil {
|
||||
*allPos = append(*allPos, *pos...)
|
||||
} else {
|
||||
for idx := off[0]; idx < off[1]; idx++ {
|
||||
*allPos = append(*allPos, int(idx))
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
} else if term.inv {
|
||||
offset, currentScore = Offset{0, 0}, 0
|
||||
matched = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
if matched {
|
||||
offsets = append(offsets, offset)
|
||||
totalScore += currentScore
|
||||
}
|
||||
}
|
||||
return offsets, totalScore, allPos
|
||||
}
|
||||
|
||||
func (p *Pattern) transformInput(item *Item) []Token {
|
||||
if item.transformed != nil {
|
||||
return *item.transformed
|
||||
}
|
||||
|
||||
tokens := Tokenize(item.text.ToString(), p.delimiter)
|
||||
ret := Transform(tokens, p.nth)
|
||||
item.transformed = &ret
|
||||
return ret
|
||||
}
|
||||
|
||||
func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
||||
for _, part := range tokens {
|
||||
if res, pos := pfun(caseSensitive, normalize, forward, part.text, pattern, withPos, slab); res.Start >= 0 {
|
||||
sidx := int32(res.Start) + part.prefixLength
|
||||
eidx := int32(res.End) + part.prefixLength
|
||||
if pos != nil {
|
||||
for idx := range *pos {
|
||||
(*pos)[idx] += int(part.prefixLength)
|
||||
}
|
||||
}
|
||||
return Offset{sidx, eidx}, res.Score, pos
|
||||
}
|
||||
}
|
||||
return Offset{-1, -1}, 0, nil
|
||||
}
|
||||
209
src/pattern_test.go
Normal file
209
src/pattern_test.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
var slab *util.Slab
|
||||
|
||||
func init() {
|
||||
slab = util.MakeSlab(slab16Size, slab32Size)
|
||||
}
|
||||
|
||||
func TestParseTermsExtended(t *testing.T) {
|
||||
terms := parseTerms(true, CaseSmart, false,
|
||||
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | zzz$ | !ZZZ |")
|
||||
if len(terms) != 9 ||
|
||||
terms[0][0].typ != termFuzzy || terms[0][0].inv ||
|
||||
terms[1][0].typ != termExact || terms[1][0].inv ||
|
||||
terms[2][0].typ != termPrefix || terms[2][0].inv ||
|
||||
terms[3][0].typ != termSuffix || terms[3][0].inv ||
|
||||
terms[4][0].typ != termExact || !terms[4][0].inv ||
|
||||
terms[5][0].typ != termFuzzy || !terms[5][0].inv ||
|
||||
terms[6][0].typ != termPrefix || !terms[6][0].inv ||
|
||||
terms[7][0].typ != termSuffix || !terms[7][0].inv ||
|
||||
terms[7][1].typ != termEqual || terms[7][1].inv ||
|
||||
terms[8][0].typ != termPrefix || terms[8][0].inv ||
|
||||
terms[8][1].typ != termExact || terms[8][1].inv ||
|
||||
terms[8][2].typ != termSuffix || terms[8][2].inv ||
|
||||
terms[8][3].typ != termExact || !terms[8][3].inv {
|
||||
t.Errorf("%v", terms)
|
||||
}
|
||||
for _, termSet := range terms[:8] {
|
||||
term := termSet[0]
|
||||
if len(term.text) != 3 {
|
||||
t.Errorf("%v", term)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTermsExtendedExact(t *testing.T) {
|
||||
terms := parseTerms(false, CaseSmart, false,
|
||||
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
|
||||
if len(terms) != 8 ||
|
||||
terms[0][0].typ != termExact || terms[0][0].inv || len(terms[0][0].text) != 3 ||
|
||||
terms[1][0].typ != termFuzzy || terms[1][0].inv || len(terms[1][0].text) != 3 ||
|
||||
terms[2][0].typ != termPrefix || terms[2][0].inv || len(terms[2][0].text) != 3 ||
|
||||
terms[3][0].typ != termSuffix || terms[3][0].inv || len(terms[3][0].text) != 3 ||
|
||||
terms[4][0].typ != termExact || !terms[4][0].inv || len(terms[4][0].text) != 3 ||
|
||||
terms[5][0].typ != termFuzzy || !terms[5][0].inv || len(terms[5][0].text) != 3 ||
|
||||
terms[6][0].typ != termPrefix || !terms[6][0].inv || len(terms[6][0].text) != 3 ||
|
||||
terms[7][0].typ != termSuffix || !terms[7][0].inv || len(terms[7][0].text) != 3 {
|
||||
t.Errorf("%v", terms)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTermsEmpty(t *testing.T) {
|
||||
terms := parseTerms(true, CaseSmart, false, "' ^ !' !^")
|
||||
if len(terms) != 0 {
|
||||
t.Errorf("%v", terms)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExact(t *testing.T) {
|
||||
defer clearPatternCache()
|
||||
clearPatternCache()
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true,
|
||||
[]Range{}, Delimiter{}, []rune("'abc"))
|
||||
chars := util.ToChars([]byte("aabbcc abc"))
|
||||
res, pos := algo.ExactMatchNaive(
|
||||
pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil)
|
||||
if res.Start != 7 || res.End != 10 {
|
||||
t.Errorf("%v / %d / %d", pattern.termSets, res.Start, res.End)
|
||||
}
|
||||
if pos != nil {
|
||||
t.Errorf("pos is expected to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqual(t *testing.T) {
|
||||
defer clearPatternCache()
|
||||
clearPatternCache()
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("^AbC$"))
|
||||
|
||||
match := func(str string, sidxExpected int, eidxExpected int) {
|
||||
chars := util.ToChars([]byte(str))
|
||||
res, pos := algo.EqualMatch(
|
||||
pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil)
|
||||
if res.Start != sidxExpected || res.End != eidxExpected {
|
||||
t.Errorf("%v / %d / %d", pattern.termSets, res.Start, res.End)
|
||||
}
|
||||
if pos != nil {
|
||||
t.Errorf("pos is expected to be nil")
|
||||
}
|
||||
}
|
||||
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"))
|
||||
|
||||
if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
|
||||
string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
|
||||
string(pat3.text) != "abc" || pat3.caseSensitive != false ||
|
||||
string(pat4.text) != "abc" || pat4.caseSensitive != false ||
|
||||
string(pat5.text) != "abc" || pat5.caseSensitive != true ||
|
||||
string(pat6.text) != "Abc" || pat6.caseSensitive != true {
|
||||
t.Error("Invalid case conversion")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrigTextAndTransformed(t *testing.T) {
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
|
||||
tokens := Tokenize("junegunn", Delimiter{})
|
||||
trans := Transform(tokens, []Range{Range{1, 1}})
|
||||
|
||||
origBytes := []byte("junegunn.choi")
|
||||
for _, extended := range []bool{false, true} {
|
||||
chunk := Chunk{count: 1}
|
||||
chunk.items[0] = Item{
|
||||
text: util.ToChars([]byte("junegunn")),
|
||||
origText: &origBytes,
|
||||
transformed: &trans}
|
||||
pattern.extended = extended
|
||||
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)) {
|
||||
t.Error("Invalid match result", matches)
|
||||
}
|
||||
|
||||
match, offsets, pos := pattern.MatchItem(&chunk.items[0], true, slab)
|
||||
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)) {
|
||||
t.Error("Invalid match result", match, offsets, extended)
|
||||
}
|
||||
if !((*pos)[0] == 4 && (*pos)[1] == 0) {
|
||||
t.Error("Invalid pos array", *pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
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)
|
||||
test(true, "foo bar baz", "foo\tbar\tbaz", true)
|
||||
test(true, "foo !bar", "foo", false)
|
||||
test(true, "foo !bar baz", "foo\tbaz", false)
|
||||
test(true, "foo | bar baz", "baz", false)
|
||||
test(true, "foo | bar | baz", "", false)
|
||||
test(true, "foo | bar !baz", "", false)
|
||||
test(true, "| | foo", "", false)
|
||||
test(true, "| | | foo", "foo", false)
|
||||
}
|
||||
|
||||
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))
|
||||
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)
|
||||
test(true, "foo !bar", "foo", false)
|
||||
|
||||
test(false, "foo bar", "foo\tbar", true)
|
||||
test(false, "foo 'bar", "foo", false)
|
||||
test(false, "foo '", "foo", true)
|
||||
test(false, "foo 'bar", "foo", false)
|
||||
test(false, "foo !bar", "foo", false)
|
||||
}
|
||||
8
src/protector/protector.go
Normal file
8
src/protector/protector.go
Normal file
@@ -0,0 +1,8 @@
|
||||
// +build !openbsd
|
||||
|
||||
package protector
|
||||
|
||||
// Protect calls OS specific protections like pledge on OpenBSD
|
||||
func Protect() {
|
||||
return
|
||||
}
|
||||
10
src/protector/protector_openbsd.go
Normal file
10
src/protector/protector_openbsd.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// +build openbsd
|
||||
|
||||
package protector
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
// Protect calls OS specific protections like pledge on OpenBSD
|
||||
func Protect() {
|
||||
unix.PledgePromises("stdio rpath tty proc exec")
|
||||
}
|
||||
201
src/reader.go
Normal file
201
src/reader.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
"github.com/saracen/walker"
|
||||
)
|
||||
|
||||
// Reader reads from command or standard input
|
||||
type Reader struct {
|
||||
pusher func([]byte) bool
|
||||
eventBox *util.EventBox
|
||||
delimNil bool
|
||||
event int32
|
||||
finChan chan bool
|
||||
mutex sync.Mutex
|
||||
exec *exec.Cmd
|
||||
command *string
|
||||
killed bool
|
||||
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 (r *Reader) startEventPoller() {
|
||||
go func() {
|
||||
ptr := &r.event
|
||||
pollInterval := readerPollIntervalMin
|
||||
for {
|
||||
if atomic.CompareAndSwapInt32(ptr, int32(EvtReadNew), int32(EvtReady)) {
|
||||
r.eventBox.Set(EvtReadNew, (*string)(nil))
|
||||
pollInterval = readerPollIntervalMin
|
||||
} else if atomic.LoadInt32(ptr) == int32(EvtReadFin) {
|
||||
if r.wait {
|
||||
r.finChan <- true
|
||||
}
|
||||
return
|
||||
} else {
|
||||
pollInterval += readerPollIntervalStep
|
||||
if pollInterval > readerPollIntervalMax {
|
||||
pollInterval = readerPollIntervalMax
|
||||
}
|
||||
}
|
||||
time.Sleep(pollInterval)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (r *Reader) fin(success bool) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadFin))
|
||||
if r.wait {
|
||||
<-r.finChan
|
||||
}
|
||||
|
||||
r.mutex.Lock()
|
||||
ret := r.command
|
||||
if success || r.killed {
|
||||
ret = nil
|
||||
}
|
||||
r.mutex.Unlock()
|
||||
|
||||
r.eventBox.Set(EvtReadFin, ret)
|
||||
}
|
||||
|
||||
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 if defaultCommand != "" {
|
||||
os.Stdin.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) restart(command string) {
|
||||
r.event = int32(EvtReady)
|
||||
r.startEventPoller()
|
||||
success := r.readFromCommand(nil, command)
|
||||
r.fin(success)
|
||||
}
|
||||
|
||||
// ReadSource reads data from the default command or from standard input
|
||||
func (r *Reader) ReadSource() {
|
||||
r.startEventPoller()
|
||||
var success bool
|
||||
if util.IsTty() {
|
||||
// The default command for *nix requires bash
|
||||
shell := "bash"
|
||||
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
|
||||
if len(cmd) == 0 {
|
||||
if defaultCommand != "" {
|
||||
success = r.readFromCommand(&shell, defaultCommand)
|
||||
} else {
|
||||
success = r.readFiles()
|
||||
}
|
||||
} else {
|
||||
success = r.readFromCommand(nil, cmd)
|
||||
}
|
||||
} else {
|
||||
success = r.readFromStdin()
|
||||
}
|
||||
r.fin(success)
|
||||
}
|
||||
|
||||
func (r *Reader) feed(src io.Reader) {
|
||||
delim := byte('\n')
|
||||
if r.delimNil {
|
||||
delim = '\000'
|
||||
}
|
||||
reader := bufio.NewReaderSize(src, readerBufferSize)
|
||||
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))
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) readFromStdin() bool {
|
||||
r.feed(os.Stdin)
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Reader) readFiles() bool {
|
||||
r.killed = false
|
||||
fn := func(path string, mode os.FileInfo) error {
|
||||
path = filepath.Clean(path)
|
||||
if path != "." {
|
||||
isDir := mode.Mode().IsDir()
|
||||
if isDir && filepath.Base(path)[0] == '.' {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if !isDir && r.pusher([]byte(path)) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
}
|
||||
}
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
if r.killed {
|
||||
return context.Canceled
|
||||
}
|
||||
return nil
|
||||
}
|
||||
cb := walker.WithErrorCallback(func(pathname string, err error) error {
|
||||
return nil
|
||||
})
|
||||
return walker.Walk(".", fn, cb) == nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
out, err := r.exec.StdoutPipe()
|
||||
if err != nil {
|
||||
r.mutex.Unlock()
|
||||
return false
|
||||
}
|
||||
err = r.exec.Start()
|
||||
r.mutex.Unlock()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
r.feed(out)
|
||||
return r.exec.Wait() == nil
|
||||
}
|
||||
63
src/reader_test.go
Normal file
63
src/reader_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func TestReadFromCommand(t *testing.T) {
|
||||
strs := []string{}
|
||||
eb := util.NewEventBox()
|
||||
reader := NewReader(
|
||||
func(s []byte) bool { strs = append(strs, string(s)); return true },
|
||||
eb, false, true)
|
||||
|
||||
reader.startEventPoller()
|
||||
|
||||
// Check EventBox
|
||||
if eb.Peek(EvtReadNew) {
|
||||
t.Error("EvtReadNew should not be set yet")
|
||||
}
|
||||
|
||||
// Normal command
|
||||
reader.fin(reader.readFromCommand(nil, `echo abc && echo def`))
|
||||
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
|
||||
t.Errorf("%s", strs)
|
||||
}
|
||||
|
||||
// Check EventBox again
|
||||
eb.WaitFor(EvtReadFin)
|
||||
|
||||
// Wait should return immediately
|
||||
eb.Wait(func(events *util.Events) {
|
||||
events.Clear()
|
||||
})
|
||||
|
||||
// EventBox is cleared
|
||||
if eb.Peek(EvtReadNew) {
|
||||
t.Error("EvtReadNew should not be set yet")
|
||||
}
|
||||
|
||||
// Make sure that event poller is finished
|
||||
time.Sleep(readerPollIntervalMax)
|
||||
|
||||
// Restart event poller
|
||||
reader.startEventPoller()
|
||||
|
||||
// Failing command
|
||||
reader.fin(reader.readFromCommand(nil, `no-such-command`))
|
||||
strs = []string{}
|
||||
if len(strs) > 0 {
|
||||
t.Errorf("%s", strs)
|
||||
}
|
||||
|
||||
// Check EventBox again
|
||||
if eb.Peek(EvtReadNew) {
|
||||
t.Error("Command failed. EvtReadNew should not be set")
|
||||
}
|
||||
if !eb.Peek(EvtReadFin) {
|
||||
t.Error("EvtReadFin should be set")
|
||||
}
|
||||
}
|
||||
231
src/result.go
Normal file
231
src/result.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
"unicode"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
// Offset holds two 32-bit integers denoting the offsets of a matched substring
|
||||
type Offset [2]int32
|
||||
|
||||
type colorOffset struct {
|
||||
offset [2]int32
|
||||
color tui.ColorPair
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
item *Item
|
||||
points [4]uint16
|
||||
}
|
||||
|
||||
func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
if len(offsets) > 1 {
|
||||
sort.Sort(ByOrder(offsets))
|
||||
}
|
||||
|
||||
result := Result{item: item}
|
||||
numChars := item.text.Length()
|
||||
minBegin := math.MaxUint16
|
||||
minEnd := math.MaxUint16
|
||||
maxEnd := 0
|
||||
validOffsetFound := false
|
||||
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)
|
||||
validOffsetFound = true
|
||||
}
|
||||
}
|
||||
|
||||
for idx, criterion := range sortCriteria {
|
||||
val := uint16(math.MaxUint16)
|
||||
switch criterion {
|
||||
case byScore:
|
||||
// Higher is better
|
||||
val = math.MaxUint16 - util.AsUint16(score)
|
||||
case byLength:
|
||||
val = item.TrimLength()
|
||||
case byBegin, byEnd:
|
||||
if validOffsetFound {
|
||||
whitePrefixLen := 0
|
||||
for idx := 0; idx < numChars; idx++ {
|
||||
r := item.text.Get(idx)
|
||||
whitePrefixLen = idx
|
||||
if idx == minBegin || !unicode.IsSpace(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if criterion == byBegin {
|
||||
val = util.AsUint16(minEnd - whitePrefixLen)
|
||||
} else {
|
||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()))
|
||||
}
|
||||
}
|
||||
}
|
||||
result.points[3-idx] = val
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Sort criteria to use. Never changes once fzf is started.
|
||||
var sortCriteria []criterion
|
||||
|
||||
// Index returns ordinal index of the Item
|
||||
func (result *Result) Index() int32 {
|
||||
return result.item.Index()
|
||||
}
|
||||
|
||||
func minRank() Result {
|
||||
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
||||
}
|
||||
|
||||
func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, current bool) []colorOffset {
|
||||
itemColors := result.item.Colors()
|
||||
|
||||
// No ANSI codes
|
||||
if len(itemColors) == 0 {
|
||||
var offsets []colorOffset
|
||||
for _, off := range matchOffsets {
|
||||
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch})
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
|
||||
// Find max column
|
||||
var maxCol int32
|
||||
for _, off := range matchOffsets {
|
||||
if off[1] > maxCol {
|
||||
maxCol = off[1]
|
||||
}
|
||||
}
|
||||
for _, ansi := range itemColors {
|
||||
if ansi.offset[1] > maxCol {
|
||||
maxCol = ansi.offset[1]
|
||||
}
|
||||
}
|
||||
|
||||
cols := make([]int, maxCol)
|
||||
for colorIndex, ansi := range itemColors {
|
||||
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
|
||||
cols[i] = colorIndex + 1 // 1-based index of itemColors
|
||||
}
|
||||
}
|
||||
|
||||
for _, off := range matchOffsets {
|
||||
for i := off[0]; i < off[1]; i++ {
|
||||
// Negative of 1-based index of itemColors
|
||||
// - The extra -1 means highlighted
|
||||
cols[i] = cols[i]*-1 - 1
|
||||
}
|
||||
}
|
||||
|
||||
// sort.Sort(ByOrder(offsets))
|
||||
|
||||
// Merge offsets
|
||||
// ------------ ---- -- ----
|
||||
// ++++++++ ++++++++++
|
||||
// --++++++++-- --++++++++++---
|
||||
curr := 0
|
||||
start := 0
|
||||
ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
|
||||
fg := ansi.color.fg
|
||||
bg := ansi.color.bg
|
||||
if fg == -1 {
|
||||
if current {
|
||||
fg = theme.Current.Color
|
||||
} else {
|
||||
fg = theme.Fg.Color
|
||||
}
|
||||
}
|
||||
if bg == -1 {
|
||||
if current {
|
||||
bg = theme.DarkBg.Color
|
||||
} else {
|
||||
bg = theme.Bg.Color
|
||||
}
|
||||
}
|
||||
return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base)
|
||||
}
|
||||
var colors []colorOffset
|
||||
add := func(idx int) {
|
||||
if curr != 0 && idx > start {
|
||||
if curr < 0 {
|
||||
color := colMatch
|
||||
if curr < -1 && theme.Colored {
|
||||
origColor := ansiToColorPair(itemColors[-curr-2], colMatch)
|
||||
color = origColor.MergeNonDefault(color)
|
||||
}
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color})
|
||||
} else {
|
||||
ansi := itemColors[curr-1]
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)},
|
||||
color: ansiToColorPair(ansi, colBase)})
|
||||
}
|
||||
}
|
||||
}
|
||||
for idx, col := range cols {
|
||||
if col != curr {
|
||||
add(idx)
|
||||
start = idx
|
||||
curr = col
|
||||
}
|
||||
}
|
||||
add(int(maxCol))
|
||||
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])
|
||||
}
|
||||
|
||||
// ByRelevance is for sorting Items
|
||||
type ByRelevance []Result
|
||||
|
||||
func (a ByRelevance) Len() int {
|
||||
return len(a)
|
||||
}
|
||||
|
||||
func (a ByRelevance) Swap(i, j int) {
|
||||
a[i], a[j] = a[j], a[i]
|
||||
}
|
||||
|
||||
func (a ByRelevance) Less(i, j int) bool {
|
||||
return compareRanks(a[i], a[j], false)
|
||||
}
|
||||
|
||||
// ByRelevanceTac is for sorting Items
|
||||
type ByRelevanceTac []Result
|
||||
|
||||
func (a ByRelevanceTac) Len() int {
|
||||
return len(a)
|
||||
}
|
||||
|
||||
func (a ByRelevanceTac) Swap(i, j int) {
|
||||
a[i], a[j] = a[j], a[i]
|
||||
}
|
||||
|
||||
func (a ByRelevanceTac) Less(i, j int) bool {
|
||||
return compareRanks(a[i], a[j], true)
|
||||
}
|
||||
16
src/result_others.go
Normal file
16
src/result_others.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// +build !386,!amd64
|
||||
|
||||
package fzf
|
||||
|
||||
func compareRanks(irank Result, jrank Result, tac bool) bool {
|
||||
for idx := 3; idx >= 0; idx-- {
|
||||
left := irank.points[idx]
|
||||
right := jrank.points[idx]
|
||||
if left < right {
|
||||
return true
|
||||
} else if left > right {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return (irank.item.Index() <= jrank.item.Index()) != tac
|
||||
}
|
||||
159
src/result_test.go
Normal file
159
src/result_test.go
Normal file
@@ -0,0 +1,159 @@
|
||||
// +build !tcell
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func withIndex(i *Item, index int) *Item {
|
||||
(*i).text.Index = int32(index)
|
||||
return i
|
||||
}
|
||||
|
||||
func TestOffsetSort(t *testing.T) {
|
||||
offsets := []Offset{
|
||||
Offset{3, 5}, Offset{2, 7},
|
||||
Offset{1, 3}, Offset{2, 9}}
|
||||
sort.Sort(ByOrder(offsets))
|
||||
|
||||
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
|
||||
offsets[1][0] != 2 || offsets[1][1] != 7 ||
|
||||
offsets[2][0] != 2 || offsets[2][1] != 9 ||
|
||||
offsets[3][0] != 3 || offsets[3][1] != 5 {
|
||||
t.Error("Invalid order:", offsets)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRankComparison(t *testing.T) {
|
||||
rank := func(vals ...uint16) Result {
|
||||
return Result{
|
||||
points: [4]uint16{vals[0], vals[1], vals[2], vals[3]},
|
||||
item: &Item{text: util.Chars{Index: int32(vals[4])}}}
|
||||
}
|
||||
if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) ||
|
||||
!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
|
||||
!compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), false) ||
|
||||
!compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) {
|
||||
t.Error("Invalid order")
|
||||
}
|
||||
|
||||
if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), true) ||
|
||||
!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
|
||||
!compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), true) ||
|
||||
!compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) {
|
||||
t.Error("Invalid order (tac)")
|
||||
}
|
||||
}
|
||||
|
||||
// Match length, string length, index
|
||||
func TestResultRank(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byScore, byLength}
|
||||
|
||||
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
|
||||
item1 := buildResult(
|
||||
withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2)
|
||||
if item1.points[3] != math.MaxUint16-2 || // Bonus
|
||||
item1.points[2] != 3 || // Length
|
||||
item1.points[1] != 0 || // Unused
|
||||
item1.points[0] != 0 || // Unused
|
||||
item1.item.Index() != 1 {
|
||||
t.Error(item1)
|
||||
}
|
||||
// Only differ in index
|
||||
item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2)
|
||||
|
||||
items := []Result{item1, item2}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if items[0] != item2 || items[1] != item1 {
|
||||
t.Error(items)
|
||||
}
|
||||
|
||||
items = []Result{item2, item1, item1, item2}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if items[0] != item2 || items[1] != item2 ||
|
||||
items[2] != item1 || items[3] != item1 {
|
||||
t.Error(items, item1, item1.item.Index(), item2, item2.item.Index())
|
||||
}
|
||||
|
||||
// Sort by relevance
|
||||
item3 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 3)
|
||||
item4 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 4)
|
||||
item5 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 5)
|
||||
item6 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 6)
|
||||
items = []Result{item1, item2, item3, item4, item5, item6}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if !(items[0] == item6 && items[1] == item5 &&
|
||||
items[2] == item4 && items[3] == item3 &&
|
||||
items[4] == item2 && items[5] == item1) {
|
||||
t.Error(items, item1, item2, item3, item4, item5, item6)
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorOffset(t *testing.T) {
|
||||
// ------------ 20 ---- -- ----
|
||||
// ++++++++ ++++++++++
|
||||
// --++++++++-- --++++++++++---
|
||||
|
||||
offsets := []Offset{{5, 15}, {25, 35}}
|
||||
item := Result{
|
||||
item: &Item{
|
||||
colors: &[]ansiOffset{
|
||||
{[2]int32{0, 20}, ansiState{1, 5, 0}},
|
||||
{[2]int32{22, 27}, ansiState{2, 6, tui.Bold}},
|
||||
{[2]int32{30, 32}, ansiState{3, 7, 0}},
|
||||
{[2]int32{33, 40}, ansiState{4, 8, tui.Bold}}}}}
|
||||
|
||||
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
||||
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
||||
colors := item.colorOffsets(offsets, tui.Dark256, colBase, colMatch, true)
|
||||
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 {
|
||||
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)
|
||||
colors = item.colorOffsets(offsets, tui.Dark256, colRegular, colUnderline, true)
|
||||
|
||||
// [{[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 40] {4 8 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, 40, tui.NewColorPair(4, 8, tui.Bold))
|
||||
}
|
||||
16
src/result_x86.go
Normal file
16
src/result_x86.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// +build 386 amd64
|
||||
|
||||
package fzf
|
||||
|
||||
import "unsafe"
|
||||
|
||||
func compareRanks(irank Result, jrank Result, tac bool) bool {
|
||||
left := *(*uint64)(unsafe.Pointer(&irank.points[0]))
|
||||
right := *(*uint64)(unsafe.Pointer(&jrank.points[0]))
|
||||
if left < right {
|
||||
return true
|
||||
} else if left > right {
|
||||
return false
|
||||
}
|
||||
return (irank.item.Index() <= jrank.item.Index()) != tac
|
||||
}
|
||||
2586
src/terminal.go
Normal file
2586
src/terminal.go
Normal file
File diff suppressed because it is too large
Load Diff
139
src/terminal_test.go
Normal file
139
src/terminal_test.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func newItem(str string) *Item {
|
||||
bytes := []byte(str)
|
||||
trimmed, _, _ := extractColor(str, nil, nil)
|
||||
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
|
||||
}
|
||||
|
||||
func TestReplacePlaceholder(t *testing.T) {
|
||||
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
||||
items1 := []*Item{item1, item1}
|
||||
items2 := []*Item{
|
||||
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
|
||||
|
||||
delim := "'"
|
||||
var regex *regexp.Regexp
|
||||
|
||||
var result string
|
||||
check := func(expected string) {
|
||||
if result != expected {
|
||||
t.Errorf("expected: %s, actual: %s", expected, result)
|
||||
}
|
||||
}
|
||||
printsep := "\n"
|
||||
// {}, preserve ansi
|
||||
result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
|
||||
|
||||
// {}, strip ansi
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz'")
|
||||
|
||||
// {}, with multiple items
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
check("echo 'foo'\\''bar baz'")
|
||||
|
||||
// {..}, strip leading whitespaces, preserve ansi
|
||||
result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
|
||||
|
||||
// {..}, strip leading whitespaces, strip ansi
|
||||
result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
check("echo 'foo'\\''bar baz'")
|
||||
|
||||
// {q}
|
||||
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz' 'query'")
|
||||
|
||||
// {q}, multiple items
|
||||
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
|
||||
|
||||
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'")
|
||||
|
||||
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
|
||||
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
|
||||
|
||||
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
|
||||
|
||||
// forcePlus
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
|
||||
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
|
||||
|
||||
// Whitespace preserving flag with "'" delimiter
|
||||
result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
check("echo ' foo'")
|
||||
|
||||
result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
check("echo 'bar baz'")
|
||||
|
||||
result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz'")
|
||||
|
||||
result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz'")
|
||||
|
||||
// Whitespace preserving flag with regex delimiter
|
||||
regex = regexp.MustCompile(`\w+`)
|
||||
|
||||
result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
check("echo ' '")
|
||||
|
||||
result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
check("echo ''\\'''")
|
||||
|
||||
result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
check("echo ' '")
|
||||
|
||||
// No match
|
||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
|
||||
check("echo /")
|
||||
|
||||
// No match, but with selections
|
||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
|
||||
check("echo /' foo'\\''bar baz'")
|
||||
|
||||
// String delimiter
|
||||
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz'/'foo'/'bar baz'")
|
||||
|
||||
// Regex delimiter
|
||||
regex = regexp.MustCompile("[oa]+")
|
||||
// foo'bar baz
|
||||
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
|
||||
}
|
||||
|
||||
func TestQuoteEntryCmd(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
`"`: `^"\^"^"`,
|
||||
`\`: `^"\\^"`,
|
||||
`\"`: `^"\\\^"^"`,
|
||||
`"\\\"`: `^"\^"\\\\\\\^"^"`,
|
||||
`&|<>()@^%!`: `^"^&^|^<^>^(^)^@^^^%^!^"`,
|
||||
`%USERPROFILE%`: `^"^%USERPROFILE^%^"`,
|
||||
`C:\Program Files (x86)\`: `^"C:\\Program Files ^(x86^)\\^"`,
|
||||
}
|
||||
|
||||
for input, expected := range tests {
|
||||
escaped := quoteEntryCmd(input)
|
||||
if escaped != expected {
|
||||
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/terminal_unix.go
Normal file
21
src/terminal_unix.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// +build !windows
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||
signal.Notify(resizeChan, syscall.SIGWINCH)
|
||||
}
|
||||
|
||||
func notifyStop(p *os.Process) {
|
||||
p.Signal(syscall.SIGSTOP)
|
||||
}
|
||||
|
||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||
signal.Notify(resizeChan, syscall.SIGCONT)
|
||||
}
|
||||
19
src/terminal_windows.go
Normal file
19
src/terminal_windows.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// +build windows
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func notifyStop(p *os.Process) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||
// NOOP
|
||||
}
|
||||
253
src/tokenizer.go
Normal file
253
src/tokenizer.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
const rangeEllipsis = 0
|
||||
|
||||
// Range represents nth-expression
|
||||
type Range struct {
|
||||
begin int
|
||||
end int
|
||||
}
|
||||
|
||||
// Token contains the tokenized part of the strings and its prefix length
|
||||
type Token struct {
|
||||
text *util.Chars
|
||||
prefixLength int32
|
||||
}
|
||||
|
||||
// String returns the string representation of a Token.
|
||||
func (t Token) String() string {
|
||||
return fmt.Sprintf("Token{text: %s, prefixLength: %d}", t.text, t.prefixLength)
|
||||
}
|
||||
|
||||
// Delimiter for tokenizing the input
|
||||
type Delimiter struct {
|
||||
regex *regexp.Regexp
|
||||
str *string
|
||||
}
|
||||
|
||||
// String returns the string representation of a Delimeter.
|
||||
func (d Delimiter) String() string {
|
||||
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
|
||||
}
|
||||
|
||||
func newRange(begin int, end int) Range {
|
||||
if begin == 1 {
|
||||
begin = rangeEllipsis
|
||||
}
|
||||
if end == -1 {
|
||||
end = rangeEllipsis
|
||||
}
|
||||
return Range{begin, end}
|
||||
}
|
||||
|
||||
// ParseRange parses nth-expression and returns the corresponding Range object
|
||||
func ParseRange(str *string) (Range, bool) {
|
||||
if (*str) == ".." {
|
||||
return newRange(rangeEllipsis, rangeEllipsis), true
|
||||
} else if strings.HasPrefix(*str, "..") {
|
||||
end, err := strconv.Atoi((*str)[2:])
|
||||
if err != nil || end == 0 {
|
||||
return Range{}, false
|
||||
}
|
||||
return newRange(rangeEllipsis, end), true
|
||||
} else if strings.HasSuffix(*str, "..") {
|
||||
begin, err := strconv.Atoi((*str)[:len(*str)-2])
|
||||
if err != nil || begin == 0 {
|
||||
return Range{}, false
|
||||
}
|
||||
return newRange(begin, rangeEllipsis), true
|
||||
} else if strings.Contains(*str, "..") {
|
||||
ns := strings.Split(*str, "..")
|
||||
if len(ns) != 2 {
|
||||
return Range{}, false
|
||||
}
|
||||
begin, err1 := strconv.Atoi(ns[0])
|
||||
end, err2 := strconv.Atoi(ns[1])
|
||||
if err1 != nil || err2 != nil || begin == 0 || end == 0 {
|
||||
return Range{}, false
|
||||
}
|
||||
return newRange(begin, end), true
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(*str)
|
||||
if err != nil || n == 0 {
|
||||
return Range{}, false
|
||||
}
|
||||
return newRange(n, n), true
|
||||
}
|
||||
|
||||
func withPrefixLengths(tokens []string, begin int) []Token {
|
||||
ret := make([]Token, len(tokens))
|
||||
|
||||
prefixLength := begin
|
||||
for idx := range tokens {
|
||||
chars := util.ToChars([]byte(tokens[idx]))
|
||||
ret[idx] = Token{&chars, int32(prefixLength)}
|
||||
prefixLength += chars.Length()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
const (
|
||||
awkNil = iota
|
||||
awkBlack
|
||||
awkWhite
|
||||
)
|
||||
|
||||
func awkTokenizer(input string) ([]string, int) {
|
||||
// 9, 32
|
||||
ret := []string{}
|
||||
prefixLength := 0
|
||||
state := awkNil
|
||||
begin := 0
|
||||
end := 0
|
||||
for idx := 0; idx < len(input); idx++ {
|
||||
r := input[idx]
|
||||
white := r == 9 || r == 32
|
||||
switch state {
|
||||
case awkNil:
|
||||
if white {
|
||||
prefixLength++
|
||||
} else {
|
||||
state, begin, end = awkBlack, idx, idx+1
|
||||
}
|
||||
case awkBlack:
|
||||
end = idx + 1
|
||||
if white {
|
||||
state = awkWhite
|
||||
}
|
||||
case awkWhite:
|
||||
if white {
|
||||
end = idx + 1
|
||||
} else {
|
||||
ret = append(ret, input[begin:end])
|
||||
state, begin, end = awkBlack, idx, idx+1
|
||||
}
|
||||
}
|
||||
}
|
||||
if begin < end {
|
||||
ret = append(ret, input[begin:end])
|
||||
}
|
||||
return ret, prefixLength
|
||||
}
|
||||
|
||||
// Tokenize tokenizes the given string with the delimiter
|
||||
func Tokenize(text string, delimiter Delimiter) []Token {
|
||||
if delimiter.str == nil && delimiter.regex == nil {
|
||||
// AWK-style (\S+\s*)
|
||||
tokens, prefixLength := awkTokenizer(text)
|
||||
return withPrefixLengths(tokens, prefixLength)
|
||||
}
|
||||
|
||||
if delimiter.str != nil {
|
||||
return withPrefixLengths(strings.SplitAfter(text, *delimiter.str), 0)
|
||||
}
|
||||
|
||||
// FIXME performance
|
||||
var tokens []string
|
||||
if delimiter.regex != nil {
|
||||
for len(text) > 0 {
|
||||
loc := delimiter.regex.FindStringIndex(text)
|
||||
if len(loc) < 2 {
|
||||
loc = []int{0, len(text)}
|
||||
}
|
||||
last := util.Max(loc[1], 1)
|
||||
tokens = append(tokens, text[:last])
|
||||
text = text[last:]
|
||||
}
|
||||
}
|
||||
return withPrefixLengths(tokens, 0)
|
||||
}
|
||||
|
||||
func joinTokens(tokens []Token) string {
|
||||
var output bytes.Buffer
|
||||
for _, token := range tokens {
|
||||
output.WriteString(token.text.ToString())
|
||||
}
|
||||
return output.String()
|
||||
}
|
||||
|
||||
// Transform is used to transform the input when --with-nth option is given
|
||||
func Transform(tokens []Token, withNth []Range) []Token {
|
||||
transTokens := make([]Token, len(withNth))
|
||||
numTokens := len(tokens)
|
||||
for idx, r := range withNth {
|
||||
parts := []*util.Chars{}
|
||||
minIdx := 0
|
||||
if r.begin == r.end {
|
||||
idx := r.begin
|
||||
if idx == rangeEllipsis {
|
||||
chars := util.ToChars([]byte(joinTokens(tokens)))
|
||||
parts = append(parts, &chars)
|
||||
} else {
|
||||
if idx < 0 {
|
||||
idx += numTokens + 1
|
||||
}
|
||||
if idx >= 1 && idx <= numTokens {
|
||||
minIdx = idx - 1
|
||||
parts = append(parts, tokens[idx-1].text)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var begin, end int
|
||||
if r.begin == rangeEllipsis { // ..N
|
||||
begin, end = 1, r.end
|
||||
if end < 0 {
|
||||
end += numTokens + 1
|
||||
}
|
||||
} else if r.end == rangeEllipsis { // N..
|
||||
begin, end = r.begin, numTokens
|
||||
if begin < 0 {
|
||||
begin += numTokens + 1
|
||||
}
|
||||
} else {
|
||||
begin, end = r.begin, r.end
|
||||
if begin < 0 {
|
||||
begin += numTokens + 1
|
||||
}
|
||||
if end < 0 {
|
||||
end += numTokens + 1
|
||||
}
|
||||
}
|
||||
minIdx = util.Max(0, begin-1)
|
||||
for idx := begin; idx <= end; idx++ {
|
||||
if idx >= 1 && idx <= numTokens {
|
||||
parts = append(parts, tokens[idx-1].text)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Merge multiple parts
|
||||
var merged util.Chars
|
||||
switch len(parts) {
|
||||
case 0:
|
||||
merged = util.ToChars([]byte{})
|
||||
case 1:
|
||||
merged = *parts[0]
|
||||
default:
|
||||
var output bytes.Buffer
|
||||
for _, part := range parts {
|
||||
output.WriteString(part.ToString())
|
||||
}
|
||||
merged = util.ToChars(output.Bytes())
|
||||
}
|
||||
|
||||
var prefixLength int32
|
||||
if minIdx < numTokens {
|
||||
prefixLength = tokens[minIdx].prefixLength
|
||||
} else {
|
||||
prefixLength = 0
|
||||
}
|
||||
transTokens[idx] = Token{&merged, prefixLength}
|
||||
}
|
||||
return transTokens
|
||||
}
|
||||
112
src/tokenizer_test.go
Normal file
112
src/tokenizer_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseRange(t *testing.T) {
|
||||
{
|
||||
i := ".."
|
||||
r, _ := ParseRange(&i)
|
||||
if r.begin != rangeEllipsis || r.end != rangeEllipsis {
|
||||
t.Errorf("%v", r)
|
||||
}
|
||||
}
|
||||
{
|
||||
i := "3.."
|
||||
r, _ := ParseRange(&i)
|
||||
if r.begin != 3 || r.end != rangeEllipsis {
|
||||
t.Errorf("%v", r)
|
||||
}
|
||||
}
|
||||
{
|
||||
i := "3..5"
|
||||
r, _ := ParseRange(&i)
|
||||
if r.begin != 3 || r.end != 5 {
|
||||
t.Errorf("%v", r)
|
||||
}
|
||||
}
|
||||
{
|
||||
i := "-3..-5"
|
||||
r, _ := ParseRange(&i)
|
||||
if r.begin != -3 || r.end != -5 {
|
||||
t.Errorf("%v", r)
|
||||
}
|
||||
}
|
||||
{
|
||||
i := "3"
|
||||
r, _ := ParseRange(&i)
|
||||
if r.begin != 3 || r.end != 3 {
|
||||
t.Errorf("%v", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenize(t *testing.T) {
|
||||
// AWK-style
|
||||
input := " abc: def: ghi "
|
||||
tokens := Tokenize(input, Delimiter{})
|
||||
if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 {
|
||||
t.Errorf("%s", tokens)
|
||||
}
|
||||
|
||||
// With delimiter
|
||||
tokens = Tokenize(input, delimiterRegexp(":"))
|
||||
if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 {
|
||||
t.Error(tokens[0].text.ToString(), tokens[0].prefixLength)
|
||||
}
|
||||
|
||||
// With delimiter regex
|
||||
tokens = Tokenize(input, delimiterRegexp("\\s+"))
|
||||
if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 ||
|
||||
tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 ||
|
||||
tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 ||
|
||||
tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 14 {
|
||||
t.Errorf("%s", tokens)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransform(t *testing.T) {
|
||||
input := " abc: def: ghi: jkl"
|
||||
{
|
||||
tokens := Tokenize(input, Delimiter{})
|
||||
{
|
||||
ranges := splitNth("1,2,3")
|
||||
tx := Transform(tokens, ranges)
|
||||
if joinTokens(tx) != "abc: def: ghi: " {
|
||||
t.Errorf("%s", tx)
|
||||
}
|
||||
}
|
||||
{
|
||||
ranges := splitNth("1..2,3,2..,1")
|
||||
tx := Transform(tokens, ranges)
|
||||
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
|
||||
len(tx) != 4 ||
|
||||
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
|
||||
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
|
||||
tx[2].text.ToString() != "def: ghi: jkl" || tx[2].prefixLength != 8 ||
|
||||
tx[3].text.ToString() != "abc: " || tx[3].prefixLength != 2 {
|
||||
t.Errorf("%s", tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
tokens := Tokenize(input, delimiterRegexp(":"))
|
||||
{
|
||||
ranges := splitNth("1..2,3,2..,1")
|
||||
tx := Transform(tokens, ranges)
|
||||
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
||||
len(tx) != 4 ||
|
||||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
|
||||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
|
||||
tx[2].text.ToString() != " def: ghi: jkl" || tx[2].prefixLength != 6 ||
|
||||
tx[3].text.ToString() != " abc:" || tx[3].prefixLength != 0 {
|
||||
t.Errorf("%s", tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransformIndexOutOfBounds(t *testing.T) {
|
||||
Transform([]Token{}, splitNth("1"))
|
||||
}
|
||||
46
src/tui/dummy.go
Normal file
46
src/tui/dummy.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// +build !ncurses
|
||||
// +build !tcell
|
||||
// +build !windows
|
||||
|
||||
package tui
|
||||
|
||||
type Attr int
|
||||
|
||||
func HasFullscreenRenderer() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a Attr) Merge(b Attr) Attr {
|
||||
return a | b
|
||||
}
|
||||
|
||||
const (
|
||||
AttrUndefined = Attr(0)
|
||||
AttrRegular = Attr(1 << 7)
|
||||
AttrClear = Attr(1 << 8)
|
||||
|
||||
Bold = Attr(1)
|
||||
Dim = Attr(1 << 1)
|
||||
Italic = Attr(1 << 2)
|
||||
Underline = Attr(1 << 3)
|
||||
Blink = Attr(1 << 4)
|
||||
Blink2 = Attr(1 << 5)
|
||||
Reverse = Attr(1 << 6)
|
||||
)
|
||||
|
||||
func (r *FullscreenRenderer) Init() {}
|
||||
func (r *FullscreenRenderer) Pause(bool) {}
|
||||
func (r *FullscreenRenderer) Resume(bool, bool) {}
|
||||
func (r *FullscreenRenderer) Clear() {}
|
||||
func (r *FullscreenRenderer) Refresh() {}
|
||||
func (r *FullscreenRenderer) Close() {}
|
||||
|
||||
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
|
||||
func (r *FullscreenRenderer) MaxX() int { return 0 }
|
||||
func (r *FullscreenRenderer) MaxY() int { return 0 }
|
||||
|
||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
|
||||
|
||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
|
||||
return nil
|
||||
}
|
||||
945
src/tui/light.go
Normal file
945
src/tui/light.go
Normal file
@@ -0,0 +1,945 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultWidth = 80
|
||||
defaultHeight = 24
|
||||
|
||||
defaultEscDelay = 100
|
||||
escPollInterval = 5
|
||||
offsetPollTries = 10
|
||||
maxInputBuffer = 10 * 1024
|
||||
)
|
||||
|
||||
const consoleDevice string = "/dev/tty"
|
||||
|
||||
var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
|
||||
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
|
||||
|
||||
func (r *LightRenderer) stderr(str string) {
|
||||
r.stderrInternal(str, true)
|
||||
}
|
||||
|
||||
// FIXME: Need better handling of non-displayable characters
|
||||
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
|
||||
bytes := []byte(str)
|
||||
runes := []rune{}
|
||||
for len(bytes) > 0 {
|
||||
r, sz := utf8.DecodeRune(bytes)
|
||||
nlcr := r == '\n' || r == '\r'
|
||||
if r >= 32 || r == '\x1b' || nlcr {
|
||||
if r == utf8.RuneError || nlcr && !allowNLCR {
|
||||
runes = append(runes, ' ')
|
||||
} else {
|
||||
runes = append(runes, r)
|
||||
}
|
||||
}
|
||||
bytes = bytes[sz:]
|
||||
}
|
||||
r.queued += string(runes)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) csi(code string) {
|
||||
r.stderr("\x1b[" + code)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) flush() {
|
||||
if len(r.queued) > 0 {
|
||||
fmt.Fprint(os.Stderr, r.queued)
|
||||
r.queued = ""
|
||||
}
|
||||
}
|
||||
|
||||
// Light renderer
|
||||
type LightRenderer struct {
|
||||
theme *ColorTheme
|
||||
mouse bool
|
||||
forceBlack bool
|
||||
clearOnExit bool
|
||||
prevDownTime time.Time
|
||||
clickY []int
|
||||
ttyin *os.File
|
||||
buffer []byte
|
||||
origState *terminal.State
|
||||
width int
|
||||
height int
|
||||
yoffset int
|
||||
tabstop int
|
||||
escDelay int
|
||||
fullscreen bool
|
||||
upOneLine bool
|
||||
queued string
|
||||
y int
|
||||
x int
|
||||
maxHeightFunc func(int) int
|
||||
|
||||
// Windows only
|
||||
ttyinChannel chan byte
|
||||
inHandle uintptr
|
||||
outHandle uintptr
|
||||
origStateInput uint32
|
||||
origStateOutput uint32
|
||||
}
|
||||
|
||||
type LightWindow struct {
|
||||
renderer *LightRenderer
|
||||
colored bool
|
||||
preview bool
|
||||
border BorderStyle
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
height int
|
||||
posx int
|
||||
posy int
|
||||
tabstop int
|
||||
fg Color
|
||||
bg Color
|
||||
}
|
||||
|
||||
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer {
|
||||
r := LightRenderer{
|
||||
theme: theme,
|
||||
forceBlack: forceBlack,
|
||||
mouse: mouse,
|
||||
clearOnExit: clearOnExit,
|
||||
ttyin: openTtyIn(),
|
||||
yoffset: 0,
|
||||
tabstop: tabstop,
|
||||
fullscreen: fullscreen,
|
||||
upOneLine: false,
|
||||
maxHeightFunc: maxHeightFunc}
|
||||
return &r
|
||||
}
|
||||
|
||||
func repeat(r rune, times int) string {
|
||||
if times > 0 {
|
||||
return strings.Repeat(string(r), times)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func atoi(s string, defaultValue int) int {
|
||||
value, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Init() {
|
||||
r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
|
||||
|
||||
if err := r.initPlatform(); err != nil {
|
||||
errorExit(err.Error())
|
||||
}
|
||||
r.updateTerminalSize()
|
||||
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
|
||||
|
||||
if r.fullscreen {
|
||||
r.smcup()
|
||||
} else {
|
||||
// We assume that --no-clear is used for repetitive relaunching of fzf.
|
||||
// So we do not clear the lower bottom of the screen.
|
||||
if r.clearOnExit {
|
||||
r.csi("J")
|
||||
}
|
||||
y, x := r.findOffset()
|
||||
r.mouse = r.mouse && y >= 0
|
||||
// When --no-clear is used for repetitive relaunching, there is a small
|
||||
// time frame between fzf processes where the user keystrokes are not
|
||||
// captured by either of fzf process which can cause x offset to be
|
||||
// increased and we're left with unwanted extra new line.
|
||||
if x > 0 && r.clearOnExit {
|
||||
r.upOneLine = true
|
||||
r.makeSpace()
|
||||
}
|
||||
for i := 1; i < r.MaxY(); i++ {
|
||||
r.makeSpace()
|
||||
}
|
||||
}
|
||||
|
||||
if r.mouse {
|
||||
r.csi("?1000h")
|
||||
}
|
||||
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
|
||||
r.csi("G")
|
||||
r.csi("K")
|
||||
if !r.clearOnExit && !r.fullscreen {
|
||||
r.csi("s")
|
||||
}
|
||||
if !r.fullscreen && r.mouse {
|
||||
r.yoffset, _ = r.findOffset()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) makeSpace() {
|
||||
r.stderr("\n")
|
||||
r.csi("G")
|
||||
}
|
||||
|
||||
func (r *LightRenderer) move(y int, x int) {
|
||||
// w.csi("u")
|
||||
if r.y < y {
|
||||
r.csi(fmt.Sprintf("%dB", y-r.y))
|
||||
} else if r.y > y {
|
||||
r.csi(fmt.Sprintf("%dA", r.y-y))
|
||||
}
|
||||
r.stderr("\r")
|
||||
if x > 0 {
|
||||
r.csi(fmt.Sprintf("%dC", x))
|
||||
}
|
||||
r.y = y
|
||||
r.x = x
|
||||
}
|
||||
|
||||
func (r *LightRenderer) origin() {
|
||||
r.move(0, 0)
|
||||
}
|
||||
|
||||
func getEnv(name string, defaultValue int) int {
|
||||
env := os.Getenv(name)
|
||||
if len(env) == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
return atoi(env, defaultValue)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getBytes() []byte {
|
||||
return r.getBytesInternal(r.buffer, false)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
|
||||
c, ok := r.getch(nonblock)
|
||||
if !nonblock && !ok {
|
||||
r.Close()
|
||||
errorExit("Failed to read " + consoleDevice)
|
||||
}
|
||||
|
||||
retries := 0
|
||||
if c == ESC || nonblock {
|
||||
retries = r.escDelay / escPollInterval
|
||||
}
|
||||
buffer = append(buffer, byte(c))
|
||||
|
||||
pc := c
|
||||
for {
|
||||
c, ok = r.getch(true)
|
||||
if !ok {
|
||||
if retries > 0 {
|
||||
retries--
|
||||
time.Sleep(escPollInterval * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
break
|
||||
} else if c == ESC && pc != c {
|
||||
retries = r.escDelay / escPollInterval
|
||||
} else {
|
||||
retries = 0
|
||||
}
|
||||
buffer = append(buffer, byte(c))
|
||||
pc = c
|
||||
|
||||
// This should never happen under normal conditions,
|
||||
// so terminate fzf immediately.
|
||||
if len(buffer) > maxInputBuffer {
|
||||
r.Close()
|
||||
panic(fmt.Sprintf("Input buffer overflow (%d): %v", len(buffer), buffer))
|
||||
}
|
||||
}
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
func (r *LightRenderer) GetChar() Event {
|
||||
if len(r.buffer) == 0 {
|
||||
r.buffer = r.getBytes()
|
||||
}
|
||||
if len(r.buffer) == 0 {
|
||||
panic("Empty buffer")
|
||||
}
|
||||
|
||||
sz := 1
|
||||
defer func() {
|
||||
r.buffer = r.buffer[sz:]
|
||||
}()
|
||||
|
||||
switch r.buffer[0] {
|
||||
case CtrlC:
|
||||
return Event{CtrlC, 0, nil}
|
||||
case CtrlG:
|
||||
return Event{CtrlG, 0, nil}
|
||||
case CtrlQ:
|
||||
return Event{CtrlQ, 0, nil}
|
||||
case 127:
|
||||
return Event{BSpace, 0, nil}
|
||||
case 0:
|
||||
return Event{CtrlSpace, 0, nil}
|
||||
case 28:
|
||||
return Event{CtrlBackSlash, 0, nil}
|
||||
case 29:
|
||||
return Event{CtrlRightBracket, 0, nil}
|
||||
case 30:
|
||||
return Event{CtrlCaret, 0, nil}
|
||||
case 31:
|
||||
return Event{CtrlSlash, 0, nil}
|
||||
case ESC:
|
||||
ev := r.escSequence(&sz)
|
||||
// Second chance
|
||||
if ev.Type == Invalid {
|
||||
r.buffer = r.getBytes()
|
||||
ev = r.escSequence(&sz)
|
||||
}
|
||||
return ev
|
||||
}
|
||||
|
||||
// CTRL-A ~ CTRL-Z
|
||||
if r.buffer[0] <= CtrlZ {
|
||||
return Event{int(r.buffer[0]), 0, nil}
|
||||
}
|
||||
char, rsz := utf8.DecodeRune(r.buffer)
|
||||
if char == utf8.RuneError {
|
||||
return Event{ESC, 0, nil}
|
||||
}
|
||||
sz = rsz
|
||||
return Event{Rune, char, nil}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
if len(r.buffer) < 2 {
|
||||
return Event{ESC, 0, nil}
|
||||
}
|
||||
|
||||
loc := offsetRegexpBegin.FindIndex(r.buffer)
|
||||
if loc != nil && loc[0] == 0 {
|
||||
*sz = loc[1]
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
*sz = 2
|
||||
if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 {
|
||||
return Event{int(CtrlAltA + r.buffer[1] - 1), 0, nil}
|
||||
}
|
||||
alt := false
|
||||
if len(r.buffer) > 2 && r.buffer[1] == ESC {
|
||||
r.buffer = r.buffer[1:]
|
||||
alt = true
|
||||
}
|
||||
switch r.buffer[1] {
|
||||
case ESC:
|
||||
return Event{ESC, 0, nil}
|
||||
case ' ':
|
||||
return Event{AltSpace, 0, nil}
|
||||
case '/':
|
||||
return Event{AltSlash, 0, nil}
|
||||
case 'b':
|
||||
return Event{AltB, 0, nil}
|
||||
case 'd':
|
||||
return Event{AltD, 0, nil}
|
||||
case 'f':
|
||||
return Event{AltF, 0, nil}
|
||||
case 127:
|
||||
return Event{AltBS, 0, nil}
|
||||
case '[', 'O':
|
||||
if len(r.buffer) < 3 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 3
|
||||
switch r.buffer[2] {
|
||||
case 'D':
|
||||
if alt {
|
||||
return Event{AltLeft, 0, nil}
|
||||
}
|
||||
return Event{Left, 0, nil}
|
||||
case 'C':
|
||||
if alt {
|
||||
// Ugh..
|
||||
return Event{AltRight, 0, nil}
|
||||
}
|
||||
return Event{Right, 0, nil}
|
||||
case 'B':
|
||||
if alt {
|
||||
return Event{AltDown, 0, nil}
|
||||
}
|
||||
return Event{Down, 0, nil}
|
||||
case 'A':
|
||||
if alt {
|
||||
return Event{AltUp, 0, nil}
|
||||
}
|
||||
return Event{Up, 0, nil}
|
||||
case 'Z':
|
||||
return Event{BTab, 0, nil}
|
||||
case 'H':
|
||||
return Event{Home, 0, nil}
|
||||
case 'F':
|
||||
return Event{End, 0, nil}
|
||||
case 'M':
|
||||
return r.mouseSequence(sz)
|
||||
case 'P':
|
||||
return Event{F1, 0, nil}
|
||||
case 'Q':
|
||||
return Event{F2, 0, nil}
|
||||
case 'R':
|
||||
return Event{F3, 0, nil}
|
||||
case 'S':
|
||||
return Event{F4, 0, nil}
|
||||
case '1', '2', '3', '4', '5', '6':
|
||||
if len(r.buffer) < 4 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 4
|
||||
switch r.buffer[2] {
|
||||
case '2':
|
||||
if r.buffer[3] == '~' {
|
||||
return Event{Insert, 0, nil}
|
||||
}
|
||||
if len(r.buffer) > 4 && r.buffer[4] == '~' {
|
||||
*sz = 5
|
||||
switch r.buffer[3] {
|
||||
case '0':
|
||||
return Event{F9, 0, nil}
|
||||
case '1':
|
||||
return Event{F10, 0, nil}
|
||||
case '3':
|
||||
return Event{F11, 0, nil}
|
||||
case '4':
|
||||
return Event{F12, 0, nil}
|
||||
}
|
||||
}
|
||||
// Bracketed paste mode: \e[200~ ... \e[201~
|
||||
if len(r.buffer) > 5 && r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' {
|
||||
// Immediately discard the sequence from the buffer and reread input
|
||||
r.buffer = r.buffer[6:]
|
||||
*sz = 0
|
||||
return r.GetChar()
|
||||
}
|
||||
return Event{Invalid, 0, nil} // INS
|
||||
case '3':
|
||||
return Event{Del, 0, nil}
|
||||
case '4':
|
||||
return Event{End, 0, nil}
|
||||
case '5':
|
||||
return Event{PgUp, 0, nil}
|
||||
case '6':
|
||||
return Event{PgDn, 0, nil}
|
||||
case '1':
|
||||
switch r.buffer[3] {
|
||||
case '~':
|
||||
return Event{Home, 0, nil}
|
||||
case '1', '2', '3', '4', '5', '7', '8', '9':
|
||||
if len(r.buffer) == 5 && r.buffer[4] == '~' {
|
||||
*sz = 5
|
||||
switch r.buffer[3] {
|
||||
case '1':
|
||||
return Event{F1, 0, nil}
|
||||
case '2':
|
||||
return Event{F2, 0, nil}
|
||||
case '3':
|
||||
return Event{F3, 0, nil}
|
||||
case '4':
|
||||
return Event{F4, 0, nil}
|
||||
case '5':
|
||||
return Event{F5, 0, nil}
|
||||
case '7':
|
||||
return Event{F6, 0, nil}
|
||||
case '8':
|
||||
return Event{F7, 0, nil}
|
||||
case '9':
|
||||
return Event{F8, 0, nil}
|
||||
}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
case ';':
|
||||
if len(r.buffer) != 6 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 6
|
||||
switch r.buffer[4] {
|
||||
case '2', '5':
|
||||
switch r.buffer[5] {
|
||||
case 'A':
|
||||
return Event{SUp, 0, nil}
|
||||
case 'B':
|
||||
return Event{SDown, 0, nil}
|
||||
case 'C':
|
||||
return Event{SRight, 0, nil}
|
||||
case 'D':
|
||||
return Event{SLeft, 0, nil}
|
||||
}
|
||||
} // r.buffer[4]
|
||||
} // r.buffer[3]
|
||||
} // r.buffer[2]
|
||||
} // r.buffer[2]
|
||||
} // r.buffer[1]
|
||||
if r.buffer[1] >= 'a' && r.buffer[1] <= 'z' {
|
||||
return Event{AltA + int(r.buffer[1]) - 'a', 0, nil}
|
||||
}
|
||||
if r.buffer[1] >= '0' && r.buffer[1] <= '9' {
|
||||
return Event{Alt0 + int(r.buffer[1]) - '0', 0, nil}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) mouseSequence(sz *int) Event {
|
||||
if len(r.buffer) < 6 || !r.mouse {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 6
|
||||
switch r.buffer[3] {
|
||||
case 32, 34, 36, 40, 48, // mouse-down / shift / cmd / ctrl
|
||||
35, 39, 43, 51: // mouse-up / shift / cmd / ctrl
|
||||
mod := r.buffer[3] >= 36
|
||||
left := r.buffer[3] == 32
|
||||
down := r.buffer[3]%2 == 0
|
||||
x := int(r.buffer[4] - 33)
|
||||
y := int(r.buffer[5]-33) - r.yoffset
|
||||
double := false
|
||||
if down {
|
||||
now := time.Now()
|
||||
if !left { // Right double click is not allowed
|
||||
r.clickY = []int{}
|
||||
} else if now.Sub(r.prevDownTime) < doubleClickDuration {
|
||||
r.clickY = append(r.clickY, y)
|
||||
} else {
|
||||
r.clickY = []int{y}
|
||||
}
|
||||
r.prevDownTime = now
|
||||
} else {
|
||||
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
|
||||
time.Since(r.prevDownTime) < doubleClickDuration {
|
||||
double = true
|
||||
}
|
||||
}
|
||||
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
|
||||
case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
|
||||
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
|
||||
mod := r.buffer[3] >= 100
|
||||
s := 1 - int(r.buffer[3]%2)*2
|
||||
x := int(r.buffer[4] - 33)
|
||||
y := int(r.buffer[5]-33) - r.yoffset
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, false, mod}}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) smcup() {
|
||||
r.csi("?1049h")
|
||||
}
|
||||
|
||||
func (r *LightRenderer) rmcup() {
|
||||
r.csi("?1049l")
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Pause(clear bool) {
|
||||
r.restoreTerminal()
|
||||
if clear {
|
||||
if r.fullscreen {
|
||||
r.rmcup()
|
||||
} else {
|
||||
r.smcup()
|
||||
r.csi("H")
|
||||
}
|
||||
r.flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Resume(clear bool, sigcont bool) {
|
||||
r.setupTerminal()
|
||||
if clear {
|
||||
if r.fullscreen {
|
||||
r.smcup()
|
||||
} else {
|
||||
r.rmcup()
|
||||
}
|
||||
r.flush()
|
||||
} else if sigcont && !r.fullscreen && r.mouse {
|
||||
// NOTE: SIGCONT (Coming back from CTRL-Z):
|
||||
// It's highly likely that the offset we obtained at the beginning is
|
||||
// no longer correct, so we simply disable mouse input.
|
||||
r.csi("?1000l")
|
||||
r.mouse = false
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Clear() {
|
||||
if r.fullscreen {
|
||||
r.csi("H")
|
||||
}
|
||||
// r.csi("u")
|
||||
r.origin()
|
||||
r.csi("J")
|
||||
r.flush()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) RefreshWindows(windows []Window) {
|
||||
r.flush()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Refresh() {
|
||||
r.updateTerminalSize()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Close() {
|
||||
// r.csi("u")
|
||||
if r.clearOnExit {
|
||||
if r.fullscreen {
|
||||
r.rmcup()
|
||||
} else {
|
||||
r.origin()
|
||||
if r.upOneLine {
|
||||
r.csi("A")
|
||||
}
|
||||
r.csi("J")
|
||||
}
|
||||
} else if !r.fullscreen {
|
||||
r.csi("u")
|
||||
}
|
||||
if r.mouse {
|
||||
r.csi("?1000l")
|
||||
}
|
||||
r.flush()
|
||||
r.closePlatform()
|
||||
r.restoreTerminal()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) MaxX() int {
|
||||
return r.width
|
||||
}
|
||||
|
||||
func (r *LightRenderer) MaxY() int {
|
||||
return r.height
|
||||
}
|
||||
|
||||
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
|
||||
w := &LightWindow{
|
||||
renderer: r,
|
||||
colored: r.theme.Colored,
|
||||
preview: preview,
|
||||
border: borderStyle,
|
||||
top: top,
|
||||
left: left,
|
||||
width: width,
|
||||
height: height,
|
||||
tabstop: r.tabstop,
|
||||
fg: colDefault,
|
||||
bg: colDefault}
|
||||
if preview {
|
||||
w.fg = r.theme.PreviewFg.Color
|
||||
w.bg = r.theme.PreviewBg.Color
|
||||
} else {
|
||||
w.fg = r.theme.Fg.Color
|
||||
w.bg = r.theme.Bg.Color
|
||||
}
|
||||
w.drawBorder()
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorder() {
|
||||
switch w.border.shape {
|
||||
case BorderRounded, BorderSharp:
|
||||
w.drawBorderAround()
|
||||
case BorderHorizontal:
|
||||
w.drawBorderHorizontal(true, true)
|
||||
case BorderVertical:
|
||||
w.drawBorderVertical(true, true)
|
||||
case BorderTop:
|
||||
w.drawBorderHorizontal(true, false)
|
||||
case BorderBottom:
|
||||
w.drawBorderHorizontal(false, true)
|
||||
case BorderLeft:
|
||||
w.drawBorderVertical(true, false)
|
||||
case BorderRight:
|
||||
w.drawBorderVertical(false, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
|
||||
if top {
|
||||
w.Move(0, 0)
|
||||
w.CPrint(ColBorder, repeat(w.border.horizontal, w.width))
|
||||
}
|
||||
if bottom {
|
||||
w.Move(w.height-1, 0)
|
||||
w.CPrint(ColBorder, repeat(w.border.horizontal, w.width))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorderVertical(left, right bool) {
|
||||
width := w.width - 2
|
||||
if !left || !right {
|
||||
width++
|
||||
}
|
||||
for y := 0; y < w.height; y++ {
|
||||
w.Move(y, 0)
|
||||
if left {
|
||||
w.CPrint(ColBorder, string(w.border.vertical))
|
||||
}
|
||||
w.CPrint(ColBorder, repeat(' ', width))
|
||||
if right {
|
||||
w.CPrint(ColBorder, string(w.border.vertical))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorderAround() {
|
||||
w.Move(0, 0)
|
||||
color := ColBorder
|
||||
if w.preview {
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
w.CPrint(color, string(w.border.topLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.topRight))
|
||||
for y := 1; y < w.height-1; y++ {
|
||||
w.Move(y, 0)
|
||||
w.CPrint(color, string(w.border.vertical))
|
||||
w.CPrint(color, repeat(' ', w.width-2))
|
||||
w.CPrint(color, string(w.border.vertical))
|
||||
}
|
||||
w.Move(w.height-1, 0)
|
||||
w.CPrint(color, string(w.border.bottomLeft)+repeat(w.border.horizontal, w.width-2)+string(w.border.bottomRight))
|
||||
}
|
||||
|
||||
func (w *LightWindow) csi(code string) {
|
||||
w.renderer.csi(code)
|
||||
}
|
||||
|
||||
func (w *LightWindow) stderrInternal(str string, allowNLCR bool) {
|
||||
w.renderer.stderrInternal(str, allowNLCR)
|
||||
}
|
||||
|
||||
func (w *LightWindow) Top() int {
|
||||
return w.top
|
||||
}
|
||||
|
||||
func (w *LightWindow) Left() int {
|
||||
return w.left
|
||||
}
|
||||
|
||||
func (w *LightWindow) Width() int {
|
||||
return w.width
|
||||
}
|
||||
|
||||
func (w *LightWindow) Height() int {
|
||||
return w.height
|
||||
}
|
||||
|
||||
func (w *LightWindow) Refresh() {
|
||||
}
|
||||
|
||||
func (w *LightWindow) Close() {
|
||||
}
|
||||
|
||||
func (w *LightWindow) X() int {
|
||||
return w.posx
|
||||
}
|
||||
|
||||
func (w *LightWindow) Y() int {
|
||||
return w.posy
|
||||
}
|
||||
|
||||
func (w *LightWindow) Enclose(y int, x int) bool {
|
||||
return x >= w.left && x < (w.left+w.width) &&
|
||||
y >= w.top && y < (w.top+w.height)
|
||||
}
|
||||
|
||||
func (w *LightWindow) Move(y int, x int) {
|
||||
w.posx = x
|
||||
w.posy = y
|
||||
|
||||
w.renderer.move(w.Top()+y, w.Left()+x)
|
||||
}
|
||||
|
||||
func (w *LightWindow) MoveAndClear(y int, x int) {
|
||||
w.Move(y, x)
|
||||
// We should not delete preview window on the right
|
||||
// csi("K")
|
||||
w.Print(repeat(' ', w.width-x))
|
||||
w.Move(y, x)
|
||||
}
|
||||
|
||||
func attrCodes(attr Attr) []string {
|
||||
codes := []string{}
|
||||
if (attr & AttrClear) > 0 {
|
||||
return codes
|
||||
}
|
||||
if (attr & Bold) > 0 {
|
||||
codes = append(codes, "1")
|
||||
}
|
||||
if (attr & Dim) > 0 {
|
||||
codes = append(codes, "2")
|
||||
}
|
||||
if (attr & Italic) > 0 {
|
||||
codes = append(codes, "3")
|
||||
}
|
||||
if (attr & Underline) > 0 {
|
||||
codes = append(codes, "4")
|
||||
}
|
||||
if (attr & Blink) > 0 {
|
||||
codes = append(codes, "5")
|
||||
}
|
||||
if (attr & Reverse) > 0 {
|
||||
codes = append(codes, "7")
|
||||
}
|
||||
return codes
|
||||
}
|
||||
|
||||
func colorCodes(fg Color, bg Color) []string {
|
||||
codes := []string{}
|
||||
appendCode := func(c Color, offset int) {
|
||||
if c == colDefault {
|
||||
return
|
||||
}
|
||||
if c.is24() {
|
||||
r := (c >> 16) & 0xff
|
||||
g := (c >> 8) & 0xff
|
||||
b := (c) & 0xff
|
||||
codes = append(codes, fmt.Sprintf("%d;2;%d;%d;%d", 38+offset, r, g, b))
|
||||
} else if c >= colBlack && c <= colWhite {
|
||||
codes = append(codes, fmt.Sprintf("%d", int(c)+30+offset))
|
||||
} else if c > colWhite && c < 16 {
|
||||
codes = append(codes, fmt.Sprintf("%d", int(c)+90+offset-8))
|
||||
} else if c >= 16 && c < 256 {
|
||||
codes = append(codes, fmt.Sprintf("%d;5;%d", 38+offset, c))
|
||||
}
|
||||
}
|
||||
appendCode(fg, 0)
|
||||
appendCode(bg, 10)
|
||||
return codes
|
||||
}
|
||||
|
||||
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool {
|
||||
codes := append(attrCodes(attr), colorCodes(fg, bg)...)
|
||||
w.csi(";" + strings.Join(codes, ";") + "m")
|
||||
return len(codes) > 0
|
||||
}
|
||||
|
||||
func (w *LightWindow) Print(text string) {
|
||||
w.cprint2(colDefault, w.bg, AttrRegular, text)
|
||||
}
|
||||
|
||||
func cleanse(str string) string {
|
||||
return strings.Replace(str, "\x1b", "", -1)
|
||||
}
|
||||
|
||||
func (w *LightWindow) CPrint(pair ColorPair, text string) {
|
||||
w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
|
||||
w.stderrInternal(cleanse(text), false)
|
||||
w.csi("m")
|
||||
}
|
||||
|
||||
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
||||
if w.csiColor(fg, bg, attr) {
|
||||
defer w.csi("m")
|
||||
}
|
||||
w.stderrInternal(cleanse(text), false)
|
||||
}
|
||||
|
||||
type wrappedLine struct {
|
||||
text string
|
||||
displayWidth int
|
||||
}
|
||||
|
||||
func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine {
|
||||
lines := []wrappedLine{}
|
||||
width := 0
|
||||
line := ""
|
||||
for _, r := range input {
|
||||
w := util.RuneWidth(r, prefixLength+width, 8)
|
||||
width += w
|
||||
str := string(r)
|
||||
if r == '\t' {
|
||||
str = repeat(' ', w)
|
||||
}
|
||||
if prefixLength+width <= max {
|
||||
line += str
|
||||
} else {
|
||||
lines = append(lines, wrappedLine{string(line), width - w})
|
||||
line = str
|
||||
prefixLength = 0
|
||||
width = util.RuneWidth(r, prefixLength, 8)
|
||||
}
|
||||
}
|
||||
lines = append(lines, wrappedLine{string(line), width})
|
||||
return lines
|
||||
}
|
||||
|
||||
func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
||||
allLines := strings.Split(str, "\n")
|
||||
for i, line := range allLines {
|
||||
lines := wrapLine(line, w.posx, w.width, w.tabstop)
|
||||
for j, wl := range lines {
|
||||
if w.posx >= w.Width()-1 && wl.displayWidth == 0 {
|
||||
if w.posy < w.height-1 {
|
||||
w.Move(w.posy+1, 0)
|
||||
}
|
||||
return FillNextLine
|
||||
}
|
||||
w.stderrInternal(wl.text, false)
|
||||
w.posx += wl.displayWidth
|
||||
|
||||
// Wrap line
|
||||
if j < len(lines)-1 || i < len(allLines)-1 {
|
||||
if w.posy+1 >= w.height {
|
||||
return FillSuspend
|
||||
}
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
w.Move(w.posy+1, 0)
|
||||
onMove()
|
||||
}
|
||||
}
|
||||
}
|
||||
return FillContinue
|
||||
}
|
||||
|
||||
func (w *LightWindow) setBg() {
|
||||
if w.bg != colDefault {
|
||||
w.csiColor(colDefault, w.bg, AttrRegular)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) Fill(text string) FillReturn {
|
||||
w.Move(w.posy, w.posx)
|
||||
w.setBg()
|
||||
return w.fill(text, w.setBg)
|
||||
}
|
||||
|
||||
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
|
||||
w.Move(w.posy, w.posx)
|
||||
if fg == colDefault {
|
||||
fg = w.fg
|
||||
}
|
||||
if bg == colDefault {
|
||||
bg = w.bg
|
||||
}
|
||||
if w.csiColor(fg, bg, attr) {
|
||||
defer w.csi("m")
|
||||
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
|
||||
}
|
||||
return w.fill(text, w.setBg)
|
||||
}
|
||||
|
||||
func (w *LightWindow) FinishFill() {
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
for y := w.posy + 1; y < w.height; y++ {
|
||||
w.MoveAndClear(y, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) Erase() {
|
||||
w.drawBorder()
|
||||
// We don't erase the window here to avoid flickering during scroll
|
||||
w.Move(0, 0)
|
||||
}
|
||||
110
src/tui/light_unix.go
Normal file
110
src/tui/light_unix.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// +build !windows
|
||||
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
func IsLightRendererSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *LightRenderer) defaultTheme() *ColorTheme {
|
||||
if strings.Contains(os.Getenv("TERM"), "256") {
|
||||
return Dark256
|
||||
}
|
||||
colors, err := exec.Command("tput", "colors").Output()
|
||||
if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 {
|
||||
return Dark256
|
||||
}
|
||||
return Default16
|
||||
}
|
||||
|
||||
func (r *LightRenderer) fd() int {
|
||||
return int(r.ttyin.Fd())
|
||||
}
|
||||
|
||||
func (r *LightRenderer) initPlatform() error {
|
||||
fd := r.fd()
|
||||
origState, err := terminal.GetState(fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.origState = origState
|
||||
terminal.MakeRaw(fd)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LightRenderer) closePlatform() {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
func openTtyIn() *os.File {
|
||||
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
tty := ttyname()
|
||||
if len(tty) > 0 {
|
||||
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
|
||||
return in
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
|
||||
os.Exit(2)
|
||||
}
|
||||
return in
|
||||
}
|
||||
|
||||
func (r *LightRenderer) setupTerminal() {
|
||||
terminal.MakeRaw(r.fd())
|
||||
}
|
||||
|
||||
func (r *LightRenderer) restoreTerminal() {
|
||||
terminal.Restore(r.fd(), r.origState)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) updateTerminalSize() {
|
||||
width, height, err := terminal.GetSize(r.fd())
|
||||
|
||||
if err == nil {
|
||||
r.width = width
|
||||
r.height = r.maxHeightFunc(height)
|
||||
} else {
|
||||
r.width = getEnv("COLUMNS", defaultWidth)
|
||||
r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) findOffset() (row int, col int) {
|
||||
r.csi("6n")
|
||||
r.flush()
|
||||
bytes := []byte{}
|
||||
for tries := 0; tries < offsetPollTries; tries++ {
|
||||
bytes = r.getBytesInternal(bytes, tries > 0)
|
||||
offsets := offsetRegexp.FindSubmatch(bytes)
|
||||
if len(offsets) > 3 {
|
||||
// Add anything we skipped over to the input buffer
|
||||
r.buffer = append(r.buffer, offsets[1]...)
|
||||
return atoi(string(offsets[2]), 0) - 1, atoi(string(offsets[3]), 0) - 1
|
||||
}
|
||||
}
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||
b := make([]byte, 1)
|
||||
fd := r.fd()
|
||||
util.SetNonblock(r.ttyin, nonblock)
|
||||
_, err := util.Read(fd, b)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return int(b[0]), true
|
||||
}
|
||||
140
src/tui/light_windows.go
Normal file
140
src/tui/light_windows.go
Normal file
@@ -0,0 +1,140 @@
|
||||
//+build windows
|
||||
|
||||
package tui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS)
|
||||
consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN)
|
||||
)
|
||||
|
||||
// IsLightRendererSupported checks to see if the Light renderer is supported
|
||||
func IsLightRendererSupported() bool {
|
||||
var oldState uint32
|
||||
// enable vt100 emulation (https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences)
|
||||
if windows.GetConsoleMode(windows.Stderr, &oldState) != nil {
|
||||
return false
|
||||
}
|
||||
// attempt to set mode to determine if we support VT 100 codes. This will work on newer Windows 10
|
||||
// version:
|
||||
canSetVt100 := windows.SetConsoleMode(windows.Stderr, oldState|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) == nil
|
||||
var checkState uint32
|
||||
if windows.GetConsoleMode(windows.Stderr, &checkState) != nil ||
|
||||
(checkState&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING {
|
||||
return false
|
||||
}
|
||||
windows.SetConsoleMode(windows.Stderr, oldState)
|
||||
return canSetVt100
|
||||
}
|
||||
|
||||
func (r *LightRenderer) defaultTheme() *ColorTheme {
|
||||
// the getenv check is borrowed from here: https://github.com/gdamore/tcell/commit/0c473b86d82f68226a142e96cc5a34c5a29b3690#diff-b008fcd5e6934bf31bc3d33bf49f47d8R178:
|
||||
if !IsLightRendererSupported() || os.Getenv("ConEmuPID") != "" || os.Getenv("TCELL_TRUECOLOR") == "disable" {
|
||||
return Default16
|
||||
}
|
||||
return Dark256
|
||||
}
|
||||
|
||||
func (r *LightRenderer) initPlatform() error {
|
||||
//outHandle := windows.Stdout
|
||||
outHandle, _ := syscall.Open("CONOUT$", syscall.O_RDWR, 0)
|
||||
// enable vt100 emulation (https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences)
|
||||
if err := windows.GetConsoleMode(windows.Handle(outHandle), &r.origStateOutput); err != nil {
|
||||
return err
|
||||
}
|
||||
r.outHandle = uintptr(outHandle)
|
||||
inHandle, _ := syscall.Open("CONIN$", syscall.O_RDWR, 0)
|
||||
if err := windows.GetConsoleMode(windows.Handle(inHandle), &r.origStateInput); err != nil {
|
||||
return err
|
||||
}
|
||||
r.inHandle = uintptr(inHandle)
|
||||
|
||||
r.setupTerminal()
|
||||
|
||||
// channel for non-blocking reads. Buffer to make sure
|
||||
// we get the ESC sets:
|
||||
r.ttyinChannel = make(chan byte, 12)
|
||||
|
||||
// the following allows for non-blocking IO.
|
||||
// syscall.SetNonblock() is a NOOP under Windows.
|
||||
go func() {
|
||||
fd := int(r.inHandle)
|
||||
b := make([]byte, 1)
|
||||
for {
|
||||
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
|
||||
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
|
||||
_, err := util.Read(fd, b)
|
||||
if err == nil {
|
||||
r.ttyinChannel <- b[0]
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LightRenderer) closePlatform() {
|
||||
windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
|
||||
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
|
||||
}
|
||||
|
||||
func openTtyIn() *os.File {
|
||||
// not used
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LightRenderer) setupTerminal() error {
|
||||
if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil {
|
||||
return err
|
||||
}
|
||||
return windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) restoreTerminal() error {
|
||||
if err := windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput); err != nil {
|
||||
return err
|
||||
}
|
||||
return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) updateTerminalSize() {
|
||||
var bufferInfo windows.ConsoleScreenBufferInfo
|
||||
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
|
||||
r.width = getEnv("COLUMNS", defaultWidth)
|
||||
r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
|
||||
|
||||
} else {
|
||||
r.width = int(bufferInfo.Window.Right - bufferInfo.Window.Left)
|
||||
r.height = r.maxHeightFunc(int(bufferInfo.Window.Bottom - bufferInfo.Window.Top))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) findOffset() (row int, col int) {
|
||||
var bufferInfo windows.ConsoleScreenBufferInfo
|
||||
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
|
||||
return -1, -1
|
||||
}
|
||||
return int(bufferInfo.CursorPosition.X), int(bufferInfo.CursorPosition.Y)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||
if nonblock {
|
||||
select {
|
||||
case bc := <-r.ttyinChannel:
|
||||
return int(bc), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
} else {
|
||||
bc := <-r.ttyinChannel
|
||||
return int(bc), true
|
||||
}
|
||||
}
|
||||
638
src/tui/tcell.go
Normal file
638
src/tui/tcell.go
Normal file
@@ -0,0 +1,638 @@
|
||||
// +build tcell windows
|
||||
|
||||
package tui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"runtime"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/gdamore/tcell/encoding"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
func HasFullscreenRenderer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p ColorPair) style() tcell.Style {
|
||||
style := tcell.StyleDefault
|
||||
return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg()))
|
||||
}
|
||||
|
||||
type Attr tcell.Style
|
||||
|
||||
type TcellWindow struct {
|
||||
color bool
|
||||
preview bool
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
height int
|
||||
normal ColorPair
|
||||
lastX int
|
||||
lastY int
|
||||
moveCursor bool
|
||||
borderStyle BorderStyle
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Top() int {
|
||||
return w.top
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Left() int {
|
||||
return w.left
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Width() int {
|
||||
return w.width
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Height() int {
|
||||
return w.height
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Refresh() {
|
||||
if w.moveCursor {
|
||||
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
|
||||
w.moveCursor = false
|
||||
}
|
||||
w.lastX = 0
|
||||
w.lastY = 0
|
||||
|
||||
w.drawBorder()
|
||||
}
|
||||
|
||||
func (w *TcellWindow) FinishFill() {
|
||||
// NO-OP
|
||||
}
|
||||
|
||||
const (
|
||||
Bold Attr = Attr(tcell.AttrBold)
|
||||
Dim = Attr(tcell.AttrDim)
|
||||
Blink = Attr(tcell.AttrBlink)
|
||||
Reverse = Attr(tcell.AttrReverse)
|
||||
Underline = Attr(tcell.AttrUnderline)
|
||||
Italic = Attr(tcell.AttrItalic)
|
||||
)
|
||||
|
||||
const (
|
||||
AttrUndefined = Attr(0)
|
||||
AttrRegular = Attr(1 << 7)
|
||||
AttrClear = Attr(1 << 8)
|
||||
)
|
||||
|
||||
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
|
||||
if _screen.Colors() >= 256 {
|
||||
return Dark256
|
||||
}
|
||||
return Default16
|
||||
}
|
||||
|
||||
var (
|
||||
_colorToAttribute = []tcell.Color{
|
||||
tcell.ColorBlack,
|
||||
tcell.ColorRed,
|
||||
tcell.ColorGreen,
|
||||
tcell.ColorYellow,
|
||||
tcell.ColorBlue,
|
||||
tcell.ColorDarkMagenta,
|
||||
tcell.ColorLightCyan,
|
||||
tcell.ColorWhite,
|
||||
}
|
||||
)
|
||||
|
||||
func (c Color) Style() tcell.Color {
|
||||
if c <= colDefault {
|
||||
return tcell.ColorDefault
|
||||
} else if c >= colBlack && c <= colWhite {
|
||||
return _colorToAttribute[int(c)]
|
||||
} else {
|
||||
return tcell.Color(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (a Attr) Merge(b Attr) Attr {
|
||||
return a | b
|
||||
}
|
||||
|
||||
var (
|
||||
_screen tcell.Screen
|
||||
)
|
||||
|
||||
func (r *FullscreenRenderer) initScreen() {
|
||||
s, e := tcell.NewScreen()
|
||||
if e != nil {
|
||||
errorExit(e.Error())
|
||||
}
|
||||
if e = s.Init(); e != nil {
|
||||
errorExit(e.Error())
|
||||
}
|
||||
if r.mouse {
|
||||
s.EnableMouse()
|
||||
} else {
|
||||
s.DisableMouse()
|
||||
}
|
||||
_screen = s
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Init() {
|
||||
if os.Getenv("TERM") == "cygwin" {
|
||||
os.Setenv("TERM", "")
|
||||
}
|
||||
encoding.Register()
|
||||
|
||||
r.initScreen()
|
||||
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) MaxX() int {
|
||||
ncols, _ := _screen.Size()
|
||||
return int(ncols)
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) MaxY() int {
|
||||
_, nlines := _screen.Size()
|
||||
return int(nlines)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) X() int {
|
||||
return w.lastX
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Y() int {
|
||||
return w.lastY
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Clear() {
|
||||
_screen.Sync()
|
||||
_screen.Clear()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Refresh() {
|
||||
// noop
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) GetChar() Event {
|
||||
ev := _screen.PollEvent()
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventResize:
|
||||
return Event{Resize, 0, nil}
|
||||
|
||||
// process mouse events:
|
||||
case *tcell.EventMouse:
|
||||
x, y := ev.Position()
|
||||
button := ev.Buttons()
|
||||
mod := ev.Modifiers() != 0
|
||||
if button&tcell.WheelDown != 0 {
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
|
||||
} else if button&tcell.WheelUp != 0 {
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
|
||||
} else if runtime.GOOS != "windows" {
|
||||
// double and single taps on Windows don't quite work due to
|
||||
// the console acting on the events and not allowing us
|
||||
// to consume them.
|
||||
|
||||
left := button&tcell.Button1 != 0
|
||||
down := left || button&tcell.Button3 != 0
|
||||
double := false
|
||||
if down {
|
||||
now := time.Now()
|
||||
if !left {
|
||||
r.clickY = []int{}
|
||||
} else if now.Sub(r.prevDownTime) < doubleClickDuration {
|
||||
r.clickY = append(r.clickY, x)
|
||||
} else {
|
||||
r.clickY = []int{x}
|
||||
r.prevDownTime = now
|
||||
}
|
||||
} else {
|
||||
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
|
||||
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
|
||||
double = true
|
||||
}
|
||||
}
|
||||
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
|
||||
}
|
||||
|
||||
// process keyboard:
|
||||
case *tcell.EventKey:
|
||||
alt := (ev.Modifiers() & tcell.ModAlt) > 0
|
||||
keyfn := func(r rune) int {
|
||||
if alt {
|
||||
return CtrlAltA - 'a' + int(r)
|
||||
}
|
||||
return CtrlA - 'a' + int(r)
|
||||
}
|
||||
switch ev.Key() {
|
||||
case tcell.KeyCtrlA:
|
||||
return Event{keyfn('a'), 0, nil}
|
||||
case tcell.KeyCtrlB:
|
||||
return Event{keyfn('b'), 0, nil}
|
||||
case tcell.KeyCtrlC:
|
||||
return Event{keyfn('c'), 0, nil}
|
||||
case tcell.KeyCtrlD:
|
||||
return Event{keyfn('d'), 0, nil}
|
||||
case tcell.KeyCtrlE:
|
||||
return Event{keyfn('e'), 0, nil}
|
||||
case tcell.KeyCtrlF:
|
||||
return Event{keyfn('f'), 0, nil}
|
||||
case tcell.KeyCtrlG:
|
||||
return Event{keyfn('g'), 0, nil}
|
||||
case tcell.KeyCtrlH:
|
||||
return Event{keyfn('h'), 0, nil}
|
||||
case tcell.KeyCtrlI:
|
||||
return Event{keyfn('i'), 0, nil}
|
||||
case tcell.KeyCtrlJ:
|
||||
return Event{keyfn('j'), 0, nil}
|
||||
case tcell.KeyCtrlK:
|
||||
return Event{keyfn('k'), 0, nil}
|
||||
case tcell.KeyCtrlL:
|
||||
return Event{keyfn('l'), 0, nil}
|
||||
case tcell.KeyCtrlM:
|
||||
return Event{keyfn('m'), 0, nil}
|
||||
case tcell.KeyCtrlN:
|
||||
return Event{keyfn('n'), 0, nil}
|
||||
case tcell.KeyCtrlO:
|
||||
return Event{keyfn('o'), 0, nil}
|
||||
case tcell.KeyCtrlP:
|
||||
return Event{keyfn('p'), 0, nil}
|
||||
case tcell.KeyCtrlQ:
|
||||
return Event{keyfn('q'), 0, nil}
|
||||
case tcell.KeyCtrlR:
|
||||
return Event{keyfn('r'), 0, nil}
|
||||
case tcell.KeyCtrlS:
|
||||
return Event{keyfn('s'), 0, nil}
|
||||
case tcell.KeyCtrlT:
|
||||
return Event{keyfn('t'), 0, nil}
|
||||
case tcell.KeyCtrlU:
|
||||
return Event{keyfn('u'), 0, nil}
|
||||
case tcell.KeyCtrlV:
|
||||
return Event{keyfn('v'), 0, nil}
|
||||
case tcell.KeyCtrlW:
|
||||
return Event{keyfn('w'), 0, nil}
|
||||
case tcell.KeyCtrlX:
|
||||
return Event{keyfn('x'), 0, nil}
|
||||
case tcell.KeyCtrlY:
|
||||
return Event{keyfn('y'), 0, nil}
|
||||
case tcell.KeyCtrlZ:
|
||||
return Event{keyfn('z'), 0, nil}
|
||||
case tcell.KeyCtrlSpace:
|
||||
return Event{CtrlSpace, 0, nil}
|
||||
case tcell.KeyCtrlBackslash:
|
||||
return Event{CtrlBackSlash, 0, nil}
|
||||
case tcell.KeyCtrlRightSq:
|
||||
return Event{CtrlRightBracket, 0, nil}
|
||||
case tcell.KeyCtrlUnderscore:
|
||||
return Event{CtrlSlash, 0, nil}
|
||||
case tcell.KeyBackspace2:
|
||||
if alt {
|
||||
return Event{AltBS, 0, nil}
|
||||
}
|
||||
return Event{BSpace, 0, nil}
|
||||
|
||||
case tcell.KeyUp:
|
||||
if alt {
|
||||
return Event{AltUp, 0, nil}
|
||||
}
|
||||
return Event{Up, 0, nil}
|
||||
case tcell.KeyDown:
|
||||
if alt {
|
||||
return Event{AltDown, 0, nil}
|
||||
}
|
||||
return Event{Down, 0, nil}
|
||||
case tcell.KeyLeft:
|
||||
if alt {
|
||||
return Event{AltLeft, 0, nil}
|
||||
}
|
||||
return Event{Left, 0, nil}
|
||||
case tcell.KeyRight:
|
||||
if alt {
|
||||
return Event{AltRight, 0, nil}
|
||||
}
|
||||
return Event{Right, 0, nil}
|
||||
|
||||
case tcell.KeyInsert:
|
||||
return Event{Insert, 0, nil}
|
||||
case tcell.KeyHome:
|
||||
return Event{Home, 0, nil}
|
||||
case tcell.KeyDelete:
|
||||
return Event{Del, 0, nil}
|
||||
case tcell.KeyEnd:
|
||||
return Event{End, 0, nil}
|
||||
case tcell.KeyPgUp:
|
||||
return Event{PgUp, 0, nil}
|
||||
case tcell.KeyPgDn:
|
||||
return Event{PgDn, 0, nil}
|
||||
|
||||
case tcell.KeyBacktab:
|
||||
return Event{BTab, 0, nil}
|
||||
|
||||
case tcell.KeyF1:
|
||||
return Event{F1, 0, nil}
|
||||
case tcell.KeyF2:
|
||||
return Event{F2, 0, nil}
|
||||
case tcell.KeyF3:
|
||||
return Event{F3, 0, nil}
|
||||
case tcell.KeyF4:
|
||||
return Event{F4, 0, nil}
|
||||
case tcell.KeyF5:
|
||||
return Event{F5, 0, nil}
|
||||
case tcell.KeyF6:
|
||||
return Event{F6, 0, nil}
|
||||
case tcell.KeyF7:
|
||||
return Event{F7, 0, nil}
|
||||
case tcell.KeyF8:
|
||||
return Event{F8, 0, nil}
|
||||
case tcell.KeyF9:
|
||||
return Event{F9, 0, nil}
|
||||
case tcell.KeyF10:
|
||||
return Event{F10, 0, nil}
|
||||
case tcell.KeyF11:
|
||||
return Event{F11, 0, nil}
|
||||
case tcell.KeyF12:
|
||||
return Event{F12, 0, nil}
|
||||
|
||||
// ev.Ch doesn't work for some reason for space:
|
||||
case tcell.KeyRune:
|
||||
r := ev.Rune()
|
||||
if alt {
|
||||
switch r {
|
||||
case ' ':
|
||||
return Event{AltSpace, 0, nil}
|
||||
case '/':
|
||||
return Event{AltSlash, 0, nil}
|
||||
}
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return Event{AltA + int(r) - 'a', 0, nil}
|
||||
}
|
||||
if r >= '0' && r <= '9' {
|
||||
return Event{Alt0 + int(r) - '0', 0, nil}
|
||||
}
|
||||
}
|
||||
return Event{Rune, r, nil}
|
||||
|
||||
case tcell.KeyEsc:
|
||||
return Event{ESC, 0, nil}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Pause(clear bool) {
|
||||
if clear {
|
||||
_screen.Fini()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Resume(clear bool, sigcont bool) {
|
||||
if clear {
|
||||
r.initScreen()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Close() {
|
||||
_screen.Fini()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
||||
// TODO
|
||||
for _, w := range windows {
|
||||
w.Refresh()
|
||||
}
|
||||
_screen.Show()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
|
||||
normal := ColNormal
|
||||
if preview {
|
||||
normal = ColPreview
|
||||
}
|
||||
return &TcellWindow{
|
||||
color: r.theme.Colored,
|
||||
preview: preview,
|
||||
top: top,
|
||||
left: left,
|
||||
width: width,
|
||||
height: height,
|
||||
normal: normal,
|
||||
borderStyle: borderStyle}
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Close() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func fill(x, y, w, h int, n ColorPair, r rune) {
|
||||
for ly := 0; ly <= h; ly++ {
|
||||
for lx := 0; lx <= w; lx++ {
|
||||
_screen.SetContent(x+lx, y+ly, r, nil, n.style())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Erase() {
|
||||
fill(w.left-1, w.top, w.width+1, w.height, w.normal, ' ')
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Enclose(y int, x int) bool {
|
||||
return x >= w.left && x < (w.left+w.width) &&
|
||||
y >= w.top && y < (w.top+w.height)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Move(y int, x int) {
|
||||
w.lastX = x
|
||||
w.lastY = y
|
||||
w.moveCursor = true
|
||||
}
|
||||
|
||||
func (w *TcellWindow) MoveAndClear(y int, x int) {
|
||||
w.Move(y, x)
|
||||
for i := w.lastX; i < w.width; i++ {
|
||||
_screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, w.normal.style())
|
||||
}
|
||||
w.lastX = x
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Print(text string) {
|
||||
w.printString(text, w.normal)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) printString(text string, pair ColorPair) {
|
||||
t := text
|
||||
lx := 0
|
||||
a := pair.Attr()
|
||||
|
||||
style := pair.style()
|
||||
if a&AttrClear == 0 {
|
||||
style = style.
|
||||
Reverse(a&Attr(tcell.AttrReverse) != 0).
|
||||
Underline(a&Attr(tcell.AttrUnderline) != 0).
|
||||
Italic(a&Attr(tcell.AttrItalic) != 0).
|
||||
Blink(a&Attr(tcell.AttrBlink) != 0).
|
||||
Dim(a&Attr(tcell.AttrDim) != 0)
|
||||
}
|
||||
|
||||
for {
|
||||
if len(t) == 0 {
|
||||
break
|
||||
}
|
||||
r, size := utf8.DecodeRuneInString(t)
|
||||
t = t[size:]
|
||||
|
||||
if r < rune(' ') { // ignore control characters
|
||||
continue
|
||||
}
|
||||
|
||||
if r == '\n' {
|
||||
w.lastY++
|
||||
lx = 0
|
||||
} else {
|
||||
|
||||
if r == '\u000D' { // skip carriage return
|
||||
continue
|
||||
}
|
||||
|
||||
var xPos = w.left + w.lastX + lx
|
||||
var yPos = w.top + w.lastY
|
||||
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
|
||||
_screen.SetContent(xPos, yPos, r, nil, style)
|
||||
}
|
||||
lx += runewidth.RuneWidth(r)
|
||||
}
|
||||
}
|
||||
w.lastX += lx
|
||||
}
|
||||
|
||||
func (w *TcellWindow) CPrint(pair ColorPair, text string) {
|
||||
w.printString(text, pair)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||
lx := 0
|
||||
a := pair.Attr()
|
||||
|
||||
var style tcell.Style
|
||||
if w.color {
|
||||
style = pair.style()
|
||||
} else {
|
||||
style = w.normal.style()
|
||||
}
|
||||
style = style.
|
||||
Blink(a&Attr(tcell.AttrBlink) != 0).
|
||||
Bold(a&Attr(tcell.AttrBold) != 0).
|
||||
Dim(a&Attr(tcell.AttrDim) != 0).
|
||||
Reverse(a&Attr(tcell.AttrReverse) != 0).
|
||||
Underline(a&Attr(tcell.AttrUnderline) != 0).
|
||||
Italic(a&Attr(tcell.AttrItalic) != 0)
|
||||
|
||||
for _, r := range text {
|
||||
if r == '\n' {
|
||||
w.lastY++
|
||||
w.lastX = 0
|
||||
lx = 0
|
||||
} else {
|
||||
var xPos = w.left + w.lastX + lx
|
||||
|
||||
// word wrap:
|
||||
if xPos >= (w.left + w.width) {
|
||||
w.lastY++
|
||||
w.lastX = 0
|
||||
lx = 0
|
||||
xPos = w.left
|
||||
}
|
||||
var yPos = w.top + w.lastY
|
||||
|
||||
if yPos >= (w.top + w.height) {
|
||||
return FillSuspend
|
||||
}
|
||||
|
||||
_screen.SetContent(xPos, yPos, r, nil, style)
|
||||
lx += runewidth.RuneWidth(r)
|
||||
}
|
||||
}
|
||||
w.lastX += lx
|
||||
if w.lastX == w.width {
|
||||
w.lastY++
|
||||
w.lastX = 0
|
||||
return FillNextLine
|
||||
}
|
||||
|
||||
return FillContinue
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Fill(str string) FillReturn {
|
||||
return w.fillString(str, w.normal)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
|
||||
if fg == colDefault {
|
||||
fg = w.normal.Fg()
|
||||
}
|
||||
if bg == colDefault {
|
||||
bg = w.normal.Bg()
|
||||
}
|
||||
return w.fillString(str, NewColorPair(fg, bg, a))
|
||||
}
|
||||
|
||||
func (w *TcellWindow) drawBorder() {
|
||||
shape := w.borderStyle.shape
|
||||
if shape == BorderNone {
|
||||
return
|
||||
}
|
||||
|
||||
left := w.left
|
||||
right := left + w.width
|
||||
top := w.top
|
||||
bot := top + w.height
|
||||
|
||||
var style tcell.Style
|
||||
if w.color {
|
||||
if w.preview {
|
||||
style = ColPreviewBorder.style()
|
||||
} else {
|
||||
style = ColBorder.style()
|
||||
}
|
||||
} else {
|
||||
style = w.normal.style()
|
||||
}
|
||||
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderHorizontal, BorderTop:
|
||||
for x := left; x < right; x++ {
|
||||
_screen.SetContent(x, top, w.borderStyle.horizontal, nil, style)
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderHorizontal, BorderBottom:
|
||||
for x := left; x < right; x++ {
|
||||
_screen.SetContent(x, bot-1, w.borderStyle.horizontal, nil, style)
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderVertical, BorderLeft:
|
||||
for y := top; y < bot; y++ {
|
||||
_screen.SetContent(left, y, w.borderStyle.vertical, nil, style)
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp, BorderVertical, BorderRight:
|
||||
for y := top; y < bot; y++ {
|
||||
_screen.SetContent(right-1, y, w.borderStyle.vertical, nil, style)
|
||||
}
|
||||
}
|
||||
switch shape {
|
||||
case BorderRounded, BorderSharp:
|
||||
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
|
||||
_screen.SetContent(right-1, top, w.borderStyle.topRight, nil, style)
|
||||
_screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)
|
||||
_screen.SetContent(right-1, bot-1, w.borderStyle.bottomRight, nil, style)
|
||||
}
|
||||
}
|
||||
31
src/tui/ttyname_unix.go
Normal file
31
src/tui/ttyname_unix.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// +build !windows
|
||||
|
||||
package tui
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var devPrefixes = [...]string{"/dev/pts/", "/dev/"}
|
||||
|
||||
func ttyname() string {
|
||||
var stderr syscall.Stat_t
|
||||
if syscall.Fstat(2, &stderr) != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, prefix := range devPrefixes {
|
||||
files, err := ioutil.ReadDir(prefix)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if stat, ok := file.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
|
||||
return prefix + file.Name()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
7
src/tui/ttyname_windows.go
Normal file
7
src/tui/ttyname_windows.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// +build windows
|
||||
|
||||
package tui
|
||||
|
||||
func ttyname() string {
|
||||
return ""
|
||||
}
|
||||
581
src/tui/tui.go
Normal file
581
src/tui/tui.go
Normal file
@@ -0,0 +1,581 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Types of user action
|
||||
const (
|
||||
Rune = iota
|
||||
|
||||
CtrlA
|
||||
CtrlB
|
||||
CtrlC
|
||||
CtrlD
|
||||
CtrlE
|
||||
CtrlF
|
||||
CtrlG
|
||||
CtrlH
|
||||
Tab
|
||||
CtrlJ
|
||||
CtrlK
|
||||
CtrlL
|
||||
CtrlM
|
||||
CtrlN
|
||||
CtrlO
|
||||
CtrlP
|
||||
CtrlQ
|
||||
CtrlR
|
||||
CtrlS
|
||||
CtrlT
|
||||
CtrlU
|
||||
CtrlV
|
||||
CtrlW
|
||||
CtrlX
|
||||
CtrlY
|
||||
CtrlZ
|
||||
ESC
|
||||
CtrlSpace
|
||||
|
||||
// https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
|
||||
CtrlBackSlash
|
||||
CtrlRightBracket
|
||||
CtrlCaret
|
||||
CtrlSlash
|
||||
|
||||
Invalid
|
||||
Resize
|
||||
Mouse
|
||||
DoubleClick
|
||||
LeftClick
|
||||
RightClick
|
||||
|
||||
BTab
|
||||
BSpace
|
||||
|
||||
Del
|
||||
PgUp
|
||||
PgDn
|
||||
|
||||
Up
|
||||
Down
|
||||
Left
|
||||
Right
|
||||
Home
|
||||
End
|
||||
Insert
|
||||
|
||||
SUp
|
||||
SDown
|
||||
SLeft
|
||||
SRight
|
||||
|
||||
F1
|
||||
F2
|
||||
F3
|
||||
F4
|
||||
F5
|
||||
F6
|
||||
F7
|
||||
F8
|
||||
F9
|
||||
F10
|
||||
F11
|
||||
F12
|
||||
|
||||
Change
|
||||
BackwardEOF
|
||||
|
||||
AltSpace
|
||||
AltSlash
|
||||
AltBS
|
||||
|
||||
AltUp
|
||||
AltDown
|
||||
AltLeft
|
||||
AltRight
|
||||
|
||||
Alt0
|
||||
)
|
||||
|
||||
const ( // Reset iota
|
||||
AltA = Alt0 + 'a' - '0' + iota
|
||||
AltB
|
||||
AltC
|
||||
AltD
|
||||
AltE
|
||||
AltF
|
||||
AltZ = AltA + 'z' - 'a'
|
||||
CtrlAltA = AltZ + 1
|
||||
CtrlAltM = CtrlAltA + 'm' - 'a'
|
||||
)
|
||||
|
||||
const (
|
||||
doubleClickDuration = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
type Color int32
|
||||
|
||||
func (c Color) is24() bool {
|
||||
return c > 0 && (c&(1<<24)) > 0
|
||||
}
|
||||
|
||||
type ColorAttr struct {
|
||||
Color Color
|
||||
Attr Attr
|
||||
}
|
||||
|
||||
func NewColorAttr() ColorAttr {
|
||||
return ColorAttr{Color: colUndefined, Attr: AttrUndefined}
|
||||
}
|
||||
|
||||
const (
|
||||
colUndefined Color = -2
|
||||
colDefault Color = -1
|
||||
)
|
||||
|
||||
const (
|
||||
colBlack Color = iota
|
||||
colRed
|
||||
colGreen
|
||||
colYellow
|
||||
colBlue
|
||||
colMagenta
|
||||
colCyan
|
||||
colWhite
|
||||
)
|
||||
|
||||
type FillReturn int
|
||||
|
||||
const (
|
||||
FillContinue FillReturn = iota
|
||||
FillNextLine
|
||||
FillSuspend
|
||||
)
|
||||
|
||||
type ColorPair struct {
|
||||
fg Color
|
||||
bg Color
|
||||
attr Attr
|
||||
}
|
||||
|
||||
func HexToColor(rrggbb string) Color {
|
||||
r, _ := strconv.ParseInt(rrggbb[1:3], 16, 0)
|
||||
g, _ := strconv.ParseInt(rrggbb[3:5], 16, 0)
|
||||
b, _ := strconv.ParseInt(rrggbb[5:7], 16, 0)
|
||||
return Color((1 << 24) + (r << 16) + (g << 8) + b)
|
||||
}
|
||||
|
||||
func NewColorPair(fg Color, bg Color, attr Attr) ColorPair {
|
||||
return ColorPair{fg, bg, attr}
|
||||
}
|
||||
|
||||
func (p ColorPair) Fg() Color {
|
||||
return p.fg
|
||||
}
|
||||
|
||||
func (p ColorPair) Bg() Color {
|
||||
return p.bg
|
||||
}
|
||||
|
||||
func (p ColorPair) Attr() Attr {
|
||||
return p.attr
|
||||
}
|
||||
|
||||
func (p ColorPair) merge(other ColorPair, except Color) ColorPair {
|
||||
dup := p
|
||||
dup.attr = dup.attr.Merge(other.attr)
|
||||
if other.fg != except {
|
||||
dup.fg = other.fg
|
||||
}
|
||||
if other.bg != except {
|
||||
dup.bg = other.bg
|
||||
}
|
||||
return dup
|
||||
}
|
||||
|
||||
func (p ColorPair) WithAttr(attr Attr) ColorPair {
|
||||
dup := p
|
||||
dup.attr = dup.attr.Merge(attr)
|
||||
return dup
|
||||
}
|
||||
|
||||
func (p ColorPair) MergeAttr(other ColorPair) ColorPair {
|
||||
return p.WithAttr(other.attr)
|
||||
}
|
||||
|
||||
func (p ColorPair) Merge(other ColorPair) ColorPair {
|
||||
return p.merge(other, colUndefined)
|
||||
}
|
||||
|
||||
func (p ColorPair) MergeNonDefault(other ColorPair) ColorPair {
|
||||
return p.merge(other, colDefault)
|
||||
}
|
||||
|
||||
type ColorTheme struct {
|
||||
Colored bool
|
||||
Input ColorAttr
|
||||
Fg ColorAttr
|
||||
Bg ColorAttr
|
||||
PreviewFg ColorAttr
|
||||
PreviewBg ColorAttr
|
||||
DarkBg ColorAttr
|
||||
Gutter ColorAttr
|
||||
Prompt ColorAttr
|
||||
Match ColorAttr
|
||||
Current ColorAttr
|
||||
CurrentMatch ColorAttr
|
||||
Spinner ColorAttr
|
||||
Info ColorAttr
|
||||
Cursor ColorAttr
|
||||
Selected ColorAttr
|
||||
Header ColorAttr
|
||||
Border ColorAttr
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
Type int
|
||||
Char rune
|
||||
MouseEvent *MouseEvent
|
||||
}
|
||||
|
||||
type MouseEvent struct {
|
||||
Y int
|
||||
X int
|
||||
S int
|
||||
Left bool
|
||||
Down bool
|
||||
Double bool
|
||||
Mod bool
|
||||
}
|
||||
|
||||
type BorderShape int
|
||||
|
||||
const (
|
||||
BorderNone BorderShape = iota
|
||||
BorderRounded
|
||||
BorderSharp
|
||||
BorderHorizontal
|
||||
BorderVertical
|
||||
BorderTop
|
||||
BorderBottom
|
||||
BorderLeft
|
||||
BorderRight
|
||||
)
|
||||
|
||||
type BorderStyle struct {
|
||||
shape BorderShape
|
||||
horizontal rune
|
||||
vertical rune
|
||||
topLeft rune
|
||||
topRight rune
|
||||
bottomLeft rune
|
||||
bottomRight rune
|
||||
}
|
||||
|
||||
type BorderCharacter int
|
||||
|
||||
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
if unicode {
|
||||
if shape == BorderRounded {
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
horizontal: '─',
|
||||
vertical: '│',
|
||||
topLeft: '╭',
|
||||
topRight: '╮',
|
||||
bottomLeft: '╰',
|
||||
bottomRight: '╯',
|
||||
}
|
||||
}
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
horizontal: '─',
|
||||
vertical: '│',
|
||||
topLeft: '┌',
|
||||
topRight: '┐',
|
||||
bottomLeft: '└',
|
||||
bottomRight: '┘',
|
||||
}
|
||||
}
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
horizontal: '-',
|
||||
vertical: '|',
|
||||
topLeft: '+',
|
||||
topRight: '+',
|
||||
bottomLeft: '+',
|
||||
bottomRight: '+',
|
||||
}
|
||||
}
|
||||
|
||||
func MakeTransparentBorder() BorderStyle {
|
||||
return BorderStyle{
|
||||
shape: BorderRounded,
|
||||
horizontal: ' ',
|
||||
vertical: ' ',
|
||||
topLeft: ' ',
|
||||
topRight: ' ',
|
||||
bottomLeft: ' ',
|
||||
bottomRight: ' '}
|
||||
}
|
||||
|
||||
type Renderer interface {
|
||||
Init()
|
||||
Pause(clear bool)
|
||||
Resume(clear bool, sigcont bool)
|
||||
Clear()
|
||||
RefreshWindows(windows []Window)
|
||||
Refresh()
|
||||
Close()
|
||||
|
||||
GetChar() Event
|
||||
|
||||
MaxX() int
|
||||
MaxY() int
|
||||
|
||||
NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window
|
||||
}
|
||||
|
||||
type Window interface {
|
||||
Top() int
|
||||
Left() int
|
||||
Width() int
|
||||
Height() int
|
||||
|
||||
Refresh()
|
||||
FinishFill()
|
||||
Close()
|
||||
|
||||
X() int
|
||||
Y() int
|
||||
Enclose(y int, x int) bool
|
||||
|
||||
Move(y int, x int)
|
||||
MoveAndClear(y int, x int)
|
||||
Print(text string)
|
||||
CPrint(color ColorPair, text string)
|
||||
Fill(text string) FillReturn
|
||||
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
|
||||
Erase()
|
||||
}
|
||||
|
||||
type FullscreenRenderer struct {
|
||||
theme *ColorTheme
|
||||
mouse bool
|
||||
forceBlack bool
|
||||
prevDownTime time.Time
|
||||
clickY []int
|
||||
}
|
||||
|
||||
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
|
||||
r := &FullscreenRenderer{
|
||||
theme: theme,
|
||||
mouse: mouse,
|
||||
forceBlack: forceBlack,
|
||||
prevDownTime: time.Unix(0, 0),
|
||||
clickY: []int{}}
|
||||
return r
|
||||
}
|
||||
|
||||
var (
|
||||
Default16 *ColorTheme
|
||||
Dark256 *ColorTheme
|
||||
Light256 *ColorTheme
|
||||
|
||||
ColPrompt ColorPair
|
||||
ColNormal ColorPair
|
||||
ColInput ColorPair
|
||||
ColMatch ColorPair
|
||||
ColCursor ColorPair
|
||||
ColCursorEmpty ColorPair
|
||||
ColSelected ColorPair
|
||||
ColCurrent ColorPair
|
||||
ColCurrentMatch ColorPair
|
||||
ColCurrentCursor ColorPair
|
||||
ColCurrentCursorEmpty ColorPair
|
||||
ColCurrentSelected ColorPair
|
||||
ColCurrentSelectedEmpty ColorPair
|
||||
ColSpinner ColorPair
|
||||
ColInfo ColorPair
|
||||
ColHeader ColorPair
|
||||
ColBorder ColorPair
|
||||
ColPreview ColorPair
|
||||
ColPreviewBorder ColorPair
|
||||
)
|
||||
|
||||
func EmptyTheme() *ColorTheme {
|
||||
return &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colUndefined, AttrUndefined},
|
||||
Fg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Bg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
Prompt: ColorAttr{colUndefined, AttrUndefined},
|
||||
Match: ColorAttr{colUndefined, AttrUndefined},
|
||||
Current: ColorAttr{colUndefined, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
Spinner: ColorAttr{colUndefined, AttrUndefined},
|
||||
Info: ColorAttr{colUndefined, AttrUndefined},
|
||||
Cursor: ColorAttr{colUndefined, AttrUndefined},
|
||||
Selected: ColorAttr{colUndefined, AttrUndefined},
|
||||
Header: ColorAttr{colUndefined, AttrUndefined},
|
||||
Border: ColorAttr{colUndefined, AttrUndefined}}
|
||||
}
|
||||
|
||||
func NoColorTheme() *ColorTheme {
|
||||
return &ColorTheme{
|
||||
Colored: false,
|
||||
Input: ColorAttr{colDefault, AttrRegular},
|
||||
Fg: ColorAttr{colDefault, AttrRegular},
|
||||
Bg: ColorAttr{colDefault, AttrRegular},
|
||||
PreviewFg: ColorAttr{colDefault, AttrRegular},
|
||||
PreviewBg: ColorAttr{colDefault, AttrRegular},
|
||||
DarkBg: ColorAttr{colDefault, AttrRegular},
|
||||
Gutter: ColorAttr{colDefault, AttrRegular},
|
||||
Prompt: ColorAttr{colDefault, AttrRegular},
|
||||
Match: ColorAttr{colDefault, Underline},
|
||||
Current: ColorAttr{colDefault, Reverse},
|
||||
CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
|
||||
Spinner: ColorAttr{colDefault, AttrRegular},
|
||||
Info: ColorAttr{colDefault, AttrRegular},
|
||||
Cursor: ColorAttr{colDefault, AttrRegular},
|
||||
Selected: ColorAttr{colDefault, AttrRegular},
|
||||
Header: ColorAttr{colDefault, AttrRegular},
|
||||
Border: ColorAttr{colDefault, AttrRegular}}
|
||||
}
|
||||
|
||||
func errorExit(message string) {
|
||||
fmt.Fprintln(os.Stderr, message)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func init() {
|
||||
Default16 = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{colBlack, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
Prompt: ColorAttr{colBlue, AttrUndefined},
|
||||
Match: ColorAttr{colGreen, AttrUndefined},
|
||||
Current: ColorAttr{colYellow, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{colGreen, AttrUndefined},
|
||||
Spinner: ColorAttr{colGreen, AttrUndefined},
|
||||
Info: ColorAttr{colWhite, AttrUndefined},
|
||||
Cursor: ColorAttr{colRed, AttrUndefined},
|
||||
Selected: ColorAttr{colMagenta, AttrUndefined},
|
||||
Header: ColorAttr{colCyan, AttrUndefined},
|
||||
Border: ColorAttr{colBlack, AttrUndefined}}
|
||||
Dark256 = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{236, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
Prompt: ColorAttr{110, AttrUndefined},
|
||||
Match: ColorAttr{108, AttrUndefined},
|
||||
Current: ColorAttr{254, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{151, AttrUndefined},
|
||||
Spinner: ColorAttr{148, AttrUndefined},
|
||||
Info: ColorAttr{144, AttrUndefined},
|
||||
Cursor: ColorAttr{161, AttrUndefined},
|
||||
Selected: ColorAttr{168, AttrUndefined},
|
||||
Header: ColorAttr{109, AttrUndefined},
|
||||
Border: ColorAttr{59, AttrUndefined}}
|
||||
Light256 = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{251, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
Prompt: ColorAttr{25, AttrUndefined},
|
||||
Match: ColorAttr{66, AttrUndefined},
|
||||
Current: ColorAttr{237, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{23, AttrUndefined},
|
||||
Spinner: ColorAttr{65, AttrUndefined},
|
||||
Info: ColorAttr{101, AttrUndefined},
|
||||
Cursor: ColorAttr{161, AttrUndefined},
|
||||
Selected: ColorAttr{168, AttrUndefined},
|
||||
Header: ColorAttr{31, AttrUndefined},
|
||||
Border: ColorAttr{145, AttrUndefined}}
|
||||
}
|
||||
|
||||
func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
|
||||
if forceBlack {
|
||||
theme.Bg = ColorAttr{colBlack, AttrUndefined}
|
||||
}
|
||||
|
||||
o := func(a ColorAttr, b ColorAttr) ColorAttr {
|
||||
c := a
|
||||
if b.Color != colUndefined {
|
||||
c.Color = b.Color
|
||||
}
|
||||
if b.Attr != AttrUndefined {
|
||||
c.Attr = b.Attr
|
||||
}
|
||||
return c
|
||||
}
|
||||
theme.Input = o(baseTheme.Input, theme.Input)
|
||||
theme.Fg = o(baseTheme.Fg, theme.Fg)
|
||||
theme.Bg = o(baseTheme.Bg, theme.Bg)
|
||||
theme.PreviewFg = o(theme.Fg, o(baseTheme.PreviewFg, theme.PreviewFg))
|
||||
theme.PreviewBg = o(theme.Bg, o(baseTheme.PreviewBg, theme.PreviewBg))
|
||||
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
|
||||
theme.Gutter = o(theme.DarkBg, o(baseTheme.Gutter, theme.Gutter))
|
||||
theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
|
||||
theme.Match = o(baseTheme.Match, theme.Match)
|
||||
theme.Current = o(baseTheme.Current, theme.Current)
|
||||
theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch)
|
||||
theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
|
||||
theme.Info = o(baseTheme.Info, theme.Info)
|
||||
theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
|
||||
theme.Selected = o(baseTheme.Selected, theme.Selected)
|
||||
theme.Header = o(baseTheme.Header, theme.Header)
|
||||
theme.Border = o(baseTheme.Border, theme.Border)
|
||||
|
||||
initPalette(theme)
|
||||
}
|
||||
|
||||
func initPalette(theme *ColorTheme) {
|
||||
pair := func(fg, bg ColorAttr) ColorPair {
|
||||
return ColorPair{fg.Color, bg.Color, fg.Attr}
|
||||
}
|
||||
blank := theme.Fg
|
||||
blank.Attr = AttrRegular
|
||||
|
||||
ColPrompt = pair(theme.Prompt, theme.Bg)
|
||||
ColNormal = pair(theme.Fg, theme.Bg)
|
||||
ColInput = pair(theme.Input, theme.Bg)
|
||||
ColMatch = pair(theme.Match, theme.Bg)
|
||||
ColCursor = pair(theme.Cursor, theme.Gutter)
|
||||
ColCursorEmpty = pair(blank, theme.Gutter)
|
||||
ColSelected = pair(theme.Selected, theme.Gutter)
|
||||
ColCurrent = pair(theme.Current, theme.DarkBg)
|
||||
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
|
||||
ColCurrentCursor = pair(theme.Cursor, theme.DarkBg)
|
||||
ColCurrentCursorEmpty = pair(blank, theme.DarkBg)
|
||||
ColCurrentSelected = pair(theme.Selected, theme.DarkBg)
|
||||
ColCurrentSelectedEmpty = pair(blank, theme.DarkBg)
|
||||
ColSpinner = pair(theme.Spinner, theme.Bg)
|
||||
ColInfo = pair(theme.Info, theme.Bg)
|
||||
ColHeader = pair(theme.Header, theme.Bg)
|
||||
ColBorder = pair(theme.Border, theme.Bg)
|
||||
ColPreview = pair(theme.PreviewFg, theme.PreviewBg)
|
||||
ColPreviewBorder = pair(theme.Border, theme.PreviewBg)
|
||||
}
|
||||
20
src/tui/tui_test.go
Normal file
20
src/tui/tui_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package tui
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestHexToColor(t *testing.T) {
|
||||
assert := func(expr string, r, g, b int) {
|
||||
color := HexToColor(expr)
|
||||
if !color.is24() ||
|
||||
int((color>>16)&0xff) != r ||
|
||||
int((color>>8)&0xff) != g ||
|
||||
int((color)&0xff) != b {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
assert("#ff0000", 255, 0, 0)
|
||||
assert("#010203", 1, 2, 3)
|
||||
assert("#102030", 16, 32, 48)
|
||||
assert("#ffffff", 255, 255, 255)
|
||||
}
|
||||
34
src/util/atomicbool.go
Normal file
34
src/util/atomicbool.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
func convertBoolToInt32(b bool) int32 {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// AtomicBool is a boxed-class that provides synchronized access to the
|
||||
// underlying boolean value
|
||||
type AtomicBool struct {
|
||||
state int32 // "1" is true, "0" is false
|
||||
}
|
||||
|
||||
// NewAtomicBool returns a new AtomicBool
|
||||
func NewAtomicBool(initialState bool) *AtomicBool {
|
||||
return &AtomicBool{state: convertBoolToInt32(initialState)}
|
||||
}
|
||||
|
||||
// Get returns the current boolean value synchronously
|
||||
func (a *AtomicBool) Get() bool {
|
||||
return atomic.LoadInt32(&a.state) == 1
|
||||
}
|
||||
|
||||
// Set updates the boolean value synchronously
|
||||
func (a *AtomicBool) Set(newState bool) bool {
|
||||
atomic.StoreInt32(&a.state, convertBoolToInt32(newState))
|
||||
return newState
|
||||
}
|
||||
17
src/util/atomicbool_test.go
Normal file
17
src/util/atomicbool_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package util
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAtomicBool(t *testing.T) {
|
||||
if !NewAtomicBool(true).Get() || NewAtomicBool(false).Get() {
|
||||
t.Error("Invalid initial value")
|
||||
}
|
||||
|
||||
ab := NewAtomicBool(true)
|
||||
if ab.Set(false) {
|
||||
t.Error("Invalid return value")
|
||||
}
|
||||
if ab.Get() {
|
||||
t.Error("Invalid state")
|
||||
}
|
||||
}
|
||||
198
src/util/chars.go
Normal file
198
src/util/chars.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
overflow64 uint64 = 0x8080808080808080
|
||||
overflow32 uint32 = 0x80808080
|
||||
)
|
||||
|
||||
type Chars struct {
|
||||
slice []byte // or []rune
|
||||
inBytes bool
|
||||
trimLengthKnown bool
|
||||
trimLength uint16
|
||||
|
||||
// XXX Piggybacking item index here is a horrible idea. But I'm trying to
|
||||
// minimize the memory footprint by not wasting padded spaces.
|
||||
Index int32
|
||||
}
|
||||
|
||||
func checkAscii(bytes []byte) (bool, int) {
|
||||
i := 0
|
||||
for ; i <= len(bytes)-8; i += 8 {
|
||||
if (overflow64 & *(*uint64)(unsafe.Pointer(&bytes[i]))) > 0 {
|
||||
return false, i
|
||||
}
|
||||
}
|
||||
for ; i <= len(bytes)-4; i += 4 {
|
||||
if (overflow32 & *(*uint32)(unsafe.Pointer(&bytes[i]))) > 0 {
|
||||
return false, i
|
||||
}
|
||||
}
|
||||
for ; i < len(bytes); i++ {
|
||||
if bytes[i] >= utf8.RuneSelf {
|
||||
return false, i
|
||||
}
|
||||
}
|
||||
return true, 0
|
||||
}
|
||||
|
||||
// ToChars converts byte array into rune array
|
||||
func ToChars(bytes []byte) Chars {
|
||||
inBytes, bytesUntil := checkAscii(bytes)
|
||||
if inBytes {
|
||||
return Chars{slice: bytes, inBytes: inBytes}
|
||||
}
|
||||
|
||||
runes := make([]rune, bytesUntil, len(bytes))
|
||||
for i := 0; i < bytesUntil; i++ {
|
||||
runes[i] = rune(bytes[i])
|
||||
}
|
||||
for i := bytesUntil; i < len(bytes); {
|
||||
r, sz := utf8.DecodeRune(bytes[i:])
|
||||
i += sz
|
||||
runes = append(runes, r)
|
||||
}
|
||||
return RunesToChars(runes)
|
||||
}
|
||||
|
||||
func RunesToChars(runes []rune) Chars {
|
||||
return Chars{slice: *(*[]byte)(unsafe.Pointer(&runes)), inBytes: false}
|
||||
}
|
||||
|
||||
func (chars *Chars) IsBytes() bool {
|
||||
return chars.inBytes
|
||||
}
|
||||
|
||||
func (chars *Chars) Bytes() []byte {
|
||||
return chars.slice
|
||||
}
|
||||
|
||||
func (chars *Chars) optionalRunes() []rune {
|
||||
if chars.inBytes {
|
||||
return nil
|
||||
}
|
||||
return *(*[]rune)(unsafe.Pointer(&chars.slice))
|
||||
}
|
||||
|
||||
func (chars *Chars) Get(i int) rune {
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
return runes[i]
|
||||
}
|
||||
return rune(chars.slice[i])
|
||||
}
|
||||
|
||||
func (chars *Chars) Length() int {
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
return len(runes)
|
||||
}
|
||||
return len(chars.slice)
|
||||
}
|
||||
|
||||
// String returns the string representation of a Chars object.
|
||||
func (chars *Chars) String() string {
|
||||
return fmt.Sprintf("Chars{slice: []byte(%q), inBytes: %v, trimLengthKnown: %v, trimLength: %d, Index: %d}", chars.slice, chars.inBytes, chars.trimLengthKnown, chars.trimLength, chars.Index)
|
||||
}
|
||||
|
||||
// TrimLength returns the length after trimming leading and trailing whitespaces
|
||||
func (chars *Chars) TrimLength() uint16 {
|
||||
if chars.trimLengthKnown {
|
||||
return chars.trimLength
|
||||
}
|
||||
chars.trimLengthKnown = true
|
||||
var i int
|
||||
len := chars.Length()
|
||||
for i = len - 1; i >= 0; i-- {
|
||||
char := chars.Get(i)
|
||||
if !unicode.IsSpace(char) {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Completely empty
|
||||
if i < 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var j int
|
||||
for j = 0; j < len; j++ {
|
||||
char := chars.Get(j)
|
||||
if !unicode.IsSpace(char) {
|
||||
break
|
||||
}
|
||||
}
|
||||
chars.trimLength = AsUint16(i - j + 1)
|
||||
return chars.trimLength
|
||||
}
|
||||
|
||||
func (chars *Chars) LeadingWhitespaces() int {
|
||||
whitespaces := 0
|
||||
for i := 0; i < chars.Length(); i++ {
|
||||
char := chars.Get(i)
|
||||
if !unicode.IsSpace(char) {
|
||||
break
|
||||
}
|
||||
whitespaces++
|
||||
}
|
||||
return whitespaces
|
||||
}
|
||||
|
||||
func (chars *Chars) TrailingWhitespaces() int {
|
||||
whitespaces := 0
|
||||
for i := chars.Length() - 1; i >= 0; i-- {
|
||||
char := chars.Get(i)
|
||||
if !unicode.IsSpace(char) {
|
||||
break
|
||||
}
|
||||
whitespaces++
|
||||
}
|
||||
return whitespaces
|
||||
}
|
||||
|
||||
func (chars *Chars) TrimTrailingWhitespaces() {
|
||||
whitespaces := chars.TrailingWhitespaces()
|
||||
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
|
||||
}
|
||||
|
||||
func (chars *Chars) ToString() string {
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
return string(runes)
|
||||
}
|
||||
return string(chars.slice)
|
||||
}
|
||||
|
||||
func (chars *Chars) ToRunes() []rune {
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
return runes
|
||||
}
|
||||
bytes := chars.slice
|
||||
runes := make([]rune, len(bytes))
|
||||
for idx, b := range bytes {
|
||||
runes[idx] = rune(b)
|
||||
}
|
||||
return runes
|
||||
}
|
||||
|
||||
func (chars *Chars) CopyRunes(dest []rune) {
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
copy(dest, runes)
|
||||
return
|
||||
}
|
||||
for idx, b := range chars.slice[:len(dest)] {
|
||||
dest[idx] = rune(b)
|
||||
}
|
||||
}
|
||||
|
||||
func (chars *Chars) Prepend(prefix string) {
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
runes = append([]rune(prefix), runes...)
|
||||
chars.slice = *(*[]byte)(unsafe.Pointer(&runes))
|
||||
} else {
|
||||
chars.slice = append([]byte(prefix), chars.slice...)
|
||||
}
|
||||
}
|
||||
46
src/util/chars_test.go
Normal file
46
src/util/chars_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package util
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestToCharsAscii(t *testing.T) {
|
||||
chars := ToChars([]byte("foobar"))
|
||||
if !chars.inBytes || chars.ToString() != "foobar" || !chars.inBytes {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCharsLength(t *testing.T) {
|
||||
chars := ToChars([]byte("\tabc한글 "))
|
||||
if chars.inBytes || chars.Length() != 8 || chars.TrimLength() != 5 {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCharsToString(t *testing.T) {
|
||||
text := "\tabc한글 "
|
||||
chars := ToChars([]byte(text))
|
||||
if chars.ToString() != text {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrimLength(t *testing.T) {
|
||||
check := func(str string, exp uint16) {
|
||||
chars := ToChars([]byte(str))
|
||||
trimmed := chars.TrimLength()
|
||||
if trimmed != exp {
|
||||
t.Errorf("Invalid TrimLength result for '%s': %d (expected %d)",
|
||||
str, trimmed, exp)
|
||||
}
|
||||
}
|
||||
check("hello", 5)
|
||||
check("hello ", 5)
|
||||
check("hello ", 5)
|
||||
check(" hello", 5)
|
||||
check(" hello", 5)
|
||||
check(" hello ", 5)
|
||||
check(" hello ", 5)
|
||||
check("h o", 5)
|
||||
check(" h o ", 5)
|
||||
check(" ", 0)
|
||||
}
|
||||
96
src/util/eventbox.go
Normal file
96
src/util/eventbox.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package util
|
||||
|
||||
import "sync"
|
||||
|
||||
// EventType is the type for fzf events
|
||||
type EventType int
|
||||
|
||||
// Events is a type that associates EventType to any data
|
||||
type Events map[EventType]interface{}
|
||||
|
||||
// EventBox is used for coordinating events
|
||||
type EventBox struct {
|
||||
events Events
|
||||
cond *sync.Cond
|
||||
ignore map[EventType]bool
|
||||
}
|
||||
|
||||
// NewEventBox returns a new EventBox
|
||||
func NewEventBox() *EventBox {
|
||||
return &EventBox{
|
||||
events: make(Events),
|
||||
cond: sync.NewCond(&sync.Mutex{}),
|
||||
ignore: make(map[EventType]bool)}
|
||||
}
|
||||
|
||||
// Wait blocks the goroutine until signaled
|
||||
func (b *EventBox) Wait(callback func(*Events)) {
|
||||
b.cond.L.Lock()
|
||||
|
||||
if len(b.events) == 0 {
|
||||
b.cond.Wait()
|
||||
}
|
||||
|
||||
callback(&b.events)
|
||||
b.cond.L.Unlock()
|
||||
}
|
||||
|
||||
// Set turns on the event type on the box
|
||||
func (b *EventBox) Set(event EventType, value interface{}) {
|
||||
b.cond.L.Lock()
|
||||
b.events[event] = value
|
||||
if _, found := b.ignore[event]; !found {
|
||||
b.cond.Broadcast()
|
||||
}
|
||||
b.cond.L.Unlock()
|
||||
}
|
||||
|
||||
// Clear clears the events
|
||||
// Unsynchronized; should be called within Wait routine
|
||||
func (events *Events) Clear() {
|
||||
for event := range *events {
|
||||
delete(*events, event)
|
||||
}
|
||||
}
|
||||
|
||||
// Peek peeks at the event box if the given event is set
|
||||
func (b *EventBox) Peek(event EventType) bool {
|
||||
b.cond.L.Lock()
|
||||
_, ok := b.events[event]
|
||||
b.cond.L.Unlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
// Watch deletes the events from the ignore list
|
||||
func (b *EventBox) Watch(events ...EventType) {
|
||||
b.cond.L.Lock()
|
||||
for _, event := range events {
|
||||
delete(b.ignore, event)
|
||||
}
|
||||
b.cond.L.Unlock()
|
||||
}
|
||||
|
||||
// Unwatch adds the events to the ignore list
|
||||
func (b *EventBox) Unwatch(events ...EventType) {
|
||||
b.cond.L.Lock()
|
||||
for _, event := range events {
|
||||
b.ignore[event] = true
|
||||
}
|
||||
b.cond.L.Unlock()
|
||||
}
|
||||
|
||||
// WaitFor blocks the execution until the event is received
|
||||
func (b *EventBox) WaitFor(event EventType) {
|
||||
looping := true
|
||||
for looping {
|
||||
b.Wait(func(events *Events) {
|
||||
for evt := range *events {
|
||||
switch evt {
|
||||
case event:
|
||||
looping = false
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
61
src/util/eventbox_test.go
Normal file
61
src/util/eventbox_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package util
|
||||
|
||||
import "testing"
|
||||
|
||||
// fzf events
|
||||
const (
|
||||
EvtReadNew EventType = iota
|
||||
EvtReadFin
|
||||
EvtSearchNew
|
||||
EvtSearchProgress
|
||||
EvtSearchFin
|
||||
EvtClose
|
||||
)
|
||||
|
||||
func TestEventBox(t *testing.T) {
|
||||
eb := NewEventBox()
|
||||
|
||||
// Wait should return immediately
|
||||
ch := make(chan bool)
|
||||
|
||||
go func() {
|
||||
eb.Set(EvtReadNew, 10)
|
||||
ch <- true
|
||||
<-ch
|
||||
eb.Set(EvtSearchNew, 10)
|
||||
eb.Set(EvtSearchNew, 15)
|
||||
eb.Set(EvtSearchNew, 20)
|
||||
eb.Set(EvtSearchProgress, 30)
|
||||
ch <- true
|
||||
<-ch
|
||||
eb.Set(EvtSearchFin, 40)
|
||||
ch <- true
|
||||
<-ch
|
||||
}()
|
||||
|
||||
count := 0
|
||||
sum := 0
|
||||
looping := true
|
||||
for looping {
|
||||
<-ch
|
||||
eb.Wait(func(events *Events) {
|
||||
for _, value := range *events {
|
||||
switch val := value.(type) {
|
||||
case int:
|
||||
sum += val
|
||||
looping = sum < 100
|
||||
}
|
||||
}
|
||||
events.Clear()
|
||||
})
|
||||
ch <- true
|
||||
count++
|
||||
}
|
||||
|
||||
if count != 3 {
|
||||
t.Error("Invalid number of events", count)
|
||||
}
|
||||
if sum != 100 {
|
||||
t.Error("Invalid sum", sum)
|
||||
}
|
||||
}
|
||||
12
src/util/slab.go
Normal file
12
src/util/slab.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package util
|
||||
|
||||
type Slab struct {
|
||||
I16 []int16
|
||||
I32 []int32
|
||||
}
|
||||
|
||||
func MakeSlab(size16 int, size32 int) *Slab {
|
||||
return &Slab{
|
||||
I16: make([]int16, size16),
|
||||
I32: make([]int32, size32)}
|
||||
}
|
||||
124
src/util/util.go
Normal file
124
src/util/util.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
var _runeWidths = make(map[rune]int)
|
||||
|
||||
// RuneWidth returns rune width
|
||||
func RuneWidth(r rune, prefixWidth int, tabstop int) int {
|
||||
if r == '\t' {
|
||||
return tabstop - prefixWidth%tabstop
|
||||
} else if w, found := _runeWidths[r]; found {
|
||||
return w
|
||||
} else if r == '\n' || r == '\r' {
|
||||
return 1
|
||||
}
|
||||
w := runewidth.RuneWidth(r)
|
||||
_runeWidths[r] = w
|
||||
return w
|
||||
}
|
||||
|
||||
// Max returns the largest integer
|
||||
func Max(first int, second int) int {
|
||||
if first >= second {
|
||||
return first
|
||||
}
|
||||
return second
|
||||
}
|
||||
|
||||
// Max16 returns the largest integer
|
||||
func Max16(first int16, second int16) int16 {
|
||||
if first >= second {
|
||||
return first
|
||||
}
|
||||
return second
|
||||
}
|
||||
|
||||
// Max32 returns the largest 32-bit integer
|
||||
func Max32(first int32, second int32) int32 {
|
||||
if first > second {
|
||||
return first
|
||||
}
|
||||
return second
|
||||
}
|
||||
|
||||
// Min returns the smallest integer
|
||||
func Min(first int, second int) int {
|
||||
if first <= second {
|
||||
return first
|
||||
}
|
||||
return second
|
||||
}
|
||||
|
||||
// Min32 returns the smallest 32-bit integer
|
||||
func Min32(first int32, second int32) int32 {
|
||||
if first <= second {
|
||||
return first
|
||||
}
|
||||
return second
|
||||
}
|
||||
|
||||
// Constrain32 limits the given 32-bit integer with the upper and lower bounds
|
||||
func Constrain32(val int32, min int32, max int32) int32 {
|
||||
if val < min {
|
||||
return min
|
||||
}
|
||||
if val > max {
|
||||
return max
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Constrain limits the given integer with the upper and lower bounds
|
||||
func Constrain(val int, min int, max int) int {
|
||||
if val < min {
|
||||
return min
|
||||
}
|
||||
if val > max {
|
||||
return max
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func AsUint16(val int) uint16 {
|
||||
if val > math.MaxUint16 {
|
||||
return math.MaxUint16
|
||||
} else if val < 0 {
|
||||
return 0
|
||||
}
|
||||
return uint16(val)
|
||||
}
|
||||
|
||||
// DurWithin limits the given time.Duration with the upper and lower bounds
|
||||
func DurWithin(
|
||||
val time.Duration, min time.Duration, max time.Duration) time.Duration {
|
||||
if val < min {
|
||||
return min
|
||||
}
|
||||
if val > max {
|
||||
return max
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// IsTty returns true is stdin is a terminal
|
||||
func IsTty() bool {
|
||||
return isatty.IsTerminal(os.Stdin.Fd())
|
||||
}
|
||||
|
||||
// Once returns a function that returns the specified boolean value only once
|
||||
func Once(nextResponse bool) func() bool {
|
||||
state := nextResponse
|
||||
return func() bool {
|
||||
prevState := state
|
||||
state = false
|
||||
return prevState
|
||||
}
|
||||
}
|
||||
40
src/util/util_test.go
Normal file
40
src/util/util_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package util
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMax(t *testing.T) {
|
||||
if Max(-2, 5) != 5 {
|
||||
t.Error("Invalid result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContrain(t *testing.T) {
|
||||
if Constrain(-3, -1, 3) != -1 {
|
||||
t.Error("Expected", -1)
|
||||
}
|
||||
if Constrain(2, -1, 3) != 2 {
|
||||
t.Error("Expected", 2)
|
||||
}
|
||||
|
||||
if Constrain(5, -1, 3) != 3 {
|
||||
t.Error("Expected", 3)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnce(t *testing.T) {
|
||||
o := Once(false)
|
||||
if o() {
|
||||
t.Error("Expected: false")
|
||||
}
|
||||
if o() {
|
||||
t.Error("Expected: false")
|
||||
}
|
||||
|
||||
o = Once(true)
|
||||
if !o() {
|
||||
t.Error("Expected: true")
|
||||
}
|
||||
if o() {
|
||||
t.Error("Expected: false")
|
||||
}
|
||||
}
|
||||
47
src/util/util_unix.go
Normal file
47
src/util/util_unix.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// +build !windows
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// ExecCommand executes the given command with $SHELL
|
||||
func ExecCommand(command string, setpgid bool) *exec.Cmd {
|
||||
shell := os.Getenv("SHELL")
|
||||
if len(shell) == 0 {
|
||||
shell = "sh"
|
||||
}
|
||||
return ExecCommandWith(shell, command, setpgid)
|
||||
}
|
||||
|
||||
// ExecCommandWith executes the given command with the specified shell
|
||||
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
|
||||
cmd := exec.Command(shell, "-c", command)
|
||||
if setpgid {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// KillCommand kills the process for the given command
|
||||
func KillCommand(cmd *exec.Cmd) error {
|
||||
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||
}
|
||||
|
||||
// IsWindows returns true on Windows
|
||||
func IsWindows() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// SetNonblock executes syscall.SetNonblock on file descriptor
|
||||
func SetNonblock(file *os.File, nonblock bool) {
|
||||
syscall.SetNonblock(int(file.Fd()), nonblock)
|
||||
}
|
||||
|
||||
// Read executes syscall.Read on file descriptor
|
||||
func Read(fd int, b []byte) (int, error) {
|
||||
return syscall.Read(int(fd), b)
|
||||
}
|
||||
49
src/util/util_windows.go
Normal file
49
src/util/util_windows.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// +build windows
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// ExecCommand executes the given command with cmd
|
||||
func ExecCommand(command string, setpgid bool) *exec.Cmd {
|
||||
return ExecCommandWith("cmd", command, setpgid)
|
||||
}
|
||||
|
||||
// ExecCommandWith executes the given command with cmd. _shell parameter is
|
||||
// ignored on Windows.
|
||||
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
|
||||
// can kill preview process with its child processes at once.
|
||||
func ExecCommandWith(_shell string, command string, setpgid bool) *exec.Cmd {
|
||||
cmd := exec.Command("cmd")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: false,
|
||||
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command),
|
||||
CreationFlags: 0,
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// KillCommand kills the process for the given command
|
||||
func KillCommand(cmd *exec.Cmd) error {
|
||||
return cmd.Process.Kill()
|
||||
}
|
||||
|
||||
// IsWindows returns true on Windows
|
||||
func IsWindows() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SetNonblock executes syscall.SetNonblock on file descriptor
|
||||
func SetNonblock(file *os.File, nonblock bool) {
|
||||
syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock)
|
||||
}
|
||||
|
||||
// Read executes syscall.Read on file descriptor
|
||||
func Read(fd int, b []byte) (int, error) {
|
||||
return syscall.Read(syscall.Handle(fd), b)
|
||||
}
|
||||
175
test/fzf.vader
Normal file
175
test/fzf.vader
Normal file
@@ -0,0 +1,175 @@
|
||||
Execute (Setup):
|
||||
let g:dir = fnamemodify(g:vader_file, ':p:h')
|
||||
unlet! g:fzf_layout g:fzf_action g:fzf_history_dir
|
||||
Log 'Test directory: ' . g:dir
|
||||
Save &acd
|
||||
|
||||
Execute (fzf#run with dir option):
|
||||
let cwd = getcwd()
|
||||
let result = fzf#run({ 'source': 'git ls-files', 'options': '--filter=vdr', 'dir': g:dir })
|
||||
AssertEqual ['fzf.vader'], result
|
||||
AssertEqual 0, haslocaldir()
|
||||
AssertEqual getcwd(), cwd
|
||||
|
||||
execute 'lcd' fnameescape(cwd)
|
||||
let result = sort(fzf#run({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir }))
|
||||
AssertEqual ['fzf.vader', 'test_go.rb'], result
|
||||
AssertEqual 1, haslocaldir()
|
||||
AssertEqual getcwd(), cwd
|
||||
|
||||
Execute (fzf#run with Funcref command):
|
||||
let g:ret = []
|
||||
function! g:FzfTest(e)
|
||||
call add(g:ret, a:e)
|
||||
endfunction
|
||||
let result = sort(fzf#run({ 'source': 'git ls-files', 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
|
||||
AssertEqual ['fzf.vader', 'test_go.rb'], result
|
||||
AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret)
|
||||
|
||||
Execute (fzf#run with string source):
|
||||
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))
|
||||
AssertEqual ['hi'], result
|
||||
|
||||
Execute (fzf#run with list source):
|
||||
let result = sort(fzf#run({ 'source': ['hello', 'world'], 'options': '-f e' }))
|
||||
AssertEqual ['hello'], result
|
||||
let result = sort(fzf#run({ 'source': ['hello', 'world'], 'options': '-f o' }))
|
||||
AssertEqual ['hello', 'world'], result
|
||||
|
||||
Execute (fzf#run with string source):
|
||||
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))
|
||||
AssertEqual ['hi'], result
|
||||
|
||||
Execute (fzf#run with dir option and noautochdir):
|
||||
set noacd
|
||||
let cwd = getcwd()
|
||||
call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'})
|
||||
" No change in working directory
|
||||
AssertEqual cwd, getcwd()
|
||||
|
||||
call fzf#run({'source': ['/foobar'], 'sink': 'tabe', 'dir': '/tmp', 'options': '-1'})
|
||||
AssertEqual cwd, getcwd()
|
||||
tabclose
|
||||
AssertEqual cwd, getcwd()
|
||||
|
||||
Execute (Incomplete fzf#run with dir option and autochdir):
|
||||
set acd
|
||||
let cwd = getcwd()
|
||||
call fzf#run({'source': [], 'sink': 'e', 'dir': '/tmp', 'options': '-0'})
|
||||
" No change in working directory even if &acd is set
|
||||
AssertEqual cwd, getcwd()
|
||||
|
||||
Execute (FIXME: fzf#run with dir option and autochdir):
|
||||
set acd
|
||||
call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'})
|
||||
" Working directory changed due to &acd
|
||||
AssertEqual '/foobar', expand('%')
|
||||
AssertEqual '/', getcwd()
|
||||
|
||||
Execute (fzf#run with dir option and autochdir when final cwd is same as dir):
|
||||
set acd
|
||||
cd /tmp
|
||||
call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/', 'options': '-1'})
|
||||
" Working directory changed due to &acd
|
||||
AssertEqual '/', getcwd()
|
||||
|
||||
Execute (fzf#wrap):
|
||||
AssertThrows fzf#wrap({'foo': 'bar'})
|
||||
|
||||
let opts = fzf#wrap('foobar')
|
||||
Log opts
|
||||
AssertEqual '~40%', opts.down
|
||||
Assert opts.options =~ '--expect='
|
||||
Assert !has_key(opts, 'sink')
|
||||
Assert has_key(opts, 'sink*')
|
||||
|
||||
let opts = fzf#wrap('foobar', {}, 0)
|
||||
Log opts
|
||||
AssertEqual '~40%', opts.down
|
||||
|
||||
let opts = fzf#wrap('foobar', {}, 1)
|
||||
Log opts
|
||||
Assert !has_key(opts, 'down')
|
||||
|
||||
let opts = fzf#wrap('foobar', {'down': '50%'})
|
||||
Log opts
|
||||
AssertEqual '50%', opts.down
|
||||
|
||||
let opts = fzf#wrap('foobar', {'down': '50%'}, 1)
|
||||
Log opts
|
||||
Assert !has_key(opts, 'down')
|
||||
|
||||
let opts = fzf#wrap('foobar', {'sink': 'e'})
|
||||
Log opts
|
||||
AssertEqual 'e', opts.sink
|
||||
Assert !has_key(opts, 'sink*')
|
||||
|
||||
let opts = fzf#wrap('foobar', {'options': '--reverse'})
|
||||
Log opts
|
||||
Assert opts.options =~ '--expect='
|
||||
Assert opts.options =~ '--reverse'
|
||||
|
||||
let g:fzf_layout = {'window': 'enew'}
|
||||
let opts = fzf#wrap('foobar')
|
||||
Log opts
|
||||
AssertEqual 'enew', opts.window
|
||||
|
||||
let opts = fzf#wrap('foobar', {}, 1)
|
||||
Log opts
|
||||
Assert !has_key(opts, 'window')
|
||||
|
||||
let opts = fzf#wrap('foobar', {'right': '50%'})
|
||||
Log opts
|
||||
Assert !has_key(opts, 'window')
|
||||
AssertEqual '50%', opts.right
|
||||
|
||||
let opts = fzf#wrap('foobar', {'right': '50%'}, 1)
|
||||
Log opts
|
||||
Assert !has_key(opts, 'window')
|
||||
Assert !has_key(opts, 'right')
|
||||
|
||||
let g:fzf_action = {'a': 'tabe'}
|
||||
let opts = fzf#wrap('foobar')
|
||||
Log opts
|
||||
Assert opts.options =~ '--expect=a'
|
||||
Assert !has_key(opts, 'sink')
|
||||
Assert has_key(opts, 'sink*')
|
||||
|
||||
let opts = fzf#wrap('foobar', {'sink': 'e'})
|
||||
Log opts
|
||||
AssertEqual 'e', opts.sink
|
||||
Assert !has_key(opts, 'sink*')
|
||||
|
||||
let g:fzf_history_dir = '/tmp'
|
||||
let opts = fzf#wrap('foobar', {'options': '--color light'})
|
||||
Log opts
|
||||
Assert opts.options =~ "--history '/tmp/foobar'"
|
||||
Assert opts.options =~ '--color light'
|
||||
|
||||
let g:fzf_colors = { 'fg': ['fg', 'Error'] }
|
||||
let opts = fzf#wrap({})
|
||||
Assert opts.options =~ '^--color=fg:'
|
||||
|
||||
Execute (fzf#shellescape with sh):
|
||||
AssertEqual '''''', fzf#shellescape('', 'sh')
|
||||
AssertEqual '''\''', fzf#shellescape('\', 'sh')
|
||||
AssertEqual '''""''', fzf#shellescape('""', 'sh')
|
||||
AssertEqual '''foobar>''', fzf#shellescape('foobar>', 'sh')
|
||||
AssertEqual '''\\\"\\\''', fzf#shellescape('\\\"\\\', 'sh')
|
||||
AssertEqual '''echo ''\''''a''\'''' && echo ''\''''b''\''''''', fzf#shellescape('echo ''a'' && echo ''b''', 'sh')
|
||||
|
||||
Execute (fzf#shellescape with cmd.exe):
|
||||
AssertEqual '^"^"', fzf#shellescape('', 'cmd.exe')
|
||||
AssertEqual '^"\\^"', fzf#shellescape('\', 'cmd.exe')
|
||||
AssertEqual '^"\^"\^"^"', fzf#shellescape('""', 'cmd.exe')
|
||||
AssertEqual '^"foobar^>^"', fzf#shellescape('foobar>', 'cmd.exe')
|
||||
AssertEqual '^"\\\\\\\^"\\\\\\^"', fzf#shellescape('\\\"\\\', 'cmd.exe')
|
||||
AssertEqual '^"echo ''a'' ^&^& echo ''b''^"', fzf#shellescape('echo ''a'' && echo ''b''', 'cmd.exe')
|
||||
|
||||
AssertEqual '^"C:\Program Files ^(x86^)\\^"', fzf#shellescape('C:\Program Files (x86)\', 'cmd.exe')
|
||||
AssertEqual '^"C:/Program Files ^(x86^)/^"', fzf#shellescape('C:/Program Files (x86)/', 'cmd.exe')
|
||||
AssertEqual '^"%%USERPROFILE%%^"', fzf#shellescape('%USERPROFILE%', 'cmd.exe')
|
||||
|
||||
Execute (Cleanup):
|
||||
unlet g:dir
|
||||
Restore
|
||||
323
test/test_fzf.rb
323
test/test_fzf.rb
@@ -1,323 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
# encoding: utf-8
|
||||
|
||||
require 'minitest/autorun'
|
||||
$LOAD_PATH.unshift File.expand_path('../..', __FILE__)
|
||||
ENV['FZF_EXECUTABLE'] = '0'
|
||||
load 'fzf'
|
||||
|
||||
class TestFZF < MiniTest::Unit::TestCase
|
||||
def test_default_options
|
||||
fzf = FZF.new []
|
||||
assert_equal 1000, fzf.sort
|
||||
assert_equal false, fzf.multi
|
||||
assert_equal true, fzf.color
|
||||
assert_equal Regexp::IGNORECASE, fzf.rxflag
|
||||
|
||||
begin
|
||||
ENV['FZF_DEFAULT_SORT'] = '1500'
|
||||
fzf = FZF.new []
|
||||
assert_equal 1500, fzf.sort
|
||||
ensure
|
||||
ENV.delete 'FZF_DEFAULT_SORT'
|
||||
end
|
||||
end
|
||||
|
||||
def test_option_parser
|
||||
# Long opts
|
||||
fzf = FZF.new %w[--sort=2000 --no-color --multi +i]
|
||||
assert_equal 2000, fzf.sort
|
||||
assert_equal true, fzf.multi
|
||||
assert_equal false, fzf.color
|
||||
assert_equal 0, fzf.rxflag
|
||||
|
||||
# Short opts
|
||||
fzf = FZF.new %w[-s 2000 +c -m +i]
|
||||
assert_equal 2000, fzf.sort
|
||||
assert_equal true, fzf.multi
|
||||
assert_equal false, fzf.color
|
||||
assert_equal 0, fzf.rxflag
|
||||
end
|
||||
|
||||
def test_invalid_option
|
||||
[%w[-s 2000 +s], %w[yo dawg]].each do |argv|
|
||||
assert_raises(SystemExit) do
|
||||
fzf = FZF.new argv
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# FIXME Only on 1.9 or above
|
||||
def test_width
|
||||
fzf = FZF.new []
|
||||
assert_equal 5, fzf.width('abcde')
|
||||
assert_equal 4, fzf.width('한글')
|
||||
assert_equal 5, fzf.width('한글.')
|
||||
end
|
||||
|
||||
def test_trim
|
||||
fzf = FZF.new []
|
||||
assert_equal ['사.', 6], fzf.trim('가나다라마바사.', 4, true)
|
||||
assert_equal ['바사.', 5], fzf.trim('가나다라마바사.', 5, true)
|
||||
assert_equal ['바사.', 5], fzf.trim('가나다라마바사.', 6, true)
|
||||
assert_equal ['마바사.', 4], fzf.trim('가나다라마바사.', 7, true)
|
||||
assert_equal ['가나', 6], fzf.trim('가나다라마바사.', 4, false)
|
||||
assert_equal ['가나', 6], fzf.trim('가나다라마바사.', 5, false)
|
||||
assert_equal ['가나a', 6], fzf.trim('가나ab라마바사.', 5, false)
|
||||
assert_equal ['가나ab', 5], fzf.trim('가나ab라마바사.', 6, false)
|
||||
assert_equal ['가나ab', 5], fzf.trim('가나ab라마바사.', 7, false)
|
||||
end
|
||||
|
||||
def test_format
|
||||
fzf = FZF.new []
|
||||
assert_equal [['01234..', false]], fzf.format('0123456789', 7, [])
|
||||
assert_equal [['012', false], ['34', true], ['..', false]],
|
||||
fzf.format('0123456789', 7, [[3, 5]])
|
||||
assert_equal [['..56', false], ['789', true]],
|
||||
fzf.format('0123456789', 7, [[7, 10]])
|
||||
assert_equal [['..56', false], ['78', true], ['9', false]],
|
||||
fzf.format('0123456789', 7, [[7, 9]])
|
||||
|
||||
(3..5).each do |i|
|
||||
assert_equal [['..', false], ['567', true], ['89', false]],
|
||||
fzf.format('0123456789', 7, [[i, 8]])
|
||||
end
|
||||
|
||||
assert_equal [['..', false], ['345', true], ['..', false]],
|
||||
fzf.format('0123456789', 7, [[3, 6]])
|
||||
assert_equal [['012', false], ['34', true], ['..', false]],
|
||||
fzf.format('0123456789', 7, [[3, 5]])
|
||||
|
||||
# Multi-region
|
||||
assert_equal [["0", true], ["1", false], ["2", true], ["34..", false]],
|
||||
fzf.format('0123456789', 7, [[0, 1], [2, 3]])
|
||||
|
||||
assert_equal [["..", false], ["5", true], ["6", false], ["78", true], ["9", false]],
|
||||
fzf.format('0123456789', 7, [[3, 6], [7, 9]])
|
||||
|
||||
assert_equal [["..", false], ["3", true], ["4", false], ["5", true], ["..", false]],
|
||||
fzf.format('0123456789', 7, [[3, 4], [5, 6]])
|
||||
|
||||
# Multi-region Overlap
|
||||
assert_equal [["..", false], ["345", true], ["..", false]],
|
||||
fzf.format('0123456789', 7, [[4, 5], [3, 6]])
|
||||
end
|
||||
|
||||
def test_fuzzy_matcher
|
||||
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE
|
||||
list = %w[
|
||||
juice
|
||||
juiceful
|
||||
juiceless
|
||||
juicily
|
||||
juiciness
|
||||
juicy]
|
||||
assert matcher.caches.empty?
|
||||
assert_equal(
|
||||
[["juice", [[0, 1]]],
|
||||
["juiceful", [[0, 1]]],
|
||||
["juiceless", [[0, 1]]],
|
||||
["juicily", [[0, 1]]],
|
||||
["juiciness", [[0, 1]]],
|
||||
["juicy", [[0, 1]]]], matcher.match(list, 'j', '', '').sort)
|
||||
assert !matcher.caches.empty?
|
||||
assert_equal [list.object_id], matcher.caches.keys
|
||||
assert_equal 1, matcher.caches[list.object_id].length
|
||||
assert_equal 6, matcher.caches[list.object_id]['j'].length
|
||||
|
||||
assert_equal(
|
||||
[["juicily", [[0, 5]]],
|
||||
["juiciness", [[0, 5]]]], matcher.match(list, 'jii', '', '').sort)
|
||||
|
||||
assert_equal(
|
||||
[["juicily", [[2, 5]]],
|
||||
["juiciness", [[2, 5]]]], matcher.match(list, 'ii', '', '').sort)
|
||||
|
||||
assert_equal 3, matcher.caches[list.object_id].length
|
||||
assert_equal 2, matcher.caches[list.object_id]['ii'].length
|
||||
|
||||
# TODO : partial_cache
|
||||
end
|
||||
|
||||
def test_fuzzy_matcher_case_sensitive
|
||||
assert_equal [['Fruit', [[0, 5]]]],
|
||||
FZF::FuzzyMatcher.new(0).match(%w[Fruit Grapefruit], 'Fruit', '', '').sort
|
||||
|
||||
assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
|
||||
FZF::FuzzyMatcher.new(Regexp::IGNORECASE).
|
||||
match(%w[Fruit Grapefruit], 'Fruit', '', '').sort
|
||||
end
|
||||
|
||||
def test_extended_fuzzy_matcher
|
||||
matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE
|
||||
list = %w[
|
||||
juice
|
||||
juiceful
|
||||
juiceless
|
||||
juicily
|
||||
juiciness
|
||||
juicy
|
||||
_juice]
|
||||
match = proc { |q, prefix|
|
||||
matcher.match(list, q, prefix, '').sort.map { |p| [p.first, p.last.sort] }
|
||||
}
|
||||
|
||||
assert matcher.caches.empty?
|
||||
3.times do
|
||||
['y j', 'j y'].each do |pat|
|
||||
(0..pat.length - 1).each do |prefix_length|
|
||||
prefix = pat[0, prefix_length]
|
||||
assert_equal(
|
||||
[["juicily", [[0, 1], [6, 7]]],
|
||||
["juicy", [[0, 1], [4, 5]]]],
|
||||
match.call(pat, prefix))
|
||||
end
|
||||
end
|
||||
|
||||
# $
|
||||
assert_equal [["juiceful", [[7, 8]]]], match.call('l$', '')
|
||||
assert_equal [["juiceful", [[7, 8]]],
|
||||
["juiceless", [[5, 6]]],
|
||||
["juicily", [[5, 6]]]], match.call('l', '')
|
||||
|
||||
# ^
|
||||
assert_equal list.length, match.call('j', '').length
|
||||
assert_equal list.length - 1, match.call('^j', '').length
|
||||
|
||||
# !
|
||||
assert_equal 0, match.call('!j', '').length
|
||||
|
||||
# ! + ^
|
||||
assert_equal [["_juice", []]], match.call('!^j', '')
|
||||
|
||||
# ! + $
|
||||
assert_equal list.length - 1, match.call('!l$', '').length
|
||||
|
||||
# ! + f
|
||||
assert_equal [["juicy", [[4, 5]]]], match.call('y !l', '')
|
||||
|
||||
# '
|
||||
assert_equal %w[juiceful juiceless juicily],
|
||||
match.call('il', '').map { |e| e.first }
|
||||
assert_equal %w[juicily],
|
||||
match.call("'il", '').map { |e| e.first }
|
||||
assert_equal (list - %w[juicily]).sort,
|
||||
match.call("!'il", '').map { |e| e.first }.sort
|
||||
end
|
||||
assert !matcher.caches.empty?
|
||||
end
|
||||
|
||||
def test_xfuzzy_matcher_prefix_cache
|
||||
matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE
|
||||
list = %w[
|
||||
a.java
|
||||
b.java
|
||||
java.jive
|
||||
c.java$
|
||||
d.java
|
||||
]
|
||||
2.times do
|
||||
assert_equal 5, matcher.match(list, 'java', 'java', '').length
|
||||
assert_equal 3, matcher.match(list, 'java$', 'java$', '').length
|
||||
assert_equal 1, matcher.match(list, 'java$$', 'java$$', '').length
|
||||
|
||||
assert_equal 0, matcher.match(list, '!java', '!java', '').length
|
||||
assert_equal 4, matcher.match(list, '!^jav', '!^jav', '').length
|
||||
assert_equal 4, matcher.match(list, '!^java', '!^java', '').length
|
||||
assert_equal 2, matcher.match(list, '!^java !b !c', '!^java', '').length
|
||||
end
|
||||
end
|
||||
|
||||
def test_sort_by_rank
|
||||
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE
|
||||
xmatcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE
|
||||
list = %w[
|
||||
0____1
|
||||
0_____1
|
||||
01
|
||||
____0_1
|
||||
01_
|
||||
_01_
|
||||
0______1
|
||||
___01___
|
||||
]
|
||||
assert_equal(
|
||||
[["01", [[0, 2]]],
|
||||
["01_", [[0, 2]]],
|
||||
["_01_", [[1, 3]]],
|
||||
["___01___", [[3, 5]]],
|
||||
["____0_1", [[4, 7]]],
|
||||
["0____1", [[0, 6]]],
|
||||
["0_____1", [[0, 7]]],
|
||||
["0______1", [[0, 8]]]],
|
||||
FZF.new([]).sort_by_rank(matcher.match(list, '01', '', '')))
|
||||
|
||||
assert_equal(
|
||||
[["01", [[0, 1], [1, 2]]],
|
||||
["01_", [[0, 1], [1, 2]]],
|
||||
["_01_", [[1, 2], [2, 3]]],
|
||||
["0____1", [[0, 1], [5, 6]]],
|
||||
["0_____1", [[0, 1], [6, 7]]],
|
||||
["____0_1", [[4, 5], [6, 7]]],
|
||||
["0______1", [[0, 1], [7, 8]]],
|
||||
["___01___", [[3, 4], [4, 5]]]],
|
||||
FZF.new([]).sort_by_rank(xmatcher.match(list, '0 1', '', '')))
|
||||
|
||||
assert_equal(
|
||||
[["_01_", [[1, 3], [0, 4]]],
|
||||
["0____1", [[0, 6], [1, 3]]],
|
||||
["0_____1", [[0, 7], [1, 3]]],
|
||||
["0______1", [[0, 8], [1, 3]]],
|
||||
["___01___", [[3, 5], [0, 2]]],
|
||||
["____0_1", [[4, 7], [0, 2]]]],
|
||||
FZF.new([]).sort_by_rank(xmatcher.match(list, '01 __', '', '')))
|
||||
end
|
||||
|
||||
if RUBY_PLATFORM =~ /darwin/
|
||||
NFD = '한글'
|
||||
def test_nfc
|
||||
assert_equal 6, NFD.length
|
||||
assert_equal ["한글", [[0, 1], [1, 2]]],
|
||||
FZF::UConv.nfc(NFD, [[0, 3], [3, 6]])
|
||||
|
||||
nfd2 = 'before' + NFD + 'after'
|
||||
assert_equal 6 + 6 + 5, nfd2.length
|
||||
|
||||
nfc, offsets = FZF::UConv.nfc(nfd2, [[4, 14], [9, 13]])
|
||||
o1, o2 = offsets
|
||||
assert_equal 'before한글after', nfc
|
||||
assert_equal 're한글af', nfc[(o1.first...o1.last)]
|
||||
assert_equal '글a', nfc[(o2.first...o2.last)]
|
||||
end
|
||||
|
||||
def test_nfd
|
||||
nfc = '한글'
|
||||
nfd = FZF::UConv.nfd(nfc)
|
||||
assert_equal 2, nfd.length
|
||||
assert_equal 6, nfd.join.length
|
||||
assert_equal NFD, nfd.join
|
||||
end
|
||||
|
||||
def test_nfd_fuzzy_matcher
|
||||
matcher = FZF::FuzzyMatcher.new 0
|
||||
assert_equal [], matcher.match([NFD + NFD], '할', '', '')
|
||||
match = matcher.match([NFD + NFD], '글글', '', '')
|
||||
assert_equal [[NFD + NFD, [[3, 12]]]], match
|
||||
assert_equal ['한글한글', [[1, 4]]], FZF::UConv.nfc(*match.first)
|
||||
end
|
||||
|
||||
def test_nfd_extended_fuzzy_matcher
|
||||
matcher = FZF::ExtendedFuzzyMatcher.new 0
|
||||
assert_equal [], matcher.match([NFD], "'글글", '', '')
|
||||
match = matcher.match([NFD], "'한글", '', '')
|
||||
assert_equal [[NFD, [[0, 6]]]], match
|
||||
assert_equal ['한글', [[0, 2]]], FZF::UConv.nfc(*match.first)
|
||||
end
|
||||
end
|
||||
|
||||
def test_split
|
||||
assert_equal ["a", "b", "c", "\xFF", "d", "e", "f"],
|
||||
FZF::UConv.split("abc\xFFdef")
|
||||
end
|
||||
end
|
||||
|
||||
2307
test/test_go.rb
Executable file
2307
test/test_go.rb
Executable file
File diff suppressed because it is too large
Load Diff
117
uninstall
Executable file
117
uninstall
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
xdg=0
|
||||
prefix='~/.fzf'
|
||||
prefix_expand=~/.fzf
|
||||
fish_dir=${XDG_CONFIG_HOME:-$HOME/.config}/fish
|
||||
|
||||
help() {
|
||||
cat << EOF
|
||||
usage: $0 [OPTIONS]
|
||||
|
||||
--help Show this message
|
||||
--xdg Remove files generated under \$XDG_CONFIG_HOME/fzf
|
||||
EOF
|
||||
}
|
||||
|
||||
for opt in "$@"; do
|
||||
case $opt in
|
||||
--help)
|
||||
help
|
||||
exit 0
|
||||
;;
|
||||
--xdg)
|
||||
xdg=1
|
||||
prefix='"${XDG_CONFIG_HOME:-$HOME/.config}"/fzf/fzf'
|
||||
prefix_expand=${XDG_CONFIG_HOME:-$HOME/.config}/fzf/fzf
|
||||
;;
|
||||
*)
|
||||
echo "unknown option: $opt"
|
||||
help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
ask() {
|
||||
while true; do
|
||||
read -p "$1 ([y]/n) " -r
|
||||
REPLY=${REPLY:-"y"}
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
return 0
|
||||
elif [[ $REPLY =~ ^[Nn]$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
remove() {
|
||||
echo "Remove $1"
|
||||
rm -f "$1"
|
||||
}
|
||||
|
||||
remove_line() {
|
||||
src=$(readlink "$1")
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Remove from $1 ($src):"
|
||||
else
|
||||
src=$1
|
||||
echo "Remove from $1:"
|
||||
fi
|
||||
|
||||
shift
|
||||
line_no=1
|
||||
match=0
|
||||
while [ -n "$1" ]; do
|
||||
line=$(sed -n "$line_no,\$p" "$src" | \grep -m1 -nF "$1")
|
||||
if [ $? -ne 0 ]; then
|
||||
shift
|
||||
line_no=1
|
||||
continue
|
||||
fi
|
||||
line_no=$(( $(sed 's/:.*//' <<< "$line") + line_no - 1 ))
|
||||
content=$(sed 's/^[0-9]*://' <<< "$line")
|
||||
match=1
|
||||
echo " - Line #$line_no: $content"
|
||||
[ "$content" = "$1" ] || ask " - Remove?"
|
||||
if [ $? -eq 0 ]; then
|
||||
awk -v n=$line_no 'NR == n {next} {print}' "$src" > "$src.bak" &&
|
||||
mv "$src.bak" "$src" || break
|
||||
echo " - Removed"
|
||||
else
|
||||
echo " - Skipped"
|
||||
line_no=$(( line_no + 1 ))
|
||||
fi
|
||||
done
|
||||
[ $match -eq 0 ] && echo " - Nothing found"
|
||||
echo
|
||||
}
|
||||
|
||||
for shell in bash zsh; do
|
||||
shell_config=${prefix_expand}.${shell}
|
||||
remove "${shell_config}"
|
||||
remove_line ~/.${shell}rc \
|
||||
"[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" \
|
||||
"source ${prefix}.${shell}"
|
||||
done
|
||||
|
||||
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
|
||||
if [ -f "$bind_file" ]; then
|
||||
remove_line "$bind_file" "fzf_key_bindings"
|
||||
fi
|
||||
|
||||
if [ -d "${fish_dir}/functions" ]; then
|
||||
remove "${fish_dir}/functions/fzf.fish"
|
||||
remove "${fish_dir}/functions/fzf_key_bindings.fish"
|
||||
|
||||
if [ "$(ls -A "${fish_dir}/functions")" ]; then
|
||||
echo "Can't delete non-empty directory: \"${fish_dir}/functions\""
|
||||
else
|
||||
rmdir "${fish_dir}/functions"
|
||||
fi
|
||||
fi
|
||||
|
||||
config_dir=$(dirname "$prefix_expand")
|
||||
if [[ "$xdg" = 1 ]] && [[ "$config_dir" = */fzf ]] && [[ -d "$config_dir" ]]; then
|
||||
rmdir "$config_dir"
|
||||
fi
|
||||
Reference in New Issue
Block a user