Compare commits

..

240 Commits

Author SHA1 Message Date
Junegunn Choi
b68e59a24b Fix ANSI offset calculation 2015-05-22 02:20:10 +09:00
Junegunn Choi
4e0e492427 Minor refactoring 2015-05-22 00:02:14 +09:00
Junegunn Choi
8f99f8fcc6 More test cases for --bind 2015-05-21 21:06:52 +09:00
Junegunn Choi
3cdf71801e Update --help 2015-05-21 01:51:24 +09:00
Junegunn Choi
801cf9ac62 Add unbound "toggle" action for customization 2015-05-21 01:37:16 +09:00
Junegunn Choi
34946b72a5 0.9.12 2015-05-21 00:44:49 +09:00
Junegunn Choi
1592bedbe8 Custom key binding support (#238) 2015-05-21 00:32:03 +09:00
Junegunn Choi
15099eb13b Remove duplicate processing of command-line options 2015-05-20 20:42:45 +09:00
Junegunn Choi
c511b45ff6 Minor tweak in test case
It may take long for find command to spot the temporary file created on
the home directory
2015-05-20 19:47:48 +09:00
Junegunn Choi
40761b11b1 [bash] Ignore asterisk (modified) in history 2015-05-20 19:45:05 +09:00
Junegunn Choi
cca543d0cd [zsh-completion] Fix #236 - zle redisplay 2015-05-20 16:18:30 +09:00
Junegunn Choi
34e5e2dd82 [vim] Use close+bufhidden=wipe instead of bd 2015-05-14 13:29:50 +09:00
Junegunn Choi
2b7c3df66b [neovim] Check tabpagenr() as well 2015-05-14 02:19:40 +09:00
Junegunn Choi
f766531e74 [neovim] Make sure that fzf buffer is closed (#225)
- bd! leaves the window open when there's no other listed buffer
- redraw! seems to help avoid Neovim issues.
2015-05-14 02:14:21 +09:00
Junegunn Choi
7f59b42b05 [vim] Escape % # \ 2015-05-13 23:20:10 +09:00
Junegunn Choi
f41de932d6 [vim] Refocus MacVim window 2015-05-13 23:14:03 +09:00
Junegunn Choi
b4a05ff27e [bash] CTRL-R to use history-expand-line
Close #146
2015-05-13 19:13:27 +09:00
Junegunn Choi
3b91467941 Suppress error message when loading completion.{zsh,bash}
Temporary workaround for https://github.com/Homebrew/homebrew/issues/39669
2015-05-12 22:54:48 +09:00
Junegunn Choi
26d2af5ee8 [zsh-completion] Respect backslash-escaped spaces (#230) 2015-05-12 01:40:44 +09:00
Junegunn Choi
2b61dc6557 [zsh-completion] Do not overwrite $fzf_default_completion 2015-05-11 22:53:35 +09:00
Junegunn Choi
0b770cd48a [zsh-completion] Remember what ^I was originally bound to (#230) 2015-05-11 21:49:40 +09:00
Junegunn Choi
c14aa99ef6 [zsh/bash-completion] Avoid caret expansion
Close #233

setopt extendedglob on zsh caused caret in grep pattern to be expanded.
Problem identified and patch submitted by @lazywei.
2015-05-11 16:59:44 +09:00
Junegunn Choi
8f371ee81c [zsh-completion] fzf-zsh-completion -> fzf-completion 2015-05-11 13:11:42 +09:00
Junegunn Choi
3b63b39810 [zsh-completion] Allow empty prefix & trigger sequence (#232) 2015-05-11 13:06:02 +09:00
Tiziano Santoro
0cd238700c [zsh-completion] Add comment clarifying trigger expansion. (#230) 2015-05-11 10:18:28 +09:00
Tiziano Santoro
14fbe06d9e [zsh-completion] Allow specifying empty completion trigger. (#230) 2015-05-11 10:18:16 +09:00
Junegunn Choi
64949bf467 [bash-completion] Allow specifying empty completion trigger (#230) 2015-05-11 10:17:33 +09:00
Junegunn Choi
732f133940 [test] Make sure to kill background process 2015-05-10 11:24:54 +09:00
Junegunn Choi
5dc4df9570 Fix test cases 2015-05-10 05:01:52 +09:00
Junegunn Choi
7dde8dbbd9 Merge pull request #231 from robinro/head-argument-typo
[zsh] `head -n1` instead of `head -1`
2015-05-10 04:50:28 +09:00
Robin Roth
01405ad92e fix typo in argument of head
at least my version of head wants -n1 to only display the first line
2015-05-09 21:11:01 +02:00
Junegunn Choi
683abb86ef Dump screen content on test failure 2015-05-10 03:25:14 +09:00
Junegunn Choi
207aa07891 [zsh-completion] Temporarily set nonomatch (#230)
No error on ~INVALID_USERNAME**<TAB>
2015-05-10 02:54:22 +09:00
Junegunn Choi
26a141c6a6 [zsh-completion] Fix ~USERNAME** (#230) 2015-05-10 02:37:17 +09:00
Junegunn Choi
dc64568c83 [zsh-completion] Completion for unknown commands 2015-05-09 21:04:59 +09:00
Junegunn Choi
f4a595eedd Fix Travis CI build 2015-05-09 20:42:13 +09:00
Junegunn Choi
5a17a6323a [zsh-completion] "\find" to bypass alias 2015-05-09 20:36:25 +09:00
Junegunn Choi
2b8e445321 Fuzzy completion for zsh (#227) 2015-05-09 20:18:38 +09:00
Junegunn Choi
315499b1d4 Merge pull request #229 from sullyj3/master
fix typo in README.md
2015-05-09 00:36:52 +09:00
James Sully
65a2bdb01d fix typo in README.md 2015-05-09 01:26:34 +10:00
Junegunn Choi
ed8202efc6 [bash-completion] Ignore 0.0.0.0
Close #228
2015-05-08 18:16:55 +09:00
Junegunn Choi
0937bd6c16 [vim] Improve binary detection
/cc @alerque

- Ask for user confirmation before running `install --bin`
- Removed `s:fzf_rb` since `install --bin` will create a wrapper
  executable that just runs Ruby version on the platforms where prebuilt
  binaries are not available.
2015-05-03 00:58:45 +09:00
Junegunn Choi
3d26b5336c [vim] Fix #220 - Prevent error after update 2015-04-28 23:49:52 +09:00
Junegunn Choi
c8f208b96b Merge pull request #171 from oschrenk/vi-insert-mode-key-bindings-fish
Support for vi insert mode in upcoming fish 2.2.0
2015-04-26 02:17:46 +09:00
Oliver Schrenk
2e339e49b8 Support for vi insert mode in upcoming fish 2.2.0 2015-04-25 19:12:11 +02:00
Junegunn Choi
5d9107fd15 Print info after prompt on redraw
This fixes the issue where "inline-info" is not immediately rendered
when the terminal is resized.
2015-04-25 23:20:40 +09:00
Junegunn Choi
4b7c571575 Fix race condition in test case 2015-04-25 10:56:08 +09:00
Junegunn Choi
5502b68a1d Test refactoring 2015-04-25 10:40:58 +09:00
Junegunn Choi
5794fd42df Fix test code 2015-04-25 01:09:25 +09:00
Junegunn Choi
9c6e46ab15 [fzf-tmux] Fix #215 - Prepend env to avoid error on fish 2015-04-24 12:54:57 +09:00
Junegunn Choi
09d0ac0347 [vim] Update default launcher for GVim (#212)
Code submitted by @lydell
2015-04-24 12:45:39 +09:00
Junegunn Choi
22ae7adac8 Update completion for fzf itself 2015-04-23 22:43:48 +09:00
Junegunn Choi
36924d0b1c [zsh] Do not change LBUFFER on empty selection (CTRL-R) 2015-04-23 22:39:07 +09:00
Junegunn Choi
6ed9de9051 [zsh] Temporarily unset no_bang_hist for CTRL-R
Close #214
2015-04-23 22:31:23 +09:00
Junegunn Choi
857619995e [vim] Ignore E325 (#213) 2015-04-23 19:29:59 +09:00
Junegunn Choi
9310ae28ab [vim] Redraw screen after running fzf on tmux pane (#213) 2015-04-23 19:29:01 +09:00
Junegunn Choi
27e26bd1ea [vim] Add g:Fzf_launcher for funcrefs (#212) 2015-04-23 12:51:08 +09:00
Junegunn Choi
305ec3b3ce [fish] Remove buffering delay by not using subroutines
Close #169
2015-04-22 14:33:03 +09:00
Junegunn Choi
f4fe93338b Update README 2015-04-22 02:09:16 +09:00
Junegunn Choi
3b84c80d56 Update README 2015-04-22 02:07:27 +09:00
Junegunn Choi
5e120e7ab5 Update man page 2015-04-22 01:44:56 +09:00
Junegunn Choi
a4cf5510e3 0.9.11 2015-04-22 01:42:38 +09:00
Junegunn Choi
edb5ab5622 Update test cases for #203 2015-04-22 00:57:25 +09:00
Junegunn Choi
06b4f75680 Fix broken FZF_TMUX switch and update test cases (#203) 2015-04-22 00:55:39 +09:00
Junegunn Choi
318edc8c35 Apply fzf-tmux to key bindings (#203)
Note that CTRL-T on bash is still using the old trick of send-keys.
2015-04-22 00:32:18 +09:00
Junegunn Choi
651a8f8cc2 Add --inline-info option
Close #202
2015-04-21 23:50:53 +09:00
Junegunn Choi
9f64a00549 Fix double-click result when scroll offset is positive 2015-04-21 23:23:39 +09:00
Junegunn Choi
a88bf87e2a Update test case 2015-04-21 22:36:40 +09:00
Junegunn Choi
e82eb27787 Smart-case for each term in extended-search mode
Close #208
2015-04-21 22:18:05 +09:00
Junegunn Choi
3f0e6a5806 Fix #209 - Invalid mutation of input on case conversion 2015-04-21 22:10:14 +09:00
Junegunn Choi
917b1759b0 [fzf-tmux/vim] Fixes for fish (#204) 2015-04-20 22:42:12 +09:00
Junegunn Choi
16ca9c688b Revert "[fzf-tmux] Fix #204 - Escape command substitution"
This reverts commit 7b6a27cb5e.
2015-04-20 16:23:15 +09:00
Junegunn Choi
7b6a27cb5e [fzf-tmux] Fix #204 - Escape command substitution 2015-04-20 15:22:59 +09:00
Junegunn Choi
869a234938 [fzf-tmux] Use bash instead of sh (#204)
The default shell can be a non-standard shell (e.g. fish)
2015-04-20 14:58:27 +09:00
Junegunn Choi
537d07c1e5 [vim] Use "system" fzf when available
1. Go binary: ../bin/fzf
2. System fzf: $(which fzf)
3. Download fzf from GitHub or create wrapper script to Ruby version (../fzf)
   when the binary for the platform is not available
4. If install script is not found or for some reason failed, try to use Ruby
   version in its expected location (../fzf)
5. If fzf is found to be a shell function, use it (type fzf)
2015-04-19 17:13:07 +09:00
Junegunn Choi
d091a2c4bb [fzf-tmux] Minor adjustment 2015-04-18 16:27:40 +09:00
Junegunn Choi
d2f95d69fb [fzf-tmux] Fix #200 - Double-quote handling
Related #199
2015-04-18 16:24:57 +09:00
Junegunn Choi
1169cc8653 0.9.10 2015-04-18 10:43:40 +09:00
Junegunn Choi
f66d94c6b0 Add --color=[dark|light|16|bw] option
- dark:  the current default for 256-color terminal
- light: color scheme for 256-color terminal with light background
- 16:    the default color scheme for 16-color terminal (`+2`)
- bw:    no colors (`+c`)
2015-04-18 02:55:17 +09:00
Junegunn Choi
2fe1e28220 Improvements in performance and memory usage
I profiled fzf and it turned out that it was spending significant amount
of time repeatedly converting character arrays into Unicode codepoints.
This commit greatly improves search performance after the initial scan
by memoizing the converted results.

This commit also addresses the problem of unbounded memory usage of fzf.
fzf is a short-lived process that usually processes small input, so it
was implemented to cache the intermediate results very aggressively with
no notion of cache expiration/eviction. I still think a proper
implementation of caching scheme is definitely an overkill. Instead this
commit introduces limits to the maximum size (or minimum selectivity) of
the intermediate results that can be cached.
2015-04-17 22:23:52 +09:00
Junegunn Choi
288131ac5a Update man page to be consistent with --help 2015-04-16 23:11:11 +09:00
Junegunn Choi
3610acec5a 0.9.9 2015-04-16 22:52:15 +09:00
Junegunn Choi
cc67d2e1cf Test case for visual indicator of --toggle sort (#194) 2015-04-16 22:39:51 +09:00
Junegunn Choi
f77ed0fb07 Fix typo in man page 2015-04-16 22:34:02 +09:00
Junegunn Choi
a30908c66a [vim] Automatically download Go binary when not found 2015-04-16 22:24:12 +09:00
Junegunn Choi
f9225f98e7 Fix sort control from Terminal 2015-04-16 22:13:31 +09:00
Junegunn Choi
2db2feea37 install --bin just for downloading the binary 2015-04-16 21:58:41 +09:00
Junegunn Choi
d1d59272a2 Add visual indication of --toggle-sort
Close #194
2015-04-16 14:46:10 +09:00
Junegunn Choi
d08542ce5d Prepare for 0.9.9 release 2015-04-16 14:34:40 +09:00
Junegunn Choi
b8904a8c3e Add --tiebreak option for customizing sort criteria
Close #191
2015-04-16 14:19:28 +09:00
Junegunn Choi
48ab87294b Add --no-hscroll option to disable horizontal scroll
Close #193
2015-04-16 12:56:01 +09:00
Junegunn Choi
3e1e75fe08 Remove unused variable 2015-04-16 10:52:04 +09:00
Junegunn Choi
120cc0aadd [vim] README: Pointer to the wiki page 2015-04-15 22:52:15 +09:00
Junegunn Choi
853012ceef [vim] Add g:fzf_action for customizing key bindings
Close #189
2015-04-15 22:49:45 +09:00
Junegunn Choi
2add45fe2f [vim] Rename g:fzf_tmux_height to g:fzf_height
Because tmux panes are not used on Neovim.
2015-04-15 22:32:45 +09:00
Junegunn Choi
b882de87ab Fix Travis CI build 2015-04-15 01:58:39 +09:00
Junegunn Choi
2d68cb8639 Fix #185 - Terminate on RuneError 2015-04-14 23:19:55 +09:00
Junegunn Choi
3a9d1df026 Fix unicode test case 2015-04-14 21:59:44 +09:00
Junegunn Choi
5c25984ea0 Fix Unicode case handling (#186) 2015-04-14 21:45:37 +09:00
Junegunn Choi
319d6ced80 [vim] Simplify :FZF
Ruby version can also accept `--expect` option although it's ignored.
2015-04-14 10:46:20 +09:00
Junegunn Choi
51a19a2804 [vim] Remove unnecessary pushd/popd in :FZF
It is already handled by its caller.
2015-04-14 10:35:51 +09:00
Junegunn Choi
a7cb1a78df Merge pull request #188 from justinmk/non-interactive-shell
install: wait for LF in non-interactive shell
2015-04-14 10:13:52 +09:00
Justin M. Keyes
d4daece76b install: wait for LF in non-interactive shell
"read -n 1 ..." ignores all but the first character of a line-delimited
stream (e.g. "yes n | ./install").
2015-04-13 20:40:46 -04:00
Junegunn Choi
3ec83babac FZF_TMUX and FZF_TMUX_HEIGHT for fuzzy completion 2015-04-14 02:12:45 +09:00
Junegunn Choi
91fc6c984b Fix fuzzy completion test 2015-04-14 02:00:50 +09:00
Junegunn Choi
a4f3d09704 Fuzzy completion using fzf-tmux 2015-04-14 01:00:39 +09:00
Junegunn Choi
40180c18ac Merge pull request #183 from qiemem/generalized-check-if-running
Check if fzf#run() is already executing
2015-04-12 02:45:14 +09:00
Bryan Head
82bea6758a Move active check to fzf#run. 2015-04-11 12:44:14 -05:00
Junegunn Choi
348731fc3b Make fzf-tmux work when fzf is not in $PATH but in the same directory
See: #181
2015-04-12 01:57:56 +09:00
Junegunn Choi
797f42ecc6 Update README 2015-04-12 00:44:41 +09:00
Junegunn Choi
8385a55bda [vim] s:pushd after s:split
It is possible that the user has an autocmd that changes the current
directory.
2015-04-11 23:47:46 +09:00
Junegunn Choi
8406cedf2d [vim] Improved compatibility with sidebar plugins (e.g. NERDtree) 2015-04-11 23:42:01 +09:00
Junegunn Choi
f22b83db6c Update README 2015-04-11 11:28:30 +09:00
Junegunn Choi
1481304d3b Suppress message from :file
Suggested by @noahfrederick
2015-04-11 11:19:22 +09:00
Junegunn Choi
2cec5c0f30 Fix typo in README 2015-04-11 09:21:23 +09:00
Junegunn Choi
4760bb7743 Merge pull request #180 from mhinz/check-if-already-running
Check if :FZF is already executing
2015-04-11 09:16:44 +09:00
Marco Hinz
c1adf0cd3d Check if :FZF is already executing
Prior to this change, you'd get a longer error message if you did:

    :FZF
    <esc>
    :FZF

The main problem being that `:file [FZF]` can be used only once.
2015-04-10 22:18:46 +02:00
Junegunn Choi
622e69ff54 [vim] Neovim compatibility (#137)
Use terminal emulator of Neovim to open fzf
2015-04-10 23:23:47 +09:00
Junegunn Choi
68503d32df [vim] Code cleanup 2015-04-04 11:55:57 +09:00
Junegunn Choi
57319f8c58 [vim] Fix #177 - :FZF with relative paths 2015-04-04 09:18:04 +09:00
Junegunn Choi
dd4d465305 Update Homebrew instruction
Close #175
2015-04-03 01:22:16 +09:00
Junegunn Choi
467a22dd36 Period. 2015-03-31 22:09:04 +09:00
Junegunn Choi
50292adacb Implement --toggle-sort option (#173) 2015-03-31 22:05:16 +09:00
Junegunn Choi
84a7499ae3 Fix #172 - Print empty line when fzf with expect finished by -1 or -0 2015-03-31 20:52:16 +09:00
Junegunn Choi
39d7177bd3 [ruby] Stub out --expect option
So that it can be used with the recent Vim plugin although extra key
bindings are not available
2015-03-31 15:40:33 +09:00
Junegunn Choi
1c65139888 Update git ls-tree example (close #168) 2015-03-30 10:15:05 +09:00
Junegunn Choi
8a4db3c004 [vim] Fix #167 - :FZF with directory 2015-03-29 11:14:16 +09:00
Junegunn Choi
cef93f700b 0.9.6 2015-03-29 04:09:45 +09:00
Junegunn Choi
0a3d3460b1 Update man page 2015-03-29 04:08:37 +09:00
Junegunn Choi
d988f3fa50 Retain ANSI background color 2015-03-29 03:12:55 +09:00
Junegunn Choi
e865144ace [vim] Implement ctrlp-compatible key bindings (#139, #96, #61) 2015-03-29 03:00:32 +09:00
Junegunn Choi
2a167aa030 Implement --expect option to support simple key bindings (#163) 2015-03-29 02:59:32 +09:00
Junegunn Choi
9cfecf7f0b Fix test failure 2015-03-28 21:37:37 +09:00
Junegunn Choi
f9d6b83f5e Drop patch number and stick with M.m.p convention 2015-03-27 12:47:42 +09:00
Junegunn Choi
ce7d4a1c53 Fix #162 - Ignore \e[K 2015-03-27 12:35:06 +09:00
Junegunn Choi
9bba6bd172 Merge pull request #158 from mrap/update-macvim-link
Updates MacVim wiki link in README
2015-03-27 09:14:02 +09:00
Michael Rapadas
4ad92e3a0b Updates MacVim wiki link in README 2015-03-26 13:28:19 -07:00
Junegunn Choi
c4bf820dc3 Update man page 2015-03-26 10:31:47 +09:00
Junegunn Choi
39f43587d0 Fix typo in man page 2015-03-26 10:28:14 +09:00
Junegunn Choi
fdaa4e9b18 Append (not prepend) bin directory to PATH
Prepending can be problematic when the user install fzf using Homebrew,
execute the install script, and later upgrade fzf with Homebrew, and do
not rerun the install script. In that case, even though the homebrew
package is upgraded, the older version will still be used.
2015-03-26 03:44:18 +09:00
Junegunn Choi
91876e98cd Avoid duplicate paths in MANPATH 2015-03-26 03:26:28 +09:00
Junegunn Choi
eb8fef0031 Add man path only when the directory exists
$fzf_base/man may not exist when installed with Homebrew.
2015-03-26 03:11:08 +09:00
Junegunn Choi
87447ddd6d Add man page (#157) 2015-03-26 03:08:39 +09:00
Junegunn Choi
9d138173be Fix #155 - Empty ANSI color code to reset color state 2015-03-23 01:24:31 +09:00
Junegunn Choi
eae53576bd Update --help message 2015-03-22 21:25:46 +09:00
Junegunn Choi
f8c49effd4 Respect "boldness" of input string 2015-03-22 17:43:28 +09:00
Junegunn Choi
618706a5f5 Fix ANSI output in the presence of multibyte characters
tree -C | fzf --ansi --tac
2015-03-22 17:22:52 +09:00
Junegunn Choi
9ffcd26d50 Update CHANGELOG - 0.9.5 2015-03-22 16:21:52 +09:00
Junegunn Choi
b431e227da Code cleanup 2015-03-22 16:05:54 +09:00
Junegunn Choi
d94dfe0876 Fix #151 - reduce initial memory footprint 2015-03-19 19:59:38 +09:00
Junegunn Choi
6130026786 Bump up the version - 0.9.5 2015-03-19 19:12:22 +09:00
Junegunn Choi
a723977b9f Fix #149 - panic on empty string filter 2015-03-19 13:06:20 +09:00
Junegunn Choi
3dddbfd8fa Fix string truncation 2015-03-19 12:14:26 +09:00
Junegunn Choi
e70a2a5817 Add support for ANSI color codes 2015-03-19 01:59:14 +09:00
Junegunn Choi
d80a41bb6d Update README
Use --depth option to avoid pulling devel branches
2015-03-18 02:19:05 +09:00
Junegunn Choi
2bebd5cdb4 Update README with fzf image 2015-03-16 02:49:09 +09:00
Junegunn Choi
7bb75b0213 Update README 2015-03-15 10:57:09 +09:00
Junegunn Choi
bc2e82efc1 [vim] Suppress error message when clear command is N/A 2015-03-13 23:04:13 +09:00
Junegunn Choi
c04e8de9b0 Make sure to start tmux pane from the current directory (#143)
- fzf-tmux
- CTRL-T of bash/zsh/fish
    - fish implementation may not work if the path contains
      double-quote characters (FIXME)
2015-03-13 22:59:23 +09:00
Junegunn Choi
4977174def [fzf-mux] Remove unnecessary env var from command 2015-03-13 22:45:28 +09:00
Junegunn Choi
5eef0acea1 Merge pull request #145 from junegunn/refactor-shell-ext
Refactor shell extensions
2015-03-13 17:44:44 +09:00
Junegunn Choi
3935aa84d8 Refactor shell extensions
- Use symlinks instead of generating the full content
- Update fish_user_paths and remove ~/.config/fish/functions/fzf.fish
- Create wrapper script for fzf when Ruby version and use it instead of
  exported function not to break fzf-tmux
2015-03-13 17:41:00 +09:00
Junegunn Choi
dd6138a655 Fix #142, #144 - Improve CTRL-R for zsh 2015-03-13 01:33:01 +09:00
Junegunn Choi
68c5bea3f8 Fix install script for platforms w/o matching Go binary (#141) 2015-03-12 10:06:15 +09:00
Junegunn Choi
0f474d541d Note on upgrade 2015-03-11 15:07:34 +09:00
Junegunn Choi
c4d59aeec4 Remove legacy test code 2015-03-11 02:16:27 +09:00
Junegunn Choi
b2c423d1ff Cleanup - no more rubygems 2015-03-11 02:12:38 +09:00
Junegunn Choi
49c752b1f7 [vim] up/down/left/right options to take boolean values
When 1 is given, 50% of the screen width or height will be used as the
default size of the pane.
2015-03-10 12:13:11 +09:00
Junegunn Choi
daa79a6df2 [vim] fzf#run with tmux panes can now return values to the caller
As they're made synchronous with the use of fzf-tmux script
2015-03-10 12:07:32 +09:00
Junegunn Choi
48e0c1e721 Ignore new options in legacy Ruby version 2015-03-10 02:16:32 +09:00
Junegunn Choi
12d81e212f [vim] Use fzf-tmux script for tmux integration 2015-03-10 01:41:35 +09:00
Junegunn Choi
c22e729d9c [fzf-tmux] Apply environment variables 2015-03-09 23:57:17 +09:00
Junegunn Choi
2b8a1c0d70 Update README - Homebrew instruction and fzf-tmux options 2015-03-09 23:40:43 +09:00
Junegunn Choi
e4b56b9702 Merge pull request #138 from junegunn/fzf-tmux-swap-pane
[fzf-tmux] Allow opening fzf on any position (up/down/left/right)
2015-03-09 23:28:53 +09:00
Junegunn Choi
789a474b28 [fzf-tmux] Allow opening fzf on any position (-u/-d/-l/-r)
The previous -w and -h will be synonyms for -r and -d respectively.
2015-03-09 12:49:26 +09:00
Junegunn Choi
fb2959c514 [fzf-tmux] Fix duplicate arguments to fzf
fzf-tmux -w -q q
fzf-tmux -w -- -q q
2015-03-08 16:40:48 +09:00
Junegunn Choi
62a28468a7 [fzf-tmux] Fix -- 2015-03-08 16:36:37 +09:00
Junegunn Choi
23dba99eda [fzf-tmux] Allow -w / -h without size argument 2015-03-08 15:08:27 +09:00
Junegunn Choi
5f62d224b0 Fix fzf-tmux script (bash 3.2 compatibility) 2015-03-07 10:07:36 +09:00
Junegunn Choi
6728870071 Merge pull request #136 from junegunn/fzf-tmux
Add fzf-tmux script
2015-03-07 10:01:23 +09:00
Junegunn Choi
87c71a3ea6 Increase timeout in test cases 2015-03-07 09:53:54 +09:00
Junegunn Choi
06ab399497 Improve how vim plugin finds fzf executable
This avoids the problem in which :FZF command silently fails when fzf
executable cannot be found in $PATH of the hosting tmux server.
2015-03-07 09:48:56 +09:00
Junegunn Choi
f7b52d2541 Use absolute path of fzf when splitting tmux window 2015-03-07 09:29:16 +09:00
Junegunn Choi
c111af0ed2 Use the term pane instead of split when not ambiguous
/cc @Tranquility
2015-03-07 09:08:41 +09:00
Junegunn Choi
07e2bd673e Update README 2015-03-06 18:57:36 +09:00
Junegunn Choi
e4ce64d10b Add fzf-tmux script 2015-03-06 18:51:50 +09:00
Junegunn Choi
5f3326a888 Deprecation alert 2015-03-06 13:21:55 +09:00
Junegunn Choi
1304428003 Update bash completion *for* fzf 2015-03-06 10:42:38 +09:00
Junegunn Choi
55828f389a Add test case for 7e2c18a 2015-03-04 13:13:11 +09:00
Junegunn Choi
7e2c18a1f6 Fix directory completion matching regular files
Related: #135
2015-03-04 13:03:54 +09:00
Junegunn Choi
79c147ed78 Fix #135 - Directory completion to append / 2015-03-04 12:59:23 +09:00
Junegunn Choi
d4b41c5e03 Merge pull request #134 from junegunn/devel
0.9.4
2015-03-01 12:35:08 +09:00
Junegunn Choi
b15a0e9650 Update CHANGELOG 2015-03-01 12:31:49 +09:00
Junegunn Choi
fe09559ee9 Build with Go 1.4.2 2015-03-01 11:49:11 +09:00
Junegunn Choi
94e8e6419f Make --filter non-blocking when --no-sort (#132)
When fzf works in filtering mode (--filter) and sorting is disabled
(--no-sort), there's no need to block until input is complete. This
commit makes fzf print the matches on-the-fly when the following
condition is met:

    --filter FILTER --no-sort [--no-tac --no-sync]

or simply:

    -f FILTER +s

This removes unnecessary delay in use cases like the following:

    fzf -f xxx +s | head -5

However, in this case, fzf processes the input lines sequentially, so it
cannot utilize multiple cores, which makes it slightly slower than the
previous mode of execution where filtering is done in parallel after the
entire input is loaded. If the user is concerned about the performance
problem, one can add --sync option to re-enable buffering.
2015-03-01 11:16:38 +09:00
Junegunn Choi
4d2d18649c Add basic test cases for shell extensions (#83)
- Key bindings for bash, zsh, and fish
- Fuzzy completion for bash (file, dir, process)
2015-03-01 03:33:56 +09:00
Junegunn Choi
c1aa5c5f33 Add --tac option and reverse display order of --no-sort
DISCLAIMER: This is a backward incompatible change
2015-02-26 01:42:15 +09:00
Junegunn Choi
4a1752d3fc 0.9.3 2015-02-18 13:19:20 +09:00
Junegunn Choi
b9b1eeffce Update Vader tests 2015-02-18 12:12:59 +09:00
Junegunn Choi
5667667d1f Add test case for --sync option 2015-02-18 12:07:54 +09:00
Junegunn Choi
f5b034095a Fix race condition in asynchronous -1 and -0 2015-02-18 00:51:44 +09:00
Junegunn Choi
95e5beb34e Update Homebrew instruction 2015-02-18 00:22:17 +09:00
Junegunn Choi
e808151c28 Make --select-1 and --exit-0 asynchronous 2015-02-18 00:08:17 +09:00
Junegunn Choi
d760b790b3 Fix typo in code 2015-02-17 19:28:10 +09:00
Junegunn Choi
1b5599972a Update installation instruction 2015-02-17 13:15:16 +09:00
Junegunn Choi
6c2ce28d0d Add --sync option 2015-02-13 12:25:19 +09:00
Junegunn Choi
ff09c275d4 Fix bash script when fzf_base contains spaces 2015-02-12 10:14:05 +09:00
Junegunn Choi
93dcd932e8 Merge pull request #123 from junegunn/fix-travis-ci
Fix Travis CI build
2015-01-29 17:44:11 +09:00
Junegunn Choi
e6a0de4094 Fix Travis CI build 2015-01-29 17:41:28 +09:00
Junegunn Choi
9f39671e65 Update README.md
Update outdated --help output
2015-01-28 01:45:34 +09:00
Junegunn Choi
423317b82a Update README.md 2015-01-28 01:18:20 +09:00
Junegunn Choi
47201c2c4d Merge pull request #122 from blueyed/improve-find-cdwidget
Improve `find` command for ALT-C: exclude proc/dev
2015-01-25 11:20:20 +09:00
Daniel Hahler
53d5d9d162 Improve find command for cd widgets: exclude proc/dev etc
When using the widget in "/", it would descend into 'dev/'.
Using '*' for the starting path would do so also with the new '-fstype'
excludes.

`cut -b3-` and `sed 1d` have been added to massage the different format
of the list.

This also uses `-L` with all calls to find, especially for the file
finders.

Ref: https://github.com/junegunn/fzf/pull/122
2015-01-25 03:09:02 +01:00
Junegunn Choi
9cb0cdb4ac 0.9.2 2015-01-24 14:49:21 +09:00
Junegunn Choi
448132c46c Fix error when --query contains wide-length characters 2015-01-24 13:26:33 +09:00
Junegunn Choi
1476fc7f3b Refactor test code 2015-01-24 13:25:11 +09:00
Junegunn Choi
71a7b3a26f Improve rendering performance by caching rune widths
Related: 8bead4a
2015-01-24 12:28:00 +09:00
Junegunn Choi
a47c06cb61 Fix update_assets script 2015-01-23 20:32:56 +09:00
Junegunn Choi
48e16edb47 Redraw and adjust upon terminal resize 2015-01-23 20:30:50 +09:00
Junegunn Choi
c35d98dc42 Nullify --nth option when it's irrelevant 2015-01-23 06:26:00 +09:00
Junegunn Choi
8bead4ae34 Improved handling of tab characters 2015-01-18 16:59:04 +09:00
Junegunn Choi
1b6cb3532d Update src/README.md 2015-01-18 16:34:10 +09:00
Junegunn Choi
0a0955755a Add note on installation 2015-01-18 16:32:37 +09:00
Junegunn Choi
a3101120fd Update install script 2015-01-17 20:40:00 +09:00
Junegunn Choi
30f9651f99 0.9.1 2015-01-17 14:15:26 +09:00
Junegunn Choi
4dcc0f10b8 Fix Travis CI build by ignoring trailing empty lines
😭
2015-01-17 13:45:56 +09:00
Junegunn Choi
3d39ab5ded Fix flaky tests 2015-01-17 13:39:11 +09:00
Junegunn Choi
c3a198d0c7 Add test cases for --select-1 and --exit-0 2015-01-17 12:37:24 +09:00
Junegunn Choi
be5c17612a Add basic test case for --reverse 2015-01-17 12:21:38 +09:00
Junegunn Choi
fe89ac8a89 Add script for updating release assets 2015-01-17 11:57:21 +09:00
Junegunn Choi
4c3ae847b6 Add test case for --with-nth + --multi 2015-01-17 11:20:17 +09:00
Junegunn Choi
5c0dc79ffa Print selected items in the order they are selected 2015-01-17 11:07:04 +09:00
Junegunn Choi
0a83705d21 Use Go 1.4.1 to build linux binaries 2015-01-17 10:57:07 +09:00
Junegunn Choi
ea22292d2c Merge pull request #117 from junegunn/fix-ctrl-y
Fix CTRL-Y key binding
2015-01-17 10:55:05 +09:00
Junegunn Choi
1990f3c992 Do not build i386 binary on Travis CI to speed up the process 2015-01-17 10:51:39 +09:00
Junegunn Choi
c0b432f7b4 Fix Travis-CI build 2015-01-17 10:39:18 +09:00
Junegunn Choi
ae3180f919 Fix CTRL-Y key binding
With tmux-based test cases
2015-01-17 06:04:59 +09:00
Junegunn Choi
62acb9adc4 Fix error with empty list and release 0.9.1-dev 2015-01-15 06:06:22 +09:00
Junegunn Choi
0b5fa56444 Remove brew target 2015-01-14 02:26:47 +09:00
Junegunn Choi
789f26b1a5 Add GIF to src/README 2015-01-14 02:16:03 +09:00
55 changed files with 4115 additions and 2065 deletions

View File

@@ -1,10 +1,24 @@
language: ruby
sudo: false
rvm:
- "1.8.7"
- "1.9.3"
- "2.0.0"
- "2.1.1"
- 2.2.0
install: gem install curses minitest
install:
- sudo apt-get update
- sudo apt-get install -y libncurses-dev lib32ncurses5-dev
- sudo add-apt-repository -y ppa:pi-rho/dev
- sudo apt-add-repository -y ppa:fish-shell/release-2
- sudo apt-get update
- sudo apt-get install -y tmux=1.9a-1~ppa1~p
- sudo apt-get install -y zsh fish
script: |
export GOPATH=~/go
export FZF_BASE=$GOPATH/src/github.com/junegunn/fzf
mkdir -p $GOPATH/src/github.com/junegunn
ln -s $(pwd) $FZF_BASE
cd $FZF_BASE/src && make test fzf/fzf-linux_amd64 install &&
cd $FZF_BASE/bin && ln -sf fzf-linux_amd64 fzf-$(./fzf --version)-linux_amd64 &&
cd $FZF_BASE && yes | ./install && rm -f fzf &&
tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ]

177
CHANGELOG.md Normal file
View File

@@ -0,0 +1,177 @@
CHANGELOG
=========
0.9.12
------
### New features
- Added `--bind` option for custom key bindings
### Bug fixes
- Fixed to update "inline-info" immediately after terminal resize
0.9.11
------
### New features
- Added `--inline-info` option for saving screen estate (#202)
- Useful inside Neovim
- e.g. `let $FZF_DEFAULT_OPTS = $FZF_DEFAULT_OPTS.' --inline-info'`
### Bug fixes
- Invalid mutation of input on case conversion (#209)
- Smart-case for each term in extended-search mode (#208)
- Fixed double-click result when scroll offset is positive
0.9.10
------
### Improvements
- Performance optimization
- Less aggressive memoization to limit memory usage
### New features
- Added color scheme for light background: `--color=light`
0.9.9
-----
### New features
- Added `--tiebreak` option (#191)
- Added `--no-hscroll` option (#193)
- Visual indication of `--toggle-sort` (#194)
0.9.8
-----
### Bug fixes
- Fixed Unicode case handling (#186)
- Fixed to terminate on RuneError (#185)
0.9.7
-----
### New features
- Added `--toggle-sort` option (#173)
- `--toggle-sort=ctrl-r` is applied to `CTRL-R` shell extension
### Bug fixes
- Fixed to print empty line if `--expect` is set and fzf is completed by
`--select-1` or `--exit-0` (#172)
- Fixed to allow comma character as an argument to `--expect` option
0.9.6
-----
### New features
#### Added `--expect` option (#163)
If you provide a comma-separated list of keys with `--expect` option, fzf will
allow you to select the match and complete the finder when any of the keys is
pressed. Additionally, fzf will print the name of the key pressed as the first
line of the output so that your script can decide what to do next based on the
information.
```sh
fzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@
```
The updated vim plugin uses this option to implement
[ctrlp](https://github.com/kien/ctrlp.vim)-compatible key bindings.
### Bug fixes
- Fixed to ignore ANSI escape code `\e[K` (#162)
0.9.5
-----
### New features
#### Added `--ansi` option (#150)
If you give `--ansi` option to fzf, fzf will interpret ANSI color codes from
the input, display the item with the ANSI colors (true colors are not
supported), and strips the codes from the output. This option is off by
default as it entails some overhead.
### Improvements
#### Reduced initial memory footprint (#151)
By removing unnecessary copy of pointers, fzf will use significantly smaller
amount of memory when it's started. The difference is hugely noticeable when
the input is extremely large. (e.g. `locate / | fzf`)
### Bug fixes
- Fixed panic on `--no-sort --filter ''` (#149)
0.9.4
-----
### New features
#### Added `--tac` option to reverse the order of the input.
One might argue that this option is unnecessary since we can already put `tac`
or `tail -r` in the command pipeline to achieve the same result. However, the
advantage of `--tac` is that it does not block until the input is complete.
### *Backward incompatible changes*
#### Changed behavior on `--no-sort`
`--no-sort` option will no longer reverse the display order within finder. You
may want to use the new `--tac` option with `--no-sort`.
```
history | fzf +s --tac
```
### Improvements
#### `--filter` will not block when sort is disabled
When fzf works in filtering mode (`--filter`) and sort is disabled
(`--no-sort`), there's no need to block until input is complete. The new
version of fzf will print the matches on-the-fly when the following condition
is met:
--filter TERM --no-sort [--no-tac --no-sync]
or simply:
-f TERM +s
This change removes unnecessary delay in the use cases like the following:
fzf -f xxx +s | head -5
However, in this case, fzf processes the lines sequentially, so it cannot
utilize multiple cores, and fzf will run slightly slower than the previous
mode of execution where filtering is done in parallel after the entire input
is loaded. If the user is concerned about this performance problem, one can
add `--sync` option to re-enable buffering.
0.9.3
-----
### New features
- Added `--sync` option for multi-staged filtering
### Improvements
- `--select-1` and `--exit-0` will start finder immediately when the condition
cannot be met

400
README.md
View File

@@ -1,44 +1,62 @@
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"> [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) <a href="http://flattr.com/thing/3115381/junegunnfzf-on-GitHub" target="_blank"><img src="http://api.flattr.com/button/flattr-badge-large.png" alt="Flattr this" title="Flattr this" border="0" /></a>
===
fzf is a general-purpose fuzzy finder for your shell.
fzf is a general-purpose command-line fuzzy finder.
![](https://raw.github.com/junegunn/i/master/fzf.gif)
It was heavily inspired by [ctrlp.vim](https://github.com/kien/ctrlp.vim) and
the likes.
Pros
----
- No dependencies
- Blazingly fast
- e.g. `locate / | fzf`
- Flexible layout
- Runs in fullscreen or in horizontal/vertical split using tmux
- The most comprehensive feature set
- Try `fzf --help` and be surprised
- Batteries included
- Vim/Neovim plugin, key bindings and fuzzy auto-completion
Installation
------------
fzf project consists of the followings:
- `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, but it's recommended that you
install the extra stuff using the attached install script.
[bin]: https://github.com/junegunn/fzf-bin/releases
#### Using git (recommended)
Clone this repository and run
[install](https://github.com/junegunn/fzf/blob/master/install) script.
```sh
git clone https://github.com/junegunn/fzf.git ~/.fzf
git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install
```
In case you don't have git installed:
#### Using Homebrew
On OS X, you can use [Homebrew](http://brew.sh/) to install fzf.
```sh
mkdir -p ~/.fzf
curl -L https://github.com/junegunn/fzf/archive/master.tar.gz |
tar xz --strip-components 1 -C ~/.fzf
~/.fzf/install
brew reinstall --HEAD fzf
# Install shell extensions
/usr/local/Cellar/fzf/HEAD/install
```
The script will setup:
- `fzf` function (bash, zsh, fish)
- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
- Fuzzy auto-completion (bash)
If you don't use any of the aforementioned shells, you have to manually place
fzf executable in a directory included in `$PATH`. Key bindings and
auto-completion will not be available in that case.
### Install as Vim plugin
#### Install as Vim plugin
Once you have cloned the repository, add the following line to your .vimrc.
@@ -46,55 +64,26 @@ Once you have cloned the repository, add the following line to your .vimrc.
set rtp+=~/.fzf
```
Or you may use [vim-plug](https://github.com/junegunn/vim-plug) to manage fzf
inside Vim:
Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf
(recommended):
```vim
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' }
```
#### Upgrading fzf
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.
- git: `cd ~/.fzf && git pull && ./install`
- brew: `brew reinstall --HEAD fzf`
- vim-plug: `:PlugUpdate fzf`
Usage
-----
```
usage: fzf [options]
Search
-x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match)
-i Case-insensitive match (default: smart-case match)
+i Case-sensitive match
-n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END])
--with-nth=N[,..] Transform the item using index expressions for search
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result
-s, --sort=MAX Maximum number of matched items to sort (default: 1000)
+s, --no-sort Do not sort the result. Keep the sequence unchanged.
Interface
-m, --multi Enable multi-select with tab/shift-tab
--no-mouse Disable mouse
+c, --no-color Disable colors
+2, --no-256 Disable 256-color
--black Use black background
--reverse Reverse orientation
--prompt=STR Input prompt (default: '> ')
Scripting
-q, --query=STR Start the finder with the given query
-1, --select-1 Automatically select the only match
-0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line
Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000")
```
fzf will launch curses-based finder, read the list from STDIN, and write the
selected item to STDOUT.
@@ -110,34 +99,16 @@ 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
```sh
history | fzf +s
```
- `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
### Keys
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, CTRL-G, or ESC will terminate the finder.
The following readline key bindings should also work as expected.
- CTRL-A / CTRL-E
- CTRL-B / CTRL-F
- CTRL-H / CTRL-D
- CTRL-W / CTRL-U / CTRL-Y
- ALT-B / ALT-F
If you enable multi-select mode with `-m` option, you can select multiple items
with TAB or Shift-TAB key.
You can also use mouse. Double-click on an item to select it or shift-click (or
ctrl-click) to select multiple items. Use mouse wheel to move the cursor up and
down.
### Extended-search mode
#### Extended-search mode
With `-x` or `--extended` option, fzf will start in "extended-search mode".
@@ -156,40 +127,12 @@ such as: `^music .mp3$ sbtrkt !rmx`
If you don't need fuzzy matching and do not wish to "quote" every word, start
fzf with `-e` or `--extended-exact` option.
Useful examples
---------------
Examples
--------
```sh
# fe [FUZZY PATTERN] - Open the selected file with the default editor
# - Bypass fuzzy finder if there's only one match (--select-1)
# - Exit if there's no match (--exit-0)
fe() {
local file
file=$(fzf --query="$1" --select-1 --exit-0)
[ -n "$file" ] && ${EDITOR:-vim} "$file"
}
# fd - cd to selected directory
fd() {
local dir
dir=$(find ${1:-*} -path '*/\.*' -prune \
-o -type d -print 2> /dev/null | fzf +m) &&
cd "$dir"
}
# fh - repeat history
fh() {
eval $(([ -n "$ZSH_NAME" ] && fc -l 1 || history) | fzf +s | sed 's/ *[0-9]* *//')
}
# fkill - kill process
fkill() {
ps -ef | sed 1d | fzf -m | awk '{print $2}' | xargs kill -${1:-9}
}
```
For more examples, see [the wiki
page](https://github.com/junegunn/fzf/wiki/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.
Key bindings for command line
-----------------------------
@@ -199,27 +142,39 @@ fish.
- `CTRL-T` - Paste the selected file path(s) into the command line
- `CTRL-R` - Paste the selected command from history into the command line
- Sort is disabled by default to respect chronological ordering
- Press `CTRL-R` again to toggle sort
- `ALT-C` - cd into the selected directory
If you're on a tmux session, `CTRL-T` will launch fzf in a new split-window. You
may disable this tmux integration by setting `FZF_TMUX` to 0, or change the
height of the window with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`).
If you're on a tmux session, fzf will start in a split pane. You may disable
this tmux integration by setting `FZF_TMUX` to 0, or change the height of the
pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`).
If you use vi mode on bash, you need to add `set -o vi` *before* `source
~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi
mode.
If you want to customize the key bindings, consider editing the
installer-generated source code: `~/.fzf.bash`, `~/.fzf.zsh`, and
`~/.config/fish/functions/fzf_key_bindings.fish`.
`fzf-tmux` script
-----------------
Auto-completion
---------------
[fzf-tmux](bin/fzf-tmux) is a bash script that opens fzf in a tmux pane.
Disclaimer: *Auto-completion feature is currently experimental, it can change
over time*
```sh
# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
# (-[udlr]: up/down/left/right)
### bash
# 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 `-[udlr]`
options, so you can invariably use `fzf-tmux` in your scripts.
Fuzzy completion for bash and zsh
---------------------------------
#### Files and directories
@@ -288,18 +243,12 @@ export FZF_COMPLETION_TRIGGER='~~'
export FZF_COMPLETION_OPTS='+c -x'
```
### zsh
TODO :smiley:
(Pull requests are appreciated.)
Usage as Vim plugin
-------------------
(Note: To use fzf in GVim, an external terminal emulator is required.)
### `:FZF[!]`
#### `:FZF[!]`
If you have set up fzf for Vim, `:FZF` command will be added.
@@ -312,51 +261,46 @@ If you have set up fzf for Vim, `:FZF` command will be added.
" With options
:FZF --no-sort -m /tmp
" Bang version starts in fullscreen instead of using tmux pane or Neovim split
:FZF!
```
Note that the environment variables `FZF_DEFAULT_COMMAND` and `FZF_DEFAULT_OPTS`
also apply here.
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.
If you're on a tmux session, `:FZF` will launch fzf in a new split-window whose
height can be adjusted with `g:fzf_tmux_height` (default: '40%'). However, the
bang version (`:FZF!`) will always start in fullscreen.
Note that the environment variables `FZF_DEFAULT_COMMAND` and
`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for
customization.
In GVim, you need an external terminal emulator to start fzf with. `xterm`
command is used by default, but you can customize it with `g:fzf_launcher`.
[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim)
```vim
" This is the default. %s is replaced with fzf command
let g:fzf_launcher = 'xterm -e bash -ic %s'
" Use urxvt instead
let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s'
```
If you're running MacVim on OSX, I recommend you to use iTerm2 as the launcher.
Refer to the [this wiki
page](https://github.com/junegunn/fzf/wiki/fzf-with-MacVim-and-iTerm2) to see
how to set up.
### `fzf#run([options])`
#### `fzf#run([options])`
For more advanced uses, you can call `fzf#run()` function which returns the list
of the selected items.
`fzf#run()` may take an options-dictionary:
| 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 |
| `options` | string | Options to fzf |
| `dir` | string | Working directory |
| `tmux_width` | number/string | Use tmux vertical split with the given height (e.g. `20`, `50%`) |
| `tmux_height` | number/string | Use tmux horizontal split with the given height (e.g. `20`, `50%`) |
| `launcher` | string | External terminal emulator to start fzf with (Only used in GVim) |
| 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 | Options to fzf |
| `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) |
| `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `launcher` | string | External terminal emulator to start fzf with (GVim only) |
| `launcher` | funcref | Function for generating `launcher` string (GVim only) |
#### Examples
_However on Neovim `fzf#run` is asynchronous and does not return values so you
should use `sink` or `sink*` to process the output from fzf._
##### Examples
If `sink` option is not given, `fzf#run` will simply return the list.
@@ -380,10 +324,10 @@ nnoremap <silent> <Leader>C :call fzf#run({
\ 'source':
\ map(split(globpath(&rtp, "colors/*.vim"), "\n"),
\ "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"),
\ 'sink': 'colo',
\ 'options': '+m',
\ 'tmux_width': 20,
\ 'launcher': 'xterm -geometry 20x30 -e bash -ic %s'
\ 'sink': 'colo',
\ 'options': '+m',
\ 'left': 20,
\ 'launcher': 'xterm -geometry 20x30 -e bash -ic %s'
\ })<CR>
```
@@ -392,47 +336,46 @@ handy mapping that selects an open buffer.
```vim
" List of buffers
function! BufList()
function! s:buflist()
redir => ls
silent ls
redir END
return split(ls, '\n')
endfunction
function! BufOpen(e)
execute 'buffer '. matchstr(a:e, '^[ 0-9]*')
function! s:bufopen(e)
execute 'buffer' matchstr(a:e, '^[ 0-9]*')
endfunction
nnoremap <silent> <Leader><Enter> :call fzf#run({
\ 'source': reverse(BufList()),
\ 'sink': function('BufOpen'),
\ 'options': '+m',
\ 'tmux_height': '40%'
\ 'source': reverse(<sid>buflist()),
\ 'sink': function('<sid>bufopen'),
\ 'options': '+m',
\ 'down': len(<sid>buflist()) + 2
\ })<CR>
```
### Articles
- [fzf+vim+tmux](http://junegunn.kr/2014/04/fzf+vim+tmux)
More examples can be found on [the wiki
page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
Tips
----
### Rendering issues
#### Rendering issues
If you have any rendering issues, check the followings:
1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it
contains `256` (e.g. `xterm-256color`)
contains `256` (e.g. `xterm-256color`)
2. If you're on screen or tmux, `$TERM` should be either `screen` or
`screen-256color`
`screen-256color`
3. Some terminal emulators (e.g. mintty) have problem displaying default
background color and make some text unable to read. In that case, try `--black`
option. And if it solves your problem, I recommend including it in
`FZF_DEFAULT_OPTS` for further convenience.
background color and make some text unable to read. In that case, try
`--black` option. And if it solves your problem, I recommend including it
in `FZF_DEFAULT_OPTS` for further convenience.
4. If you still have problem, try `--no-256` option or even `--no-color`.
### Respecting `.gitignore`, `.hgignore`, and `svn:ignore`
#### Respecting `.gitignore`, `.hgignore`, and `svn:ignore`
[ag](https://github.com/ggreer/the_silver_searcher) or
[pt](https://github.com/monochromegane/the_platinum_searcher) will do the
@@ -449,60 +392,18 @@ export FZF_DEFAULT_COMMAND='ag -l -g ""'
fzf
```
### `git ls-tree` for fast traversal
#### `git ls-tree` for fast traversal
If you're running fzf in a large git repository, `git ls-tree` can boost up the
speed of the traversal.
```sh
# Copy the original fzf function to __fzf
declare -f __fzf > /dev/null ||
eval "$(echo "__fzf() {"; declare -f fzf | \grep -v '^{' | tail -n +2)"
# Use git ls-tree when possible
fzf() {
if [ -n "$(git rev-parse HEAD 2> /dev/null)" ]; then
FZF_DEFAULT_COMMAND="git ls-tree -r --name-only HEAD" __fzf "$@"
else
__fzf "$@"
fi
}
export FZF_DEFAULT_COMMAND='
(git ls-tree -r --name-only HEAD ||
find * -name ".*" -prune -o -type f -print -o -type l -print) 2> /dev/null'
```
### Using fzf with tmux splits
It isn't too hard to write your own fzf-tmux combo like the default
CTRL-T key binding. (Or is it?)
```sh
# This is a helper function that splits the current pane to start the given
# command ($1) and sends its output back to the original pane with any number of
# optional keys (shift; $*).
fzf_tmux_helper() {
[ -n "$TMUX_PANE" ] || return
local cmd=$1
shift
tmux split-window -p 40 \
"bash -c \"\$(tmux send-keys -t $TMUX_PANE \"\$(source ~/.fzf.bash; $cmd)\" $*)\""
}
# This is the function we are going to run in the split pane.
# - "find" to list the directories
# - "sed" will escape spaces in the paths.
# - "paste" will join the selected paths into a single line
fzf_tmux_dir() {
fzf_tmux_helper \
'find * -path "*/\.*" -prune -o -type d -print 2> /dev/null |
fzf --multi |
sed "s/ /\\\\ /g" |
paste -sd" " -' Space
}
# Bind CTRL-X-CTRL-D to fzf_tmux_dir
bind '"\C-x\C-d": "$(fzf_tmux_dir)\e\C-e"'
```
### Fish shell
#### Fish shell
It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362)
that it doesn't allow reading from STDIN in command substitution, which means
@@ -510,22 +411,10 @@ simple `vim (fzf)` won't work as expected. The workaround is to store the result
of fzf to a temporary file.
```sh
function vimf
if fzf > $TMPDIR/fzf.result
vim (cat $TMPDIR/fzf.result)
end
end
function fe
set tmp $TMPDIR/fzf.result
fzf --query="$argv[1]" --select-1 --exit-0 > $tmp
if [ (cat $tmp | wc -l) -gt 0 ]
vim (cat $tmp)
end
end
fzf > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result)
```
### Handling UTF-8 NFD paths on OSX
#### Handling UTF-8 NFD paths on OSX
Use iconv to convert NFD paths to NFC:
@@ -542,4 +431,3 @@ Author
------
Junegunn Choi

View File

@@ -1,9 +0,0 @@
require "bundler/gem_tasks"
require 'rake/testtask'
Rake::TestTask.new(:test) do |test|
test.pattern = 'test/**/test_*.rb'
test.verbose = true
end
task :default => :test

136
bin/fzf-tmux Executable file
View File

@@ -0,0 +1,136 @@
#!/usr/bin/env bash
# fzf-tmux: starts fzf in a tmux pane
# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
args=()
opt=""
skip=""
swap=""
close=""
term=""
while [ $# -gt 0 ]; do
arg="$1"
case "$arg" in
-)
term=1
;;
-w*|-h*|-d*|-u*|-r*|-l*)
if [ -n "$skip" ]; then
args+=("$1")
shift
continue
fi
if [[ "$arg" =~ ^.[lrw] ]]; 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
shift
if [[ "$1" =~ ^[0-9]+%?$ ]]; then
size="$1"
else
[ -n "$1" -a "$1" != "--" ] && args+=("$1")
shift
continue
fi
fi
if [[ "$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
[ -n "$COLUMNS" ] && max=$COLUMNS || max=$(tput cols)
else
[ -n "$LINES" ] && max=$LINES || max=$(tput 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
;;
*)
args+=("$1")
;;
esac
shift
done
if [ -z "$TMUX_PANE" ]; then
fzf "${args[@]}"
exit $?
fi
set -e
# Clean up named pipes on exit
id=$RANDOM
argsf=/tmp/fzf-args-$id
fifo1=/tmp/fzf-fifo1-$id
fifo2=/tmp/fzf-fifo2-$id
fifo3=/tmp/fzf-fifo3-$id
cleanup() {
rm -f $argsf $fifo1 $fifo2 $fifo3
}
trap cleanup EXIT SIGINT SIGTERM
fail() {
>&2 echo "$1"
exit 1
}
fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
[ -x "$fzf" ] || fail "fzf executable not found"
envs="env "
[ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
[ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
mkfifo $fifo2
mkfifo $fifo3
# Build arguments to fzf
opts=""
for arg in "${args[@]}"; do
opts="$opts \"${arg//\"/\\\"}\""
done
if [ -n "$term" -o -t 0 ]; then
cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf
tmux set-window-option -q synchronize-panes off \;\
split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap
else
mkfifo $fifo1
cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf
tmux set-window-option -q synchronize-panes off \;\
split-window $opt "$envs bash $argsf" $swap
cat <&0 > $fifo1 &
fi
cat $fifo2
[ "$(cat $fifo3)" = '0' ]

View File

@@ -1,9 +0,0 @@
require 'rubygems/dependency_installer'
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.1.0')
Gem::DependencyInstaller.new.install 'curses', '~> 1.0'
end
File.open(File.expand_path('../Rakefile', __FILE__), 'w') do |f|
f.puts 'task :default'
end

20
fzf
View File

@@ -8,6 +8,8 @@
# /_/ /___/_/ Fuzzy finder for your shell
#
# Version: 0.8.9 (Dec 24, 2014)
# Deprecation alert:
# This script is no longer maintained. Use the new Go version.
#
# Author: Junegunn Choi
# URL: https://github.com/junegunn/fzf
@@ -124,6 +126,7 @@ class FZF
@reverse = false
@prompt = '> '
@shr_mtx = Mutex.new
@expect = false
@print_query = false
argv =
@@ -198,6 +201,17 @@ class FZF
when '--no-print-query' then @print_query = false
when '-e', '--extended-exact' then @extended = :exact
when '+e', '--no-extended-exact' then @extended = nil
when '--expect'
argv.shift
@expect = true
when /^--expect=(.*)$/
@expect = true
when '--toggle-sort', '--tiebreak', '--color', '--bind'
argv.shift
when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll',
'--inline-info', '--no-inline-info', /^--bind=(.*)$/,
/^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/
# XXX
else
usage 1, "illegal option: #{o}"
end
@@ -272,10 +286,12 @@ class FZF
if loaded
if @select1 && len == 1
puts @query if @print_query
puts if @expect
burp(empty ? matches.first : matches.first.first)
exit 0
elsif @exit0 && len == 0
puts @query if @print_query
puts if @expect
exit 0
end
end
@@ -619,7 +635,8 @@ class FZF
def start_reader
stream =
if @source.tty?
if default_command = ENV['FZF_DEFAULT_COMMAND']
default_command = ENV['FZF_DEFAULT_COMMAND']
if default_command && !default_command.empty?
IO.popen(default_command)
elsif !`which find`.empty?
IO.popen("find * -path '*/\\.*' -prune -o -type f -print -o -type l -print 2> /dev/null")
@@ -1148,6 +1165,7 @@ class FZF
C.close_screen
q, selects = geta(:@query, :@selects)
@stdout.puts q if @print_query
@stdout.puts if @expect
if got
if selects.empty?
burp got

View File

@@ -1,9 +0,0 @@
#!/bin/zsh
# ____ ____
# / __/___ / __/
# / /_/_ / / /_
# / __/ / /_/ __/
# /_/ /___/_/-completion.zsh
#
# TODO

View File

@@ -1,17 +0,0 @@
# coding: utf-8
Gem::Specification.new do |spec|
spec.name = 'fzf'
spec.version = '0.8.4'
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'
spec.extensions += ['ext/mkrf_conf.rb']
end

334
install
View File

@@ -1,12 +1,19 @@
#!/usr/bin/env bash
version=0.9.0
version=0.9.12
cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd)
# If stdin is a tty, we are "interactive".
[ -t 0 ] && interactive=yes
ask() {
read -p "$1 ([y]/n) " -n 1 -r
# non-interactive shell: wait for a linefeed
# interactive shell: continue after a single keypress
[ -n "$interactive" ] && read_n='-n 1' || read_n=
read -p "$1 ([y]/n) " $read_n -r
echo
[[ ! $REPLY =~ ^[Nn]$ ]]
}
@@ -16,6 +23,7 @@ check_binary() {
local output=$("$fzf_base"/bin/fzf --version 2>&1)
if [ "$version" = "$output" ]; then
echo "$output"
binary_error=""
else
echo "$output != $version"
rm -f "$fzf_base"/bin/fzf
@@ -27,18 +35,21 @@ check_binary() {
symlink() {
echo " - Creating symlink: bin/$1 -> bin/fzf"
(cd "$fzf_base"/bin &&
rm -f fzf
rm -f fzf &&
ln -sf $1 fzf)
if [ $? -ne 0 ]; then
binary_error="Failed to create symlink"
return 1
fi
}
download() {
echo "Downloading bin/fzf ..."
if [ -x "$fzf_base"/bin/fzf ]; then
if [[ ! $1 =~ dev && -x "$fzf_base"/bin/fzf ]]; then
echo " - Already exists"
check_binary && return
elif [ -x "$fzf_base"/bin/$1 ]; then
symlink $1
check_binary && return
symlink $1 && check_binary && return
fi
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
if [ $? -ne 0 ]; then
@@ -73,7 +84,7 @@ case "$archi" in
Darwin\ i*86) download fzf-$version-darwin_386 ;;
Linux\ x86_64) download fzf-$version-linux_amd64 ;;
Linux\ i*86) download fzf-$version-linux_386 ;;
*) binary_available=0 ;;
*) binary_available=0 binary_error=1 ;;
esac
cd "$fzf_base"
@@ -140,8 +151,18 @@ if [ -n "$binary_error" ]; then
echo "< 1.9"
fzf_cmd="$ruby $fzf_base/fzf"
fi
# Create fzf script
echo -n "Creating wrapper script for fzf ... "
rm -f "$fzf_base"/bin/fzf
echo "#!/bin/sh" > "$fzf_base"/bin/fzf
echo "$fzf_cmd \"\$@\"" >> "$fzf_base"/bin/fzf
chmod +x "$fzf_base"/bin/fzf
echo "OK"
fi
[[ "$*" =~ "--bin" ]] && exit 0
# Auto-completion
ask "Do you want to add auto-completion support?"
auto_completion=$?
@@ -155,293 +176,62 @@ for shell in bash zsh; do
echo -n "Generate ~/.fzf.$shell ... "
src=~/.fzf.${shell}
fzf_completion="[[ \$- =~ i ]] && source $fzf_base/fzf-completion.${shell}"
fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null"
if [ $auto_completion -ne 0 ]; then
fzf_completion="# $fzf_completion"
fi
if [ -n "$binary_error" ]; then
cat > $src << EOF
# Setup fzf function
# ------------------
unalias fzf 2> /dev/null
fzf() {
$fzf_cmd "\$@"
}
export -f fzf > /dev/null
fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
if [ $key_bindings -ne 0 ]; then
fzf_key_bindings="# $fzf_key_bindings"
fi
# Auto-completion
# ---------------
$fzf_completion
EOF
else
cat > $src << EOF
cat > $src << EOF
# Setup fzf
# ---------
unalias fzf 2> /dev/null
unset fzf 2> /dev/null
if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then
export PATH="$fzf_base/bin:\$PATH"
export PATH="\$PATH:$fzf_base/bin"
fi
# Man path
# --------
if [[ ! "\$MANPATH" =~ "$fzf_base/man" && -d "$fzf_base/man" ]]; then
export MANPATH="\$MANPATH:$fzf_base/man"
fi
# Auto-completion
# ---------------
$fzf_completion
# Key bindings
# ------------
$fzf_key_bindings
EOF
fi
if [ $key_bindings -eq 0 ]; then
if [ $shell = bash ]; then
cat >> $src << "EOFZF"
# Key bindings
# ------------
__fsel() {
command find * -path '*/\.*' -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | fzf -m | while read item; do
printf '%q ' "$item"
done
echo
}
if [[ $- =~ i ]]; then
__fsel_tmux() {
local height
height=${FZF_TMUX_HEIGHT:-40%}
if [[ $height =~ %$ ]]; then
height="-p ${height%\%}"
else
height="-l $height"
fi
tmux split-window $height "bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'"
}
__fcd() {
local dir
dir=$(command find -L ${1:-*} -path '*/\.*' -prune -o -type d -print 2> /dev/null | fzf +m) && printf 'cd %q' "$dir"
}
__use_tmux=0
[ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1
if [ -z "$(set -o | \grep '^vi.*on')" ]; then
# Required to refresh the prompt after fzf
bind '"\er": redraw-current-line'
# CTRL-T - Paste the selected file path into the command line
if [ $__use_tmux -eq 1 ]; then
bind '"\C-t": " \C-u \C-a\C-k$(__fsel_tmux)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"'
else
bind '"\C-t": " \C-u \C-a\C-k$(__fsel)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"'
fi
# CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s +m -n2..,.. | sed \"s/ *[0-9]* *//\")\e\C-e\er"'
# ALT-C - cd into the selected directory
bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"'
else
bind '"\C-x\C-e": shell-expand-line'
bind '"\C-x\C-r": redraw-current-line'
# CTRL-T - Paste the selected file path into the command line
# - FIXME: Selected items are attached to the end regardless of cursor position
if [ $__use_tmux -eq 1 ]; then
bind '"\C-t": "\e$a \eddi$(__fsel_tmux)\C-x\C-e\e0P$xa"'
else
bind '"\C-t": "\e$a \eddi$(__fsel)\C-x\C-e\e0Px$a \C-x\C-r\exa "'
fi
bind -m vi-command '"\C-t": "i\C-t"'
# CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s +m -n2..,.. | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"'
bind -m vi-command '"\C-r": "i\C-r"'
# ALT-C - cd into the selected directory
bind '"\ec": "\eddi$(__fcd)\C-x\C-e\C-x\C-r\C-m"'
bind -m vi-command '"\ec": "i\ec"'
fi
unset __use_tmux
fi
EOFZF
else
cat >> $src << "EOFZF"
# Key bindings
# ------------
# CTRL-T - Paste the selected file path(s) into the command line
__fsel() {
set -o nonomatch
command find * -path '*/\.*' -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | fzf -m | while read item; do
printf '%q ' "$item"
done
echo
}
if [[ $- =~ i ]]; then
if [ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ]; then
fzf-file-widget() {
local height
height=${FZF_TMUX_HEIGHT:-40%}
if [[ $height =~ %$ ]]; then
height="-p ${height%\%}"
else
height="-l $height"
fi
tmux split-window $height "zsh -c 'source ~/.fzf.zsh; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'"
}
else
fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)"
zle redisplay
}
fi
zle -N fzf-file-widget
bindkey '^T' fzf-file-widget
# ALT-C - cd into the selected directory
fzf-cd-widget() {
cd "${$(set -o nonomatch; command find -L * -path '*/\.*' -prune \
-o -type d -print 2> /dev/null | fzf):-.}"
zle reset-prompt
}
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() {
LBUFFER=$(fc -l 1 | fzf +s +m -n2..,.. | sed "s/ *[0-9*]* *//")
zle redisplay
}
zle -N fzf-history-widget
bindkey '^R' fzf-history-widget
fi
EOFZF
fi
fi
echo "OK"
done
# fish
has_fish=0
if [ -n "$(which fish)" ]; then
if [ -n "$(which fish 2> /dev/null)" ]; then
has_fish=1
echo -n "Generate ~/.config/fish/functions/fzf.fish ... "
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 ~/.config/fish/functions
if [ -n "$binary_error" ]; then
cat > ~/.config/fish/functions/fzf.fish << EOFZF
function fzf
$fzf_cmd \$argv
end
EOFZF
else
cat > ~/.config/fish/functions/fzf.fish << EOFZF
function fzf
$fzf_base/bin/fzf \$argv
end
EOFZF
if [ -e ~/.config/fish/functions/fzf.fish ]; then
echo -n "Remove unnecessary ~/.config/fish/functions/fzf.fish ... "
rm -f ~/.config/fish/functions/fzf.fish && echo "OK" || echo "Failed"
fi
echo "OK"
if [ $key_bindings -eq 0 ]; then
echo -n "Generate ~/.config/fish/functions/fzf_key_bindings.fish ... "
cat > ~/.config/fish/functions/fzf_key_bindings.fish << "EOFZF"
function fzf_key_bindings
# Due to a bug of fish, we cannot use command substitution,
# so we use temporary file instead
if [ -z "$TMPDIR" ]
set -g TMPDIR /tmp
end
function __fzf_list
command find * -path '*/\.*' -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null
end
function __fzf_list_dir
command find -L * -path '*/\.*' -prune -o -type d -print 2> /dev/null
end
function __fzf_escape
while read item
echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' '
end
end
function __fzf_ctrl_t
if [ -n "$TMUX_PANE" -a "$FZF_TMUX" != "0" ]
tmux split-window (__fzf_tmux_height) "fish -c 'fzf_key_bindings; __fzf_ctrl_t_tmux \\$TMUX_PANE'"
else
__fzf_list | fzf -m > $TMPDIR/fzf.result
and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape)
commandline -f repaint
rm -f $TMPDIR/fzf.result
end
end
function __fzf_ctrl_t_tmux
__fzf_list | fzf -m > $TMPDIR/fzf.result
and tmux send-keys -t $argv[1] (cat $TMPDIR/fzf.result | __fzf_escape)
rm -f $TMPDIR/fzf.result
end
function __fzf_reverse
if which tac > /dev/null
tac $argv
else
tail -r $argv
end
end
function __fzf_ctrl_r
history | __fzf_reverse | fzf +s +m > $TMPDIR/fzf.result
and commandline (cat $TMPDIR/fzf.result)
commandline -f repaint
rm -f $TMPDIR/fzf.result
end
function __fzf_alt_c
# Fish hangs if the command before pipe redirects (2> /dev/null)
__fzf_list_dir | fzf +m > $TMPDIR/fzf.result
[ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ]
and cd (cat $TMPDIR/fzf.result)
commandline -f repaint
rm -f $TMPDIR/fzf.result
end
function __fzf_tmux_height
if set -q FZF_TMUX_HEIGHT
set height $FZF_TMUX_HEIGHT
else
set height 40%
end
if echo $height | \grep -q -E '%$'
echo "-p "(echo $height | sed 's/%$//')
else
echo "-l $height"
end
set -e height
end
bind \ct '__fzf_ctrl_t'
bind \cr '__fzf_ctrl_r'
bind \ec '__fzf_alt_c'
end
EOFZF
echo "OK"
echo -n "Symlink ~/.config/fish/functions/fzf_key_bindings.fish ... "
ln -sf $fzf_base/shell/key-bindings.fish \
~/.config/fish/functions/fzf_key_bindings.fish && echo "OK" || echo "Failed"
fi
fi
@@ -471,12 +261,6 @@ done
if [ $key_bindings -eq 0 -a $has_fish -eq 1 ]; then
bind_file=~/.config/fish/functions/fish_user_key_bindings.fish
append_line "fzf_key_bindings" "$bind_file"
echo ' * Due to a known bug of fish, you may have issues running fzf on fish.'
echo ' * If that happens, try the following:'
echo ' - Remove ~/.config/fish/functions/fzf.fish'
echo ' - Place fzf executable in a directory included in $PATH'
echo
fi
cat << EOF

274
man/man1/fzf.1 Normal file
View File

@@ -0,0 +1,274 @@
.ig
The MIT License (MIT)
Copyright (c) 2015 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 "May 2015" "fzf 0.9.12" "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
.TP
.B "-e, --extended-exact"
Extended-search mode (exact match)
.TP
.B "-i"
Case-insensitive match (default: smart-case match)
.TP
.B "+i"
Case-sensitive match
.TP
.BI "-n, --nth=" "N[,..]"
Comma-separated list of field index expressions for limiting search scope.
See \fBFIELD INDEX EXPRESSION\fR for details.
.TP
.BI "--with-nth=" "N[,..]"
Transform the item using the list of index expressions for search
.TP
.BI "-d, --delimiter=" "STR"
Field delimiter regex for \fI--nth\fR and \fI--with-nth\fR (default: AWK-style)
.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"
Sort criterion to use when the scores are tied
.br
.R ""
.br
.BR length " Prefers item with shorter length"
.br
.BR begin " Prefers item with matched substring closer to the beginning"
.br
.BR end " Prefers item with matched substring closer to the end"
.br
.BR index " Prefers item that appeared earlier in the input stream"
.br
.SS Interface
.TP
.B "-m, --multi"
Enable multi-select with tab/shift-tab
.TP
.B "--ansi"
Enable processing of ANSI color codes
.TP
.B "--no-mouse"
Disable mouse
.TP
.B "--color=COL"
Color scheme: [dark|light|16|bw]
.br
(default: dark on 256-color terminal, otherwise 16)
.br
.R ""
.br
.BR dark " Color scheme for dark 256-color terminal"
.br
.BR light " Color scheme for light 256-color terminal"
.br
.BR 16 " Color scheme for 16-color terminal"
.br
.BR bw " No colors"
.br
.TP
.B "--black"
Use black background
.TP
.B "--reverse"
Reverse orientation
.TP
.B "--no-hscroll"
Disable horizontal scroll
.TP
.B "--inline-info"
Display finder info inline with the query
.TP
.BI "--prompt=" "STR"
Input prompt (default: '> ')
.TP
.BI "--toggle-sort=" "KEY"
Key to toggle sort (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR,
or any single character)
.TP
.BI "--bind=" "KEYBINDS"
Comma-separated list of custom key bindings. Each key binding expression
follows the following format: \fBKEY:ACTION\fR
.RS
e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.RE
.RS
.B KEY:
\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, or any single character
.RE
.RS
.B ACTION:
abort
accept
backward-char
backward-delete-char
backward-kill-word
backward-word
beginning-of-line
clear-screen
delete-char
down
end-of-line
forward-char
forward-word
kill-line (not bound)
kill-word
page-down
page-up
toggle (not bound)
toggle-down
toggle-sort (not bound; equivalent to \fB--toggle-sort\fR)
toggle-up
unix-line-discard
unix-word-rubout
up
yank
.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 (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR,
or any single character) 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.
.RS
e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
.RE
.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
.SH ENVIRONMENT
.TP
.B FZF_DEFAULT_COMMAND
Default command to use when input is tty
.TP
.B FZF_DEFAULT_OPTS
Default options. e.g. \fB--extended --ansi\fR
.SH EXIT STATUS
.BR 0 " Normal exit"
.br
.BR 1 " 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]). \fI--nth\fR and \fI--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
With \fI-x\fR or \fI--extended\fR option, 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
.SS Exact-match (quoted)
A term that is prefixed by a single-quote character (') 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 ^, or suffixed by $ to become an anchored-match term.
Then fzf will search for the items 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 !, fzf will exclude the items that satisfy the term
from the result.
.SS Extended-exact mode
If you don't need fuzzy matching at all and do not wish to "quote" (prefixing
with ') every word, start fzf with \fI-e\fR or \fI--extended-exact\fR option
(instead of \fI-x\fR or \fI--extended\fR).
.SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR)
.SH SEE ALSO
.I https://github.com/junegunn/fzf
.SH LICENSE
MIT

View File

@@ -21,32 +21,37 @@
" 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:min_tmux_width = 10
let s:min_tmux_height = 3
let s:default_tmux_height = '40%'
let s:launcher = 'xterm -e bash -ic %s'
let s:default_height = '40%'
let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf'
let s:fzf_rb = expand('<sfile>:h:h').'/fzf'
let s:install = expand('<sfile>:h:h').'/install'
let s:installed = 0
let s:fzf_tmux = expand('<sfile>:h:h').'/bin/fzf-tmux'
let s:cpo_save = &cpo
set cpo&vim
function! s:fzf_exec()
if !exists('s:exec')
call system('type fzf')
if v:shell_error
let s:exec = executable(s:fzf_go) ?
\ s:fzf_go : (executable(s:fzf_rb) ? s:fzf_rb : '')
else
if executable(s:fzf_go)
let s:exec = s:fzf_go
elseif executable('fzf')
let s:exec = 'fzf'
elseif !s:installed && executable(s:install) &&
\ input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
redraw
echo
echohl WarningMsg
echo 'Downloading fzf binary. Please wait ...'
echohl None
let s:installed = 1
call system(s:install.' --bin')
return s:fzf_exec()
else
redraw
throw 'fzf executable not found'
endif
return s:fzf_exec()
elseif empty(s:exec)
unlet s:exec
throw 'fzf executable not found'
else
return s:exec
endif
return s:exec
endfunction
function! s:tmux_enabled()
@@ -59,7 +64,7 @@ function! s:tmux_enabled()
endif
let s:tmux = 0
if exists('$TMUX')
if exists('$TMUX') && executable(s:fzf_tmux)
let output = system('tmux -V')
let s:tmux = !v:shell_error && output >= 'tmux 1.7'
endif
@@ -71,11 +76,35 @@ function! s:shellesc(arg)
endfunction
function! s:escape(path)
return substitute(a:path, ' ', '\\ ', 'g')
return escape(a:path, ' %#\')
endfunction
" Upgrade legacy options
function! s:upgrade(dict)
let copy = copy(a:dict)
if has_key(copy, 'tmux')
let copy.down = remove(copy, 'tmux')
endif
if has_key(copy, 'tmux_height')
let copy.down = remove(copy, 'tmux_height')
endif
if has_key(copy, 'tmux_width')
let copy.right = remove(copy, 'tmux_width')
endif
return copy
endfunction
function! fzf#run(...) abort
let dict = exists('a:1') ? a:1 : {}
try
let oshell = &shell
set shell=sh
if has('nvim') && bufexists('[FZF]')
echohl WarningMsg
echomsg 'FZF is already running!'
echohl None
return []
endif
let dict = exists('a:1') ? s:upgrade(a:1) : {}
let temps = { 'result': tempname() }
let optstr = get(dict, 'options', '')
try
@@ -99,26 +128,62 @@ function! fzf#run(...) abort
else
let prefix = ''
endif
let command = prefix.fzf_exec.' '.optstr.' > '.temps.result
let tmux = !has('nvim') && s:tmux_enabled() && s:splittable(dict)
let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if s:tmux_enabled() && s:tmux_splittable(dict)
return s:execute_tmux(dict, command, temps)
else
return s:execute(dict, command, temps)
endif
try
if tmux
return s:execute_tmux(dict, command, temps)
elseif has('nvim')
return s:execute_term(dict, command, temps)
else
return s:execute(dict, command, temps)
endif
finally
call s:popd(dict)
endtry
finally
let &shell = oshell
endtry
endfunction
function! s:tmux_splittable(dict)
return
\ min([&columns, get(a:dict, 'tmux_width', 0)]) >= s:min_tmux_width ||
\ min([&lines, get(a:dict, 'tmux_height', get(a:dict, 'tmux', 0))]) >= s:min_tmux_height
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 = ''
for o in ['up', 'down', 'left', 'right']
if s:present(a:dict, o)
let size = '-'.o[0].(a:dict[o] == 1 ? '' : a:dict[o])
break
endif
endfor
return printf('LINES=%d COLUMNS=%d %s %s %s --',
\ &lines, &columns, s:fzf_tmux, size, (has_key(a:dict, 'source') ? '' : '-'))
endfunction
function! s:splittable(dict)
return s:present(a:dict, 'up', 'down', 'left', 'right')
endfunction
function! s:pushd(dict)
if !empty(get(a:dict, 'dir', ''))
let a:dict.prev_dir = getcwd()
if s:present(a:dict, 'dir')
let cwd = getcwd()
if get(a:dict, 'prev_dir', '') ==# cwd
return 1
endif
let a:dict.prev_dir = cwd
execute 'chdir '.s:escape(a:dict.dir)
let a:dict.dir = getcwd()
return 1
endif
return 0
endfunction
function! s:popd(dict)
@@ -127,12 +192,25 @@ function! s:popd(dict)
endif
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,
\ synIDattr(hlID("Normal"), "bg"), synIDattr(hlID("Normal"), "fg"),
\ &columns, &lines/2, getwinposx(), getwinposy())
endfunction
unlet! s:launcher
let s:launcher = function('s:xterm_launcher')
function! s:execute(dict, command, temps)
call s:pushd(a:dict)
silent !clear
silent! !clear 2> /dev/null
if has('gui_running')
let launcher = get(a:dict, 'launcher', get(g:, 'fzf_launcher', s:launcher))
let command = printf(launcher, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'")
let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher)))
let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher
let command = printf(fmt, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'")
else
let command = a:command
endif
@@ -146,105 +224,155 @@ function! s:execute(dict, command, temps)
endif
return []
else
return s:callback(a:dict, a:temps, 0)
endif
endfunction
function! s:env_var(name)
if exists('$'.a:name)
return a:name . "='". substitute(expand('$'.a:name), "'", "'\\\\''", 'g') . "' "
else
return ''
return s:callback(a:dict, a:temps)
endif
endfunction
function! s:execute_tmux(dict, command, temps)
let command = s:env_var('FZF_DEFAULT_OPTS').s:env_var('FZF_DEFAULT_COMMAND').a:command
if !empty(get(a:dict, 'dir', ''))
let command = a:command
if s:pushd(a:dict)
" -c '#{pane_current_path}' is only available on tmux 1.9 or above
let command = 'cd '.s:escape(a:dict.dir).' && '.command
endif
let splitopt = '-v'
if has_key(a:dict, 'tmux_width')
let splitopt = '-h'
let size = a:dict.tmux_width
else
let size = get(a:dict, 'tmux_height', get(a:dict, 'tmux'))
endif
if type(size) == 1 && size =~ '%$'
let sizeopt = '-p '.size[0:-2]
else
let sizeopt = '-l '.size
endif
let s:pane = substitute(
\ system(
\ printf(
\ 'tmux split-window %s %s -P -F "#{pane_id}" %s',
\ splitopt, sizeopt, s:shellesc(command))), '\n', '', 'g')
let s:dict = a:dict
let s:temps = a:temps
augroup fzf_tmux
autocmd!
autocmd VimResized * nested call s:tmux_check()
augroup END
call system(command)
redraw!
return s:callback(a:dict, a:temps)
endfunction
function! s:tmux_check()
let panes = split(system('tmux list-panes -a -F "#{pane_id}"'), '\n')
if index(panes, s:pane) < 0
augroup fzf_tmux
autocmd!
augroup END
call s:callback(s:dict, s:temps, 1)
redraw
function! s:calc_size(max, val)
if a:val =~ '%$'
return a:max * str2nr(a:val[:-2]) / 100
else
return min([a:max, a:val])
endif
endfunction
function! s:callback(dict, temps, cd)
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 s:ptab = tabpagenr()
try
for [dir, triple] in items(directions)
let val = get(a:dict, dir, '')
if !empty(val)
let [cmd, resz, max] = triple
let sz = s:calc_size(max, val)
execute cmd sz.'new'
execute resz sz
return
endif
endfor
if s:present(a:dict, 'window')
execute a:dict.window
else
tabnew
endif
finally
setlocal winfixwidth winfixheight buftype=nofile bufhidden=wipe nobuflisted
endtry
endfunction
function! s:execute_term(dict, command, temps)
call s:split(a:dict)
call s:pushd(a:dict)
let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps }
function! fzf.on_exit(id, code)
let tab = tabpagenr()
if bufnr('') == self.buf
" We use close instead of bd! since Vim does not close the split when
" there's no other listed buffer
close
" FIXME This should be unnecessary due to `bufhidden=wipe` but in some
" cases Neovim fails to clean up the buffer and `bufexists('[FZF]')
" returns 1 even when it cannot be seen anywhere else. e.g. `FZF!`
silent! execute 'bd!' self.buf
endif
if s:ptab == tab
wincmd p
endif
call s:pushd(self.dict)
try
redraw!
call s:callback(self.dict, self.temps)
finally
call s:popd(self.dict)
endtry
endfunction
call termopen(a:command, fzf)
silent file [FZF]
startinsert
return []
endfunction
function! s:callback(dict, temps)
try
if !filereadable(a:temps.result)
let lines = []
else
if a:cd | call s:pushd(a:dict) | endif
let lines = readfile(a:temps.result)
if has_key(a:dict, 'sink')
for line in lines
if type(a:dict.sink) == 2
call a:dict.sink(line)
else
execute a:dict.sink.' '.s:escape(line)
execute a:dict.sink s:escape(line)
endif
endfor
endif
if has_key(a:dict, 'sink*')
call a:dict['sink*'](lines)
endif
endif
for tf in values(a:temps)
silent! call delete(tf)
endfor
call s:popd(a:dict)
return lines
catch
if stridx(v:exception, ':E325:') < 0
echoerr v:exception
endif
endtry
endfunction
let s:default_action = {
\ 'ctrl-m': 'e',
\ 'ctrl-t': 'tabedit',
\ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' }
function! s:cmd_callback(lines) abort
if empty(a:lines)
return
endif
let key = remove(a:lines, 0)
let cmd = get(s:action, key, 'e')
for item in a:lines
execute cmd s:escape(item)
endfor
endfunction
function! s:cmd(bang, ...) abort
let args = copy(a:000)
let s:action = get(g:, 'fzf_action', s:default_action)
let args = extend(['--expect='.join(keys(s:action), ',')], a:000)
let opts = {}
if len(args) > 0 && isdirectory(expand(args[-1]))
let opts.dir = remove(args, -1)
endif
if !a:bang
let opts.tmux = get(g:, 'fzf_tmux_height', s:default_tmux_height)
let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height))
endif
call fzf#run(extend({ 'sink': 'e', 'options': join(args) }, opts))
call fzf#run(extend({'options': join(args), 'sink*': function('<sid>cmd_callback')}, opts))
endfunction
command! -nargs=* -complete=dir -bang FZF call s:cmd('<bang>' == '!', <f-args>)
command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>)
let &cpo = s:cpo_save
unlet s:cpo_save

View File

@@ -5,6 +5,8 @@
# / __/ / /_/ __/
# /_/ /___/_/-completion.bash
#
# - $FZF_TMUX (default: 1)
# - $FZF_TMUX_HEIGHT (default: '40%')
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
@@ -24,23 +26,34 @@ _fzf_opts_completion() {
-i +i
-n --nth
-d --delimiter
-s --sort +s
+s --no-sort
--tac
--tiebreak
--bind
-m --multi
--no-mouse
+c --no-color
+2 --no-256
--color
--black
--reverse
--no-hscroll
--inline-info
--prompt
-q --query
-1 --select-1
-0 --exit-0
-f --filter
--print-query"
--print-query
--expect
--toggle-sort
--sync"
case "${prev}" in
--sort|-s)
COMPREPLY=( $(compgen -W "$(seq 2000 1000 10000)" -- ${cur}) )
--tiebreak)
COMPREPLY=( $(compgen -W "length begin end index" -- ${cur}) )
return 0
;;
--color)
COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) )
return 0
;;
esac
@@ -71,10 +84,11 @@ _fzf_handle_dynamic_completion() {
}
_fzf_path_completion() {
local cur base dir leftover matches trigger cmd
local cur base dir leftover matches trigger cmd fzf
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g')
COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER:-**}
trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ ${cur} == *"$trigger" ]]; then
base=${cur:0:${#cur}-${#trigger}}
@@ -87,8 +101,8 @@ _fzf_path_completion() {
leftover=${leftover/#\/}
[ "$dir" = './' ] && dir=''
tput sc
matches=$(find -L "$dir"* $1 2> /dev/null | fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do
printf '%q ' "$item"
matches=$(find -L "$dir"* $1 2> /dev/null | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do
printf "%q$3 " "$item"
done)
matches=${matches% }
if [ -n "$matches" ]; then
@@ -103,6 +117,7 @@ _fzf_path_completion() {
[[ "$dir" =~ /$ ]] || dir="$dir"/
done
else
shift
shift
shift
_fzf_handle_dynamic_completion "$cmd" "$@"
@@ -110,16 +125,17 @@ _fzf_path_completion() {
}
_fzf_list_completion() {
local cur selected trigger cmd src
local cur selected trigger cmd src fzf
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
read -r src
cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g')
trigger=${FZF_COMPLETION_TRIGGER:-**}
trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ ${cur} == *"$trigger" ]]; then
cur=${cur:0:${#cur}-${#trigger}}
tput sc
selected=$(eval "$src | fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ')
selected=$(eval "$src | $fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ')
selected=${selected% }
tput rc
@@ -136,27 +152,28 @@ _fzf_list_completion() {
_fzf_all_completion() {
_fzf_path_completion \
"-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \
"-m" "$@"
"-m" "" "$@"
}
_fzf_file_completion() {
_fzf_path_completion \
"-name .git -prune -o -name .svn -prune -o -type f -print -o -type l -print" \
"-m" "$@"
"-m" "" "$@"
}
_fzf_dir_completion() {
_fzf_path_completion \
"-name .git -prune -o -name .svn -prune -o -type d -print" \
"" "$@"
"" "/" "$@"
}
_fzf_kill_completion() {
[ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1
local selected
local selected fzf
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
tput sc
selected=$(ps -ef | sed 1d | fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ')
selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ')
tput rc
if [ -n "$selected" ]; then
@@ -167,13 +184,13 @@ _fzf_kill_completion() {
_fzf_telnet_completion() {
_fzf_list_completion '+m' "$@" << "EOF"
\grep -v '^\s*\(#\|$\)' /etc/hosts | awk '{if (length($2) > 0) {print $2}}' | sort -u
\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u
EOF
}
_fzf_ssh_completion() {
_fzf_list_completion '+m' "$@" << "EOF"
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i ^host | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts) | awk '{print $2}' | sort -u
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u
EOF
}
@@ -206,11 +223,11 @@ a_cmds="
x_cmds="kill ssh telnet unset unalias export"
# Preserve existing completion
if [ "$_fzf_completion_loaded" != '0.8.6-1' ]; then
if [ "$_fzf_completion_loaded" != '0.9.12' ]; then
# Really wish I could use associative array but OSX comes with bash 3.2 :(
eval $(complete | \grep '\-F' | \grep -v _fzf_ |
\grep -E " ($(echo $d_cmds $f_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter)
export _fzf_completion_loaded=0.8.6-1
export _fzf_completion_loaded=0.9.12
fi
if type _completion_loader > /dev/null 2>&1; then
@@ -219,7 +236,7 @@ fi
# Directory
for cmd in $d_cmds; do
complete -F _fzf_dir_completion -o default -o bashdefault $cmd
complete -F _fzf_dir_completion -o nospace -o plusdirs $cmd
done
# File

158
shell/completion.zsh Normal file
View File

@@ -0,0 +1,158 @@
#!/bin/zsh
# ____ ____
# / __/___ / __/
# / /_/_ / / /_
# / __/ / /_/ __/
# /_/ /___/_/-completion.zsh
#
# - $FZF_TMUX (default: 1)
# - $FZF_TMUX_HEIGHT (default: '40%')
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
_fzf_path_completion() {
local base lbuf find_opts fzf_opts suffix tail fzf dir leftover matches nnm
base=${(Q)1}
lbuf=$2
find_opts=$3
fzf_opts=$4
suffix=$5
tail=$6
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
if ! setopt | grep nonomatch > /dev/null; then
nnm=1
setopt nonomatch
fi
dir="$base"
while [ 1 ]; do
if [ -z "$dir" -o -d ${~dir} ]; then
leftover=${base/#"$dir"}
leftover=${leftover/#\/}
[ "$dir" = './' ] && dir=''
dir=${~dir}
matches=$(\find -L $dir* ${=find_opts} 2> /dev/null | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do
printf "%q$suffix " "$item"
done)
matches=${matches% }
if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches$tail"
fi
zle redisplay
break
fi
dir=$(dirname "$dir")
dir=${dir%/}/
done
[ -n "$nnm" ] && unsetopt nonomatch
}
_fzf_all_completion() {
_fzf_path_completion "$1" "$2" \
"-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \
"-m" "" " "
}
_fzf_dir_completion() {
_fzf_path_completion "$1" "$2" \
"-name .git -prune -o -name .svn -prune -o -type d -print" \
"" "/" ""
}
_fzf_list_completion() {
local prefix lbuf fzf_opts src fzf matches
prefix=$1
lbuf=$2
fzf_opts=$3
read -r src
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
matches=$(eval "$src" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$prefix")
if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches "
fi
zle redisplay
}
_fzf_telnet_completion() {
_fzf_list_completion "$1" "$2" '+m' << "EOF"
\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u
EOF
}
_fzf_ssh_completion() {
_fzf_list_completion "$1" "$2" '+m' << "EOF"
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u
EOF
}
_fzf_env_var_completion() {
_fzf_list_completion "$1" "$2" '+m' << "EOF"
declare -xp | sed 's/=.*//' | sed 's/.* //'
EOF
}
_fzf_alias_completion() {
_fzf_list_completion "$1" "$2" '+m' << "EOF"
alias | sed 's/=.*//'
EOF
}
fzf-completion() {
local tokens cmd prefix trigger tail fzf matches lbuf d_cmds
# 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
eval "zle ${fzf_default_completion:-expand-or-complete}"
return
fi
cmd=${tokens[1]}
# Explicitly allow for empty trigger.
trigger=${FZF_COMPLETION_TRIGGER-'**'}
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
# Kill completion (do not require trigger sequence)
if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
matches=$(ps -ef | sed 1d | ${=fzf} ${=FZF_COMPLETION_OPTS} -m | awk '{print $2}' | tr '\n' ' ')
if [ -n "$matches" ]; then
LBUFFER="$LBUFFER$matches"
fi
zle redisplay
# Trigger sequence given
elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
d_cmds=(cd pushd rmdir)
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
[ -z "${tokens[-1]}" ] && lbuf=$LBUFFER || lbuf=${LBUFFER:0:-${#tokens[-1]}}
if [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
_fzf_dir_completion "$prefix" $lbuf
elif [ $cmd = telnet ]; then
_fzf_telnet_completion "$prefix" $lbuf
elif [ $cmd = ssh ]; then
_fzf_ssh_completion "$prefix" $lbuf
elif [ $cmd = unset -o $cmd = export ]; then
_fzf_env_var_completion "$prefix" $lbuf
elif [ $cmd = unalias ]; then
_fzf_alias_completion "$prefix" $lbuf
else
_fzf_all_completion "$prefix" $lbuf
fi
# Fall back to default completion
else
eval "zle ${fzf_default_completion:-expand-or-complete}"
fi
}
[ -z "$fzf_default_completion" ] &&
fzf_default_completion=$(bindkey '^I' | grep -v undefined-key | awk '{print $2}')
zle -N fzf-completion
bindkey '^I' fzf-completion

89
shell/key-bindings.bash Normal file
View File

@@ -0,0 +1,89 @@
# Key bindings
# ------------
__fzf_select__() {
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do
printf '%q ' "$item"
done
echo
}
if [[ $- =~ i ]]; then
__fzfcmd() {
[ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
}
__fzf_select_tmux__() {
local height
height=${FZF_TMUX_HEIGHT:-40%}
if [[ $height =~ %$ ]]; then
height="-p ${height%\%}"
else
height="-l $height"
fi
tmux split-window $height "cd $(printf %q "$PWD");bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'"
}
__fzf_cd__() {
local dir
dir=$(command find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m) && printf 'cd %q' "$dir"
}
__fzf_history__() {
local line
line=$(
HISTTIMEFORMAT= history |
$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r |
\grep '^ *[0-9]') && sed 's/ *\([0-9]*\)\** .*/!\1/' <<< "$line"
}
__use_tmux=0
[ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1
if [ -z "$(set -o | \grep '^vi.*on')" ]; then
# Required to refresh the prompt after fzf
bind '"\er": redraw-current-line'
bind '"\e^": history-expand-line'
# CTRL-T - Paste the selected file path into the command line
if [ $__use_tmux -eq 1 ]; then
bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select_tmux__)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"'
else
bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select__)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"'
fi
# CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": " \C-e\C-u$(__fzf_history__)\e\C-e\e^\er"'
# ALT-C - cd into the selected directory
bind '"\ec": " \C-e\C-u$(__fzf_cd__)\e\C-e\er\C-m"'
else
bind '"\C-x\C-e": shell-expand-line'
bind '"\C-x\C-r": redraw-current-line'
bind '"\C-x^": history-expand-line'
# CTRL-T - Paste the selected file path into the command line
# - FIXME: Selected items are attached to the end regardless of cursor position
if [ $__use_tmux -eq 1 ]; then
bind '"\C-t": "\e$a \eddi$(__fzf_select_tmux__)\C-x\C-e\e0P$xa"'
else
bind '"\C-t": "\e$a \eddi$(__fzf_select__)\C-x\C-e\e0Px$a \C-x\C-r\exa "'
fi
bind -m vi-command '"\C-t": "i\C-t"'
# CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": "\eddi$(__fzf_history__)\C-x\C-e\C-x^\e$a\C-x\C-r"'
bind -m vi-command '"\C-r": "i\C-r"'
# ALT-C - cd into the selected directory
bind '"\ec": "\eddi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"'
bind -m vi-command '"\ec": "i\ec"'
fi
unset __use_tmux
fi

67
shell/key-bindings.fish Normal file
View File

@@ -0,0 +1,67 @@
# Key bindings
# ------------
function fzf_key_bindings
# Due to a bug of fish, we cannot use command substitution,
# so we use temporary file instead
if [ -z "$TMPDIR" ]
set -g TMPDIR /tmp
end
function __fzf_escape
while read item
echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' '
end
end
function __fzf_ctrl_t
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3- | eval (__fzfcmd) -m > $TMPDIR/fzf.result
and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape)
commandline -f repaint
rm -f $TMPDIR/fzf.result
end
function __fzf_ctrl_r
history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r > $TMPDIR/fzf.result
and commandline (cat $TMPDIR/fzf.result)
commandline -f repaint
rm -f $TMPDIR/fzf.result
end
function __fzf_alt_c
# Fish hangs if the command before pipe redirects (2> /dev/null)
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) \
-prune -o -type d -print 2> /dev/null | sed 1d | cut -b3- | eval (__fzfcmd) +m > $TMPDIR/fzf.result
[ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ]
and cd (cat $TMPDIR/fzf.result)
commandline -f repaint
rm -f $TMPDIR/fzf.result
end
function __fzfcmd
set -q FZF_TMUX; or set FZF_TMUX 1
if [ $FZF_TMUX -eq 1 ]
if set -q FZF_TMUX_HEIGHT
echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
else
echo "fzf-tmux -d40%"
end
else
echo "fzf"
end
end
bind \ct '__fzf_ctrl_t'
bind \cr '__fzf_ctrl_r'
bind \ec '__fzf_alt_c'
if bind -M insert > /dev/null 2>&1
bind -M insert \ct '__fzf_ctrl_t'
bind -M insert \cr '__fzf_ctrl_r'
bind -M insert \ec '__fzf_alt_c'
end
end

57
shell/key-bindings.zsh Normal file
View File

@@ -0,0 +1,57 @@
# Key bindings
# ------------
# CTRL-T - Paste the selected file path(s) into the command line
__fsel() {
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) -m | while read item; do
printf '%q ' "$item"
done
echo
}
__fzfcmd() {
[ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
}
if [[ $- =~ i ]]; then
fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)"
zle redisplay
}
zle -N fzf-file-widget
bindkey '^T' fzf-file-widget
# ALT-C - cd into the selected directory
fzf-cd-widget() {
cd "${$(command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m):-.}"
zle reset-prompt
}
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 restore_no_bang_hist
if selected=$(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER"); then
num=$(echo "$selected" | head -n1 | awk '{print $1}' | sed 's/[^0-9]//g')
if [ -n "$num" ]; then
LBUFFER=!$num
if setopt | grep nobanghist > /dev/null; then
restore_no_bang_hist=1
unsetopt no_bang_hist
fi
zle expand-history
[ -n "$restore_no_bang_hist" ] && setopt no_bang_hist
fi
fi
zle redisplay
}
zle -N fzf-history-widget
bindkey '^R' fzf-history-widget
fi

View File

@@ -6,7 +6,7 @@ RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git
# Install Go 1.4
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4
ENV GOPATH /go

View File

@@ -6,7 +6,7 @@ RUN yum install -y git gcc make tar ncurses-devel
# Install Go 1.4
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4
ENV GOPATH /go

View File

@@ -7,7 +7,7 @@ RUN apt-get update && apt-get -y upgrade && \
# Install Go 1.4
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4
ENV GOPATH /go

View File

@@ -21,31 +21,16 @@ BINARY64 := fzf-$(GOOS)_amd64
VERSION = $(shell fzf/$(BINARY64) --version)
RELEASE32 = fzf-$(VERSION)-$(GOOS)_386
RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64
BREW = fzf-$(VERSION)-homebrew.tgz
all: test release
brew: ../$(BREW)
../$(BREW): release
ifneq ($(UNAME_S),Darwin)
$(error brew package must be built on OS X)
endif
mkdir -p ../bin && \
cp fzf/$(RELEASE64) fzf/$(RELEASE32) ../bin && \
cd .. && ln -sf . fzf-$(VERSION) && \
tar -cvzf $(BREW) \
fzf-$(VERSION)/{{,un}install,fzf-completion.{ba,z}sh,LICENSE} \
fzf-$(VERSION)/{plugin/fzf.vim,bin/{$(RELEASE64),$(RELEASE32)}} && \
rm fzf-$(VERSION) && \
openssl sha1 $(notdir $@)
all: release
release: build
cd fzf && \
cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) && \
cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64)
cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \
rm $(RELEASE32) $(RELEASE64)
build: fzf/$(BINARY32) fzf/$(BINARY64)
build: test fzf/$(BINARY32) fzf/$(BINARY64)
test:
go get
@@ -86,4 +71,4 @@ $(DISTRO): docker
docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \
sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash'
.PHONY: all brew build release test install uninstall clean docker linux $(DISTRO)
.PHONY: all build release test install uninstall clean docker linux $(DISTRO)

View File

@@ -1,6 +1,8 @@
fzf in Go
=========
<img src="https://cloud.githubusercontent.com/assets/700826/5725028/028ea834-9b93-11e4-9198-43088c3f295d.gif" height="463" alt="fzf in go">
This directory contains the source code for the new fzf implementation in
[Go][go].
@@ -17,6 +19,9 @@ git pull
./install
```
Otherwise, follow [the instruction][install] as before. You can also install
fzf using Homebrew if you prefer that way.
Motivations
-----------
@@ -81,9 +86,6 @@ make install
# Build executables and tarballs for Linux using Docker
make linux
# Build tarball for Homebrew release
make brew
```
Contribution
@@ -111,6 +113,7 @@ License
[MIT](LICENSE)
[install]: https://github.com/junegunn/fzf#installation
[go]: https://golang.org/
[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock
[ncurses]: https://www.gnu.org/software/ncurses/

View File

@@ -1,6 +1,10 @@
package algo
import "strings"
import (
"unicode"
"github.com/junegunn/fzf/src/util"
)
/*
* String matching algorithms here do not use strings.ToLower to avoid
@@ -11,8 +15,10 @@ import "strings"
*/
// FuzzyMatch performs fuzzy-match
func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
runes := []rune(*input)
func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
if len(pattern) == 0 {
return 0, 0
}
// 0. (FIXME) How to find the shortest match?
// a_____b__c__abc
@@ -27,11 +33,18 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
sidx := -1
eidx := -1
for index, char := range runes {
for index, char := range *runes {
// This is considerably faster than blindly applying strings.ToLower to the
// whole string
if !caseSensitive && char >= 65 && char <= 90 {
char += 32
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 char == pattern[pidx] {
if sidx < 0 {
@@ -47,9 +60,13 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
if sidx >= 0 && eidx >= 0 {
pidx--
for index := eidx - 1; index >= sidx; index-- {
char := runes[index]
if !caseSensitive && char >= 65 && char <= 90 {
char += 32
char := (*runes)[index]
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
} else if char > unicode.MaxASCII {
char = unicode.To(unicode.LowerCase, char)
}
}
if char == pattern[pidx] {
if pidx--; pidx < 0 {
@@ -63,23 +80,6 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
return -1, -1
}
// ExactMatchStrings performs exact-match using strings package.
// Currently not used.
func ExactMatchStrings(caseSensitive bool, input *string, pattern []rune) (int, int) {
var str string
if caseSensitive {
str = *input
} else {
str = strings.ToLower(*input)
}
if idx := strings.Index(str, string(pattern)); idx >= 0 {
prefixRuneLen := len([]rune((*input)[:idx]))
return prefixRuneLen, prefixRuneLen + len(pattern)
}
return -1, -1
}
// 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
@@ -87,9 +87,12 @@ func ExactMatchStrings(caseSensitive bool, input *string, pattern []rune) (int,
//
// We might try to implement better algorithms in the future:
// http://en.wikipedia.org/wiki/String_searching_algorithm
func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, int) {
runes := []rune(*input)
numRunes := len(runes)
func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
if len(pattern) == 0 {
return 0, 0
}
numRunes := len(*runes)
plen := len(pattern)
if numRunes < plen {
return -1, -1
@@ -97,9 +100,13 @@ func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, in
pidx := 0
for index := 0; index < numRunes; index++ {
char := runes[index]
if !caseSensitive && char >= 65 && char <= 90 {
char += 32
char := (*runes)[index]
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
} else if char > unicode.MaxASCII {
char = unicode.To(unicode.LowerCase, char)
}
}
if pattern[pidx] == char {
pidx++
@@ -115,16 +122,15 @@ func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, in
}
// PrefixMatch performs prefix-match
func PrefixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
runes := []rune(*input)
if len(runes) < len(pattern) {
func PrefixMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
if len(*runes) < len(pattern) {
return -1, -1
}
for index, r := range pattern {
char := runes[index]
if !caseSensitive && char >= 65 && char <= 90 {
char += 32
char := (*runes)[index]
if !caseSensitive {
char = unicode.ToLower(char)
}
if char != r {
return -1, -1
@@ -134,8 +140,8 @@ func PrefixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
}
// SuffixMatch performs suffix-match
func SuffixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
runes := []rune(strings.TrimRight(*input, " "))
func SuffixMatch(caseSensitive bool, input *[]rune, pattern []rune) (int, int) {
runes := util.TrimRight(input)
trimmedLen := len(runes)
diff := trimmedLen - len(pattern)
if diff < 0 {
@@ -144,8 +150,8 @@ func SuffixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
for index, r := range pattern {
char := runes[index+diff]
if !caseSensitive && char >= 65 && char <= 90 {
char += 32
if !caseSensitive {
char = unicode.ToLower(char)
}
if char != r {
return -1, -1

View File

@@ -5,11 +5,12 @@ import (
"testing"
)
func assertMatch(t *testing.T, fun func(bool, *string, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) {
func assertMatch(t *testing.T, fun func(bool, *[]rune, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) {
if !caseSensitive {
pattern = strings.ToLower(pattern)
}
s, e := fun(caseSensitive, &input, []rune(pattern))
runes := []rune(input)
s, e := fun(caseSensitive, &runes, []rune(pattern))
if s != sidx {
t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern)
}
@@ -42,3 +43,10 @@ func TestSuffixMatch(t *testing.T) {
assertMatch(t, SuffixMatch, false, "fooBarbaz", "baz", 6, 9)
assertMatch(t, SuffixMatch, true, "fooBarbaz", "Baz", -1, -1)
}
func TestEmptyPattern(t *testing.T) {
assertMatch(t, FuzzyMatch, true, "foobar", "", 0, 0)
assertMatch(t, ExactMatchNaive, true, "foobar", "", 0, 0)
assertMatch(t, PrefixMatch, true, "foobar", "", 0, 0)
assertMatch(t, SuffixMatch, true, "foobar", "", 6, 6)
}

148
src/ansi.go Normal file
View File

@@ -0,0 +1,148 @@
package fzf
import (
"bytes"
"regexp"
"strconv"
"strings"
"unicode/utf8"
)
type ansiOffset struct {
offset [2]int32
color ansiState
}
type ansiState struct {
fg int
bg int
bold bool
}
func (s *ansiState) colored() bool {
return s.fg != -1 || s.bg != -1 || s.bold
}
func (s *ansiState) equals(t *ansiState) bool {
if t == nil {
return !s.colored()
}
return s.fg == t.fg && s.bg == t.bg && s.bold == t.bold
}
var ansiRegex *regexp.Regexp
func init() {
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]")
}
func extractColor(str *string) (*string, []ansiOffset) {
var offsets []ansiOffset
var output bytes.Buffer
var state *ansiState
idx := 0
for _, offset := range ansiRegex.FindAllStringIndex(*str, -1) {
output.WriteString((*str)[idx:offset[0]])
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(utf8.RuneCount(output.Bytes()))
}
if newState.colored() {
// Append new offset
state = newState
newLen := int32(utf8.RuneCount(output.Bytes()))
offsets = append(offsets, ansiOffset{[2]int32{newLen, newLen}, *state})
} else {
// Discard state
state = nil
}
}
idx = offset[1]
}
rest := (*str)[idx:]
if len(rest) > 0 {
output.WriteString(rest)
if state != nil {
// Update last offset
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
}
}
outputStr := output.String()
return &outputStr, offsets
}
func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
// State
var state *ansiState
if prevState == nil {
state = &ansiState{-1, -1, false}
} else {
state = &ansiState{prevState.fg, prevState.bg, prevState.bold}
}
if ansiCode[len(ansiCode)-1] == 'K' {
return state
}
ptr := &state.fg
state256 := 0
init := func() {
state.fg = -1
state.bg = -1
state.bold = false
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.bold = true
case 0:
init()
default:
if num >= 30 && num <= 37 {
state.fg = num - 30
} else if num >= 40 && num <= 47 {
state.bg = num - 40
}
}
case 1:
switch num {
case 5:
state256++
default:
state256 = 0
}
case 2:
*ptr = num
state256 = 0
}
}
}
return state
}

107
src/ansi_test.go Normal file
View File

@@ -0,0 +1,107 @@
package fzf
import (
"fmt"
"testing"
)
func TestExtractColor(t *testing.T) {
assert := func(offset ansiOffset, b int32, e int32, fg int, bg int, bold bool) {
if offset.offset[0] != b || offset.offset[1] != e ||
offset.color.fg != fg || offset.color.bg != bg || offset.color.bold != bold {
t.Error(offset, b, e, fg, bg, bold)
}
}
src := "hello world"
clean := "\x1b[0m"
check := func(assertion func(ansiOffsets []ansiOffset)) {
output, ansiOffsets := extractColor(&src)
if *output != "hello world" {
t.Errorf("Invalid output: {}", output)
}
fmt.Println(src, ansiOffsets, clean)
assertion(ansiOffsets)
}
check(func(offsets []ansiOffset) {
if len(offsets) > 0 {
t.Fail()
}
})
src = "\x1b[0mhello world"
check(func(offsets []ansiOffset) {
if len(offsets) > 0 {
t.Fail()
}
})
src = "\x1b[1mhello world"
check(func(offsets []ansiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 0, 11, -1, -1, true)
})
src = "\x1b[1mhello \x1b[mworld"
check(func(offsets []ansiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 0, 6, -1, -1, true)
})
src = "\x1b[1mhello \x1b[Kworld"
check(func(offsets []ansiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 0, 11, -1, -1, true)
})
src = "hello \x1b[34;45;1mworld"
check(func(offsets []ansiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 6, 11, 4, 5, true)
})
src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld"
check(func(offsets []ansiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 6, 11, 4, 5, true)
})
src = "hello \x1b[34;45;1mwor\x1b[0mld"
check(func(offsets []ansiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 6, 9, 4, 5, true)
})
src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md"
check(func(offsets []ansiOffset) {
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}
src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md"
check(func(offsets []ansiOffset) {
if len(offsets) != 2 {
t.Fail()
}
assert(offsets[0], 6, 9, 38, 48, true)
assert(offsets[1], 9, 10, 48, 38, true)
})
}

View File

@@ -2,23 +2,23 @@ package fzf
import "sync"
// QueryCache associates strings to lists of items
type QueryCache map[string][]*Item
// queryCache associates strings to lists of items
type queryCache map[string][]*Item
// ChunkCache associates Chunk and query string to lists of items
type ChunkCache struct {
mutex sync.Mutex
cache map[*Chunk]*QueryCache
cache map[*Chunk]*queryCache
}
// NewChunkCache returns a new ChunkCache
func NewChunkCache() ChunkCache {
return ChunkCache{sync.Mutex{}, make(map[*Chunk]*QueryCache)}
return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
}
// Add adds the list to the cache
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) {
if len(key) == 0 || !chunk.IsFull() {
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
return
}
@@ -27,7 +27,7 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) {
qc, ok := cc.cache[chunk]
if !ok {
cc.cache[chunk] = &QueryCache{}
cc.cache[chunk] = &queryCache{}
qc = cc.cache[chunk]
}
(*qc)[key] = list

View File

@@ -4,7 +4,7 @@ import "testing"
func TestChunkCache(t *testing.T) {
cache := NewChunkCache()
chunk2 := make(Chunk, ChunkSize)
chunk2 := make(Chunk, chunkSize)
chunk1p := &Chunk{}
chunk2p := &chunk2
items1 := []*Item{&Item{}}

View File

@@ -2,10 +2,7 @@ package fzf
import "sync"
// Capacity of each chunk
const ChunkSize int = 100
// Chunk is a list of Item pointers whose size has the upper limit of ChunkSize
// Chunk is a list of Item pointers whose size has the upper limit of chunkSize
type Chunk []*Item // >>> []Item
// ItemBuilder is a closure type that builds Item object from a pointer to a
@@ -35,7 +32,7 @@ func (c *Chunk) push(trans ItemBuilder, data *string, index int) {
// IsFull returns true if the Chunk is full
func (c *Chunk) IsFull() bool {
return len(*c) == ChunkSize
return len(*c) == chunkSize
}
func (cl *ChunkList) lastChunk() *Chunk {
@@ -47,7 +44,7 @@ func CountItems(cs []*Chunk) int {
if len(cs) == 0 {
return 0
}
return ChunkSize*(len(cs)-1) + len(*(cs[len(cs)-1]))
return chunkSize*(len(cs)-1) + len(*(cs[len(cs)-1]))
}
// Push adds the item to the list
@@ -56,7 +53,7 @@ func (cl *ChunkList) Push(data string) {
defer cl.mutex.Unlock()
if len(cl.chunks) == 0 || cl.lastChunk().IsFull() {
newChunk := Chunk(make([]*Item, 0, ChunkSize))
newChunk := Chunk(make([]*Item, 0, chunkSize))
cl.chunks = append(cl.chunks, &newChunk)
}

View File

@@ -45,7 +45,7 @@ func TestChunkList(t *testing.T) {
}
// Add more data
for i := 0; i < ChunkSize*2; i++ {
for i := 0; i < chunkSize*2; i++ {
cl.Push(fmt.Sprintf("item %d", i))
}
@@ -57,7 +57,7 @@ func TestChunkList(t *testing.T) {
// New snapshot
snapshot, count = cl.Snapshot()
if len(snapshot) != 3 || !snapshot[0].IsFull() ||
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != ChunkSize*2+2 {
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
t.Error("Expected two full chunks and one more chunk")
}
if len(*snapshot[2]) != 2 {

View File

@@ -1,11 +1,38 @@
package fzf
import (
"time"
"github.com/junegunn/fzf/src/util"
)
// Current version
const Version = "0.9.0"
const (
// Current version
Version = "0.9.12"
// Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond
coordinatorDelayStep time.Duration = 10 * time.Millisecond
// Reader
defaultCommand = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null`
// Terminal
initialDelay = 100 * time.Millisecond
spinnerDuration = 200 * time.Millisecond
// Matcher
progressMinDuration = 200 * time.Millisecond
// Capacity of each chunk
chunkSize int = 100
// Do not cache results of low selectivity queries
queryCacheMax int = chunkSize / 5
// Not to cache mergers with large lists
mergerCacheMax int = 100000
)
// fzf events
const (

View File

@@ -34,9 +34,6 @@ import (
"github.com/junegunn/fzf/src/util"
)
const coordinatorDelayMax time.Duration = 100 * time.Millisecond
const coordinatorDelayStep time.Duration = 10 * time.Millisecond
func initProcs() {
runtime.GOMAXPROCS(runtime.NumCPU())
}
@@ -44,16 +41,17 @@ func initProcs() {
/*
Reader -> EvtReadFin
Reader -> EvtReadNew -> Matcher (restart)
Terminal -> EvtSearchNew -> Matcher (restart)
Terminal -> EvtSearchNew:bool -> Matcher (restart)
Matcher -> EvtSearchProgress -> Terminal (update info)
Matcher -> EvtSearchFin -> Terminal (update list)
*/
// Run starts fzf
func Run(options *Options) {
func Run(opts *Options) {
initProcs()
opts := ParseOptions()
sort := opts.Sort > 0
rankTiebreak = opts.Tiebreak
if opts.Version {
fmt.Println(Version)
@@ -63,83 +61,105 @@ func Run(options *Options) {
// Event channel
eventBox := util.NewEventBox()
// ANSI code processor
ansiProcessor := func(data *string) (*string, []ansiOffset) {
// By default, we do nothing
return data, nil
}
if opts.Ansi {
if opts.Theme != nil {
ansiProcessor = func(data *string) (*string, []ansiOffset) {
return extractColor(data)
}
} else {
// When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input
ansiProcessor = func(data *string) (*string, []ansiOffset) {
trimmed, _ := extractColor(data)
return trimmed, nil
}
}
}
// Chunk list
var chunkList *ChunkList
if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(data *string, index int) *Item {
data, colors := ansiProcessor(data)
return &Item{
text: data,
index: uint32(index),
rank: Rank{0, 0, uint32(index)}}
text: data,
index: uint32(index),
colors: colors,
rank: Rank{0, 0, uint32(index)}}
})
} else {
chunkList = NewChunkList(func(data *string, index int) *Item {
tokens := Tokenize(data, opts.Delimiter)
trans := Transform(tokens, opts.WithNth)
item := Item{
text: Transform(tokens, opts.WithNth).whole,
text: joinTokens(trans),
origText: data,
index: uint32(index),
colors: nil,
rank: Rank{0, 0, uint32(index)}}
trimmed, colors := ansiProcessor(item.text)
item.text = trimmed
item.colors = colors
return &item
})
}
// Reader
reader := Reader{func(str string) { chunkList.Push(str) }, eventBox}
go reader.ReadSource()
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
if !streamingFilter {
reader := Reader{func(str string) { chunkList.Push(str) }, eventBox}
go reader.ReadSource()
}
// Matcher
patternBuilder := func(runes []rune) *Pattern {
return BuildPattern(
opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes)
}
matcher := NewMatcher(patternBuilder, opts.Sort > 0, eventBox)
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
// Defered-interactive / Non-interactive
// --select-1 | --exit-0 | --filter
if filtering := opts.Filter != nil; filtering || opts.Select1 || opts.Exit0 {
limit := 0
var patternString string
if filtering {
patternString = *opts.Filter
} else {
if opts.Select1 || opts.Exit0 {
limit = 1
}
patternString = opts.Query
// Filtering mode
if opts.Filter != nil {
if opts.PrintQuery {
fmt.Println(*opts.Filter)
}
pattern := patternBuilder([]rune(patternString))
looping := true
eventBox.Unwatch(EvtReadNew)
for looping {
eventBox.Wait(func(events *util.Events) {
for evt := range *events {
switch evt {
case EvtReadFin:
looping = false
return
pattern := patternBuilder([]rune(*opts.Filter))
if streamingFilter {
reader := Reader{
func(str string) {
item := chunkList.trans(&str, 0)
if pattern.MatchItem(item) {
fmt.Println(*item.text)
}
}
})
}
}, eventBox}
reader.ReadSource()
} else {
eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin)
snapshot, _ := chunkList.Snapshot()
merger, cancelled := matcher.scan(MatchRequest{
chunks: snapshot,
pattern: pattern}, limit)
if !cancelled && (filtering ||
opts.Exit0 && merger.Length() == 0 ||
opts.Select1 && merger.Length() == 1) {
if opts.PrintQuery {
fmt.Println(patternString)
}
snapshot, _ := chunkList.Snapshot()
merger, _ := matcher.scan(MatchRequest{
chunks: snapshot,
pattern: pattern})
for i := 0; i < merger.Length(); i++ {
fmt.Println(merger.Get(i).AsString())
}
os.Exit(0)
}
os.Exit(0)
}
// Synchronous search
if opts.Sync {
eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin)
}
// Go interactive
@@ -147,7 +167,11 @@ func Run(options *Options) {
// Terminal I/O
terminal := NewTerminal(opts, eventBox)
deferred := opts.Select1 || opts.Exit0
go terminal.Loop()
if !deferred {
terminal.startChan <- true
}
// Event coordination
reading := true
@@ -165,11 +189,15 @@ func Run(options *Options) {
reading = reading && evt == EvtReadNew
snapshot, count := chunkList.Snapshot()
terminal.UpdateCount(count, !reading)
matcher.Reset(snapshot, terminal.Input(), false)
matcher.Reset(snapshot, terminal.Input(), false, !reading, sort)
case EvtSearchNew:
switch val := value.(type) {
case bool:
sort = val
}
snapshot, _ := chunkList.Snapshot()
matcher.Reset(snapshot, terminal.Input(), true)
matcher.Reset(snapshot, terminal.Input(), true, !reading, sort)
delay = false
case EvtSearchProgress:
@@ -181,6 +209,28 @@ func Run(options *Options) {
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 {
fmt.Println(opts.Query)
}
if len(opts.Expect) > 0 {
fmt.Println()
}
for i := 0; i < count; i++ {
fmt.Println(val.Get(i).AsString())
}
os.Exit(0)
}
deferred = false
terminal.startChan <- true
}
}
terminal.UpdateList(val)
}
}

View File

@@ -61,10 +61,30 @@ const (
PgUp
PgDn
AltB
AltF
AltD
Up
Down
Left
Right
Home
End
SLeft
SRight
F1
F2
F3
F4
AltBS
AltA
AltB
AltC
AltD
AltE
AltF
AltZ = AltA + 'z' - 'a'
)
// Pallete
@@ -78,12 +98,25 @@ const (
ColInfo
ColCursor
ColSelected
ColUser
)
const (
doubleClickDuration = 500 * time.Millisecond
)
type ColorTheme struct {
darkBg C.short
prompt C.short
match C.short
current C.short
currentMatch C.short
spinner C.short
info C.short
cursor C.short
selected C.short
}
type Event struct {
Type int
Char rune
@@ -103,14 +136,49 @@ var (
_buf []byte
_in *os.File
_color func(int, bool) C.int
_colorMap map[int]int
_prevDownTime time.Time
_prevDownY int
_clickY []int
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
DarkBG C.short
)
func init() {
_prevDownTime = time.Unix(0, 0)
_clickY = []int{}
_colorMap = make(map[int]int)
Default16 = &ColorTheme{
darkBg: C.COLOR_BLACK,
prompt: C.COLOR_BLUE,
match: C.COLOR_GREEN,
current: C.COLOR_YELLOW,
currentMatch: C.COLOR_GREEN,
spinner: C.COLOR_GREEN,
info: C.COLOR_WHITE,
cursor: C.COLOR_RED,
selected: C.COLOR_MAGENTA}
Dark256 = &ColorTheme{
darkBg: 236,
prompt: 110,
match: 108,
current: 254,
currentMatch: 151,
spinner: 148,
info: 144,
cursor: 161,
selected: 168}
Light256 = &ColorTheme{
darkBg: 251,
prompt: 25,
match: 66,
current: 237,
currentMatch: 23,
spinner: 65,
info: 101,
cursor: 161,
selected: 168}
}
func attrColored(pair int, bold bool) C.int {
@@ -160,7 +228,7 @@ func getch(nonblock bool) int {
return int(b[0])
}
func Init(color bool, color256 bool, black bool, mouse bool) {
func Init(theme *ColorTheme, black bool, mouse bool) {
{
in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0)
if err != nil {
@@ -190,40 +258,35 @@ func Init(color bool, color256 bool, black bool, mouse bool) {
os.Exit(1)
}()
if color {
if theme != nil {
C.start_color()
var bg C.short
if black {
bg = C.COLOR_BLACK
} else {
C.use_default_colors()
bg = -1
}
if color256 {
C.init_pair(ColPrompt, 110, bg)
C.init_pair(ColMatch, 108, bg)
C.init_pair(ColCurrent, 254, 236)
C.init_pair(ColCurrentMatch, 151, 236)
C.init_pair(ColSpinner, 148, bg)
C.init_pair(ColInfo, 144, bg)
C.init_pair(ColCursor, 161, 236)
C.init_pair(ColSelected, 168, 236)
} else {
C.init_pair(ColPrompt, C.COLOR_BLUE, bg)
C.init_pair(ColMatch, C.COLOR_GREEN, bg)
C.init_pair(ColCurrent, C.COLOR_YELLOW, C.COLOR_BLACK)
C.init_pair(ColCurrentMatch, C.COLOR_GREEN, C.COLOR_BLACK)
C.init_pair(ColSpinner, C.COLOR_GREEN, bg)
C.init_pair(ColInfo, C.COLOR_WHITE, bg)
C.init_pair(ColCursor, C.COLOR_RED, C.COLOR_BLACK)
C.init_pair(ColSelected, C.COLOR_MAGENTA, C.COLOR_BLACK)
}
initPairs(theme, black)
_color = attrColored
} else {
_color = attrMono
}
}
func initPairs(theme *ColorTheme, black bool) {
var bg C.short
if black {
bg = C.COLOR_BLACK
} else {
C.use_default_colors()
bg = -1
}
DarkBG = theme.darkBg
C.init_pair(ColPrompt, theme.prompt, bg)
C.init_pair(ColMatch, theme.match, bg)
C.init_pair(ColCurrent, theme.current, DarkBG)
C.init_pair(ColCurrentMatch, theme.currentMatch, DarkBG)
C.init_pair(ColSpinner, theme.spinner, bg)
C.init_pair(ColInfo, theme.info, bg)
C.init_pair(ColCursor, theme.cursor, DarkBG)
C.init_pair(ColSelected, theme.selected, DarkBG)
}
func Close() {
C.endwin()
C.swapOutput()
@@ -303,21 +366,29 @@ func escSequence(sz *int) Event {
*sz = 3
switch _buf[2] {
case 68:
return Event{CtrlB, 0, nil}
return Event{Left, 0, nil}
case 67:
return Event{CtrlF, 0, nil}
return Event{Right, 0, nil}
case 66:
return Event{CtrlJ, 0, nil}
return Event{Down, 0, nil}
case 65:
return Event{CtrlK, 0, nil}
return Event{Up, 0, nil}
case 90:
return Event{BTab, 0, nil}
case 72:
return Event{CtrlA, 0, nil}
return Event{Home, 0, nil}
case 70:
return Event{CtrlE, 0, nil}
return Event{End, 0, nil}
case 77:
return mouseSequence(sz)
case 80:
return Event{F1, 0, nil}
case 81:
return Event{F2, 0, nil}
case 82:
return Event{F3, 0, nil}
case 83:
return Event{F4, 0, nil}
case 49, 50, 51, 52, 53, 54:
if len(_buf) < 4 {
return Event{Invalid, 0, nil}
@@ -329,7 +400,7 @@ func escSequence(sz *int) Event {
case 51:
return Event{Del, 0, nil}
case 52:
return Event{CtrlE, 0, nil}
return Event{End, 0, nil}
case 53:
return Event{PgUp, 0, nil}
case 54:
@@ -337,7 +408,7 @@ func escSequence(sz *int) Event {
case 49:
switch _buf[3] {
case 126:
return Event{CtrlA, 0, nil}
return Event{Home, 0, nil}
case 59:
if len(_buf) != 6 {
return Event{Invalid, 0, nil}
@@ -347,22 +418,25 @@ func escSequence(sz *int) Event {
case 50:
switch _buf[5] {
case 68:
return Event{CtrlA, 0, nil}
return Event{Home, 0, nil}
case 67:
return Event{CtrlE, 0, nil}
return Event{End, 0, nil}
}
case 53:
switch _buf[5] {
case 68:
return Event{AltB, 0, nil}
return Event{SLeft, 0, nil}
case 67:
return Event{AltF, 0, nil}
return Event{SRight, 0, nil}
}
} // _buf[4]
} // _buf[3]
} // _buf[2]
} // _buf[2]
} // _buf[1]
if _buf[1] >= 'a' && _buf[1] <= 'z' {
return Event{AltA + int(_buf[1]) - 'a', 0, nil}
}
return Event{Invalid, 0, nil}
}
@@ -393,6 +467,9 @@ func GetChar() Event {
return Event{int(_buf[0]), 0, nil}
}
r, rsz := utf8.DecodeRune(_buf)
if r == utf8.RuneError {
return Event{ESC, 0, nil}
}
sz = rsz
return Event{Rune, r, nil}
}
@@ -421,6 +498,22 @@ func Clear() {
C.clear()
}
func Endwin() {
C.endwin()
}
func Refresh() {
C.refresh()
}
func PairFor(fg int, bg int) int {
key := (fg << 8) + bg
if found, prs := _colorMap[key]; prs {
return found
}
id := len(_colorMap) + ColUser
C.init_pair(C.short(id), C.short(fg), C.short(bg))
_colorMap[key] = id
return id
}

14
src/curses/curses_test.go Normal file
View File

@@ -0,0 +1,14 @@
package curses
import (
"testing"
)
func TestPairFor(t *testing.T) {
if PairFor(30, 50) != PairFor(30, 50) {
t.Fail()
}
if PairFor(-1, 10) != PairFor(-1, 10) {
t.Fail()
}
}

View File

@@ -1,32 +1,49 @@
package fzf
import (
"math"
"github.com/junegunn/fzf/src/curses"
)
// Offset holds two 32-bit integers denoting the offsets of a matched substring
type Offset [2]int32
type colorOffset struct {
offset [2]int32
color int
bold bool
}
// Item represents each input line
type Item struct {
text *string
origText *string
transformed *Transformed
transformed *[]Token
index uint32
offsets []Offset
colors []ansiOffset
rank Rank
}
// Rank is used to sort the search result
type Rank struct {
matchlen uint16
strlen uint16
tiebreak uint16
index uint32
}
// Tiebreak criterion to use. Never changes once fzf is started.
var rankTiebreak tiebreak
// Rank calculates rank of the Item
func (i *Item) Rank(cache bool) Rank {
if cache && (i.rank.matchlen > 0 || i.rank.strlen > 0) {
if cache && (i.rank.matchlen > 0 || i.rank.tiebreak > 0) {
return i.rank
}
matchlen := 0
prevEnd := 0
minBegin := math.MaxUint16
for _, offset := range i.offsets {
begin := int(offset[0])
end := int(offset[1])
@@ -37,10 +54,30 @@ func (i *Item) Rank(cache bool) Rank {
prevEnd = end
}
if end > begin {
if begin < minBegin {
minBegin = begin
}
matchlen += end - begin
}
}
rank := Rank{uint16(matchlen), uint16(len(*i.text)), i.index}
var tiebreak uint16
switch rankTiebreak {
case byLength:
tiebreak = uint16(len(*i.text))
case byBegin:
// We can't just look at i.offsets[0][0] because it can be an inverse term
tiebreak = uint16(minBegin)
case byEnd:
if prevEnd > 0 {
tiebreak = uint16(1 + len(*i.text) - prevEnd)
} else {
// Empty offsets due to inverse terms.
tiebreak = 1
}
case byIndex:
tiebreak = 1
}
rank := Rank{uint16(matchlen), tiebreak, i.index}
if cache {
i.rank = rank
}
@@ -55,6 +92,79 @@ func (i *Item) AsString() string {
return *i.text
}
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {
if len(item.colors) == 0 {
var offsets []colorOffset
for _, off := range item.offsets {
offsets = append(offsets, colorOffset{offset: off, color: color, bold: bold})
}
return offsets
}
// Find max column
var maxCol int32
for _, off := range item.offsets {
if off[1] > maxCol {
maxCol = off[1]
}
}
for _, ansi := range item.colors {
if ansi.offset[1] > maxCol {
maxCol = ansi.offset[1]
}
}
cols := make([]int, maxCol)
for colorIndex, ansi := range item.colors {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = colorIndex + 1 // XXX
}
}
for _, off := range item.offsets {
for i := off[0]; i < off[1]; i++ {
cols[i] = -1
}
}
// sort.Sort(ByOrder(offsets))
// Merge offsets
// ------------ ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
curr := 0
start := 0
var offsets []colorOffset
add := func(idx int) {
if curr != 0 && idx > start {
if curr == -1 {
offsets = append(offsets, colorOffset{
offset: Offset{int32(start), int32(idx)}, color: color, bold: bold})
} else {
ansi := item.colors[curr-1]
bg := ansi.color.bg
if current && bg == -1 {
bg = int(curses.DarkBG)
}
offsets = append(offsets, colorOffset{
offset: Offset{int32(start), int32(idx)},
color: curses.PairFor(ansi.color.fg, bg),
bold: ansi.color.bold || bold})
}
}
}
for idx, col := range cols {
if col != curr {
add(idx)
start = idx
curr = col
}
}
add(int(maxCol))
return offsets
}
// ByOrder is for sorting substring offsets
type ByOrder []Offset
@@ -87,24 +197,39 @@ func (a ByRelevance) Less(i, j int) bool {
irank := a[i].Rank(true)
jrank := a[j].Rank(true)
return compareRanks(irank, jrank)
return compareRanks(irank, jrank, false)
}
func compareRanks(irank Rank, jrank Rank) bool {
// ByRelevanceTac is for sorting Items
type ByRelevanceTac []*Item
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 {
irank := a[i].Rank(true)
jrank := a[j].Rank(true)
return compareRanks(irank, jrank, true)
}
func compareRanks(irank Rank, jrank Rank, tac bool) bool {
if irank.matchlen < jrank.matchlen {
return true
} else if irank.matchlen > jrank.matchlen {
return false
}
if irank.strlen < jrank.strlen {
if irank.tiebreak < jrank.tiebreak {
return true
} else if irank.strlen > jrank.strlen {
} else if irank.tiebreak > jrank.tiebreak {
return false
}
if irank.index <= jrank.index {
return true
}
return false
return (irank.index <= jrank.index) != tac
}

View File

@@ -3,6 +3,8 @@ package fzf
import (
"sort"
"testing"
"github.com/junegunn/fzf/src/curses"
)
func TestOffsetSort(t *testing.T) {
@@ -20,12 +22,19 @@ func TestOffsetSort(t *testing.T) {
}
func TestRankComparison(t *testing.T) {
if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}) ||
!compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}) ||
!compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}) ||
!compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}) {
if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, false) ||
!compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) ||
!compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, false) ||
!compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) {
t.Error("Invalid order")
}
if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, true) ||
!compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) ||
!compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, true) ||
!compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) {
t.Error("Invalid order (tac)")
}
}
// Match length, string length, index
@@ -33,7 +42,7 @@ func TestItemRank(t *testing.T) {
strs := []string{"foo", "foobar", "bar", "baz"}
item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}}
rank1 := item1.Rank(true)
if rank1.matchlen != 0 || rank1.strlen != 3 || rank1.index != 1 {
if rank1.matchlen != 0 || rank1.tiebreak != 3 || rank1.index != 1 {
t.Error(item1.Rank(true))
}
// Only differ in index
@@ -65,3 +74,31 @@ func TestItemRank(t *testing.T) {
t.Error(items)
}
}
func TestColorOffset(t *testing.T) {
// ------------ 20 ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
item := Item{
offsets: []Offset{Offset{5, 15}, Offset{25, 35}},
colors: []ansiOffset{
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}},
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}},
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}},
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
offsets := item.colorOffsets(99, false, true)
assert := func(idx int, b int32, e int32, c int, bold bool) {
o := offsets[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold {
t.Error(o)
}
}
assert(0, 0, 5, curses.ColUser, false)
assert(1, 5, 15, 99, false)
assert(2, 15, 20, curses.ColUser, false)
assert(3, 22, 25, curses.ColUser+1, true)
assert(4, 25, 35, 99, false)
assert(5, 35, 40, curses.ColUser+2, true)
}

View File

@@ -14,12 +14,15 @@ import (
type MatchRequest struct {
chunks []*Chunk
pattern *Pattern
final bool
sort 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
@@ -31,16 +34,13 @@ const (
reqReset
)
const (
progressMinDuration = 200 * time.Millisecond
)
// NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern,
sort bool, eventBox *util.EventBox) *Matcher {
sort bool, tac bool, eventBox *util.EventBox) *Matcher {
return &Matcher{
patternBuilder: patternBuilder,
sort: sort,
tac: tac,
eventBox: eventBox,
reqBox: util.NewEventBox(),
partitions: runtime.NumCPU(),
@@ -66,6 +66,12 @@ func (m *Matcher) Loop() {
events.Clear()
})
if request.sort != m.sort {
m.sort = request.sort
m.mergerCache = make(map[string]*Merger)
clearChunkCache()
}
// Restart search
patternString := request.pattern.AsString()
var merger *Merger
@@ -86,11 +92,14 @@ func (m *Matcher) Loop() {
}
if !foundCache {
merger, cancelled = m.scan(request, 0)
merger, cancelled = m.scan(request)
}
if !cancelled {
m.mergerCache[patternString] = merger
if merger.Cacheable() {
m.mergerCache[patternString] = merger
}
merger.final = request.final
m.eventBox.Set(EvtSearchFin, merger)
}
}
@@ -121,7 +130,7 @@ type partialResult struct {
matches []*Item
}
func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
startedAt := time.Now()
numChunks := len(request.chunks)
@@ -129,7 +138,10 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
return EmptyMerger, false
}
pattern := request.pattern
empty := pattern.IsEmpty()
if pattern.IsEmpty() {
return PassMerger(&request.chunks, m.tac), false
}
cancelled := util.NewAtomicBool(false)
slices := m.sliceChunks(request.chunks)
@@ -144,20 +156,19 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
defer func() { waitGroup.Done() }()
sliceMatches := []*Item{}
for _, chunk := range chunks {
var matches []*Item
if empty {
matches = *chunk
} else {
matches = request.pattern.Match(chunk)
}
matches := request.pattern.Match(chunk)
sliceMatches = append(sliceMatches, matches...)
if cancelled.Get() {
return
}
countChan <- len(matches)
}
if !empty && m.sort {
sort.Sort(ByRelevance(sliceMatches))
if m.sort {
if m.tac {
sort.Sort(ByRelevanceTac(sliceMatches))
} else {
sort.Sort(ByRelevance(sliceMatches))
}
}
resultChan <- partialResult{idx, sliceMatches}
}(idx, chunks)
@@ -175,15 +186,11 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
count++
matchCount += matchesInChunk
if limit > 0 && matchCount > limit {
return nil, wait() // For --select-1 and --exit-0
}
if count == numChunks {
break
}
if !empty && m.reqBox.Peak(reqReset) {
if m.reqBox.Peek(reqReset) {
return nil, wait()
}
@@ -197,11 +204,11 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches
}
return NewMerger(partialResults, !empty && m.sort), false
return NewMerger(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) {
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool) {
pattern := m.patternBuilder(patternRunes)
var event util.EventType
@@ -210,5 +217,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) {
} else {
event = reqRetry
}
m.reqBox.Set(event, MatchRequest{chunks, pattern})
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort})
}

View File

@@ -3,25 +3,45 @@ package fzf
import "fmt"
// Merger with no data
var EmptyMerger = NewMerger([][]*Item{}, false)
var EmptyMerger = NewMerger([][]*Item{}, 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 {
lists [][]*Item
merged []*Item
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{
chunks: chunks,
tac: tac,
count: 0}
for _, chunk := range *mg.chunks {
mg.count += len(*chunk)
}
return &mg
}
// NewMerger returns a new Merger
func NewMerger(lists [][]*Item, sorted bool) *Merger {
func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger {
mg := Merger{
lists: lists,
merged: []*Item{},
chunks: nil,
cursors: make([]int, len(lists)),
sorted: sorted,
tac: tac,
final: false,
count: 0}
for _, list := range mg.lists {
@@ -37,19 +57,33 @@ func (mg *Merger) Length() int {
// Get returns the pointer to the Item object indexed by the given integer
func (mg *Merger) Get(idx int) *Item {
if len(mg.lists) == 1 {
return mg.lists[0][idx]
} else if !mg.sorted {
for _, list := range mg.lists {
numItems := len(list)
if idx < numItems {
return list[idx]
}
idx -= numItems
if mg.chunks != nil {
if mg.tac {
idx = mg.count - idx - 1
}
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
chunk := (*mg.chunks)[idx/chunkSize]
return (*chunk)[idx%chunkSize]
}
return mg.mergedGet(idx)
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) *Item {
@@ -64,7 +98,7 @@ func (mg *Merger) mergedGet(idx int) *Item {
}
if cursor >= 0 {
rank := list[cursor].Rank(false)
if minIdx < 0 || compareRanks(rank, minRank) {
if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
minRank = rank
minIdx = listIdx
}

View File

@@ -62,7 +62,7 @@ func TestMergerUnsorted(t *testing.T) {
cnt := len(items)
// Not sorted: same order
mg := NewMerger(lists, false)
mg := NewMerger(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")
@@ -74,7 +74,7 @@ func TestMergerSorted(t *testing.T) {
cnt := len(items)
// Sorted sorted order
mg := NewMerger(lists, true)
mg := NewMerger(lists, true, false)
assert(t, cnt == mg.Length(), "Invalid Length")
sort.Sort(ByRelevance(items))
for i := 0; i < cnt; i++ {
@@ -84,7 +84,7 @@ func TestMergerSorted(t *testing.T) {
}
// Inverse order
mg2 := NewMerger(lists, true)
mg2 := NewMerger(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))

View File

@@ -5,13 +5,16 @@ import (
"os"
"regexp"
"strings"
"unicode/utf8"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/go-shellwords"
)
const usage = `usage: fzf [options]
Search
Search mode
-x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match)
-i Case-insensitive match (default: smart-case match)
@@ -23,17 +26,24 @@ const usage = `usage: fzf [options]
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result
-s, --sort Sort the result
+s, --no-sort Do not sort the result. Keep the sequence unchanged.
+s, --no-sort Do not sort the result
--tac Reverse the order of the input
--tiebreak=CRI Sort criterion when the scores are tied;
[length|begin|end|index] (default: length)
Interface
-m, --multi Enable multi-select with tab/shift-tab
--ansi Enable processing of ANSI color codes
--no-mouse Disable mouse
+c, --no-color Disable colors
+2, --no-256 Disable 256-color
--color=COL Color scheme; [dark|light|16|bw]
(default: dark on 256-color terminal, otherwise 16)
--black Use black background
--reverse Reverse orientation
--no-hscroll Disable horizontal scroll
--inline-info Display finder info inline with the query
--prompt=STR Input prompt (default: '> ')
--toggle-sort=KEY Key to toggle sort
--bind=KEYBINDS Custom key bindings. Refer to the man page.
Scripting
-q, --query=STR Start the finder with the given query
@@ -41,10 +51,12 @@ const usage = `usage: fzf [options]
-0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line
--expect=KEYS Comma-separated list of keys to complete fzf
--sync Synchronous search for multi-staged filtering
Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m")
FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m')
`
@@ -68,6 +80,16 @@ const (
CaseRespect
)
// Sort criteria
type tiebreak int
const (
byLength tiebreak = iota
byBegin
byEnd
byIndex
)
// Options stores the values of command-line options
type Options struct {
Mode Mode
@@ -76,22 +98,37 @@ type Options struct {
WithNth []Range
Delimiter *regexp.Regexp
Sort int
Tac bool
Tiebreak tiebreak
Multi bool
Ansi bool
Mouse bool
Color bool
Color256 bool
Theme *curses.ColorTheme
Black bool
Reverse bool
Hscroll bool
InlineInfo bool
Prompt string
Query string
Select1 bool
Exit0 bool
Filter *string
ToggleSort bool
Expect []int
Keymap map[int]actionType
PrintQuery bool
Sync bool
Version bool
}
func defaultOptions() *Options {
var defaultTheme *curses.ColorTheme
if strings.Contains(os.Getenv("TERM"), "256") {
defaultTheme = curses.Dark256
} else {
defaultTheme = curses.Default16
}
return &Options{
Mode: ModeFuzzy,
Case: CaseSmart,
@@ -99,18 +136,26 @@ func defaultOptions() *Options {
WithNth: make([]Range, 0),
Delimiter: nil,
Sort: 1000,
Tac: false,
Tiebreak: byLength,
Multi: false,
Ansi: false,
Mouse: true,
Color: true,
Color256: strings.Contains(os.Getenv("TERM"), "256"),
Theme: defaultTheme,
Black: false,
Reverse: false,
Hscroll: true,
InlineInfo: false,
Prompt: "> ",
Query: "",
Select1: false,
Exit0: false,
Filter: nil,
ToggleSort: false,
Expect: []int{},
Keymap: defaultKeymap(),
PrintQuery: false,
Sync: false,
Version: false}
}
@@ -181,6 +226,156 @@ func delimiterRegexp(str string) *regexp.Regexp {
return rx
}
func isAlphabet(char uint8) bool {
return char >= 'a' && char <= 'z'
}
func parseKeyChords(str string, message string) []int {
if len(str) == 0 {
errorExit(message)
}
tokens := strings.Split(str, ",")
if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Index(str, ",,,") >= 0 {
tokens = append(tokens, ",")
}
var chords []int
for _, key := range tokens {
if len(key) == 0 {
continue // ignore
}
lkey := strings.ToLower(key)
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chords = append(chords, curses.CtrlA+int(lkey[5])-'a')
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
chords = append(chords, curses.AltA+int(lkey[4])-'a')
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' {
chords = append(chords, curses.F1+int(key[1])-'1')
} else if utf8.RuneCountInString(key) == 1 {
chords = append(chords, curses.AltZ+int([]rune(key)[0]))
} else {
errorExit("unsupported key: " + key)
}
}
return chords
}
func parseTiebreak(str string) tiebreak {
switch strings.ToLower(str) {
case "length":
return byLength
case "index":
return byIndex
case "begin":
return byBegin
case "end":
return byEnd
default:
errorExit("invalid sort criterion: " + str)
}
return byLength
}
func parseTheme(str string) *curses.ColorTheme {
switch strings.ToLower(str) {
case "dark":
return curses.Dark256
case "light":
return curses.Light256
case "16":
return curses.Default16
case "bw", "no":
return nil
default:
errorExit("invalid color scheme: " + str)
}
return nil
}
func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[int]actionType, bool) {
for _, pairStr := range strings.Split(str, ",") {
fail := func() {
errorExit("invalid key binding: " + pairStr)
}
pair := strings.Split(pairStr, ":")
if len(pair) != 2 {
fail()
}
keys := parseKeyChords(pair[0], "key name required")
if len(keys) != 1 {
fail()
}
key := keys[0]
act := strings.ToLower(pair[1])
switch act {
case "beginning-of-line":
keymap[key] = actBeginningOfLine
case "abort":
keymap[key] = actAbort
case "accept":
keymap[key] = actAccept
case "backward-char":
keymap[key] = actBackwardChar
case "backward-delete-char":
keymap[key] = actBackwardDeleteChar
case "backward-word":
keymap[key] = actBackwardWord
case "clear-screen":
keymap[key] = actClearScreen
case "delete-char":
keymap[key] = actDeleteChar
case "end-of-line":
keymap[key] = actEndOfLine
case "forward-char":
keymap[key] = actForwardChar
case "forward-word":
keymap[key] = actForwardWord
case "kill-line":
keymap[key] = actKillLine
case "kill-word":
keymap[key] = actKillWord
case "unix-line-discard", "line-discard":
keymap[key] = actUnixLineDiscard
case "unix-word-rubout", "word-rubout":
keymap[key] = actUnixWordRubout
case "yank":
keymap[key] = actYank
case "backward-kill-word":
keymap[key] = actBackwardKillWord
case "toggle-down":
keymap[key] = actToggleDown
case "toggle-up":
keymap[key] = actToggleUp
case "toggle":
keymap[key] = actToggle
case "down":
keymap[key] = actDown
case "up":
keymap[key] = actUp
case "page-up":
keymap[key] = actPageUp
case "page-down":
keymap[key] = actPageDown
case "toggle-sort":
keymap[key] = actToggleSort
toggleSort = true
default:
errorExit("unknown action: " + act)
}
}
return keymap, toggleSort
}
func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
keys := parseKeyChords(str, "key name required")
if len(keys) != 1 {
errorExit("multiple keys specified")
}
keymap[keys[0]] = actToggleSort
return keymap
}
func parseOptions(opts *Options, allArgs []string) {
for i := 0; i < len(allArgs); i++ {
arg := allArgs[i]
@@ -198,6 +393,17 @@ func parseOptions(opts *Options, allArgs []string) {
case "-f", "--filter":
filter := nextString(allArgs, &i, "query string required")
opts.Filter = &filter
case "--expect":
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
case "--tiebreak":
opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind":
opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required"))
case "--color":
opts.Theme = parseTheme(nextString(allArgs, &i, "color scheme name required"))
case "--toggle-sort":
opts.Keymap = checkToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required"))
opts.ToggleSort = true
case "-d", "--delimiter":
opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required"))
case "-n", "--nth":
@@ -208,6 +414,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Sort = optionalNumeric(allArgs, &i)
case "+s", "--no-sort":
opts.Sort = 0
case "--tac":
opts.Tac = true
case "--no-tac":
opts.Tac = false
case "-i":
opts.Case = CaseIgnore
case "+i":
@@ -216,12 +426,16 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Multi = true
case "+m", "--no-multi":
opts.Multi = false
case "--ansi":
opts.Ansi = true
case "--no-ansi":
opts.Ansi = false
case "--no-mouse":
opts.Mouse = false
case "+c", "--no-color":
opts.Color = false
opts.Theme = nil
case "+2", "--no-256":
opts.Color256 = false
opts.Theme = curses.Default16
case "--black":
opts.Black = true
case "--no-black":
@@ -230,6 +444,14 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Reverse = true
case "--no-reverse":
opts.Reverse = false
case "--hscroll":
opts.Hscroll = true
case "--no-hscroll":
opts.Hscroll = false
case "--inline-info":
opts.InlineInfo = true
case "--no-inline-info":
opts.InlineInfo = false
case "-1", "--select-1":
opts.Select1 = true
case "+1", "--no-select-1":
@@ -244,6 +466,12 @@ func parseOptions(opts *Options, allArgs []string) {
opts.PrintQuery = false
case "--prompt":
opts.Prompt = nextString(allArgs, &i, "prompt string required")
case "--sync":
opts.Sync = true
case "--no-sync":
opts.Sync = false
case "--async":
opts.Sync = false
case "--version":
opts.Version = true
default:
@@ -261,11 +489,33 @@ func parseOptions(opts *Options, allArgs []string) {
opts.WithNth = splitNth(value)
} else if match, _ := optString(arg, "-s|--sort="); match {
opts.Sort = 1 // Don't care
} else if match, value := optString(arg, "--toggle-sort="); match {
opts.Keymap = checkToggleSort(opts.Keymap, value)
opts.ToggleSort = true
} else if match, value := optString(arg, "--expect="); match {
opts.Expect = parseKeyChords(value, "key names required")
} else if match, value := optString(arg, "--tiebreak="); match {
opts.Tiebreak = parseTiebreak(value)
} else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(value)
} else if match, value := optString(arg, "--bind="); match {
opts.Keymap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.ToggleSort, value)
} else {
errorExit("unknown option: " + arg)
}
}
}
// If we're not using extended search mode, --nth option becomes irrelevant
// if it contains the whole range
if opts.Mode == ModeFuzzy || len(opts.Nth) == 1 {
for _, r := range opts.Nth {
if r.begin == rangeEllipsis && r.end == rangeEllipsis {
opts.Nth = make([]Range, 0)
return
}
}
}
}
// ParseOptions parses command-line options

View File

@@ -1,6 +1,10 @@
package fzf
import "testing"
import (
"testing"
"github.com/junegunn/fzf/src/curses"
)
func TestDelimiterRegex(t *testing.T) {
rx := delimiterRegexp("*")
@@ -21,17 +25,133 @@ func TestSplitNth(t *testing.T) {
}
}
{
ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2")
if len(ranges) != 8 ||
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 != 1 || ranges[1].end != rangeEllipsis ||
ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis ||
ranges[2].begin != 2 || ranges[2].end != 3 ||
ranges[3].begin != 4 || ranges[3].end != -1 ||
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[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("%s", ranges)
}
}
}
func TestIrrelevantNth(t *testing.T) {
{
opts := defaultOptions()
words := []string{"--nth", "..", "-x"}
parseOptions(opts, words)
if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %s", opts.Nth)
}
}
for _, words := range [][]string{[]string{"--nth", "..,3"}, []string{"--nth", "3,1.."}, []string{"--nth", "..-1,1"}} {
{
opts := defaultOptions()
parseOptions(opts, words)
if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %s", opts.Nth)
}
}
{
opts := defaultOptions()
words = append(words, "-x")
parseOptions(opts, words)
if len(opts.Nth) != 2 {
t.Errorf("nth should not be empty: %s", opts.Nth)
}
}
}
}
func TestParseKeys(t *testing.T) {
keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "")
check := func(key int, expected int) {
if key != expected {
t.Errorf("%d != %d", key, expected)
}
}
check(len(keys), 9)
check(keys[0], curses.CtrlZ)
check(keys[1], curses.AltZ)
check(keys[2], curses.F2)
check(keys[3], curses.AltZ+'@')
check(keys[4], curses.AltA)
check(keys[5], curses.AltZ+'!')
check(keys[6], curses.CtrlA+'g'-'a')
check(keys[7], curses.AltZ+'J')
check(keys[8], curses.AltZ+'g')
}
func TestParseKeysWithComma(t *testing.T) {
check := func(key int, expected int) {
if key != expected {
t.Errorf("%d != %d", key, expected)
}
}
keys := parseKeyChords(",", "")
check(len(keys), 1)
check(keys[0], curses.AltZ+',')
keys = parseKeyChords(",,a,b", "")
check(len(keys), 3)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+',')
keys = parseKeyChords("a,b,,", "")
check(len(keys), 3)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+',')
keys = parseKeyChords("a,,,b", "")
check(len(keys), 3)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+',')
keys = parseKeyChords("a,,,b,c", "")
check(len(keys), 4)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+'c')
check(keys[3], curses.AltZ+',')
keys = parseKeyChords(",,,", "")
check(len(keys), 1)
check(keys[0], curses.AltZ+',')
}
func TestBind(t *testing.T) {
check := func(action actionType, expected actionType) {
if action != expected {
t.Errorf("%d != %d", action, expected)
}
}
keymap := defaultKeymap()
check(actBeginningOfLine, keymap[curses.CtrlA])
keymap, toggleSort :=
parseKeymap(keymap, false,
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down")
if !toggleSort {
t.Errorf("toggleSort not set")
}
check(actKillLine, keymap[curses.CtrlA])
check(actToggleSort, keymap[curses.CtrlB])
check(actPageUp, keymap[curses.AltZ+'c'])
check(actPageDown, keymap[curses.AltZ])
keymap, toggleSort = parseKeymap(keymap, false, "f1:abort")
if toggleSort {
t.Errorf("toggleSort set")
}
check(actAbort, keymap[curses.F1])
}

View File

@@ -8,8 +8,6 @@ import (
"github.com/junegunn/fzf/src/algo"
)
const uppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// fuzzy
// 'exact
// ^exact-prefix
@@ -29,10 +27,11 @@ const (
)
type term struct {
typ termType
inv bool
text []rune
origText []rune
typ termType
inv bool
text []rune
caseSensitive bool
origText []rune
}
// Pattern represents search pattern
@@ -44,7 +43,7 @@ type Pattern struct {
hasInvTerm bool
delimiter *regexp.Regexp
nth []Range
procFun map[termType]func(bool, *string, []rune) (int, int)
procFun map[termType]func(bool, *[]rune, []rune) (int, int)
}
var (
@@ -54,17 +53,21 @@ var (
)
func init() {
// We can uniquely identify the pattern for a given string since
// mode and caseMode do not change while the program is running
_patternCache = make(map[string]*Pattern)
_splitRegex = regexp.MustCompile("\\s+")
_cache = NewChunkCache()
clearPatternCache()
clearChunkCache()
}
func clearPatternCache() {
// We can uniquely identify the pattern for a given string since
// 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(mode Mode, caseMode Case,
nth []Range, delimiter *regexp.Regexp, runes []rune) *Pattern {
@@ -85,34 +88,32 @@ func BuildPattern(mode Mode, caseMode Case,
caseSensitive, hasInvTerm := true, false
terms := []term{}
switch caseMode {
case CaseSmart:
if !strings.ContainsAny(asString, uppercaseLetters) {
runes, caseSensitive = []rune(strings.ToLower(asString)), false
}
case CaseIgnore:
runes, caseSensitive = []rune(strings.ToLower(asString)), false
}
switch mode {
case ModeExtended, ModeExtendedExact:
terms = parseTerms(mode, string(runes))
terms = parseTerms(mode, caseMode, asString)
for _, term := range terms {
if term.inv {
hasInvTerm = true
}
}
default:
lowerString := strings.ToLower(asString)
caseSensitive = caseMode == CaseRespect ||
caseMode == CaseSmart && lowerString != asString
if !caseSensitive {
asString = lowerString
}
}
ptr := &Pattern{
mode: mode,
caseSensitive: caseSensitive,
text: runes,
text: []rune(asString),
terms: terms,
hasInvTerm: hasInvTerm,
nth: nth,
delimiter: delimiter,
procFun: make(map[termType]func(bool, *string, []rune) (int, int))}
procFun: make(map[termType]func(bool, *[]rune, []rune) (int, int))}
ptr.procFun[termFuzzy] = algo.FuzzyMatch
ptr.procFun[termExact] = algo.ExactMatchNaive
@@ -123,11 +124,17 @@ func BuildPattern(mode Mode, caseMode Case,
return ptr
}
func parseTerms(mode Mode, str string) []term {
func parseTerms(mode Mode, caseMode Case, str string) []term {
tokens := _splitRegex.Split(str, -1)
terms := []term{}
for _, token := range tokens {
typ, inv, text := termFuzzy, false, token
lowerText := strings.ToLower(text)
caseSensitive := caseMode == CaseRespect ||
caseMode == CaseSmart && text != lowerText
if !caseSensitive {
text = lowerText
}
origText := []rune(text)
if mode == ModeExtendedExact {
typ = termExact
@@ -153,10 +160,11 @@ func parseTerms(mode Mode, str string) []term {
if len(text) > 0 {
terms = append(terms, term{
typ: typ,
inv: inv,
text: []rune(text),
origText: origText})
typ: typ,
inv: inv,
text: []rune(text),
caseSensitive: caseSensitive,
origText: origText})
}
}
return terms
@@ -219,12 +227,7 @@ Loop:
}
}
var matches []*Item
if p.mode == ModeFuzzy {
matches = p.fuzzyMatch(space)
} else {
matches = p.extendedMatch(space)
}
matches := p.matchChunk(space)
if !p.hasInvTerm {
_cache.Add(chunk, cacheKey, matches)
@@ -232,6 +235,35 @@ Loop:
return matches
}
func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
matches := []*Item{}
if p.mode == ModeFuzzy {
for _, item := range *chunk {
if sidx, eidx := p.fuzzyMatch(item); sidx >= 0 {
matches = append(matches,
dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}}))
}
}
} else {
for _, item := range *chunk {
if offsets := p.extendedMatch(item); len(offsets) == len(p.terms) {
matches = append(matches, dupItem(item, offsets))
}
}
}
return matches
}
// MatchItem returns true if the Item is a match
func (p *Pattern) MatchItem(item *Item) bool {
if p.mode == ModeFuzzy {
sidx, _ := p.fuzzyMatch(item)
return sidx >= 0
}
offsets := p.extendedMatch(item)
return len(offsets) == len(p.terms)
}
func dupItem(item *Item, offsets []Offset) *Item {
sort.Sort(ByOrder(offsets))
return &Item{
@@ -240,68 +272,55 @@ func dupItem(item *Item, offsets []Offset) *Item {
transformed: item.transformed,
index: item.index,
offsets: offsets,
colors: item.colors,
rank: Rank{0, 0, item.index}}
}
func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item {
matches := []*Item{}
for _, item := range *chunk {
input := p.prepareInput(item)
if sidx, eidx := p.iter(algo.FuzzyMatch, input, p.text); sidx >= 0 {
matches = append(matches,
dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}}))
}
}
return matches
func (p *Pattern) fuzzyMatch(item *Item) (int, int) {
input := p.prepareInput(item)
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.text)
}
func (p *Pattern) extendedMatch(chunk *Chunk) []*Item {
matches := []*Item{}
for _, item := range *chunk {
input := p.prepareInput(item)
offsets := []Offset{}
for _, term := range p.terms {
pfun := p.procFun[term.typ]
if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 {
if term.inv {
break
}
offsets = append(offsets, Offset{int32(sidx), int32(eidx)})
} else if term.inv {
offsets = append(offsets, Offset{0, 0})
func (p *Pattern) extendedMatch(item *Item) []Offset {
input := p.prepareInput(item)
offsets := []Offset{}
for _, term := range p.terms {
pfun := p.procFun[term.typ]
if sidx, eidx := p.iter(pfun, input, term.caseSensitive, term.text); sidx >= 0 {
if term.inv {
break
}
}
if len(offsets) == len(p.terms) {
matches = append(matches, dupItem(item, offsets))
offsets = append(offsets, Offset{int32(sidx), int32(eidx)})
} else if term.inv {
offsets = append(offsets, Offset{0, 0})
}
}
return matches
return offsets
}
func (p *Pattern) prepareInput(item *Item) *Transformed {
func (p *Pattern) prepareInput(item *Item) *[]Token {
if item.transformed != nil {
return item.transformed
}
var ret *Transformed
var ret *[]Token
if len(p.nth) > 0 {
tokens := Tokenize(item.text, p.delimiter)
ret = Transform(tokens, p.nth)
} else {
trans := Transformed{
whole: item.text,
parts: []Token{Token{text: item.text, prefixLength: 0}}}
runes := []rune(*item.text)
trans := []Token{Token{text: &runes, prefixLength: 0}}
ret = &trans
}
item.transformed = ret
return ret
}
func (p *Pattern) iter(pfun func(bool, *string, []rune) (int, int),
inputs *Transformed, pattern []rune) (int, int) {
for _, part := range inputs.parts {
func (p *Pattern) iter(pfun func(bool, *[]rune, []rune) (int, int),
tokens *[]Token, caseSensitive bool, pattern []rune) (int, int) {
for _, part := range *tokens {
prefixLength := part.prefixLength
if sidx, eidx := pfun(p.caseSensitive, part.text, pattern); sidx >= 0 {
if sidx, eidx := pfun(caseSensitive, part.text, pattern); sidx >= 0 {
return sidx + prefixLength, eidx + prefixLength
}
}

View File

@@ -7,7 +7,7 @@ import (
)
func TestParseTermsExtended(t *testing.T) {
terms := parseTerms(ModeExtended,
terms := parseTerms(ModeExtended, CaseSmart,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
if len(terms) != 8 ||
terms[0].typ != termFuzzy || terms[0].inv ||
@@ -31,7 +31,7 @@ func TestParseTermsExtended(t *testing.T) {
}
func TestParseTermsExtendedExact(t *testing.T) {
terms := parseTerms(ModeExtendedExact,
terms := parseTerms(ModeExtendedExact, CaseSmart,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
if len(terms) != 8 ||
terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 ||
@@ -47,7 +47,7 @@ func TestParseTermsExtendedExact(t *testing.T) {
}
func TestParseTermsEmpty(t *testing.T) {
terms := parseTerms(ModeExtended, "' $ ^ !' !^ !$")
terms := parseTerms(ModeExtended, CaseSmart, "' $ ^ !' !^ !$")
if len(terms) != 0 {
t.Errorf("%s", terms)
}
@@ -58,8 +58,8 @@ func TestExact(t *testing.T) {
clearPatternCache()
pattern := BuildPattern(ModeExtended, CaseSmart,
[]Range{}, nil, []rune("'abc"))
str := "aabbcc abc"
sidx, eidx := algo.ExactMatchNaive(pattern.caseSensitive, &str, pattern.terms[0].text)
runes := []rune("aabbcc abc")
sidx, eidx := algo.ExactMatchNaive(pattern.caseSensitive, &runes, pattern.terms[0].text)
if sidx != 7 || eidx != 10 {
t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
}
@@ -98,14 +98,15 @@ func TestOrigTextAndTransformed(t *testing.T) {
tokens := Tokenize(strptr("junegunn"), nil)
trans := Transform(tokens, []Range{Range{1, 1}})
for _, fun := range []func(*Chunk) []*Item{pattern.fuzzyMatch, pattern.extendedMatch} {
for _, mode := range []Mode{ModeFuzzy, ModeExtended} {
chunk := Chunk{
&Item{
text: strptr("junegunn"),
origText: strptr("junegunn.choi"),
transformed: trans},
}
matches := fun(&chunk)
pattern.mode = mode
matches := pattern.matchChunk(&chunk)
if *matches[0].text != "junegunn" || *matches[0].origText != "junegunn.choi" ||
matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 ||
matches[0].transformed != trans {

View File

@@ -9,8 +9,6 @@ import (
"github.com/junegunn/fzf/src/util"
)
const defaultCommand = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null`
// Reader reads from command or standard input
type Reader struct {
pusher func(string)

View File

@@ -14,7 +14,7 @@ func TestReadFromCommand(t *testing.T) {
eventBox: eb}
// Check EventBox
if eb.Peak(EvtReadNew) {
if eb.Peek(EvtReadNew) {
t.Error("EvtReadNew should not be set yet")
}
@@ -25,7 +25,7 @@ func TestReadFromCommand(t *testing.T) {
}
// Check EventBox again
if !eb.Peak(EvtReadNew) {
if !eb.Peek(EvtReadNew) {
t.Error("EvtReadNew should be set yet")
}
@@ -38,7 +38,7 @@ func TestReadFromCommand(t *testing.T) {
})
// EventBox is cleared
if eb.Peak(EvtReadNew) {
if eb.Peek(EvtReadNew) {
t.Error("EvtReadNew should not be set yet")
}
@@ -50,7 +50,7 @@ func TestReadFromCommand(t *testing.T) {
}
// Check EventBox again
if eb.Peak(EvtReadNew) {
if eb.Peek(EvtReadNew) {
t.Error("Command failed. EvtReadNew should be set")
}
}

View File

@@ -1,11 +1,15 @@
package fzf
import (
"bytes"
"fmt"
"os"
"os/signal"
"regexp"
"sort"
"strings"
"sync"
"syscall"
"time"
C "github.com/junegunn/fzf/src/curses"
@@ -16,29 +20,56 @@ import (
// Terminal represents terminal input/output
type Terminal struct {
inlineInfo bool
prompt string
reverse bool
tac bool
hscroll bool
cx int
cy int
offset int
yanked []rune
input []rune
multi bool
sort bool
toggleSort bool
expect []int
keymap map[int]actionType
pressed int
printQuery bool
count int
progress int
reading bool
merger *Merger
selected map[*string]*string
selected map[uint32]selectedItem
reqBox *util.EventBox
eventBox *util.EventBox
mutex sync.Mutex
initFunc func()
suppress bool
startChan chan bool
}
type selectedItem struct {
at time.Time
text *string
}
type byTimeOrder []selectedItem
func (a byTimeOrder) Len() int {
return len(a)
}
func (a byTimeOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a byTimeOrder) Less(i, j int) bool {
return a[i].at.Before(a[j].at)
}
var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
var _runeWidths = make(map[rune]int)
const (
reqPrompt util.EventType = iota
@@ -50,33 +81,117 @@ const (
reqQuit
)
type actionType int
const (
initialDelay = 100 * time.Millisecond
spinnerDuration = 200 * time.Millisecond
actIgnore actionType = iota
actInvalid
actRune
actMouse
actBeginningOfLine
actAbort
actAccept
actBackwardChar
actBackwardDeleteChar
actBackwardWord
actClearScreen
actDeleteChar
actEndOfLine
actForwardChar
actForwardWord
actKillLine
actKillWord
actUnixLineDiscard
actUnixWordRubout
actYank
actBackwardKillWord
actToggle
actToggleDown
actToggleUp
actDown
actUp
actPageUp
actPageDown
actToggleSort
)
func defaultKeymap() map[int]actionType {
keymap := make(map[int]actionType)
keymap[C.Invalid] = actInvalid
keymap[C.CtrlA] = actBeginningOfLine
keymap[C.CtrlB] = actBackwardChar
keymap[C.CtrlC] = actAbort
keymap[C.CtrlG] = actAbort
keymap[C.CtrlQ] = actAbort
keymap[C.ESC] = actAbort
keymap[C.CtrlD] = actDeleteChar
keymap[C.CtrlE] = actEndOfLine
keymap[C.CtrlF] = actForwardChar
keymap[C.CtrlH] = actBackwardDeleteChar
keymap[C.Tab] = actToggleDown
keymap[C.BTab] = actToggleUp
keymap[C.CtrlJ] = actDown
keymap[C.CtrlK] = actUp
keymap[C.CtrlL] = actClearScreen
keymap[C.CtrlM] = actAccept
keymap[C.CtrlN] = actDown
keymap[C.CtrlP] = actUp
keymap[C.CtrlU] = actUnixLineDiscard
keymap[C.CtrlW] = actUnixWordRubout
keymap[C.CtrlY] = actYank
keymap[C.AltB] = actBackwardWord
keymap[C.SLeft] = actBackwardWord
keymap[C.AltF] = actForwardWord
keymap[C.SRight] = actForwardWord
keymap[C.AltD] = actKillWord
keymap[C.AltBS] = actBackwardKillWord
keymap[C.Up] = actUp
keymap[C.Down] = actDown
keymap[C.Left] = actBackwardChar
keymap[C.Right] = actForwardChar
keymap[C.Home] = actBeginningOfLine
keymap[C.End] = actEndOfLine
keymap[C.Del] = actDeleteChar // FIXME Del vs. CTRL-D
keymap[C.PgUp] = actPageUp
keymap[C.PgDn] = actPageDown
keymap[C.Rune] = actRune
keymap[C.Mouse] = actMouse
return keymap
}
// NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := []rune(opts.Query)
return &Terminal{
inlineInfo: opts.InlineInfo,
prompt: opts.Prompt,
tac: opts.Sort == 0,
reverse: opts.Reverse,
cx: displayWidth(input),
hscroll: opts.Hscroll,
cx: len(input),
cy: 0,
offset: 0,
yanked: []rune{},
input: input,
multi: opts.Multi,
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
expect: opts.Expect,
keymap: opts.Keymap,
pressed: 0,
printQuery: opts.PrintQuery,
merger: EmptyMerger,
selected: make(map[*string]*string),
selected: make(map[uint32]selectedItem),
reqBox: util.NewEventBox(),
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
startChan: make(chan bool, 1),
initFunc: func() {
C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse)
C.Init(opts.Theme, opts.Black, opts.Mouse)
}}
}
@@ -122,36 +237,56 @@ func (t *Terminal) UpdateList(merger *Merger) {
t.reqBox.Set(reqList, nil)
}
func (t *Terminal) listIndex(y int) int {
if t.tac {
return t.merger.Length() - y - 1
}
return y
}
func (t *Terminal) output() {
if t.printQuery {
fmt.Println(string(t.input))
}
if len(t.expect) > 0 {
if t.pressed == 0 {
fmt.Println()
} else if util.Between(t.pressed, C.AltA, C.AltZ) {
fmt.Printf("alt-%c\n", t.pressed+'a'-C.AltA)
} else if util.Between(t.pressed, C.F1, C.F4) {
fmt.Printf("f%c\n", t.pressed+'1'-C.F1)
} else if util.Between(t.pressed, C.CtrlA, C.CtrlZ) {
fmt.Printf("ctrl-%c\n", t.pressed+'a'-C.CtrlA)
} else {
fmt.Printf("%c\n", t.pressed-C.AltZ)
}
}
if len(t.selected) == 0 {
if t.merger.Length() > t.cy {
fmt.Println(t.merger.Get(t.listIndex(t.cy)).AsString())
cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy {
fmt.Println(t.merger.Get(t.cy).AsString())
}
} else {
for ptr, orig := range t.selected {
if orig != nil {
fmt.Println(*orig)
} else {
fmt.Println(*ptr)
}
sels := make([]selectedItem, 0, len(t.selected))
for _, sel := range t.selected {
sels = append(sels, sel)
}
sort.Sort(byTimeOrder(sels))
for _, sel := range sels {
fmt.Println(*sel.text)
}
}
}
func runeWidth(r rune, prefixWidth int) int {
if r == '\t' {
return 8 - prefixWidth%8
} else if w, found := _runeWidths[r]; found {
return w
} else {
w := runewidth.RuneWidth(r)
_runeWidths[r] = w
return w
}
}
func displayWidth(runes []rune) int {
l := 0
for _, r := range runes {
l += runewidth.RuneWidth(r)
l += runeWidth(r, l)
}
return l
}
@@ -180,15 +315,31 @@ func (t *Terminal) printPrompt() {
}
func (t *Terminal) printInfo() {
t.move(1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
C.CPrint(C.ColSpinner, true, _spinner[idx])
if t.inlineInfo {
t.move(0, len(t.prompt)+displayWidth(t.input)+1, true)
if t.reading {
C.CPrint(C.ColSpinner, true, " < ")
} else {
C.CPrint(C.ColPrompt, true, " < ")
}
} else {
t.move(1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
C.CPrint(C.ColSpinner, true, _spinner[idx])
}
t.move(1, 2, false)
}
t.move(1, 2, false)
output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count)
if t.toggleSort {
if t.sort {
output += "/S"
} else {
output += " "
}
}
if t.multi && len(t.selected) > 0 {
output += fmt.Sprintf(" (%d)", len(t.selected))
}
@@ -201,18 +352,24 @@ func (t *Terminal) printInfo() {
func (t *Terminal) printList() {
t.constrain()
maxy := maxItems()
maxy := t.maxItems()
count := t.merger.Length() - t.offset
for i := 0; i < maxy; i++ {
t.move(i+2, 0, true)
var line int
if t.inlineInfo {
line = i + 1
} else {
line = i + 2
}
t.move(line, 0, true)
if i < count {
t.printItem(t.merger.Get(t.listIndex(i+t.offset)), i == t.cy-t.offset)
t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset)
}
}
}
func (t *Terminal) printItem(item *Item, current bool) {
_, selected := t.selected[item.text]
_, selected := t.selected[item.index]
if current {
C.CPrint(C.ColCursor, true, ">")
if selected {
@@ -220,7 +377,7 @@ func (t *Terminal) printItem(item *Item, current bool) {
} else {
C.CPrint(C.ColCurrent, true, " ")
}
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch)
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true)
} else {
C.CPrint(C.ColCursor, true, " ")
if selected {
@@ -228,21 +385,32 @@ func (t *Terminal) printItem(item *Item, current bool) {
} else {
C.Print(" ")
}
t.printHighlighted(item, false, 0, C.ColMatch)
t.printHighlighted(item, false, 0, C.ColMatch, false)
}
}
func trimRight(runes []rune, width int) ([]rune, int) {
currentWidth := displayWidth(runes)
trimmed := 0
for currentWidth > width && len(runes) > 0 {
sz := len(runes)
currentWidth -= runewidth.RuneWidth(runes[sz-1])
runes = runes[:sz-1]
trimmed++
// We start from the beginning to handle tab characters
l := 0
for idx, r := range runes {
l += runeWidth(r, l)
if idx > 0 && l > width {
return runes[:idx], len(runes) - idx
}
}
return runes, trimmed
return runes, 0
}
func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
l := 0
for _, r := range runes {
l += runeWidth(r, l+prefixWidth)
if l > limit {
// Early exit
return l
}
}
return l
}
func trimLeft(runes []rune, width int) ([]rune, int32) {
@@ -250,14 +418,14 @@ func trimLeft(runes []rune, width int) ([]rune, int32) {
var trimmed int32
for currentWidth > width && len(runes) > 0 {
currentWidth -= runewidth.RuneWidth(runes[0])
runes = runes[1:]
trimmed++
currentWidth = displayWidthWithLimit(runes, 2, width)
}
return runes, trimmed
}
func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) {
func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) {
var maxe int32
for _, offset := range item.offsets {
if offset[1] > maxe {
@@ -267,57 +435,93 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) {
// Overflow
text := []rune(*item.text)
offsets := item.offsets
offsets := item.colorOffsets(col2, bold, current)
maxWidth := C.MaxX() - 3
fullWidth := displayWidth(text)
if fullWidth > maxWidth {
// Stri..
matchEndWidth := displayWidth(text[:maxe])
if matchEndWidth <= maxWidth-2 {
if t.hscroll {
// Stri..
matchEndWidth := displayWidth(text[:maxe])
if matchEndWidth <= maxWidth-2 {
text, _ = trimRight(text, maxWidth-2)
text = append(text, []rune("..")...)
} else {
// Stri..
if matchEndWidth < fullWidth-2 {
text = append(text[:maxe], []rune("..")...)
}
// ..ri..
var diff int32
text, diff = trimLeft(text, maxWidth-2)
// Transform offsets
for idx, offset := range offsets {
b, e := offset.offset[0], offset.offset[1]
b += 2 - diff
e += 2 - diff
b = util.Max32(b, 2)
offsets[idx].offset[0] = b
offsets[idx].offset[1] = util.Max32(b, e)
}
text = append([]rune(".."), text...)
}
} else {
text, _ = trimRight(text, maxWidth-2)
text = append(text, []rune("..")...)
} else {
// Stri..
if matchEndWidth < fullWidth-2 {
text = append(text[:maxe], []rune("..")...)
}
// ..ri..
var diff int32
text, diff = trimLeft(text, maxWidth-2)
// Transform offsets
offsets = make([]Offset, len(item.offsets))
for idx, offset := range item.offsets {
b, e := offset[0], offset[1]
b += 2 - diff
e += 2 - diff
b = util.Max32(b, 2)
if b < e {
offsets[idx] = Offset{b, e}
}
for idx, offset := range offsets {
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-2))
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
}
text = append([]rune(".."), text...)
}
}
sort.Sort(ByOrder(offsets))
var index int32
var substr string
var prefixWidth int
maxOffset := int32(len(text))
for _, offset := range offsets {
b := util.Max32(index, offset[0])
e := util.Max32(index, offset[1])
C.CPrint(col1, bold, string(text[index:b]))
C.CPrint(col2, bold, string(text[b:e]))
b := util.Constrain32(offset.offset[0], index, maxOffset)
e := util.Constrain32(offset.offset[1], index, maxOffset)
substr, prefixWidth = processTabs(text[index:b], prefixWidth)
C.CPrint(col1, bold, substr)
if b < e {
substr, prefixWidth = processTabs(text[b:e], prefixWidth)
C.CPrint(offset.color, offset.bold, substr)
}
index = e
if index >= maxOffset {
break
}
}
if index < int32(len(text)) {
C.CPrint(col1, bold, string(text[index:]))
if index < maxOffset {
substr, _ = processTabs(text[index:], prefixWidth)
C.CPrint(col1, bold, substr)
}
}
func processTabs(runes []rune, prefixWidth int) (string, int) {
var strbuf bytes.Buffer
l := prefixWidth
for _, r := range runes {
w := runeWidth(r, l)
l += w
if r == '\t' {
strbuf.WriteString(strings.Repeat(" ", w))
} else {
strbuf.WriteRune(r)
}
}
return strbuf.String(), l
}
func (t *Terminal) printAll() {
t.printList()
t.printInfo()
t.printPrompt()
t.printInfo()
}
func (t *Terminal) refresh() {
@@ -372,8 +576,13 @@ func (t *Terminal) rubout(pattern string) {
t.input = append(t.input[:t.cx], after...)
}
func keyMatch(key int, event C.Event) bool {
return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ
}
// Loop is called to start Terminal I/O
func (t *Terminal) Loop() {
<-t.startChan
{ // Late initialization
t.mutex.Lock()
t.initFunc()
@@ -387,6 +596,15 @@ func (t *Terminal) Loop() {
<-timer.C
t.reqBox.Set(reqRefresh, nil)
}()
resizeChan := make(chan os.Signal, 1)
signal.Notify(resizeChan, syscall.SIGWINCH)
go func() {
for {
<-resizeChan
t.reqBox.Set(reqRedraw, nil)
}
}()
}
go func() {
@@ -398,6 +616,9 @@ func (t *Terminal) Loop() {
switch req {
case reqPrompt:
t.printPrompt()
if t.inlineInfo {
t.printInfo()
}
case reqInfo:
t.printInfo()
case reqList:
@@ -406,6 +627,8 @@ func (t *Terminal) Loop() {
t.suppress = false
case reqRedraw:
C.Clear()
C.Endwin()
C.Refresh()
t.printAll()
case reqClose:
C.Close()
@@ -439,112 +662,150 @@ func (t *Terminal) Loop() {
}
}
toggle := func() {
idx := t.listIndex(t.cy)
if idx < t.merger.Length() {
item := t.merger.Get(idx)
if _, found := t.selected[item.text]; !found {
t.selected[item.text] = item.origText
if t.cy < t.merger.Length() {
item := t.merger.Get(t.cy)
if _, found := t.selected[item.index]; !found {
var strptr *string
if item.origText != nil {
strptr = item.origText
} else {
strptr = item.text
}
t.selected[item.index] = selectedItem{time.Now(), strptr}
} else {
delete(t.selected, item.text)
delete(t.selected, item.index)
}
req(reqInfo)
}
}
switch event.Type {
case C.Invalid:
for _, key := range t.expect {
if keyMatch(key, event) {
t.pressed = key
req(reqClose)
break
}
}
action := t.keymap[event.Type]
if event.Type == C.Rune {
code := int(event.Char) + int(C.AltZ)
if act, prs := t.keymap[code]; prs {
action = act
}
}
switch action {
case actInvalid:
t.mutex.Unlock()
continue
case C.CtrlA:
case actToggleSort:
t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock()
continue
case actBeginningOfLine:
t.cx = 0
case C.CtrlB:
case actBackwardChar:
if t.cx > 0 {
t.cx--
}
case C.CtrlC, C.CtrlG, C.CtrlQ, C.ESC:
case actAbort:
req(reqQuit)
case C.CtrlD:
case actDeleteChar:
if !t.delChar() && t.cx == 0 {
req(reqQuit)
}
case C.CtrlE:
case actEndOfLine:
t.cx = len(t.input)
case C.CtrlF:
case actForwardChar:
if t.cx < len(t.input) {
t.cx++
}
case C.CtrlH:
case actBackwardDeleteChar:
if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
t.cx--
}
case C.Tab:
case actToggle:
if t.multi && t.merger.Length() > 0 {
toggle()
req(reqList)
}
case actToggleDown:
if t.multi && t.merger.Length() > 0 {
toggle()
t.vmove(-1)
req(reqList)
}
case C.BTab:
case actToggleUp:
if t.multi && t.merger.Length() > 0 {
toggle()
t.vmove(1)
req(reqList)
}
case C.CtrlJ, C.CtrlN:
case actDown:
t.vmove(-1)
req(reqList)
case C.CtrlK, C.CtrlP:
case actUp:
t.vmove(1)
req(reqList)
case C.CtrlM:
case actAccept:
req(reqClose)
case C.CtrlL:
case actClearScreen:
req(reqRedraw)
case C.CtrlU:
case actUnixLineDiscard:
if t.cx > 0 {
t.yanked = copySlice(t.input[:t.cx])
t.input = t.input[t.cx:]
t.cx = 0
}
case C.CtrlW:
case actUnixWordRubout:
if t.cx > 0 {
t.rubout("\\s\\S")
}
case C.AltBS:
case actBackwardKillWord:
if t.cx > 0 {
t.rubout("[^[:alnum:]][[:alnum:]]")
}
case C.CtrlY:
t.input = append(append(t.input[:t.cx], t.yanked...), t.input[t.cx:]...)
case actYank:
suffix := copySlice(t.input[t.cx:])
t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
t.cx += len(t.yanked)
case C.Del:
t.delChar()
case C.PgUp:
t.vmove(maxItems() - 1)
case actPageUp:
t.vmove(t.maxItems() - 1)
req(reqList)
case C.PgDn:
t.vmove(-(maxItems() - 1))
case actPageDown:
t.vmove(-(t.maxItems() - 1))
req(reqList)
case C.AltB:
case actBackwardWord:
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
case C.AltF:
case actForwardWord:
t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
case C.AltD:
case actKillWord:
ncx := t.cx +
findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
if ncx > t.cx {
t.yanked = copySlice(t.input[t.cx:ncx])
t.input = append(t.input[:t.cx], t.input[ncx:]...)
}
case C.Rune:
case actKillLine:
if t.cx < len(t.input) {
t.yanked = copySlice(t.input[t.cx:])
t.input = t.input[:t.cx]
}
case actRune:
prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
t.cx++
case C.Mouse:
case actMouse:
me := event.MouseEvent
mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y
if !t.reverse {
my = C.MaxY() - my - 1
}
min := 2
if t.inlineInfo {
min = 1
}
if me.S != 0 {
// Scroll
if t.merger.Length() > 0 {
@@ -556,8 +817,8 @@ func (t *Terminal) Loop() {
}
} else if me.Double {
// Double-click
if my >= 2 {
if t.vset(my-2) && t.listIndex(t.cy) < t.merger.Length() {
if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
req(reqClose)
}
}
@@ -565,9 +826,9 @@ func (t *Terminal) Loop() {
if my == 0 && mx >= 0 {
// Prompt
t.cx = mx
} else if my >= 2 {
} else if my >= min {
// List
if t.vset(t.offset+my-2) && t.multi && me.Mod {
if t.vset(t.offset+my-min) && t.multi && me.Mod {
toggle()
}
req(reqList)
@@ -578,7 +839,7 @@ func (t *Terminal) Loop() {
t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed {
t.eventBox.Set(EvtSearchNew, nil)
t.eventBox.Set(EvtSearchNew, t.sort)
}
for _, event := range events {
t.reqBox.Set(event, nil)
@@ -588,7 +849,7 @@ func (t *Terminal) Loop() {
func (t *Terminal) constrain() {
count := t.merger.Length()
height := C.MaxY() - 2
height := t.maxItems()
diffpos := t.cy - t.offset
t.cy = util.Constrain(t.cy, 0, count-1)
@@ -621,6 +882,10 @@ func (t *Terminal) vset(o int) bool {
return t.cy == o
}
func maxItems() int {
return C.MaxY() - 2
func (t *Terminal) maxItems() int {
if t.inlineInfo {
return C.MaxY() - 1
} else {
return C.MaxY() - 2
}
}

View File

@@ -16,34 +16,38 @@ type Range struct {
end int
}
// Transformed holds the result of tokenization and transformation
type Transformed struct {
whole *string
parts []Token
}
// Token contains the tokenized part of the strings and its prefix length
type Token struct {
text *string
text *[]rune
prefixLength int
}
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 Range{rangeEllipsis, rangeEllipsis}, true
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 Range{rangeEllipsis, end}, true
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 Range{begin, rangeEllipsis}, true
return newRange(begin, rangeEllipsis), true
} else if strings.Contains(*str, "..") {
ns := strings.Split(*str, "..")
if len(ns) != 2 {
@@ -51,17 +55,17 @@ func ParseRange(str *string) (Range, bool) {
}
begin, err1 := strconv.Atoi(ns[0])
end, err2 := strconv.Atoi(ns[1])
if err1 != nil || err2 != nil {
if err1 != nil || err2 != nil || begin == 0 || end == 0 {
return Range{}, false
}
return Range{begin, end}, true
return newRange(begin, end), true
}
n, err := strconv.Atoi(*str)
if err != nil || n == 0 {
return Range{}, false
}
return Range{n, n}, true
return newRange(n, n), true
}
func withPrefixLengths(tokens []string, begin int) []Token {
@@ -71,8 +75,8 @@ func withPrefixLengths(tokens []string, begin int) []Token {
for idx, token := range tokens {
// Need to define a new local variable instead of the reused token to take
// the pointer to it
str := token
ret[idx] = Token{text: &str, prefixLength: prefixLength}
runes := []rune(token)
ret[idx] = Token{text: &runes, prefixLength: prefixLength}
prefixLength += len([]rune(token))
}
return ret
@@ -132,33 +136,40 @@ func Tokenize(str *string, delimiter *regexp.Regexp) []Token {
return withPrefixLengths(tokens, 0)
}
func joinTokens(tokens []Token) string {
func joinTokens(tokens *[]Token) *string {
ret := ""
for _, token := range tokens {
ret += *token.text
for _, token := range *tokens {
ret += string(*token.text)
}
return ret
return &ret
}
func joinTokensAsRunes(tokens *[]Token) *[]rune {
ret := []rune{}
for _, token := range *tokens {
ret = append(ret, *token.text...)
}
return &ret
}
// Transform is used to transform the input when --with-nth option is given
func Transform(tokens []Token, withNth []Range) *Transformed {
func Transform(tokens []Token, withNth []Range) *[]Token {
transTokens := make([]Token, len(withNth))
numTokens := len(tokens)
whole := ""
for idx, r := range withNth {
part := ""
part := []rune{}
minIdx := 0
if r.begin == r.end {
idx := r.begin
if idx == rangeEllipsis {
part += joinTokens(tokens)
part = append(part, *joinTokensAsRunes(&tokens)...)
} else {
if idx < 0 {
idx += numTokens + 1
}
if idx >= 1 && idx <= numTokens {
minIdx = idx - 1
part += *tokens[idx-1].text
part = append(part, *tokens[idx-1].text...)
}
}
} else {
@@ -185,11 +196,10 @@ func Transform(tokens []Token, withNth []Range) *Transformed {
minIdx = util.Max(0, begin-1)
for idx := begin; idx <= end; idx++ {
if idx >= 1 && idx <= numTokens {
part += *tokens[idx-1].text
part = append(part, *tokens[idx-1].text...)
}
}
}
whole += part
var prefixLength int
if minIdx < numTokens {
prefixLength = tokens[minIdx].prefixLength
@@ -198,7 +208,5 @@ func Transform(tokens []Token, withNth []Range) *Transformed {
}
transTokens[idx] = Token{&part, prefixLength}
}
return &Transformed{
whole: &whole,
parts: transTokens}
return &transTokens
}

View File

@@ -44,13 +44,13 @@ func TestTokenize(t *testing.T) {
// AWK-style
input := " abc: def: ghi "
tokens := Tokenize(&input, nil)
if *tokens[0].text != "abc: " || tokens[0].prefixLength != 2 {
if string(*tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 {
t.Errorf("%s", tokens)
}
// With delimiter
tokens = Tokenize(&input, delimiterRegexp(":"))
if *tokens[0].text != " abc:" || tokens[0].prefixLength != 0 {
if string(*tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 {
t.Errorf("%s", tokens)
}
}
@@ -62,19 +62,19 @@ func TestTransform(t *testing.T) {
{
ranges := splitNth("1,2,3")
tx := Transform(tokens, ranges)
if *tx.whole != "abc: def: ghi: " {
if *joinTokens(tx) != "abc: def: ghi: " {
t.Errorf("%s", *tx)
}
}
{
ranges := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges)
if *tx.whole != "abc: def: ghi: def: ghi: jklabc: " ||
len(tx.parts) != 4 ||
*tx.parts[0].text != "abc: def: " || tx.parts[0].prefixLength != 2 ||
*tx.parts[1].text != "ghi: " || tx.parts[1].prefixLength != 14 ||
*tx.parts[2].text != "def: ghi: jkl" || tx.parts[2].prefixLength != 8 ||
*tx.parts[3].text != "abc: " || tx.parts[3].prefixLength != 2 {
if *joinTokens(tx) != "abc: def: ghi: def: ghi: jklabc: " ||
len(*tx) != 4 ||
string(*(*tx)[0].text) != "abc: def: " || (*tx)[0].prefixLength != 2 ||
string(*(*tx)[1].text) != "ghi: " || (*tx)[1].prefixLength != 14 ||
string(*(*tx)[2].text) != "def: ghi: jkl" || (*tx)[2].prefixLength != 8 ||
string(*(*tx)[3].text) != "abc: " || (*tx)[3].prefixLength != 2 {
t.Errorf("%s", *tx)
}
}
@@ -84,12 +84,12 @@ func TestTransform(t *testing.T) {
{
ranges := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges)
if *tx.whole != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx.parts) != 4 ||
*tx.parts[0].text != " abc: def:" || tx.parts[0].prefixLength != 0 ||
*tx.parts[1].text != " ghi:" || tx.parts[1].prefixLength != 12 ||
*tx.parts[2].text != " def: ghi: jkl" || tx.parts[2].prefixLength != 6 ||
*tx.parts[3].text != " abc:" || tx.parts[3].prefixLength != 0 {
if *joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(*tx) != 4 ||
string(*(*tx)[0].text) != " abc: def:" || (*tx)[0].prefixLength != 0 ||
string(*(*tx)[1].text) != " ghi:" || (*tx)[1].prefixLength != 12 ||
string(*(*tx)[2].text) != " def: ghi: jkl" || (*tx)[2].prefixLength != 6 ||
string(*(*tx)[3].text) != " abc:" || (*tx)[3].prefixLength != 0 {
t.Errorf("%s", *tx)
}
}

42
src/update_assets.rb Executable file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env ruby
# http://www.rubydoc.info/github/rest-client/rest-client/RestClient
require 'rest_client'
if ARGV.length < 3
puts "usage: #$0 <token> <version> <files...>"
exit 1
end
token, version, *files = ARGV
base = "https://api.github.com/repos/junegunn/fzf-bin/releases"
# List releases
rels = JSON.parse(RestClient.get(base, :authorization => "token #{token}"))
rel = rels.find { |r| r['tag_name'] == version }
unless rel
puts "#{version} not found"
exit 1
end
# List assets
assets = Hash[rel['assets'].map { |a| a.values_at *%w[name id] }]
files.select { |f| File.exists? f }.each do |file|
name = File.basename file
if asset_id = assets[name]
puts "#{name} found. Deleting asset id #{asset_id}."
RestClient.delete "#{base}/assets/#{asset_id}",
:authorization => "token #{token}"
else
puts "#{name} not found"
end
puts "Uploading #{name}"
RestClient.post(
"#{base.sub 'api', 'uploads'}/#{rel['id']}/assets?name=#{name}",
File.read(file),
:authorization => "token #{token}",
:content_type => "application/octet-stream")
end

View File

@@ -53,8 +53,8 @@ func (events *Events) Clear() {
}
}
// Peak peaks at the event box if the given event is set
func (b *EventBox) Peak(event EventType) bool {
// Peek peeks at the event box if the given event is set
func (b *EventBox) Peek(event EventType) bool {
b.cond.L.Lock()
defer b.cond.L.Unlock()
_, ok := b.events[event]
@@ -78,3 +78,19 @@ func (b *EventBox) Unwatch(events ...EventType) {
b.ignore[event] = true
}
}
// 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
}
}
})
}
}

View File

@@ -19,6 +19,14 @@ func Max(first int, items ...int) int {
return max
}
// Max32 returns the smallest 32-bit integer
func Min32(first int32, second int32) int32 {
if first <= second {
return first
}
return second
}
// Max32 returns the largest 32-bit integer
func Max32(first int32, second int32) int32 {
if first > second {
@@ -27,6 +35,17 @@ func Max32(first int32, second int32) int32 {
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 {
@@ -50,7 +69,22 @@ func DurWithin(
return val
}
func Between(val int, min int, max int) bool {
return val >= min && val <= max
}
// IsTty returns true is stdin is a terminal
func IsTty() bool {
return int(C.isatty(C.int(os.Stdin.Fd()))) != 0
}
func TrimRight(runes *[]rune) []rune {
var i int
for i = len(*runes) - 1; i >= 0; i-- {
char := (*runes)[i]
if char != ' ' && char != '\t' {
break
}
}
return (*runes)[0 : i+1]
}

View File

@@ -3,20 +3,23 @@ Execute (Setup):
Log 'Test directory: ' . g:dir
Execute (fzf#run with dir option):
let cwd = getcwd()
let result = fzf#run({ 'options': '--filter=vdr', 'dir': g:dir })
AssertEqual ['fzf.vader'], result
AssertEqual getcwd(), cwd
let result = sort(fzf#run({ 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_fzf.rb'], result
AssertEqual ['fzf.vader', 'test_go.rb'], result
AssertEqual getcwd(), cwd
Execute (fzf#run with Funcref command):
let g:ret = []
function! g:proc(e)
function! g:FzfTest(e)
call add(g:ret, a:e)
endfunction
let result = sort(fzf#run({ 'sink': function('g:proc'), 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_fzf.rb'], result
AssertEqual ['fzf.vader', 'test_fzf.rb'], sort(g:ret)
let result = sort(fzf#run({ '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' }))

View File

@@ -1,850 +0,0 @@
#!/usr/bin/env ruby
# encoding: utf-8
require 'rubygems'
require 'curses'
require 'timeout'
require 'stringio'
require 'minitest/autorun'
require 'tempfile'
$LOAD_PATH.unshift File.expand_path('../..', __FILE__)
ENV['FZF_EXECUTABLE'] = '0'
load 'fzf'
class MockTTY
def initialize
@buffer = ''
@mutex = Mutex.new
@condv = ConditionVariable.new
end
def read_nonblock sz
@mutex.synchronize do
take sz
end
end
def take sz
if @buffer.length >= sz
ret = @buffer[0, sz]
@buffer = @buffer[sz..-1]
ret
end
end
def getc
sleep 0.1
while true
@mutex.synchronize do
if char = take(1)
return char
else
@condv.wait(@mutex)
end
end
end
end
def << str
@mutex.synchronize do
@buffer << str
@condv.broadcast
end
self
end
end
class TestFZF < MiniTest::Unit::TestCase
def setup
ENV.delete 'FZF_DEFAULT_SORT'
ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_DEFAULT_COMMAND'
end
def test_default_options
fzf = FZF.new []
assert_equal 1000, fzf.sort
assert_equal false, fzf.multi
assert_equal true, fzf.color
assert_equal nil, fzf.rxflag
assert_equal true, fzf.mouse
assert_equal nil, fzf.nth
assert_equal nil, fzf.with_nth
assert_equal true, fzf.color
assert_equal false, fzf.black
assert_equal true, fzf.ansi256
assert_equal '', fzf.query
assert_equal false, fzf.select1
assert_equal false, fzf.exit0
assert_equal nil, fzf.filter
assert_equal nil, fzf.extended
assert_equal false, fzf.reverse
assert_equal '> ', fzf.prompt
assert_equal false, fzf.print_query
end
def test_environment_variables
# Deprecated
ENV['FZF_DEFAULT_SORT'] = '20000'
fzf = FZF.new []
assert_equal 20000, fzf.sort
assert_equal nil, fzf.nth
ENV['FZF_DEFAULT_OPTS'] =
'-x -m -s 10000 -q " hello world " +c +2 --select-1 -0 ' <<
'--no-mouse -f "goodbye world" --black --with-nth=3,-3..,2 --nth=3,-1,2 --reverse --print-query'
fzf = FZF.new []
assert_equal 10000, fzf.sort
assert_equal ' hello world ',
fzf.query
assert_equal 'goodbye world',
fzf.filter
assert_equal :fuzzy, fzf.extended
assert_equal true, fzf.multi
assert_equal false, fzf.color
assert_equal false, fzf.ansi256
assert_equal true, fzf.black
assert_equal false, fzf.mouse
assert_equal true, fzf.select1
assert_equal true, fzf.exit0
assert_equal true, fzf.reverse
assert_equal true, fzf.print_query
assert_equal [2..2, -1..-1, 1..1], fzf.nth
assert_equal [2..2, -3..-1, 1..1], fzf.with_nth
end
def test_option_parser
# Long opts
fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello --select-1
--exit-0 --filter=howdy --extended-exact
--no-mouse --no-256 --nth=1 --with-nth=.. --reverse --prompt (hi)
--print-query]
assert_equal 2000, fzf.sort
assert_equal true, fzf.multi
assert_equal false, fzf.color
assert_equal false, fzf.ansi256
assert_equal false, fzf.black
assert_equal false, fzf.mouse
assert_equal 0, fzf.rxflag
assert_equal 'hello', fzf.query
assert_equal true, fzf.select1
assert_equal true, fzf.exit0
assert_equal 'howdy', fzf.filter
assert_equal :exact, fzf.extended
assert_equal [0..0], fzf.nth
assert_equal nil, fzf.with_nth
assert_equal true, fzf.reverse
assert_equal '(hi)', fzf.prompt
assert_equal true, fzf.print_query
# Long opts (left-to-right)
fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query=hello
--filter a --filter b --no-256 --black --nth -1 --nth -2
--select-1 --exit-0 --no-select-1 --no-exit-0
--no-sort -i --color --no-multi --256
--reverse --no-reverse --prompt (hi) --prompt=(HI)
--print-query --no-print-query]
assert_equal nil, fzf.sort
assert_equal false, fzf.multi
assert_equal true, fzf.color
assert_equal true, fzf.ansi256
assert_equal true, fzf.black
assert_equal true, fzf.mouse
assert_equal 1, fzf.rxflag
assert_equal 'b', fzf.filter
assert_equal 'hello', fzf.query
assert_equal false, fzf.select1
assert_equal false, fzf.exit0
assert_equal nil, fzf.extended
assert_equal [-2..-2], fzf.nth
assert_equal false, fzf.reverse
assert_equal '(HI)', fzf.prompt
assert_equal false, fzf.print_query
# Short opts
fzf = FZF.new %w[-s2000 +c -m +i -qhello -x -fhowdy +2 -n3 -1 -0]
assert_equal 2000, fzf.sort
assert_equal true, fzf.multi
assert_equal false, fzf.color
assert_equal false, fzf.ansi256
assert_equal 0, fzf.rxflag
assert_equal 'hello', fzf.query
assert_equal 'howdy', fzf.filter
assert_equal :fuzzy, fzf.extended
assert_equal [2..2], fzf.nth
assert_equal true, fzf.select1
assert_equal true, fzf.exit0
# Left-to-right
fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x -fgoodbye +2 -n3 -n4,5
-s 3000 -c +m -i -q world +x -fworld -2 --black --no-black
-1 -0 +1 +0
]
assert_equal 3000, fzf.sort
assert_equal false, fzf.multi
assert_equal true, fzf.color
assert_equal true, fzf.ansi256
assert_equal false, fzf.black
assert_equal 1, fzf.rxflag
assert_equal 'world', fzf.query
assert_equal false, fzf.select1
assert_equal false, fzf.exit0
assert_equal 'world', fzf.filter
assert_equal nil, fzf.extended
assert_equal [3..3, 4..4], fzf.nth
rescue SystemExit => e
assert false, "Exited"
end
def test_invalid_option
[
%w[--unknown],
%w[yo dawg],
%w[--nth=0],
%w[-n 0],
%w[-n 1..2..3],
%w[-n 1....],
%w[-n ....3],
%w[-n 1....3],
%w[-n 1..0],
%w[--nth ..0],
].each do |argv|
assert_raises(SystemExit) do
fzf = FZF.new argv
end
end
end
def test_width
fzf = FZF.new []
assert_equal 5, fzf.width('abcde')
assert_equal 4, fzf.width('한글')
assert_equal 5, fzf.width('한글.')
end if RUBY_VERSION >= '1.9'
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 if RUBY_VERSION >= '1.9'
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_rxflag
assert_equal nil, FZF::FuzzyMatcher.new(nil).rxflag
assert_equal 0, FZF::FuzzyMatcher.new(0).rxflag
assert_equal 1, FZF::FuzzyMatcher.new(1).rxflag
assert_equal 1, FZF::FuzzyMatcher.new(nil).rxflag_for('abc')
assert_equal 0, FZF::FuzzyMatcher.new(nil).rxflag_for('Abc')
assert_equal 0, FZF::FuzzyMatcher.new(0).rxflag_for('abc')
assert_equal 0, FZF::FuzzyMatcher.new(0).rxflag_for('Abc')
assert_equal 1, FZF::FuzzyMatcher.new(1).rxflag_for('abc')
assert_equal 1, FZF::FuzzyMatcher.new(1).rxflag_for('Abc')
end
def test_fuzzy_matcher_case_sensitive
# Smart-case match (Uppercase found)
assert_equal [['Fruit', [[0, 5]]]],
FZF::FuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], 'Fruit', '', '').sort
# Smart-case match (Uppercase not-found)
assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
FZF::FuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], 'fruit', '', '').sort
# Case-sensitive match (-i)
assert_equal [['Fruit', [[0, 5]]]],
FZF::FuzzyMatcher.new(0).match(%w[Fruit Grapefruit], 'Fruit', '', '').sort
# Case-insensitive match (+i)
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_case_sensitive
%w['Fruit Fruit$].each do |q|
# Smart-case match (Uppercase found)
assert_equal [['Fruit', [[0, 5]]]],
FZF::ExtendedFuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], q, '', '').sort
# Smart-case match (Uppercase not-found)
assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
FZF::ExtendedFuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], q.downcase, '', '').sort
# Case-sensitive match (-i)
assert_equal [['Fruit', [[0, 5]]]],
FZF::ExtendedFuzzyMatcher.new(0).match(%w[Fruit Grapefruit], q, '', '').sort
# Case-insensitive match (+i)
assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
FZF::ExtendedFuzzyMatcher.new(Regexp::IGNORECASE).
match(%w[Fruit Grapefruit], q, '', '').sort
end
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('^juici$', '').length
assert_equal 1, match.call('^juice$', '').length
assert_equal 0, match.call('^.*$', '').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.sort(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.sort(xmatcher.match(list, '0 1', '', '')))
assert_equal(
[["_01_", [[1, 3], [0, 4]], [4, 4, "_01_"]],
["___01___", [[3, 5], [0, 2]], [4, 8, "___01___"]],
["____0_1", [[4, 7], [0, 2]], [5, 7, "____0_1"]],
["0____1", [[0, 6], [1, 3]], [6, 6, "0____1"]],
["0_____1", [[0, 7], [1, 3]], [7, 7, "0_____1"]],
["0______1", [[0, 8], [1, 3]], [8, 8, "0______1"]]],
FZF.sort(xmatcher.match(list, '01 __', '', '')).map { |tuple|
tuple << FZF.rank(tuple)
}
)
end
def test_extended_exact_mode
exact = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :exact
fuzzy = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy
list = %w[
extended-exact-mode-not-fuzzy
extended'-fuzzy-mode
]
assert_equal 2, fuzzy.match(list, 'extended', '', '').length
assert_equal 2, fuzzy.match(list, 'mode extended', '', '').length
assert_equal 2, fuzzy.match(list, 'xtndd', '', '').length
assert_equal 2, fuzzy.match(list, "'-fuzzy", '', '').length
assert_equal 2, exact.match(list, 'extended', '', '').length
assert_equal 2, exact.match(list, 'mode extended', '', '').length
assert_equal 0, exact.match(list, 'xtndd', '', '').length
assert_equal 1, exact.match(list, "'-fuzzy", '', '').length
assert_equal 2, exact.match(list, "-fuzzy", '', '').length
end
# ^$ -> matches empty item
def test_format_empty_item
fzf = FZF.new []
item = ['', [[0, 0]]]
line, offsets = item
tokens = fzf.format line, 80, offsets
assert_equal [], tokens
end
def test_mouse_event
interval = FZF::MouseEvent::DOUBLE_CLICK_INTERVAL
me = FZF::MouseEvent.new nil
me.v = 10
assert_equal false, me.double?(10)
assert_equal false, me.double?(20)
me.v = 20
assert_equal false, me.double?(10)
assert_equal false, me.double?(20)
me.v = 20
assert_equal false, me.double?(10)
assert_equal true, me.double?(20)
sleep interval
assert_equal false, me.double?(20)
end
def test_nth_match
list = [
' first second third',
'fourth fifth sixth',
]
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE
assert_equal list, matcher.match(list, 'f', '', '').map(&:first)
assert_equal [
[list[0], [[2, 5]]],
[list[1], [[9, 17]]]], matcher.match(list, 'is', '', '')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1]
assert_equal [[list[1], [[8, 9]]]], matcher.match(list, 'f', '', '')
assert_equal [[list[0], [[8, 9]]]], matcher.match(list, 's', '', '')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2..2]
assert_equal [[list[0], [[19, 20]]]], matcher.match(list, 'r', '', '')
# Comma-separated
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2..2, 0..0]
assert_equal [[list[0], [[19, 20]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '')
# Ordered
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0, 2..2]
assert_equal [[list[0], [[3, 4]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '')
regex = FZF.build_delim_regex "\t"
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0], regex
assert_equal [[list[0], [[3, 10]]]], matcher.match(list, 're', '', '')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1], regex
assert_equal [], matcher.match(list, 'r', '', '')
assert_equal [[list[1], [[9, 17]]]], matcher.match(list, 'is', '', '')
# Negative indexing
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [-1..-1], regex
assert_equal [[list[0], [[3, 6]]]], matcher.match(list, 'rt', '', '')
assert_equal [[list[0], [[2, 5]]], [list[1], [[9, 17]]]], matcher.match(list, 'is', '', '')
# Regex delimiter
regex = FZF.build_delim_regex "[ \t]+"
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0], regex
assert_equal [list[1]], matcher.match(list, 'f', '', '').map(&:first)
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1], regex
assert_equal [[list[0], [[1, 2]]], [list[1], [[8, 9]]]], matcher.match(list, 'f', '', '')
end
def test_nth_match_range
list = [
' first second third',
'fourth fifth sixth',
]
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..2]
assert_equal [[list[0], [[8, 20]]]], matcher.match(list, 'sr', '', '')
assert_equal [], matcher.match(list, 'fo', '', '')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..-1, 0..0]
assert_equal [[list[0], [[8, 20]]]], matcher.match(list, 'sr', '', '')
assert_equal [[list[1], [[0, 2]]]], matcher.match(list, 'fo', '', '')
matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [0..0, 1..2]
assert_equal [], matcher.match(list, '^t', '', '')
matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [0..1, 2..2]
assert_equal [[list[0], [[16, 17]]]], matcher.match(list, '^t', '', '')
matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [1..-1]
assert_equal [[list[0], [[8, 9]]]], matcher.match(list, '^s', '', '')
end
def stream_for str, delay = 0
StringIO.new(str).tap do |sio|
sio.instance_eval do
alias org_gets gets
def gets
org_gets.tap { |e| sleep(@delay) unless e.nil? }
end
def reopen _
end
end
sio.instance_variable_set :@delay, delay
end
end
def assert_fzf_output opts, given, expected
stream = stream_for given
output = stream_for ''
def sorted_lines line
line.split($/).sort
end
begin
tty = MockTTY.new
$stdout = output
fzf = FZF.new(opts, stream)
fzf.instance_variable_set :@tty, tty
thr = block_given? && Thread.new { yield tty }
fzf.start
thr && thr.join
rescue SystemExit => e
assert_equal 0, e.status
assert_equal sorted_lines(expected), sorted_lines(output.string)
ensure
$stdout = STDOUT
end
end
def test_filter
{
%w[--filter=ol] => 'World',
%w[--filter=ol --print-query] => "ol\nWorld",
}.each do |opts, expected|
assert_fzf_output opts, "Hello\nWorld", expected
end
end
def test_select_1
{
%w[--query=ol --select-1] => 'World',
%w[--query=ol --select-1 --print-query] => "ol\nWorld",
}.each do |opts, expected|
assert_fzf_output opts, "Hello\nWorld", expected
end
end
def test_select_1_without_query
assert_fzf_output %w[--select-1], 'Hello World', 'Hello World'
end
def test_select_1_ambiguity
begin
Timeout::timeout(0.5) do
assert_fzf_output %w[--query=o --select-1], "hello\nworld", "should not match"
end
rescue Timeout::Error
Curses.close_screen
end
end
def test_exit_0
{
%w[--query=zz --exit-0] => '',
%w[--query=zz --exit-0 --print-query] => 'zz',
}.each do |opts, expected|
assert_fzf_output opts, "Hello\nWorld", expected
end
end
def test_exit_0_without_query
assert_fzf_output %w[--exit-0], '', ''
end
def test_with_nth
source = "hello world\nbatman"
assert_fzf_output %w[-0 -1 --with-nth=2,1 -x -q ^worl],
source, 'hello world'
assert_fzf_output %w[-0 -1 --with-nth=2,1 -x -q llo$],
source, 'hello world'
assert_fzf_output %w[-0 -1 --with-nth=.. -x -q llo$],
source, ''
assert_fzf_output %w[-0 -1 --with-nth=2,2,2,..,1 -x -q worlworlworlhellworlhell],
source, 'hello world'
assert_fzf_output %w[-0 -1 --with-nth=1,1,-1,1 -x -q batbatbatbat],
source, 'batman'
end
def test_with_nth_transform
fzf = FZF.new %w[--with-nth 2..,1]
assert_equal 'my world hello', fzf.transform('hello my world')
assert_equal 'my world hello', fzf.transform('hello my world')
assert_equal 'my world hello', fzf.transform('hello my world ')
fzf = FZF.new %w[--with-nth 2,-1,2]
assert_equal 'my world my', fzf.transform('hello my world')
assert_equal 'world world world', fzf.transform('hello world')
assert_equal 'world world world', fzf.transform('hello world ')
end
def test_ranking_overlap_match_regions
list = [
'1 3 4 2',
'1 2 3 4'
]
assert_equal [
['1 2 3 4', [[0, 13], [16, 22]]],
['1 3 4 2', [[0, 24], [12, 17]]],
], FZF.sort(FZF::ExtendedFuzzyMatcher.new(nil).match(list, '12 34', '', ''))
end
def test_constrain
fzf = FZF.new []
# [#**** ]
assert_equal [false, 0, 0], fzf.constrain(0, 0, 5, 100)
# *****[**#** ... ] => [**#******* ... ]
assert_equal [true, 0, 2], fzf.constrain(5, 7, 10, 100)
# [**********]**#** => ***[*********#]**
assert_equal [true, 3, 12], fzf.constrain(0, 12, 15, 10)
# *****[**#** ] => ***[**#****]
assert_equal [true, 3, 5], fzf.constrain(5, 7, 10, 7)
# *****[**#** ] => ****[**#***]
assert_equal [true, 4, 6], fzf.constrain(5, 7, 10, 6)
# ***** [#] => ****[#]
assert_equal [true, 4, 4], fzf.constrain(10, 10, 5, 1)
# [ ] #**** => [#]****
assert_equal [true, 0, 0], fzf.constrain(-5, 0, 5, 1)
# [ ] **#** => **[#]**
assert_equal [true, 2, 2], fzf.constrain(-5, 2, 5, 1)
# [***** #] => [****# ]
assert_equal [true, 0, 4], fzf.constrain(0, 7, 5, 10)
# **[***** #] => [******# ]
assert_equal [true, 0, 6], fzf.constrain(2, 10, 7, 10)
end
def test_invalid_utf8
tmp = Tempfile.new('fzf')
tmp << 'hello ' << [0xff].pack('C*') << ' world' << $/ << [0xff].pack('C*')
tmp.close
begin
Timeout::timeout(0.5) do
FZF.new(%w[-n..,1,2.. -q^ -x], File.open(tmp.path)).start
end
rescue Timeout::Error
Curses.close_screen
end
ensure
tmp.unlink
end
def test_with_nth_mock_tty
# Manual selection with input
assert_fzf_output ["--with-nth=2,1"], "hello world", "hello world" do |tty|
tty << "world"
tty << "hell"
tty << "\r"
end
# Manual selection without input
assert_fzf_output ["--with-nth=2,1"], "hello world", "hello world" do |tty|
tty << "\r"
end
# Manual selection with input and --multi
lines = "hello world\ngoodbye world"
assert_fzf_output %w[-m --with-nth=2,1], lines, lines do |tty|
tty << "o"
tty << "\e[Z\e[Z"
tty << "\r"
end
# Manual selection without input and --multi
assert_fzf_output %w[-m --with-nth=2,1], lines, lines do |tty|
tty << "\e[Z\e[Z"
tty << "\r"
end
# ALT-D
assert_fzf_output %w[--print-query], "", "hello baby = world" do |tty|
tty << "hello world baby"
tty << alt(:b) << alt(:b) << alt(:d)
tty << ctrl(:e) << " = " << ctrl(:y)
tty << "\r"
end
# ALT-BACKSPACE
assert_fzf_output %w[--print-query], "", "hello baby = world " do |tty|
tty << "hello world baby"
tty << alt(:b) << alt(127.chr)
tty << ctrl(:e) << " = " << ctrl(:y)
tty << "\r"
end
# Word-movements
assert_fzf_output %w[--print-query], "", "ello!_orld!~ foo=?" do |tty|
tty << "hello_world==baby?"
tty << alt(:b) << ctrl(:d)
tty << alt(:b) << ctrl(:d)
tty << alt(:b) << ctrl(:d)
tty << alt(:f) << '!'
tty << alt(:f) << '!'
tty << alt(:d) << '~'
tty << " foo=bar foo=bar"
tty << ctrl(:w)
tty << alt(127.chr)
tty << "\r"
end
end
def alt chr
"\e#{chr}"
end
def ctrl char
char.to_s.ord - 'a'.ord + 1
end
end

739
test/test_go.rb Normal file
View File

@@ -0,0 +1,739 @@
#!/usr/bin/env ruby
# encoding: utf-8
require 'minitest/autorun'
require 'fileutils'
DEFAULT_TIMEOUT = 20
base = File.expand_path('../../', __FILE__)
Dir.chdir base
FZF = "#{base}/bin/fzf"
class NilClass
def include? str
false
end
def start_with? str
false
end
def end_with? str
false
end
end
def wait
since = Time.now
while Time.now - since < DEFAULT_TIMEOUT
return if yield
sleep 0.05
end
throw 'timeout'
end
class Shell
class << self
def bash
'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.bash'
end
def zsh
FileUtils.mkdir_p '/tmp/fzf-zsh'
FileUtils.cp File.expand_path('~/.fzf.zsh'), '/tmp/fzf-zsh/.zshrc'
'PS1= PROMPT_COMMAND= HISTSIZE=100 ZDOTDIR=/tmp/fzf-zsh zsh'
end
end
end
class Tmux
TEMPNAME = '/tmp/fzf-test.txt'
attr_reader :win
def initialize shell = :bash
@win =
case shell
when :bash
go("new-window -d -P -F '#I' '#{Shell.bash}'").first
when :zsh
go("new-window -d -P -F '#I' '#{Shell.zsh}'").first
when :fish
go("new-window -d -P -F '#I' 'fish'").first
else
raise "Unknown shell: #{shell}"
end
@lines = `tput lines`.chomp.to_i
if shell == :fish
send_keys('function fish_prompt; end; clear', :Enter)
self.until { |lines| lines.empty? }
end
end
def closed?
!go("list-window -F '#I'").include?(win)
end
def close
wait do
send_keys 'C-c', 'C-u', 'exit', :Enter
closed?
end
end
def kill
go("kill-window -t #{win} 2> /dev/null")
end
def send_keys *args
target =
if args.last.is_a?(Hash)
hash = args.pop
go("select-window -t #{win}")
"#{win}.#{hash[:pane]}"
else
win
end
args = args.map { |a| %{"#{a}"} }.join ' '
go("send-keys -t #{target} #{args}")
end
def capture pane = 0
File.unlink TEMPNAME while File.exists? TEMPNAME
wait do
go("capture-pane -t #{win}.#{pane} \\; save-buffer #{TEMPNAME} 2> /dev/null")
$?.exitstatus == 0
end
File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
end
def until pane = 0
lines = nil
begin
wait do
lines = capture(pane)
class << lines
def item_count
self[-2] ? self[-2].strip.split('/').last.to_i : 0
end
end
yield lines
end
rescue Exception
puts $!.backtrace
puts '>' * 80
puts lines
puts '<' * 80
raise
end
lines
end
def prepare
tries = 0
begin
self.send_keys 'C-u', 'hello', 'Right'
self.until { |lines| lines[-1].end_with?('hello') }
rescue Exception
(tries += 1) < 5 ? retry : raise
end
self.send_keys 'C-u'
end
private
def go *args
%x[tmux #{args.join ' '}].split($/)
end
end
class TestBase < Minitest::Test
TEMPNAME = '/tmp/output'
attr_reader :tmux
def setup
ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_DEFAULT_COMMAND'
File.unlink TEMPNAME while File.exists?(TEMPNAME)
end
def readonce
wait { File.exists?(TEMPNAME) }
File.read(TEMPNAME)
ensure
File.unlink TEMPNAME while File.exists?(TEMPNAME)
end
def fzf(*opts)
fzf!(*opts) + " > #{TEMPNAME}.tmp; mv #{TEMPNAME}.tmp #{TEMPNAME}"
end
def fzf!(*opts)
opts = opts.map { |o|
case o
when Symbol
o = o.to_s
o.length > 1 ? "--#{o.gsub('_', '-')}" : "-#{o}"
when String, Numeric
o.to_s
else
nil
end
}.compact
"#{FZF} #{opts.join ' '}"
end
end
class TestGoFZF < TestBase
def setup
super
@tmux = Tmux.new
end
def teardown
@tmux.kill
end
def test_vanilla
tmux.send_keys "seq 1 100000 | #{fzf}", :Enter
tmux.until { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ }
lines = tmux.capture
assert_equal ' 2', lines[-4]
assert_equal '> 1', lines[-3]
assert_equal ' 100000/100000', lines[-2]
assert_equal '>', lines[-1]
# Testing basic key bindings
tmux.send_keys '99', 'C-a', '1', 'C-f', '3', 'C-b', 'C-h', 'C-u', 'C-e', 'C-y', 'C-k', 'Tab', 'BTab'
tmux.until { |lines| lines[-2] == ' 856/100000' }
lines = tmux.capture
assert_equal '> 1391', lines[-4]
assert_equal ' 391', lines[-3]
assert_equal ' 856/100000', lines[-2]
assert_equal '> 391', lines[-1]
tmux.send_keys :Enter
tmux.close
assert_equal '1391', readonce.chomp
end
def test_fzf_default_command
tmux.send_keys "FZF_DEFAULT_COMMAND='echo hello' #{fzf}", :Enter
tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Enter
tmux.close
assert_equal 'hello', readonce.chomp
end
def test_key_bindings
tmux.send_keys "#{FZF} -q 'foo bar foo-bar'", :Enter
tmux.until { |lines| lines.last =~ /^>/ }
# CTRL-A
tmux.send_keys "C-A", "("
tmux.until { |lines| lines.last == '> (foo bar foo-bar' }
# META-F
tmux.send_keys :Escape, :f, ")"
tmux.until { |lines| lines.last == '> (foo) bar foo-bar' }
# CTRL-B
tmux.send_keys "C-B", "var"
tmux.until { |lines| lines.last == '> (foovar) bar foo-bar' }
# Left, CTRL-D
tmux.send_keys :Left, :Left, "C-D"
tmux.until { |lines| lines.last == '> (foovr) bar foo-bar' }
# META-BS
tmux.send_keys :Escape, :BSpace
tmux.until { |lines| lines.last == '> (r) bar foo-bar' }
# CTRL-Y
tmux.send_keys "C-Y", "C-Y"
tmux.until { |lines| lines.last == '> (foovfoovr) bar foo-bar' }
# META-B
tmux.send_keys :Escape, :b, :Space, :Space
tmux.until { |lines| lines.last == '> ( foovfoovr) bar foo-bar' }
# CTRL-F / Right
tmux.send_keys 'C-F', :Right, '/'
tmux.until { |lines| lines.last == '> ( fo/ovfoovr) bar foo-bar' }
# CTRL-H / BS
tmux.send_keys 'C-H', :BSpace
tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-bar' }
# CTRL-E
tmux.send_keys "C-E", 'baz'
tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-barbaz' }
# CTRL-U
tmux.send_keys "C-U"
tmux.until { |lines| lines.last == '>' }
# CTRL-Y
tmux.send_keys "C-Y"
tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-barbaz' }
# CTRL-W
tmux.send_keys "C-W", "bar-foo"
tmux.until { |lines| lines.last == '> ( fovfoovr) bar bar-foo' }
# META-D
tmux.send_keys :Escape, :b, :Escape, :b, :Escape, :d, "C-A", "C-Y"
tmux.until { |lines| lines.last == '> bar( fovfoovr) bar -foo' }
# CTRL-M
tmux.send_keys "C-M"
tmux.until { |lines| lines.last !~ /^>/ }
tmux.close
end
def test_multi_order
tmux.send_keys "seq 1 10 | #{fzf :multi}", :Enter
tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Tab, :Up, :Up, :Tab, :Tab, :Tab, # 3, 2
'C-K', 'C-K', 'C-K', 'C-K', :BTab, :BTab, # 5, 6
:PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7
tmux.until { |lines| lines[-2].include? '(6)' }
tmux.send_keys "C-M"
assert_equal %w[3 2 5 6 8 7], readonce.split($/)
tmux.close
end
def test_with_nth
[true, false].each do |multi|
tmux.send_keys "(echo ' 1st 2nd 3rd/';
echo ' first second third/') |
#{fzf multi && :multi, :x, :nth, 2, :with_nth, '2,-1,1'}",
:Enter
tmux.until { |lines| lines[-2].include?('2/2') }
# Transformed list
lines = tmux.capture
assert_equal ' second third/first', lines[-4]
assert_equal '> 2nd 3rd/1st', lines[-3]
# However, the output must not be transformed
if multi
tmux.send_keys :BTab, :BTab, :Enter
assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.split($/)
else
tmux.send_keys '^', '3'
tmux.until { |lines| lines[-2].include?('1/2') }
tmux.send_keys :Enter
assert_equal [' 1st 2nd 3rd/'], readonce.split($/)
end
end
end
def test_scroll
[true, false].each do |rev|
tmux.send_keys "seq 1 100 | #{fzf rev && :reverse}", :Enter
tmux.until { |lines| lines.include? ' 100/100' }
tmux.send_keys *110.times.map { rev ? :Down : :Up }
tmux.until { |lines| lines.include? '> 100' }
tmux.send_keys :Enter
assert_equal '100', readonce.chomp
end
end
def test_select_1
tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 5555, :'1'}", :Enter
assert_equal ['5555', '55'], readonce.split($/)
end
def test_exit_0
tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 555555, :'0'}", :Enter
assert_equal ['555555'], readonce.split($/)
end
def test_select_1_exit_0_fail
[:'0', :'1', [:'1', :'0']].each do |opt|
tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter
tmux.until { |lines| lines.last =~ /^> 5/ }
tmux.send_keys :BTab, :BTab, :BTab, :Enter
assert_equal ['5', '5', '15', '25'], readonce.split($/)
end
end
def test_query_unicode
tmux.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter
tmux.until { |lines| lines[-2].include? '1/2' }
tmux.send_keys :Enter
assert_equal ['가나다'], readonce.split($/)
end
def test_sync
tmux.send_keys "seq 1 100 | #{fzf! :multi} | awk '{print \\$1 \\$1}' | #{fzf :sync}", :Enter
tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 9
tmux.until { |lines| lines[-2] == ' 19/100' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter
tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 'C-K', :Enter
assert_equal ['1919'], readonce.split($/)
end
def test_tac
tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter
assert_equal %w[1000 999 998], readonce.split($/)
end
def test_tac_sort
tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys '99'
tmux.send_keys :BTab, :BTab, :BTab, :Enter
assert_equal %w[99 999 998], readonce.split($/)
end
def test_tac_nosort
tmux.send_keys "seq 1 1000 | #{fzf :tac, :no_sort, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys '00'
tmux.until { |lines| lines[-2].include? '10/1000' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter
assert_equal %w[1000 900 800], readonce.split($/)
end
def test_expect
test = lambda do |key, feed, expected = key|
tmux.send_keys "seq 1 100 | #{fzf :expect, key}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys '55'
tmux.until { |lines| lines[-2].include? '1/100' }
tmux.send_keys *feed
assert_equal [expected, '55'], readonce.split($/)
end
test.call 'ctrl-t', 'C-T'
test.call 'ctrl-t', 'Enter', ''
test.call 'alt-c', [:Escape, :c]
test.call 'f1', 'f1'
test.call 'f2', 'f2'
test.call 'f3', 'f3'
test.call 'f2,f4', 'f2', 'f2'
test.call 'f2,f4', 'f4', 'f4'
test.call '@', '@'
end
def test_expect_print_query
tmux.send_keys "seq 1 100 | #{fzf '--expect=alt-z', :print_query}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys '55'
tmux.until { |lines| lines[-2].include? '1/100' }
tmux.send_keys :Escape, :z
assert_equal ['55', 'alt-z', '55'], readonce.split($/)
end
def test_expect_print_query_select_1
tmux.send_keys "seq 1 100 | #{fzf '-q55 -1 --expect=alt-z --print-query'}", :Enter
assert_equal ['55', '', '55'], readonce.split($/)
end
def test_toggle_sort
['--toggle-sort=ctrl-r', '--bind=ctrl-r:toggle-sort'].each do |opt|
tmux.send_keys "seq 1 111 | #{fzf "-m +s --tac #{opt} -q11"}", :Enter
tmux.until { |lines| lines[-3].include? '> 111' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include? '4/111 (1)' }
tmux.send_keys 'C-R'
tmux.until { |lines| lines[-3].include? '> 11' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include? '4/111/S (2)' }
tmux.send_keys :Enter
assert_equal ['111', '11'], readonce.split($/)
end
end
def test_unicode_case
tempname = TEMPNAME + Time.now.to_f.to_s
writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4]
assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/)
assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/)
rescue
File.unlink tempname
end
def test_tiebreak
tempname = TEMPNAME + Time.now.to_f.to_s
input = %w[
--foobar--------
-----foobar---
----foobar--
-------foobar-
]
writelines tempname, input
assert_equal input, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=index`.split($/)
by_length = %w[
----foobar--
-----foobar---
-------foobar-
--foobar--------
]
assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar`.split($/)
assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=length`.split($/)
by_begin = %w[
--foobar--------
----foobar--
-----foobar---
-------foobar-
]
assert_equal by_begin, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=begin`.split($/)
assert_equal by_begin, `cat #{tempname} | #{FZF} -f"!z foobar" -x --tiebreak begin`.split($/)
assert_equal %w[
-------foobar-
----foobar--
-----foobar---
--foobar--------
], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/)
assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/)
rescue
File.unlink tempname
end
def test_invalid_cache
tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter
tmux.until { |lines| lines[-2].include? '2/3' }
tmux.send_keys :BSpace
tmux.until { |lines| lines[-2].include? '3/3' }
tmux.send_keys :D
tmux.until { |lines| lines[-2].include? '1/3' }
tmux.send_keys :Enter
end
def test_smart_case_for_each_term
assert_equal 1, `echo Foo bar | #{FZF} -x -f "foo Fbar" | wc -l`.to_i
end
def test_bind
tmux.send_keys "seq 1 1000 | #{
fzf '-m --bind=ctrl-j:accept,u:up,T:toggle-up,t:toggle'}", :Enter
tmux.until { |lines| lines[-2].end_with? '/1000' }
tmux.send_keys 'uuu', 'TTT', 'tt', 'uu', 'ttt', 'C-j'
assert_equal %w[4 5 6 9], readonce.split($/)
end
private
def writelines path, lines
File.unlink path while File.exists? path
File.open(path, 'w') { |f| f << lines.join($/) }
end
end
module TestShell
def setup
super
end
def teardown
@tmux.kill
end
def test_ctrl_t
tmux.prepare
tmux.send_keys 'C-t', pane: 0
lines = tmux.until(1) { |lines| lines.item_count > 1 }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, :Enter, pane: 1
tmux.until(0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c'
# FZF_TMUX=0
new_shell
tmux.send_keys 'C-t', pane: 0
lines = tmux.until(0) { |lines| lines.item_count > 1 }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, :Enter, pane: 0
tmux.until(0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c', 'C-d'
end
def test_alt_c
tmux.prepare
tmux.send_keys :Escape, :c, pane: 0
lines = tmux.until(1) { |lines| lines.item_count > 0 }
expected = lines[-3][2..-1]
tmux.send_keys :Enter, pane: 1
tmux.prepare
tmux.send_keys :pwd, :Enter
tmux.until { |lines| lines[-1].end_with?(expected) }
end
def test_ctrl_r
tmux.prepare
tmux.send_keys 'echo 1st', :Enter; tmux.prepare
tmux.send_keys 'echo 2nd', :Enter; tmux.prepare
tmux.send_keys 'echo 3d', :Enter; tmux.prepare
tmux.send_keys 'echo 3rd', :Enter; tmux.prepare
tmux.send_keys 'echo 4th', :Enter; tmux.prepare
tmux.send_keys 'C-r', pane: 0
tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys '3d', pane: 1
tmux.until(1) { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort
tmux.send_keys :Enter, pane: 1
tmux.until { |lines| lines[-1] == 'echo 3rd' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == '3rd' }
end
end
module CompletionTest
def test_file_completion
FileUtils.mkdir_p '/tmp/fzf-test'
FileUtils.mkdir_p '/tmp/fzf test'
(1..100).each { |i| FileUtils.touch "/tmp/fzf-test/#{i}" }
['no~such~user', '/tmp/fzf test/foobar', '~/.fzf-home'].each do |f|
FileUtils.touch File.expand_path(f)
end
tmux.prepare
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0
tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys :BTab, :BTab, :Enter
tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1].include?('/tmp/fzf-test/10') &&
lines[-1].include?('/tmp/fzf-test/100')
end
# ~USERNAME**<TAB>
tmux.send_keys 'C-u'
tmux.send_keys "cat ~#{ENV['USER']}**", :Tab, pane: 0
tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys '.fzf-home'
tmux.until(1) { |lines| lines[-3].end_with? '.fzf-home' }
tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1].end_with?('.fzf-home')
end
# ~INVALID_USERNAME**<TAB>
tmux.send_keys 'C-u'
tmux.send_keys "cat ~such**", :Tab, pane: 0
tmux.until(1) { |lines| lines[-3].end_with? 'no~such~user' }
tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1].end_with?('no~such~user')
end
# /tmp/fzf\ test**<TAB>
tmux.send_keys 'C-u'
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab, pane: 0
tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1].end_with?('/tmp/fzf\ test/foobar')
end
ensure
['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f|
FileUtils.rm_rf File.expand_path(f)
end
end
def test_dir_completion
tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0
tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys :BTab, :BTab # BTab does not work here
tmux.send_keys 55
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1] == 'cd /tmp/fzf-test/d55/'
end
tmux.send_keys :xx
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
# Should not match regular files (bash-only)
if self.class == TestBash
tmux.send_keys :Tab
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
end
# Fail back to plusdirs
tmux.send_keys :BSpace, :BSpace, :BSpace
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' }
end
def test_process_completion
tmux.send_keys 'sleep 12345 &', :Enter
lines = tmux.until { |lines| lines[-1].start_with? '[1]' }
pid = lines[-1].split.last
tmux.prepare
tmux.send_keys 'kill ', :Tab, pane: 0
tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys 'sleep12345'
tmux.until(1) { |lines| lines[-3].include? 'sleep 12345' }
tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1] == "kill #{pid}"
end
ensure
Process.kill 'KILL', pid.to_i rescue nil if pid
end
end
class TestBash < TestBase
include TestShell
include CompletionTest
def new_shell
tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter
tmux.prepare
end
def setup
super
@tmux = Tmux.new :bash
end
end
class TestZsh < TestBase
include TestShell
include CompletionTest
def new_shell
tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter
tmux.prepare
end
def setup
super
@tmux = Tmux.new :zsh
end
end
class TestFish < TestBase
include TestShell
def new_shell
tmux.send_keys 'env FZF_TMUX=0 fish', :Enter
tmux.send_keys 'function fish_prompt; end; clear', :Enter
tmux.until { |lines| lines.empty? }
end
def setup
super
@tmux = Tmux.new :fish
end
end

View File

@@ -68,6 +68,7 @@ fi
if [ -d ~/.config/fish/functions ]; then
remove ~/.config/fish/functions/fzf.fish
remove ~/.config/fish/functions/fzf_key_bindings.fish
if [ "$(ls -A ~/.config/fish/functions)" ]; then
echo "Can't delete non-empty directory: \"~/.config/fish/functions\""