Compare commits

...

207 Commits

Author SHA1 Message Date
Junegunn Choi
c656cfbdce Update doc 2015-09-12 13:31:07 +09:00
Junegunn Choi
de829c0938 0.10.5 2015-09-12 12:50:32 +09:00
Junegunn Choi
64443221aa Fix #344 - Backward scan when --tiebreak=end 2015-09-12 11:37:55 +09:00
Junegunn Choi
9017e29741 Make it possible to unquote the term in extended-exact mode
Close #338
2015-09-12 11:00:30 +09:00
Junegunn Choi
0a22142d88 [fzf-tmux] Fix #343 - Escape backticks in --query 2015-09-07 18:40:39 +09:00
Junegunn Choi
ac160f98a8 [gvim] Fix #342 - Should not escape launcher part of the command 2015-09-05 21:39:12 +09:00
Junegunn Choi
62e01a2a62 [vim] Escape newline character when running fzf with :!
Fixes Helptags! command from fzf.vim
2015-09-01 01:13:35 +09:00
Junegunn Choi
5660cebaf6 [zsh-completion] Temporarily unset shwordsplit (#328) 2015-09-01 00:51:28 +09:00
Junegunn Choi
a7e588ceac Merge pull request #336 from fazibear/fix-fish-streams
Fix CTRL-T on fish to work asynchronously
2015-08-30 21:21:13 +09:00
Michał Kalbarczyk
5baf1c5536 fix fish streams 2015-08-30 14:05:24 +02:00
Junegunn Choi
9a2d9ad947 0.10.4 2015-08-29 02:36:27 +09:00
Junegunn Choi
90b0cd44ac Should not strip ANSI codes when --ansi is not set 2015-08-28 21:23:10 +09:00
Junegunn Choi
698e8008df [vim] Dynamic height specification for 'up' and 'down' options
Values for 'up' and 'down' can be written with ~ prefix. Only applies
when the source is a Vim list.

    e.g. { 'source': range(10), 'down': '~40%' }
2015-08-28 18:38:47 +09:00
Junegunn Choi
1de4cc3ba8 [install] Fall back statically-linked binary on 64-bit linux
Close #322
2015-08-27 22:50:59 +09:00
Junegunn Choi
0d66ad23c6 Fix build script 2015-08-27 22:48:42 +09:00
Junegunn Choi
7f7741099b make linux-static (#322) 2015-08-27 03:28:05 +09:00
Junegunn Choi
5a72dc6922 Fix #329 - Trim ANSI codes from output when --ansi & --with-nth are set 2015-08-26 23:58:18 +09:00
Junegunn Choi
80ed02e72e Add failing test case for #329 2015-08-26 23:35:31 +09:00
Junegunn Choi
8fb31e1b4d [vim] Escape % and # when running source command with :! 2015-08-24 01:52:16 +09:00
Junegunn Choi
148f21415a Mention fzf.vim project 2015-08-22 19:33:04 +09:00
Junegunn Choi
1c31e07d34 [install] Improve error message 2015-08-19 19:42:06 +09:00
Junegunn Choi
55d566b72f Revert "[vim] Open silently"
This reverts commit c601fc6437.
2015-08-18 12:03:08 +09:00
Junegunn Choi
60336c7423 Remove Vim examples from README.md 2015-08-16 02:47:52 +09:00
Junegunn Choi
7ae877bd3a [vim] Handle single/double quote characters in 'dir' option 2015-08-16 00:04:45 +09:00
Junegunn Choi
c601fc6437 [vim] Open silently 2015-08-15 23:53:27 +09:00
Junegunn Choi
e5fec408c4 [vim] tab split instead of tabedit 2015-08-15 23:53:11 +09:00
Junegunn Choi
8156e9894e 0.10.3 2015-08-12 02:09:46 +09:00
Junegunn Choi
cacc212f12 [install] Prerelease of 0.10.3 2015-08-11 00:21:09 +09:00
Junegunn Choi
d0f2c00f9f Fix --with-nth performance; use simpler regular expression
Related #317
2015-08-11 00:15:41 +09:00
Junegunn Choi
766427de0c Fix --with-nth performance; avoid regex if possible
Close #317
2015-08-10 18:34:20 +09:00
Junegunn Choi
a7b75c99a5 [install] Stop installer when failed to download the binary
Close #312
2015-08-08 03:53:46 +09:00
Junegunn Choi
bae10a6582 [install] Add an extra new line character
so that it doesn't corrupt file that doesn't end with a new line
character. Close #311.
2015-08-05 23:50:38 +09:00
Junegunn Choi
c4cf90a3d2 0.10.2 2015-08-03 00:21:21 +09:00
Junegunn Choi
15c49a3e08 Fix race condition 2015-08-03 00:14:34 +09:00
Junegunn Choi
ae87f6548a GoLint 2015-08-02 23:54:53 +09:00
Junegunn Choi
7833fa7396 [install] Always download binary when --pre is set 2015-08-02 15:09:57 +09:00
Junegunn Choi
9278f3acd2 [install] Add --pre option for downloading prerelease binary 2015-08-02 15:02:12 +09:00
Junegunn Choi
e83ae34a3b Update CHANGELOG - 0.10.2 2015-08-02 14:32:34 +09:00
Junegunn Choi
e13bafc1ab Performance fix - unnecessary rune convertion on --ansi
> time cat /tmp/list | fzf-0.10.1-darwin_amd64 --ansi -fqwerty > /dev/null

    real    0m4.364s
    user    0m8.231s
    sys     0m0.820s

    > time cat /tmp/list | fzf --ansi -fqwerty > /dev/null

    real    0m4.624s
    user    0m5.755s
    sys     0m0.732s
2015-08-02 14:25:57 +09:00
Junegunn Choi
0ea66329b8 Performance tuning - eager rune array conversion
> wc -l /tmp/list2
     2594098 /tmp/list2

    > time cat /tmp/list2 | fzf-0.10.1-darwin_amd64 -fqwerty > /dev/null

    real    0m5.418s
    user    0m10.990s
    sys     0m1.302s

    > time cat /tmp/list2 | fzf-head -fqwerty > /dev/null

    real    0m4.862s
    user    0m6.619s
    sys     0m0.982s
2015-08-02 14:00:18 +09:00
Junegunn Choi
634670e3ea Lint 2015-08-02 13:11:59 +09:00
Junegunn Choi
dea60b11bc Only consider the lengths of the relevant parts when --nth is set 2015-08-01 23:13:24 +09:00
Junegunn Choi
5e90f0a57b Fix default command so that it doesn't fail on dash-prefixed files
Close #310
2015-08-01 21:51:10 +09:00
Junegunn Choi
0b4542fcdf [vim] Temporarily disable &autochdir when opening files (#306) 2015-07-29 17:55:58 +09:00
Junegunn Choi
02bd2d2adf Do not proceed if $TERM is invalid
Related #305
2015-07-28 14:35:46 +09:00
Junegunn Choi
dce6fe6f2d [fzf-tmux] Ensure that the same $TERM value is used in split
Fix #305. ncurses can crash on invalid $TERM. fzf-tmux uses bash on
a new pane so we have to make sure that the $TERM is consistent with
that of the hosting shell.
2015-07-28 14:17:25 +09:00
Junegunn Choi
fcae99f09b No need to "tmux list-panes" when obviously not on tmux (#303) 2015-07-28 00:56:03 +09:00
Junegunn Choi
fb1b026d3d Always check if the pane is zoomed
Close #303
2015-07-28 00:30:17 +09:00
Junegunn Choi
9f953fc944 Do not use tmux pane if the current pane is zoomed
Close #303
2015-07-28 00:22:04 +09:00
Junegunn Choi
909ea1a698 0.10.1 2015-07-27 00:09:07 +09:00
Junegunn Choi
7231acd442 Fix mouse scroll when --margin is set 2015-07-27 00:06:44 +09:00
Junegunn Choi
7814371a9a Revert "0.10.1"
This reverts commit 6166e2dd80.
2015-07-27 00:03:14 +09:00
Junegunn Choi
6166e2dd80 0.10.1 2015-07-26 23:57:26 +09:00
Junegunn Choi
ee0c8a2635 Add --margin option
Close #299
2015-07-26 23:02:04 +09:00
Junegunn Choi
2bebddefc0 Do not print the entire --help on invalid option 2015-07-26 13:39:34 +09:00
Junegunn Choi
fdbf3d3fec Replace eof action with cancel (#289) 2015-07-23 21:05:33 +09:00
Junegunn Choi
f9136cffe6 Update man page 2015-07-23 10:45:01 +09:00
Junegunn Choi
51d84b1869 [bash] Update fzf option completion 2015-07-23 00:58:20 +09:00
Junegunn Choi
13e040baee Bind CTRL-D to the new delete-char/eof action
- CTRL-D - delete-char/eof
- DEL - delete-char
2015-07-23 00:56:03 +09:00
Junegunn Choi
cc0d5539ba Add "eof" action which closes the finder only when input is empty
Close #289
2015-07-22 22:57:48 +09:00
Junegunn Choi
b53f61fc59 Remove cbreak before raw 2015-07-22 22:36:39 +09:00
Junegunn Choi
4e0e03403e Fix --header-lines unaffected by --with-nth 2015-07-22 21:24:02 +09:00
Junegunn Choi
928fccc15b Fix header not shown when the lines go beyond the screen limit 2015-07-22 21:22:59 +09:00
Junegunn Choi
bbaa3ab8bd Update CHANGELOG 2015-07-22 14:19:55 +09:00
Junegunn Choi
5e3cb3a4ea Fix ANSI processor to handle multi-line regions 2015-07-22 14:19:45 +09:00
Junegunn Choi
f71ea5f3ea Add test cases for header and fix corner cases 2015-07-22 13:45:38 +09:00
Junegunn Choi
f469c25730 Add --header-lines option 2015-07-22 03:21:20 +09:00
Junegunn Choi
18469b6954 Adjust header color for dark color scheme 2015-07-22 03:07:27 +09:00
Junegunn Choi
d01db4862b Update documentation 2015-07-22 01:12:50 +09:00
Junegunn Choi
8b2adba8d6 Redraw of header on resize 2015-07-22 00:47:14 +09:00
Junegunn Choi
d459e9abce Add --header-file option 2015-07-22 00:38:38 +09:00
Junegunn Choi
c9abe1b1ff Show more specific error message on invalid binding 2015-07-18 02:31:35 +09:00
Junegunn Choi
a0e6147bb5 Fix #292 - Allow binding of colon and comma 2015-07-16 21:14:08 +09:00
Junegunn Choi
b0f491d3c3 Fix travis CI build
- Fix test failures on new fish 2.2.0
- Make timeout-based test cases more robust
2015-07-13 19:24:22 +09:00
Junegunn Choi
392da53f53 [bash] Make CTRL-R work when histexpand is unset (#286)
Note that it still can't handle properly multi-line commands.
Thanks to @jpcirrus for the bug report and the fix.
2015-07-13 00:22:13 +09:00
Junegunn Choi
ae72b0fb70 Merge pull request #285 from evverx/possible-retry-loop
[bash-completion] Fix g++: possible retry loop
2015-07-04 11:24:08 +09:00
Evgeny Vereshchagin
a79d080ea8 Fix g++: possible retry loop
See http://unix.stackexchange.com/q/213432/120177
2015-07-04 01:20:36 +00:00
Junegunn Choi
ec85fd552d Update README - how to use ag with CTRL-T 2015-06-30 13:17:48 +09:00
Junegunn Choi
11db046fc7 [neovim] Fix #281 - Properly close window with winnr 1 2015-06-27 14:23:51 +09:00
Junegunn Choi
938151a834 [shell] Add FZF_CTRL_T_COMMAND for CTRL-T
Close #40
2015-06-26 01:02:44 +09:00
Junegunn Choi
14e3b84073 [zsh] No need to define __fsel in non-interactive shell
Since we now use fzf-tmux instead of tmux split-window
2015-06-26 00:14:36 +09:00
Junegunn Choi
56100f0fa7 [bash] Use command \find for ALT-C
ALT-C can fail with the following aliases as pointed out in #272

    alias find='noglob find'
    alias command='command '
2015-06-25 23:54:05 +09:00
Junegunn Choi
5254ee2e2a Update documentation (#277) 2015-06-22 01:35:36 +09:00
Junegunn Choi
355d004895 [neovim] Fix error with {'window': 'enew'} (#274) 2015-06-21 21:45:10 +09:00
Junegunn Choi
a336494f5d 0.10.0 2015-06-21 17:40:36 +09:00
Junegunn Choi
8270f7f0ca Rename --null to --read0 and undocument the option
`--null` is ambiguous. For completeness' sake, we need both `--read0`
and `--print0`.

`--read0` only makes sense when the input contains multiline entries.
However, fzf currently cannot correctly display multiline entries,
I'm going to make `--read0` an undocumented feature.
2015-06-21 17:29:58 +09:00
Junegunn Choi
638a956a9e Merge pull request #272 from okapia/zsh-simplify
Use vi-fetch-history on zsh to get history line
2015-06-21 16:57:14 +09:00
Oliver Kiddle
d395ebd28f use vi-fetch-history on zsh to get history line
In addition to being simpler, it allows subsequent up/down history
or accept-line-and-down-history widgets to work.
Also allow for find being and alias if alias expansion
after command is enabled.
2015-06-21 09:21:35 +02:00
Junegunn Choi
c0d3faa84f Hide --toggle-sort from --help output
Since the same can be now achieved with --bind KEY:toggle-sort
2015-06-19 01:06:56 +09:00
Junegunn Choi
3492c8b780 Rename --history-max to --history-size
Considering HISTSIZE and HISTFILESIZE of bash
2015-06-19 01:03:25 +09:00
Junegunn Choi
a8b2c257cd Improve handling of key names
Remember the exact string given as the key name so that it's possible to
correctly handle synonyms and print the original string.
2015-06-19 00:31:48 +09:00
Junegunn Choi
5e8d8dab82 More key names for --bind 2015-06-18 02:27:50 +09:00
Junegunn Choi
b504c6eb39 Avoid intermittent test failures
by making sure that we're back on shell command-line
2015-06-18 02:09:44 +09:00
Junegunn Choi
d261c36cde Keep the spinner spinning even when the source stream is idle 2015-06-18 00:42:38 +09:00
Junegunn Choi
fe4e452d68 Add --cycle option for cyclic scrolling
Close #266
2015-06-16 23:16:34 +09:00
Junegunn Choi
d54a4fa223 Add key name "bspace" for --bind (bspace != ctrl-h) 2015-06-16 02:18:49 +09:00
Junegunn Choi
45bd323cab Allow binding CTRL-G and CTRL-Q 2015-06-16 02:17:06 +09:00
Junegunn Choi
8677dbded1 Change alternative notation for execute action (#265)
e.g. fzf --bind "ctrl-m:execute:COMMAND..." --bind ctrl-j:accept
2015-06-15 23:27:05 +09:00
Junegunn Choi
794ad5785d Fix . to match newlines as well (#265) 2015-06-15 23:11:22 +09:00
Junegunn Choi
fa5b58968e Add alternative execute notation that does not require closing char
This can be used to avoid parse errors that can happen when the command
contains the closing character. Since the command does not finish at
a certain character, the key binding should be the last one in the
group. Suggested by @tiziano88. (#265)

  e.g. fzf --bind "ctrl-m:execute=COMMAND..." --bind ctrl-j:accept
2015-06-15 23:00:38 +09:00
Junegunn Choi
e720f56ea8 Fix test code for docker build 2015-06-15 22:45:31 +09:00
Junegunn Choi
7db53e6459 Add synonyms for some keys to be used with --bind and --toggle-sort
enter (return), space, tab, btab, esc, up, down, left, right
2015-06-15 01:26:18 +09:00
Junegunn Choi
e287bd7f04 Fix Travis CI build 2015-06-14 23:44:42 +09:00
Junegunn Choi
022435a90a More alternative notations for execute action
execute(...)
    execute[...]
    execute~...~
    execute!...!
    execute@...@
    execute#...#
    execute$...$
    execute%...%
    execute^...^
    execute&...&
    execute*...*
    execute:...:
    execute;...;
    execute/.../
    execute|...|
2015-06-14 23:36:49 +09:00
Junegunn Choi
6c99cc1700 Add bind action for executing arbitrary command (#265)
e.g. fzf --bind "ctrl-m:execute(less {})"
     fzf --bind "ctrl-t:execute[tmux new-window -d 'vim {}']"
2015-06-14 12:25:08 +09:00
Junegunn Choi
fe5b190a7d Remove unnecessary regexp matches
This change does have positive effect on startup time of fzf when many
number of options are provided.

    time fzf --query=____ --filter=____ --delimiter=q --prompt=________ \
    --nth=1,2,3,4 --with-nth=1,2,3,4 --toggle-sort=ctrl-r \
    --expect=ctrl-x --tiebreak=index --color=light --bind=ctrl-t:accept \
    --history=/tmp/xxx --history-max=1000 --help

    0m0.013s -> 0m0.008s
2015-06-14 11:23:07 +09:00
Junegunn Choi
77bab51696 GoLint fix 2015-06-14 03:19:18 +09:00
Junegunn Choi
77048f3e3b Fix Travis CI build 2015-06-14 02:51:45 +09:00
Junegunn Choi
8b618f7439 Test refactoring 2015-06-14 02:44:22 +09:00
Junegunn Choi
8973207bb4 Fix Travis CI build 2015-06-14 02:13:02 +09:00
Junegunn Choi
6ad1736832 Fix ignore action 2015-06-14 02:11:27 +09:00
Junegunn Choi
9fca611c4a Add ignore action for --bind 2015-06-14 01:54:56 +09:00
Junegunn Choi
8e7164553f Add missing files from the previous commit
:(
2015-06-14 00:53:45 +09:00
Junegunn Choi
3b52811796 Add support for search history
- Add `--history` option (e.g. fzf --history ~/.fzf.history)
- Add `--history-max` option for limiting the size of the file (default 1000)
- Add `previous-history` and `next-history` actions for `--bind`
    - CTRL-P and CTRL-N are automatically remapped to these actions when
      `--history` is used

Closes #249, #251
2015-06-14 00:48:48 +09:00
Junegunn Choi
2e84b1db64 Merge pull request #264 from kassio/master
Do not rename terminal buffer
2015-06-14 00:11:10 +09:00
Kassio Borges
9f33068ab3 Avoid conflict with other neoterm plugins.
To avoid conflict with other neoterm plugins that manage terminals,
prefer named terminals.
2015-06-13 11:13:33 -03:00
Junegunn Choi
eaa3c67a5e Add actions for --bind: select-all / deselect-all / toggle-all
Close #257
2015-06-09 23:44:54 +09:00
Junegunn Choi
1b9b1d15bc Adjust --help output 2015-06-08 23:28:41 +09:00
Junegunn Choi
97f433a274 Merge branch 'dullgiulio-121-accept-nil-input' 2015-06-08 23:28:06 +09:00
Junegunn Choi
45a3655eaf Add test case for --null option 2015-06-08 23:27:50 +09:00
Junegunn Choi
81ffde92fb Merge branch '121-accept-nil-input' of https://github.com/dullgiulio/fzf into dullgiulio-121-accept-nil-input 2015-06-08 23:21:16 +09:00
Junegunn Choi
0be4cead20 Allow ^EqualMatch$ 2015-06-08 23:17:24 +09:00
Giulio Iotti
f6dd32046e add support to nil-byte separated input strings, closes #121 2015-06-08 08:38:40 +00:00
Junegunn Choi
443a80f254 Always use the same color for multi-select markers 2015-06-07 23:32:07 +09:00
Junegunn Choi
8017635a71 Merge pull request #252 from dominikh/portable-swapOutput
Use ncurses's newterm instead of swapping stdout and stderr
2015-06-07 14:31:44 +09:00
Dominik Honnef
98f62b191a Use ncurses's newterm instead of swapping stdout and stderr 2015-06-07 07:26:26 +02:00
Junegunn Choi
52771a6226 0.9.13 2015-06-03 02:09:07 +09:00
Junegunn Choi
b00bcf506e Fix #248 - Premature termination of Reader on long input 2015-06-03 01:48:02 +09:00
Junegunn Choi
fdbfe36c0b Color customization (#245) 2015-06-03 01:46:03 +09:00
Junegunn Choi
446e822723 Update CHANGELOG 2015-05-22 02:37:38 +09:00
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
42 changed files with 3483 additions and 1177 deletions

View File

@@ -1,6 +1,147 @@
CHANGELOG CHANGELOG
========= =========
0.10.5
------
- `'`-prefix to unquote the term in `--extended-exact` mode
- Backward scan when `--tiebreak=end` is set
0.10.4
------
- Fixed to remove ANSI code from output when `--with-nth` is set
0.10.3
------
- Fixed slow performance of `--with-nth` when used with `--delimiter`
- Regular expression engine of Golang as of now is very slow, so the fixed
version will treat the given delimiter pattern as a plain string instead
of a regular expression unless it contains special characters and is
a valid regular expression.
- Simpler regular expression for delimiter for better performance
0.10.2
------
### Fixes and improvements
- Improvement in perceived response time of queries
- Eager, efficient rune array conversion
- Graceful exit when failed to initialize ncurses (invalid $TERM)
- Improved ranking algorithm when `--nth` option is set
- Changed the default command not to fail when there are files whose names
start with dash
0.10.1
------
### New features
- Added `--margin` option
- Added options for sticky header
- `--header-file`
- `--header-lines`
- Added `cancel` action which clears the input or closes the finder when the
input is already empty
- e.g. `export FZF_DEFAULT_OPTS="--bind esc:cancel"`
- Added `delete-char/eof` action to differentiate `CTRL-D` and `DEL`
### Minor improvements/fixes
- Fixed to allow binding colon and comma keys
- Fixed ANSI processor to handle color regions spanning multiple lines
0.10.0
------
### New features
- More actions for `--bind`
- `select-all`
- `deselect-all`
- `toggle-all`
- `ignore`
- `execute(...)` action for running arbitrary command without leaving fzf
- `fzf --bind "ctrl-m:execute(less {})"`
- `fzf --bind "ctrl-t:execute(tmux new-window -d 'vim {}')"`
- If the command contains parentheses, use any of the follows alternative
notations to avoid parse errors
- `execute[...]`
- `execute~...~`
- `execute!...!`
- `execute@...@`
- `execute#...#`
- `execute$...$`
- `execute%...%`
- `execute^...^`
- `execute&...&`
- `execute*...*`
- `execute;...;`
- `execute/.../`
- `execute|...|`
- `execute:...`
- This is the special form that frees you from parse errors as it
does not expect the closing character
- The catch is that it should be the last one in the
comma-separated list
- Added support for optional search history
- `--history HISTORY_FILE`
- When used, `CTRL-N` and `CTRL-P` are automatically remapped to
`next-history` and `previous-history`
- `--history-size MAX_ENTRIES` (default: 1000)
- Cyclic scrolling can be enabled with `--cycle`
- Fixed the bug where the spinner was not spinning on idle input stream
- e.g. `sleep 100 | fzf`
### Minor improvements/fixes
- Added synonyms for key names that can be specified for `--bind`,
`--toggle-sort`, and `--expect`
- Fixed the color of multi-select marker on the current line
- Fixed to allow `^pattern$` in extended-search mode
0.9.13
------
### New features
- Color customization with the extended `--color` option
### Bug fixes
- Fixed premature termination of Reader in the presence of a long line which
is longer than 64KB
0.9.12
------
### New features
- Added `--bind` option for custom key bindings
### Bug fixes
- Fixed to update "inline-info" immediately after terminal resize
- Fixed ANSI code offset calculation
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 0.9.10
------ ------

196
README.md
View File

@@ -8,7 +8,7 @@ fzf is a general-purpose command-line fuzzy finder.
Pros Pros
---- ----
- No dependency - No dependencies
- Blazingly fast - Blazingly fast
- e.g. `locate / | fzf` - e.g. `locate / | fzf`
- Flexible layout - Flexible layout
@@ -27,7 +27,7 @@ fzf project consists of the followings:
- `fzf-tmux` script for launching fzf in a tmux pane - `fzf-tmux` script for launching fzf in a tmux pane
- Shell extensions - Shell extensions
- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish) - Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
- Fuzzy auto-completion (bash only) - Fuzzy auto-completion (bash, zsh)
- Vim/Neovim plugin - Vim/Neovim plugin
You can [download fzf executable][bin] alone, but it's recommended that you You can [download fzf executable][bin] alone, but it's recommended that you
@@ -45,17 +45,6 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install ~/.fzf/install
``` ```
#### Using curl
In case you don't have git installed:
```sh
mkdir -p ~/.fzf
curl -L https://github.com/junegunn/fzf/archive/master.tar.gz |
tar xz --strip-components 1 -C ~/.fzf
~/.fzf/install
```
#### Using Homebrew #### Using Homebrew
On OS X, you can use [Homebrew](http://brew.sh/) to install fzf. On OS X, you can use [Homebrew](http://brew.sh/) to install fzf.
@@ -135,8 +124,16 @@ such as: `^music .mp3$ sbtrkt !rmx`
| `'wild` | Items that include `wild` | exact-match (quoted) | | `'wild` | Items that include `wild` | exact-match (quoted) |
| `!'fire` | Items that do not include `fire` | inverse-exact-match | | `!'fire` | Items that do not include `fire` | inverse-exact-match |
If you don't need fuzzy matching and do not wish to "quote" every word, start If you don't prefer fuzzy matching and do not wish to "quote" every word,
fzf with `-e` or `--extended-exact` option. start fzf with `-e` or `--extended-exact` option. Note that in
`--extended-exact` mode, `'`-prefix "unquotes" the term.
#### Environment variables
- `FZF_DEFAULT_COMMAND`
- Default command to use when input is tty
- `FZF_DEFAULT_OPTS`
- Default options. e.g. `export FZF_DEFAULT_OPTS="--extended --cycle"`
Examples Examples
-------- --------
@@ -151,24 +148,21 @@ Key bindings for command line
The install script will setup the following key bindings for bash, zsh, and The install script will setup the following key bindings for bash, zsh, and
fish. fish.
- `CTRL-T` - Paste the selected file path(s) into the command line - `CTRL-T` - Paste the selected files and directories onto the command line
- `CTRL-R` - Paste the selected command from history into the command line - Set `FZF_CTRL_T_COMMAND` to override the default command
- `CTRL-R` - Paste the selected command from history onto the command line
- Sort is disabled by default to respect chronological ordering - Sort is disabled by default to respect chronological ordering
- Press `CTRL-R` again to toggle sort - Press `CTRL-R` again to toggle sort
- `ALT-C` - cd into the selected directory - `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 If you're on a tmux session, fzf will start in a split pane. You may disable
may disable this tmux integration by setting `FZF_TMUX` to 0, or change the this tmux integration by setting `FZF_TMUX` to 0, or change the height of the
height of the window with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`). 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 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 ~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi
mode. 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 `fzf-tmux` script
----------------- -----------------
@@ -188,8 +182,8 @@ 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]` 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. options, so you can invariably use `fzf-tmux` in your scripts.
Fuzzy completion for bash Fuzzy completion for bash and zsh
------------------------- ---------------------------------
#### Files and directories #### Files and directories
@@ -261,6 +255,9 @@ export FZF_COMPLETION_OPTS='+c -x'
Usage as Vim plugin Usage as Vim plugin
------------------- -------------------
This repository only enables basic integration with Vim. If you're looking for
more, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project.
(Note: To use fzf in GVim, an external terminal emulator is required.) (Note: To use fzf in GVim, an external terminal emulator is required.)
#### `:FZF[!]` #### `:FZF[!]`
@@ -286,17 +283,15 @@ Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key,
in new tabs, in horizontal splits, or in vertical splits respectively. in new tabs, in horizontal splits, or in vertical splits respectively.
Note that the environment variables `FZF_DEFAULT_COMMAND` and Note that the environment variables `FZF_DEFAULT_COMMAND` and
`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][vim-examples] for `FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for
customization. customization.
[vim-examples]: https://github.com/junegunn/fzf/wiki/Examples-(vim) [fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim)
#### `fzf#run([options])` #### `fzf#run([options])`
For more advanced uses, you can call `fzf#run()` function which returns the list For more advanced uses, you can use `fzf#run()` function with the following
of the selected items. options.
`fzf#run()` may take an options-dictionary:
| Option name | Type | Description | | Option name | Type | Description |
| -------------------------- | ------------- | ---------------------------------------------------------------- | | -------------------------- | ------------- | ---------------------------------------------------------------- |
@@ -309,73 +304,12 @@ of the selected items.
| `dir` | string | Working directory | | `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) | | `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`) | | `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `launcher` | string | External terminal emulator to start fzf with (Only used in GVim) | | `launcher` | string | External terminal emulator to start fzf with (GVim only) |
| `launcher` | funcref | Function for generating `launcher` string (GVim only) |
_However on Neovim `fzf#run` is asynchronous and does not return values so you Examples can be found on [the wiki
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.
```vim
let items = fzf#run({ 'options': '-m +c', 'dir': '~', 'source': 'ls' })
```
But if `sink` is given as a string, the command will be executed for each
selected item.
```vim
" Each selected item will be opened in a new tab
let items = fzf#run({ 'sink': 'tabe', 'options': '-m +c', 'dir': '~', 'source': 'ls' })
```
We can also use a Vim list as the source as follows:
```vim
" Choose a color scheme with fzf
nnoremap <silent> <Leader>C :call fzf#run({
\ 'source':
\ map(split(globpath(&rtp, "colors/*.vim"), "\n"),
\ "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"),
\ 'sink': 'colo',
\ 'options': '+m',
\ 'left': 20,
\ 'launcher': 'xterm -geometry 20x30 -e bash -ic %s'
\ })<CR>
```
`sink` option can be a function reference. The following example creates a
handy mapping that selects an open buffer.
```vim
" List of buffers
function! s:buflist()
redir => ls
silent ls
redir END
return split(ls, '\n')
endfunction
function! s:bufopen(e)
execute 'buffer' matchstr(a:e, '^[ 0-9]*')
endfunction
nnoremap <silent> <Leader><Enter> :call fzf#run({
\ 'source': reverse(<sid>buflist()),
\ 'sink': function('<sid>bufopen'),
\ 'options': '+m',
\ 'down': len(<sid>buflist()) + 2
\ })<CR>
```
More examples can be found on [the wiki
page](https://github.com/junegunn/fzf/wiki/Examples-(vim)). page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
#### Articles
- [fzf+vim+tmux](http://junegunn.kr/2014/04/fzf+vim+tmux)
Tips Tips
---- ----
@@ -384,13 +318,13 @@ Tips
If you have any rendering issues, check the followings: If you have any rendering issues, check the followings:
1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it 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 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 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` background color and make some text unable to read. In that case, try
option. And if it solves your problem, I recommend including it in `--black` option. And if it solves your problem, I recommend including it
`FZF_DEFAULT_OPTS` for further convenience. in `FZF_DEFAULT_OPTS` for further convenience.
4. If you still have problem, try `--no-256` option or even `--no-color`. 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`
@@ -408,6 +342,9 @@ export FZF_DEFAULT_COMMAND='ag -l -g ""'
# Now fzf (w/o pipe) will use ag instead of find # Now fzf (w/o pipe) will use ag instead of find
fzf fzf
# To apply the command to CTRL-T as well
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
``` ```
#### `git ls-tree` for fast traversal #### `git ls-tree` for fast traversal
@@ -421,41 +358,6 @@ export FZF_DEFAULT_COMMAND='
find * -name ".*" -prune -o -type f -print -o -type l -print) 2> /dev/null' find * -name ".*" -prune -o -type f -print -o -type l -print) 2> /dev/null'
``` ```
#### Using fzf with tmux panes
The supplied [fzf-tmux](bin/fzf-tmux) script should suffice in most of the
cases, but if you want to be able to update command line like the default
`CTRL-T` key binding, you'll have to use `send-keys` command of tmux. The
following example will show you how it can be done.
```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) It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362)
@@ -464,27 +366,7 @@ simple `vim (fzf)` won't work as expected. The workaround is to store the result
of fzf to a temporary file. of fzf to a temporary file.
```sh ```sh
function vimf fzf > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result)
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
```
#### Handling UTF-8 NFD paths on OSX
Use iconv to convert NFD paths to NFC:
```sh
find . | iconv -f utf-8-mac -t utf8//ignore | fzf
``` ```
License License

View File

@@ -82,23 +82,21 @@ while [ $# -gt 0 ]; do
shift shift
done done
if [ -z "$TMUX_PANE" ]; then if [ -z "$TMUX_PANE" ] || tmux list-panes -F '#F' | grep -q Z; then
fzf "${args[@]}" fzf "${args[@]}"
exit $? exit $?
fi fi
set -e set -e
# Build arguments to fzf
[ ${#args[@]} -gt 0 ] && fzf_args=$(printf '\\"%s\\" ' "${args[@]}"; echo '')
# Clean up named pipes on exit # Clean up named pipes on exit
id=$RANDOM id=$RANDOM
argsf=/tmp/fzf-args-$id
fifo1=/tmp/fzf-fifo1-$id fifo1=/tmp/fzf-fifo1-$id
fifo2=/tmp/fzf-fifo2-$id fifo2=/tmp/fzf-fifo2-$id
fifo3=/tmp/fzf-fifo3-$id fifo3=/tmp/fzf-fifo3-$id
cleanup() { cleanup() {
rm -f $fifo1 $fifo2 $fifo3 rm -f $argsf $fifo1 $fifo2 $fifo3
} }
trap cleanup EXIT SIGINT SIGTERM trap cleanup EXIT SIGINT SIGTERM
@@ -109,19 +107,30 @@ fail() {
fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
[ -x "$fzf" ] || fail "fzf executable not found" [ -x "$fzf" ] || fail "fzf executable not found"
envs="" envs="env TERM=$TERM "
[ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
[ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" [ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
mkfifo $fifo2 mkfifo $fifo2
mkfifo $fifo3 mkfifo $fifo3
# Build arguments to fzf
opts=""
for arg in "${args[@]}"; do
arg="${arg//\"/\\\"}"
arg="${arg//\`/\\\`}"
opts="$opts \"$arg\""
done
if [ -n "$term" -o -t 0 ]; then if [ -n "$term" -o -t 0 ]; then
cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf
tmux set-window-option -q synchronize-panes off \;\ tmux set-window-option -q synchronize-panes off \;\
split-window $opt "cd $(printf %q "$PWD");$envs"' sh -c "'$fzf' '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap
else else
mkfifo $fifo1 mkfifo $fifo1
cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf
tmux set-window-option -q synchronize-panes off \;\ tmux set-window-option -q synchronize-panes off \;\
split-window $opt "$envs"' sh -c "'$fzf' '"$fzf_args"' < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap split-window $opt "$envs bash $argsf" $swap
cat <&0 > $fifo1 & cat <&0 > $fifo1 &
fi fi
cat $fifo2 cat $fifo2

5
fzf
View File

@@ -206,10 +206,11 @@ class FZF
@expect = true @expect = true
when /^--expect=(.*)$/ when /^--expect=(.*)$/
@expect = true @expect = true
when '--toggle-sort', '--tiebreak', '--color' when '--toggle-sort', '--tiebreak', '--color', '--bind', '--history', '--history-size'
argv.shift argv.shift
when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll', when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll',
/^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/ '--inline-info', '--no-inline-info', '--read0', '--cycle', /^--bind=(.*)$/,
/^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/, /^--history(-max)?=(.*)$/
# XXX # XXX
else else
usage 1, "illegal option: #{o}" usage 1, "illegal option: #{o}"

47
install
View File

@@ -1,6 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
version=0.9.10 [[ "$@" =~ --pre ]] && version=0.10.5 pre=1 ||
version=0.10.5 pre=0
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base=$(pwd)
@@ -20,16 +21,21 @@ ask() {
check_binary() { check_binary() {
echo -n " - Checking fzf executable ... " echo -n " - Checking fzf executable ... "
local output=$("$fzf_base"/bin/fzf --version 2>&1) local output
if [ "$version" = "$output" ]; then output=$("$fzf_base"/bin/fzf --version 2>&1)
if [ $? -ne 0 ]; then
echo "Error: $output"
binary_error="Invalid binary"
elif [ "$version" != "$output" ]; then
echo "$output != $version"
binary_error="Invalid version"
else
echo "$output" echo "$output"
binary_error="" binary_error=""
else return 0
echo "$output != $version"
rm -f "$fzf_base"/bin/fzf
binary_error="Invalid binary"
return 1
fi fi
rm -f "$fzf_base"/bin/fzf
return 1
} }
symlink() { symlink() {
@@ -45,11 +51,13 @@ symlink() {
download() { download() {
echo "Downloading bin/fzf ..." echo "Downloading bin/fzf ..."
if [[ ! $1 =~ dev && -x "$fzf_base"/bin/fzf ]]; then if [ $pre = 0 ]; then
echo " - Already exists" if [ -x "$fzf_base"/bin/fzf ]; then
check_binary && return echo " - Already exists"
elif [ -x "$fzf_base"/bin/$1 ]; then check_binary && return
symlink $1 && check_binary && return elif [ -x "$fzf_base"/bin/$1 ]; then
symlink $1 && check_binary && return
fi
fi fi
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@@ -72,7 +80,12 @@ download() {
return return
fi fi
chmod +x $1 && symlink $1 && check_binary chmod +x $1 && symlink $1 || return 1
if [[ $1 =~ linux_amd64$ ]]; then
check_binary || download $1-static
else
check_binary
fi
} }
# Try to download binary executable # Try to download binary executable
@@ -93,6 +106,7 @@ if [ -n "$binary_error" ]; then
echo "No prebuilt binary for $archi ... " echo "No prebuilt binary for $archi ... "
else else
echo " - $binary_error !!!" echo " - $binary_error !!!"
exit 1
fi fi
echo "Installing legacy Ruby version ..." echo "Installing legacy Ruby version ..."
@@ -176,8 +190,8 @@ for shell in bash zsh; do
echo -n "Generate ~/.fzf.$shell ... " echo -n "Generate ~/.fzf.$shell ... "
src=~/.fzf.${shell} src=~/.fzf.${shell}
fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/shell/completion.${shell}\"" fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null"
if [ $shell != bash -o $auto_completion -ne 0 ]; then if [ $auto_completion -ne 0 ]; then
fzf_completion="# $fzf_completion" fzf_completion="# $fzf_completion"
fi fi
@@ -247,6 +261,7 @@ append_line() {
if [ -n "$line" ]; then if [ -n "$line" ]; then
echo " - Already exists: line #$line" echo " - Already exists: line #$line"
else else
echo >> "$2"
echo "$1" >> "$2" echo "$1" >> "$2"
echo " + Added" echo " + Added"
fi fi

View File

@@ -21,7 +21,7 @@ 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "April 2015" "fzf 0.9.10" "fzf - a command-line fuzzy finder" .TH fzf 1 "Sep 2015" "fzf 0.10.5" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -52,10 +52,10 @@ Comma-separated list of field index expressions for limiting search scope.
See \fBFIELD INDEX EXPRESSION\fR for details. See \fBFIELD INDEX EXPRESSION\fR for details.
.TP .TP
.BI "--with-nth=" "N[,..]" .BI "--with-nth=" "N[,..]"
Transform the item using the list of index expressions for search Transform each item using index expressions within finder
.TP .TP
.BI "-d, --delimiter=" "STR" .BI "-d, --delimiter=" "STR"
Field delimiter regex for \fI--nth\fR and \fI--with-nth\fR (default: AWK-style) Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style)
.SS Search result .SS Search result
.TP .TP
.B "+s, --no-sort" .B "+s, --no-sort"
@@ -91,21 +91,39 @@ Enable processing of ANSI color codes
.B "--no-mouse" .B "--no-mouse"
Disable mouse Disable mouse
.TP .TP
.B "--color=COL" .BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]"
Color scheme: [dark|light|16|bw] Color configuration. The name of the base color scheme is followed by custom
.br color mappings. Ansi color code of -1 denotes terminal default
(default: dark on 256-color terminal, otherwise 16) foreground/background color.
.br
.R "" .RS
.br e.g. \fBfzf --color=bg+:24\fR
.BR dark " Color scheme for dark 256-color terminal" \fBfzf --color=light,fg:232,bg:255,bg+:116,info:27\fR
.br .RE
.BR light " Color scheme for light 256-color terminal"
.br .RS
.BR 16 " Color scheme for 16-color terminal" .B BASE SCHEME:
.br (default: dark on 256-color terminal, otherwise 16)
.BR bw " No colors"
.br \fBdark \fRColor scheme for dark 256-color terminal
\fBlight \fRColor scheme for light 256-color terminal
\fB16 \fRColor scheme for 16-color terminal
\fBbw \fRNo colors
.B COLOR:
\fBfg \fRText
\fBbg \fRBackground
\fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line)
\fBbg+ \fRBackground (current line)
\fBhl+ \fRHighlighted substrings (current line)
\fBinfo \fRInfo
\fBprompt \fRPrompt
\fBpointer \fRPointer to the current line
\fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator
\fBheader \fRHeader
.RE
.TP .TP
.B "--black" .B "--black"
Use black background Use black background
@@ -113,11 +131,170 @@ Use black background
.B "--reverse" .B "--reverse"
Reverse orientation Reverse orientation
.TP .TP
.BI "--margin=" MARGIN
Comma-separated expression for margins around the finder.
.br
.R ""
.br
.RS
.BR TRBL " Same margin for top, right, bottom, and left"
.br
.BR TB,RL " Vertical, horizontal margin"
.br
.BR T,RL,B " Top, horizontal, bottom margin"
.br
.BR T,R,B,L " Top, right, bottom, left margin"
.br
.R ""
.br
Each part can be given in absolute number or in percentage relative to the
terminal size with \fB%\fR suffix.
.br
.R ""
.br
e.g. \fBfzf --margin 10%\fR
\fBfzf --margin 1,5%\fR
.RE
.TP
.B "--cycle"
Enable cyclic scroll
.TP
.B "--no-hscroll" .B "--no-hscroll"
Disable horizontal scroll Disable horizontal scroll
.TP .TP
.B "--inline-info"
Display finder info inline with the query
.TP
.BI "--prompt=" "STR" .BI "--prompt=" "STR"
Input prompt (default: '> ') Input prompt (default: '> ')
.TP
.BI "--toggle-sort=" "KEY"
Key to toggle sort. For the list of the allowed key names, see \fB--bind\fR.
.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 AVAILABLE KEYS:
\fIctrl-[a-z]\fR
\fIalt-[a-z]\fR
\fIf[1-4]\fR
\fIenter\fR (\fIreturn\fR)
\fIspace\fR
\fIbspace\fR (\fIbs\fR)
\fIalt-bspace\fR (\fIalt-bs\fR)
\fItab\fR
\fIbtab\fR (\fIshift-tab\fR)
\fIesc\fR
\fIdel\fR
\fIup\fR
\fIdown\fR
\fIleft\fR
\fIright\fR
\fIhome\fR
\fIend\fR
\fIpgup\fR (\fIpage-up\fR)
\fIpgdn\fR (\fIpage-down\fR)
\fIshift-left\fR
\fIshift-right\fR
or any single character
.RE
.RS
\fBACTION: DEFAULT BINDINGS:
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIctrl-m (enter)\fR
\fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR
\fBclear-screen\fR \fIctrl-l\fR
\fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR
\fBdeselect-all\fR
\fBdown\fR \fIctrl-j ctrl-n down\fR
\fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details)
\fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR
\fBkill-line\fR
\fBkill-word\fR \fIalt-d\fR
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBselect-all\fR
\fBtoggle\fR
\fBtoggle-all\fR
\fBtoggle-down\fR \fIctrl-i (tab)\fR
\fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR)
\fBtoggle-up\fR \fIbtab (shift-tab)\fR
\fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR
.RE
.RS
With \fBexecute(...)\fR action, you can execute arbitrary commands without
leaving fzf. For example, you can turn fzf into a simple file browser by
binding \fBenter\fR key to \fBless\fR command like follows.
.RS
\fBfzf --bind "enter:execute(less {})"\fR
.RE
\fB{}\fR is the placeholder for the double-quoted string of the current line.
If the command contains parentheses, you can use any of the following
alternative notations to avoid parse errors.
\fBexecute[...]\fR
\fBexecute~...~\fR
\fBexecute!...!\fR
\fBexecute@...@\fR
\fBexecute#...#\fR
\fBexecute$...$\fR
\fBexecute%...%\fR
\fBexecute^...^\fR
\fBexecute&...&\fR
\fBexecute*...*\fR
\fBexecute;...;\fR
\fBexecute/.../\fR
\fBexecute|...|\fR
\fBexecute:...\fR
.RS
This is the special form that frees you from parse errors as it does not expect
the closing character. The catch is that it should be the last one in the
comma-separated list.
.RE
.RE
.TP
.BI "--history=" "HISTORY_FILE"
Load search history from the specified file and update the file on completion.
When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to
\fBnext-history\fR and \fBprevious-history\fR.
.TP
.BI "--history-size=" "N"
Maximum number of entries in the history file (default: 1000). The file is
automatically truncated when the number of the lines exceeds the value.
.TP
.BI "--header-file=" "FILE"
The content of the file will be printed as the sticky header. The lines in the
file are displayed in order from top to bottom regardless of \fB--reverse\fR,
and are not affected by \fB--with-nth\fR. ANSI color codes are processed even
when \fB--ansi\fR is not set.
.TP
.BI "--header-lines=" "N"
The first N lines of the input are treated as the sticky header. When
\fB--with-nth\fR is set, the lines are transformed just like the other
lines that follow.
.SS Scripting .SS Scripting
.TP .TP
.BI "-q, --query=" "STR" .BI "-q, --query=" "STR"
@@ -137,20 +314,15 @@ fzf becomes a fuzzy-version of grep.
Print query as the first line Print query as the first line
.TP .TP
.BI "--expect=" "KEY[,..]" .BI "--expect=" "KEY[,..]"
Comma-separated list of keys (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR, Comma-separated list of keys that can be used to complete fzf in addition to
or any single character) that can be used to complete fzf in addition to the the default enter key. When this option is set, fzf will print the name of the
default enter key. When this option is set, fzf will print the name of the key key pressed as the first line of its output (or as the second line if
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 \fB--print-query\fR is also used). The line will be empty if fzf is completed
with the default enter key. with the default enter key.
.RS .RS
e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
.RE .RE
.TP .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
.B "--sync" .B "--sync"
Synchronous search for multi-staged filtering. If specified, fzf will launch Synchronous search for multi-staged filtering. If specified, fzf will launch
ncurses finder only after the input stream is complete. ncurses finder only after the input stream is complete.
@@ -164,7 +336,7 @@ e.g. \fBfzf --multi | fzf --sync\fR
Default command to use when input is tty Default command to use when input is tty
.TP .TP
.B FZF_DEFAULT_OPTS .B FZF_DEFAULT_OPTS
Default options. e.g. \fB--extended --ansi\fR Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR
.SH EXIT STATUS .SH EXIT STATUS
.BR 0 " Normal exit" .BR 0 " Normal exit"
@@ -174,7 +346,7 @@ Default options. e.g. \fB--extended --ansi\fR
.SH FIELD INDEX EXPRESSION .SH FIELD INDEX EXPRESSION
A field index expression can be a non-zero integer or a range 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 ([BEGIN]..[END]). \fB--nth\fR and \fB--with-nth\fR take a comma-separated list
of field index expressions. of field index expressions.
.SS Examples .SS Examples
@@ -197,34 +369,45 @@ of field index expressions.
.SH EXTENDED SEARCH MODE .SH EXTENDED SEARCH MODE
With \fI-x\fR or \fI--extended\fR option, fzf will start in "extended-search With \fB-x\fR or \fB--extended\fR option, fzf will start in "extended-search
mode". In this mode, you can specify multiple patterns delimited by spaces, mode". In this mode, you can specify multiple patterns delimited by spaces,
such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR
.SS Exact-match (quoted) .SS Exact-match (quoted)
A term that is prefixed by a single-quote character (') is interpreted as an A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as
"exact-match" (or "non-fuzzy") term. fzf will search for the exact occurrences an "exact-match" (or "non-fuzzy") term. fzf will search for the exact
of the string. occurrences of the string.
.SS Anchored-match .SS Anchored-match
A term can be prefixed by ^, or suffixed by $ to become an anchored-match term. A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an
Then fzf will search for the items that start with or end with the given anchored-match term. Then fzf will search for the items that start with or end
string. An anchored-match term is also an exact-match term. with the given string. An anchored-match term is also an exact-match term.
.SS Negation .SS Negation
If a term is prefixed by !, fzf will exclude the items that satisfy the term If a term is prefixed by \fB!\fR, fzf will exclude the items that satisfy the
from the result. term from the result.
.SS Extended-exact mode .SS Extended-exact mode
If you don't need fuzzy matching at all and do not wish to "quote" (prefixing If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with
with ') every word, start fzf with \fI-e\fR or \fI--extended-exact\fR option \fB'\fR) every word, start fzf with \fB-e\fR or \fB--extended-exact\fR option
(instead of \fI-x\fR or \fI--extended\fR). (instead of \fB-x\fR or \fB--extended\fR). Note that in \fB--extended-exact\fR
mode, \fB'\fR-prefix "unquotes" the term.
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)
.SH SEE ALSO .SH SEE ALSO
.B Project homepage:
.RS
.I https://github.com/junegunn/fzf .I https://github.com/junegunn/fzf
.RE
.br
.R ""
.br
.B Extra Vim plugin:
.RS
.I https://github.com/junegunn/fzf.vim
.RE
.SH LICENSE .SH LICENSE
MIT MIT

View File

@@ -22,11 +22,9 @@
" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
let s:default_height = '40%' let s:default_height = '40%'
let s:launcher = 'xterm -e bash -ic %s'
let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf' let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf'
let s:install = expand('<sfile>:h:h').'/install' let s:install = expand('<sfile>:h:h').'/install'
let s:installed = 0 let s:installed = 0
let s:fzf_rb = expand('<sfile>:h:h').'/fzf'
let s:fzf_tmux = expand('<sfile>:h:h').'/bin/fzf-tmux' let s:fzf_tmux = expand('<sfile>:h:h').'/bin/fzf-tmux'
let s:cpo_save = &cpo let s:cpo_save = &cpo
@@ -36,7 +34,12 @@ function! s:fzf_exec()
if !exists('s:exec') if !exists('s:exec')
if executable(s:fzf_go) if executable(s:fzf_go)
let s:exec = s:fzf_go let s:exec = s:fzf_go
elseif !s:installed && executable(s:install) 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 echohl WarningMsg
echo 'Downloading fzf binary. Please wait ...' echo 'Downloading fzf binary. Please wait ...'
echohl None echohl None
@@ -44,24 +47,15 @@ function! s:fzf_exec()
call system(s:install.' --bin') call system(s:install.' --bin')
return s:fzf_exec() return s:fzf_exec()
else else
let path = split(system('which fzf 2> /dev/null'), '\n') redraw
if !v:shell_error && !empty(path) throw 'fzf executable not found'
let s:exec = path[0]
elseif executable(s:fzf_rb)
let s:exec = s:fzf_rb
else
call system('type fzf')
if v:shell_error
throw 'fzf executable not found'
else
let s:exec = 'fzf'
endif
endif
endif endif
return s:exec
else
return s:exec
endif endif
return s:exec
endfunction
function! s:tmux_not_zoomed()
return system('tmux list-panes -F "#F"') !~# 'Z'
endfunction endfunction
function! s:tmux_enabled() function! s:tmux_enabled()
@@ -70,7 +64,7 @@ function! s:tmux_enabled()
endif endif
if exists('s:tmux') if exists('s:tmux')
return s:tmux return s:tmux && s:tmux_not_zoomed()
endif endif
let s:tmux = 0 let s:tmux = 0
@@ -78,7 +72,7 @@ function! s:tmux_enabled()
let output = system('tmux -V') let output = system('tmux -V')
let s:tmux = !v:shell_error && output >= 'tmux 1.7' let s:tmux = !v:shell_error && output >= 'tmux 1.7'
endif endif
return s:tmux return s:tmux && s:tmux_not_zoomed()
endfunction endfunction
function! s:shellesc(arg) function! s:shellesc(arg)
@@ -86,7 +80,7 @@ function! s:shellesc(arg)
endfunction endfunction
function! s:escape(path) function! s:escape(path)
return substitute(a:path, ' ', '\\ ', 'g') return escape(a:path, ' %#''"\')
endfunction endfunction
" Upgrade legacy options " Upgrade legacy options
@@ -105,7 +99,10 @@ function! s:upgrade(dict)
endfunction endfunction
function! fzf#run(...) abort function! fzf#run(...) abort
if has('nvim') && bufexists('[FZF]') try
let oshell = &shell
set shell=sh
if has('nvim') && bufexists('term://*:FZF')
echohl WarningMsg echohl WarningMsg
echomsg 'FZF is already running!' echomsg 'FZF is already running!'
echohl None echohl None
@@ -149,6 +146,9 @@ function! fzf#run(...) abort
finally finally
call s:popd(dict) call s:popd(dict)
endtry endtry
finally
let &shell = oshell
endtry
endfunction endfunction
function! s:present(dict, ...) function! s:present(dict, ...)
@@ -164,7 +164,13 @@ function! s:fzf_tmux(dict)
let size = '' let size = ''
for o in ['up', 'down', 'left', 'right'] for o in ['up', 'down', 'left', 'right']
if s:present(a:dict, o) if s:present(a:dict, o)
let size = '-'.o[0].(a:dict[o] == 1 ? '' : a:dict[o]) let spec = a:dict[o]
if (o == 'up' || o == 'down') && spec[0] == '~'
let size = '-'.o[0].s:calc_size(&lines, spec[1:], a:dict)
else
" Legacy boolean option
let size = '-'.o[0].(spec == 1 ? '' : spec)
endif
break break
endif endif
endfor endfor
@@ -196,14 +202,28 @@ function! s:popd(dict)
endif endif
endfunction 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) function! s:execute(dict, command, temps)
call s:pushd(a:dict) call s:pushd(a:dict)
silent! !clear 2> /dev/null silent! !clear 2> /dev/null
let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#')
if has('gui_running') if has('gui_running')
let launcher = get(a:dict, 'launcher', get(g:, 'fzf_launcher', s:launcher)) let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher)))
let command = printf(launcher, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'") let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher
let command = printf(fmt, "'".substitute(escaped, "'", "'\"'\"'", 'g')."'")
else else
let command = a:command let command = escaped
endif endif
execute 'silent !'.command execute 'silent !'.command
redraw! redraw!
@@ -227,15 +247,29 @@ function! s:execute_tmux(dict, command, temps)
endif endif
call system(command) call system(command)
redraw!
return s:callback(a:dict, a:temps) return s:callback(a:dict, a:temps)
endfunction endfunction
function! s:calc_size(max, val) function! s:calc_size(max, val, dict)
if a:val =~ '%$' if a:val =~ '%$'
return a:max * str2nr(a:val[:-2]) / 100 let size = a:max * str2nr(a:val[:-2]) / 100
else else
return min([a:max, a:val]) let size = min([a:max, str2nr(a:val)])
endif endif
let srcsz = -1
if type(get(a:dict, 'source', 0)) == type([])
let srcsz = len(a:dict.source)
endif
let opts = get(a:dict, 'options', '').$FZF_DEFAULT_OPTS
let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2
return srcsz >= 0 ? min([srcsz + margin, size]) : size
endfunction
function! s:getpos()
return {'tab': tabpagenr(), 'win': winnr(), 'cnt': winnr('$')}
endfunction endfunction
function! s:split(dict) function! s:split(dict)
@@ -244,13 +278,17 @@ function! s:split(dict)
\ 'down': ['botright', 'resize', &lines], \ 'down': ['botright', 'resize', &lines],
\ 'left': ['vertical topleft', 'vertical resize', &columns], \ 'left': ['vertical topleft', 'vertical resize', &columns],
\ 'right': ['vertical botright', 'vertical resize', &columns] } \ 'right': ['vertical botright', 'vertical resize', &columns] }
let s:ptab = tabpagenr() let s:ppos = s:getpos()
try try
for [dir, triple] in items(directions) for [dir, triple] in items(directions)
let val = get(a:dict, dir, '') let val = get(a:dict, dir, '')
if !empty(val) if !empty(val)
let [cmd, resz, max] = triple let [cmd, resz, max] = triple
let sz = s:calc_size(max, val) if (dir == 'up' || dir == 'down') && val[0] == '~'
let sz = s:calc_size(max, val[1:], a:dict)
else
let sz = s:calc_size(max, val, {})
endif
execute cmd sz.'new' execute cmd sz.'new'
execute resz sz execute resz sz
return return
@@ -262,7 +300,7 @@ function! s:split(dict)
tabnew tabnew
endif endif
finally finally
setlocal winfixwidth winfixheight setlocal winfixwidth winfixheight buftype=nofile bufhidden=wipe nobuflisted
endtry endtry
endfunction endfunction
@@ -270,28 +308,44 @@ function! s:execute_term(dict, command, temps)
call s:split(a:dict) call s:split(a:dict)
call s:pushd(a:dict) call s:pushd(a:dict)
let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps } let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF' }
function! fzf.on_exit(id, code) function! fzf.on_exit(id, code)
let tab = tabpagenr() let pos = s:getpos()
execute 'bd!' self.buf let inplace = pos == s:ppos " {'window': 'enew'}
if s:ptab == tab if !inplace
wincmd p if bufnr('') == self.buf
" We use close instead of bd! since Vim does not close the split when
" there's no other listed buffer (nvim +'set nobuflisted')
close
endif
if pos.tab == s:ppos.tab
wincmd p
endif
endif endif
call s:pushd(self.dict) call s:pushd(self.dict)
try try
redraw!
call s:callback(self.dict, self.temps) call s:callback(self.dict, self.temps)
if inplace && bufnr('') == self.buf
execute "normal! \<c-^>"
" No other listed buffer
if bufnr('') == self.buf
bd!
endif
endif
finally finally
call s:popd(self.dict) call s:popd(self.dict)
endtry endtry
endfunction endfunction
call termopen(a:command, fzf) call termopen(a:command, fzf)
silent file [FZF]
startinsert startinsert
return [] return []
endfunction endfunction
function! s:callback(dict, temps) function! s:callback(dict, temps)
try
if !filereadable(a:temps.result) if !filereadable(a:temps.result)
let lines = [] let lines = []
else else
@@ -315,11 +369,16 @@ function! s:callback(dict, temps)
endfor endfor
return lines return lines
catch
if stridx(v:exception, ':E325:') < 0
echoerr v:exception
endif
endtry
endfunction endfunction
let s:default_action = { let s:default_action = {
\ 'ctrl-m': 'e', \ 'ctrl-m': 'e',
\ 'ctrl-t': 'tabedit', \ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split', \ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' } \ 'ctrl-v': 'vsplit' }
@@ -329,9 +388,15 @@ function! s:cmd_callback(lines) abort
endif endif
let key = remove(a:lines, 0) let key = remove(a:lines, 0)
let cmd = get(s:action, key, 'e') let cmd = get(s:action, key, 'e')
for item in a:lines try
execute cmd s:escape(item) let autochdir = &autochdir
endfor set noautochdir
for item in a:lines
execute cmd s:escape(item)
endfor
finally
let &autochdir = autochdir
endtry
endfunction endfunction
function! s:cmd(bang, ...) abort function! s:cmd(bang, ...) abort
@@ -339,7 +404,7 @@ function! s:cmd(bang, ...) abort
let args = extend(['--expect='.join(keys(s:action), ',')], a:000) let args = extend(['--expect='.join(keys(s:action), ',')], a:000)
let opts = {} let opts = {}
if len(args) > 0 && isdirectory(expand(args[-1])) if len(args) > 0 && isdirectory(expand(args[-1]))
let opts.dir = remove(args, -1) let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g')
endif endif
if !a:bang if !a:bang
let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height)) let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height))

View File

@@ -29,13 +29,14 @@ _fzf_opts_completion() {
+s --no-sort +s --no-sort
--tac --tac
--tiebreak --tiebreak
--bind
-m --multi -m --multi
--no-mouse --no-mouse
+c --no-color --color
+2 --no-256
--black --black
--reverse --reverse
--no-hscroll --no-hscroll
--inline-info
--prompt --prompt
-q --query -q --query
-1 --select-1 -1 --select-1
@@ -44,13 +45,27 @@ _fzf_opts_completion() {
--print-query --print-query
--expect --expect
--toggle-sort --toggle-sort
--sync" --sync
--cycle
--history
--history-size
--header-file
--header-lines
--margin"
case "${prev}" in case "${prev}" in
--tiebreak) --tiebreak)
COMPREPLY=( $(compgen -W "length begin end index" -- ${cur}) ) COMPREPLY=( $(compgen -W "length begin end index" -- ${cur}) )
return 0 return 0
;; ;;
--color)
COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) )
return 0
;;
--history|--header-file)
COMPREPLY=()
return 0
;;
esac esac
if [[ ${cur} =~ ^-|\+ ]]; then if [[ ${cur} =~ ^-|\+ ]]; then
@@ -62,9 +77,10 @@ _fzf_opts_completion() {
} }
_fzf_handle_dynamic_completion() { _fzf_handle_dynamic_completion() {
local cmd orig ret local cmd orig ret orig_cmd
cmd="$1" cmd="$1"
shift shift
orig_cmd="$1"
orig=$(eval "echo \$_fzf_orig_completion_$cmd") orig=$(eval "echo \$_fzf_orig_completion_$cmd")
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
@@ -72,7 +88,7 @@ _fzf_handle_dynamic_completion() {
elif [ -n "$_fzf_completion_loader" ]; then elif [ -n "$_fzf_completion_loader" ]; then
_completion_loader "$@" _completion_loader "$@"
ret=$? ret=$?
eval $(complete | \grep "\-F.* $cmd$" | _fzf_orig_completion_filter) eval $(complete | \grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)
source $BASH_SOURCE source $BASH_SOURCE
return $ret return $ret
fi fi
@@ -83,7 +99,7 @@ _fzf_path_completion() {
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="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') cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g')
COMPREPLY=() COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER:-**} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ ${cur} == *"$trigger" ]]; then if [[ ${cur} == *"$trigger" ]]; then
base=${cur:0:${#cur}-${#trigger}} base=${cur:0:${#cur}-${#trigger}}
@@ -124,7 +140,7 @@ _fzf_list_completion() {
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
read -r src read -r src
cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g')
trigger=${FZF_COMPLETION_TRIGGER:-**} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ ${cur} == *"$trigger" ]]; then if [[ ${cur} == *"$trigger" ]]; then
cur=${cur:0:${#cur}-${#trigger}} cur=${cur:0:${#cur}-${#trigger}}
@@ -179,13 +195,13 @@ _fzf_kill_completion() {
_fzf_telnet_completion() { _fzf_telnet_completion() {
_fzf_list_completion '+m' "$@" << "EOF" _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 EOF
} }
_fzf_ssh_completion() { _fzf_ssh_completion() {
_fzf_list_completion '+m' "$@" << "EOF" _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 EOF
} }
@@ -202,7 +218,7 @@ EOF
} }
# fzf options # fzf options
complete -F _fzf_opts_completion fzf complete -o default -F _fzf_opts_completion fzf
d_cmds="cd pushd rmdir" d_cmds="cd pushd rmdir"
f_cmds=" f_cmds="
@@ -218,11 +234,11 @@ a_cmds="
x_cmds="kill ssh telnet unset unalias export" x_cmds="kill ssh telnet unset unalias export"
# Preserve existing completion # 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 :( # Really wish I could use associative array but OSX comes with bash 3.2 :(
eval $(complete | \grep '\-F' | \grep -v _fzf_ | 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) \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 fi
if type _completion_loader > /dev/null 2>&1; then if type _completion_loader > /dev/null 2>&1; then

163
shell/completion.zsh Normal file
View File

@@ -0,0 +1,163 @@
#!/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 sws
if setopt | grep shwordsplit > /dev/null; then
sws=1
unsetopt shwordsplit
fi
# 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
[ -n "$sws" ] && setopt shwordsplit
}
[ -z "$fzf_default_completion" ] &&
fzf_default_completion=$(bindkey '^I' | grep -v undefined-key | awk '{print $2}')
zle -N fzf-completion
bindkey '^I' fzf-completion

View File

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

View File

@@ -7,18 +7,6 @@ function fzf_key_bindings
set -g TMPDIR /tmp set -g TMPDIR /tmp
end end
function __fzf_list
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-
end
function __fzf_list_dir
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) \
-prune -o -type d -print 2> /dev/null | sed 1d | cut -b3-
end
function __fzf_escape function __fzf_escape
while read item while read item
echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' ' echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' '
@@ -26,25 +14,19 @@ function fzf_key_bindings
end end
function __fzf_ctrl_t function __fzf_ctrl_t
if [ -n "$TMUX_PANE" -a "$FZF_TMUX" != "0" ] set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND "
# FIXME need to handle directory with double-quotes command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
tmux split-window (__fzf_tmux_height) "cd \"$PWD\";fish -c 'fzf_key_bindings; __fzf_ctrl_t_tmux \\$TMUX_PANE'" -o -type f -print \
else -o -type d -print \
__fzf_list | fzf -m > $TMPDIR/fzf.result -o -type l -print 2> /dev/null | sed 1d | cut -b3-"
and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m > $TMPDIR/fzf.result"
commandline -f repaint and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape)
rm -f $TMPDIR/fzf.result commandline -f repaint
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 rm -f $TMPDIR/fzf.result
end end
function __fzf_ctrl_r function __fzf_ctrl_r
history | fzf +s +m --tiebreak=index --toggle-sort=ctrl-r > $TMPDIR/fzf.result history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r > $TMPDIR/fzf.result
and commandline (cat $TMPDIR/fzf.result) and commandline (cat $TMPDIR/fzf.result)
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result rm -f $TMPDIR/fzf.result
@@ -52,29 +34,36 @@ function fzf_key_bindings
function __fzf_alt_c function __fzf_alt_c
# Fish hangs if the command before pipe redirects (2> /dev/null) # Fish hangs if the command before pipe redirects (2> /dev/null)
__fzf_list_dir | fzf +m > $TMPDIR/fzf.result 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 ] [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ]
and cd (cat $TMPDIR/fzf.result) and cd (cat $TMPDIR/fzf.result)
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result rm -f $TMPDIR/fzf.result
end end
function __fzf_tmux_height function __fzfcmd
if set -q FZF_TMUX_HEIGHT set -q FZF_TMUX; or set FZF_TMUX 1
set height $FZF_TMUX_HEIGHT
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 else
set height 40% echo "fzf"
end end
if echo $height | \grep -q -E '%$'
echo "-p "(echo $height | sed 's/%$//')
else
echo "-l $height"
end
set -e height
end end
bind \ct '__fzf_ctrl_t' bind \ct '__fzf_ctrl_t'
bind \cr '__fzf_ctrl_r' bind \cr '__fzf_ctrl_r'
bind \ec '__fzf_alt_c' 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 end

View File

@@ -1,42 +1,34 @@
# Key bindings # Key bindings
# ------------ # ------------
if [[ $- =~ i ]]; then
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line
__fsel() { __fsel() {
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ local cmd="${FZF_CTRL_T_COMMAND:-"command \\find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}"
eval "$cmd" | $(__fzfcmd) -m | while read item; do
printf '%q ' "$item" printf '%q ' "$item"
done done
echo echo
} }
if [[ $- =~ i ]]; then __fzfcmd() {
[ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
}
if [ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ]; then fzf-file-widget() {
fzf-file-widget() { LBUFFER="${LBUFFER}$(__fsel)"
local height zle redisplay
height=${FZF_TMUX_HEIGHT:-40%} }
if [[ $height =~ %$ ]]; then
height="-p ${height%\%}"
else
height="-l $height"
fi
tmux split-window $height "cd $(printf %q "$PWD");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 zle -N fzf-file-widget
bindkey '^T' fzf-file-widget bindkey '^T' fzf-file-widget
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
cd "${$(command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ cd "${$(command \find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m):-.}" -o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m):-.}"
zle reset-prompt zle reset-prompt
} }
zle -N fzf-cd-widget zle -N fzf-cd-widget
@@ -44,11 +36,12 @@ bindkey '\ec' fzf-cd-widget
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() { fzf-history-widget() {
local selected local selected restore_no_bang_hist
if selected=$(fc -l 1 | fzf +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER"); then if selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER") ); then
num=$(echo "$selected" | head -1 | awk '{print $1}' | sed 's/[^0-9]//g') num=$selected[1]
LBUFFER=!$num if [ -n "$num" ]; then
zle expand-history zle vi-fetch-history -n $num
fi
fi fi
zle redisplay zle redisplay
} }

View File

@@ -2,6 +2,7 @@ FROM base/archlinux:2014.07.03
MAINTAINER Junegunn Choi <junegunn.c@gmail.com> MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# apt-get # apt-get
RUN pacman-key --populate archlinux && pacman-key --refresh-keys
RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git
# Install Go 1.4 # Install Go 1.4

View File

@@ -3,7 +3,7 @@ MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# apt-get # apt-get
RUN apt-get update && apt-get -y upgrade && \ RUN apt-get update && apt-get -y upgrade && \
apt-get install -y --force-yes git curl build-essential libncurses-dev apt-get install -y --force-yes git curl build-essential libncurses-dev libgpm-dev
# Install Go 1.4 # Install Go 1.4
RUN cd / && curl \ RUN cd / && curl \

View File

@@ -5,8 +5,19 @@ endif
UNAME_S := $(shell uname -s) UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin) ifeq ($(UNAME_S),Darwin)
GOOS := darwin GOOS := darwin
LDFLAGS :=
ifdef STATIC
$(error Static linking not possible on OS X)
endif
else ifeq ($(UNAME_S),Linux) else ifeq ($(UNAME_S),Linux)
GOOS := linux GOOS := linux
ifdef STATIC
SUFFIX := -static
LDFLAGS := --ldflags '-extldflags "-static -ltinfo -lgpm"'
else
SUFFIX :=
LDFLAGS :=
endif
endif endif
ifneq ($(shell uname -m),x86_64) ifneq ($(shell uname -m),x86_64)
@@ -16,21 +27,24 @@ endif
SOURCES := $(wildcard *.go */*.go) SOURCES := $(wildcard *.go */*.go)
BINDIR := ../bin BINDIR := ../bin
BINARY32 := fzf-$(GOOS)_386 BINARY32 := fzf-$(GOOS)_386$(SUFFIX)
BINARY64 := fzf-$(GOOS)_amd64 BINARY64 := fzf-$(GOOS)_amd64$(SUFFIX)
VERSION = $(shell fzf/$(BINARY64) --version) VERSION = $(shell fzf/$(BINARY64) --version)
RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 RELEASE32 = fzf-$(VERSION)-$(GOOS)_386$(SUFFIX)
RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64$(SUFFIX)
all: release all: release
release: build release: build
cd fzf && \ -cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32)
cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) && \ cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \
cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ rm -f $(RELEASE32) $(RELEASE64)
rm $(RELEASE32) $(RELEASE64)
ifndef STATIC
build: test fzf/$(BINARY32) fzf/$(BINARY64) build: test fzf/$(BINARY32) fzf/$(BINARY64)
else
build: test fzf/$(BINARY64)
endif
test: test:
go get go get
@@ -42,13 +56,13 @@ uninstall:
rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64) rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64)
clean: clean:
cd fzf && rm -f $(BINARY32) $(BINARY64) $(RELEASE32).tgz $(RELEASE64).tgz cd fzf && rm -f fzf-*
fzf/$(BINARY32): $(SOURCES) fzf/$(BINARY32): $(SOURCES)
cd fzf && GOARCH=386 CGO_ENABLED=1 go build -o $(BINARY32) cd fzf && GOARCH=386 CGO_ENABLED=1 go build -o $(BINARY32)
fzf/$(BINARY64): $(SOURCES) fzf/$(BINARY64): $(SOURCES)
cd fzf && go build -o $(BINARY64) cd fzf && go build $(LDFLAGS) -o $(BINARY64)
$(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) $(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR)
cp -f fzf/$(BINARY64) $(BINDIR) cp -f fzf/$(BINARY64) $(BINDIR)
@@ -57,18 +71,27 @@ $(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR)
$(BINDIR): $(BINDIR):
mkdir -p $@ mkdir -p $@
# Linux distribution to build fzf on docker-arch:
DISTRO := arch docker build -t junegunn/arch-sandbox - < Dockerfile.arch
docker: docker-ubuntu:
docker build -t junegunn/$(DISTRO)-sandbox - < Dockerfile.$(DISTRO) docker build -t junegunn/ubuntu-sandbox - < Dockerfile.ubuntu
linux: docker arch: docker-arch
docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \ docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \
/bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make'
$(DISTRO): docker
docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \
sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash'
.PHONY: all build release test install uninstall clean docker linux $(DISTRO) ubuntu: docker-ubuntu
docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \
sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash'
linux: docker-arch
docker run -i -t -v $(GOPATH):/go junegunn/arch-sandbox \
/bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make'
linux-static: docker-ubuntu
docker run -i -t -v $(GOPATH):/go junegunn/ubuntu-sandbox \
/bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make STATIC=1'
.PHONY: all build release test install uninstall clean docker \
linux linux-static arch ubuntu docker-arch docker-ubuntu

View File

@@ -1,6 +1,7 @@
package algo package algo
import ( import (
"strings"
"unicode" "unicode"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
@@ -14,8 +15,15 @@ import (
* In short: They try to do as little work as possible. * In short: They try to do as little work as possible.
*/ */
func runeAt(runes []rune, index int, max int, forward bool) rune {
if forward {
return runes[index]
}
return runes[max-index-1]
}
// FuzzyMatch performs fuzzy-match // FuzzyMatch performs fuzzy-match
func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
if len(pattern) == 0 { if len(pattern) == 0 {
return 0, 0 return 0, 0
} }
@@ -33,7 +41,11 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
sidx := -1 sidx := -1
eidx := -1 eidx := -1
for index, char := range *runes { lenRunes := len(runes)
lenPattern := len(pattern)
for index := range runes {
char := runeAt(runes, index, lenRunes, forward)
// This is considerably faster than blindly applying strings.ToLower to the // This is considerably faster than blindly applying strings.ToLower to the
// whole string // whole string
if !caseSensitive { if !caseSensitive {
@@ -42,17 +54,16 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
// compiler as of now does not inline non-leaf functions.) // compiler as of now does not inline non-leaf functions.)
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
(*runes)[index] = char
} else if char > unicode.MaxASCII { } else if char > unicode.MaxASCII {
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
(*runes)[index] = char
} }
} }
if char == pattern[pidx] { pchar := runeAt(pattern, pidx, lenPattern, forward)
if char == pchar {
if sidx < 0 { if sidx < 0 {
sidx = index sidx = index
} }
if pidx++; pidx == len(pattern) { if pidx++; pidx == lenPattern {
eidx = index + 1 eidx = index + 1
break break
} }
@@ -62,15 +73,27 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
if sidx >= 0 && eidx >= 0 { if sidx >= 0 && eidx >= 0 {
pidx-- pidx--
for index := eidx - 1; index >= sidx; index-- { for index := eidx - 1; index >= sidx; index-- {
char := (*runes)[index] char := runeAt(runes, index, lenRunes, forward)
if char == pattern[pidx] { if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
} else if char > unicode.MaxASCII {
char = unicode.To(unicode.LowerCase, char)
}
}
pchar := runeAt(pattern, pidx, lenPattern, forward)
if char == pchar {
if pidx--; pidx < 0 { if pidx--; pidx < 0 {
sidx = index sidx = index
break break
} }
} }
} }
return sidx, eidx if forward {
return sidx, eidx
}
return lenRunes - eidx, lenRunes - sidx
} }
return -1, -1 return -1, -1
} }
@@ -82,20 +105,21 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
// //
// We might try to implement better algorithms in the future: // We might try to implement better algorithms in the future:
// http://en.wikipedia.org/wiki/String_searching_algorithm // http://en.wikipedia.org/wiki/String_searching_algorithm
func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
if len(pattern) == 0 { if len(pattern) == 0 {
return 0, 0 return 0, 0
} }
numRunes := len(*runes) lenRunes := len(runes)
plen := len(pattern) lenPattern := len(pattern)
if numRunes < plen {
if lenRunes < lenPattern {
return -1, -1 return -1, -1
} }
pidx := 0 pidx := 0
for index := 0; index < numRunes; index++ { for index := 0; index < lenRunes; index++ {
char := (*runes)[index] char := runeAt(runes, index, lenRunes, forward)
if !caseSensitive { if !caseSensitive {
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
@@ -103,10 +127,14 @@ func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, in
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
if pattern[pidx] == char { pchar := runeAt(pattern, pidx, lenPattern, forward)
if pchar == char {
pidx++ pidx++
if pidx == plen { if pidx == lenPattern {
return index - plen + 1, index + 1 if forward {
return index - lenPattern + 1, index + 1
}
return lenRunes - (index + 1), lenRunes - (index - lenPattern + 1)
} }
} else { } else {
index -= pidx index -= pidx
@@ -117,13 +145,13 @@ func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, in
} }
// PrefixMatch performs prefix-match // PrefixMatch performs prefix-match
func PrefixMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
if len(*runes) < len(pattern) { if len(runes) < len(pattern) {
return -1, -1 return -1, -1
} }
for index, r := range pattern { for index, r := range pattern {
char := (*runes)[index] char := runes[index]
if !caseSensitive { if !caseSensitive {
char = unicode.ToLower(char) char = unicode.ToLower(char)
} }
@@ -135,7 +163,7 @@ func PrefixMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
} }
// SuffixMatch performs suffix-match // SuffixMatch performs suffix-match
func SuffixMatch(caseSensitive bool, input *[]rune, pattern []rune) (int, int) { func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) (int, int) {
runes := util.TrimRight(input) runes := util.TrimRight(input)
trimmedLen := len(runes) trimmedLen := len(runes)
diff := trimmedLen - len(pattern) diff := trimmedLen - len(pattern)
@@ -154,3 +182,18 @@ func SuffixMatch(caseSensitive bool, input *[]rune, pattern []rune) (int, int) {
} }
return trimmedLen - len(pattern), trimmedLen return trimmedLen - len(pattern), trimmedLen
} }
// EqualMatch performs equal-match
func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
if len(runes) != len(pattern) {
return -1, -1
}
runesStr := string(runes)
if !caseSensitive {
runesStr = strings.ToLower(runesStr)
}
if runesStr == string(pattern) {
return 0, len(pattern)
}
return -1, -1
}

View File

@@ -5,12 +5,11 @@ import (
"testing" "testing"
) )
func assertMatch(t *testing.T, fun func(bool, *[]rune, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) { func assertMatch(t *testing.T, fun func(bool, bool, []rune, []rune) (int, int), caseSensitive bool, forward bool, input string, pattern string, sidx int, eidx int) {
if !caseSensitive { if !caseSensitive {
pattern = strings.ToLower(pattern) pattern = strings.ToLower(pattern)
} }
runes := []rune(input) s, e := fun(caseSensitive, forward, []rune(input), []rune(pattern))
s, e := fun(caseSensitive, &runes, []rune(pattern))
if s != sidx { if s != sidx {
t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern) t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern)
} }
@@ -20,33 +19,51 @@ func assertMatch(t *testing.T, fun func(bool, *[]rune, []rune) (int, int), caseS
} }
func TestFuzzyMatch(t *testing.T) { func TestFuzzyMatch(t *testing.T) {
assertMatch(t, FuzzyMatch, false, "fooBarbaz", "oBZ", 2, 9) assertMatch(t, FuzzyMatch, false, true, "fooBarbaz", "oBZ", 2, 9)
assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBZ", -1, -1) assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBZ", -1, -1)
assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBz", 2, 9) assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBz", 2, 9)
assertMatch(t, FuzzyMatch, true, "fooBarbaz", "fooBarbazz", -1, -1) assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "fooBarbazz", -1, -1)
}
func TestFuzzyMatchBackward(t *testing.T) {
assertMatch(t, FuzzyMatch, false, true, "foobar fb", "fb", 0, 4)
assertMatch(t, FuzzyMatch, false, false, "foobar fb", "fb", 7, 9)
} }
func TestExactMatchNaive(t *testing.T) { func TestExactMatchNaive(t *testing.T) {
assertMatch(t, ExactMatchNaive, false, "fooBarbaz", "oBA", 2, 5) for _, dir := range []bool{true, false} {
assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "oBA", -1, -1) assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5)
assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "fooBarbazz", -1, -1) assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1)
assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1)
}
}
func TestExactMatchNaiveBackward(t *testing.T) {
assertMatch(t, FuzzyMatch, false, true, "foobar foob", "oo", 1, 3)
assertMatch(t, FuzzyMatch, false, false, "foobar foob", "oo", 8, 10)
} }
func TestPrefixMatch(t *testing.T) { func TestPrefixMatch(t *testing.T) {
assertMatch(t, PrefixMatch, false, "fooBarbaz", "Foo", 0, 3) for _, dir := range []bool{true, false} {
assertMatch(t, PrefixMatch, true, "fooBarbaz", "Foo", -1, -1) assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3)
assertMatch(t, PrefixMatch, false, "fooBarbaz", "baz", -1, -1) assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1)
assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "baz", -1, -1)
}
} }
func TestSuffixMatch(t *testing.T) { func TestSuffixMatch(t *testing.T) {
assertMatch(t, SuffixMatch, false, "fooBarbaz", "Foo", -1, -1) for _, dir := range []bool{true, false} {
assertMatch(t, SuffixMatch, false, "fooBarbaz", "baz", 6, 9) assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1)
assertMatch(t, SuffixMatch, true, "fooBarbaz", "Baz", -1, -1) assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9)
assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1)
}
} }
func TestEmptyPattern(t *testing.T) { func TestEmptyPattern(t *testing.T) {
assertMatch(t, FuzzyMatch, true, "foobar", "", 0, 0) for _, dir := range []bool{true, false} {
assertMatch(t, ExactMatchNaive, true, "foobar", "", 0, 0) assertMatch(t, FuzzyMatch, true, dir, "foobar", "", 0, 0)
assertMatch(t, PrefixMatch, true, "foobar", "", 0, 0) assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0)
assertMatch(t, SuffixMatch, true, "foobar", "", 6, 6) assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0)
assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6)
}
} }

View File

@@ -36,21 +36,23 @@ func init() {
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]") ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]")
} }
func extractColor(str *string) (*string, []ansiOffset) { func extractColor(str string, state *ansiState) (string, []ansiOffset, *ansiState) {
var offsets []ansiOffset var offsets []ansiOffset
var output bytes.Buffer var output bytes.Buffer
var state *ansiState
if state != nil {
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
}
idx := 0 idx := 0
for _, offset := range ansiRegex.FindAllStringIndex(*str, -1) { for _, offset := range ansiRegex.FindAllStringIndex(str, -1) {
output.WriteString((*str)[idx:offset[0]]) output.WriteString(str[idx:offset[0]])
newState := interpretCode((*str)[offset[0]:offset[1]], state) newState := interpretCode(str[offset[0]:offset[1]], state)
if !newState.equals(state) { if !newState.equals(state) {
if state != nil { if state != nil {
// Update last offset // Update last offset
(&offsets[len(offsets)-1]).offset[1] = int32(output.Len()) (&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
} }
if newState.colored() { if newState.colored() {
@@ -67,7 +69,7 @@ func extractColor(str *string) (*string, []ansiOffset) {
idx = offset[1] idx = offset[1]
} }
rest := (*str)[idx:] rest := str[idx:]
if len(rest) > 0 { if len(rest) > 0 {
output.WriteString(rest) output.WriteString(rest)
if state != nil { if state != nil {
@@ -75,8 +77,7 @@ func extractColor(str *string) (*string, []ansiOffset) {
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes())) (&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
} }
} }
outputStr := output.String() return output.String(), offsets, state
return &outputStr, offsets
} }
func interpretCode(ansiCode string, prevState *ansiState) *ansiState { func interpretCode(ansiCode string, prevState *ansiState) *ansiState {

View File

@@ -14,79 +14,89 @@ func TestExtractColor(t *testing.T) {
} }
src := "hello world" src := "hello world"
var state *ansiState
clean := "\x1b[0m" clean := "\x1b[0m"
check := func(assertion func(ansiOffsets []ansiOffset)) { check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) {
output, ansiOffsets := extractColor(&src) output, ansiOffsets, newState := extractColor(src, state)
if *output != "hello world" { state = newState
if output != "hello world" {
t.Errorf("Invalid output: {}", output) t.Errorf("Invalid output: {}", output)
} }
fmt.Println(src, ansiOffsets, clean) fmt.Println(src, ansiOffsets, clean)
assertion(ansiOffsets) assertion(ansiOffsets, state)
} }
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) > 0 { if len(offsets) > 0 {
t.Fail() t.Fail()
} }
}) })
state = nil
src = "\x1b[0mhello world" src = "\x1b[0mhello world"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) > 0 { if len(offsets) > 0 {
t.Fail() t.Fail()
} }
}) })
state = nil
src = "\x1b[1mhello world" src = "\x1b[1mhello world"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 11, -1, -1, true) assert(offsets[0], 0, 11, -1, -1, true)
}) })
state = nil
src = "\x1b[1mhello \x1b[mworld" src = "\x1b[1mhello \x1b[mworld"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 6, -1, -1, true) assert(offsets[0], 0, 6, -1, -1, true)
}) })
state = nil
src = "\x1b[1mhello \x1b[Kworld" src = "\x1b[1mhello \x1b[Kworld"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 11, -1, -1, true) assert(offsets[0], 0, 11, -1, -1, true)
}) })
state = nil
src = "hello \x1b[34;45;1mworld" src = "hello \x1b[34;45;1mworld"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 11, 4, 5, true) assert(offsets[0], 6, 11, 4, 5, true)
}) })
state = nil
src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld" src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 11, 4, 5, true) assert(offsets[0], 6, 11, 4, 5, true)
}) })
state = nil
src = "hello \x1b[34;45;1mwor\x1b[0mld" src = "hello \x1b[34;45;1mwor\x1b[0mld"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 9, 4, 5, true) assert(offsets[0], 6, 9, 4, 5, true)
}) })
state = nil
src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md" src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 3 { if len(offsets) != 3 {
t.Fail() t.Fail()
} }
@@ -96,12 +106,47 @@ func TestExtractColor(t *testing.T) {
}) })
// {38,48};5;{38,48} // {38,48};5;{38,48}
state = nil
src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md" src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 2 { if len(offsets) != 2 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 9, 38, 48, true) assert(offsets[0], 6, 9, 38, 48, true)
assert(offsets[1], 9, 10, 48, 38, true) assert(offsets[1], 9, 10, 48, 38, true)
}) })
src = "hello \x1b[32;1mworld"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 {
t.Fail()
}
if state.fg != 2 || state.bg != -1 || !state.bold {
t.Fail()
}
assert(offsets[0], 6, 11, 2, -1, true)
})
src = "hello world"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 {
t.Fail()
}
if state.fg != 2 || state.bg != -1 || !state.bold {
t.Fail()
}
assert(offsets[0], 0, 11, 2, -1, true)
})
src = "hello \x1b[0;38;5;200;48;5;100mworld"
check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 2 {
t.Fail()
}
if state.fg != 200 || state.bg != 100 || state.bold {
t.Fail()
}
assert(offsets[0], 0, 6, 2, -1, true)
assert(offsets[1], 6, 11, 200, 100, false)
})
} }

View File

@@ -7,7 +7,7 @@ type Chunk []*Item // >>> []Item
// ItemBuilder is a closure type that builds Item object from a pointer to a // ItemBuilder is a closure type that builds Item object from a pointer to a
// string and an integer // string and an integer
type ItemBuilder func(*string, int) *Item type ItemBuilder func([]byte, int) *Item
// ChunkList is a list of Chunks // ChunkList is a list of Chunks
type ChunkList struct { type ChunkList struct {
@@ -26,8 +26,13 @@ func NewChunkList(trans ItemBuilder) *ChunkList {
trans: trans} trans: trans}
} }
func (c *Chunk) push(trans ItemBuilder, data *string, index int) { func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool {
*c = append(*c, trans(data, index)) item := trans(data, index)
if item != nil {
*c = append(*c, item)
return true
}
return false
} }
// IsFull returns true if the Chunk is full // IsFull returns true if the Chunk is full
@@ -48,7 +53,7 @@ func CountItems(cs []*Chunk) int {
} }
// Push adds the item to the list // Push adds the item to the list
func (cl *ChunkList) Push(data string) { func (cl *ChunkList) Push(data []byte) bool {
cl.mutex.Lock() cl.mutex.Lock()
defer cl.mutex.Unlock() defer cl.mutex.Unlock()
@@ -57,8 +62,11 @@ func (cl *ChunkList) Push(data string) {
cl.chunks = append(cl.chunks, &newChunk) cl.chunks = append(cl.chunks, &newChunk)
} }
cl.lastChunk().push(cl.trans, &data, cl.count) if cl.lastChunk().push(cl.trans, data, cl.count) {
cl.count++ cl.count++
return true
}
return false
} }
// Snapshot returns immutable snapshot of the ChunkList // Snapshot returns immutable snapshot of the ChunkList

View File

@@ -6,8 +6,8 @@ import (
) )
func TestChunkList(t *testing.T) { func TestChunkList(t *testing.T) {
cl := NewChunkList(func(s *string, i int) *Item { cl := NewChunkList(func(s []byte, i int) *Item {
return &Item{text: s, rank: Rank{0, 0, uint32(i * 2)}} return &Item{text: []rune(string(s)), rank: Rank{0, 0, uint32(i * 2)}}
}) })
// Snapshot // Snapshot
@@ -17,8 +17,8 @@ func TestChunkList(t *testing.T) {
} }
// Add some data // Add some data
cl.Push("hello") cl.Push([]byte("hello"))
cl.Push("world") cl.Push([]byte("world"))
// Previously created snapshot should remain the same // Previously created snapshot should remain the same
if len(snapshot) > 0 { if len(snapshot) > 0 {
@@ -36,8 +36,8 @@ func TestChunkList(t *testing.T) {
if len(*chunk1) != 2 { if len(*chunk1) != 2 {
t.Error("Snapshot should contain only two items") t.Error("Snapshot should contain only two items")
} }
if *(*chunk1)[0].text != "hello" || (*chunk1)[0].rank.index != 0 || if string((*chunk1)[0].text) != "hello" || (*chunk1)[0].rank.index != 0 ||
*(*chunk1)[1].text != "world" || (*chunk1)[1].rank.index != 2 { string((*chunk1)[1].text) != "world" || (*chunk1)[1].rank.index != 2 {
t.Error("Invalid data") t.Error("Invalid data")
} }
if chunk1.IsFull() { if chunk1.IsFull() {
@@ -46,7 +46,7 @@ func TestChunkList(t *testing.T) {
// Add more data // Add more data
for i := 0; i < chunkSize*2; i++ { for i := 0; i < chunkSize*2; i++ {
cl.Push(fmt.Sprintf("item %d", i)) cl.Push([]byte(fmt.Sprintf("item %d", i)))
} }
// Previous snapshot should remain the same // Previous snapshot should remain the same
@@ -64,8 +64,8 @@ func TestChunkList(t *testing.T) {
t.Error("Unexpected number of items") t.Error("Unexpected number of items")
} }
cl.Push("hello") cl.Push([]byte("hello"))
cl.Push("world") cl.Push([]byte("world"))
lastChunkCount := len(*snapshot[len(snapshot)-1]) lastChunkCount := len(*snapshot[len(snapshot)-1])
if lastChunkCount != 2 { if lastChunkCount != 2 {

View File

@@ -8,14 +8,14 @@ import (
const ( const (
// Current version // Current version
Version = "0.9.10" version = "0.10.5"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond
coordinatorDelayStep time.Duration = 10 * time.Millisecond coordinatorDelayStep time.Duration = 10 * time.Millisecond
// Reader // Reader
defaultCommand = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null` defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
// Terminal // Terminal
initialDelay = 100 * time.Millisecond initialDelay = 100 * time.Millisecond
@@ -32,6 +32,9 @@ const (
// Not to cache mergers with large lists // Not to cache mergers with large lists
mergerCacheMax int = 100000 mergerCacheMax int = 100000
// History
defaultHistoryMax int = 1000
) )
// fzf events // fzf events
@@ -41,5 +44,6 @@ const (
EvtSearchNew EvtSearchNew
EvtSearchProgress EvtSearchProgress
EvtSearchFin EvtSearchFin
EvtHeader
EvtClose EvtClose
) )

View File

@@ -44,18 +44,18 @@ Reader -> EvtReadNew -> Matcher (restart)
Terminal -> EvtSearchNew:bool -> Matcher (restart) Terminal -> EvtSearchNew:bool -> Matcher (restart)
Matcher -> EvtSearchProgress -> Terminal (update info) Matcher -> EvtSearchProgress -> Terminal (update info)
Matcher -> EvtSearchFin -> Terminal (update list) Matcher -> EvtSearchFin -> Terminal (update list)
Matcher -> EvtHeader -> Terminal (update header)
*/ */
// Run starts fzf // Run starts fzf
func Run(options *Options) { func Run(opts *Options) {
initProcs() initProcs()
opts := ParseOptions()
sort := opts.Sort > 0 sort := opts.Sort > 0
rankTiebreak = opts.Tiebreak rankTiebreak = opts.Tiebreak
if opts.Version { if opts.Version {
fmt.Println(Version) fmt.Println(version)
os.Exit(0) os.Exit(0)
} }
@@ -63,48 +63,68 @@ func Run(options *Options) {
eventBox := util.NewEventBox() eventBox := util.NewEventBox()
// ANSI code processor // ANSI code processor
ansiProcessor := func(data *string) (*string, []ansiOffset) { ansiProcessor := func(data []byte) ([]rune, []ansiOffset) {
// By default, we do nothing return util.BytesToRunes(data), nil
}
ansiProcessorRunes := func(data []rune) ([]rune, []ansiOffset) {
return data, nil return data, nil
} }
if opts.Ansi { if opts.Ansi {
if opts.Theme != nil { if opts.Theme != nil {
ansiProcessor = func(data *string) (*string, []ansiOffset) { var state *ansiState
return extractColor(data) ansiProcessor = func(data []byte) ([]rune, []ansiOffset) {
trimmed, offsets, newState := extractColor(string(data), state)
state = newState
return []rune(trimmed), offsets
} }
} else { } else {
// When color is disabled but ansi option is given, // When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input // we simply strip out ANSI codes from the input
ansiProcessor = func(data *string) (*string, []ansiOffset) { ansiProcessor = func(data []byte) ([]rune, []ansiOffset) {
trimmed, _ := extractColor(data) trimmed, _, _ := extractColor(string(data), nil)
return trimmed, nil return []rune(trimmed), nil
} }
} }
ansiProcessorRunes = func(data []rune) ([]rune, []ansiOffset) {
return ansiProcessor([]byte(string(data)))
}
} }
// Chunk list // Chunk list
var chunkList *ChunkList var chunkList *ChunkList
header := make([]string, 0, opts.HeaderLines)
if len(opts.WithNth) == 0 { if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(data *string, index int) *Item { chunkList = NewChunkList(func(data []byte, index int) *Item {
data, colors := ansiProcessor(data) if len(header) < opts.HeaderLines {
header = append(header, string(data))
eventBox.Set(EvtHeader, header)
return nil
}
runes, colors := ansiProcessor(data)
return &Item{ return &Item{
text: data, text: runes,
index: uint32(index), index: uint32(index),
colors: colors, colors: colors,
rank: Rank{0, 0, uint32(index)}} rank: Rank{0, 0, uint32(index)}}
}) })
} else { } else {
chunkList = NewChunkList(func(data *string, index int) *Item { chunkList = NewChunkList(func(data []byte, index int) *Item {
tokens := Tokenize(data, opts.Delimiter) runes := util.BytesToRunes(data)
tokens := Tokenize(runes, opts.Delimiter)
trans := Transform(tokens, opts.WithNth) trans := Transform(tokens, opts.WithNth)
if len(header) < opts.HeaderLines {
header = append(header, string(joinTokens(trans)))
eventBox.Set(EvtHeader, header)
return nil
}
item := Item{ item := Item{
text: joinTokens(trans), text: joinTokens(trans),
origText: data, origText: &runes,
index: uint32(index), index: uint32(index),
colors: nil, colors: nil,
rank: Rank{0, 0, uint32(index)}} rank: Rank{0, 0, uint32(index)}}
trimmed, colors := ansiProcessor(item.text) trimmed, colors := ansiProcessorRunes(item.text)
item.text = trimmed item.text = trimmed
item.colors = colors item.colors = colors
return &item return &item
@@ -114,14 +134,17 @@ func Run(options *Options) {
// Reader // Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
if !streamingFilter { if !streamingFilter {
reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} reader := Reader{func(data []byte) bool {
return chunkList.Push(data)
}, eventBox, opts.ReadZero}
go reader.ReadSource() go reader.ReadSource()
} }
// Matcher // Matcher
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern( return BuildPattern(
opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes) opts.Mode, opts.Case, opts.Tiebreak != byEnd,
opts.Nth, opts.Delimiter, runes)
} }
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
@@ -135,12 +158,13 @@ func Run(options *Options) {
if streamingFilter { if streamingFilter {
reader := Reader{ reader := Reader{
func(str string) { func(runes []byte) bool {
item := chunkList.trans(&str, 0) item := chunkList.trans(runes, 0)
if pattern.MatchItem(item) { if item != nil && pattern.MatchItem(item) {
fmt.Println(*item.text) fmt.Println(string(item.text))
} }
}, eventBox} return false
}, eventBox, opts.ReadZero}
reader.ReadSource() reader.ReadSource()
} else { } else {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
@@ -151,7 +175,7 @@ func Run(options *Options) {
chunks: snapshot, chunks: snapshot,
pattern: pattern}) pattern: pattern})
for i := 0; i < merger.Length(); i++ { for i := 0; i < merger.Length(); i++ {
fmt.Println(merger.Get(i).AsString()) fmt.Println(merger.Get(i).AsString(opts.Ansi))
} }
} }
os.Exit(0) os.Exit(0)
@@ -207,6 +231,9 @@ func Run(options *Options) {
terminal.UpdateProgress(val) terminal.UpdateProgress(val)
} }
case EvtHeader:
terminal.UpdateHeader(value.([]string), opts.HeaderLines)
case EvtSearchFin: case EvtSearchFin:
switch val := value.(type) { switch val := value.(type) {
case *Merger: case *Merger:
@@ -224,7 +251,7 @@ func Run(options *Options) {
fmt.Println() fmt.Println()
} }
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
fmt.Println(val.Get(i).AsString()) fmt.Println(val.Get(i).AsString(opts.Ansi))
} }
os.Exit(0) os.Exit(0)
} }

View File

@@ -4,15 +4,11 @@ package curses
#include <ncurses.h> #include <ncurses.h>
#include <locale.h> #include <locale.h>
#cgo LDFLAGS: -lncurses #cgo LDFLAGS: -lncurses
void swapOutput() {
FILE* temp = stdout;
stdout = stderr;
stderr = temp;
}
*/ */
import "C" import "C"
import ( import (
"fmt"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
@@ -56,11 +52,22 @@ const (
Mouse Mouse
BTab BTab
BSpace
Del Del
PgUp PgUp
PgDn PgDn
Up
Down
Left
Right
Home
End
SLeft
SRight
F1 F1
F2 F2
F3 F3
@@ -88,6 +95,7 @@ const (
ColInfo ColInfo
ColCursor ColCursor
ColSelected ColSelected
ColHeader
ColUser ColUser
) )
@@ -96,15 +104,19 @@ const (
) )
type ColorTheme struct { type ColorTheme struct {
darkBg C.short UseDefault bool
prompt C.short Fg int16
match C.short Bg int16
current C.short DarkBg int16
currentMatch C.short Prompt int16
spinner C.short Match int16
info C.short Current int16
cursor C.short CurrentMatch int16
selected C.short Spinner int16
Info int16
Cursor int16
Selected int16
Header int16
} }
type Event struct { type Event struct {
@@ -129,10 +141,14 @@ var (
_colorMap map[int]int _colorMap map[int]int
_prevDownTime time.Time _prevDownTime time.Time
_clickY []int _clickY []int
_screen *C.SCREEN
Default16 *ColorTheme Default16 *ColorTheme
Dark256 *ColorTheme Dark256 *ColorTheme
Light256 *ColorTheme Light256 *ColorTheme
DarkBG C.short FG int
CurrentFG int
BG int
DarkBG int
) )
func init() { func init() {
@@ -140,35 +156,47 @@ func init() {
_clickY = []int{} _clickY = []int{}
_colorMap = make(map[int]int) _colorMap = make(map[int]int)
Default16 = &ColorTheme{ Default16 = &ColorTheme{
darkBg: C.COLOR_BLACK, UseDefault: true,
prompt: C.COLOR_BLUE, Fg: 15,
match: C.COLOR_GREEN, Bg: 0,
current: C.COLOR_YELLOW, DarkBg: C.COLOR_BLACK,
currentMatch: C.COLOR_GREEN, Prompt: C.COLOR_BLUE,
spinner: C.COLOR_GREEN, Match: C.COLOR_GREEN,
info: C.COLOR_WHITE, Current: C.COLOR_YELLOW,
cursor: C.COLOR_RED, CurrentMatch: C.COLOR_GREEN,
selected: C.COLOR_MAGENTA} Spinner: C.COLOR_GREEN,
Info: C.COLOR_WHITE,
Cursor: C.COLOR_RED,
Selected: C.COLOR_MAGENTA,
Header: C.COLOR_CYAN}
Dark256 = &ColorTheme{ Dark256 = &ColorTheme{
darkBg: 236, UseDefault: true,
prompt: 110, Fg: 15,
match: 108, Bg: 0,
current: 254, DarkBg: 236,
currentMatch: 151, Prompt: 110,
spinner: 148, Match: 108,
info: 144, Current: 254,
cursor: 161, CurrentMatch: 151,
selected: 168} Spinner: 148,
Info: 144,
Cursor: 161,
Selected: 168,
Header: 109}
Light256 = &ColorTheme{ Light256 = &ColorTheme{
darkBg: 251, UseDefault: true,
prompt: 25, Fg: 15,
match: 66, Bg: 0,
current: 237, DarkBg: 251,
currentMatch: 23, Prompt: 25,
spinner: 65, Match: 66,
info: 101, Current: 237,
cursor: 161, CurrentMatch: 23,
selected: 168} Spinner: 65,
Info: 101,
Cursor: 161,
Selected: 168,
Header: 31}
} }
func attrColored(pair int, bold bool) C.int { func attrColored(pair int, bold bool) C.int {
@@ -229,14 +257,16 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
// syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd())) // syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd()))
} }
C.swapOutput()
C.setlocale(C.LC_ALL, C.CString("")) C.setlocale(C.LC_ALL, C.CString(""))
C.initscr() _screen = C.newterm(nil, C.stderr, C.stdin)
if _screen == nil {
fmt.Println("Invalid $TERM: " + os.Getenv("TERM"))
os.Exit(1)
}
C.set_term(_screen)
if mouse { if mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil) C.mousemask(C.ALL_MOUSE_EVENTS, nil)
} }
C.cbreak()
C.noecho() C.noecho()
C.raw() // stty dsusp undef C.raw() // stty dsusp undef
@@ -258,28 +288,41 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
} }
func initPairs(theme *ColorTheme, black bool) { func initPairs(theme *ColorTheme, black bool) {
var bg C.short fg := C.short(theme.Fg)
bg := C.short(theme.Bg)
if black { if black {
bg = C.COLOR_BLACK bg = C.COLOR_BLACK
} else { } else if theme.UseDefault {
C.use_default_colors() fg = -1
bg = -1 bg = -1
C.use_default_colors()
}
if theme.UseDefault {
FG = -1
BG = -1
} else {
FG = int(fg)
BG = int(bg)
C.assume_default_colors(C.int(theme.Fg), C.int(bg))
} }
DarkBG = theme.darkBg CurrentFG = int(theme.Current)
C.init_pair(ColPrompt, theme.prompt, bg) DarkBG = int(theme.DarkBg)
C.init_pair(ColMatch, theme.match, bg) darkBG := C.short(DarkBG)
C.init_pair(ColCurrent, theme.current, DarkBG) C.init_pair(ColPrompt, C.short(theme.Prompt), bg)
C.init_pair(ColCurrentMatch, theme.currentMatch, DarkBG) C.init_pair(ColMatch, C.short(theme.Match), bg)
C.init_pair(ColSpinner, theme.spinner, bg) C.init_pair(ColCurrent, C.short(theme.Current), darkBG)
C.init_pair(ColInfo, theme.info, bg) C.init_pair(ColCurrentMatch, C.short(theme.CurrentMatch), darkBG)
C.init_pair(ColCursor, theme.cursor, DarkBG) C.init_pair(ColSpinner, C.short(theme.Spinner), bg)
C.init_pair(ColSelected, theme.selected, DarkBG) C.init_pair(ColInfo, C.short(theme.Info), bg)
C.init_pair(ColCursor, C.short(theme.Cursor), darkBG)
C.init_pair(ColSelected, C.short(theme.Selected), darkBG)
C.init_pair(ColHeader, C.short(theme.Header), bg)
} }
func Close() { func Close() {
C.endwin() C.endwin()
C.swapOutput() C.delscreen(_screen)
} }
func GetBytes() []byte { func GetBytes() []byte {
@@ -356,19 +399,19 @@ func escSequence(sz *int) Event {
*sz = 3 *sz = 3
switch _buf[2] { switch _buf[2] {
case 68: case 68:
return Event{CtrlB, 0, nil} return Event{Left, 0, nil}
case 67: case 67:
return Event{CtrlF, 0, nil} return Event{Right, 0, nil}
case 66: case 66:
return Event{CtrlJ, 0, nil} return Event{Down, 0, nil}
case 65: case 65:
return Event{CtrlK, 0, nil} return Event{Up, 0, nil}
case 90: case 90:
return Event{BTab, 0, nil} return Event{BTab, 0, nil}
case 72: case 72:
return Event{CtrlA, 0, nil} return Event{Home, 0, nil}
case 70: case 70:
return Event{CtrlE, 0, nil} return Event{End, 0, nil}
case 77: case 77:
return mouseSequence(sz) return mouseSequence(sz)
case 80: case 80:
@@ -390,7 +433,7 @@ func escSequence(sz *int) Event {
case 51: case 51:
return Event{Del, 0, nil} return Event{Del, 0, nil}
case 52: case 52:
return Event{CtrlE, 0, nil} return Event{End, 0, nil}
case 53: case 53:
return Event{PgUp, 0, nil} return Event{PgUp, 0, nil}
case 54: case 54:
@@ -398,7 +441,7 @@ func escSequence(sz *int) Event {
case 49: case 49:
switch _buf[3] { switch _buf[3] {
case 126: case 126:
return Event{CtrlA, 0, nil} return Event{Home, 0, nil}
case 59: case 59:
if len(_buf) != 6 { if len(_buf) != 6 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
@@ -408,16 +451,16 @@ func escSequence(sz *int) Event {
case 50: case 50:
switch _buf[5] { switch _buf[5] {
case 68: case 68:
return Event{CtrlA, 0, nil} return Event{Home, 0, nil}
case 67: case 67:
return Event{CtrlE, 0, nil} return Event{End, 0, nil}
} }
case 53: case 53:
switch _buf[5] { switch _buf[5] {
case 68: case 68:
return Event{AltB, 0, nil} return Event{SLeft, 0, nil}
case 67: case 67:
return Event{AltF, 0, nil} return Event{SRight, 0, nil}
} }
} // _buf[4] } // _buf[4]
} // _buf[3] } // _buf[3]
@@ -444,10 +487,14 @@ func GetChar() Event {
}() }()
switch _buf[0] { switch _buf[0] {
case CtrlC, CtrlG, CtrlQ: case CtrlC:
return Event{CtrlC, 0, nil} return Event{CtrlC, 0, nil}
case CtrlG:
return Event{CtrlG, 0, nil}
case CtrlQ:
return Event{CtrlQ, 0, nil}
case 127: case 127:
return Event{CtrlH, 0, nil} return Event{BSpace, 0, nil}
case ESC: case ESC:
return escSequence(&sz) return escSequence(&sz)
} }

96
src/history.go Normal file
View File

@@ -0,0 +1,96 @@
package fzf
import (
"errors"
"io/ioutil"
"os"
"strings"
)
// History struct represents input history
type History struct {
path string
lines []string
modified map[int]string
maxSize int
cursor int
}
// NewHistory returns the pointer to a new History struct
func NewHistory(path string, maxSize int) (*History, error) {
fmtError := func(e error) error {
if os.IsPermission(e) {
return errors.New("permission denied: " + path)
}
return errors.New("invalid history file: " + e.Error())
}
// Read history file
data, err := ioutil.ReadFile(path)
if err != nil {
// If it doesn't exist, check if we can create a file with the name
if os.IsNotExist(err) {
data = []byte{}
if err := ioutil.WriteFile(path, data, 0600); err != nil {
return nil, fmtError(err)
}
} else {
return nil, fmtError(err)
}
}
// Split lines and limit the maximum number of lines
lines := strings.Split(strings.Trim(string(data), "\n"), "\n")
if len(lines[len(lines)-1]) > 0 {
lines = append(lines, "")
}
return &History{
path: path,
maxSize: maxSize,
lines: lines,
modified: make(map[int]string),
cursor: len(lines) - 1}, nil
}
func (h *History) append(line string) error {
// We don't append empty lines
if len(line) == 0 {
return nil
}
lines := append(h.lines[:len(h.lines)-1], line)
if len(lines) > h.maxSize {
lines = lines[len(lines)-h.maxSize : len(lines)]
}
h.lines = append(lines, "")
return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600)
}
func (h *History) override(str string) {
// You can update the history but they're not written to the file
if h.cursor == len(h.lines)-1 {
h.lines[h.cursor] = str
} else if h.cursor < len(h.lines)-1 {
h.modified[h.cursor] = str
}
}
func (h *History) current() string {
if str, prs := h.modified[h.cursor]; prs {
return str
}
return h.lines[h.cursor]
}
func (h *History) previous() string {
if h.cursor > 0 {
h.cursor--
}
return h.current()
}
func (h *History) next() string {
if h.cursor < len(h.lines)-1 {
h.cursor++
}
return h.current()
}

59
src/history_test.go Normal file
View File

@@ -0,0 +1,59 @@
package fzf
import (
"os/user"
"testing"
)
func TestHistory(t *testing.T) {
maxHistory := 50
// Invalid arguments
user, _ := user.Current()
paths := []string{"/etc", "/proc"}
if user.Name != "root" {
paths = append(paths, "/etc/sudoers")
}
for _, path := range paths {
if _, e := NewHistory(path, maxHistory); e == nil {
t.Error("Error expected for: " + path)
}
}
{ // Append lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory)
for i := 0; i < maxHistory+10; i++ {
h.append("foobar")
}
}
{ // Read lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory)
if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
}
for i := 0; i < maxHistory; i++ {
if h.lines[i] != "foobar" {
t.Error("Expected: foobar, actual: " + h.lines[i])
}
}
}
{ // Append lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory)
h.append("barfoo")
h.append("")
h.append("foobarbaz")
}
{ // Read lines again
h, _ := NewHistory("/tmp/fzf-history", maxHistory)
if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
}
compare := func(idx int, exp string) {
if h.lines[idx] != exp {
t.Errorf("Expected: %s, actual: %s\n", exp, h.lines[idx])
}
}
compare(maxHistory-3, "foobar")
compare(maxHistory-2, "barfoo")
compare(maxHistory-1, "foobarbaz")
}
}

View File

@@ -17,9 +17,9 @@ type colorOffset struct {
// Item represents each input line // Item represents each input line
type Item struct { type Item struct {
text *string text []rune
origText *string origText *[]rune
transformed *[]Token transformed []Token
index uint32 index uint32
offsets []Offset offsets []Offset
colors []ansiOffset colors []ansiOffset
@@ -37,14 +37,14 @@ type Rank struct {
var rankTiebreak tiebreak var rankTiebreak tiebreak
// Rank calculates rank of the Item // Rank calculates rank of the Item
func (i *Item) Rank(cache bool) Rank { func (item *Item) Rank(cache bool) Rank {
if cache && (i.rank.matchlen > 0 || i.rank.tiebreak > 0) { if cache && (item.rank.matchlen > 0 || item.rank.tiebreak > 0) {
return i.rank return item.rank
} }
matchlen := 0 matchlen := 0
prevEnd := 0 prevEnd := 0
minBegin := math.MaxUint16 minBegin := math.MaxUint16
for _, offset := range i.offsets { for _, offset := range item.offsets {
begin := int(offset[0]) begin := int(offset[0])
end := int(offset[1]) end := int(offset[1])
if prevEnd > begin { if prevEnd > begin {
@@ -63,13 +63,22 @@ func (i *Item) Rank(cache bool) Rank {
var tiebreak uint16 var tiebreak uint16
switch rankTiebreak { switch rankTiebreak {
case byLength: case byLength:
tiebreak = uint16(len(*i.text)) // It is guaranteed that .transformed in not null in normal execution
if item.transformed != nil {
lenSum := 0
for _, token := range item.transformed {
lenSum += len(token.text)
}
tiebreak = uint16(lenSum)
} else {
tiebreak = uint16(len(item.text))
}
case byBegin: case byBegin:
// We can't just look at i.offsets[0][0] because it can be an inverse term // We can't just look at item.offsets[0][0] because it can be an inverse term
tiebreak = uint16(minBegin) tiebreak = uint16(minBegin)
case byEnd: case byEnd:
if prevEnd > 0 { if prevEnd > 0 {
tiebreak = uint16(1 + len(*i.text) - prevEnd) tiebreak = uint16(1 + len(item.text) - prevEnd)
} else { } else {
// Empty offsets due to inverse terms. // Empty offsets due to inverse terms.
tiebreak = 1 tiebreak = 1
@@ -77,19 +86,30 @@ func (i *Item) Rank(cache bool) Rank {
case byIndex: case byIndex:
tiebreak = 1 tiebreak = 1
} }
rank := Rank{uint16(matchlen), tiebreak, i.index} rank := Rank{uint16(matchlen), tiebreak, item.index}
if cache { if cache {
i.rank = rank item.rank = rank
} }
return rank return rank
} }
// AsString returns the original string // AsString returns the original string
func (i *Item) AsString() string { func (item *Item) AsString(stripAnsi bool) string {
if i.origText != nil { return *item.StringPtr(stripAnsi)
return *i.origText }
// StringPtr returns the pointer to the original string
func (item *Item) StringPtr(stripAnsi bool) *string {
if item.origText != nil {
if stripAnsi {
trimmed, _, _ := extractColor(string(*item.origText), nil)
return &trimmed
}
orig := string(*item.origText)
return &orig
} }
return *i.text str := string(item.text)
return &str
} }
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset { func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {
@@ -143,13 +163,25 @@ func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset
offset: Offset{int32(start), int32(idx)}, color: color, bold: bold}) offset: Offset{int32(start), int32(idx)}, color: color, bold: bold})
} else { } else {
ansi := item.colors[curr-1] ansi := item.colors[curr-1]
fg := ansi.color.fg
if fg == -1 {
if current {
fg = curses.CurrentFG
} else {
fg = curses.FG
}
}
bg := ansi.color.bg bg := ansi.color.bg
if current && bg == -1 { if bg == -1 {
bg = int(curses.DarkBG) if current {
bg = curses.DarkBG
} else {
bg = curses.BG
}
} }
offsets = append(offsets, colorOffset{ offsets = append(offsets, colorOffset{
offset: Offset{int32(start), int32(idx)}, offset: Offset{int32(start), int32(idx)},
color: curses.PairFor(ansi.color.fg, bg), color: curses.PairFor(fg, bg),
bold: ansi.color.bold || bold}) bold: ansi.color.bold || bold})
} }
} }

View File

@@ -39,14 +39,14 @@ func TestRankComparison(t *testing.T) {
// Match length, string length, index // Match length, string length, index
func TestItemRank(t *testing.T) { func TestItemRank(t *testing.T) {
strs := []string{"foo", "foobar", "bar", "baz"} strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}} item1 := Item{text: strs[0], index: 1, offsets: []Offset{}}
rank1 := item1.Rank(true) rank1 := item1.Rank(true)
if rank1.matchlen != 0 || rank1.tiebreak != 3 || rank1.index != 1 { if rank1.matchlen != 0 || rank1.tiebreak != 3 || rank1.index != 1 {
t.Error(item1.Rank(true)) t.Error(item1.Rank(true))
} }
// Only differ in index // Only differ in index
item2 := Item{text: &strs[0], index: 0, offsets: []Offset{}} item2 := Item{text: strs[0], index: 0, offsets: []Offset{}}
items := []*Item{&item1, &item2} items := []*Item{&item1, &item2}
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
@@ -62,10 +62,10 @@ func TestItemRank(t *testing.T) {
} }
// Sort by relevance // Sort by relevance
item3 := Item{text: &strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} item3 := Item{text: strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item4 := Item{text: &strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} item4 := Item{text: strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
item5 := Item{text: &strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} item5 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item6 := Item{text: &strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} item6 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6} items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6}
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
if items[0] != &item2 || items[1] != &item1 || if items[0] != &item2 || items[1] != &item1 ||

View File

@@ -96,7 +96,7 @@ func (m *Matcher) Loop() {
} }
if !cancelled { if !cancelled {
if merger.Cacheable() { if merger.cacheable() {
m.mergerCache[patternString] = merger m.mergerCache[patternString] = merger
} }
merger.final = request.final merger.final = request.final

View File

@@ -82,7 +82,7 @@ func (mg *Merger) Get(idx int) *Item {
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count)) panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
} }
func (mg *Merger) Cacheable() bool { func (mg *Merger) cacheable() bool {
return mg.count < mergerCacheMax return mg.count < mergerCacheMax
} }

View File

@@ -22,7 +22,7 @@ func randItem() *Item {
offsets[idx] = Offset{sidx, eidx} offsets[idx] = Offset{sidx, eidx}
} }
return &Item{ return &Item{
text: &str, text: []rune(str),
index: rand.Uint32(), index: rand.Uint32(),
offsets: offsets} offsets: offsets}
} }

View File

@@ -1,9 +1,10 @@
package fzf package fzf
import ( import (
"fmt" "io/ioutil"
"os" "os"
"regexp" "regexp"
"strconv"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@@ -14,7 +15,7 @@ import (
const usage = `usage: fzf [options] const usage = `usage: fzf [options]
Search mode Search
-x, --extended Extended-search mode -x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match) -e, --extended-exact Extended-search mode (exact match)
-i Case-insensitive match (default: smart-case match) -i Case-insensitive match (default: smart-case match)
@@ -22,35 +23,39 @@ const usage = `usage: fzf [options]
-n, --nth=N[,..] Comma-separated list of field index expressions -n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END]) integer or a range expression ([BEGIN]..[END])
--with-nth=N[,..] Transform the item using index expressions for search --with-nth=N[,..] Transform item using index expressions within finder
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result
+s, --no-sort Do not sort the result +s, --no-sort Do not sort the result
--tac Reverse the order of the input --tac Reverse the order of the input
--tiebreak=CRI Sort criterion when the scores are tied; --tiebreak=CRITERION Sort criterion when the scores are tied;
[length|begin|end|index] (default: length) [length|begin|end|index] (default: length)
Interface Interface
-m, --multi Enable multi-select with tab/shift-tab -m, --multi Enable multi-select with tab/shift-tab
--ansi Enable processing of ANSI color codes --ansi Enable processing of ANSI color codes
--no-mouse Disable mouse --no-mouse Disable mouse
--color=COL Color scheme; [dark|light|16|bw] --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
(default: dark on 256-color terminal, otherwise 16) --black Use black background
--black Use black background --reverse Reverse orientation
--reverse Reverse orientation --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--no-hscroll Disable horizontal scroll --cycle Enable cyclic scroll
--prompt=STR Input prompt (default: '> ') --no-hscroll Disable horizontal scroll
--inline-info Display finder info inline with the query
--prompt=STR Input prompt (default: '> ')
--bind=KEYBINDS Custom key bindings. Refer to the man page.
--history=FILE History file
--history-size=N Maximum number of history entries (default: 1000)
--header-file=FILE The file whose content to be printed as header
--header-lines=N The first N lines of the input are treated as header
Scripting Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
-1, --select-1 Automatically select the only match -1, --select-1 Automatically select the only match
-0, --exit-0 Exit immediately when there's no match -0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder. -f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line --print-query Print query as the first line
--expect=KEYS Comma-separated list of keys to complete fzf --expect=KEYS Comma-separated list of keys to complete fzf
--toggle-sort=KEY Key to toggle sort --sync Synchronous search for multi-staged filtering
--sync Synchronous search for multi-staged filtering
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
@@ -88,69 +93,91 @@ const (
byIndex byIndex
) )
func defaultMargin() [4]string {
return [4]string{"0", "0", "0", "0"}
}
// Options stores the values of command-line options // Options stores the values of command-line options
type Options struct { type Options struct {
Mode Mode Mode Mode
Case Case Case Case
Nth []Range Nth []Range
WithNth []Range WithNth []Range
Delimiter *regexp.Regexp Delimiter Delimiter
Sort int Sort int
Tac bool Tac bool
Tiebreak tiebreak Tiebreak tiebreak
Multi bool Multi bool
Ansi bool Ansi bool
Mouse bool Mouse bool
Theme *curses.ColorTheme Theme *curses.ColorTheme
Black bool Black bool
Reverse bool Reverse bool
Hscroll bool Cycle bool
Prompt string Hscroll bool
Query string InlineInfo bool
Select1 bool Prompt string
Exit0 bool Query string
Filter *string Select1 bool
ToggleSort int Exit0 bool
Expect []int Filter *string
PrintQuery bool ToggleSort bool
Sync bool Expect map[int]string
Version bool Keymap map[int]actionType
Execmap map[int]string
PrintQuery bool
ReadZero bool
Sync bool
History *History
Header []string
HeaderLines int
Margin [4]string
Version bool
}
func defaultTheme() *curses.ColorTheme {
if strings.Contains(os.Getenv("TERM"), "256") {
return curses.Dark256
}
return curses.Default16
} }
func defaultOptions() *Options { func defaultOptions() *Options {
var defaultTheme *curses.ColorTheme
if strings.Contains(os.Getenv("TERM"), "256") {
defaultTheme = curses.Dark256
} else {
defaultTheme = curses.Default16
}
return &Options{ return &Options{
Mode: ModeFuzzy, Mode: ModeFuzzy,
Case: CaseSmart, Case: CaseSmart,
Nth: make([]Range, 0), Nth: make([]Range, 0),
WithNth: make([]Range, 0), WithNth: make([]Range, 0),
Delimiter: nil, Delimiter: Delimiter{},
Sort: 1000, Sort: 1000,
Tac: false, Tac: false,
Tiebreak: byLength, Tiebreak: byLength,
Multi: false, Multi: false,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
Theme: defaultTheme, Theme: defaultTheme(),
Black: false, Black: false,
Reverse: false, Reverse: false,
Hscroll: true, Cycle: false,
Prompt: "> ", Hscroll: true,
Query: "", InlineInfo: false,
Select1: false, Prompt: "> ",
Exit0: false, Query: "",
Filter: nil, Select1: false,
ToggleSort: 0, Exit0: false,
Expect: []int{}, Filter: nil,
PrintQuery: false, ToggleSort: false,
Sync: false, Expect: make(map[int]string),
Version: false} Keymap: defaultKeymap(),
Execmap: make(map[int]string),
PrintQuery: false,
ReadZero: false,
Sync: false,
History: nil,
Header: make([]string, 0),
HeaderLines: 0,
Margin: defaultMargin(),
Version: false}
} }
func help(ok int) { func help(ok int) {
@@ -160,14 +187,14 @@ func help(ok int) {
func errorExit(msg string) { func errorExit(msg string) {
os.Stderr.WriteString(msg + "\n") os.Stderr.WriteString(msg + "\n")
help(1) os.Exit(1)
} }
func optString(arg string, prefix string) (bool, string) { func optString(arg string, prefixes ...string) (bool, string) {
rx, _ := regexp.Compile(fmt.Sprintf("^(?:%s)(.*)$", prefix)) for _, prefix := range prefixes {
matches := rx.FindStringSubmatch(arg) if strings.HasPrefix(arg, prefix) {
if len(matches) > 1 { return true, arg[len(prefix):]
return true, matches[1] }
} }
return false, "" return false, ""
} }
@@ -181,6 +208,39 @@ func nextString(args []string, i *int, message string) string {
return args[*i] return args[*i]
} }
func optionalNextString(args []string, i *int) string {
if len(args) > *i+1 {
*i++
return args[*i]
}
return ""
}
func atoi(str string) int {
num, err := strconv.Atoi(str)
if err != nil {
errorExit("not a valid integer: " + str)
}
return num
}
func atof(str string) float64 {
num, err := strconv.ParseFloat(str, 64)
if err != nil {
errorExit("not a valid number: " + str)
}
return num
}
func nextInt(args []string, i *int, message string) int {
if len(args) > *i+1 {
*i++
} else {
errorExit(message)
}
return atoi(args[*i])
}
func optionalNumeric(args []string, i *int) int { func optionalNumeric(args []string, i *int) int {
if len(args) > *i+1 { if len(args) > *i+1 {
if strings.IndexAny(args[*i+1], "0123456789") == 0 { if strings.IndexAny(args[*i+1], "0123456789") == 0 {
@@ -207,24 +267,30 @@ func splitNth(str string) []Range {
return ranges return ranges
} }
func delimiterRegexp(str string) *regexp.Regexp { func delimiterRegexp(str string) Delimiter {
rx, e := regexp.Compile(str) // Special handling of \t
if e != nil { str = strings.Replace(str, "\\t", "\t", -1)
str = regexp.QuoteMeta(str)
// 1. Pattern does not contain any special character
if regexp.QuoteMeta(str) == str {
return Delimiter{str: &str}
} }
rx, e = regexp.Compile(fmt.Sprintf("(?:.*?%s)|(?:.+?$)", str)) rx, e := regexp.Compile(str)
// 2. Pattern is not a valid regular expression
if e != nil { if e != nil {
errorExit("invalid regular expression: " + e.Error()) return Delimiter{str: &str}
} }
return rx
// 3. Pattern as regular expression. Slow.
return Delimiter{regex: rx}
} }
func isAlphabet(char uint8) bool { func isAlphabet(char uint8) bool {
return char >= 'a' && char <= 'z' return char >= 'a' && char <= 'z'
} }
func parseKeyChords(str string, message string) []int { func parseKeyChords(str string, message string) map[int]string {
if len(str) == 0 { if len(str) == 0 {
errorExit(message) errorExit(message)
} }
@@ -234,22 +300,65 @@ func parseKeyChords(str string, message string) []int {
tokens = append(tokens, ",") tokens = append(tokens, ",")
} }
var chords []int chords := make(map[int]string)
for _, key := range tokens { for _, key := range tokens {
if len(key) == 0 { if len(key) == 0 {
continue // ignore continue // ignore
} }
lkey := strings.ToLower(key) lkey := strings.ToLower(key)
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { chord := 0
chords = append(chords, curses.CtrlA+int(lkey[5])-'a') switch lkey {
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { case "up":
chords = append(chords, curses.AltA+int(lkey[4])-'a') chord = curses.Up
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' { case "down":
chords = append(chords, curses.F1+int(key[1])-'1') chord = curses.Down
} else if utf8.RuneCountInString(key) == 1 { case "left":
chords = append(chords, curses.AltZ+int([]rune(key)[0])) chord = curses.Left
} else { case "right":
errorExit("unsupported key: " + key) chord = curses.Right
case "enter", "return":
chord = curses.CtrlM
case "space":
chord = curses.AltZ + int(' ')
case "bspace", "bs":
chord = curses.BSpace
case "alt-bs", "alt-bspace":
chord = curses.AltBS
case "tab":
chord = curses.Tab
case "btab", "shift-tab":
chord = curses.BTab
case "esc":
chord = curses.ESC
case "del":
chord = curses.Del
case "home":
chord = curses.Home
case "end":
chord = curses.End
case "pgup", "page-up":
chord = curses.PgUp
case "pgdn", "page-down":
chord = curses.PgDn
case "shift-left":
chord = curses.SLeft
case "shift-right":
chord = curses.SRight
default:
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chord = curses.CtrlA + int(lkey[5]) - 'a'
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
chord = curses.AltA + int(lkey[4]) - 'a'
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' {
chord = curses.F1 + int(key[1]) - '1'
} else if utf8.RuneCountInString(key) == 1 {
chord = curses.AltZ + int([]rune(key)[0])
} else {
errorExit("unsupported key: " + key)
}
}
if chord > 0 {
chords[chord] = key
} }
} }
return chords return chords
@@ -271,31 +380,304 @@ func parseTiebreak(str string) tiebreak {
return byLength return byLength
} }
func parseTheme(str string) *curses.ColorTheme { func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
switch strings.ToLower(str) { dupe := *theme
case "dark": return &dupe
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 checkToggleSort(str string) int { func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme {
theme := dupeTheme(defaultTheme)
for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str {
case "dark":
theme = dupeTheme(curses.Dark256)
case "light":
theme = dupeTheme(curses.Light256)
case "16":
theme = dupeTheme(curses.Default16)
case "bw", "no":
theme = nil
default:
fail := func() {
errorExit("invalid color specification: " + str)
}
// Color is disabled
if theme == nil {
errorExit("colors disabled; cannot customize colors")
}
pair := strings.Split(str, ":")
if len(pair) != 2 {
fail()
}
ansi32, err := strconv.Atoi(pair[1])
if err != nil || ansi32 < -1 || ansi32 > 255 {
fail()
}
ansi := int16(ansi32)
switch pair[0] {
case "fg":
theme.Fg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "bg":
theme.Bg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "fg+":
theme.Current = ansi
case "bg+":
theme.DarkBg = ansi
case "hl":
theme.Match = ansi
case "hl+":
theme.CurrentMatch = ansi
case "prompt":
theme.Prompt = ansi
case "spinner":
theme.Spinner = ansi
case "info":
theme.Info = ansi
case "pointer":
theme.Cursor = ansi
case "marker":
theme.Selected = ansi
case "header":
theme.Header = ansi
default:
fail()
}
}
}
return theme
}
var executeRegexp *regexp.Regexp
func firstKey(keymap map[int]string) int {
for k := range keymap {
return k
}
return 0
}
const (
escapedColon = 0
escapedComma = 1
)
func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) {
if executeRegexp == nil {
// Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile(
"(?s):execute:.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
}
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
return ":execute(" + strings.Repeat(" ", len(src)-10) + ")"
})
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
idx := 0
for _, pairStr := range strings.Split(masked, ",") {
origPairStr := str[idx : idx+len(pairStr)]
idx += len(pairStr) + 1
pair := strings.SplitN(pairStr, ":", 2)
if len(pair) < 2 {
errorExit("bind action not specified: " + origPairStr)
}
var key int
if len(pair[0]) == 1 && pair[0][0] == escapedColon {
key = ':' + curses.AltZ
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
key = ',' + curses.AltZ
} else {
keys := parseKeyChords(pair[0], "key name required")
key = firstKey(keys)
}
act := origPairStr[len(pair[0])+1 : len(origPairStr)]
actLower := strings.ToLower(act)
switch actLower {
case "ignore":
keymap[key] = actIgnore
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 "delete-char/eof":
keymap[key] = actDeleteCharEOF
case "end-of-line":
keymap[key] = actEndOfLine
case "cancel":
keymap[key] = actCancel
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-all":
keymap[key] = actToggleAll
case "select-all":
keymap[key] = actSelectAll
case "deselect-all":
keymap[key] = actDeselectAll
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 "previous-history":
keymap[key] = actPreviousHistory
case "next-history":
keymap[key] = actNextHistory
case "toggle-sort":
keymap[key] = actToggleSort
toggleSort = true
default:
if isExecuteAction(actLower) {
keymap[key] = actExecute
if act[7] == ':' {
execmap[key] = act[8:]
} else {
execmap[key] = act[8 : len(act)-1]
}
} else {
errorExit("unknown action: " + act)
}
}
}
return keymap, execmap, toggleSort
}
func isExecuteAction(str string) bool {
if !strings.HasPrefix(str, "execute") || len(str) < 9 {
return false
}
b := str[7]
e := str[len(str)-1]
if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' ||
b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") {
return true
}
return false
}
func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
keys := parseKeyChords(str, "key name required") keys := parseKeyChords(str, "key name required")
if len(keys) != 1 { if len(keys) != 1 {
errorExit("multiple keys specified") errorExit("multiple keys specified")
} }
return keys[0] keymap[firstKey(keys)] = actToggleSort
return keymap
}
func readHeaderFile(filename string) []string {
content, err := ioutil.ReadFile(filename)
if err != nil {
errorExit("failed to read header file: " + filename)
}
return strings.Split(strings.TrimSuffix(string(content), "\n"), "\n")
}
func parseMargin(margin string) [4]string {
margins := strings.Split(margin, ",")
checked := func(str string) string {
if strings.HasSuffix(str, "%") {
val := atof(str[:len(str)-1])
if val < 0 {
errorExit("margin must be non-negative")
}
if val > 100 {
errorExit("margin too large")
}
} else {
val := atoi(str)
if val < 0 {
errorExit("margin must be non-negative")
}
}
return str
}
switch len(margins) {
case 1:
m := checked(margins[0])
return [4]string{m, m, m, m}
case 2:
tb := checked(margins[0])
rl := checked(margins[1])
return [4]string{tb, rl, tb, rl}
case 3:
t := checked(margins[0])
rl := checked(margins[1])
b := checked(margins[2])
return [4]string{t, rl, b, rl}
case 4:
return [4]string{
checked(margins[0]), checked(margins[1]),
checked(margins[2]), checked(margins[3])}
default:
errorExit("invalid margin: " + margin)
}
return defaultMargin()
} }
func parseOptions(opts *Options, allArgs []string) { func parseOptions(opts *Options, allArgs []string) {
keymap := make(map[int]actionType)
var historyMax int
if opts.History == nil {
historyMax = defaultHistoryMax
} else {
historyMax = opts.History.maxSize
}
setHistory := func(path string) {
h, e := NewHistory(path, historyMax)
if e != nil {
errorExit(e.Error())
}
opts.History = h
}
setHistoryMax := func(max int) {
historyMax = max
if historyMax < 1 {
errorExit("history max must be a positive integer")
}
if opts.History != nil {
opts.History.maxSize = historyMax
}
}
for i := 0; i < len(allArgs); i++ { for i := 0; i < len(allArgs); i++ {
arg := allArgs[i] arg := allArgs[i]
switch arg { switch arg {
@@ -316,10 +698,19 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
case "--tiebreak": case "--tiebreak":
opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind":
keymap, opts.Execmap, opts.ToggleSort =
parseKeymap(keymap, opts.Execmap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required"))
case "--color": case "--color":
opts.Theme = parseTheme(nextString(allArgs, &i, "color scheme name required")) spec := optionalNextString(allArgs, &i)
if len(spec) == 0 {
opts.Theme = defaultTheme()
} else {
opts.Theme = parseTheme(opts.Theme, spec)
}
case "--toggle-sort": case "--toggle-sort":
opts.ToggleSort = checkToggleSort(nextString(allArgs, &i, "key name required")) keymap = checkToggleSort(keymap, nextString(allArgs, &i, "key name required"))
opts.ToggleSort = true
case "-d", "--delimiter": case "-d", "--delimiter":
opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required"))
case "-n", "--nth": case "-n", "--nth":
@@ -360,10 +751,18 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Reverse = true opts.Reverse = true
case "--no-reverse": case "--no-reverse":
opts.Reverse = false opts.Reverse = false
case "--cycle":
opts.Cycle = true
case "--no-cycle":
opts.Cycle = false
case "--hscroll": case "--hscroll":
opts.Hscroll = true opts.Hscroll = true
case "--no-hscroll": case "--no-hscroll":
opts.Hscroll = false opts.Hscroll = false
case "--inline-info":
opts.InlineInfo = true
case "--no-inline-info":
opts.InlineInfo = false
case "-1", "--select-1": case "-1", "--select-1":
opts.Select1 = true opts.Select1 = true
case "+1", "--no-select-1": case "+1", "--no-select-1":
@@ -372,6 +771,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Exit0 = true opts.Exit0 = true
case "+0", "--no-exit-0": case "+0", "--no-exit-0":
opts.Exit0 = false opts.Exit0 = false
case "--read0":
opts.ReadZero = true
case "--no-read0":
opts.ReadZero = false
case "--print-query": case "--print-query":
opts.PrintQuery = true opts.PrintQuery = true
case "--no-print-query": case "--no-print-query":
@@ -384,37 +787,95 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Sync = false opts.Sync = false
case "--async": case "--async":
opts.Sync = false opts.Sync = false
case "--no-history":
opts.History = nil
case "--history":
setHistory(nextString(allArgs, &i, "history file path required"))
case "--history-size":
setHistoryMax(nextInt(allArgs, &i, "history max size required"))
case "--no-header-file":
opts.Header = []string{}
case "--no-header-lines":
opts.HeaderLines = 0
case "--header-file":
opts.Header = readHeaderFile(
nextString(allArgs, &i, "header file name required"))
opts.HeaderLines = 0
case "--header-lines":
opts.Header = []string{}
opts.HeaderLines = atoi(
nextString(allArgs, &i, "number of header lines required"))
case "--no-margin":
opts.Margin = defaultMargin()
case "--margin":
opts.Margin = parseMargin(
nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
case "--version": case "--version":
opts.Version = true opts.Version = true
default: default:
if match, value := optString(arg, "-q|--query="); match { if match, value := optString(arg, "-q", "--query="); match {
opts.Query = value opts.Query = value
} else if match, value := optString(arg, "-f|--filter="); match { } else if match, value := optString(arg, "-f", "--filter="); match {
opts.Filter = &value opts.Filter = &value
} else if match, value := optString(arg, "-d|--delimiter="); match { } else if match, value := optString(arg, "-d", "--delimiter="); match {
opts.Delimiter = delimiterRegexp(value) opts.Delimiter = delimiterRegexp(value)
} else if match, value := optString(arg, "--prompt="); match { } else if match, value := optString(arg, "--prompt="); match {
opts.Prompt = value opts.Prompt = value
} else if match, value := optString(arg, "-n|--nth="); match { } else if match, value := optString(arg, "-n", "--nth="); match {
opts.Nth = splitNth(value) opts.Nth = splitNth(value)
} else if match, value := optString(arg, "--with-nth="); match { } else if match, value := optString(arg, "--with-nth="); match {
opts.WithNth = splitNth(value) opts.WithNth = splitNth(value)
} else if match, _ := optString(arg, "-s|--sort="); match { } else if match, _ := optString(arg, "-s", "--sort="); match {
opts.Sort = 1 // Don't care opts.Sort = 1 // Don't care
} else if match, value := optString(arg, "--toggle-sort="); match { } else if match, value := optString(arg, "--toggle-sort="); match {
opts.ToggleSort = checkToggleSort(value) keymap = checkToggleSort(keymap, value)
opts.ToggleSort = true
} else if match, value := optString(arg, "--expect="); match { } else if match, value := optString(arg, "--expect="); match {
opts.Expect = parseKeyChords(value, "key names required") opts.Expect = parseKeyChords(value, "key names required")
} else if match, value := optString(arg, "--tiebreak="); match { } else if match, value := optString(arg, "--tiebreak="); match {
opts.Tiebreak = parseTiebreak(value) opts.Tiebreak = parseTiebreak(value)
} else if match, value := optString(arg, "--color="); match { } else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(value) opts.Theme = parseTheme(opts.Theme, value)
} else if match, value := optString(arg, "--bind="); match {
keymap, opts.Execmap, opts.ToggleSort =
parseKeymap(keymap, opts.Execmap, opts.ToggleSort, value)
} else if match, value := optString(arg, "--history="); match {
setHistory(value)
} else if match, value := optString(arg, "--history-size="); match {
setHistoryMax(atoi(value))
} else if match, value := optString(arg, "--header-file="); match {
opts.Header = readHeaderFile(value)
opts.HeaderLines = 0
} else if match, value := optString(arg, "--header-lines="); match {
opts.Header = []string{}
opts.HeaderLines = atoi(value)
} else if match, value := optString(arg, "--margin="); match {
opts.Margin = parseMargin(value)
} else { } else {
errorExit("unknown option: " + arg) errorExit("unknown option: " + arg)
} }
} }
} }
if opts.HeaderLines < 0 {
errorExit("header lines must be a non-negative integer")
}
// Change default actions for CTRL-N / CTRL-P when --history is used
if opts.History != nil {
if _, prs := keymap[curses.CtrlP]; !prs {
keymap[curses.CtrlP] = actPreviousHistory
}
if _, prs := keymap[curses.CtrlN]; !prs {
keymap[curses.CtrlN] = actNextHistory
}
}
// Override default key bindings
for key, act := range keymap {
opts.Keymap[key] = act
}
// If we're not using extended search mode, --nth option becomes irrelevant // If we're not using extended search mode, --nth option becomes irrelevant
// if it contains the whole range // if it contains the whole range
if opts.Mode == ModeFuzzy || len(opts.Nth) == 1 { if opts.Mode == ModeFuzzy || len(opts.Nth) == 1 {

View File

@@ -1,17 +1,66 @@
package fzf package fzf
import ( import (
"fmt"
"testing" "testing"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/curses"
) )
func TestDelimiterRegex(t *testing.T) { func TestDelimiterRegex(t *testing.T) {
rx := delimiterRegexp("*") // Valid regex
tokens := rx.FindAllString("-*--*---**---", -1) delim := delimiterRegexp(".")
if tokens[0] != "-*" || tokens[1] != "--*" || tokens[2] != "---*" || if delim.regex == nil || delim.str != nil {
tokens[3] != "*" || tokens[4] != "---" { t.Error(delim)
t.Errorf("%s %s %d", rx, tokens, len(tokens)) }
// Broken regex -> string
delim = delimiterRegexp("[0-9")
if delim.regex != nil || *delim.str != "[0-9" {
t.Error(delim)
}
// Valid regex
delim = delimiterRegexp("[0-9]")
if delim.regex.String() != "[0-9]" || delim.str != nil {
t.Error(delim)
}
// Tab character
delim = delimiterRegexp("\t")
if delim.regex != nil || *delim.str != "\t" {
t.Error(delim)
}
// Tab expression
delim = delimiterRegexp("\\t")
if delim.regex != nil || *delim.str != "\t" {
t.Error(delim)
}
// Tabs -> regex
delim = delimiterRegexp("\t+")
if delim.regex == nil || delim.str != nil {
t.Error(delim)
}
}
func TestDelimiterRegexString(t *testing.T) {
delim := delimiterRegexp("*")
tokens := Tokenize([]rune("-*--*---**---"), delim)
if delim.regex != nil ||
string(tokens[0].text) != "-*" ||
string(tokens[1].text) != "--*" ||
string(tokens[2].text) != "---*" ||
string(tokens[3].text) != "*" ||
string(tokens[4].text) != "---" {
t.Errorf("%s %s %d", delim, tokens, len(tokens))
}
}
func TestDelimiterRegexRegex(t *testing.T) {
delim := delimiterRegexp("--\\*")
tokens := Tokenize([]rune("-*--*---**---"), delim)
if delim.str != nil ||
string(tokens[0].text) != "-*--*" ||
string(tokens[1].text) != "---*" ||
string(tokens[2].text) != "*---" {
t.Errorf("%s %d", tokens, len(tokens))
} }
} }
@@ -71,61 +120,199 @@ func TestIrrelevantNth(t *testing.T) {
} }
func TestParseKeys(t *testing.T) { func TestParseKeys(t *testing.T) {
keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "") pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "")
check := func(key int, expected int) { check := func(i int, s string) {
if key != expected { if pairs[i] != s {
t.Errorf("%d != %d", key, expected) t.Errorf("%s != %s", pairs[i], s)
} }
} }
check(len(keys), 9) if len(pairs) != 9 {
check(keys[0], curses.CtrlZ) t.Error(9)
check(keys[1], curses.AltZ) }
check(keys[2], curses.F2) check(curses.CtrlZ, "ctrl-z")
check(keys[3], curses.AltZ+'@') check(curses.AltZ, "alt-z")
check(keys[4], curses.AltA) check(curses.F2, "f2")
check(keys[5], curses.AltZ+'!') check(curses.AltZ+'@', "@")
check(keys[6], curses.CtrlA+'g'-'a') check(curses.AltA, "Alt-a")
check(keys[7], curses.AltZ+'J') check(curses.AltZ+'!', "!")
check(keys[8], curses.AltZ+'g') check(curses.CtrlA+'g'-'a', "ctrl-G")
check(curses.AltZ+'J', "J")
check(curses.AltZ+'g', "g")
// Synonyms
pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
if len(pairs) != 9 {
t.Error(9)
}
check(curses.CtrlM, "Return")
check(curses.AltZ+' ', "space")
check(curses.Tab, "tab")
check(curses.BTab, "btab")
check(curses.ESC, "esc")
check(curses.Up, "up")
check(curses.Down, "down")
check(curses.Left, "left")
check(curses.Right, "right")
pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
if len(pairs) != 11 {
t.Error(11)
}
check(curses.Tab, "Ctrl-I")
check(curses.PgUp, "page-up")
check(curses.PgDn, "Page-Down")
check(curses.Home, "Home")
check(curses.End, "End")
check(curses.AltBS, "Alt-BSpace")
check(curses.SLeft, "shift-left")
check(curses.SRight, "shift-right")
check(curses.BTab, "shift-tab")
check(curses.CtrlM, "Enter")
check(curses.BSpace, "bspace")
} }
func TestParseKeysWithComma(t *testing.T) { func TestParseKeysWithComma(t *testing.T) {
check := func(key int, expected int) { checkN := func(a int, b int) {
if key != expected { if a != b {
t.Errorf("%d != %d", key, expected) t.Errorf("%d != %d", a, b)
}
}
check := func(pairs map[int]string, i int, s string) {
if pairs[i] != s {
t.Errorf("%s != %s", pairs[i], s)
} }
} }
keys := parseKeyChords(",", "") pairs := parseKeyChords(",", "")
check(len(keys), 1) checkN(len(pairs), 1)
check(keys[0], curses.AltZ+',') check(pairs, curses.AltZ+',', ",")
keys = parseKeyChords(",,a,b", "") pairs = parseKeyChords(",,a,b", "")
check(len(keys), 3) checkN(len(pairs), 3)
check(keys[0], curses.AltZ+'a') check(pairs, curses.AltZ+'a', "a")
check(keys[1], curses.AltZ+'b') check(pairs, curses.AltZ+'b', "b")
check(keys[2], curses.AltZ+',') check(pairs, curses.AltZ+',', ",")
keys = parseKeyChords("a,b,,", "") pairs = parseKeyChords("a,b,,", "")
check(len(keys), 3) checkN(len(pairs), 3)
check(keys[0], curses.AltZ+'a') check(pairs, curses.AltZ+'a', "a")
check(keys[1], curses.AltZ+'b') check(pairs, curses.AltZ+'b', "b")
check(keys[2], curses.AltZ+',') check(pairs, curses.AltZ+',', ",")
keys = parseKeyChords("a,,,b", "") pairs = parseKeyChords("a,,,b", "")
check(len(keys), 3) checkN(len(pairs), 3)
check(keys[0], curses.AltZ+'a') check(pairs, curses.AltZ+'a', "a")
check(keys[1], curses.AltZ+'b') check(pairs, curses.AltZ+'b', "b")
check(keys[2], curses.AltZ+',') check(pairs, curses.AltZ+',', ",")
keys = parseKeyChords("a,,,b,c", "") pairs = parseKeyChords("a,,,b,c", "")
check(len(keys), 4) checkN(len(pairs), 4)
check(keys[0], curses.AltZ+'a') check(pairs, curses.AltZ+'a', "a")
check(keys[1], curses.AltZ+'b') check(pairs, curses.AltZ+'b', "b")
check(keys[2], curses.AltZ+'c') check(pairs, curses.AltZ+'c', "c")
check(keys[3], curses.AltZ+',') check(pairs, curses.AltZ+',', ",")
keys = parseKeyChords(",,,", "") pairs = parseKeyChords(",,,", "")
check(len(keys), 1) checkN(len(pairs), 1)
check(keys[0], curses.AltZ+',') check(pairs, curses.AltZ+',', ",")
}
func TestBind(t *testing.T) {
check := func(action actionType, expected actionType) {
if action != expected {
t.Errorf("%d != %d", action, expected)
}
}
checkString := func(action string, expected string) {
if action != expected {
t.Errorf("%d != %d", action, expected)
}
}
keymap := defaultKeymap()
execmap := make(map[int]string)
check(actBeginningOfLine, keymap[curses.CtrlA])
keymap, execmap, toggleSort :=
parseKeymap(keymap, execmap, false,
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+
"f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+
",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)")
if !toggleSort {
t.Errorf("toggleSort not set")
}
check(actKillLine, keymap[curses.CtrlA])
check(actToggleSort, keymap[curses.CtrlB])
check(actPageUp, keymap[curses.AltZ+'c'])
check(actAbort, keymap[curses.AltZ+','])
check(actAccept, keymap[curses.AltZ+':'])
check(actPageDown, keymap[curses.AltZ])
check(actExecute, keymap[curses.F1])
check(actExecute, keymap[curses.F2])
check(actExecute, keymap[curses.F3])
check(actExecute, keymap[curses.F4])
checkString("ls {}", execmap[curses.F1])
checkString("echo {}, {}, {}", execmap[curses.F2])
checkString("echo '({})'", execmap[curses.F3])
checkString("less {}", execmap[curses.F4])
checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA])
checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB])
checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X'])
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
keymap, execmap, toggleSort =
parseKeymap(keymap, execmap, false, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])])
}
keymap, execmap, toggleSort = parseKeymap(keymap, execmap, false, "f1:abort")
if toggleSort {
t.Errorf("toggleSort set")
}
check(actAbort, keymap[curses.F1])
}
func TestColorSpec(t *testing.T) {
theme := curses.Dark256
dark := parseTheme(theme, "dark")
if *dark != *theme {
t.Errorf("colors should be equivalent")
}
if dark == theme {
t.Errorf("point should not be equivalent")
}
light := parseTheme(theme, "dark,light")
if *light == *theme {
t.Errorf("should not be equivalent")
}
if *light != *curses.Light256 {
t.Errorf("colors should be equivalent")
}
if light == theme {
t.Errorf("point should not be equivalent")
}
customized := parseTheme(theme, "fg:231,bg:232")
if customized.Fg != 231 || customized.Bg != 232 {
t.Errorf("color not customized")
}
if *curses.Dark256 == *customized {
t.Errorf("colors should not be equivalent")
}
customized.Fg = curses.Dark256.Fg
customized.Bg = curses.Dark256.Bg
if *curses.Dark256 == *customized {
t.Errorf("colors should now be equivalent")
}
customized = parseTheme(theme, "fg:231,dark,bg:232")
if customized.Fg != curses.Dark256.Fg || customized.Bg == curses.Dark256.Bg {
t.Errorf("color not customized")
}
if customized.UseDefault {
t.Errorf("not using default colors")
}
if !curses.Dark256.UseDefault {
t.Errorf("using default colors")
}
} }

View File

@@ -4,7 +4,6 @@ import (
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"unicode"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
) )
@@ -25,25 +24,28 @@ const (
termExact termExact
termPrefix termPrefix
termSuffix termSuffix
termEqual
) )
type term struct { type term struct {
typ termType typ termType
inv bool inv bool
text []rune text []rune
origText []rune caseSensitive bool
origText []rune
} }
// Pattern represents search pattern // Pattern represents search pattern
type Pattern struct { type Pattern struct {
mode Mode mode Mode
caseSensitive bool caseSensitive bool
forward bool
text []rune text []rune
terms []term terms []term
hasInvTerm bool hasInvTerm bool
delimiter *regexp.Regexp delimiter Delimiter
nth []Range nth []Range
procFun map[termType]func(bool, *[]rune, []rune) (int, int) procFun map[termType]func(bool, bool, []rune, []rune) (int, int)
} }
var ( var (
@@ -69,8 +71,8 @@ func clearChunkCache() {
} }
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(mode Mode, caseMode Case, func BuildPattern(mode Mode, caseMode Case, forward bool,
nth []Range, delimiter *regexp.Regexp, runes []rune) *Pattern { nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string var asString string
switch mode { switch mode {
@@ -88,43 +90,36 @@ func BuildPattern(mode Mode, caseMode Case,
caseSensitive, hasInvTerm := true, false caseSensitive, hasInvTerm := true, false
terms := []term{} terms := []term{}
switch caseMode {
case CaseSmart:
hasUppercase := false
for _, r := range runes {
if unicode.IsUpper(r) {
hasUppercase = true
break
}
}
if !hasUppercase {
runes, caseSensitive = []rune(strings.ToLower(asString)), false
}
case CaseIgnore:
runes, caseSensitive = []rune(strings.ToLower(asString)), false
}
switch mode { switch mode {
case ModeExtended, ModeExtendedExact: case ModeExtended, ModeExtendedExact:
terms = parseTerms(mode, string(runes)) terms = parseTerms(mode, caseMode, asString)
for _, term := range terms { for _, term := range terms {
if term.inv { if term.inv {
hasInvTerm = true hasInvTerm = true
} }
} }
default:
lowerString := strings.ToLower(asString)
caseSensitive = caseMode == CaseRespect ||
caseMode == CaseSmart && lowerString != asString
if !caseSensitive {
asString = lowerString
}
} }
ptr := &Pattern{ ptr := &Pattern{
mode: mode, mode: mode,
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
text: runes, forward: forward,
text: []rune(asString),
terms: terms, terms: terms,
hasInvTerm: hasInvTerm, hasInvTerm: hasInvTerm,
nth: nth, nth: nth,
delimiter: delimiter, delimiter: delimiter,
procFun: make(map[termType]func(bool, *[]rune, []rune) (int, int))} procFun: make(map[termType]func(bool, bool, []rune, []rune) (int, int))}
ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termFuzzy] = algo.FuzzyMatch
ptr.procFun[termEqual] = algo.EqualMatch
ptr.procFun[termExact] = algo.ExactMatchNaive ptr.procFun[termExact] = algo.ExactMatchNaive
ptr.procFun[termPrefix] = algo.PrefixMatch ptr.procFun[termPrefix] = algo.PrefixMatch
ptr.procFun[termSuffix] = algo.SuffixMatch ptr.procFun[termSuffix] = algo.SuffixMatch
@@ -133,11 +128,17 @@ func BuildPattern(mode Mode, caseMode Case,
return ptr return ptr
} }
func parseTerms(mode Mode, str string) []term { func parseTerms(mode Mode, caseMode Case, str string) []term {
tokens := _splitRegex.Split(str, -1) tokens := _splitRegex.Split(str, -1)
terms := []term{} terms := []term{}
for _, token := range tokens { for _, token := range tokens {
typ, inv, text := termFuzzy, false, token typ, inv, text := termFuzzy, false, token
lowerText := strings.ToLower(text)
caseSensitive := caseMode == CaseRespect ||
caseMode == CaseSmart && text != lowerText
if !caseSensitive {
text = lowerText
}
origText := []rune(text) origText := []rune(text)
if mode == ModeExtendedExact { if mode == ModeExtendedExact {
typ = termExact typ = termExact
@@ -152,10 +153,18 @@ func parseTerms(mode Mode, str string) []term {
if mode == ModeExtended { if mode == ModeExtended {
typ = termExact typ = termExact
text = text[1:] text = text[1:]
} else if mode == ModeExtendedExact {
typ = termFuzzy
text = text[1:]
} }
} else if strings.HasPrefix(text, "^") { } else if strings.HasPrefix(text, "^") {
typ = termPrefix if strings.HasSuffix(text, "$") {
text = text[1:] typ = termEqual
text = text[1 : len(text)-1]
} else {
typ = termPrefix
text = text[1:]
}
} else if strings.HasSuffix(text, "$") { } else if strings.HasSuffix(text, "$") {
typ = termSuffix typ = termSuffix
text = text[:len(text)-1] text = text[:len(text)-1]
@@ -163,10 +172,11 @@ func parseTerms(mode Mode, str string) []term {
if len(text) > 0 { if len(text) > 0 {
terms = append(terms, term{ terms = append(terms, term{
typ: typ, typ: typ,
inv: inv, inv: inv,
text: []rune(text), text: []rune(text),
origText: origText}) caseSensitive: caseSensitive,
origText: origText})
} }
} }
return terms return terms
@@ -280,7 +290,7 @@ func dupItem(item *Item, offsets []Offset) *Item {
func (p *Pattern) fuzzyMatch(item *Item) (int, int) { func (p *Pattern) fuzzyMatch(item *Item) (int, int) {
input := p.prepareInput(item) input := p.prepareInput(item)
return p.iter(algo.FuzzyMatch, input, p.text) return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text)
} }
func (p *Pattern) extendedMatch(item *Item) []Offset { func (p *Pattern) extendedMatch(item *Item) []Offset {
@@ -288,7 +298,7 @@ func (p *Pattern) extendedMatch(item *Item) []Offset {
offsets := []Offset{} offsets := []Offset{}
for _, term := range p.terms { for _, term := range p.terms {
pfun := p.procFun[term.typ] pfun := p.procFun[term.typ]
if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 { if sidx, eidx := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 {
if term.inv { if term.inv {
break break
} }
@@ -300,29 +310,27 @@ func (p *Pattern) extendedMatch(item *Item) []Offset {
return offsets return offsets
} }
func (p *Pattern) prepareInput(item *Item) *[]Token { func (p *Pattern) prepareInput(item *Item) []Token {
if item.transformed != nil { if item.transformed != nil {
return item.transformed return item.transformed
} }
var ret *[]Token var ret []Token
if len(p.nth) > 0 { if len(p.nth) > 0 {
tokens := Tokenize(item.text, p.delimiter) tokens := Tokenize(item.text, p.delimiter)
ret = Transform(tokens, p.nth) ret = Transform(tokens, p.nth)
} else { } else {
runes := []rune(*item.text) ret = []Token{Token{text: item.text, prefixLength: 0}}
trans := []Token{Token{text: &runes, prefixLength: 0}}
ret = &trans
} }
item.transformed = ret item.transformed = ret
return ret return ret
} }
func (p *Pattern) iter(pfun func(bool, *[]rune, []rune) (int, int), func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) (int, int),
tokens *[]Token, pattern []rune) (int, int) { tokens []Token, caseSensitive bool, forward bool, pattern []rune) (int, int) {
for _, part := range *tokens { for _, part := range tokens {
prefixLength := part.prefixLength prefixLength := part.prefixLength
if sidx, eidx := pfun(p.caseSensitive, part.text, pattern); sidx >= 0 { if sidx, eidx := pfun(caseSensitive, forward, part.text, pattern); sidx >= 0 {
return sidx + prefixLength, eidx + prefixLength return sidx + prefixLength, eidx + prefixLength
} }
} }

View File

@@ -1,15 +1,16 @@
package fzf package fzf
import ( import (
"reflect"
"testing" "testing"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
) )
func TestParseTermsExtended(t *testing.T) { func TestParseTermsExtended(t *testing.T) {
terms := parseTerms(ModeExtended, terms := parseTerms(ModeExtended, CaseSmart,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ ^iii$")
if len(terms) != 8 || if len(terms) != 9 ||
terms[0].typ != termFuzzy || terms[0].inv || terms[0].typ != termFuzzy || terms[0].inv ||
terms[1].typ != termExact || terms[1].inv || terms[1].typ != termExact || terms[1].inv ||
terms[2].typ != termPrefix || terms[2].inv || terms[2].typ != termPrefix || terms[2].inv ||
@@ -17,7 +18,8 @@ func TestParseTermsExtended(t *testing.T) {
terms[4].typ != termFuzzy || !terms[4].inv || terms[4].typ != termFuzzy || !terms[4].inv ||
terms[5].typ != termExact || !terms[5].inv || terms[5].typ != termExact || !terms[5].inv ||
terms[6].typ != termPrefix || !terms[6].inv || terms[6].typ != termPrefix || !terms[6].inv ||
terms[7].typ != termSuffix || !terms[7].inv { terms[7].typ != termSuffix || !terms[7].inv ||
terms[8].typ != termEqual || terms[8].inv {
t.Errorf("%s", terms) t.Errorf("%s", terms)
} }
for idx, term := range terms { for idx, term := range terms {
@@ -31,15 +33,15 @@ func TestParseTermsExtended(t *testing.T) {
} }
func TestParseTermsExtendedExact(t *testing.T) { func TestParseTermsExtendedExact(t *testing.T) {
terms := parseTerms(ModeExtendedExact, terms := parseTerms(ModeExtendedExact, CaseSmart,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
if len(terms) != 8 || if len(terms) != 8 ||
terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 || terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 ||
terms[1].typ != termExact || terms[1].inv || len(terms[1].text) != 4 || terms[1].typ != termFuzzy || terms[1].inv || len(terms[1].text) != 3 ||
terms[2].typ != termPrefix || terms[2].inv || len(terms[2].text) != 3 || terms[2].typ != termPrefix || terms[2].inv || len(terms[2].text) != 3 ||
terms[3].typ != termSuffix || terms[3].inv || len(terms[3].text) != 3 || terms[3].typ != termSuffix || terms[3].inv || len(terms[3].text) != 3 ||
terms[4].typ != termExact || !terms[4].inv || len(terms[4].text) != 3 || terms[4].typ != termExact || !terms[4].inv || len(terms[4].text) != 3 ||
terms[5].typ != termExact || !terms[5].inv || len(terms[5].text) != 4 || terms[5].typ != termFuzzy || !terms[5].inv || len(terms[5].text) != 3 ||
terms[6].typ != termPrefix || !terms[6].inv || len(terms[6].text) != 3 || terms[6].typ != termPrefix || !terms[6].inv || len(terms[6].text) != 3 ||
terms[7].typ != termSuffix || !terms[7].inv || len(terms[7].text) != 3 { terms[7].typ != termSuffix || !terms[7].inv || len(terms[7].text) != 3 {
t.Errorf("%s", terms) t.Errorf("%s", terms)
@@ -47,7 +49,7 @@ func TestParseTermsExtendedExact(t *testing.T) {
} }
func TestParseTermsEmpty(t *testing.T) { func TestParseTermsEmpty(t *testing.T) {
terms := parseTerms(ModeExtended, "' $ ^ !' !^ !$") terms := parseTerms(ModeExtended, CaseSmart, "' $ ^ !' !^ !$")
if len(terms) != 0 { if len(terms) != 0 {
t.Errorf("%s", terms) t.Errorf("%s", terms)
} }
@@ -56,29 +58,45 @@ func TestParseTermsEmpty(t *testing.T) {
func TestExact(t *testing.T) { func TestExact(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pattern := BuildPattern(ModeExtended, CaseSmart, pattern := BuildPattern(ModeExtended, CaseSmart, true,
[]Range{}, nil, []rune("'abc")) []Range{}, Delimiter{}, []rune("'abc"))
runes := []rune("aabbcc abc") sidx, eidx := algo.ExactMatchNaive(
sidx, eidx := algo.ExactMatchNaive(pattern.caseSensitive, &runes, pattern.terms[0].text) pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.terms[0].text)
if sidx != 7 || eidx != 10 { if sidx != 7 || eidx != 10 {
t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
} }
} }
func TestEqual(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int, eidxExpected int) {
sidx, eidx := algo.EqualMatch(
pattern.caseSensitive, pattern.forward, []rune(str), pattern.terms[0].text)
if sidx != sidxExpected || eidx != eidxExpected {
t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
}
}
match("ABC", -1, -1)
match("AbC", 0, 3)
}
func TestCaseSensitivity(t *testing.T) { func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pat1 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, nil, []rune("abc")) pat1 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat2 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, nil, []rune("Abc")) pat2 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat3 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, nil, []rune("abc")) pat3 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat4 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, nil, []rune("Abc")) pat4 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat5 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, nil, []rune("abc")) pat5 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat6 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, nil, []rune("Abc")) pat6 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc"))
if string(pat1.text) != "abc" || pat1.caseSensitive != false || if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true || string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@@ -91,25 +109,23 @@ func TestCaseSensitivity(t *testing.T) {
} }
func TestOrigTextAndTransformed(t *testing.T) { func TestOrigTextAndTransformed(t *testing.T) {
strptr := func(str string) *string { pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg"))
return &str tokens := Tokenize([]rune("junegunn"), Delimiter{})
}
pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("jg"))
tokens := Tokenize(strptr("junegunn"), nil)
trans := Transform(tokens, []Range{Range{1, 1}}) trans := Transform(tokens, []Range{Range{1, 1}})
origRunes := []rune("junegunn.choi")
for _, mode := range []Mode{ModeFuzzy, ModeExtended} { for _, mode := range []Mode{ModeFuzzy, ModeExtended} {
chunk := Chunk{ chunk := Chunk{
&Item{ &Item{
text: strptr("junegunn"), text: []rune("junegunn"),
origText: strptr("junegunn.choi"), origText: &origRunes,
transformed: trans}, transformed: trans},
} }
pattern.mode = mode pattern.mode = mode
matches := pattern.matchChunk(&chunk) matches := pattern.matchChunk(&chunk)
if *matches[0].text != "junegunn" || *matches[0].origText != "junegunn.choi" || if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" ||
matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 ||
matches[0].transformed != trans { !reflect.DeepEqual(matches[0].transformed, trans) {
t.Error("Invalid match result", matches) t.Error("Invalid match result", matches)
} }
} }

View File

@@ -11,8 +11,9 @@ import (
// Reader reads from command or standard input // Reader reads from command or standard input
type Reader struct { type Reader struct {
pusher func(string) pusher func([]byte) bool
eventBox *util.EventBox eventBox *util.EventBox
delimNil bool
} }
// ReadSource reads data from the default command or from standard input // ReadSource reads data from the default command or from standard input
@@ -30,10 +31,25 @@ func (r *Reader) ReadSource() {
} }
func (r *Reader) feed(src io.Reader) { func (r *Reader) feed(src io.Reader) {
if scanner := bufio.NewScanner(src); scanner != nil { delim := byte('\n')
for scanner.Scan() { if r.delimNil {
r.pusher(scanner.Text()) delim = '\000'
r.eventBox.Set(EvtReadNew, nil) }
reader := bufio.NewReader(src)
for {
// ReadBytes returns err != nil if and only if the returned data does not
// end in delim.
bytea, err := reader.ReadBytes(delim)
if len(bytea) > 0 {
if err == nil {
bytea = bytea[:len(bytea)-1]
}
if r.pusher(bytea) {
r.eventBox.Set(EvtReadNew, nil)
}
}
if err != nil {
break
} }
} }
} }

View File

@@ -10,7 +10,7 @@ func TestReadFromCommand(t *testing.T) {
strs := []string{} strs := []string{}
eb := util.NewEventBox() eb := util.NewEventBox()
reader := Reader{ reader := Reader{
pusher: func(s string) { strs = append(strs, s) }, pusher: func(s []byte) bool { strs = append(strs, string(s)); return true },
eventBox: eb} eventBox: eb}
// Check EventBox // Check EventBox

View File

@@ -4,9 +4,11 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
@@ -20,6 +22,7 @@ import (
// Terminal represents terminal input/output // Terminal represents terminal input/output
type Terminal struct { type Terminal struct {
inlineInfo bool
prompt string prompt string
reverse bool reverse bool
hscroll bool hscroll bool
@@ -30,10 +33,18 @@ type Terminal struct {
input []rune input []rune
multi bool multi bool
sort bool sort bool
toggleSort int toggleSort bool
expect []int expect map[int]string
pressed int keymap map[int]actionType
execmap map[int]string
pressed string
printQuery bool printQuery bool
history *History
cycle bool
header []string
ansi bool
margin [4]string
marginInt [4]int
count int count int
progress int progress int
reading bool reading bool
@@ -72,6 +83,7 @@ var _runeWidths = make(map[rune]int)
const ( const (
reqPrompt util.EventType = iota reqPrompt util.EventType = iota
reqInfo reqInfo
reqHeader
reqList reqList
reqRefresh reqRefresh
reqRedraw reqRedraw
@@ -79,10 +91,102 @@ const (
reqQuit reqQuit
) )
type actionType int
const (
actIgnore actionType = iota
actInvalid
actRune
actMouse
actBeginningOfLine
actAbort
actAccept
actBackwardChar
actBackwardDeleteChar
actBackwardWord
actCancel
actClearScreen
actDeleteChar
actDeleteCharEOF
actEndOfLine
actForwardChar
actForwardWord
actKillLine
actKillWord
actUnixLineDiscard
actUnixWordRubout
actYank
actBackwardKillWord
actSelectAll
actDeselectAll
actToggle
actToggleAll
actToggleDown
actToggleUp
actDown
actUp
actPageUp
actPageDown
actToggleSort
actPreviousHistory
actNextHistory
actExecute
)
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] = actDeleteCharEOF
keymap[C.CtrlE] = actEndOfLine
keymap[C.CtrlF] = actForwardChar
keymap[C.CtrlH] = actBackwardDeleteChar
keymap[C.BSpace] = 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
keymap[C.PgUp] = actPageUp
keymap[C.PgDn] = actPageDown
keymap[C.Rune] = actRune
keymap[C.Mouse] = actMouse
return keymap
}
// NewTerminal returns new Terminal object // NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := []rune(opts.Query) input := []rune(opts.Query)
return &Terminal{ return &Terminal{
inlineInfo: opts.InlineInfo,
prompt: opts.Prompt, prompt: opts.Prompt,
reverse: opts.Reverse, reverse: opts.Reverse,
hscroll: opts.Hscroll, hscroll: opts.Hscroll,
@@ -95,8 +199,17 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
sort: opts.Sort > 0, sort: opts.Sort > 0,
toggleSort: opts.ToggleSort, toggleSort: opts.ToggleSort,
expect: opts.Expect, expect: opts.Expect,
pressed: 0, keymap: opts.Keymap,
execmap: opts.Execmap,
pressed: "",
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
history: opts.History,
margin: opts.Margin,
marginInt: [4]int{0, 0, 0, 0},
cycle: opts.Cycle,
header: opts.Header,
ansi: opts.Ansi,
reading: true,
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[uint32]selectedItem), selected: make(map[uint32]selectedItem),
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
@@ -128,6 +241,22 @@ func (t *Terminal) UpdateCount(cnt int, final bool) {
} }
} }
// UpdateHeader updates the header
func (t *Terminal) UpdateHeader(header []string, lines int) {
t.mutex.Lock()
t.header = make([]string, lines)
copy(t.header, header)
if !t.reverse {
reversed := make([]string, lines)
for idx, str := range t.header {
reversed[lines-idx-1] = str
}
t.header = reversed
}
t.mutex.Unlock()
t.reqBox.Set(reqHeader, nil)
}
// UpdateProgress updates the search progress // UpdateProgress updates the search progress
func (t *Terminal) UpdateProgress(progress float32) { func (t *Terminal) UpdateProgress(progress float32) {
t.mutex.Lock() t.mutex.Lock()
@@ -156,22 +285,12 @@ func (t *Terminal) output() {
fmt.Println(string(t.input)) fmt.Println(string(t.input))
} }
if len(t.expect) > 0 { if len(t.expect) > 0 {
if t.pressed == 0 { fmt.Println(t.pressed)
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 len(t.selected) == 0 {
cnt := t.merger.Length() cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy { if cnt > 0 && cnt > t.cy {
fmt.Println(t.merger.Get(t.cy).AsString()) fmt.Println(t.merger.Get(t.cy).AsString(t.ansi))
} }
} else { } else {
sels := make([]selectedItem, 0, len(t.selected)) sels := make([]selectedItem, 0, len(t.selected))
@@ -205,10 +324,50 @@ func displayWidth(runes []rune) int {
return l return l
} }
const minWidth = 16
const minHeight = 4
func (t *Terminal) calculateMargins() {
screenWidth := C.MaxX()
screenHeight := C.MaxY()
for idx, str := range t.margin {
if str == "0" {
t.marginInt[idx] = 0
} else if strings.HasSuffix(str, "%") {
num, _ := strconv.ParseFloat(str[:len(str)-1], 64)
var val float64
if idx%2 == 0 {
val = float64(screenHeight)
} else {
val = float64(screenWidth)
}
t.marginInt[idx] = int(val * num * 0.01)
} else {
num, _ := strconv.Atoi(str)
t.marginInt[idx] = num
}
}
adjust := func(idx1 int, idx2 int, max int, min int) {
if max >= min {
margin := t.marginInt[idx1] + t.marginInt[idx2]
if max-margin < min {
desired := max - min
t.marginInt[idx1] = desired * t.marginInt[idx1] / margin
t.marginInt[idx2] = desired * t.marginInt[idx2] / margin
}
}
}
adjust(1, 3, screenWidth, minWidth)
adjust(0, 2, screenHeight, minHeight)
}
func (t *Terminal) move(y int, x int, clear bool) { func (t *Terminal) move(y int, x int, clear bool) {
x += t.marginInt[3]
maxy := C.MaxY() maxy := C.MaxY()
if !t.reverse { if !t.reverse {
y = maxy - y - 1 y = maxy - y - 1 - t.marginInt[2]
} else {
y += t.marginInt[0]
} }
if clear { if clear {
@@ -229,16 +388,25 @@ func (t *Terminal) printPrompt() {
} }
func (t *Terminal) printInfo() { func (t *Terminal) printInfo() {
t.move(1, 0, true) if t.inlineInfo {
if t.reading { t.move(0, len(t.prompt)+displayWidth(t.input)+1, true)
duration := int64(spinnerDuration) if t.reading {
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration C.CPrint(C.ColSpinner, true, " < ")
C.CPrint(C.ColSpinner, true, _spinner[idx]) } 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) output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count)
if t.toggleSort > 0 { if t.toggleSort {
if t.sort { if t.sort {
output += "/S" output += "/S"
} else { } else {
@@ -254,13 +422,51 @@ func (t *Terminal) printInfo() {
C.CPrint(C.ColInfo, false, output) C.CPrint(C.ColInfo, false, output)
} }
func (t *Terminal) maxHeight() int {
return C.MaxY() - t.marginInt[0] - t.marginInt[2]
}
func (t *Terminal) printHeader() {
if len(t.header) == 0 {
return
}
max := t.maxHeight()
var state *ansiState
for idx, lineStr := range t.header {
if !t.reverse {
idx = len(t.header) - idx - 1
}
line := idx + 2
if t.inlineInfo {
line--
}
if line >= max {
continue
}
trimmed, colors, newState := extractColor(lineStr, state)
state = newState
item := &Item{
text: []rune(trimmed),
index: 0,
colors: colors,
rank: Rank{0, 0, 0}}
t.move(line, 2, true)
t.printHighlighted(item, false, C.ColHeader, 0, false)
}
}
func (t *Terminal) printList() { func (t *Terminal) printList() {
t.constrain() t.constrain()
maxy := maxItems() maxy := t.maxItems()
count := t.merger.Length() - t.offset count := t.merger.Length() - t.offset
for i := 0; i < maxy; i++ { for i := 0; i < maxy; i++ {
t.move(i+2, 0, true) line := i + 2 + len(t.header)
if t.inlineInfo {
line--
}
t.move(line, 0, true)
if i < count { if i < count {
t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset) t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset)
} }
@@ -272,7 +478,7 @@ func (t *Terminal) printItem(item *Item, current bool) {
if current { if current {
C.CPrint(C.ColCursor, true, ">") C.CPrint(C.ColCursor, true, ">")
if selected { if selected {
C.CPrint(C.ColCurrent, true, ">") C.CPrint(C.ColSelected, true, ">")
} else { } else {
C.CPrint(C.ColCurrent, true, " ") C.CPrint(C.ColCurrent, true, " ")
} }
@@ -333,9 +539,10 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
} }
// Overflow // Overflow
text := []rune(*item.text) text := make([]rune, len(item.text))
copy(text, item.text)
offsets := item.colorOffsets(col2, bold, current) offsets := item.colorOffsets(col2, bold, current)
maxWidth := C.MaxX() - 3 maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3]
fullWidth := displayWidth(text) fullWidth := displayWidth(text)
if fullWidth > maxWidth { if fullWidth > maxWidth {
if t.hscroll { if t.hscroll {
@@ -418,9 +625,11 @@ func processTabs(runes []rune, prefixWidth int) (string, int) {
} }
func (t *Terminal) printAll() { func (t *Terminal) printAll() {
t.calculateMargins()
t.printList() t.printList()
t.printInfo()
t.printPrompt() t.printPrompt()
t.printInfo()
t.printHeader()
} }
func (t *Terminal) refresh() { func (t *Terminal) refresh() {
@@ -479,16 +688,29 @@ func keyMatch(key int, event C.Event) bool {
return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ
} }
func executeCommand(template string, current string) {
command := strings.Replace(template, "{}", fmt.Sprintf("%q", current), -1)
cmd := exec.Command("sh", "-c", command)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
C.Endwin()
cmd.Run()
C.Refresh()
}
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
func (t *Terminal) Loop() { func (t *Terminal) Loop() {
<-t.startChan <-t.startChan
{ // Late initialization { // Late initialization
t.mutex.Lock() t.mutex.Lock()
t.initFunc() t.initFunc()
t.calculateMargins()
t.printPrompt() t.printPrompt()
t.placeCursor() t.placeCursor()
C.Refresh() C.Refresh()
t.printInfo() t.printInfo()
t.printHeader()
t.mutex.Unlock() t.mutex.Unlock()
go func() { go func() {
timer := time.NewTimer(initialDelay) timer := time.NewTimer(initialDelay)
@@ -504,6 +726,27 @@ func (t *Terminal) Loop() {
t.reqBox.Set(reqRedraw, nil) t.reqBox.Set(reqRedraw, nil)
} }
}() }()
// Keep the spinner spinning
go func() {
for {
t.mutex.Lock()
reading := t.reading
t.mutex.Unlock()
if !reading {
break
}
time.Sleep(spinnerDuration)
t.reqBox.Set(reqInfo, nil)
}
}()
}
exit := func(code int) {
if code == 0 && t.history != nil {
t.history.append(string(t.input))
}
os.Exit(code)
} }
go func() { go func() {
@@ -515,10 +758,15 @@ func (t *Terminal) Loop() {
switch req { switch req {
case reqPrompt: case reqPrompt:
t.printPrompt() t.printPrompt()
if t.inlineInfo {
t.printInfo()
}
case reqInfo: case reqInfo:
t.printInfo() t.printInfo()
case reqList: case reqList:
t.printList() t.printList()
case reqHeader:
t.printHeader()
case reqRefresh: case reqRefresh:
t.suppress = false t.suppress = false
case reqRedraw: case reqRedraw:
@@ -529,10 +777,10 @@ func (t *Terminal) Loop() {
case reqClose: case reqClose:
C.Close() C.Close()
t.output() t.output()
os.Exit(0) exit(0)
case reqQuit: case reqQuit:
C.Close() C.Close()
os.Exit(1) exit(1)
} }
} }
t.placeCursor() t.placeCursor()
@@ -557,134 +805,198 @@ func (t *Terminal) Loop() {
} }
} }
} }
selectItem := func(item *Item) bool {
if _, found := t.selected[item.index]; !found {
t.selected[item.index] = selectedItem{time.Now(), item.StringPtr(t.ansi)}
return true
}
return false
}
toggleY := func(y int) {
item := t.merger.Get(y)
if !selectItem(item) {
delete(t.selected, item.index)
}
}
toggle := func() { toggle := func() {
if t.cy < t.merger.Length() { if t.cy < t.merger.Length() {
item := t.merger.Get(t.cy) toggleY(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.index)
}
req(reqInfo) req(reqInfo)
} }
} }
for _, key := range t.expect { for key, ret := range t.expect {
if keyMatch(key, event) { if keyMatch(key, event) {
t.pressed = key t.pressed = ret
req(reqClose) req(reqClose)
break break
} }
} }
if t.toggleSort > 0 {
if keyMatch(t.toggleSort, event) { action := t.keymap[event.Type]
t.sort = !t.sort mapkey := event.Type
t.eventBox.Set(EvtSearchNew, t.sort) if event.Type == C.Rune {
t.mutex.Unlock() mapkey = int(event.Char) + int(C.AltZ)
continue if act, prs := t.keymap[mapkey]; prs {
action = act
} }
} }
switch event.Type { switch action {
case C.Invalid: case actIgnore:
case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy)
executeCommand(t.execmap[mapkey], item.AsString(t.ansi))
}
case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
continue 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 t.cx = 0
case C.CtrlB: case actBackwardChar:
if t.cx > 0 { if t.cx > 0 {
t.cx-- t.cx--
} }
case C.CtrlC, C.CtrlG, C.CtrlQ, C.ESC: case actAbort:
req(reqQuit) req(reqQuit)
case C.CtrlD: case actDeleteChar:
t.delChar()
case actDeleteCharEOF:
if !t.delChar() && t.cx == 0 { if !t.delChar() && t.cx == 0 {
req(reqQuit) req(reqQuit)
} }
case C.CtrlE: case actEndOfLine:
t.cx = len(t.input) t.cx = len(t.input)
case C.CtrlF: case actCancel:
if len(t.input) == 0 {
req(reqQuit)
} else {
t.yanked = t.input
t.input = []rune{}
t.cx = 0
}
case actForwardChar:
if t.cx < len(t.input) { if t.cx < len(t.input) {
t.cx++ t.cx++
} }
case C.CtrlH: case actBackwardDeleteChar:
if t.cx > 0 { if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
t.cx-- t.cx--
} }
case C.Tab: case actSelectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
selectItem(item)
}
req(reqList, reqInfo)
}
case actDeselectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
delete(t.selected, item.index)
}
req(reqList, reqInfo)
}
case actToggle:
if t.multi && t.merger.Length() > 0 {
toggle()
req(reqList)
}
case actToggleAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
toggleY(i)
}
req(reqList, reqInfo)
}
case actToggleDown:
if t.multi && t.merger.Length() > 0 { if t.multi && t.merger.Length() > 0 {
toggle() toggle()
t.vmove(-1) t.vmove(-1)
req(reqList) req(reqList)
} }
case C.BTab: case actToggleUp:
if t.multi && t.merger.Length() > 0 { if t.multi && t.merger.Length() > 0 {
toggle() toggle()
t.vmove(1) t.vmove(1)
req(reqList) req(reqList)
} }
case C.CtrlJ, C.CtrlN: case actDown:
t.vmove(-1) t.vmove(-1)
req(reqList) req(reqList)
case C.CtrlK, C.CtrlP: case actUp:
t.vmove(1) t.vmove(1)
req(reqList) req(reqList)
case C.CtrlM: case actAccept:
req(reqClose) req(reqClose)
case C.CtrlL: case actClearScreen:
req(reqRedraw) req(reqRedraw)
case C.CtrlU: case actUnixLineDiscard:
if t.cx > 0 { if t.cx > 0 {
t.yanked = copySlice(t.input[:t.cx]) t.yanked = copySlice(t.input[:t.cx])
t.input = t.input[t.cx:] t.input = t.input[t.cx:]
t.cx = 0 t.cx = 0
} }
case C.CtrlW: case actUnixWordRubout:
if t.cx > 0 { if t.cx > 0 {
t.rubout("\\s\\S") t.rubout("\\s\\S")
} }
case C.AltBS: case actBackwardKillWord:
if t.cx > 0 { if t.cx > 0 {
t.rubout("[^[:alnum:]][[:alnum:]]") t.rubout("[^[:alnum:]][[:alnum:]]")
} }
case C.CtrlY: case actYank:
suffix := copySlice(t.input[t.cx:]) suffix := copySlice(t.input[t.cx:])
t.input = append(append(t.input[:t.cx], t.yanked...), suffix...) t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
t.cx += len(t.yanked) t.cx += len(t.yanked)
case C.Del: case actPageUp:
t.delChar() t.vmove(t.maxItems() - 1)
case C.PgUp:
t.vmove(maxItems() - 1)
req(reqList) req(reqList)
case C.PgDn: case actPageDown:
t.vmove(-(maxItems() - 1)) t.vmove(-(t.maxItems() - 1))
req(reqList) req(reqList)
case C.AltB: case actBackwardWord:
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 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 t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
case C.AltD: case actKillWord:
ncx := t.cx + ncx := t.cx +
findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
if ncx > t.cx { if ncx > t.cx {
t.yanked = copySlice(t.input[t.cx:ncx]) t.yanked = copySlice(t.input[t.cx:ncx])
t.input = append(t.input[:t.cx], t.input[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]) prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[t.cx:]...) t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
t.cx++ t.cx++
case C.Mouse: case actPreviousHistory:
me := event.MouseEvent if t.history != nil {
mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y t.history.override(string(t.input))
if !t.reverse { t.input = []rune(t.history.previous())
my = C.MaxY() - my - 1 t.cx = len(t.input)
} }
case actNextHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = []rune(t.history.next())
t.cx = len(t.input)
}
case actMouse:
me := event.MouseEvent
mx, my := me.X, me.Y
if me.S != 0 { if me.S != 0 {
// Scroll // Scroll
if t.merger.Length() > 0 { if t.merger.Length() > 0 {
@@ -694,23 +1006,36 @@ func (t *Terminal) Loop() {
t.vmove(me.S) t.vmove(me.S)
req(reqList) req(reqList)
} }
} else if me.Double { } else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] &&
// Double-click my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] {
if my >= 2 { mx -= t.marginInt[3]
if t.vset(my-2) && t.cy < t.merger.Length() { my -= t.marginInt[0]
req(reqClose) mx = util.Constrain(mx-len(t.prompt), 0, len(t.input))
} if !t.reverse {
my = t.maxHeight() - my - 1
} }
} else if me.Down { min := 2 + len(t.header)
if my == 0 && mx >= 0 { if t.inlineInfo {
// Prompt min--
t.cx = mx }
} else if my >= 2 { if me.Double {
// List // Double-click
if t.vset(t.offset+my-2) && t.multi && me.Mod { if my >= min {
toggle() if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
req(reqClose)
}
}
} else if me.Down {
if my == 0 && mx >= 0 {
// Prompt
t.cx = mx
} else if my >= min {
// List
if t.vset(t.offset+my-min) && t.multi && me.Mod {
toggle()
}
req(reqList)
} }
req(reqList)
} }
} }
} }
@@ -728,7 +1053,7 @@ func (t *Terminal) Loop() {
func (t *Terminal) constrain() { func (t *Terminal) constrain() {
count := t.merger.Length() count := t.merger.Length()
height := C.MaxY() - 2 height := t.maxItems()
diffpos := t.cy - t.offset diffpos := t.cy - t.offset
t.cy = util.Constrain(t.cy, 0, count-1) t.cy = util.Constrain(t.cy, 0, count-1)
@@ -746,14 +1071,27 @@ func (t *Terminal) constrain() {
t.offset = util.Max(0, count-height) t.offset = util.Max(0, count-height)
t.cy = util.Constrain(t.offset+diffpos, 0, count-1) t.cy = util.Constrain(t.offset+diffpos, 0, count-1)
} }
t.offset = util.Max(0, t.offset)
} }
func (t *Terminal) vmove(o int) { func (t *Terminal) vmove(o int) {
if t.reverse { if t.reverse {
t.vset(t.cy - o) o *= -1
} else {
t.vset(t.cy + o)
} }
dest := t.cy + o
if t.cycle {
max := t.merger.Length() - 1
if dest > max {
if t.cy == max {
dest = 0
}
} else if dest < 0 {
if t.cy == 0 {
dest = max
}
}
}
t.vset(dest)
} }
func (t *Terminal) vset(o int) bool { func (t *Terminal) vset(o int) bool {
@@ -761,6 +1099,10 @@ func (t *Terminal) vset(o int) bool {
return t.cy == o return t.cy == o
} }
func maxItems() int { func (t *Terminal) maxItems() int {
return C.MaxY() - 2 max := t.maxHeight() - 2 - len(t.header)
if t.inlineInfo {
max++
}
return util.Max(max, 0)
} }

View File

@@ -18,10 +18,16 @@ type Range struct {
// Token contains the tokenized part of the strings and its prefix length // Token contains the tokenized part of the strings and its prefix length
type Token struct { type Token struct {
text *[]rune text []rune
prefixLength int prefixLength int
} }
// Delimiter for tokenizing the input
type Delimiter struct {
regex *regexp.Regexp
str *string
}
func newRange(begin int, end int) Range { func newRange(begin int, end int) Range {
if begin == 1 { if begin == 1 {
begin = rangeEllipsis begin = rangeEllipsis
@@ -68,16 +74,15 @@ func ParseRange(str *string) (Range, bool) {
return newRange(n, n), true return newRange(n, n), true
} }
func withPrefixLengths(tokens []string, begin int) []Token { func withPrefixLengths(tokens [][]rune, begin int) []Token {
ret := make([]Token, len(tokens)) ret := make([]Token, len(tokens))
prefixLength := begin prefixLength := begin
for idx, token := range tokens { for idx, token := range tokens {
// Need to define a new local variable instead of the reused token to take // Need to define a new local variable instead of the reused token to take
// the pointer to it // the pointer to it
runes := []rune(token) ret[idx] = Token{text: token, prefixLength: prefixLength}
ret[idx] = Token{text: &runes, prefixLength: prefixLength} prefixLength += len(token)
prefixLength += len([]rune(token))
} }
return ret return ret
} }
@@ -88,13 +93,13 @@ const (
awkWhite awkWhite
) )
func awkTokenizer(input *string) ([]string, int) { func awkTokenizer(input []rune) ([][]rune, int) {
// 9, 32 // 9, 32
ret := []string{} ret := [][]rune{}
str := []rune{} str := []rune{}
prefixLength := 0 prefixLength := 0
state := awkNil state := awkNil
for _, r := range []rune(*input) { for _, r := range input {
white := r == 9 || r == 32 white := r == 9 || r == 32
switch state { switch state {
case awkNil: case awkNil:
@@ -113,47 +118,69 @@ func awkTokenizer(input *string) ([]string, int) {
if white { if white {
str = append(str, r) str = append(str, r)
} else { } else {
ret = append(ret, string(str)) ret = append(ret, str)
state = awkBlack state = awkBlack
str = []rune{r} str = []rune{r}
} }
} }
} }
if len(str) > 0 { if len(str) > 0 {
ret = append(ret, string(str)) ret = append(ret, str)
} }
return ret, prefixLength return ret, prefixLength
} }
// Tokenize tokenizes the given string with the delimiter // Tokenize tokenizes the given string with the delimiter
func Tokenize(str *string, delimiter *regexp.Regexp) []Token { func Tokenize(runes []rune, delimiter Delimiter) []Token {
if delimiter == nil { if delimiter.str == nil && delimiter.regex == nil {
// AWK-style (\S+\s*) // AWK-style (\S+\s*)
tokens, prefixLength := awkTokenizer(str) tokens, prefixLength := awkTokenizer(runes)
return withPrefixLengths(tokens, prefixLength) return withPrefixLengths(tokens, prefixLength)
} }
tokens := delimiter.FindAllString(*str, -1)
return withPrefixLengths(tokens, 0)
}
func joinTokens(tokens *[]Token) *string { var tokens []string
ret := "" if delimiter.str != nil {
for _, token := range *tokens { tokens = strings.Split(string(runes), *delimiter.str)
ret += string(*token.text) for i := 0; i < len(tokens)-1; i++ {
tokens[i] = tokens[i] + *delimiter.str
}
} else if delimiter.regex != nil {
str := string(runes)
for len(str) > 0 {
loc := delimiter.regex.FindStringIndex(str)
if loc == nil {
loc = []int{0, len(str)}
}
last := util.Max(loc[1], 1)
tokens = append(tokens, str[:last])
str = str[last:]
}
} }
return &ret asRunes := make([][]rune, len(tokens))
for i, token := range tokens {
asRunes[i] = []rune(token)
}
return withPrefixLengths(asRunes, 0)
} }
func joinTokensAsRunes(tokens *[]Token) *[]rune { func joinTokens(tokens []Token) []rune {
ret := []rune{} ret := []rune{}
for _, token := range *tokens { for _, token := range tokens {
ret = append(ret, *token.text...) ret = append(ret, 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 // Transform is used to transform the input when --with-nth option is given
func Transform(tokens []Token, withNth []Range) *[]Token { func Transform(tokens []Token, withNth []Range) []Token {
transTokens := make([]Token, len(withNth)) transTokens := make([]Token, len(withNth))
numTokens := len(tokens) numTokens := len(tokens)
for idx, r := range withNth { for idx, r := range withNth {
@@ -162,14 +189,14 @@ func Transform(tokens []Token, withNth []Range) *[]Token {
if r.begin == r.end { if r.begin == r.end {
idx := r.begin idx := r.begin
if idx == rangeEllipsis { if idx == rangeEllipsis {
part = append(part, *joinTokensAsRunes(&tokens)...) part = append(part, joinTokensAsRunes(tokens)...)
} else { } else {
if idx < 0 { if idx < 0 {
idx += numTokens + 1 idx += numTokens + 1
} }
if idx >= 1 && idx <= numTokens { if idx >= 1 && idx <= numTokens {
minIdx = idx - 1 minIdx = idx - 1
part = append(part, *tokens[idx-1].text...) part = append(part, tokens[idx-1].text...)
} }
} }
} else { } else {
@@ -196,7 +223,7 @@ func Transform(tokens []Token, withNth []Range) *[]Token {
minIdx = util.Max(0, begin-1) minIdx = util.Max(0, begin-1)
for idx := begin; idx <= end; idx++ { for idx := begin; idx <= end; idx++ {
if idx >= 1 && idx <= numTokens { if idx >= 1 && idx <= numTokens {
part = append(part, *tokens[idx-1].text...) part = append(part, tokens[idx-1].text...)
} }
} }
} }
@@ -206,7 +233,7 @@ func Transform(tokens []Token, withNth []Range) *[]Token {
} else { } else {
prefixLength = 0 prefixLength = 0
} }
transTokens[idx] = Token{&part, prefixLength} transTokens[idx] = Token{part, prefixLength}
} }
return &transTokens return transTokens
} }

View File

@@ -43,14 +43,23 @@ func TestParseRange(t *testing.T) {
func TestTokenize(t *testing.T) { func TestTokenize(t *testing.T) {
// AWK-style // AWK-style
input := " abc: def: ghi " input := " abc: def: ghi "
tokens := Tokenize(&input, nil) tokens := Tokenize([]rune(input), Delimiter{})
if string(*tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 { if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
// With delimiter // With delimiter
tokens = Tokenize(&input, delimiterRegexp(":")) tokens = Tokenize([]rune(input), delimiterRegexp(":"))
if string(*tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 { if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 {
t.Errorf("%s", tokens)
}
// With delimiter regex
tokens = Tokenize([]rune(input), delimiterRegexp("\\s+"))
if string(tokens[0].text) != " " || tokens[0].prefixLength != 0 ||
string(tokens[1].text) != "abc: " || tokens[1].prefixLength != 2 ||
string(tokens[2].text) != "def: " || tokens[2].prefixLength != 8 ||
string(tokens[3].text) != "ghi " || tokens[3].prefixLength != 14 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
} }
@@ -58,39 +67,39 @@ func TestTokenize(t *testing.T) {
func TestTransform(t *testing.T) { func TestTransform(t *testing.T) {
input := " abc: def: ghi: jkl" input := " abc: def: ghi: jkl"
{ {
tokens := Tokenize(&input, nil) tokens := Tokenize([]rune(input), Delimiter{})
{ {
ranges := splitNth("1,2,3") ranges := splitNth("1,2,3")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if *joinTokens(tx) != "abc: def: ghi: " { if string(joinTokens(tx)) != "abc: def: ghi: " {
t.Errorf("%s", *tx) t.Errorf("%s", tx)
} }
} }
{ {
ranges := splitNth("1..2,3,2..,1") ranges := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if *joinTokens(tx) != "abc: def: ghi: def: ghi: jklabc: " || if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
len(*tx) != 4 || len(tx) != 4 ||
string(*(*tx)[0].text) != "abc: def: " || (*tx)[0].prefixLength != 2 || string(tx[0].text) != "abc: def: " || tx[0].prefixLength != 2 ||
string(*(*tx)[1].text) != "ghi: " || (*tx)[1].prefixLength != 14 || string(tx[1].text) != "ghi: " || tx[1].prefixLength != 14 ||
string(*(*tx)[2].text) != "def: ghi: jkl" || (*tx)[2].prefixLength != 8 || string(tx[2].text) != "def: ghi: jkl" || tx[2].prefixLength != 8 ||
string(*(*tx)[3].text) != "abc: " || (*tx)[3].prefixLength != 2 { string(tx[3].text) != "abc: " || tx[3].prefixLength != 2 {
t.Errorf("%s", *tx) t.Errorf("%s", tx)
} }
} }
} }
{ {
tokens := Tokenize(&input, delimiterRegexp(":")) tokens := Tokenize([]rune(input), delimiterRegexp(":"))
{ {
ranges := splitNth("1..2,3,2..,1") ranges := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if *joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" || if string(joinTokens(tx)) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(*tx) != 4 || len(tx) != 4 ||
string(*(*tx)[0].text) != " abc: def:" || (*tx)[0].prefixLength != 0 || string(tx[0].text) != " abc: def:" || tx[0].prefixLength != 0 ||
string(*(*tx)[1].text) != " ghi:" || (*tx)[1].prefixLength != 12 || string(tx[1].text) != " ghi:" || tx[1].prefixLength != 12 ||
string(*(*tx)[2].text) != " def: ghi: jkl" || (*tx)[2].prefixLength != 6 || string(tx[2].text) != " def: ghi: jkl" || tx[2].prefixLength != 6 ||
string(*(*tx)[3].text) != " abc:" || (*tx)[3].prefixLength != 0 { string(tx[3].text) != " abc:" || tx[3].prefixLength != 0 {
t.Errorf("%s", *tx) t.Errorf("%s", tx)
} }
} }
} }

View File

@@ -6,6 +6,7 @@ import "C"
import ( import (
"os" "os"
"time" "time"
"unicode/utf8"
) )
// Max returns the largest integer // Max returns the largest integer
@@ -19,7 +20,7 @@ func Max(first int, items ...int) int {
return max return max
} }
// Max32 returns the smallest 32-bit integer // Min32 returns the smallest 32-bit integer
func Min32(first int32, second int32) int32 { func Min32(first int32, second int32) int32 {
if first <= second { if first <= second {
return first return first
@@ -69,22 +70,33 @@ func DurWithin(
return val return val
} }
func Between(val int, min int, max int) bool {
return val >= min && val <= max
}
// IsTty returns true is stdin is a terminal // IsTty returns true is stdin is a terminal
func IsTty() bool { func IsTty() bool {
return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 return int(C.isatty(C.int(os.Stdin.Fd()))) != 0
} }
func TrimRight(runes *[]rune) []rune { func TrimRight(runes []rune) []rune {
var i int var i int
for i = len(*runes) - 1; i >= 0; i-- { for i = len(runes) - 1; i >= 0; i-- {
char := (*runes)[i] char := runes[i]
if char != ' ' && char != '\t' { if char != ' ' && char != '\t' {
break break
} }
} }
return (*runes)[0 : i+1] return runes[0 : i+1]
}
func BytesToRunes(bytea []byte) []rune {
runes := make([]rune, 0, len(bytea))
for i := 0; i < len(bytea); {
if bytea[i] < utf8.RuneSelf {
runes = append(runes, rune(bytea[i]))
i++
} else {
r, sz := utf8.DecodeRune(bytea[i:])
i += sz
runes = append(runes, r)
}
}
return runes
} }

View File

@@ -4,6 +4,8 @@
require 'minitest/autorun' require 'minitest/autorun'
require 'fileutils' require 'fileutils'
DEFAULT_TIMEOUT = 20
base = File.expand_path('../../', __FILE__) base = File.expand_path('../../', __FILE__)
Dir.chdir base Dir.chdir base
FZF = "#{base}/bin/fzf" FZF = "#{base}/bin/fzf"
@@ -22,26 +24,13 @@ class NilClass
end end
end end
module Temp def wait
def readonce since = Time.now
name = self.class::TEMPNAME while Time.now - since < DEFAULT_TIMEOUT
waited = 0 return if yield
while waited < 5 sleep 0.05
begin
system 'sync'
data = File.read(name)
return data unless data.empty?
rescue
sleep 0.1
waited += 0.1
end
end
raise "failed to read tempfile"
ensure
while File.exists? name
File.unlink name rescue nil
end
end end
throw 'timeout'
end end
class Shell class Shell
@@ -59,8 +48,6 @@ class Shell
end end
class Tmux class Tmux
include Temp
TEMPNAME = '/tmp/fzf-test.txt' TEMPNAME = '/tmp/fzf-test.txt'
attr_reader :win attr_reader :win
@@ -85,15 +72,6 @@ class Tmux
end end
end end
def closed?
!go("list-window -F '#I'").include?(win)
end
def close
send_keys 'C-c', 'C-u', 'exit', :Enter
wait { closed? }
end
def kill def kill
go("kill-window -t #{win} 2> /dev/null") go("kill-window -t #{win} 2> /dev/null")
end end
@@ -111,79 +89,79 @@ class Tmux
go("send-keys -t #{target} #{args}") go("send-keys -t #{target} #{args}")
end end
def capture opts = {} def capture pane = 0
timeout, pane = defaults(opts).values_at(:timeout, :pane) File.unlink TEMPNAME while File.exists? TEMPNAME
waited = 0 wait do
loop do go("capture-pane -t #{win}.#{pane} \\; save-buffer #{TEMPNAME} 2> /dev/null")
go("capture-pane -t #{win}.#{pane} \\; save-buffer #{TEMPNAME}") $?.exitstatus == 0
break if $?.exitstatus == 0
if waited > timeout
raise "Window not found"
end
waited += 0.1
sleep 0.1
end end
readonce.split($/)[0, @lines].reverse.drop_while(&:empty?).reverse File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
end end
def until opts = {} def until pane = 0
lines = nil lines = nil
wait(opts) do begin
yield lines = capture(opts) 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 end
lines lines
end end
def prepare def prepare
self.send_keys 'echo hello', :Enter tries = 0
self.until { |lines| lines[-1].start_with?('hello') } begin
self.send_keys 'clear', :Enter self.send_keys 'C-u', 'hello', 'Right'
self.until { |lines| lines.empty? } self.until { |lines| lines[-1].end_with?('hello') }
rescue Exception
(tries += 1) < 5 ? retry : raise
end
self.send_keys 'C-u'
end end
private private
def defaults opts
{ timeout: 10, pane: 0 }.merge(opts)
end
def wait opts = {}
timeout, pane = defaults(opts).values_at(:timeout, :pane)
waited = 0
until yield
if waited > timeout
hl = '=' * 10
puts hl
capture(opts).each_with_index do |line, idx|
puts [idx.to_s.rjust(2), line].join(': ')
end
puts hl
raise "timeout"
end
waited += 0.1
sleep 0.1
end
end
def go *args def go *args
%x[tmux #{args.join ' '}].split($/) %x[tmux #{args.join ' '}].split($/)
end end
end end
class TestBase < Minitest::Test class TestBase < Minitest::Test
include Temp
FIN = 'FIN'
TEMPNAME = '/tmp/output' TEMPNAME = '/tmp/output'
attr_reader :tmux attr_reader :tmux
def tempname
[TEMPNAME,
caller_locations.map(&:label).find { |l| l =~ /^test_/ }].join '-'
end
def setup def setup
ENV.delete 'FZF_DEFAULT_OPTS' ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_CTRL_T_COMMAND'
ENV.delete 'FZF_DEFAULT_COMMAND' ENV.delete 'FZF_DEFAULT_COMMAND'
end end
def readonce
wait { File.exists?(tempname) }
File.read(tempname)
ensure
File.unlink tempname while File.exists?(tempname)
tmux.prepare
end
def fzf(*opts) def fzf(*opts)
fzf!(*opts) + " > #{TEMPNAME} && echo #{FIN}" fzf!(*opts) + " > #{tempname}.tmp; mv #{tempname}.tmp #{tempname}"
end end
def fzf!(*opts) def fzf!(*opts)
@@ -214,8 +192,7 @@ class TestGoFZF < TestBase
def test_vanilla def test_vanilla
tmux.send_keys "seq 1 100000 | #{fzf}", :Enter tmux.send_keys "seq 1 100000 | #{fzf}", :Enter
tmux.until(timeout: 20) { |lines| tmux.until { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ }
lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ }
lines = tmux.capture lines = tmux.capture
assert_equal ' 2', lines[-4] assert_equal ' 2', lines[-4]
assert_equal '> 1', lines[-3] assert_equal '> 1', lines[-3]
@@ -232,7 +209,6 @@ class TestGoFZF < TestBase
assert_equal '> 391', lines[-1] assert_equal '> 391', lines[-1]
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.close
assert_equal '1391', readonce.chomp assert_equal '1391', readonce.chomp
end end
@@ -241,7 +217,6 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines.last =~ /^>/ } tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.close
assert_equal 'hello', readonce.chomp assert_equal 'hello', readonce.chomp
end end
@@ -308,7 +283,6 @@ class TestGoFZF < TestBase
# CTRL-M # CTRL-M
tmux.send_keys "C-M" tmux.send_keys "C-M"
tmux.until { |lines| lines.last !~ /^>/ } tmux.until { |lines| lines.last !~ /^>/ }
tmux.close
end end
def test_multi_order def test_multi_order
@@ -320,9 +294,7 @@ class TestGoFZF < TestBase
:PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7 :PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7
tmux.until { |lines| lines[-2].include? '(6)' } tmux.until { |lines| lines[-2].include? '(6)' }
tmux.send_keys "C-M" tmux.send_keys "C-M"
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal %w[3 2 5 6 8 7], readonce.split($/) assert_equal %w[3 2 5 6 8 7], readonce.split($/)
tmux.close
end end
def test_with_nth def test_with_nth
@@ -340,14 +312,14 @@ class TestGoFZF < TestBase
# However, the output must not be transformed # However, the output must not be transformed
if multi if multi
tmux.send_keys :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab
tmux.until { |lines| lines[-1].include?(FIN) } tmux.until { |lines| lines[-2].include?('(2)') }
tmux.send_keys :Enter
assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.split($/) assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.split($/)
else else
tmux.send_keys '^', '3' tmux.send_keys '^', '3'
tmux.until { |lines| lines[-2].include?('1/2') } tmux.until { |lines| lines[-2].include?('1/2') }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal [' 1st 2nd 3rd/'], readonce.split($/) assert_equal [' 1st 2nd 3rd/'], readonce.split($/)
end end
end end
@@ -360,20 +332,17 @@ class TestGoFZF < TestBase
tmux.send_keys *110.times.map { rev ? :Down : :Up } tmux.send_keys *110.times.map { rev ? :Down : :Up }
tmux.until { |lines| lines.include? '> 100' } tmux.until { |lines| lines.include? '> 100' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal '100', readonce.chomp assert_equal '100', readonce.chomp
end end
end end
def test_select_1 def test_select_1
tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 5555, :'1'}", :Enter tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 5555, :'1'}", :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['5555', '55'], readonce.split($/) assert_equal ['5555', '55'], readonce.split($/)
end end
def test_exit_0 def test_exit_0
tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 555555, :'0'}", :Enter tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 555555, :'0'}", :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['555555'], readonce.split($/) assert_equal ['555555'], readonce.split($/)
end end
@@ -381,17 +350,17 @@ class TestGoFZF < TestBase
[:'0', :'1', [:'1', :'0']].each do |opt| [:'0', :'1', [:'1', :'0']].each do |opt|
tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter
tmux.until { |lines| lines.last =~ /^> 5/ } tmux.until { |lines| lines.last =~ /^> 5/ }
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-1].include?(FIN) } tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter
assert_equal ['5', '5', '15', '25'], readonce.split($/) assert_equal ['5', '5', '15', '25'], readonce.split($/)
end end
end end
def test_query_unicode def test_query_unicode
tmux.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter tmux.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter
tmux.until { |lines| lines.last.start_with? '>' } tmux.until { |lines| lines[-2].include? '1/2' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['가나다'], readonce.split($/) assert_equal ['가나다'], readonce.split($/)
end end
@@ -400,7 +369,9 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-1] == '>' } tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 9 tmux.send_keys 9
tmux.until { |lines| lines[-2] == ' 19/100' } tmux.until { |lines| lines[-2] == ' 19/100' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == '>' } tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 'C-K', :Enter tmux.send_keys 'C-K', :Enter
assert_equal ['1919'], readonce.split($/) assert_equal ['1919'], readonce.split($/)
@@ -409,7 +380,9 @@ class TestGoFZF < TestBase
def test_tac def test_tac
tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter
assert_equal %w[1000 999 998], readonce.split($/) assert_equal %w[1000 999 998], readonce.split($/)
end end
@@ -417,7 +390,9 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys '99' tmux.send_keys '99'
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter
assert_equal %w[99 999 998], readonce.split($/) assert_equal %w[99 999 998], readonce.split($/)
end end
@@ -425,7 +400,10 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 1 1000 | #{fzf :tac, :no_sort, :multi}", :Enter tmux.send_keys "seq 1 1000 | #{fzf :tac, :no_sort, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys '00' tmux.send_keys '00'
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.until { |lines| lines[-2].include? '10/1000' }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter
assert_equal %w[1000 900 800], readonce.split($/) assert_equal %w[1000 900 800], readonce.split($/)
end end
@@ -434,6 +412,7 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 1 100 | #{fzf :expect, key}", :Enter tmux.send_keys "seq 1 100 | #{fzf :expect, key}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' } tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys '55' tmux.send_keys '55'
tmux.until { |lines| lines[-2].include? '1/100' }
tmux.send_keys *feed tmux.send_keys *feed
assert_equal [expected, '55'], readonce.split($/) assert_equal [expected, '55'], readonce.split($/)
end end
@@ -452,6 +431,7 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 1 100 | #{fzf '--expect=alt-z', :print_query}", :Enter tmux.send_keys "seq 1 100 | #{fzf '--expect=alt-z', :print_query}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' } tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys '55' tmux.send_keys '55'
tmux.until { |lines| lines[-2].include? '1/100' }
tmux.send_keys :Escape, :z tmux.send_keys :Escape, :z
assert_equal ['55', 'alt-z', '55'], readonce.split($/) assert_equal ['55', 'alt-z', '55'], readonce.split($/)
end end
@@ -462,29 +442,27 @@ class TestGoFZF < TestBase
end end
def test_toggle_sort def test_toggle_sort
tmux.send_keys "seq 1 111 | #{fzf '-m +s --tac --toggle-sort=ctrl-r -q11'}", :Enter ['--toggle-sort=ctrl-r', '--bind=ctrl-r:toggle-sort'].each do |opt|
tmux.until { |lines| lines[-3].include? '> 111' } tmux.send_keys "seq 1 111 | #{fzf "-m +s --tac #{opt} -q11"}", :Enter
tmux.send_keys :Tab tmux.until { |lines| lines[-3].include? '> 111' }
tmux.until { |lines| lines[-2].include? '4/111 (1)' } tmux.send_keys :Tab
tmux.send_keys 'C-R' tmux.until { |lines| lines[-2].include? '4/111 (1)' }
tmux.until { |lines| lines[-3].include? '> 11' } tmux.send_keys 'C-R'
tmux.send_keys :Tab tmux.until { |lines| lines[-3].include? '> 11' }
tmux.until { |lines| lines[-2].include? '4/111/S (2)' } tmux.send_keys :Tab
tmux.send_keys :Enter tmux.until { |lines| lines[-2].include? '4/111/S (2)' }
assert_equal ['111', '11'], readonce.split($/) tmux.send_keys :Enter
assert_equal ['111', '11'], readonce.split($/)
end
end end
def test_unicode_case def test_unicode_case
tempname = TEMPNAME + Time.now.to_f.to_s
writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4] writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4]
assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/) assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/)
assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/) assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/)
rescue
File.unlink tempname
end end
def test_tiebreak def test_tiebreak
tempname = TEMPNAME + Time.now.to_f.to_s
input = %w[ input = %w[
--foobar-------- --foobar--------
-----foobar--- -----foobar---
@@ -521,20 +499,312 @@ class TestGoFZF < TestBase
], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/) ], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/)
assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/) assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/)
rescue end
File.unlink tempname
def test_tiebreak_length_with_nth
input = %w[
1:hell
123:hello
12345:he
1234567:h
]
writelines tempname, input
output = %w[
1:hell
12345:he
123:hello
1234567:h
]
assert_equal output, `cat #{tempname} | #{FZF} -fh`.split($/)
output = %w[
1234567:h
12345:he
1:hell
123:hello
]
assert_equal output, `cat #{tempname} | #{FZF} -fh -n2 -d:`.split($/)
end
def test_tiebreak_end_backward_scan
input = %w[
foobar-fb
fubar
]
writelines tempname, input
assert_equal input.reverse, `cat #{tempname} | #{FZF} -f fb`.split($/)
assert_equal input, `cat #{tempname} | #{FZF} -f fb --tiebreak=end`.split($/)
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
def test_long_line
data = '.' * 256 * 1024
File.open(tempname, 'w') do |f|
f << data
end
assert_equal data, `cat #{tempname} | #{FZF} -f .`.chomp
end
def test_read0
lines = `find .`.split($/)
assert_equal lines.last, `find . | #{FZF} -e -f "^#{lines.last}$"`.chomp
assert_equal lines.last, `find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp
end
def test_select_all_deselect_all_toggle_all
tmux.send_keys "seq 100 | #{fzf '--bind ctrl-a:select-all,ctrl-d:deselect-all,ctrl-t:toggle-all --multi'}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include? '(3)' }
tmux.send_keys 'C-t'
tmux.until { |lines| lines[-2].include? '(97)' }
tmux.send_keys 'C-a'
tmux.until { |lines| lines[-2].include? '(100)' }
tmux.send_keys :Tab, :Tab
tmux.until { |lines| lines[-2].include? '(98)' }
tmux.send_keys 'C-d'
tmux.until { |lines| !lines[-2].include? '(' }
tmux.send_keys :Tab, :Tab
tmux.until { |lines| lines[-2].include? '(2)' }
tmux.send_keys 0
tmux.until { |lines| lines[-2].include? '10/100' }
tmux.send_keys 'C-a'
tmux.until { |lines| lines[-2].include? '(12)' }
tmux.send_keys :Enter
assert_equal %w[2 1 10 20 30 40 50 60 70 80 90 100], readonce.split($/)
end
def test_history
history_file = '/tmp/fzf-test-history'
# History with limited number of entries
File.unlink history_file rescue nil
opts = "--history=#{history_file} --history-size=4"
input = %w[00 11 22 33 44].map { |e| e + $/ }
input.each do |keys|
tmux.send_keys "seq 100 | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys keys
tmux.until { |lines| lines[-2].include? '1/100' }
tmux.send_keys :Enter
readonce
end
assert_equal input[1..-1], File.readlines(history_file)
# Update history entries (not changed on disk)
tmux.send_keys "seq 100 | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys 'C-p'
tmux.until { |lines| lines[-1].end_with? '> 44' }
tmux.send_keys 'C-p'
tmux.until { |lines| lines[-1].end_with? '> 33' }
tmux.send_keys :BSpace
tmux.until { |lines| lines[-1].end_with? '> 3' }
tmux.send_keys 1
tmux.until { |lines| lines[-1].end_with? '> 31' }
tmux.send_keys 'C-p'
tmux.until { |lines| lines[-1].end_with? '> 22' }
tmux.send_keys 'C-n'
tmux.until { |lines| lines[-1].end_with? '> 31' }
tmux.send_keys 0
tmux.until { |lines| lines[-1].end_with? '> 310' }
tmux.send_keys :Enter
readonce
assert_equal %w[22 33 44 310].map { |e| e + $/ }, File.readlines(history_file)
# Respect --bind option
tmux.send_keys "seq 100 | #{fzf opts + ' --bind ctrl-p:next-history,ctrl-n:previous-history'}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys 'C-n', 'C-n', 'C-n', 'C-n', 'C-p'
tmux.until { |lines| lines[-1].end_with?('33') }
tmux.send_keys :Enter
ensure
File.unlink history_file
end
def test_execute
output = '/tmp/fzf-test-execute'
opts = %[--bind \\"alt-a:execute(echo '[{}]' >> #{output}),alt-b:execute[echo '({}), ({})' >> #{output}],C:execute:echo '({}), [{}], @{}@' >> #{output}\\"]
tmux.send_keys "seq 100 | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys :Escape, :a, :Escape, :a
tmux.send_keys :Up
tmux.send_keys :Escape, :b, :Escape, :b
tmux.send_keys :Up
tmux.send_keys :C
tmux.send_keys 'foobar'
tmux.until { |lines| lines[-2].include? '0/100' }
tmux.send_keys :Escape, :a, :Escape, :b, :Escape, :c
tmux.send_keys :Enter
readonce
assert_equal ['["1"]', '["1"]', '("2"), ("2")', '("2"), ("2")', '("3"), ["3"], @"3"@'],
File.readlines(output).map(&:chomp)
ensure
File.unlink output rescue nil
end
def test_cycle
tmux.send_keys "seq 8 | #{fzf :cycle}", :Enter
tmux.until { |lines| lines[-2].include? '8/8' }
tmux.send_keys :Down
tmux.until { |lines| lines[-10].start_with? '>' }
tmux.send_keys :Down
tmux.until { |lines| lines[-9].start_with? '>' }
tmux.send_keys :PgUp
tmux.until { |lines| lines[-10].start_with? '>' }
tmux.send_keys :PgUp
tmux.until { |lines| lines[-3].start_with? '>' }
tmux.send_keys :Up
tmux.until { |lines| lines[-4].start_with? '>' }
tmux.send_keys :PgDn
tmux.until { |lines| lines[-3].start_with? '>' }
tmux.send_keys :PgDn
tmux.until { |lines| lines[-10].start_with? '>' }
end
def test_header_lines
tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5'}", :Enter
2.times do
tmux.until do |lines|
lines[-2].include?('/90') &&
lines[-3] == ' 1' &&
lines[-4] == ' 2' &&
lines[-13] == '> 15'
end
tmux.send_keys :Down
end
tmux.send_keys :Enter
assert_equal '15', readonce.chomp
end
def test_header_lines_reverse
tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5 --reverse'}", :Enter
2.times do
tmux.until do |lines|
lines[1].include?('/90') &&
lines[2] == ' 1' &&
lines[3] == ' 2' &&
lines[12] == '> 15'
end
tmux.send_keys :Up
end
tmux.send_keys :Enter
assert_equal '15', readonce.chomp
end
def test_header_lines_overflow
tmux.send_keys "seq 100 | #{fzf '--header-lines=200'}", :Enter
tmux.until do |lines|
lines[-2].include?('0/0') &&
lines[-3].include?(' 1')
end
tmux.send_keys :Enter
assert_equal '', readonce.chomp
end
def test_header_lines_with_nth
tmux.send_keys "seq 100 | #{fzf "--header-lines 5 --with-nth 1,1,1,1,1"}", :Enter
tmux.until do |lines|
lines[-2].include?('95/95') &&
lines[-3] == ' 11111' &&
lines[-7] == ' 55555' &&
lines[-8] == '> 66666'
end
tmux.send_keys :Enter
assert_equal '6', readonce.chomp
end
def test_header_file
tmux.send_keys "seq 100 | #{fzf "--header-file <(head -5 #{__FILE__})"}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip)
tmux.until do |lines|
lines[-2].include?('100/100') &&
lines[-7..-3].map(&:strip) == header
end
end
def test_header_file_reverse
tmux.send_keys "seq 100 | #{fzf "--header-file=<(head -5 #{__FILE__}) --reverse"}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip)
tmux.until do |lines|
lines[1].include?('100/100') &&
lines[2..6].map(&:strip) == header
end
end
def test_canel
tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter
tmux.until { |lines| lines[-2].include?('10/10') }
tmux.send_keys '123'
tmux.until { |lines| lines[-1] == '> 3' && lines[-2].include?('1/10') }
tmux.send_keys 'C-y', 'C-y'
tmux.until { |lines| lines[-1] == '> 311' }
tmux.send_keys 2
tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 2
tmux.prepare
end
def test_margin
tmux.send_keys "yes | head -1000 | #{fzf "--margin 5,3"}", :Enter
tmux.until { |lines| lines[4] == '' && lines[5] == ' y' }
tmux.send_keys :Enter
end
def test_margin_reverse
tmux.send_keys "seq 1000 | #{fzf "--margin 7,5 --reverse"}", :Enter
tmux.until { |lines| lines[1 + 7] == ' 1000/1000' }
tmux.send_keys :Enter
end
def test_invalid_term
tmux.send_keys "TERM=xxx fzf", :Enter
tmux.until { |lines| lines.any? { |l| l.include? 'Invalid $TERM: xxx' } }
end
def test_with_nth
writelines tempname, ['hello world ', 'byebye']
assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1`.chomp
end
def test_with_nth_ansi
writelines tempname, ["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye']
assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi`.chomp
end
def test_with_nth_no_ansi
src = "\x1b[33mhello \x1b[34;1mworld\x1b[m "
writelines tempname, [src, 'byebye']
assert_equal src, `cat #{tempname} | #{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi`.chomp
end end
private private
def writelines path, lines, timeout = 10 def writelines path, lines
File.open(path, 'w') do |f| File.unlink path while File.exists? path
f << lines.join($/) File.open(path, 'w') { |f| f << lines.join($/) }
f.sync
end
since = Time.now
while `cat #{path}`.split($/).length != lines.length && (Time.now - since) < 10
sleep 0.1
end
end end
end end
@@ -547,35 +817,54 @@ module TestShell
@tmux.kill @tmux.kill
end end
def set_var name, val
tmux.prepare
tmux.send_keys "export #{name}='#{val}'", :Enter
tmux.prepare
end
def test_ctrl_t def test_ctrl_t
tmux.prepare tmux.prepare
tmux.send_keys 'C-t', pane: 0 tmux.send_keys 'C-t', pane: 0
lines = tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } lines = tmux.until(1) { |lines| lines.item_count > 1 }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, :Enter, pane: 1 tmux.send_keys :BTab, :BTab, pane: 1
tmux.until(pane: 0) { |lines| lines[-1].include? expected } tmux.until(1) { |lines| lines[-2].include?('(2)') }
tmux.send_keys :Enter, pane: 1
tmux.until(0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c' tmux.send_keys 'C-c'
# FZF_TMUX=0 # FZF_TMUX=0
new_shell new_shell
tmux.send_keys 'C-t', pane: 0 tmux.send_keys 'C-t', pane: 0
lines = tmux.until(pane: 0) { |lines| lines[-1].start_with? '>' } lines = tmux.until(0) { |lines| lines.item_count > 1 }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, :Enter, pane: 0 tmux.send_keys :BTab, :BTab, pane: 0
tmux.until(pane: 0) { |lines| lines[-1].include? expected } tmux.until(0) { |lines| lines[-2].include?('(2)') }
tmux.send_keys :Enter, pane: 0
tmux.until(0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c', 'C-d' tmux.send_keys 'C-c', 'C-d'
end end
def test_ctrl_t_command
set_var "FZF_CTRL_T_COMMAND", "seq 100"
tmux.send_keys 'C-t', pane: 0
lines = tmux.until(1) { |lines| lines.item_count == 100 }
tmux.send_keys :BTab, :BTab, :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter, pane: 1
tmux.until(0) { |lines| lines[-1].include? '1 2 3' }
end
def test_alt_c def test_alt_c
tmux.prepare tmux.prepare
tmux.send_keys :Escape, :c tmux.send_keys :Escape, :c, pane: 0
lines = tmux.until { |lines| lines[-1].start_with? '>' } lines = tmux.until(1) { |lines| lines.item_count > 0 && lines[-3][2..-1] }
expected = lines[-3][2..-1] expected = lines[-3][2..-1]
p expected tmux.send_keys :Enter, pane: 1
tmux.send_keys :Enter
tmux.prepare tmux.prepare
tmux.send_keys :pwd, :Enter tmux.send_keys :pwd, :Enter
tmux.until { |lines| p lines; lines[-1].end_with?(expected) } tmux.until { |lines| lines[-1].end_with?(expected) }
end end
def test_ctrl_r def test_ctrl_r
@@ -585,51 +874,82 @@ module TestShell
tmux.send_keys 'echo 3d', :Enter; tmux.prepare tmux.send_keys 'echo 3d', :Enter; tmux.prepare
tmux.send_keys 'echo 3rd', :Enter; tmux.prepare tmux.send_keys 'echo 3rd', :Enter; tmux.prepare
tmux.send_keys 'echo 4th', :Enter; tmux.prepare tmux.send_keys 'echo 4th', :Enter; tmux.prepare
tmux.send_keys 'C-r' tmux.send_keys 'C-r', pane: 0
tmux.until { |lines| lines[-1].start_with? '>' } tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys '3d' tmux.send_keys '3d', pane: 1
tmux.until { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort tmux.until(1) { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort
tmux.send_keys :Enter tmux.send_keys :Enter, pane: 1
tmux.until { |lines| lines[-1] == 'echo 3rd' } tmux.until { |lines| lines[-1] == 'echo 3rd' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == '3rd' } tmux.until { |lines| lines[-1] == '3rd' }
end end
end end
class TestBash < TestBase module CompletionTest
include TestShell
def new_shell
tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter
tmux.prepare
end
def setup
super
@tmux = Tmux.new :bash
end
def test_file_completion def test_file_completion
tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}', :Enter 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.prepare
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0
tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab
tmux.until(1) { |lines| lines[-2].include?('(2)') }
tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
lines[-1].include?('/tmp/fzf-test/10') && lines[-1].include?('/tmp/fzf-test/10') &&
lines[-1].include?('/tmp/fzf-test/100') lines[-1].include?('/tmp/fzf-test/100')
end 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 end
def test_dir_completion def test_dir_completion
tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter
tmux.prepare tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0 tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0
tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys :BTab, :BTab # BTab does not work here tmux.send_keys :BTab, :BTab # BTab does not work here
tmux.send_keys 55 tmux.send_keys 55
tmux.until(pane: 1) { |lines| lines[-2].start_with? ' 1/' } tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
@@ -638,9 +958,11 @@ class TestBash < TestBase
tmux.send_keys :xx tmux.send_keys :xx
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
# Should not match regular files # Should not match regular files (bash-only)
tmux.send_keys :Tab if self.class == TestBash
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } tmux.send_keys :Tab
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
end
# Fail back to plusdirs # Fail back to plusdirs
tmux.send_keys :BSpace, :BSpace, :BSpace tmux.send_keys :BSpace, :BSpace, :BSpace
@@ -655,19 +977,38 @@ class TestBash < TestBase
pid = lines[-1].split.last pid = lines[-1].split.last
tmux.prepare tmux.prepare
tmux.send_keys 'kill ', :Tab, pane: 0 tmux.send_keys 'kill ', :Tab, pane: 0
tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' } tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys 'sleep12345' tmux.send_keys 'sleep12345'
tmux.until(pane: 1) { |lines| lines[-3].include? 'sleep 12345' } tmux.until(1) { |lines| lines[-3].include? 'sleep 12345' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
lines[-1] == "kill #{pid}" lines[-1] == "kill #{pid}"
end 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.prepare
tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter
tmux.prepare
end
def setup
super
@tmux = Tmux.new :bash
end end
end end
class TestZsh < TestBase class TestZsh < TestBase
include TestShell include TestShell
include CompletionTest
def new_shell def new_shell
tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter
@@ -689,6 +1030,12 @@ class TestFish < TestBase
tmux.until { |lines| lines.empty? } tmux.until { |lines| lines.empty? }
end end
def set_var name, val
tmux.prepare
tmux.send_keys "set -g #{name} '#{val}'", :Enter
tmux.prepare
end
def setup def setup
super super
@tmux = Tmux.new :fish @tmux = Tmux.new :fish