Compare commits

...

502 Commits

Author SHA1 Message Date
Junegunn Choi
390b49653b 0.17.3 2017-12-03 23:55:24 +09:00
Junegunn Choi
b877c385f0 Fix assertions in test_dynamic_completion_loader 2017-12-03 23:54:58 +09:00
Junegunn Choi
9c47739c0e Fix panic when replace-query is triggered on empty result set 2017-12-03 23:48:59 +09:00
Junegunn Choi
04aa2992e7 Revert "0.17.2"
This reverts commit 2f1edeff78.
2017-12-03 23:42:38 +09:00
Junegunn Choi
2f1edeff78 0.17.2 2017-12-03 23:34:37 +09:00
Junegunn Choi
306d51cdcf Update tcell to fix double-enter problem on Windows GVim
- Close #1169
- https://github.com/gdamore/tcell/pull/159
2017-12-03 23:32:45 +09:00
Junegunn Choi
54a026525a [vim] Remove unnecessary term_wait workaround
The issue is fixed in 1232624ae5
2017-12-03 23:32:43 +09:00
Junegunn Choi
d6588fc835 [bash-completion] Fix custom completion with dynamic loader enabled
After _completion_loader is called, instead of loading the entire
completion.bash file, just restore the fzf completion for the current
command. `_fzf_orig_completion_$cmd` is only set if _completion_loader
actually changed the completion options to avoid infinite loop.

Close #1170
2017-12-03 23:32:41 +09:00
Junegunn Choi
5a7b41a2cf Add accept-non-empty action
'accept-non-empty' is similar to 'accept' (which is bound to 'enter' and
'double-click' by default) but it prevents fzf from exiting without any
selection.

Close #1162
2017-12-02 02:28:36 +09:00
Junegunn Choi
338a73d764 [man] Describe 'cancel' action 2017-12-01 19:13:51 +09:00
Junegunn Choi
c20954f020 Add replace-query action
replace-query action replaces the query string with the current
selection. If the selection is too long, it will be truncated.

If the line contains meta-characters of fzf search syntax, it is
possible that the line is no longer included in the updated result.

e.g.

  echo '!hello' | fzf --bind ctrl-v:replace-query

Close #1137
2017-12-01 13:08:55 +09:00
Junegunn Choi
1e8e1d3c9d Fix test case on older versions of Ruby 2017-12-01 13:03:02 +09:00
Junegunn Choi
f6b1962056 Inject $LINES and $COLUMNS when running preview command
Close #1168
2017-12-01 03:28:10 +09:00
Junegunn Choi
b3b101a89c Support binding of left-click and right-click
left-click and right-click are respectively bound to "ignore" and
"toggle" (after implicitly moving the cursor) by default.

Close #1130
2017-12-01 03:28:08 +09:00
Junegunn Choi
9615c4edf1 Fix test case for invalid FZF_DEFAULT_COMMAND 2017-12-01 02:22:36 +09:00
Junegunn Choi
85a75ee035 Revert default command: find with -fstype required
In #1061 we changed the default command to retry with a simpler find
command with fewer arguments if the first find command failed. This was
to support stripped-down verions of find that do not support -fstype
argument.

However, this caused an unwanted side-effect of yielding duplicate
entries when the first command failed after producing some lines.

We revert the change in this commit, so the default command will not
work with find without -fstype support. But we now print better error
message in that case so that the user can set up a working
$FZF_DEFAULT_COMMAND.

Close #1120 #1167
2017-12-01 01:40:42 +09:00
Junegunn Choi
1e5bd55672 [install] Change the order of case patterns for $archi (#1060)
/cc @ehandal
2017-11-27 15:44:19 +09:00
Jan Edmund Lazo
37d4015d56 [vim] Don't use :terminal on msys2 or Cygwin (#1155)
Close #1152

msys2 terminal Vim assumes that it runs in mintty
so `:terminal` uses `TERM=xterm`.
fzf doesn't support `TERM=xterm` on Windows.
2017-11-22 13:34:02 +09:00
Junegunn Choi
6b27554cdb Clarify installation instructions 2017-11-22 03:10:20 +09:00
Junegunn Choi
fc1b119159 [vim] Add instruction to hide statusline of terminal buffer (#1143) 2017-11-19 12:07:54 +09:00
Aaron Jensen
2cd0d4a9f7 [zsh] Fire zsh precmd functions after cd (#1136)
Fixes #915
2017-11-14 12:43:52 +09:00
Elliott Sales de Andrade
fd03aabeb2 Add Fedora installation information (#1141) 2017-11-14 12:40:17 +09:00
Justin Toniazzo
8068c975c2 Fix broken link in readme TOC (#1131)
The `Respecting .gitignore` link pointed to a section of the readme which no longer exists.
2017-11-08 23:54:46 +09:00
Junegunn Choi
a6d2ab3360 Update README: Examples using fd
- https://github.com/sharkdp/fd
- https://mike.place/2017/fzf-fd/

/cc @williamsmj
2017-10-29 23:48:55 +09:00
Adam Dinwoodie
fe7b91dfd9 Add bin/fzf.exe to .gitignore (#1111)
On Cygwin and MinGW, the fzf binary will have a .exe extension, so
ignore that binary if it exists as well as the bare binary.
2017-10-27 09:12:12 +09:00
Junegunn Choi
5784101bea Suggest ripgrep instead of the silver searcher
Since https://github.com/BurntSushi/ripgrep/issues/200 is fixed in
0.7.1, we can safely suggest ripgrep as the candidate generator as it
has a more precise implementation of gitignore filtering than the silver
searcher.
2017-10-23 13:19:11 +09:00
Igor Urazov
eaf6eb8978 [completion] Ensure ps called as command (#1098)
When `ps` is aliased for something uncommon, like `alias ps=grc ps` which colorizes ps output, the output of `ps` can be unexpected and/or undesired.

This change makes ps to be always executed as command, even if it's aliased.
2017-10-21 10:31:34 +09:00
Daniel Schaffrath
3af63bcf1f [zsh] Use fc -r instead of fzf --tac to speed up loadtime (#1097)
Reference: http://zsh.sourceforge.net/Doc/Release/Shell-Builtin-Commands.html

> The flag -r reverses the order of the events
2017-10-20 12:56:02 +09:00
Andrey Chernih
80a21f7a75 [completion] Fix known_hosts completion for custom port number (#1092)
Handles records like "[20.20.7.168]:9722 ssh-rsa ..."

This is a standard format for servers running on custom port according to http://man.openbsd.org/sshd.8#SSH_KNOWN_HOSTS_FILE_FORMAT

    A hostname or address may optionally be enclosed within ‘[’ and ‘]’
    brackets then followed by ‘:’ and a non-standard port number.
2017-10-19 22:04:32 +09:00
Junegunn Choi
0b33dc6ce1 0.17.1 2017-10-16 01:58:57 +09:00
Junegunn Choi
64a6ced62e Do not immediately check --height option on Windows (#1082) 2017-10-15 19:02:05 +09:00
Junegunn Choi
438f6c96cd Fix compilation error of Windows binary 2017-10-15 18:32:59 +09:00
Junegunn Choi
6ae085f974 Add link to Windows wiki page
Related: #1072
/cc @janlazo
2017-10-15 17:44:25 +09:00
Junegunn Choi
cb8e97274e Update README to add an example of _fzf_compgen_dir
/cc @chrisjohnson

Close #1067
Close #1083
2017-10-14 16:18:46 +09:00
Jan Edmund Lazo
c4185e81e8 Fix ExecCommandWith for cmd.exe in Windows (#1072)
Close #1018

Run the command as is in cmd.exe with no parsing and escaping.
Explicity set cmd.SysProcAttr so execCommand does not escape the command.
Technically, the command should be escaped with ^ for special characters,
including ". This allows cmd.exe commands to be chained together.

See https://github.com/neovim/neovim/pull/7343#issuecomment-333350201

This commit also updates quoteEntry to use strings.Replace instead of
strconv.Quote which escapes more than \ and ".
2017-10-14 15:26:37 +09:00
Ionel Cristian Mărieș
0580fe9046 Don't do shell quoting for weird chars (#1079)
* Don't do shell quoting for weird chars

This would prevent tabs from being escaped as `$'\t'` (definitely not what I would want to see as initial value in the search).

* Do different escape.
2017-10-10 12:27:01 +09:00
Junegunn Choi
1b1bc9ea36 [install] Download arm8 binaries on Linux aarch64
Close #1060
2017-10-08 03:52:55 +09:00
Junegunn Choi
c2614467cf [neovim] Fix Neovim plugin to use terminal instead of --height
Close #1066
Close #1068
2017-09-30 22:13:43 +09:00
Junegunn Choi
077ae51f05 [vim] Use Vim 8 terminal when appropriate
Close #1055
2017-09-29 01:11:00 +09:00
Junegunn Choi
ee40212e97 Update FZF_DEFAULT_COMMAND
- Use bash for `set -o pipefail`
- Fall back to simpler find command when the original command failed

Related: #1061
2017-09-28 23:05:02 +09:00
Ricardo González
7f5f6efbac [fzf-tmux] Executes fzf from fzf-tmux with a process name (#1056) 2017-09-28 22:23:44 +09:00
Josh Pencheon
45d4c57d91 [completion] Include host aliases in ssh completion (#1062) 2017-09-27 00:18:01 +09:00
Robert Orzanna
41e0208335 Update Homebrew/Linuxbrew instructions (#1052) 2017-09-17 17:12:20 +09:00
Lawrence Wu
2f8238342b [install] Don't touch dotfiles if not requested (#1048) 2017-09-11 09:18:26 +09:00
Junegunn Choi
e1582b8323 Clean up renderer code
Remove code that is no longer relevant after the removal of ncurses
renderer. This commit also fixes background color issue on tcell-based
FullscreenRenderer (Windows).
2017-09-09 23:20:42 +09:00
Junegunn Choi
7cfa6f0265 Fix custom foreground color inside preview window (addendum)
This fixes foreground color inside preview window when the text has ANSI
attributes except for foreground color.

Close #1046
2017-09-08 18:33:17 +09:00
Junegunn Choi
e3973c74e7 Fix custom foreground color inside preview window
Close #1046
2017-09-08 18:18:54 +09:00
Junegunn Choi
a8deca2dd9 [vim] Update README-VIM: fzf can run inside GVim 2017-09-07 12:42:40 +09:00
Junegunn Choi
a78ade1771 Update link to performance chart 2017-09-07 12:38:34 +09:00
Jan Edmund Lazo
79d2ef4616 [vim] Do not pathshorten prompt in cygwin (#1043)
Prevents the following case:
before pathshorten - /usr/bin
after pathshorten - /u/bin
piped to cmd.exe - U:/bin
2017-09-07 11:03:26 +09:00
Junegunn Choi
5edc3f755c [vim] Update FZF command not set up lengthy prompt on narrow screen
Port of e7928d154a

Since :FZF does not enable preview window, we determine based on full
&columns instead of &columns / 2.
2017-09-07 11:01:40 +09:00
Junegunn Choi
288976310b Update g:fzf_colors example 2017-09-06 10:44:25 +09:00
Junegunn Choi
58b5be8ab6 0.17.0-2 2017-09-05 13:40:58 +09:00
Jan Edmund Lazo
26d7896877 [vim] Bind Ctrl-J in Vim terminal to fix enter key
Temporary workaround for non-Windows environment

Reference:
https://github.com/vim/vim/issues/1998
https://github.com/junegunn/fzf/pull/1019#issuecomment-327008348
2017-09-05 13:29:46 +09:00
Jan Edmund Lazo
fd6bc7308f [vim] Use s:execute_term in Windows
IMPORTANT:
cmd.exe and powershell are fine in default Windows terminal.
cmd.exe prompt is broken on ConEmu because it natively supports ucs-2 only.
utf-16 support is exclusive to .Net (ie. powershell).
utf-8 supports requires chcp, external program, but does not fix the cmd.exe prompt.
Use powershell on ConEmu to avoid corrupted text on display
2017-09-05 13:29:46 +09:00
Jan Edmund Lazo
6c41c95f28 [vim] s:execute_term works in GVim on Windows
Requirements:
- compiled with +terminal
- has patch-8.0.995
- has('gui_running') returns 1
2017-09-05 13:29:46 +09:00
Jan Edmund Lazo
446e04469d [neovim] use batchfile for s:execute_term in Windows 2017-09-05 13:29:46 +09:00
Michael Smith
5097e563df [neovim] Fix terminal buffer marker on Windows
Original Patch: a9bf29b65e
2017-09-05 13:29:46 +09:00
Jan Edmund Lazo
c7ad97c641 [neovim] use terminal in Windows for v0.2.1+ 2017-09-05 13:29:46 +09:00
Junegunn Choi
9516fe3324 [install] Add --no-{bash,zsh,fish}
Close #1040
2017-09-03 11:45:22 +09:00
Junegunn Choi
20cdbac8c3 [install] Ignore user-defined grep aliases 2017-09-03 11:38:22 +09:00
Junegunn Choi
e3e7b3360c Delete ncurses implementation 2017-09-02 03:19:50 +09:00
Junegunn Choi
655dfb8328 [fzf-tmux] Remove cat command
Close #1039
2017-09-01 18:46:00 +09:00
Mike Hearn
9b9c67b768 [fzf-tmux] Add pane_height/pane_width fallback (#1037) 2017-09-01 11:16:00 +09:00
Junegunn Choi
5b7457ff08 [install] Wait for a linefeed when asking for confirmation
Close #1035
2017-09-01 02:45:48 +09:00
Junegunn Choi
48adad5454 [neovim] Set &shell to sh (again) after opening a new window
Close #1031
2017-08-30 18:58:28 +09:00
Jack O'Connor
b27dc3eb17 [vim] Add parens around piped source commands (#1029)
Previously a command like `echo a && echo b` would get transformed into
`echo a && echo b | fzf`, which only pipes the output of the second
command. Adding parentheses around the source command avoids this issue,
and works on both Unix and Windows.
2017-08-28 22:32:13 +09:00
Junegunn Choi
e89eebb7ba 0.17.0 2017-08-27 03:32:21 +09:00
Junegunn Choi
fee404399a Make --expect additive
Similarly to --bind or --color.

--expect used to replace the previously specified keys, and
fzf#wrap({'options': '--expect=f1'}) wouldn't work as expected. It
forced us to come up with some ugly hacks like the following:

13b27c45c8/autoload/fzf/vim.vim (L1086)
2017-08-27 02:19:56 +09:00
Junegunn Choi
6b4805ca1a Optimize rank comparison on x86 (little-endian) 2017-08-27 01:46:11 +09:00
Junegunn Choi
159699b5d7 Remove an unnecessary code branch 2017-08-26 20:09:46 +09:00
Junegunn Choi
af809c9661 Minor refactorings 2017-08-26 03:24:42 +09:00
Junegunn Choi
329de8f416 [fzf-tmux] Execute trap with bash instead of the default shell
Close #1007
2017-08-26 02:51:19 +09:00
Junegunn Choi
e825b07e85 [neovim] Allow running FZF in multiple windows
Close #1023
2017-08-26 01:56:49 +09:00
Junegunn Choi
71fdb99a07 Remove bound checkings in inner loops 2017-08-26 01:28:39 +09:00
Junegunn Choi
55ee4186aa Ignore EvtReadNew if EvtReadFin is already set 2017-08-20 14:30:17 +09:00
Junegunn Choi
941b0a0ff7 Minor optimization of FuzzyMatchV2
Calculate the first row of the score matrix during phase 2
2017-08-20 12:29:11 +09:00
Junegunn Choi
6aae12288e Extract debug code from FuzzyMatchV2 2017-08-20 12:29:11 +09:00
Junegunn Choi
302cc552ef Remove unused clear arguments of alloc16 and alloc32 2017-08-20 12:29:11 +09:00
Junegunn Choi
a2a4df0886 Pass util.Chars by pointer 2017-08-20 12:29:11 +09:00
Jan Edmund Lazo
3399e39968 [vim] Escape backslashes in fzf#shellescape (#1021) 2017-08-20 12:28:36 +09:00
Junegunn Choi
87874bba88 Remove redundant read event when --sync is used 2017-08-20 01:58:51 +09:00
Junegunn Choi
c304fc4333 Delay slab allocation 2017-08-19 12:14:48 +09:00
Junegunn Choi
6977cf268f Limit search scope of uppercase letter 2017-08-18 05:30:13 +09:00
Junegunn Choi
931c78a70c Short-circuit ANSI processing if no ANSI codes are found
Rework of 656963e. Makes --ansi processing around 20% faster on plain
strings without ANSI codes.
2017-08-18 03:04:11 +09:00
Junegunn Choi
8d23646fe6 Revert "Short-circuit ANSI processing if no ANSI codes are found"
This reverts commit 656963e018.
2017-08-17 19:12:44 +09:00
Junegunn Choi
656963e018 Short-circuit ANSI processing if no ANSI codes are found 2017-08-17 19:12:06 +09:00
Junegunn Choi
644277faf1 Linuxbrew can install fzf
Close #1017
2017-08-17 16:57:02 +09:00
Junegunn Choi
0558dfee79 Remove count field from ChunkList 2017-08-16 12:26:06 +09:00
Junegunn Choi
487c8fe88f Make Reader event notification asynchronous
Instead of notifying the event coordinator (EventBox) whenever a new
line is arrived, start a background goroutine that periodically does the
task. Atomic.StoreInt32 is much cheaper than mutex synchronization
that happens during EventBox update.
2017-08-16 03:33:48 +09:00
Junegunn Choi
0d171ba1d8 Remove special nilItem 2017-08-15 01:10:41 +09:00
Junegunn Choi
2069bbc8b5 [vim] Allow Funcref in g:fzf_action
https://github.com/junegunn/fzf.vim/issues/185
2017-08-14 16:23:18 +09:00
Jan Edmund Lazo
053d628b53 Add MinGW 64 to install fzf in Windows 64-bit (#1015) 2017-08-13 23:20:06 +09:00
Junegunn Choi
6bc592e6c9 Update FuzzyMatchV1 to use skip optimization used in V2 2017-08-12 00:28:30 +09:00
Junegunn Choi
6c76d8cd1c Disallow escaping of meta characters except for spaces
https://github.com/junegunn/fzf/issues/444#issuecomment-321719604
2017-08-11 13:09:33 +09:00
Junegunn Choi
a09e411936 Treat | as proper query when it can't be an OR operator 2017-08-11 00:07:18 +09:00
Junegunn Choi
02a7b96f33 Treat $ as proper search query
When $ is the leading character in a query, it's probably not meant to
be an anchor.
2017-08-10 23:59:52 +09:00
Junegunn Choi
e55e029ae8 Build cache key for a pattern only once 2017-08-10 23:18:52 +09:00
Junegunn Choi
6b18b144cf Fix escaping of meta characters after ' or ! prefix
https://github.com/junegunn/fzf/issues/444#issuecomment-321432803
2017-08-10 12:40:53 +09:00
Junegunn Choi
6d53089cc1 Allow escaping term starting with |
Close #444
2017-08-09 23:33:37 +09:00
Junegunn Choi
e85a8a68d0 Allow escaping meta characters with backslashes
One can escape meta characters in extended-search mode with backslashes.

  Prefixes:
    \'
    \!
    \^

  Suffix:
    \$

  Term separator:
    \<SPACE>

To keep things simple, we are not going to support escaping of escaped
sequences (e.g. \\') for matching them literally.

Since this is a breaking change, we will bump the minor version.

Close #444
2017-08-09 23:28:47 +09:00
Junegunn Choi
dc55e68524 Remove unnecessary SCP (Save Cursor Position)
It is reported that it can have an unwanted side effect of clearing the
screen on terminal emulators that do not properly support it.

Patch suggested by @arya.

Close #1011
2017-08-09 01:58:29 +09:00
Junegunn Choi
462c68b625 [vim] Fix issues with other plugins changing working directory
Close #1005
2017-08-09 01:54:01 +09:00
Junegunn Choi
999d374f0c Fix invalid cache lookups 2017-08-08 13:23:33 +09:00
Junegunn Choi
b208aa675e Update Travis build to run on Trusty 2017-08-05 04:28:43 +09:00
Junegunn Choi
2b98fee136 Fix Travis CI build
tcell build is commented out as it doesn't reliably respond to tmux
send-keys.
2017-08-05 04:01:17 +09:00
Junegunn Choi
e5e75efebc [vim] Fix vader test cases 2017-08-04 19:25:06 +09:00
Junegunn Choi
4a4fef2daf Update performance comparison chart 2017-08-04 09:28:29 +09:00
Junegunn Choi
ecb6b234cc 0.16.11 2017-08-02 02:50:28 +09:00
Junegunn Choi
39dbc8acdb Exit 2 instead of panic when failed to open /dev/tty 2017-08-02 02:50:26 +09:00
Junegunn Choi
a56489bc7f Remove non-exclusive access to ChunkList field 2017-08-02 00:09:00 +09:00
Junegunn Choi
99927c7071 Modify loop conditions in checkAscii function 2017-08-01 22:04:42 +09:00
Junegunn Choi
3e28403978 [man] Add note on --no- convention
Close #1003
2017-08-01 21:34:44 +09:00
Junegunn Choi
37370f057f Do not use defer in performance-sensitive contexts 2017-08-01 03:44:55 +09:00
Junegunn Choi
f4b46fad27 Inline function calls in a tight loop
Manually inline function calls in a tight loop as Go compiler does not
inline non-leaf functions. It is observed that this unpleasant code
change resulted up to 10% performance improvement.
2017-08-01 03:44:38 +09:00
Junegunn Choi
9d2c6a95f4 Revert "[bash] Do not append space when path completion is cancelled"
This reverts commit 376a76d1d3 as it
affects normal completion
2017-07-31 14:08:17 +09:00
Junegunn Choi
376a76d1d3 [bash] Do not append space when path completion is cancelled
Close #990
2017-07-30 21:51:44 +09:00
Jan Edmund Lazo
1fcc07e54e [vim] Fix escape of backslash in s:shortpath
Close #1000
2017-07-30 20:05:01 +09:00
Junegunn Choi
8db3345c2f Optimize exact match by applying the same trick for fuzzy match 2017-07-30 18:16:54 +09:00
Junegunn Choi
69aa2fea68 Optimize fuzzy search performance for ASCII strings 2017-07-30 17:31:50 +09:00
Junegunn Choi
298749bfcd Update README 2017-07-29 17:12:46 +09:00
Junegunn Choi
f1f31baae1 Update README: Missing TOC 2017-07-29 17:10:00 +09:00
Junegunn Choi
e1c8f19e8f Update README: Advanced topics 2017-07-29 17:09:05 +09:00
Junegunn Choi
5e302c70e9 Update README: rg intead of pt 2017-07-29 17:09:05 +09:00
Junegunn Choi
4c5a679066 Make deselect-all instantaneous 2017-07-28 13:13:03 +09:00
Andrew Halberstadt
41f0b2c354 Add MinGW on Windows to install script (#998)
Running uname -sm yields:
MINGW32_NT-6.2 i686
2017-07-28 12:22:33 +09:00
Junegunn Choi
a0a3c349c9 Update preview window when selection has changed
Close #995
2017-07-28 01:39:25 +09:00
Alexey Shamrin
bc3983181d Update fish comments, because 2.6.0 was released (#991) 2017-07-25 19:10:34 +09:00
Junegunn Choi
980b58ef5a Update README
Removed outdated animated GIF.
2017-07-23 22:07:24 +09:00
Junegunn Choi
a2604c0963 [nvim] Disable number in fzf buffer
https://github.com/junegunn/fzf.vim/issues/396#issuecomment-317214036

One can override the setting on FileType fzf autocmd.
2017-07-23 13:12:15 +09:00
Junegunn Choi
6dbc108da2 0.16.10 2017-07-21 18:41:11 +09:00
Junegunn Choi
bd98f988f0 Further reduce unnecessary rune array conversion
I was too quick to release 0.16.9, this commit makes --ansi processing
even faster.
2017-07-21 17:31:11 +09:00
Junegunn Choi
06301c7847 Fix regression: ANSI color in preview window not cleared 2017-07-21 16:44:59 +09:00
Junegunn Choi
18a1aeaa91 0.16.9 2017-07-21 00:08:55 +09:00
Junegunn Choi
c9f16b6430 Avoid unconditionally storsing input as runes
When --with-nth is used, fzf used to preprocess each line and store the
result as rune array, which was wasteful if the line only contains ascii
characters.
2017-07-20 02:44:30 +09:00
Junegunn Choi
bc9d2abdb6 Improve preview window rendering
- Fix incorrect display of the last line when more than a line is
  wrapped above
- Avoid unnecessary flickering of the window
2017-07-19 22:47:15 +09:00
Junegunn Choi
28810c178f Optimize ANSI code scanner
This change gives 5x speed improvement
2017-07-19 21:49:41 +09:00
Junegunn Choi
a9e64efe45 Fix regression: output printed on alternate screen 2017-07-19 13:17:06 +09:00
Junegunn Choi
6b5886c034 Adjust --no-clear option for repetitive relaunching
Related: https://gist.github.com/junegunn/4963bab6ace453f7f529d2d0e01b1d85

Close #974
2017-07-18 21:10:49 +09:00
Junegunn Choi
7727ad43af [vim] Use fnameescape to escape command line arguments
Fix https://github.com/junegunn/fzf.vim/issues/404

Thanks to @janlazo.
2017-07-18 16:33:58 +09:00
Junegunn Choi
bbe10f4f77 Consolidate Result and rank structs
By not storing item index twice, we can cut down the size of Result
struct and now it makes more sense to store and pass Results by values.
Benchmarks show no degradation of performance by additional pointer
indirection for looking up index.
2017-07-18 03:14:33 +09:00
Junegunn Choi
5e72709613 Speed up initial scanning with bitwise AND operation 2017-07-18 02:17:05 +09:00
Junegunn Choi
9e85cba0d0 Reduce memory footprint of Item struct 2017-07-16 23:34:32 +09:00
Junegunn Choi
4b59ced08f Add gopath to gitignore 2017-07-16 23:34:32 +09:00
Junegunn Choi
8dbdd55730 Refactor cache lookup
- Remove multiple mutex locks in partial cache lookup
- Simplify return values
2017-07-16 23:34:32 +09:00
Junegunn Choi
6725151a99 Remove unnecessary copy of Chunk slice 2017-07-16 23:34:32 +09:00
Junegunn Choi
d4f3d5a164 Remove pointer indirection by changing Chunk definition 2017-07-16 23:34:32 +09:00
Tom Fitzhenry
7b5ccc45bc [fish] Fix ctrl-r regression in versions <2.4 (#972)
Close #966
2017-07-15 18:50:23 +09:00
Jan Edmund Lazo
940214a1a2 [neovim] Fix lcd when fzf job exits on Windows (#970)
Related: #960 (relative filepaths)
2017-07-10 02:06:13 +09:00
Jan Edmund Lazo
68bd410159 [vim] Don't pipe FZF_DEFAULT_COMMAND in Windows (#969)
Related #960, #552
2017-07-09 13:08:16 +09:00
Junegunn Choi
b13fcfd831 Add missing --no-expect flag 2017-07-04 23:02:15 +09:00
Junegunn Choi
07ef2b051c Print [ERROR] on info line when the default command failed
With zero result.

Related: https://github.com/junegunn/fzf.vim/issues/22#issuecomment-311869805
2017-07-01 01:13:15 +09:00
Junegunn Choi
3fc795340d Fix test failulre with non-zero pane-base-index 2017-07-01 01:05:47 +09:00
John Nguyen
70cfa6af13 [fish] Accept starting dir for <M-c> key binding (#944)
This also modifies <C-t> behaviour.
The longest file path in the input is used as root directory for `find`
command. The remainder of the input is passed to fzf's --query as a
initial search parameters.
2017-06-25 21:16:15 +09:00
Tom Fitzhenry
dbcaec59ae [fish] Support multiline commands (#954)
Fix found by @amosbird at https://github.com/junegunn/fzf/issues/953#issuecomment-310309055

closes #440
2017-06-25 21:09:51 +09:00
Junegunn Choi
faedae708e Fix FZF_CTRL_T_COMMAND example for fish
See #944
2017-06-23 01:50:45 +09:00
Junegunn Choi
0c66521b23 Fix handling of bracketed paste mode
fzf should immediately continue consuming the buffer after discarding
bracketed paste mode sequence.

Close #951
2017-06-22 02:35:57 +09:00
Junegunn Choi
bf92862459 Update man page: missing name "border" for --color 2017-06-20 14:15:11 +09:00
John Nguyen
1a68698d76 [fish] Fix <C-t> completion for current dir search (#946)
If "." is given as the argument to begin <C-t> completion, the leading
"." is not correctly removed. In general, if user selects a fzf
completion, the current token should be "consumed".
2017-06-12 18:24:45 +09:00
Junegunn Choi
842a73357c [fish] Fix CTRL-T with paths that don't start with ./
Close #943
2017-06-10 13:35:24 +09:00
Junegunn Choi
5efdeccdbb [vim] Expand 'dir' on Cygwin to handle Windows-style paths
See https://github.com/junegunn/fzf/pull/933#discussion_r120011934

Close https://github.com/junegunn/fzf.vim/pull/386
2017-06-09 12:00:59 +09:00
Jan Edmund Lazo
050777b8c4 [vim] Uncomment test case to escape % in cmd.exe (#941) 2017-06-08 10:25:35 +09:00
Uri Gorelik
a4d78e2200 Update CHANGELOG with unix-line-discard+top (#940)
Also change the example binding for `unix-word-rubout` to *ctrl-w* instead of *ctrl-u*
2017-06-08 10:02:34 +09:00
Junegunn Choi
b49f22cdf9 0.16.8 2017-06-05 23:21:50 +09:00
Jan Edmund Lazo
7e483b0c25 [vim] Add support for Cygwin (#933) 2017-06-05 13:54:47 +09:00
Junegunn Choi
3cf9ae04c7 [fzf-tmux] Fix cleanup of temporary files
Close #935
2017-06-04 23:24:57 +09:00
Junegunn Choi
bf0cb4bfe2 Use find as the default command on Cygwin environment 2017-06-04 16:23:47 +09:00
Junegunn Choi
773133c4ce [vim] Allow running install --bin on Cygwin 2017-06-04 15:15:46 +09:00
Junegunn Choi
ca0b3b6fd7 Fixes for Cygwin
- Update install script to download Windows binary if $TERM == cygwin
- Unset TERM if $TERM == cygwin (#933)
- Always use cmd.exe instead of $SHELL when running commands
2017-06-03 19:47:53 +09:00
Junegunn Choi
f4731c0514 Merge branch 'master' into devel 2017-06-03 19:42:26 +09:00
Junegunn Choi
34f16e5b7d Fix Makefile and install script for the new project layout 2017-06-02 18:19:21 +09:00
Junegunn Choi
83e9af6601 Add git revision to --version output 2017-06-02 17:59:12 +09:00
Junegunn Choi
8bbf9335e1 Restructuring: main package in project root 2017-06-02 17:59:01 +09:00
Junegunn Choi
159f30b37f Merge branch 'glide' of https://github.com/hinshun/fzf into hinshun-glide 2017-06-02 13:35:40 +09:00
Junegunn Choi
2e3dc75425 Fix inconsistent tiebreak scores when --nth is used
Make sure to consistently calculate tiebreak scores based on the
original line.

This change may not be preferable if you filter aligned tabular input on
a subset of columns using --nth. However, if we calculate length
tiebreak only on the matched components instead of the entire line, the
result can be very confusing when multiple --nth components are
specified, so let's keep it simple and consistent.

Close #926
2017-06-02 13:25:35 +09:00
Edgar Lee
7d3575b362 Use glide to handle go dependencies 2017-06-01 17:08:47 -07:00
Junegunn Choi
35d407021c [vim] Replace invalid s:escape calls with fzf#shellescape 2017-05-31 23:59:11 +09:00
Junegunn Choi
076f49d447 [vim] Make sure to delete temporary batchfile on Windows 2017-05-31 10:03:23 +09:00
Junegunn Choi
0665fe0413 [vim] Remove unnecessary ternary expression
Related: https://github.com/junegunn/fzf.vim/issues/378
2017-05-31 10:02:04 +09:00
Jan Edmund Lazo
669a6fee40 [vim] Use utf-8 for cmd.exe (#929) 2017-05-31 09:56:01 +09:00
Jan Edmund Lazo
8aab0fc189 [vim] Replace s:fzf_shellescape and s:shellesc with fzf#shellescape (#916) 2017-05-29 10:06:06 +09:00
Junegunn Choi
5d6eb5bfd6 Respect ANSI color state from the previous line in preview output 2017-05-28 02:26:42 +09:00
Junegunn Choi
cf4711d878 Fix display of tab characters in --prompt 2017-05-26 19:02:49 +09:00
Junegunn Choi
21d664d670 Update extra bash completion example 2017-05-25 19:09:04 +09:00
Tw
ab182e276b Use read syscall directly to get character (#931)
Due to go std lib uses poller for os.File introducing in this commit:
c05b06a12d
There are two changes to watch out:
1. os.File.Fd will always return a blocking fd except on bsd.
2. os.File.Read won't return EAGAIN error for nonblocking fd.

So
For 1, we just get tty's fd in advance and then set its block mode.
For 2, we use read syscall directly to get what we wanted error(EAGAIN).

Fix issue #910.

Signed-off-by: Tw <tw19881113@gmail.com>
2017-05-25 01:36:59 +09:00
Junegunn Choi
96a3250152 Update test case for --cycle 2017-05-24 13:20:13 +09:00
Junegunn Choi
f5746002fd Do not "--cycle" on page-up/page-down
Close #928
2017-05-24 02:43:39 +09:00
Junegunn Choi
e1e3339770 Implement bindable "change" event and "top" action
# Move cursor to the top result whenever the query string is changed
    fzf --bind change:top

Close #925
2017-05-22 17:07:05 +09:00
Junegunn Choi
3a5086796d [vim] Prevent 'wildignore' from affecting expand() (#917) 2017-05-22 01:23:59 +09:00
Junegunn Choi
11300913a4 [vim] Do not expand s:fzf_go
expand() may return an empty string depending on the value of
&wildignore. Since expand('<sfile>') always returns an absolute path, we
can remove expand() call here. Close #917.
2017-05-22 01:04:04 +09:00
Aurelien Rainone
e65f14cbed Update README: Add table of contents (#927) 2017-05-20 19:08:56 +09:00
Theodore Dubois
6898849e3e Mention that the fish bug has been fixed (#912) 2017-05-05 10:48:28 +09:00
Junegunn Choi
2d61691bb2 0.16.7 2017-04-30 11:54:40 +09:00
Junegunn Choi
eba9e04e2e Export FZF_PREVIEW_HEIGHT instead of FZF_HEIGHT
https://github.com/junegunn/fzf.vim/issues/361
2017-04-30 11:36:23 +09:00
Junegunn Choi
33f32de690 Merge branch 'master' into devel 2017-04-30 11:23:42 +09:00
Junegunn Choi
93b8f61551 [vim] Export $FZF_HEIGHT for previewer scripts
Preview script cannot properly determine the height of fzf finder if
`--height` option is used.

https://github.com/junegunn/fzf.vim/issues/361
2017-04-30 11:18:56 +09:00
Junegunn Choi
7f17a9d1b4 Update mattn/go-shellwords 2017-04-30 00:47:44 +09:00
Junegunn Choi
d34e4cf698 Support CTRL-Z (SIGSTOP) 2017-04-28 22:58:08 +09:00
Junegunn Choi
6b592137b9 Add support for ctrl-alt-[a-z] key chords
Close #906
2017-04-28 02:36:36 +09:00
Junegunn Choi
d5e72bf55d Update README-VIM: options as list (#896) 2017-04-28 02:09:55 +09:00
Junegunn Choi
5677e5e133 [fish] Fix ~/.config/fish/functions/fish_user_key_bindings.fish
Install script will create the file with the proper function body only
if the file doesn't exist. If it already exists, it will try to append
`fzf_key_bindings` as before.

Close #851
2017-04-28 01:57:38 +09:00
Jan Edmund Lazo
7a11a06cbd [vim] Use backslash for Windows filepaths (#896)
- Fix shellescaping issues for filepaths
    - Supports both forward slashes or backslashes
    - Paths with spaces
- Use jobstart for neovim in s:execute (Windows)
    - https://github.com/neovim/neovim/pull/6497
- Make 2 s:fzf_shellescape functions
    - (Windows) Substitute \" with \\" to escape the last backslash
    - (Default) Regular shellescape
- Support list 'options'
- Add "@echo off" to the batchfile used to execute fzf
2017-04-22 11:30:51 +09:00
Junegunn Choi
a5862d4b9c [bash-completion] Use -o dirnames instead of -o plusdirs
Close #903
Related #135
2017-04-11 22:21:16 +09:00
Junegunn Choi
a50909e806 Correction: fzf no longer depends on ncurses 2017-04-06 02:08:49 +09:00
Kouki Higashikawa
7c8f7d3f20 [fzf-tmux] Close with exit code 130 when tmux pane is killed
Fix #796
2017-04-03 11:49:54 +09:00
Junegunn Choi
9078197446 Add --version to --help output and man page
Close #888
Close #894
2017-04-02 11:30:22 +09:00
Junegunn Choi
0fe07cf9fe Update README.md
Add PayPal donation button
2017-04-02 10:47:06 +09:00
Junegunn Choi
2216169ca1 Update doc/fzf.txt accordingly 2017-04-01 12:19:39 +09:00
Junegunn Choi
50e989ca85 Update example in README-VIM 2017-04-01 12:06:25 +09:00
Junegunn Choi
fa1fc3d855 Add vim doc
Close #893
2017-04-01 12:00:30 +09:00
五所和哉
bbe696e925 [fzf-tmux] Fix issue with zoomed pane on fish (#891) 2017-04-01 11:09:46 +09:00
Miodrag Milić
5d12f523a3 Add chocolatey upgrade instruction to Readme (#890) 2017-03-30 01:59:41 +09:00
Daniel Hahler
d295d20dc4 fzf#run: improve "is already running" message (#885)
This displays the buffer(s) in this case, which is useful when FZF got
stuck, and you have to manually remove the buffer.
2017-03-27 13:41:39 +09:00
Sam Van Den Berge
2ba10071c9 Add support for IPv6 addresses in ssh completion (#877)
Signed-off-by: Sam Van Den Berge <sam@drgt.net>
2017-03-21 01:06:13 +09:00
Christian Sturm
505dc0491b Make install script to work with non GNU tar (#871) 2017-03-10 23:22:37 +09:00
Junegunn Choi
54a4ab0f26 Add Chocolatey instruction
Thanks to @majkinetor. Close #869.
2017-03-07 23:03:14 +09:00
Junegunn Choi
e03e91477b 0.16.6 2017-03-05 03:05:06 +09:00
Junegunn Choi
88ac397158 Add test case for --no-clear 2017-03-04 14:26:47 +09:00
Junegunn Choi
6fd4be580b Use alternate screen only when the value of height is 100%
Do not automatically decide to use alternate screen when the value of
height exceeds the height of the terminal.

    # Use alternate screen
    fzf
    fzf --height 100%
    fzf --no-height

    # Still use current screen
    fzf --height 10000
2017-03-04 14:09:36 +09:00
Junegunn Choi
53348feb89 Add --no-clear option 2017-03-04 11:29:31 +09:00
Junegunn Choi
337cdbb37c [zsh] Use setopt noposixbuiltins instead of emulate -L zsh
Close #858
3a6af27586 (commitcomment-21135641)
2017-03-03 19:09:29 +09:00
Junegunn Choi
05fdf91fc5 Revert "[zsh] emulate -L zsh to avoid issues with incompatible options"
This reverts commit 3a6af27586.
2017-03-03 18:57:22 +09:00
Junegunn Choi
c387689d1c [shell] Enable sorting by default in CTRL-R
CTRL-R binding used to start with --no-sort to list the matched commands
in chronological order. However, it has been a constant source of
confusion. Let's enable it by default from now on. The sorted result
shouldn't be too confusing as we use --tiebreak=index.
2017-03-03 12:20:01 +09:00
Junegunn Choi
cb9238dc4e Display -S if sort is disabled and toggle-sort is used
This is to address a common confusion that one does not realize that
sorting is intentionally turned off by default and can be enabled by
a bind key.
2017-03-03 02:26:30 +09:00
Junegunn Choi
a484811f78 [vim] Capitalize exception messages 2017-03-02 14:17:59 +09:00
Junegunn Choi
111d1934c4 [vim] Throw error if g:fzf_layout is incorrectly used
https://github.com/junegunn/fzf.vim/issues/327
https://github.com/junegunn/fzf.vim/issues/317
2017-03-02 14:14:57 +09:00
Junegunn Choi
972fb1a29d Suppress ANSI colors in preview window if --no-color is set 2017-03-02 12:49:51 +09:00
Junegunn Choi
3a6af27586 [zsh] emulate -L zsh to avoid issues with incompatible options
Close #858
2017-03-01 16:07:04 +09:00
Junegunn Choi
c89ac341e4 Clear background even if background color is not set
This is needed when fzf is started from inside a program (e.g. Vim)
and it uses a different background color than the terminal.

- https://github.com/junegunn/fzf.vim/issues/325
- https://github.com/junegunn/fzf.vim/issues/300
2017-03-01 16:00:08 +09:00
Junegunn Choi
cd59e5d07b [neovim] Set 'dir' to the current direcotry
Close https://github.com/junegunn/fzf.vim/issues/308
2017-02-25 23:52:56 +09:00
Junegunn Choi
0b940e4b2b Redraw item if query string has changed 2017-02-24 02:30:11 +09:00
Junegunn Choi
b29375c844 [vim] Minor refactoring 2017-02-19 20:53:12 +09:00
Junegunn Choi
76d3f6d248 [vim] Escape ! when using :! to execute command
- call fzf#run({'source': "echo '!'"})
- call fzf#run({'source': "echo '!'", 'down': '40%'})

Close https://github.com/junegunn/fzf.vim/issues/315
2017-02-19 20:47:44 +09:00
Junegunn Choi
e87a85a179 0.16.5 2017-02-19 01:40:25 +09:00
Junegunn Choi
11407bf656 Exclude sysfs in find commands 2017-02-19 01:33:13 +09:00
Junegunn Choi
c82fb3c9b9 Add toggle-preview-wrap action 2017-02-18 23:49:00 +09:00
Junegunn Choi
309e1d8619 Properly truncate long query string 2017-02-18 23:17:29 +09:00
Junegunn Choi
c2db67c1c0 [vim] Prepend @echo off to $FZF_DEFAULT_COMMAND on Windows (#847) 2017-02-18 21:58:03 +09:00
Junegunn Choi
9526594905 [vim] Fix FZF_DEFAULT_COMMAND on Windows
Close #847. Patch submitted by @wontoncc.
2017-02-18 18:17:37 +09:00
Junegunn Choi
3d74d277aa Use cut instead of sed in the default command 2017-02-17 13:07:45 +09:00
Junegunn Choi
fc274c2ba4 [vim] Do not escape % when using system() instead of !
Close https://github.com/junegunn/fzf.vim/issues/309
2017-02-17 10:20:39 +09:00
Pierre Neidhardt
ce43ea9f42 [shell] Replace sed with -mindepth 1 and cut (#844) 2017-02-16 17:18:01 +09:00
Junegunn Choi
21da02fac2 Fix indentation 2017-02-14 22:30:09 +09:00
Junegunn Choi
19569bd5c5 Move cursor to the top-left when returning to alternate screen
Fix broken preview border. Reported by Thomas Sattler.

    fzf --bind 'enter:execute(date)' --preview=date --reverse
2017-02-14 22:28:04 +09:00
Daniel Gray
afa25d8c57 [zsh] Do not cd when cancelling alt+c keybind (#840) 2017-02-09 14:05:02 +09:00
Junegunn Choi
1ba7acf4bd [fzf-tmux] Fix race condition when using -l/-u on zoomed panes
Using a dummy command that works as the barrier.
2017-02-08 14:55:51 +09:00
Prabir Shrestha
a847fe8754 Use "type" instead of "cat" on windows (#836) 2017-02-07 14:42:08 +09:00
Junegunn Choi
5bb18b6441 Remove Dockerfiles and clean up Makefile
Due to the recent removal of ncurses dependency, we can cross-compile
binaries for different platforms without virtual machines.
2017-02-06 21:15:29 +09:00
Junegunn Choi
876c233a26 Remove Ruby version
Related #832
2017-02-06 21:06:12 +09:00
Junegunn Choi
ee5aeb80a4 0.16.4 2017-02-05 16:17:54 +09:00
Junegunn Choi
02ceae15a2 [vim] Download instruction for Windows 2017-02-05 02:07:54 +09:00
Junegunn Choi
e514739280 Fix failing test case 2017-02-04 22:49:17 +09:00
Junegunn Choi
72265298f9 [vim] Apply --no-height when running fzf in full screen mode
To override --height option in FZF_DEFAULT_OPTS
2017-02-04 21:52:05 +09:00
Junegunn Choi
4b700192c1 Add --border option to draw horizontal lines above and below the finder
Goes well with --height
2017-02-04 21:51:22 +09:00
Junegunn Choi
fe83589ade Add test case for --tiebreak=begin 2017-02-03 02:14:14 +09:00
Junegunn Choi
fcf63c74f1 Fix --tiebreak=begin with algo v2
Due to performance consideration, FuzzyMatchV2 does not return the exact
positions of the matching characters by default. However, the ommission
caused `--tiebreak=begin` to produce inaccurate result in some cases.

  (echo baz foo bar; echo foo bar baz) | fzf --tiebreak=begin -fbar | head -1

  # Expected: foo bar baz
  # Actual:   baz foo bar

This commit fixes the problem by using the end offset which is
guaranteed to be correct.
2017-02-02 13:46:46 +09:00
Junegunn Choi
c95bb109c8 Suppress CSI codes in the output 2017-02-02 13:14:27 +09:00
Junegunn Choi
bd9c46ee34 Update ANSI processor to strip ^H along with its preceding character 2017-02-02 13:00:41 +09:00
Junegunn Choi
736aeaa1d3 Update go-runewidth
https://github.com/junegunn/go-runewidth/pull/1

/cc @joshuarubin
2017-02-02 10:08:56 +09:00
Junegunn Choi
dd1f26522c Fix caching scheme when --exact is set and '-prefix is used 2017-02-01 02:06:56 +09:00
Kassio Borges
712b7b2188 [vim] Expose buffer variable with the current fzf setup (#828)
Exposing the `b:fzf` variable will be useful to get information about
which command is being executed on the current fzf window. With that,
now, it's possible to use the current command name on the statusline:

```viml
au User FzfStatusLine call <SID>fzf_statusline()

function! s:fzf_statusline()
  let fzf_cmd_name = get(b:fzf, 'name', 'FZF')
  let &l:statusline = '> '.fzf_cmd_name
endfunction
```
2017-02-01 01:06:52 +09:00
Junegunn Choi
5b749e2d5c Update documentation 2017-01-31 21:43:41 +09:00
Junegunn Choi
d85a69a709 0.16.3 2017-01-30 01:53:17 +09:00
Junegunn Choi
a425e96fb2 [vim] g:fzf_prefer_tmux for choosing fzf-tmux over --height
https://github.com/junegunn/fzf.vim/issues/296
2017-01-30 01:47:30 +09:00
Junegunn Choi
7763fdf6ba Update man pages 2017-01-30 01:27:12 +09:00
Junegunn Choi
dd156b59fc Fix display issues with execute action
- Move cursor to the top-left corner when starting a command in
  alternate screen
- Fix cursor position when returning to alternate screen when fzf is
  running in full screen mode
2017-01-30 01:08:07 +09:00
Junegunn Choi
36dceecd58 Add support for ctrl-space key
Close #825
2017-01-28 02:54:47 +09:00
Junegunn Choi
421b9b271a Add execute-silent action
Close #823
2017-01-27 18:56:41 +09:00
Junegunn Choi
ed57dcb924 Extend placeholder expression for multiple selections
Close #788
2017-01-27 16:38:42 +09:00
Junegunn Choi
95c77bfb98 Use --bind instead of --toggle-sort
Related #822
2017-01-26 11:54:08 +09:00
Junegunn Choi
2e3e721344 Merge branch 'devel' 2017-01-26 11:52:24 +09:00
Junegunn Choi
da2c28d5c2 Add --read0 and --print0 to --help output
Close #822
2017-01-26 11:41:20 +09:00
Junegunn Choi
dbddee9de9 [fish] Add toggle-sort back to CTRL-R (#759) 2017-01-25 10:21:14 +09:00
Junegunn Choi
8731d75607 Recalculate the width of trimmed line
Close #821
2017-01-25 02:39:49 +09:00
Junegunn Choi
f2ce233a6d 0.16.2 2017-01-24 00:37:47 +09:00
Junegunn Choi
6a75e30941 Allow invisible preview window (--preview-window 0)
Close #820
2017-01-24 00:23:16 +09:00
Junegunn Choi
a3244c4892 Delete every line below the cursor 2017-01-23 22:07:18 +09:00
Junegunn Choi
a5ad8fd3bd Minor refactoring 2017-01-23 12:55:13 +09:00
Junegunn Choi
deccdb1ec5 Cursor postition response can be preceded by user key strokes 2017-01-23 12:55:11 +09:00
Junegunn Choi
12a43b5e62 Disable mouse if failed to query cursor position 2017-01-23 12:55:04 +09:00
Junegunn Choi
e1291aa6d2 Fix make deps to see the right git dir 2017-01-23 12:10:43 +09:00
Junegunn Choi
bb26f32ac7 Allow build on OpenBSD/FreeBSD/Android
Close #497
2017-01-22 18:51:04 +09:00
Junegunn Choi
4d928001b8 Update release script to upload assets in parallel 2017-01-22 18:33:30 +09:00
Junegunn Choi
c4baa6a10c Update man page: 24-bit color 2017-01-22 18:33:03 +09:00
Junegunn Choi
71dec3dc5e Fix bug where screen is not properly cleared on toggle-preview 2017-01-22 17:43:27 +09:00
Junegunn Choi
e5017c0431 Remove unnecesasry test case 2017-01-22 17:41:47 +09:00
Junegunn Choi
cbb5134874 [vim] Use 24-bit colors if termguicolors is set 2017-01-22 14:40:30 +09:00
Junegunn Choi
ff248d566d Drop ncurses dependency
Close #818
2017-01-22 14:13:37 +09:00
Junegunn Choi
6ccc12c332 Use alternate screen if --height needs the entire screen
- Remove unnecessary scrolling
- Allow us to use `--height 100%` under Neovim terminal for 24-bit colors

Related:
- #789
- https://github.com/neovim/neovim/issues/4151
2017-01-22 05:26:38 +09:00
Junegunn Choi
2a669e9a17 Clear lines even when background color is not set
Also revert the workaround in Vim plugin introduced in fa7c897.

Related: #814
2017-01-22 03:19:50 +09:00
Junegunn Choi
5130abe76f Merge branch 'master' into devel 2017-01-22 03:10:06 +09:00
Junegunn Choi
fa7c8977a8 [vim] tput el to clear the last line
Close #814

Not grouping commands to avoid errors on non-standard shells.
2017-01-22 03:03:26 +09:00
Junegunn Choi
24fa183297 make deps 2017-01-22 02:54:19 +09:00
Junegunn Choi
131aa5dd15 Composable actions in --bind
Close #816
2017-01-22 02:32:49 +09:00
Junegunn Choi
a06ccc928f Fix flakies 2017-01-21 04:17:51 +09:00
Junegunn Choi
d09ad13208 [zsh] Workaround trailing esacped space bug in go-shellwords
https://github.com/mattn/go-shellwords/issues/3

Close #812
2017-01-21 03:59:36 +09:00
Junegunn Choi
8ac37d5927 [shell] Do not override --reverse in CTRL-R
Close #807
2017-01-17 18:09:29 +09:00
Junegunn Choi
7ef0e50507 [bash/zsh] Remove unused --reverse in CTRL-R binding
Related #807
2017-01-17 11:58:25 +09:00
Junegunn Choi
62ab8ece5e 0.16.1 2017-01-16 12:27:40 +09:00
Junegunn Choi
8e2e63f9b9 Propertly fill window with background color
Close #805
2017-01-16 12:27:32 +09:00
Junegunn Choi
f96173cbe4 Add -L flag to the default find command
Close #781
2017-01-16 12:01:58 +09:00
Amos Bird
11015df52f Add half-page-{up,down} actions (#784) 2017-01-16 11:58:13 +09:00
Junegunn Choi
05ed57a9f0 Merge pull request #794 from junegunn/devel
0.16.0
2017-01-16 02:43:56 +09:00
Junegunn Choi
4bece04207 0.16.0 2017-01-16 02:39:37 +09:00
Junegunn Choi
ede7bfb901 Optimize LightRenderer for slow terminals 2017-01-16 02:26:36 +09:00
Junegunn Choi
44d3faa048 [completion] Restore --height option for kill completion 2017-01-15 22:02:04 +09:00
Junegunn Choi
e0036b5ad2 Add --filepath-word option
Close #802
2017-01-15 19:42:28 +09:00
Junegunn Choi
208d4f2173 [shell] Make layout configurable via $FZF_DEFAULT_OPTS and $FZF_{KEY}_OPTS 2017-01-15 16:15:51 +09:00
Junegunn Choi
dc3957ce79 [completion] Add preview window to kill completion 2017-01-15 15:06:37 +09:00
Junegunn Choi
4ecb7f3a16 Replace --normalize with --literal and enable normalization by default
Ref #790
2017-01-15 13:22:09 +09:00
Junegunn Choi
03f5ef08c8 Use crypto/ssh/terminal instead of external stty command 2017-01-15 13:10:59 +09:00
Junegunn Choi
2720816266 [vim] Use /dev/tty as STDIN when using --height w/o explicit source 2017-01-15 04:33:03 +09:00
Justin M. Keyes
1896aa1748 s:common_sink(): Avoid duplicate BufEnter. (#803)
Later versions of Vim/Nvim handle `:edit <dir>` inside try-catch.

e13b9afe12
https://github.com/vim/vim/pull/1375
2017-01-14 20:55:30 +09:00
Junegunn Choi
5b68027bee Fix $FZF_COMPLETION_OPTS evaluation
Close #799
2017-01-14 01:10:34 +09:00
Junegunn Choi
48863ac55c Update invalid $TERM test case 2017-01-14 01:04:17 +09:00
Junegunn Choi
d64828ce6d Print error message to stderr on unexpected exit 2017-01-11 23:01:56 +09:00
Junegunn Choi
2aa739be81 Fix bug where occurrence of the pattern in header lines are highlighted 2017-01-11 22:47:26 +09:00
Junegunn Choi
9977a3e9fc Make preview renderer suspend early on line wrap 2017-01-11 22:13:40 +09:00
Junegunn Choi
f8082bc53a No need to use /bin/sh to execute stty and tput 2017-01-11 21:48:36 +09:00
Junegunn Choi
996dcb14a3 Make fzf immediately quit when failed to read /dev/tty
Close #798
2017-01-11 02:12:32 +09:00
Junegunn Choi
0c127cfdc1 No need to query row position of the cursor if mouse is disabled 2017-01-10 22:55:55 +09:00
Junegunn Choi
ae274158de Add experimental support for 24-bit colors 2017-01-10 02:16:12 +09:00
Junegunn Choi
340af463cd Add --min-height option for percent --height 2017-01-10 01:04:36 +09:00
Junegunn Choi
78a3f81972 Do not use \e[s and \e[u
Excerpt from http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x361.html:

> - Save cursor position:
>   \033[s
> - Restore cursor position:
>   \033[u
>
> The latter two codes are NOT honoured by many terminal emulators. The
> only ones that I'm aware of that do are xterm and nxterm - even though
> the majority of terminal emulators are based on xterm code. As far as
> I can tell, rxvt, kvt, xiterm, and Eterm do not support them. They are
> supported on the console.

They are also unsupported by Neovim terminal.
2017-01-09 19:09:30 +09:00
Junegunn Choi
d18b8e0d2c Retry flaky test cases 2017-01-09 13:22:24 +09:00
Junegunn Choi
6c6c0a4778 Make util.RuneWidth return 1 for non-displayable characters
Fix line wrapping in preview window
2017-01-09 10:49:05 +09:00
Junegunn Choi
a16d8f66a9 Normalize pattern string before passing it to Algo function 2017-01-09 09:52:17 +09:00
Junegunn Choi
45793d75c2 Add --normalize option to normalize latin script characters
Close #790
2017-01-09 03:12:23 +09:00
Junegunn Choi
9d545f9578 Fix update of multi-select pointer 2017-01-08 02:29:31 +09:00
Junegunn Choi
a30999a785 Prepare for 0.16.0 release 2017-01-08 02:09:56 +09:00
Junegunn Choi
1a50f1eca1 [vim] Use --height instead of fzf-tmux 2017-01-08 02:09:56 +09:00
Junegunn Choi
1448d631a7 Add --height option 2017-01-08 02:09:56 +09:00
Junegunn Choi
fd137a9e87 [bash/zsh-completion] Filter ~/.ssh/known_hosts
Close #791
2017-01-07 22:46:34 +09:00
Jan Edmund Lazo
3670273719 [vim] Use cmd.exe directly on GVim (launcher='%s') (#787) 2017-01-04 14:07:01 +09:00
Jan Edmund Lazo
6c0fd7f9ca [vim] FZF command to handle Windows paths with spaces
- Use noshellslash for strict path expansion in fzf#run and s:cmd
  (shellescape depends on shellslash)
- Double-quote the fzf command for cmd.exe
- Add fzf#shellescape to encapsulate the logic
- Close #786
2017-01-02 02:16:25 +09:00
Jan Edmund Lazo
42a2371d26 [vim] Use cmd.exe in Windows (#785) 2017-01-01 11:48:15 +09:00
Junegunn Choi
45faad7e04 [bash] Addendum fix for #580 2017-01-01 02:23:20 +09:00
Junegunn Choi
73eacf1137 [bash-completion] Always backup existing completion definitions
_fzf_completion_loaded is no longer checked. This change increases the
load time by a few milliseconds, but I can't think of a better way to
handle the issue.

Close #783.
2016-12-31 00:30:00 +09:00
Junegunn Choi
7b0d9e1e07 Apply --tabstop to preview window 2016-12-27 01:35:09 +09:00
Pierre Neidhardt
c7b0764002 [shell] Use '-mindepth 1' to omit root folder in 'find' output (#779)
This removes the need for the 'sed' call. Faster, cleaner.
2016-12-24 12:53:07 +09:00
Daniel Hahler
847c512539 s:execute_term: switch_back: check that self.pbuf exists (#776)
With a `bufhidden=wipe` buffer (e.g. vim-startify) the buffer would not
exist anymore, resulting in an error.
2016-12-19 02:51:19 +09:00
Junegunn Choi
97330ee8fc No need to set MANPATH
Close #774
2016-12-17 11:20:28 +09:00
Pierre Neidhardt
0508e70f9b Overhaul fish functions (#759)
Replace the "temp file" workaround with the "read" function: it's
simpler and faster.

Use proper escaping, remove the custom function.

The "file" widget uses last token as root for the "find" command.
This replaces the equivalent of '**' completion in bash/zsh.
The "$dir" non-expanded variable can be used in FZF_CTRL_T_COMMAND to
set the root.
2016-12-14 15:37:27 +09:00
Marco Hinz
8a502af4c1 Neovim: event handlers always expect three arguments (#768) 2016-12-14 01:56:53 +09:00
Junegunn Choi
c60bfb2b0f [neovim] Keep alternate file unchanged
Close https://github.com/junegunn/fzf.vim/issues/265
2016-12-11 22:32:59 +09:00
Junegunn Choi
16b5902aa2 Fix Linux build (#756) 2016-12-05 02:27:38 +09:00
Junegunn Choi
a442fe0fd0 Truncate long lines in preview window
Add `:wrap` to --preview-window to wrap lines instead

Close #756
2016-12-05 02:13:59 +09:00
Junegunn Choi
ab9ae4f643 [vim] Fix path display in FZF when cwd is ~ 2016-12-03 01:13:56 +09:00
Junegunn Choi
d9a51030ea [vim] Display relative path in prompt 2016-12-02 21:07:23 +09:00
Junegunn Choi
67026718c1 Add BUILD.md 2016-11-27 15:16:53 +09:00
Junegunn Choi
a71c471405 0.15.9 2016-11-26 12:36:24 +09:00
Junegunn Choi
3858086047 Always print scroll indicator in preview window 2016-11-26 12:34:16 +09:00
Junegunn Choi
dffef3d9f3 Update build instructions for ncurses 6 and tcell
Close #357
Close #738
2016-11-26 11:41:57 +09:00
Junegunn Choi
de1c6b8727 [tcell] 24-bit color support
TAGS=tcell make install

    printf "\x1b[38;2;100;200;250mTRUECOLOR\x1b[m\n" |
        TERM=xterm-truecolor fzf --ansi
2016-11-26 00:36:38 +09:00
Junegunn Choi
6f17f412ba Workaround for rendering glitch in case of short-lived input process
: | fzf --preview 'echo foo'
2016-11-25 14:05:37 +09:00
Junegunn Choi
746961bf43 [ncurses6] Suppress tui.Italic on ncurses 5 2016-11-24 13:42:14 +09:00
Junegunn Choi
182a6d99fd [ncurses6] Support italics 2016-11-24 00:13:10 +09:00
Junegunn Choi
af31088481 [ncurses6] Use wcolor_set to support more than 256 color pairs
To build fzf with ncurses 6 on macOS:

    brew install homebrew/dupes/ncurses
    LDFLAGS="-L/usr/local/opt/ncurses/lib" make install
2016-11-24 00:12:43 +09:00
Junegunn Choi
43425158f4 Make escape delay configurable via ncurses standard $ESCDELAY
Also reduce the default delay to 50ms. We should not set it to 0ms as it
breaks escape sequences on WSL. If 50ms is not enough, one can increase
the delay by setting $ESCDELAY to a larger value.
2016-11-23 02:28:03 +09:00
Junegunn Choi
8524ea7441 Do not ignore resize event from ncurses and tcell 2016-11-23 01:58:46 +09:00
Junegunn Choi
6a65006f55 0.15.8 2016-11-19 23:13:26 +09:00
Junegunn Choi
d75ed841a9 Fix --no-bold on --no-color 2016-11-19 23:12:28 +09:00
Junegunn Choi
3cd2547e91 Reduce ESC delay to 100ms 2016-11-19 23:03:27 +09:00
Junegunn Choi
8c661d4e8c Revamp escape sequence processing for WSL
Also add support for alt-[0-9] and f1[12]
2016-11-19 22:42:15 +09:00
Junegunn Choi
4b332d831e Add --no-bold option 2016-11-15 23:57:32 +09:00
Junegunn Choi
22487810ba Update README: link to wiki page 2016-11-15 23:44:04 +09:00
Junegunn Choi
c49e65d926 [shell] Fix pruning condition of find command for CTRL-T and ALT-C
`-fstype dev` is invalid. It's devfs on macOS and devtmpfs on Linux.
2016-11-15 01:52:54 +09:00
Junegunn Choi
2e8814bb57 Add WSL to .github/ISSUE_TEMPLATE.md 2016-11-14 12:26:46 +09:00
Junegunn Choi
dc557c0d4c Update ANSI processor to handle more VT-100 escape sequences
The updated regular expression should include not all but most of the
frequently used ANSI sequences. Close #735.
2016-11-14 02:15:23 +09:00
Junegunn Choi
a2beb159f1 0.15.7 2016-11-09 12:41:46 +09:00
Junegunn Choi
7ce427ff47 Fix panic when color is disabled and header lines contain ANSI colors
Close #732
2016-11-09 12:05:45 +09:00
Junegunn Choi
a221c672fb 0.15.6 2016-11-09 01:45:27 +09:00
Junegunn Choi
f87d382ec8 Fix --color=bw on tcell build 2016-11-09 01:45:06 +09:00
Junegunn Choi
3dfc020fac Merge pull request #730 from laur89/master
Minor README markup fix
2016-11-09 00:06:42 +09:00
Laur Aliste
2d87896939 Minor README markup fix. 2016-11-08 15:41:46 +01:00
Junegunn Choi
2192d8d816 GOOS=windows make release 2016-11-08 03:32:41 +09:00
Junegunn Choi
d206949f62 Wait for additional keys after ESC for up to 100ms
Close #661
2016-11-08 03:07:26 +09:00
Junegunn Choi
4accc69022 Fix flaky test cases 2016-11-08 02:19:05 +09:00
Junegunn Choi
898d8d94c8 Fix issues in tcell renderer and Windows build
- Fix display of CJK wide characters
- Fix horizontal offset of header lines
- Add support for keys with ALT modifier, shift-tab, page-up and down
- Fix util.ExecCommand to properly parse command-line arguments
- Fix redraw on resize
- Implement Pause/Resume for execute action
- Remove runtime check of GOOS
- Change exit status to 2 when tcell failed to start
- TBD: Travis CI build for tcell renderer
    - Pending. tcell cannot reliably ingest keys from tmux send-keys
2016-11-08 02:06:34 +09:00
Michael Kelley
26895da969 Implement tcell-based renderer 2016-11-07 02:32:14 +09:00
Junegunn Choi
0c573b3dff Prepare for termbox/windows build
`TAGS=termbox make` (or `go build -tags termbox`)
2016-11-07 02:32:14 +09:00
Junegunn Choi
2cff00dce2 man fzf in README
Close #726
2016-11-01 00:39:02 +09:00
Junegunn Choi
06a6ad8bca Update ANSI processor to ignore ^N and ^O
This reverts commit 02c6ad0e59.
2016-10-30 12:29:29 +09:00
Junegunn Choi
02c6ad0e59 Strip ^N and ^O from preview output
https://github.com/junegunn/fzf/issues/391#issuecomment-257090266

e.g. fzf --preview 'printf "$(tput setaf 2)foo$(tput sgr0)bar\nbar\n"'
2016-10-30 11:43:06 +09:00
Junegunn Choi
9f321cbe13 Fix header lines being cleared on toggle-preview
Close #722
2016-10-28 03:13:50 +09:00
Junegunn Choi
9f30ca2923 0.15.5 2016-10-23 22:00:32 +09:00
Junegunn Choi
37f2d8f795 [vim] Respect g:fzf_colors
Close #711
2016-10-22 01:14:16 +09:00
Junegunn Choi
400e443a0a Make test cases less susceptible to timeout errors 2016-10-22 00:01:21 +09:00
Junegunn Choi
0a8d2996dc Set foreground color without affecting background
Close #712
2016-10-21 19:35:59 +09:00
Junegunn Choi
cfdb00b971 Allow other options to follow --color without spec 2016-10-21 19:20:16 +09:00
Junegunn Choi
9b9ad39143 [vim] Set g:loaded_fzf 2016-10-18 15:00:47 +09:00
Junegunn Choi
0541c0dbcf Use relative position instead of absolute distance for --tiebreak=end
Fix unintuitive result where `*fzf*/install` is ranked higher than
`fzf/src/fzf/*fzf*-linux_386` on --tiebreak=end.
2016-10-18 01:13:57 +09:00
Junegunn Choi
47b11cb8b4 Merge pull request #701 from nthapaliya/zsh_script_improvements
[zsh] GNU coreutils compatibility
2016-10-14 10:00:58 +09:00
Niraj Thapaliya
d3da310b92 Use command to ignore shell function 2016-10-13 09:53:24 -06:00
Niraj Thapaliya
93e0a6a9de Gnu [ evaluates both sides of a -o condition regardless
It doesn't short circuit like we expect, causing trouble when $dir is
empty

Use shell builtin instead
2016-10-13 09:52:49 -06:00
Junegunn Choi
ac549a853a [fzf-tmux] Fix bash condition
Fix #702
2016-10-13 10:42:26 +09:00
Junegunn Choi
053af9a1c8 [fzf-tmux/vim/nvim] Do not split small window
Close #699
2016-10-12 23:10:21 +09:00
Junegunn Choi
60112def02 Merge pull request #698 from Ambrevar/master
[fish] Yank commandline in fzf-history-widget
2016-10-12 01:54:51 +09:00
Pierre Neidhardt
2134c0c8a9 key-bindings.fish: Yank commandline in fzf-history-widget 2016-10-11 21:15:00 +05:30
Junegunn Choi
3222d62ddf 0.15.4 2016-10-04 02:17:36 +09:00
Junegunn Choi
aeb957a285 Use exact match by default for inverse search term
This is a breaking change, but I believe it makes much more sense. It is
almost impossible to predict which entries will be filtered out due to
a fuzzy inverse term. You can still perform inverse-fuzzy-match by
prepending `!'` to the term.

| Token    | Match type                 | Description                       |
| -------- | -------------------------- | --------------------------------- |
| `sbtrkt` | fuzzy-match                | Items that match `sbtrkt`         |
| `^music` | prefix-exact-match         | Items that start with `music`     |
| `.mp3$`  | suffix-exact-match         | Items that end with `.mp3`        |
| `'wild`  | exact-match (quoted)       | Items that include `wild`         |
| `!fire`  | inverse-exact-match        | Items that do not include `fire`  |
| `!.mp3$` | inverse-suffix-exact-match | Items that do not end with `.mp3` |
2016-10-04 02:09:03 +09:00
Junegunn Choi
154cf22ffa Display scroll indicator in preview window 2016-10-04 01:40:45 +09:00
Junegunn Choi
51f532697e Adjust maximum scroll offset
It was possible that a few lines at the bottom may not be visible if
there are lines above that span multiple lines.
2016-10-04 01:39:48 +09:00
Junegunn Choi
01b88539ba [vim] Apply --multi and --prompt to :FZF command 2016-10-04 00:30:04 +09:00
Junegunn Choi
3066b206af Support field index expressions in preview and execute action
Also close #679. The placeholder for the current query is {q}.
2016-10-03 14:33:28 +09:00
Junegunn Choi
04492bab10 Use unicode.IsSpace to cover more whitespace characters 2016-09-29 22:40:22 +09:00
Junegunn Choi
8b0d0342d4 0.15.3 2016-09-29 03:05:20 +09:00
Junegunn Choi
957c12e7d7 Fix SEGV when trying to render preview but the window is closed
Close #677
2016-09-29 02:53:05 +09:00
Junegunn Choi
3b5ae0f8a2 Fix failing unit tests on ANSI attributes 2016-09-29 01:06:47 +09:00
Junegunn Choi
1fc5659842 Add support for more ANSI color attributes (#674)
Dim, underline, blink, reverse
2016-09-29 00:54:27 +09:00
Junegunn Choi
1acd2adce2 Update man page: missing actions 2016-09-26 15:33:46 +09:00
Junegunn Choi
1bc223d4b3 0.15.2 2016-09-25 22:20:43 +09:00
Junegunn Choi
bef405bfa5 Ignore VT100-related escape codes 2016-09-25 19:03:08 +09:00
Junegunn Choi
0612074abe Support high intensity colors
Close #671
2016-09-25 18:11:35 +09:00
Junegunn Choi
3bf51d8362 Merge pull request #670 from maverickwoo/fix-668
[bash-completion] Fix #668
2016-09-25 05:15:24 +09:00
Maverick Woo
2c8479a7c5 Fix #668
Handle uppercase letters in program names. This also deals with `-` and
`.`, both of which are quite common in program names, e.g., `xdg-open`
and `foo.sh`.
2016-09-24 15:39:13 -04:00
Junegunn Choi
8c8b5b313e Add preview-page-up and preview-page-down actions 2016-09-25 04:12:44 +09:00
Junegunn Choi
66d55fd893 Make preview windows scrollable
Close #669

You can use your mouse or binadble preview-up and preview-down actions
to scroll the content of the preview window.

    fzf --preview 'highlight -O ansi {}' --bind alt-j:preview-down,alt-k:preview-up
2016-09-25 02:02:00 +09:00
Junegunn Choi
7fa5e6c861 0.15.1 2016-09-21 01:28:24 +09:00
Junegunn Choi
00f96aae76 Avoid rendering delay when displaying extremely long lines
Related #666
2016-09-21 01:23:41 +09:00
Junegunn Choi
a749e6bd16 Fix temp directory in a test case 2016-09-21 01:15:35 +09:00
Junegunn Choi
791076d366 Fix panic when pattern occurs after 2^15-th column
Fix #666
2016-09-21 01:15:06 +09:00
Junegunn Choi
37f43fbb35 Add --print0 option
Related: #660
2016-09-19 01:15:38 +09:00
Junegunn Choi
401a5fd5ff Printable character in --expect set should not affect --print-query 2016-09-18 14:34:50 +09:00
Junegunn Choi
1854922f0c Truncate the query string if it's too long
Use hard-coded limit to keep it simple. An alternative is to dynamically
calculate the width of the visible area and use it as the limit, but it
can cause unwanted truncation of the query on screen resize/split.
2016-09-18 14:34:48 +09:00
Junegunn Choi
2fc7c18747 Revise ranking algorithm 2016-09-18 14:34:46 +09:00
Junegunn Choi
8ef2420677 Update README 2016-09-13 04:12:03 +09:00
Junegunn Choi
cf6f4d74c4 Merge pull request #657 from ishanray/patch-1
Fix typo in comment
2016-09-11 12:13:40 +09:00
ishanray
f44d40f6b4 Update algo.go 2016-09-10 23:40:55 +04:00
Junegunn Choi
1c81a58127 Merge pull request #654 from qiemem/fix-tmux-groups-dont-break-sockets
[fzf-tmux] Make fzf target correct session in group
2016-09-07 21:36:32 +09:00
Bryan Head
9baf7c4874 Make fzf target correct session in group
Fixes #643
Doesn't break #648
2016-09-06 13:03:07 -05:00
Junegunn Choi
22b089e47e Revert "Unset TMUX before splitting window" (#648)
This reverts commit 4d4447779f.
2016-08-31 14:20:29 +09:00
Junegunn Choi
b166f18220 Merge pull request #646 from qiemem/fix-tmux-groups
[fzf-tmux] Fix grouped tmux session confusion
2016-08-29 12:47:43 +09:00
Junegunn Choi
68600f6ecf Merge pull request #645 from ckafi/split-without-IFS
[zsh-completion] Split default zsh binding at the correct place
2016-08-29 12:47:14 +09:00
Bryan Head
4d4447779f Unset TMUX before splitting window
Avoids confusing grouped sessions.
Fixes #643
2016-08-28 16:57:38 -05:00
Tobias Frilling
639de4c27b Split default zsh binding at the correct place
The command substitution and following word splitting to determine the default
zle widget for ^I formerly only works if the IFS parameter contains a space. Now
it specifically splits at spaces, regardless of IFS.
2016-08-28 20:34:36 +02:00
Junegunn Choi
d87390934e [neovim] Do not resize if the size of the screen has changed
Related #642
2016-08-28 19:27:18 +09:00
Junegunn Choi
411ec2e557 Merge branch 'joshuarubin-master' 2016-08-28 19:18:13 +09:00
Joshua Rubin
f025602841 [vim] Reset window sizes on close
Fix #520
Fix junegunn/fzf.vim#42
2016-08-28 19:17:24 +09:00
Junegunn Choi
f958c9daf5 [vim] Tilde prefix is not allowed for left or right layout 2016-08-24 01:15:35 +09:00
Junegunn Choi
b86838c2b0 0.13.5 2016-08-21 05:02:45 +09:00
Junegunn Choi
1f7d1f9b15 Update Centos Dockerfile to use Go 1.7 2016-08-21 04:54:53 +09:00
Junegunn Choi
f8fdf9618a No need to cache the result in filtering mode (--filter) 2016-08-20 02:06:57 +09:00
Junegunn Choi
827a83efbc Remove Offset slice from Result struct 2016-08-20 01:53:32 +09:00
Junegunn Choi
3e88849386 [vim] Fix "E706: Variable type mismatch for: arg" 2016-08-19 18:02:32 +09:00
Junegunn Choi
608c416207 Add missing sources 2016-08-19 03:27:42 +09:00
Junegunn Choi
62f6ff9d6c [vim] Make arguments to fzf#wrap() optional
fzf#wrap([name string,] [opts dict,] [fullscreen boolean])
2016-08-19 03:05:22 +09:00
Junegunn Choi
37dc273148 Micro-optimizations
- Make structs smaller
- Introduce Result struct and use it to represent matched items instead of
  reusing Item struct for that purpose
- Avoid unnecessary memory allocation
- Avoid growing slice from the initial capacity
- Code cleanup
2016-08-19 02:39:32 +09:00
Junegunn Choi
f7f01d109e Set the upper limit of the number of search go routines 2016-08-19 01:55:38 +09:00
Junegunn Choi
01ee335521 Remove duplicate code 2016-08-18 03:11:54 +09:00
Junegunn Choi
0e0de29b87 Inline function calls in tight loops
By only using leaf functions
2016-08-18 01:48:52 +09:00
Junegunn Choi
babf877fd6 Increase the number of go routines for search
Sort performance increases as the size of each sublist decreases (n in
nlog(n) decreases). Merger is then responsible for merging the sorted
lists in order, and since in most cases we are only interesed in the
matches in the first page on the screen so the overhead in the process
is negligible.
2016-08-18 01:46:05 +09:00
Junegunn Choi
935272824e Setting GOMAXPROCS is no longer needed
https://golang.org/doc/go1.5
2016-08-17 02:21:33 +09:00
Junegunn Choi
3a9532c8fd Increase read buffer size to 64KB 2016-08-16 02:06:15 +09:00
Junegunn Choi
c4c92142a6 0.13.4 2016-08-14 18:10:21 +09:00
Junegunn Choi
d4b6338102 Lint 2016-08-14 17:51:34 +09:00
Junegunn Choi
8df7d962e6 Improve rendering time of long lines 2016-08-14 17:44:11 +09:00
Junegunn Choi
41e916a511 [perf] evaluateBonus can start from sidx - 1 2016-08-14 11:58:47 +09:00
Junegunn Choi
d9c8a9a880 [perf] Remove memory copy when using string delimiter 2016-08-14 04:30:55 +09:00
Junegunn Choi
ddc7bb9064 [perf] Optimize AWK-style tokenizer for --nth
Approx. 50% less memory footprint and 40% improvement in query time
2016-08-14 02:19:29 +09:00
Junegunn Choi
1d4057c209 [perf] Avoid allocating rune array for ascii string
In the best case (all ascii), this reduces the memory footprint by 60%
and the response time by 15% to 20%. In the worst case (every line has
non-ascii characters), 3 to 4% overhead is observed.
2016-08-14 00:41:30 +09:00
Junegunn Choi
822b86942c [test] Clear environment variables 2016-08-13 19:26:36 +09:00
Junegunn Choi
1e74dbb937 :hidden property of previous --preview-window should be cleared
Fix #636. Patch suggested by @edi9999.
2016-08-12 01:16:59 +09:00
Junegunn Choi
7cef92fffe [vim] Delete fzf buffer even when exit status is non-zero
Fix #183
2016-08-02 03:30:17 +09:00
Junegunn Choi
42e4992f06 [vim] Make sure to delete fzf buffer
Close junegunn/fzf.vim#173 and #630
2016-08-02 02:25:02 +09:00
Junegunn Choi
a6066175c6 Merge pull request #630 from kassio/master
Remove `name` option from `termopen`.
2016-07-30 19:05:43 +09:00
Kassio Borges
27444d6b1e Remove name option from termopen.
`termopen` no longer accepts a `name` option, instead we should suffix the
command with `;#NAME`.
2016-07-29 11:10:46 +01:00
Junegunn Choi
d6a99c0391 [vim] v:shell_error can change around redraw!
Patch suggested by Mariusz Atamańczuk
2016-07-28 01:41:11 +09:00
Junegunn Choi
f787f7e651 [vim] Add fzf#wrap helper function
Close #627
2016-07-26 02:37:12 +09:00
Junegunn Choi
a7c9c08371 [vim] Make :FZF command configurable with g:fzf_layout
To make it consistent with the other commands in fzf.vim
2016-07-21 01:47:08 +09:00
Junegunn Choi
fccc93176b 0.13.3 2016-07-16 01:06:53 +09:00
Junegunn Choi
6439a138fe [install] Build fzf if prebuilt binary doesn't work
Close #617
2016-07-16 00:36:35 +09:00
Junegunn Choi
a9a29dff4f Fix duplicate rendering of the last line in preview window 2016-07-15 23:24:14 +09:00
Junegunn Choi
6a52f8b8dd [zsh-completion] setopt localoptions noksh_arrays
Close #607
2016-07-15 01:26:29 +09:00
Junegunn Choi
a1049328d6 [vim] Adjust split size when --header option is set
Close #622
2016-07-14 13:35:18 +09:00
Junegunn Choi
5c2b96bd00 [vim] Fix error with multi-line $FZF_DEFAULT_COMMAND
Close #620
2016-07-13 13:15:14 +09:00
Junegunn Choi
c36413fdf6 [zsh] Suppress error message when pipefail is not supported
Close #615
2016-07-11 17:47:41 +09:00
Junegunn Choi
52cf5af91c [test] Fix test failure on Travis CI
No guarantee in the order in which files are listed
2016-07-10 15:44:44 +09:00
Junegunn Choi
3a4e053af7 [bash] Fall back to send-keys if named paste buffer is not supported
Related: #616
2016-07-10 15:21:28 +09:00
Junegunn Choi
049bc9ec68 [fzf-tmux] Add man page 2016-07-10 14:44:00 +09:00
Junegunn Choi
b461a555b8 [fzf-tmux] Add --version and --help flags 2016-07-10 14:41:06 +09:00
Junegunn Choi
0f87b2d1e1 [fzf-tmux] Use double brackets
For consistency and (negligible) performance improvement
2016-07-10 14:34:29 +09:00
Junegunn Choi
0fb5b76c0d [fzf-tmux] Fail fast if fzf excutable is not found 2016-07-10 14:28:58 +09:00
Junegunn Choi
0c918dd87a Merge pull request #616 from seanlaguna/master
Use tmux buffers for sending output to preserve character encoding
2016-07-10 12:29:32 +09:00
Junegunn Choi
05299a0fee [test] Use tmux buffer in unicode test cases
Related #616
2016-07-10 12:27:01 +09:00
Sean
b36b0a91f5 use tmux buffers for sending output to preserve character encoding 2016-07-09 09:47:20 -05:00
Junegunn Choi
6081eac58a [shell] Suppress alias/function expansion
Close #611
2016-07-07 01:40:14 +09:00
Junegunn Choi
942ba749c7 [vim] Restore working directory even when new window is opened
Close #612
2016-07-06 13:31:04 +09:00
Junegunn Choi
f941012687 Merge pull request #610 from eigengrau/master
[zsh] Re-initialize zle when widgets finish
2016-07-05 21:50:43 +09:00
Sebastian Reuße
fed5e5d5af [zsh] Re-initialize zle when widgets finish
zle automatically calls zle-line-init when it starts to read a new line. Many
Zsh setups use this hook to set the terminal into application mode, since this
will then allow defining keybinds based on the $terminfo variable (the escape
codes in said variable are only valid in application mode).

However, fzf resets the terminal into raw mode, rendering $terminfo values
invalid once the widget has finished. Accordingly, keyboard bindings defined
via $terminfo won’t work anymore.

This fixes the issue by calling zle-line-init when widgets finish. Care is taken
to not call this widget when it is undefined.

Fixes #279
2016-07-05 08:57:11 +02:00
Junegunn Choi
b864885753 [install] Make sure to unset pipefail 2016-07-04 13:05:26 +09:00
Junegunn Choi
64747c2324 [install] Fix error in install script
Close #608
2016-07-04 13:00:30 +09:00
Junegunn Choi
34965edcda [install] Fall back to wget if curl failed
Close #605
2016-07-04 01:41:43 +09:00
Junegunn Choi
bd4377084d Merge pull request #601 from blueyed/zsh-ret-for-fzf-file-widget
zsh: pass through exit code from fzf with fzf-file-widget
2016-06-18 23:17:10 +09:00
Daniel Hahler
38a2076b89 zsh: pass through exit code from widgets
This allows to have a custom widget like the following, which would
additionally accept the line, but only in case of entries being
selected:

    fzf-file-widget-with-accept() {
      zle fzf-file-widget
      if [[ "$?" == 0 ]] && (( $#BUFFER )); then
        zle accept-line
      fi
    }
    zle     -N   fzf-file-widget-with-accept
    bindkey '\e^T' fzf-file-widget-with-accept

With this `<C-a>t` will launch fzf, and simulate the pressing of "Enter"
afterwards.
2016-06-16 20:20:29 +02:00
Junegunn Choi
5759d50d4a 0.13.2 2016-06-16 02:16:13 +09:00
Junegunn Choi
e455836cc9 Fix race condition where preview window is not properly cleared 2016-06-15 13:15:17 +09:00
Junegunn Choi
8a90f26c8a 0.13.1 2016-06-14 21:53:00 +09:00
Junegunn Choi
24e1fabf2e Do not process ANSI codes in --preview output at once
Close #598
2016-06-14 21:52:47 +09:00
Junegunn Choi
c39c039e15 [shell] Add $FZF_CTRL_T_OPTS and $FZF_ALT_C_OPTS
Close #596
2016-06-12 20:48:23 +09:00
Junegunn Choi
07f176f426 Merge pull request #595 from aykamko/speed-up-fzf-completion
Optimize fzf_default_completion binding
2016-06-12 11:56:49 +09:00
Aleks Kamko
19339e3a6d optimize fzf_default_completion binding 2016-06-11 15:19:16 -07:00
81 changed files with 9801 additions and 5507 deletions

View File

@@ -11,6 +11,7 @@
- [ ] Linux - [ ] Linux
- [ ] Mac OS X - [ ] Mac OS X
- [ ] Windows - [ ] Windows
- [ ] Windows Subsystem for Linux
- [ ] Etc. - [ ] Etc.
- Shell - Shell
- [ ] bash - [ ] bash

9
.gitignore vendored
View File

@@ -1,6 +1,9 @@
bin bin/fzf
src/fzf/fzf-* bin/fzf.exe
gopath target
pkg pkg
Gemfile.lock Gemfile.lock
.DS_Store .DS_Store
doc/tags
vendor
gopath

View File

@@ -1,24 +1,20 @@
language: ruby language: ruby
rvm: dist: trusty
- 2.2.0 sudo: required
matrix:
include:
- env: TAGS=
rvm: 2.3.3
# - env: TAGS=tcell
# rvm: 2.3.3
install: install:
- sudo apt-get update
- sudo apt-get install -y libncurses-dev lib32ncurses5-dev libgpm-dev
- sudo add-apt-repository -y ppa:pi-rho/dev - sudo add-apt-repository -y ppa:pi-rho/dev
- sudo apt-add-repository -y ppa:fish-shell/release-2 - sudo apt-add-repository -y ppa:fish-shell/release-2
- sudo apt-get update - sudo apt-get update
- sudo apt-get install -y tmux=1.9a-1~ppa1~p - sudo apt-get install -y tmux zsh fish
- sudo apt-get install -y zsh fish
script: | script: |
export GOPATH=~/go make test install &&
export FZF_BASE=$GOPATH/src/github.com/junegunn/fzf ./install --all &&
mkdir -p $GOPATH/src/github.com/junegunn
ln -s $(pwd) $FZF_BASE
cd $FZF_BASE/src && make test fzf/fzf-linux_amd64 install &&
cd $FZF_BASE/bin && ln -sf fzf-linux_amd64 fzf-$(./fzf --version)-linux_amd64 &&
cd $FZF_BASE && yes | ./install && rm -f fzf &&
tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ] tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ]

53
BUILD.md Normal file
View File

@@ -0,0 +1,53 @@
Building fzf
============
Build instructions
------------------
### Prerequisites
- `go` executable in $PATH
### Using Makefile
Makefile will set up and use its own `$GOPATH` under the project root.
```sh
# Build fzf binary for your platform in target
make
# Build fzf binary and copy it to bin directory
make install
# Build 32-bit and 64-bit executables and tarballs in target
make release
# Make release archives for all supported platforms in target
make release-all
```
### Using `go get`
Alternatively, you can build fzf directly with `go get` command without
manually cloning the repository.
```sh
go get -u github.com/junegunn/fzf
```
Third-party libraries used
--------------------------
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
- Licensed under [MIT](http://mattn.mit-license.org)
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
- Licensed under [MIT](http://mattn.mit-license.org)
- [mattn/go-isatty](https://github.com/mattn/go-isatty)
- Licensed under [MIT](http://mattn.mit-license.org)
- [tcell](https://github.com/gdamore/tcell)
- Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE)
License
-------
[MIT](LICENSE)

View File

@@ -1,6 +1,263 @@
CHANGELOG CHANGELOG
========= =========
0.17.3
------
- `$LINES` and `$COLUMNS` are exported to preview command so that the command
knows the exact size of the preview window.
- Better error messages when the default command or `$FZF_DEFAULT_COMMAND`
fails.
- Reverted #1061 to avoid having duplicate entries in the list when find
command detected a file system loop (#1120). The default command now
requires that find supports `-fstype` option.
- fzf now distinguishes mouse left click and right click (#1130)
- Right click is now bound to `toggle` action by default
- `--bind` understands `left-click` and `right-click`
- Added `replace-query` action (#1137)
- Replaces query string with the current selection
- Added `accept-non-empty` action (#1162)
- Same as accept, except that it prevents fzf from exiting without any
selection
0.17.1
------
- Fixed custom background color of preview window (#1046)
- Fixed background color issues of Windows binary
- Fixed Windows binary to execute command using cmd.exe with no parsing and
escaping (#1072)
- Added support for `window` layout on Vim 8 using Vim 8 terminal (#1055)
0.17.0-2
--------
A maintenance release for auxiliary scripts. fzf binaries are not updated.
- Experimental support for the builtin terminal of Vim 8
- fzf can now run inside GVim
- Updated Vim plugin to better handle `&shell` issue on fish
- Fixed a bug of fzf-tmux where invalid output is generated
- Fixed fzf-tmux to work even when `tput` does not work
0.17.0
------
- Performance optimization
- One can match literal spaces in extended-search mode with a space prepended
by a backslash.
- `--expect` is now additive and can be specified multiple times.
0.16.11
-------
- Performance optimization
- Fixed missing preview update
0.16.10
-------
- Fixed invalid handling of ANSI colors in preview window
- Further improved `--ansi` performance
0.16.9
------
- Memory and performance optimization
- Around 20% performance improvement for general use cases
- Up to 5x faster processing of `--ansi`
- Up to 50% reduction of memory usage
- Bug fixes and usability improvements
- Fixed handling of bracketed paste mode
- [ERROR] on info line when the default command failed
- More efficient rendering of preview window
- `--no-clear` updated for repetitive relaunching scenarios
0.16.8
------
- New `change` event and `top` action for `--bind`
- `fzf --bind change:top`
- Move cursor to the top result whenever the query string is changed
- `fzf --bind 'ctrl-w:unix-word-rubout+top,ctrl-u:unix-line-discard+top'`
- `top` combined with `unix-word-rubout` and `unix-line-discard`
- Fixed inconsistent tiebreak scores when `--nth` is used
- Proper display of tab characters in `--prompt`
- Fixed not to `--cycle` on page-up/page-down to prevent overshoot
- Git revision in `--version` output
- Basic support for Cygwin environment
- Many fixes in Vim plugin on Windows/Cygwin (thanks to @janlazo)
0.16.7
------
- Added support for `ctrl-alt-[a-z]` key chords
- CTRL-Z (SIGSTOP) now works with fzf
- fzf will export `$FZF_PREVIEW_WINDOW` so that the scripts can use it
- Bug fixes and improvements in Vim plugin and shell extensions
0.16.6
------
- Minor bug fixes and improvements
- Added `--no-clear` option for scripting purposes
0.16.5
------
- Minor bug fixes
- Added `toggle-preview-wrap` action
- Built with Go 1.8
0.16.4
------
- Added `--border` option to draw border above and below the finder
- Bug fixes and improvements
0.16.3
------
- Fixed a bug where fzf incorrectly display the lines when straddling tab
characters are trimmed
- Placeholder expression used in `--preview` and `execute` action can
optionally take `+` flag to be used with multiple selections
- e.g. `git log --oneline | fzf --multi --preview 'git show {+1}'`
- Added `execute-silent` action for executing a command silently without
switching to the alternate screen. This is useful when the process is
short-lived and you're not interested in its output.
- e.g. `fzf --bind 'ctrl-y:execute!(echo -n {} | pbcopy)'`
- `ctrl-space` is allowed in `--bind`
0.16.2
------
- Dropped ncurses dependency
- Binaries for freebsd, openbsd, arm5, arm6, arm7, and arm8
- Official 24-bit color support
- Added support for composite actions in `--bind`. Multiple actions can be
chained using `+` separator.
- e.g. `fzf --bind 'ctrl-y:execute(echo -n {} | pbcopy)+abort'`
- `--preview-window` with size 0 is allowed. This is used to make fzf execute
preview command in the background without displaying the result.
- Minor bug fixes and improvements
0.16.1
------
- Fixed `--height` option to properly fill the window with the background
color
- Added `half-page-up` and `half-page-down` actions
- Added `-L` flag to the default find command
0.16.0
------
- *Added `--height HEIGHT[%]` option*
- fzf can now display finder without occupying the full screen
- Preview window will truncate long lines by default. Line wrap can be enabled
by `:wrap` flag in `--preview-window`.
- Latin script letters will be normalized before matching so that it's easier
to match against accented letters. e.g. `sodanco` can match `Só Danço Samba`.
- Normalization can be disabled via `--literal`
- Added `--filepath-word` to make word-wise movements/actions (`alt-b`,
`alt-f`, `alt-bs`, `alt-d`) respect path separators
0.15.9
------
- Fixed rendering glitches introduced in 0.15.8
- The default escape delay is reduced to 50ms and is configurable via
`$ESCDELAY`
- Scroll indicator at the top-right corner of the preview window is always
displayed when there's overflow
- Can now be built with ncurses 6 or tcell to support extra features
- *ncurses 6*
- Supports more than 256 color pairs
- Supports italics
- *tcell*
- 24-bit color support
- See https://github.com/junegunn/fzf/blob/master/BUILD.md
0.15.8
------
- Updated ANSI processor to handle more VT-100 escape sequences
- Added `--no-bold` (and `--bold`) option
- Improved escape sequence processing for WSL
- Added support for `alt-[0-9]`, `f11`, and `f12` for `--bind` and `--expect`
0.15.7
------
- Fixed panic when color is disabled and header lines contain ANSI colors
0.15.6
------
- Windows binaries! (@kelleyma49)
- Fixed the bug where header lines are cleared when preview window is toggled
- Fixed not to display ^N and ^O on screen
- Fixed cursor keys (or any key sequence that starts with ESC) on WSL by
making fzf wait for additional keystrokes after ESC for up to 100ms
0.15.5
------
- Setting foreground color will no longer set background color to black
- e.g. `fzf --color fg:153`
- `--tiebreak=end` will consider relative position instead of absolute distance
- Updated `fzf#wrap` function to respect `g:fzf_colors`
0.15.4
------
- Added support for range expression in preview and execute action
- e.g. `ls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1`
- `{q}` will be replaced to the single-quoted string of the current query
- Fixed to properly handle unicode whitespace characters
- Display scroll indicator in preview window
- Inverse search term will use exact matcher by default
- This is a breaking change, but I believe it makes much more sense. It is
almost impossible to predict which entries will be filtered out due to
a fuzzy inverse term. You can still perform inverse-fuzzy-match by
prepending `!'` to the term.
0.15.3
------
- Added support for more ANSI attributes: dim, underline, blink, and reverse
- Fixed race condition in `toggle-preview`
0.15.2
------
- Preview window is now scrollable
- With mouse scroll or with bindable actions
- `preview-up`
- `preview-down`
- `preview-page-up`
- `preview-page-down`
- Updated ANSI processor to support high intensity colors and ignore
some VT100-related escape sequences
0.15.1
------
- Fixed panic when the pattern occurs after 2^15-th column
- Fixed rendering delay when displaying extremely long lines
0.15.0
------
- Improved fuzzy search algorithm
- Added `--algo=[v1|v2]` option so one can still choose the old algorithm
which values the search performance over the quality of the result
- Advanced scoring criteria
- `--read0` to read input delimited by ASCII NUL character
- `--print0` to print output delimited by ASCII NUL character
0.13.5
------
- Memory and performance optimization
- Up to 2x performance with half the amount of memory
0.13.4
------
- Performance optimization
- Memory footprint for ascii string is reduced by 60%
- 15 to 20% improvement of query performance
- Up to 45% better performance of `--nth` with non-regex delimiters
- Fixed invalid handling of `hidden` property of `--preview-window`
0.13.3
------
- Fixed duplicate rendering of the last line in preview window
0.13.2
------
- Fixed race condition where preview window is not properly cleared
0.13.1
------
- Fixed UI issue with large `--preview` output with many ANSI codes
0.13.0 0.13.0
------ ------
- Added preview feature - Added preview feature

138
Makefile Normal file
View File

@@ -0,0 +1,138 @@
ifndef GOOS
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
GOOS := darwin
else ifeq ($(UNAME_S),Linux)
GOOS := linux
else
$(error "$$GOOS is not defined.")
endif
endif
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
ROOT_DIR := $(shell dirname $(MAKEFILE))
GOPATH := $(ROOT_DIR)/gopath
SRC_LINK := $(GOPATH)/src/github.com/junegunn/fzf/src
VENDOR_LINK := $(GOPATH)/src/github.com/junegunn/fzf/vendor
export GOPATH
GLIDE_YAML := glide.yaml
GLIDE_LOCK := glide.lock
SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(SRC_LINK) $(VENDOR_LINK) $(GLIDE_LOCK) $(MAKEFILE)
REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES))
BUILD_FLAGS := -a -ldflags "-X main.revision=$(REVISION) -w -extldflags=$(LDFLAGS)" -tags "$(TAGS)"
BINARY32 := fzf-$(GOOS)_386
BINARY64 := fzf-$(GOOS)_amd64
BINARYARM5 := fzf-$(GOOS)_arm5
BINARYARM6 := fzf-$(GOOS)_arm6
BINARYARM7 := fzf-$(GOOS)_arm7
BINARYARM8 := fzf-$(GOOS)_arm8
VERSION := $(shell awk -F= '/version =/ {print $$2}' src/constants.go | tr -d "\" ")
RELEASE32 := fzf-$(VERSION)-$(GOOS)_386
RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64
RELEASEARM5 := fzf-$(VERSION)-$(GOOS)_arm5
RELEASEARM6 := fzf-$(VERSION)-$(GOOS)_arm6
RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7
RELEASEARM8 := fzf-$(VERSION)-$(GOOS)_arm8
# https://en.wikipedia.org/wiki/Uname
UNAME_M := $(shell uname -m)
ifeq ($(UNAME_M),x86_64)
BINARY := $(BINARY64)
else ifeq ($(UNAME_M),amd64)
BINARY := $(BINARY64)
else ifeq ($(UNAME_M),i686)
BINARY := $(BINARY32)
else ifeq ($(UNAME_M),i386)
BINARY := $(BINARY32)
else ifeq ($(UNAME_M),armv5l)
BINARY := $(BINARYARM5)
else ifeq ($(UNAME_M),armv6l)
BINARY := $(BINARYARM6)
else ifeq ($(UNAME_M),armv7l)
BINARY := $(BINARYARM7)
else
$(error "Build on $(UNAME_M) is not supported, yet.")
endif
all: target/$(BINARY)
target:
mkdir -p $@
ifeq ($(GOOS),windows)
release: target/$(BINARY32) target/$(BINARY64)
cd target && cp -f $(BINARY32) fzf.exe && zip $(RELEASE32).zip fzf.exe
cd target && cp -f $(BINARY64) fzf.exe && zip $(RELEASE64).zip fzf.exe
cd target && rm -f fzf.exe
else ifeq ($(GOOS),linux)
release: target/$(BINARY32) target/$(BINARY64) target/$(BINARYARM5) target/$(BINARYARM6) target/$(BINARYARM7) target/$(BINARYARM8)
cd target && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf
cd target && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf
cd target && cp -f $(BINARYARM5) fzf && tar -czf $(RELEASEARM5).tgz fzf
cd target && cp -f $(BINARYARM6) fzf && tar -czf $(RELEASEARM6).tgz fzf
cd target && cp -f $(BINARYARM7) fzf && tar -czf $(RELEASEARM7).tgz fzf
cd target && cp -f $(BINARYARM8) fzf && tar -czf $(RELEASEARM8).tgz fzf
cd target && rm -f fzf
else
release: target/$(BINARY32) target/$(BINARY64)
cd target && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf
cd target && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf
cd target && rm -f fzf
endif
release-all: clean test
GOOS=darwin make release
GOOS=linux make release
GOOS=freebsd make release
GOOS=openbsd make release
GOOS=windows make release
$(SRC_LINK):
mkdir -p $(shell dirname $(SRC_LINK))
ln -sf $(ROOT_DIR)/src $(SRC_LINK)
$(VENDOR_LINK):
mkdir -p $(shell dirname $(VENDOR_LINK))
ln -sf $(ROOT_DIR)/vendor $(VENDOR_LINK)
vendor: $(GLIDE_YAML)
go get -u github.com/Masterminds/glide && $(GOPATH)/bin/glide install && touch $@
test: $(SOURCES) vendor
SHELL=/bin/sh GOOS= go test -v -tags "$(TAGS)" \
github.com/junegunn/fzf/src \
github.com/junegunn/fzf/src/algo \
github.com/junegunn/fzf/src/tui \
github.com/junegunn/fzf/src/util
install: bin/fzf
clean:
rm -rf target
target/$(BINARY32): $(SOURCES) vendor
GOARCH=386 go build $(BUILD_FLAGS) -o $@
target/$(BINARY64): $(SOURCES) vendor
GOARCH=amd64 go build $(BUILD_FLAGS) -o $@
# https://github.com/golang/go/wiki/GoArm
target/$(BINARYARM5): $(SOURCES) vendor
GOARCH=arm GOARM=5 go build $(BUILD_FLAGS) -o $@
target/$(BINARYARM6): $(SOURCES) vendor
GOARCH=arm GOARM=6 go build $(BUILD_FLAGS) -o $@
target/$(BINARYARM7): $(SOURCES) vendor
GOARCH=arm GOARM=7 go build $(BUILD_FLAGS) -o $@
target/$(BINARYARM8): $(SOURCES) vendor
GOARCH=arm64 go build $(BUILD_FLAGS) -o $@
bin/fzf: target/$(BINARY) | bin
cp -f target/$(BINARY) bin/fzf
.PHONY: all release release-all test install clean

196
README-VIM.md Normal file
View File

@@ -0,0 +1,196 @@
FZF Vim integration
===================
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.)
`:FZF[!]`
---------
If you have set up fzf for Vim, `:FZF` command will be added.
```vim
" Look for files under current directory
:FZF
" Look for files under your home directory
:FZF ~
" With options
:FZF --no-sort --reverse --inline-info /tmp
" Bang version starts fzf in fullscreen mode
:FZF!
```
Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key,
`CTRL-T`, `CTRL-X` or `CTRL-V` to open selected files in the current window,
in new tabs, in horizontal splits, or in vertical splits respectively.
Note that the environment variables `FZF_DEFAULT_COMMAND` and
`FZF_DEFAULT_OPTS` also apply here.
### Configuration
- `g:fzf_action`
- Customizable extra key bindings for opening selected files in different ways
- `g:fzf_layout`
- Determines the size and position of fzf window
- `g:fzf_colors`
- Customizes fzf colors to match the current color scheme
- `g:fzf_history_dir`
- Enables history feature
- `g:fzf_launcher`
- (Only in GVim) Terminal emulator to open fzf with
- `g:Fzf_launcher` for function reference
#### Examples
```vim
" This is the default extra key bindings
let g:fzf_action = {
\ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' }
" An action can be a reference to a function that processes selected lines
function! s:build_quickfix_list(lines)
call setqflist(map(copy(a:lines), '{ "filename": v:val }'))
copen
cc
endfunction
let g:fzf_action = {
\ 'ctrl-q': function('s:build_quickfix_list'),
\ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' }
" Default fzf layout
" - down / up / left / right
let g:fzf_layout = { 'down': '~40%' }
" You can set up fzf window using a Vim command (Neovim or latest Vim 8 required)
let g:fzf_layout = { 'window': 'enew' }
let g:fzf_layout = { 'window': '-tabnew' }
let g:fzf_layout = { 'window': '10split enew' }
" Customize fzf colors to match your color scheme
let g:fzf_colors =
\ { 'fg': ['fg', 'Normal'],
\ 'bg': ['bg', 'Normal'],
\ 'hl': ['fg', 'Comment'],
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
\ 'hl+': ['fg', 'Statement'],
\ 'info': ['fg', 'PreProc'],
\ 'border': ['fg', 'Ignore'],
\ 'prompt': ['fg', 'Conditional'],
\ 'pointer': ['fg', 'Exception'],
\ 'marker': ['fg', 'Keyword'],
\ 'spinner': ['fg', 'Label'],
\ 'header': ['fg', 'Comment'] }
" Enable per-command history.
" CTRL-N and CTRL-P will be automatically bound to next-history and
" previous-history instead of down and up. If you don't like the change,
" explicitly bind the keys to down and up in your $FZF_DEFAULT_OPTS.
let g:fzf_history_dir = '~/.local/share/fzf-history'
```
`fzf#run`
---------
For more advanced uses, you can use `fzf#run([options])` function with the
following options.
| Option name | Type | Description |
| -------------------------- | ------------- | ---------------------------------------------------------------- |
| `source` | string | External command to generate input to fzf (e.g. `find .`) |
| `source` | list | Vim list as input to fzf |
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
| `sink` | funcref | Reference to function to process each selected item |
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
| `options` | string/list | Options to fzf |
| `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) |
| `window` (Vim 8 / Neovim) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `launcher` | string | External terminal emulator to start fzf with (GVim only) |
| `launcher` | funcref | Function for generating `launcher` string (GVim only) |
`options` entry can be either a string or a list. For simple cases, string
should suffice, but prefer to use list type if you're concerned about escaping
issues on different platforms.
```vim
call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'})
call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']})
```
`fzf#wrap`
----------
`fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper
function that decorates the options dictionary so that it understands
`g:fzf_layout`, `g:fzf_action`, `g:fzf_colors`, and `g:fzf_history_dir` like
`:FZF`.
```vim
command! -bang MyStuff
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0))
```
fzf inside terminal buffer
--------------------------
The latest versions of Vim and Neovim include builtin terminal emulator
(`:terminal`) and fzf will start in a terminal buffer in the following cases:
- On Neovim
- On GVim
- On Terminal Vim with the non-default layout
- `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}`
### Hide statusline
When fzf starts in a terminal buffer, you may want to hide the statusline of
the containing buffer.
```vim
autocmd! FileType fzf
autocmd FileType fzf set laststatus=0 noshowmode noruler
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
```
GVim
----
With the latest version of GVim, fzf will start inside the builtin terminal
emulator of Vim. Please note that this terminal feature of Vim is still young
and unstable and you may run into some issues.
If you have an older version of GVim, you need an external terminal emulator
to start fzf with. `xterm` command is used by default, but you can customize
it with `g:fzf_launcher`.
```vim
" This is the default. %s is replaced with fzf command
let g:fzf_launcher = 'xterm -e bash -ic %s'
" Use urxvt instead
let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s'
```
If you're running MacVim on OSX, I recommend you to use iTerm2 as the
launcher. Refer to the [this wiki page][macvim-iterm2] to see how to set up.
[macvim-iterm2]: https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2
[License](LICENSE)
------------------
The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi

445
README.md
View File

@@ -1,27 +1,66 @@
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf.png" height="170" alt="fzf - a command-line fuzzy finder"> [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf.png" height="170" alt="fzf - a command-line fuzzy finder"> [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=EKYAW9PGKPD2N)
=== ===
fzf is a general-purpose command-line fuzzy finder. fzf is a general-purpose command-line fuzzy finder.
![](https://raw.github.com/junegunn/i/master/fzf.gif) <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-preview.png" width=640>
It's an interactive Unix filter for command-line that can be used with any
list; files, command history, processes, hostnames, bookmarks, git commits,
etc.
Pros Pros
---- ----
- No dependencies - Portable, no dependencies
- Blazingly fast - Blazingly fast
- e.g. `locate / | fzf`
- Flexible layout
- Runs in fullscreen or in horizontal/vertical split using tmux
- The most comprehensive feature set - The most comprehensive feature set
- Try `fzf --help` and be surprised - Flexible layout
- Batteries included - Batteries included
- Vim/Neovim plugin, key bindings and fuzzy auto-completion - Vim/Neovim plugin, key bindings and fuzzy auto-completion
Table of Contents
-----------------
* [Installation](#installation)
* [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew)
* [Using git](#using-git)
* [As Vim plugin](#as-vim-plugin)
* [Fedora](#fedora)
* [Windows](#windows)
* [Upgrading fzf](#upgrading-fzf)
* [Building fzf](#building-fzf)
* [Usage](#usage)
* [Using the finder](#using-the-finder)
* [Layout](#layout)
* [Search syntax](#search-syntax)
* [Environment variables](#environment-variables)
* [Options](#options)
* [Examples](#examples)
* [fzf-tmux script](#fzf-tmux-script)
* [Key bindings for command line](#key-bindings-for-command-line)
* [Fuzzy completion for bash and zsh](#fuzzy-completion-for-bash-and-zsh)
* [Files and directories](#files-and-directories)
* [Process IDs](#process-ids)
* [Host names](#host-names)
* [Environment variables / Aliases](#environment-variables--aliases)
* [Settings](#settings)
* [Supported commands](#supported-commands)
* [Vim plugin](#vim-plugin)
* [Advanced topics](#advanced-topics)
* [Performance](#performance)
* [Executing external programs](#executing-external-programs)
* [Preview window](#preview-window)
* [Tips](#tips)
* [Respecting .gitignore](#respecting-gitignore)
* [git ls-tree for fast traversal](#git-ls-tree-for-fast-traversal)
* [Fish shell](#fish-shell)
* [<a href="LICENSE">License</a>](#license)
Installation Installation
------------ ------------
fzf project consists of the following: fzf project consists of the following components:
- `fzf` executable - `fzf` executable
- `fzf-tmux` script for launching fzf in a tmux pane - `fzf-tmux` script for launching fzf in a tmux pane
@@ -30,14 +69,26 @@ fzf project consists of the following:
- Fuzzy auto-completion (bash, zsh) - 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 if you don't need the extra
install the extra stuff using the attached install script. stuff.
[bin]: https://github.com/junegunn/fzf-bin/releases [bin]: https://github.com/junegunn/fzf-bin/releases
#### Using git (recommended) ### Using Homebrew or Linuxbrew
Clone this repository and run You can use [Homebrew](http://brew.sh/) or [Linuxbrew](http://linuxbrew.sh/)
to install fzf.
```sh
brew install fzf
# To install useful key bindings and fuzzy completion:
$(brew --prefix)/opt/fzf/install
```
### Using git
Alternatively, you can "git clone" this repository to any directory and run
[install](https://github.com/junegunn/fzf/blob/master/install) script. [install](https://github.com/junegunn/fzf/blob/master/install) script.
```sh ```sh
@@ -45,46 +96,95 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install ~/.fzf/install
``` ```
#### Using Homebrew ### As Vim plugin
On OS X, you can use [Homebrew](http://brew.sh/) to install fzf. Once you have fzf installed, you can enable it inside Vim simply by adding the
directory to `&runtimepath` as follows:
```sh
brew install fzf
# Install shell extensions
/usr/local/opt/fzf/install
```
#### Install as Vim plugin
Once you have cloned the repository, add the following line to your .vimrc.
```vim ```vim
" If installed using Homebrew
set rtp+=/usr/local/opt/fzf
" If installed using git
set rtp+=~/.fzf set rtp+=~/.fzf
``` ```
Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf If you use [vim-plug](https://github.com/junegunn/vim-plug), the same can be
(recommended): written as:
```vim ```vim
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' } " If installed using Homebrew
Plug '/usr/local/opt/fzf'
" If installed using git
Plug '~/.fzf'
``` ```
#### Upgrading fzf But instead of separately installing fzf on your system (using Homebrew or
"git clone") and enabling it on Vim (adding it to `&runtimepath`), you can use
vim-plug to do both.
```vim
" PlugInstall and PlugUpdate will clone fzf in ~/.fzf and run install script
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' }
" Both options are optional. You don't have to install fzf in ~/.fzf
" and you don't have to run install script if you use fzf only in Vim.
```
### Fedora
fzf is available in Fedora 26 and above, and can be installed using the usual
method:
```sh
sudo dnf install fzf
```
Shell completion and plugins for vim or neovim are enabled by default. Shell
key bindings are installed but not enabled by default. See Fedora's package
documentation for more information.
### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
available as a [Chocolatey package][choco].
[choco]: https://chocolatey.org/packages/fzf
```sh
choco install fzf
```
However, other components of the project may not work on Windows. Known issues
and limitations can be found on [the wiki page][windows-wiki]. You might want
to consider installing fzf on [Windows Subsystem for Linux][wsl] where
everything runs flawlessly.
[windows-wiki]: https://github.com/junegunn/fzf/wiki/Windows
[wsl]: https://blogs.msdn.microsoft.com/wsl/
Upgrading fzf
-------------
fzf is being actively developed and you might want to upgrade it once in a fzf is being actively developed and you might want to upgrade it once in a
while. Please follow the instruction below depending on the installation while. Please follow the instruction below depending on the installation
method. method used.
- git: `cd ~/.fzf && git pull && ./install` - git: `cd ~/.fzf && git pull && ./install`
- brew: `brew update; brew reinstall fzf` - brew: `brew update; brew reinstall fzf`
- chocolatey: `choco upgrade fzf`
- vim-plug: `:PlugUpdate fzf` - vim-plug: `:PlugUpdate fzf`
Building fzf
------------
See [BUILD.md](BUILD.md).
Usage Usage
----- -----
fzf will launch curses-based finder, read the list from STDIN, and write the fzf will launch interactive finder, read the list from STDIN, and write the
selected item to STDOUT. selected item to STDOUT.
```sh ```sh
@@ -101,27 +201,50 @@ vim $(fzf)
#### Using the finder #### Using the finder
- `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P)` to move cursor up and down - `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P`) to move cursor up and down
- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit - `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit
- On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items - On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items
- Emacs style key bindings - Emacs style key bindings
- Mouse: scroll, click, double-click; shift-click and shift-scroll on - Mouse: scroll, click, double-click; shift-click and shift-scroll on
multi-select mode multi-select mode
#### Layout
fzf by default starts in fullscreen mode, but you can make it start below the
cursor with `--height` option.
```sh
vim $(fzf --height 40%)
```
Also check out `--reverse` option if you prefer "top-down" layout instead of
the default "bottom-up" layout.
```sh
vim $(fzf --height 40% --reverse)
```
You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by
default. For example,
```sh
export FZF_DEFAULT_OPTS='--height 40% --reverse --border'
```
#### Search syntax #### Search syntax
Unless otherwise specified, fzf starts in "extended-search mode" where you can Unless otherwise specified, fzf starts in "extended-search mode" where you can
type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt
!rmx` !fire`
| Token | Match type | Description | | Token | Match type | Description |
| -------- | -------------------- | -------------------------------- | | -------- | -------------------------- | --------------------------------- |
| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` | | `sbtrkt` | fuzzy-match | Items that match `sbtrkt` |
| `^music` | prefix-exact-match | Items that start with `music` | | `^music` | prefix-exact-match | Items that start with `music` |
| `.mp3$` | suffix-exact-match | Items that end with `.mp3` | | `.mp3$` | suffix-exact-match | Items that end with `.mp3` |
| `'wild` | exact-match (quoted) | Items that include `wild` | | `'wild` | exact-match (quoted) | Items that include `wild` |
| `!rmx` | inverse-fuzzy-match | Items that do not match `rmx` | | `!fire` | inverse-exact-match | Items that do not include `fire` |
| `!'fire` | inverse-exact-match | Items that do not include `fire` | | `!.mp3$` | inverse-suffix-exact-match | Items that do not end with `.mp3` |
If you don't prefer fuzzy matching and do not wish to "quote" every word, If you don't prefer fuzzy matching and do not wish to "quote" every word,
start fzf with `-e` or `--exact` option. Note that when `--exact` is set, start fzf with `-e` or `--exact` option. Note that when `--exact` is set,
@@ -139,11 +262,15 @@ or `py`.
- `FZF_DEFAULT_COMMAND` - `FZF_DEFAULT_COMMAND`
- Default command to use when input is tty - Default command to use when input is tty
- e.g. `export FZF_DEFAULT_COMMAND='ag -g ""'` - e.g. `export FZF_DEFAULT_COMMAND='fd --type f'`
- `FZF_DEFAULT_OPTS` - `FZF_DEFAULT_OPTS`
- Default options - Default options
- e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"` - e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"`
#### Options
See the man page (`man fzf`) for the full list of options.
Examples Examples
-------- --------
@@ -170,6 +297,13 @@ 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.
Alternatively, you can use `--height HEIGHT[%]` option not to start fzf in
fullscreen mode.
```sh
fzf --height 40%
```
Key bindings for command line Key bindings for command line
----------------------------- -----------------------------
@@ -178,20 +312,25 @@ fish.
- `CTRL-T` - Paste the selected files and directories onto the command line - `CTRL-T` - Paste the selected files and directories onto the command line
- Set `FZF_CTRL_T_COMMAND` to override the default command - Set `FZF_CTRL_T_COMMAND` to override the default command
- Set `FZF_CTRL_T_OPTS` to pass additional options
- `CTRL-R` - Paste the selected command from history onto the command line - `CTRL-R` - Paste the selected command from history onto the command line
- Sort is disabled by default to respect chronological ordering - If you want to see the commands in chronological order, press `CTRL-R`
- Press `CTRL-R` again to toggle sort again which toggles sorting by relevance
- Set `FZF_CTRL_R_OPTS` to pass additional options
- `ALT-C` - cd into the selected directory - `ALT-C` - cd into the selected directory
- Set `FZF_ALT_C_COMMAND` to override the default command - Set `FZF_ALT_C_COMMAND` to override the default command
- Set `FZF_ALT_C_OPTS` to pass additional options
If you're on a tmux session, fzf will start in a split pane. You may disable If you're on a tmux session, you can start fzf in a split pane by setting
this tmux integration by setting `FZF_TMUX` to 0, or change the height of the `FZF_TMUX` to 1, and change the height of the pane with `FZF_TMUX_HEIGHT`
pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`). (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.
More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings).
Fuzzy completion for bash and zsh Fuzzy completion for bash and zsh
--------------------------------- ---------------------------------
@@ -261,12 +400,17 @@ export FZF_COMPLETION_TRIGGER='~~'
# Options to fzf command # Options to fzf command
export FZF_COMPLETION_OPTS='+c -x' export FZF_COMPLETION_OPTS='+c -x'
# Use ag instead of the default find command for listing candidates. # Use fd (https://github.com/sharkdp/fd) instead of the default find
# - The first argument to the function is the base path to start traversal # command for listing path candidates.
# - Note that ag only lists files not directories # - The first argument to the function ($1) is the base path to start traversal
# - See the source code (completion.{bash,zsh}) for the details. # - See the source code (completion.{bash,zsh}) for the details.
_fzf_compgen_path() { _fzf_compgen_path() {
ag -g "" "$1" fd --hidden --follow --exclude ".git" . "$1"
}
# Use fd to generate the list for directory completion
_fzf_compgen_dir() {
fd --type d --hidden --follow --exclude ".git" . "$1"
} }
``` ```
@@ -277,105 +421,128 @@ On bash, fuzzy completion is enabled only for a predefined set of commands
commands as well like follows. commands as well like follows.
```sh ```sh
# There are also _fzf_path_completion and _fzf_dir_completion complete -F _fzf_path_completion -o default -o bashdefault ag
complete -F _fzf_file_completion -o default -o bashdefault doge complete -F _fzf_dir_completion -o default -o bashdefault tree
``` ```
Usage as Vim plugin Vim plugin
------------------- ----------
This repository only enables basic integration with Vim. If you're looking for See [README-VIM.md](README-VIM.md).
more, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project.
(Note: To use fzf in GVim, an external terminal emulator is required.) Advanced topics
---------------
#### `:FZF[!]` ### Performance
If you have set up fzf for Vim, `:FZF` command will be added. fzf is fast, and is [getting even faster][perf]. Performance should not be
a problem in most use cases. However, you might want to be aware of the
options that affect the performance.
```vim - `--ansi` tells fzf to extract and parse ANSI color codes in the input and it
" Look for files under current directory makes the initial scanning slower. So it's not recommended that you add it
:FZF to your `$FZF_DEFAULT_OPTS`.
- `--nth` makes fzf slower as fzf has to tokenize each line.
- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each
line.
- If you absolutely need better performance, you can consider using
`--algo=v1` (the default being `v2`) to make fzf use faster greedy
algorithm. However, this algorithm is not guaranteed to find the optimal
ordering of the matches and is not recommended.
" Look for files under your home directory [perf]: https://junegunn.kr/images/fzf-0.17.0.png
:FZF ~
" With options ### Executing external programs
:FZF --no-sort -m /tmp
" Bang version starts in fullscreen instead of using tmux pane or Neovim split You can set up key bindings for starting external processes without leaving
:FZF! fzf (`execute`, `execute-silent`).
```bash
# Press F1 to open the file with less without leaving fzf
# Press CTRL-Y to copy the line to clipboard and aborts fzf (requires pbcopy)
fzf --bind 'f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort'
``` ```
Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key, See *KEY BINDINGS* section of the man page for details.
`CTRL-T`, `CTRL-X` or `CTRL-V` to open selected files in the current window,
in new tabs, in horizontal splits, or in vertical splits respectively.
Note that the environment variables `FZF_DEFAULT_COMMAND` and ### Preview window
`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for
customization.
[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim) When `--preview` option is set, fzf automatically starts external process with
the current line as the argument and shows the result in the split window.
#### `fzf#run([options])` ```bash
# {} is replaced to the single-quoted string of the focused line
fzf --preview 'cat {}'
```
For more advanced uses, you can use `fzf#run()` function with the following Since preview window is updated only after the process is complete, it's
options. important that the command finishes quickly.
| Option name | Type | Description | ```bash
| -------------------------- | ------------- | ---------------------------------------------------------------- | # Use head instead of cat so that the command doesn't take too long to finish
| `source` | string | External command to generate input to fzf (e.g. `find .`) | fzf --preview 'head -100 {}'
| `source` | list | Vim list as input to fzf | ```
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
| `sink` | funcref | Reference to function to process each selected item |
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
| `options` | string | Options to fzf |
| `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) |
| `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `launcher` | string | External terminal emulator to start fzf with (GVim only) |
| `launcher` | funcref | Function for generating `launcher` string (GVim only) |
Examples can be found on [the wiki Preview window supports ANSI colors, so you can use programs that
page](https://github.com/junegunn/fzf/wiki/Examples-(vim)). syntax-highlights the content of a file.
- Highlight: http://www.andre-simon.de/doku/highlight/en/highlight.php
- CodeRay: http://coderay.rubychan.de/
- Rouge: https://github.com/jneen/rouge
```bash
# Try highlight, coderay, rougify in turn, then fall back to cat
fzf --preview '[[ $(file --mime {}) =~ binary ]] &&
echo {} is a binary file ||
(highlight -O ansi -l {} ||
coderay {} ||
rougify {} ||
cat {}) 2> /dev/null | head -500'
```
You can customize the size and position of the preview window using
`--preview-window` option. For example,
```bash
fzf --height 40% --reverse --preview 'file {}' --preview-window down:1
```
For more advanced examples, see [Key bindings for git with fzf][fzf-git].
[fzf-git]: https://junegunn.kr/2016/07/fzf-git/
Tips Tips
---- ----
#### Rendering issues #### Respecting `.gitignore`
If you have any rendering issues, check the following: You can use [fd](https://github.com/sharkdp/fd),
[ripgrep](https://github.com/BurntSushi/ripgrep), or [the silver
1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it searcher](https://github.com/ggreer/the_silver_searcher) instead of the
contains `256` (e.g. `xterm-256color`) default find command to traverse the file system while respecting
2. If you're on screen or tmux, `$TERM` should be either `screen` or `.gitignore`.
`screen-256color`
3. Some terminal emulators (e.g. mintty) have problem displaying default
background color and make some text unable to read. In that case, try
`--black` option. And if it solves your problem, I recommend including it
in `FZF_DEFAULT_OPTS` for further convenience.
4. If you still have problem, try `--no-256` option or even `--no-color`.
#### Respecting `.gitignore`, `.hgignore`, and `svn:ignore`
[ag](https://github.com/ggreer/the_silver_searcher) or
[pt](https://github.com/monochromegane/the_platinum_searcher) will do the
filtering:
```sh ```sh
# Feed the output of ag into fzf # Feed the output of fd into fzf
ag -g "" | fzf fd --type f | fzf
# Setting ag as the default source for fzf # Setting fd as the default source for fzf
export FZF_DEFAULT_COMMAND='ag -g ""' export FZF_DEFAULT_COMMAND='fd --type f'
# Now fzf (w/o pipe) will use ag instead of find # Now fzf (w/o pipe) will use fd instead of find
fzf fzf
# To apply the command to CTRL-T as well # To apply the command to CTRL-T as well
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
``` ```
If you want the command to follow symbolic links, and don't want it to exclude
hidden files, use the following command:
```sh
export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
```
#### `git ls-tree` for fast traversal #### `git ls-tree` for fast traversal
If you're running fzf in a large git repository, `git ls-tree` can boost up the If you're running fzf in a large git repository, `git ls-tree` can boost up the
@@ -390,21 +557,43 @@ export FZF_DEFAULT_COMMAND='
#### Fish shell #### Fish shell
It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362) Fish shell before version 2.6.0 [doesn't allow](https://github.com/fish-shell/fish-shell/issues/1362)
that it doesn't allow reading from STDIN in command substitution, which means reading from STDIN in command substitution, which means simple `vim (fzf)`
simple `vim (fzf)` won't work as expected. The workaround is to store the result doesn't work as expected. The workaround for fish 2.5.0 and earlier is to use
of fzf to a temporary file. the `read` fish command:
```sh ```sh
fzf > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result) fzf | read -l result; and vim $result
``` ```
License or, for multiple results:
-------
[MIT](LICENSE) ```sh
fzf -m | while read -l r; set result $result $r; end; and vim $result
```
Author The globbing system is different in fish and thus `**` completion will not work.
------ However, the `CTRL-T` command will use the last token on the commandline as the
root folder for the recursive search. For instance, hitting `CTRL-T` at the end
of the following commandline
Junegunn Choi ```sh
ls /var/
```
will list all files and folders under `/var/`.
When using a custom `FZF_CTRL_T_COMMAND`, use the unexpanded `$dir` variable to
make use of this feature. `$dir` defaults to `.` when the last token is not a
valid directory. Example:
```sh
set -g FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'"
```
[License](LICENSE)
------------------
The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi

View File

@@ -2,25 +2,52 @@
# fzf-tmux: starts fzf in a tmux pane # fzf-tmux: starts fzf in a tmux pane
# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] # usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
fail() {
>&2 echo "$1"
exit 2
}
fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
[[ -x "$fzf" ]] || fail 'fzf executable not found'
args=() args=()
opt="" opt=""
skip="" skip=""
swap="" swap=""
close="" close=""
term="" term=""
[ -n "$LINES" ] && lines=$LINES || lines=$(tput lines) [[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
while [ $# -gt 0 ]; do [[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
help() {
>&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
Layout
-u [HEIGHT[%]] Split above (up)
-d [HEIGHT[%]] Split below (down)
-l [WIDTH[%]] Split left
-r [WIDTH[%]] Split right
(default: -d 50%)
'
exit
}
while [[ $# -gt 0 ]]; do
arg="$1" arg="$1"
case "$arg" in shift
[[ -z "$skip" ]] && case "$arg" in
-) -)
term=1 term=1
;; ;;
--help)
help
;;
--version)
echo "fzf-tmux (with fzf $("$fzf" --version))"
exit
;;
-w*|-h*|-d*|-u*|-r*|-l*) -w*|-h*|-d*|-u*|-r*|-l*)
if [ -n "$skip" ]; then
args+=("$1")
shift
continue
fi
if [[ "$arg" =~ ^.[lrw] ]]; then if [[ "$arg" =~ ^.[lrw] ]]; then
opt="-h" opt="-h"
if [[ "$arg" =~ ^.l ]]; then if [[ "$arg" =~ ^.l ]]; then
@@ -36,35 +63,33 @@ while [ $# -gt 0 ]; do
close="; tmux swap-pane -D" close="; tmux swap-pane -D"
fi fi
fi fi
if [ ${#arg} -gt 2 ]; then if [[ ${#arg} -gt 2 ]]; then
size="${arg:2}" size="${arg:2}"
else else
shift
if [[ "$1" =~ ^[0-9]+%?$ ]]; then if [[ "$1" =~ ^[0-9]+%?$ ]]; then
size="$1" size="$1"
else
[ -n "$1" -a "$1" != "--" ] && args+=("$1")
shift shift
else
continue continue
fi fi
fi fi
if [[ "$size" =~ %$ ]]; then if [[ "$size" =~ %$ ]]; then
size=${size:0:((${#size}-1))} size=${size:0:((${#size}-1))}
if [ -n "$swap" ]; then if [[ -n "$swap" ]]; then
opt="$opt -p $(( 100 - size ))" opt="$opt -p $(( 100 - size ))"
else else
opt="$opt -p $size" opt="$opt -p $size"
fi fi
else else
if [ -n "$swap" ]; then if [[ -n "$swap" ]]; then
if [[ "$arg" =~ ^.l ]]; then if [[ "$arg" =~ ^.l ]]; then
[ -n "$COLUMNS" ] && max=$COLUMNS || max=$(tput cols) max=$columns
else else
max=$lines max=$lines
fi fi
size=$(( max - size )) size=$(( max - size ))
[ $size -lt 0 ] && size=0 [[ $size -lt 0 ]] && size=0
opt="$opt -l $size" opt="$opt -l $size"
else else
opt="$opt -l $size" opt="$opt -l $size"
@@ -75,24 +100,28 @@ while [ $# -gt 0 ]; do
# "--" can be used to separate fzf-tmux options from fzf options to # "--" can be used to separate fzf-tmux options from fzf options to
# avoid conflicts # avoid conflicts
skip=1 skip=1
continue
;; ;;
*) *)
args+=("$1") args+=("$arg")
;; ;;
esac esac
shift [[ -n "$skip" ]] && args+=("$arg")
done done
if ! [ -n "$TMUX" -a "$lines" -gt 15 ]; then if [[ -z "$TMUX" || "$opt" =~ ^-h && "$columns" -le 40 || ! "$opt" =~ ^-h && "$lines" -le 15 ]]; then
fzf "${args[@]}" "$fzf" "${args[@]}"
exit $? exit $?
fi fi
# --height option is not allowed
args+=("--no-height")
# Handle zoomed tmux pane by moving it to a temp window # Handle zoomed tmux pane by moving it to a temp window
if tmux list-panes -F '#F' | grep -q Z; then if tmux list-panes -F '#F' | grep -q Z; then
zoomed=1 zoomed=1
original_window=$(tmux display-message -p "#{window_id}") original_window=$(tmux display-message -p "#{window_id}")
tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - \\\\; do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'") tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'")
tmux swap-pane -t $tmp_window \; select-window -t $tmp_window tmux swap-pane -t $tmp_window \; select-window -t $tmp_window
fi fi
@@ -105,28 +134,28 @@ fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id" fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id" fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
cleanup() { cleanup() {
rm -f $argsf $fifo1 $fifo2 $fifo3 \rm -f $argsf $fifo1 $fifo2 $fifo3
# Remove temp window if we were zoomed # Remove temp window if we were zoomed
if [ -n "$zoomed" ]; then if [[ -n "$zoomed" ]]; then
tmux display-message -p "#{window_id}" > /dev/null
tmux swap-pane -t $original_window \; \ tmux swap-pane -t $original_window \; \
select-window -t $original_window \; \ select-window -t $original_window \; \
kill-window -t $tmp_window \; \ kill-window -t $tmp_window \; \
resize-pane -Z resize-pane -Z
fi fi
}
trap cleanup EXIT SIGINT SIGTERM
fail() { if [ $# -gt 0 ]; then
>&2 echo "$1" trap - EXIT
exit 2 exit 130
fi
} }
fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" trap 'cleanup 1' SIGUSR1
[ -x "$fzf" ] || fail "fzf executable not found" trap 'cleanup' EXIT
envs="env TERM=$TERM " 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 -m o+w $fifo2 mkfifo -m o+w $fifo2
mkfifo -m o+w $fifo3 mkfifo -m o+w $fifo3
@@ -141,18 +170,22 @@ for arg in "${args[@]}"; do
opts="$opts \"$arg\"" opts="$opts \"$arg\""
done done
if [ -n "$term" -o -t 0 ]; then pppid=$$
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf echo -n "trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM;" > $argsf
tmux set-window-option synchronize-panes off \;\ close="; trap - EXIT SIGINT SIGTERM $close"
if [[ -n "$term" ]] || [[ -t 0 ]]; then
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf
TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\
set-window-option remain-on-exit off \;\ set-window-option remain-on-exit off \;\
split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \ split-window $opt "$envs bash -c 'cd $(printf %q "$PWD"); exec -a fzf bash $argsf'" $swap \
> /dev/null 2>&1 > /dev/null 2>&1
else else
mkfifo $fifo1 mkfifo $fifo1
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf
tmux set-window-option synchronize-panes off \;\ TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\
set-window-option remain-on-exit off \;\ set-window-option remain-on-exit off \;\
split-window $opt "$envs bash $argsf" $swap \ split-window $opt "$envs bash -c 'exec -a fzf bash $argsf'" $swap \
> /dev/null 2>&1 > /dev/null 2>&1
cat <&0 > $fifo1 & cat <&0 > $fifo1 &
fi fi

226
doc/fzf.txt Normal file
View File

@@ -0,0 +1,226 @@
fzf.txt fzf Last change: November 19 2017
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
==============================================================================
FZF Vim integration
:FZF[!]
Configuration
Examples
fzf#run
fzf#wrap
fzf inside terminal buffer
Hide statusline
GVim
License
FZF VIM INTEGRATION *fzf-vim-integration*
==============================================================================
This repository only enables basic integration with Vim. If you're looking for
more, check out {fzf.vim}{1} project.
(Note: To use fzf in GVim, an external terminal emulator is required.)
{1} https://github.com/junegunn/fzf.vim
:FZF[!]
==============================================================================
*:FZF*
If you have set up fzf for Vim, `:FZF` command will be added.
>
" Look for files under current directory
:FZF
" Look for files under your home directory
:FZF ~
" With options
:FZF --no-sort --reverse --inline-info /tmp
" Bang version starts fzf in fullscreen mode
:FZF!
<
Similarly to {ctrlp.vim}{2}, use enter key, CTRL-T, CTRL-X or CTRL-V to open
selected files in the current window, in new tabs, in horizontal splits, or in
vertical splits respectively.
Note that the environment variables `FZF_DEFAULT_COMMAND` and
`FZF_DEFAULT_OPTS` also apply here.
{2} https://github.com/kien/ctrlp.vim
< Configuration >_____________________________________________________________~
*fzf-configuration*
*g:fzf_action* *g:fzf_layout* *g:fzf_colors* *g:fzf_history_dir* *g:fzf_launcher*
*g:Fzf_launcher*
- `g:fzf_action`
- Customizable extra key bindings for opening selected files in different
ways
- `g:fzf_layout`
- Determines the size and position of fzf window
- `g:fzf_colors`
- Customizes fzf colors to match the current color scheme
- `g:fzf_history_dir`
- Enables history feature
- `g:fzf_launcher`
- (Only in GVim) Terminal emulator to open fzf with
- `g:Fzf_launcher` for function reference
Examples~
*fzf-examples*
>
" This is the default extra key bindings
let g:fzf_action = {
\ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' }
" An action can be a reference to a function that processes selected lines
function! s:build_quickfix_list(lines)
call setqflist(map(copy(a:lines), '{ "filename": v:val }'))
copen
cc
endfunction
let g:fzf_action = {
\ 'ctrl-q': function('s:build_quickfix_list'),
\ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' }
" Default fzf layout
" - down / up / left / right
let g:fzf_layout = { 'down': '~40%' }
" You can set up fzf window using a Vim command (Neovim or latest Vim 8 required)
let g:fzf_layout = { 'window': 'enew' }
let g:fzf_layout = { 'window': '-tabnew' }
let g:fzf_layout = { 'window': '10split enew' }
" Customize fzf colors to match your color scheme
let g:fzf_colors =
\ { 'fg': ['fg', 'Normal'],
\ 'bg': ['bg', 'Normal'],
\ 'hl': ['fg', 'Comment'],
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
\ 'hl+': ['fg', 'Statement'],
\ 'info': ['fg', 'PreProc'],
\ 'border': ['fg', 'Ignore'],
\ 'prompt': ['fg', 'Conditional'],
\ 'pointer': ['fg', 'Exception'],
\ 'marker': ['fg', 'Keyword'],
\ 'spinner': ['fg', 'Label'],
\ 'header': ['fg', 'Comment'] }
" Enable per-command history.
" CTRL-N and CTRL-P will be automatically bound to next-history and
" previous-history instead of down and up. If you don't like the change,
" explicitly bind the keys to down and up in your $FZF_DEFAULT_OPTS.
let g:fzf_history_dir = '~/.local/share/fzf-history'
<
FZF#RUN *fzf#run*
==============================================================================
For more advanced uses, you can use `fzf#run([options])` function with the
following options.
---------------------------+---------------+--------------------------------------------------------------
Option name | Type | Description ~
---------------------------+---------------+--------------------------------------------------------------
`source` | string | External command to generate input to fzf (e.g. `find.` )
`source` | list | Vim list as input to fzf
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
`sink` | funcref | Reference to function to process each selected item
`sink*` | funcref | Similar to `sink` , but takes the list of output lines at once
`options` | string/list | Options to fzf
`dir` | string | Working directory
`up` / `down` / `left` / `right` | number/string | Use tmux pane with the given size (e.g. `20` , `50%` )
`window` (Vim 8 / Neovim) | string | Command to open fzf window (e.g. `verticalaboveleft30new` )
`launcher` | string | External terminal emulator to start fzf with (GVim only)
`launcher` | funcref | Function for generating `launcher` string (GVim only)
---------------------------+---------------+--------------------------------------------------------------
`options` entry can be either a string or a list. For simple cases, string
should suffice, but prefer to use list type if you're concerned about escaping
issues on different platforms.
>
call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'})
call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']})
<
FZF#WRAP *fzf#wrap*
==============================================================================
`fzf#wrap([namestring,][optsdict,][fullscreenboolean])` is a helper
function that decorates the options dictionary so that it understands
`g:fzf_layout`, `g:fzf_action`, `g:fzf_colors`, and `g:fzf_history_dir` like
`:FZF`.
>
command! -bang MyStuff
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0))
<
FZF INSIDE TERMINAL BUFFER *fzf-inside-terminal-buffer*
==============================================================================
The latest versions of Vim and Neovim include builtin terminal emulator
(`:terminal`) and fzf will start in a terminal buffer in the following cases:
- On Neovim
- On GVim
- On Terminal Vim with the non-default layout
- `callfzf#run({'left':'30%'})` or `letg:fzf_layout={'left':'30%'}`
< Hide statusline >___________________________________________________________~
*fzf-hide-statusline*
When fzf starts in a terminal buffer, you may want to hide the statusline of
the containing buffer.
>
autocmd! FileType fzf
autocmd FileType fzf set laststatus=0 noshowmode noruler
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
<
GVIM *fzf-gvim*
==============================================================================
With the latest version of GVim, fzf will start inside the builtin terminal
emulator of Vim. Please note that this terminal feature of Vim is still young
and unstable and you may run into some issues.
If you have an older version of GVim, you need an external terminal emulator
to start fzf with. `xterm` command is used by default, but you can customize
it with `g:fzf_launcher`.
>
" This is the default. %s is replaced with fzf command
let g:fzf_launcher = 'xterm -e bash -ic %s'
" Use urxvt instead
let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s'
<
If you're running MacVim on OSX, I recommend you to use iTerm2 as the
launcher. Refer to the {this wiki page}{3} to see how to set up.
{3} https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2
LICENSE *fzf-license*
==============================================================================
The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi
==============================================================================
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:

1348
fzf

File diff suppressed because it is too large Load Diff

134
glide.lock generated Normal file
View File

@@ -0,0 +1,134 @@
hash: 92a208bfbaecdf8d1ccaf99a465884c49f9cd91f44f1756d7bbf3290795c781b
updated: 2017-12-03T13:37:23.420874333+09:00
imports:
- name: github.com/bjwbell/gensimd
version: 06eb18285485c0d572cc7f024050fc6cb652ed4c
subpackages:
- simd
- name: github.com/codegangsta/cli
version: c6af8847eb2b7b297d07c3ede98903e95e680ef9
- name: github.com/gdamore/encoding
version: b23993cbb6353f0e6aa98d0ee318a34728f628b9
- name: github.com/gdamore/tcell
version: 0a0db94084dfe181108c18508ebd312f12d331fb
subpackages:
- encoding
- name: github.com/lucasb-eyer/go-colorful
version: c900de9dbbc73129068f5af6a823068fc5f2308c
- name: github.com/Masterminds/semver
version: 15d8430ab86497c5c0da827b748823945e1cf1e1
- name: github.com/Masterminds/vcs
version: 6f1c6d150500e452704e9863f68c2559f58616bf
- name: github.com/mattn/go-isatty
version: 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8
- name: github.com/mattn/go-runewidth
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
- name: github.com/mattn/go-shellwords
version: 02e3cf038dcea8290e44424da473dd12be796a8a
- name: github.com/mengzhuo/intrinsic
version: 34b800838e0bcd9c5b6abd414e3ad03dc1a686b8
subpackages:
- sse2
- name: github.com/mitchellh/go-homedir
version: b8bc1bf767474819792c23f32d8286a45736f1c6
- name: golang.org/x/crypto
version: e1a4589e7d3ea14a3352255d04b6f1a418845e5e
subpackages:
- acme
- blowfish
- cast5
- chacha20poly1305/internal/chacha20
- curve25519
- ed25519
- ed25519/internal/edwards25519
- hkdf
- nacl/secretbox
- openpgp
- openpgp/armor
- openpgp/elgamal
- openpgp/errors
- openpgp/packet
- openpgp/s2k
- pbkdf2
- pkcs12/internal/rc2
- poly1305
- ripemd160
- salsa20/salsa
- ssh
- ssh/agent
- ssh/terminal
- ssh/testdata
- name: golang.org/x/net
version: a8b9294777976932365dabb6640cf1468d95c70f
subpackages:
- context
- context/ctxhttp
- name: golang.org/x/sys
version: b90f89a1e7a9c1f6b918820b3daa7f08488c8594
subpackages:
- unix
- name: golang.org/x/text
version: 4ee4af566555f5fbe026368b75596286a312663a
subpackages:
- cases
- collate
- collate/build
- currency
- encoding
- encoding/charmap
- encoding/ianaindex
- encoding/internal
- encoding/internal/identifier
- encoding/japanese
- encoding/korean
- encoding/simplifiedchinese
- encoding/traditionalchinese
- encoding/unicode
- encoding/unicode/utf32
- internal
- internal/colltab
- internal/format
- internal/gen
- internal/stringset
- internal/tag
- internal/testtext
- internal/triegen
- internal/ucd
- internal/utf8internal
- language
- language/display
- message
- runes
- secure/bidirule
- transform
- unicode/bidi
- unicode/cldr
- unicode/norm
- unicode/rangetable
- width
- name: golang.org/x/tools
version: 04447353bc504b9a5c02eb227b9ecd252e64ea20
subpackages:
- go/ast/astutil
- go/buildutil
- go/loader
- name: gopkg.in/yaml.v2
version: 287cf08546ab5e7e37d55a84f7ed3fd1db036de5
testImports:
- name: github.com/gopherjs/gopherjs
version: 444abdf920945de5d4a977b572bcc6c674d1e4eb
subpackages:
- js
- name: github.com/jtolds/gls
version: 77f18212c9c7edc9bd6a33d383a7b545ce62f064
- name: github.com/smartystreets/assertions
version: 0b37b35ec7434b77e77a4bb29b79677cced992ea
subpackages:
- internal/go-render/render
- internal/oglematchers
- name: github.com/smartystreets/goconvey
version: e5b2b7c9111590d019a696c7800593f666e1a7f4
subpackages:
- convey
- convey/gotest
- convey/reporting

16
glide.yaml Normal file
View File

@@ -0,0 +1,16 @@
package: github.com/junegunn/fzf
import:
- package: github.com/mattn/go-isatty
version: 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8
- package: github.com/mattn/go-runewidth
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
- package: github.com/mattn/go-shellwords
version: 02e3cf038dcea8290e44424da473dd12be796a8a
- package: github.com/gdamore/tcell
version: 0a0db94084dfe181108c18508ebd312f12d331fb
subpackages:
- encoding
- package: golang.org/x/crypto
version: e1a4589e7d3ea14a3352255d04b6f1a418845e5e
subpackages:
- ssh/terminal

282
install
View File

@@ -2,13 +2,13 @@
set -u set -u
[[ "$@" =~ --pre ]] && version=0.13.0 pre=1 || version=0.17.3
version=0.13.0 pre=0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
binary_arch= binary_arch=
allow_legacy=
shells="bash zsh fish"
help() { help() {
cat << EOF cat << EOF
@@ -22,6 +22,10 @@ usage: $0 [OPTIONS]
--[no-]completion Enable/disable fuzzy completion (bash & zsh) --[no-]completion Enable/disable fuzzy completion (bash & zsh)
--[no-]update-rc Whether or not to update shell configuration files --[no-]update-rc Whether or not to update shell configuration files
--no-bash Do not set up bash configuration
--no-zsh Do not set up zsh configuration
--no-fish Do not set up fish configuration
--32 Download 32-bit binary --32 Download 32-bit binary
--64 Download 64-bit binary --64 Download 64-bit binary
EOF EOF
@@ -37,6 +41,7 @@ for opt in "$@"; do
auto_completion=1 auto_completion=1
key_bindings=1 key_bindings=1
update_config=1 update_config=1
allow_legacy=1
;; ;;
--key-bindings) key_bindings=1 ;; --key-bindings) key_bindings=1 ;;
--no-key-bindings) key_bindings=0 ;; --no-key-bindings) key_bindings=0 ;;
@@ -46,7 +51,10 @@ for opt in "$@"; do
--no-update-rc) update_config=0 ;; --no-update-rc) update_config=0 ;;
--32) binary_arch=386 ;; --32) binary_arch=386 ;;
--64) binary_arch=amd64 ;; --64) binary_arch=amd64 ;;
--bin|--pre) ;; --bin) ;;
--no-bash) shells=${shells/bash/} ;;
--no-zsh) shells=${shells/zsh/} ;;
--no-fish) shells=${shells/fish/} ;;
*) *)
echo "unknown option: $opt" echo "unknown option: $opt"
help help
@@ -59,20 +67,20 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
fzf_base="$(pwd)" fzf_base="$(pwd)"
ask() { ask() {
# If stdin is a tty, we are "interactive". while true; do
# non-interactive shell: wait for a linefeed read -p "$1 ([y]/n) " -r
# interactive shell: continue after a single keypress if [[ $REPLY =~ ^[Yy]$ ]]; then
read_n=$([ -t 0 ] && echo "-n 1") return 1
elif [[ $REPLY =~ ^[Nn]$ ]]; then
read -p "$1 ([y]/n) " $read_n -r return 0
echo fi
[[ $REPLY =~ ^[Nn]$ ]] done
} }
check_binary() { check_binary() {
echo -n " - Checking fzf executable ... " echo -n " - Checking fzf executable ... "
local output local output
output=$("$fzf_base"/bin/fzf --version 2>&1) output=$("$fzf_base"/bin/fzf --version 2>&1 | awk '{print $1}')
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Error: $output" echo "Error: $output"
binary_error="Invalid binary" binary_error="Invalid binary"
@@ -88,17 +96,6 @@ check_binary() {
return 1 return 1
} }
symlink() {
echo " - Creating symlink: bin/$1 -> bin/fzf"
(cd "$fzf_base"/bin &&
rm -f fzf &&
ln -sf $1 fzf)
if [ $? -ne 0 ]; then
binary_error="Failed to create symlink"
return 1
fi
}
link_fzf_in_path() { link_fzf_in_path() {
if which_fzf="$(command -v fzf)"; then if which_fzf="$(command -v fzf)"; then
echo " - Found in \$PATH" echo " - Found in \$PATH"
@@ -109,16 +106,33 @@ link_fzf_in_path() {
return 1 return 1
} }
try_curl() {
command -v curl > /dev/null &&
if [[ $1 =~ tgz$ ]]; then
curl -fL $1 | tar -xzf -
else
local temp=${TMPDIR:-/tmp}/fzf.zip
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
fi
}
try_wget() {
command -v wget > /dev/null &&
if [[ $1 =~ tgz$ ]]; then
wget -O - $1 | tar -xzf -
else
local temp=${TMPDIR:-/tmp}/fzf.zip
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
fi
}
download() { download() {
echo "Downloading bin/fzf ..." echo "Downloading bin/fzf ..."
if [ $pre = 0 ]; then if [[ ! "$version" =~ alpha ]]; then
if [ -x "$fzf_base"/bin/fzf ]; then if [ -x "$fzf_base"/bin/fzf ]; then
echo " - Already exists" echo " - Already exists"
check_binary && return check_binary && return
fi fi
if [ -x "$fzf_base"/bin/$1 ]; then
symlink $1 && check_binary && return
fi
link_fzf_in_path && return link_fzf_in_path && return
fi fi
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
@@ -127,22 +141,24 @@ download() {
return return
fi fi
local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz local url
if command -v curl > /dev/null; then [[ "$version" =~ alpha ]] &&
curl -fL $url | tar -xz url=https://github.com/junegunn/fzf-bin/releases/download/alpha/${1} ||
elif command -v wget > /dev/null; then url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}
wget -O - $url | tar -xz set -o pipefail
else if ! (try_curl $url || try_wget $url); then
binary_error="curl or wget not found" set +o pipefail
binary_error="Failed to download with curl and wget"
return return
fi fi
set +o pipefail
if [ ! -f $1 ]; then if [ ! -f fzf ]; then
binary_error="Failed to download ${1}" binary_error="Failed to download ${1}"
return return
fi fi
chmod +x $1 && symlink $1 && check_binary chmod +x fzf && check_binary
} }
# Try to download binary executable # Try to download binary executable
@@ -150,110 +166,64 @@ archi=$(uname -sm)
binary_available=1 binary_available=1
binary_error="" binary_error=""
case "$archi" in case "$archi" in
Darwin\ x86_64) download fzf-$version-darwin_${binary_arch:-amd64} ;; Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64}.tgz ;;
Darwin\ i*86) download fzf-$version-darwin_${binary_arch:-386} ;; Darwin\ *86) download fzf-$version-darwin_${binary_arch:-386}.tgz ;;
Linux\ x86_64) download fzf-$version-linux_${binary_arch:-amd64} ;; Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5}.tgz ;;
Linux\ i*86) download fzf-$version-linux_${binary_arch:-386} ;; Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6}.tgz ;;
*) binary_available=0 binary_error=1 ;; Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7}.tgz ;;
Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;;
Linux\ aarch64*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;;
Linux\ *64) download fzf-$version-linux_${binary_arch:-amd64}.tgz ;;
Linux\ *86) download fzf-$version-linux_${binary_arch:-386}.tgz ;;
FreeBSD\ *64) download fzf-$version-freebsd_${binary_arch:-amd64}.tgz ;;
FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386}.tgz ;;
OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64}.tgz ;;
OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386}.tgz ;;
CYGWIN*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
MINGW*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;;
MINGW*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
*) binary_available=0 binary_error=1 ;;
esac esac
install_ruby_fzf() {
echo "Installing legacy Ruby version ..."
# ruby executable
echo -n "Checking Ruby executable ... "
ruby=$(command -v ruby)
if [ $? -ne 0 ]; then
echo "ruby executable not found !!!"
exit 1
fi
# System ruby is preferred
system_ruby=/usr/bin/ruby
if [ -x $system_ruby ] && [ $system_ruby != "$ruby" ]; then
$system_ruby --disable-gems -rcurses -e0 2> /dev/null
[ $? -eq 0 ] && ruby=$system_ruby
fi
echo "OK ($ruby)"
# Curses-support
echo -n "Checking Curses support ... "
"$ruby" -rcurses -e0 2> /dev/null
if [ $? -eq 0 ]; then
echo "OK"
else
echo "Not found"
echo "Installing 'curses' gem ... "
if (( EUID )); then
/usr/bin/env gem install curses --user-install
else
/usr/bin/env gem install curses
fi
if [ $? -ne 0 ]; then
echo
echo "Failed to install 'curses' gem."
if [[ $(uname -r) =~ 'ARCH' ]]; then
echo "Make sure that base-devel package group is installed."
fi
exit 1
fi
fi
# Ruby version
echo -n "Checking Ruby version ... "
"$ruby" -e 'exit RUBY_VERSION >= "1.9"'
if [ $? -eq 0 ]; then
echo ">= 1.9"
"$ruby" --disable-gems -rcurses -e0 2> /dev/null
if [ $? -eq 0 ]; then
fzf_cmd="$ruby --disable-gems $fzf_base/fzf"
else
fzf_cmd="$ruby $fzf_base/fzf"
fi
else
echo "< 1.9"
fzf_cmd="$ruby $fzf_base/fzf"
fi
# Create fzf script
echo -n "Creating wrapper script for fzf ... "
rm -f "$fzf_base"/bin/fzf
echo "#!/bin/sh" > "$fzf_base"/bin/fzf
echo "$fzf_cmd \"\$@\"" >> "$fzf_base"/bin/fzf
chmod +x "$fzf_base"/bin/fzf
echo "OK"
}
cd "$fzf_base" cd "$fzf_base"
if [ -n "$binary_error" ]; then if [ -n "$binary_error" ]; then
if [ $binary_available -eq 0 ]; then if [ $binary_available -eq 0 ]; then
echo "No prebuilt binary for $archi ..." echo "No prebuilt binary for $archi ..."
if command -v go > /dev/null; then
echo -n "Building binary (go get -u github.com/junegunn/fzf/src/fzf) ... "
if [ -z "${GOPATH-}" ]; then
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
mkdir -p "$GOPATH"
fi
if go get -u github.com/junegunn/fzf/src/fzf; then
echo "OK"
cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
else
echo "Failed to build binary ..."
install_ruby_fzf
fi
else
echo "go executable not found. Cannot build binary ..."
install_ruby_fzf
fi
else else
echo " - $binary_error !!!" echo " - $binary_error !!!"
fi
if command -v go > /dev/null; then
echo -n "Building binary (go get -u github.com/junegunn/fzf) ... "
if [ -z "${GOPATH-}" ]; then
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
mkdir -p "$GOPATH"
fi
if go get -u github.com/junegunn/fzf; then
echo "OK"
cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
else
echo "Failed to build binary. Installation failed."
exit 1
fi
else
echo "go executable not found. Installation failed."
exit 1 exit 1
fi fi
fi fi
[[ "$*" =~ "--bin" ]] && exit 0 [[ "$*" =~ "--bin" ]] && exit 0
for s in $shells; do
if ! command -v "$s" > /dev/null; then
shells=${shells/$s/}
fi
done
if [[ ${#shells} -lt 3 ]]; then
echo "No shell configuration to be updated."
exit 0
fi
# Auto-completion # Auto-completion
if [ -z "$auto_completion" ]; then if [ -z "$auto_completion" ]; then
ask "Do you want to enable fuzzy auto-completion?" ask "Do you want to enable fuzzy auto-completion?"
@@ -267,9 +237,8 @@ if [ -z "$key_bindings" ]; then
fi fi
echo echo
has_zsh=$(command -v zsh > /dev/null && echo 1 || echo 0)
shells=$([ $has_zsh -eq 1 ] && echo "bash zsh" || echo "bash")
for shell in $shells; do for shell in $shells; do
[[ "$shell" = fish ]] && continue
echo -n "Generate ~/.fzf.$shell ... " echo -n "Generate ~/.fzf.$shell ... "
src=~/.fzf.${shell} src=~/.fzf.${shell}
@@ -290,12 +259,6 @@ if [[ ! "\$PATH" == *$fzf_base/bin* ]]; then
export PATH="\$PATH:$fzf_base/bin" export PATH="\$PATH:$fzf_base/bin"
fi fi
# Man path
# --------
if [[ ! "\$MANPATH" == *$fzf_base/man* && -d "$fzf_base/man" ]]; then
export MANPATH="\$MANPATH:$fzf_base/man"
fi
# Auto-completion # Auto-completion
# --------------- # ---------------
$fzf_completion $fzf_completion
@@ -309,11 +272,10 @@ EOF
done done
# fish # fish
has_fish=$(command -v fish > /dev/null && echo 1 || echo 0) if [[ "$shells" =~ fish ]]; then
if [ $has_fish -eq 1 ]; then
echo -n "Update fish_user_paths ... " echo -n "Update fish_user_paths ... "
fish << EOF fish << EOF
echo \$fish_user_paths | grep $fzf_base/bin > /dev/null echo \$fish_user_paths | \grep $fzf_base/bin > /dev/null
or set --universal fish_user_paths \$fish_user_paths $fzf_base/bin or set --universal fish_user_paths \$fish_user_paths $fzf_base/bin
EOF EOF
[ $? -eq 0 ] && echo "OK" || echo "Failed" [ $? -eq 0 ] && echo "OK" || echo "Failed"
@@ -344,20 +306,22 @@ append_line() {
line="$2" line="$2"
file="$3" file="$3"
pat="${4:-}" pat="${4:-}"
lno=""
echo "Update $file:" echo "Update $file:"
echo " - $line" echo " - $line"
[ -f "$file" ] || touch "$file" if [ -f "$file" ]; then
if [ $# -lt 4 ]; then if [ $# -lt 4 ]; then
lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ') lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ')
else else
lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ') lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ')
fi
fi fi
if [ -n "$lno" ]; then if [ -n "$lno" ]; then
echo " - Already exists: line #$lno" echo " - Already exists: line #$lno"
else else
if [ $update -eq 1 ]; then if [ $update -eq 1 ]; then
echo >> "$file" [ -f "$file" ] && echo >> "$file"
echo "$line" >> "$file" echo "$line" >> "$file"
echo " + Added" echo " + Added"
else else
@@ -368,6 +332,17 @@ append_line() {
set +e set +e
} }
create_file() {
local file="$1"
shift
echo "Create $file:"
for line in "$@"; do
echo " $line"
echo "$line" >> "$file"
done
echo
}
if [ $update_config -eq 2 ]; then if [ $update_config -eq 2 ]; then
echo echo
ask "Do you want to update your shell configuration files?" ask "Do you want to update your shell configuration files?"
@@ -375,23 +350,30 @@ if [ $update_config -eq 2 ]; then
fi fi
echo echo
for shell in $shells; do for shell in $shells; do
[[ "$shell" = fish ]] && continue
[ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc [ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc
append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" "$dest" "~/.fzf.${shell}" append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" "$dest" "~/.fzf.${shell}"
done done
if [ $key_bindings -eq 1 ] && [ $has_fish -eq 1 ]; then if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then
bind_file=~/.config/fish/functions/fish_user_key_bindings.fish bind_file=~/.config/fish/functions/fish_user_key_bindings.fish
append_line $update_config "fzf_key_bindings" "$bind_file" if [ ! -e "$bind_file" ]; then
create_file "$bind_file" \
'function fish_user_key_bindings' \
' fzf_key_bindings' \
'end'
else
append_line $update_config "fzf_key_bindings" "$bind_file"
fi
fi fi
if [ $update_config -eq 1 ]; then if [ $update_config -eq 1 ]; then
echo 'Finished. Restart your shell or reload config file.' echo 'Finished. Restart your shell or reload config file.'
echo ' source ~/.bashrc # bash' [[ "$shells" =~ bash ]] && echo ' source ~/.bashrc # bash'
[ $has_zsh -eq 1 ] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh" [[ "$shells" =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
[ $has_fish -eq 1 ] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish' [[ "$shells" =~ fish ]] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish'
echo echo
echo 'Use uninstall script to remove fzf.' echo 'Use uninstall script to remove fzf.'
echo echo
fi fi
echo 'For more information, see: https://github.com/junegunn/fzf' echo 'For more information, see: https://github.com/junegunn/fzf'

View File

@@ -2,6 +2,8 @@ package main
import "github.com/junegunn/fzf/src" import "github.com/junegunn/fzf/src"
var revision string
func main() { func main() {
fzf.Run(fzf.ParseOptions()) fzf.Run(fzf.ParseOptions(), revision)
} }

54
man/man1/fzf-tmux.1 Normal file
View File

@@ -0,0 +1,54 @@
.ig
The MIT License (MIT)
Copyright (c) 2017 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf-tmux 1 "Dec 2017" "fzf 0.17.3" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane
.SH SYNOPSIS
.B fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
.SH DESCRIPTION
fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane. It is
designed to work just like fzf except that it does not take up the whole
screen. You can safely use fzf-tmux instead of fzf in your scripts as the extra
options will be silently ignored if you're not on tmux.
.SH OPTIONS
.SS Layout
(default: \fB-d 50%\fR)
.TP
.B "-u [height[%]]"
Split above (up)
.TP
.B "-d [height[%]]"
Split below (down)
.TP
.B "-l [width[%]]"
Split left
.TP
.B "-r [width[%]]"
Split right

View File

@@ -1,7 +1,7 @@
.ig .ig
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Junegunn Choi Copyright (c) 2017 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -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 "Jun 2016" "fzf 0.13.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Dec 2017" "fzf 0.17.3" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -47,6 +47,19 @@ Case-insensitive match (default: smart-case match)
.TP .TP
.B "+i" .B "+i"
Case-sensitive match Case-sensitive match
.TP
.B "--literal"
Do not normalize latin script letters for matching.
.TP
.BI "--algo=" TYPE
Fuzzy matching algorithm (default: v2)
.br
.BR v2 " Optimal scoring algorithm (quality)"
.br
.BR v1 " Faster but not guaranteed to find the optimal result (performance)"
.br
.TP .TP
.BI "-n, --nth=" "N[,..]" .BI "-n, --nth=" "N[,..]"
Comma-separated list of field index expressions for limiting search scope. Comma-separated list of field index expressions for limiting search scope.
@@ -98,6 +111,9 @@ Comma-separated list of sort criteria to apply when the scores are tied.
.B "-m, --multi" .B "-m, --multi"
Enable multi-select with tab/shift-tab Enable multi-select with tab/shift-tab
.TP .TP
.B "+m, --no-multi"
Disable multi-select
.TP
.B "--no-mouse" .B "--no-mouse"
Disable mouse Disable mouse
.TP .TP
@@ -116,13 +132,36 @@ Number of screen columns to keep to the right of the highlighted substring
(default: 10). Setting it to a large value will cause the text to be positioned (default: 10). Setting it to a large value will cause the text to be positioned
on the center of the screen. on the center of the screen.
.TP .TP
.B "--filepath-word"
Make word-wise movements and actions respect path separators. The following
actions are affected:
\fBbackward-kill-word\fR
.br
\fBbackward-word\fR
.br
\fBforward-word\fR
.br
\fBkill-word\fR
.TP
.BI "--jump-labels=" "CHARS" .BI "--jump-labels=" "CHARS"
Label characters for \fBjump\fR and \fBjump-accept\fR Label characters for \fBjump\fR and \fBjump-accept\fR
.SS Layout .SS Layout
.TP .TP
.BI "--height=" "HEIGHT[%]"
Display fzf window below the cursor with the given height instead of using
the full screen.
.TP
.BI "--min-height=" "HEIGHT"
Minimum height when \fB--height\fR is given in percent (default: 10).
Ignored when \fB--height\fR is not specified.
.TP
.B "--reverse" .B "--reverse"
Reverse orientation Reverse orientation
.TP .TP
.B "--border"
Draw border above and below the finder
.TP
.BI "--margin=" MARGIN .BI "--margin=" MARGIN
Comma-separated expression for margins around the finder. Comma-separated expression for margins around the finder.
.br .br
@@ -175,7 +214,8 @@ Number of spaces for a tab character (default: 8)
.BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]" .BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]"
Color configuration. The name of the base color scheme is followed by custom Color configuration. The name of the base color scheme is followed by custom
color mappings. Ansi color code of -1 denotes terminal default color mappings. Ansi color code of -1 denotes terminal default
foreground/background color. foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR
format.
.RS .RS
e.g. \fBfzf --color=bg+:24\fR e.g. \fBfzf --color=bg+:24\fR
@@ -199,6 +239,7 @@ e.g. \fBfzf --color=bg+:24\fR
\fBbg+ \fRBackground (current line) \fBbg+ \fRBackground (current line)
\fBhl+ \fRHighlighted substrings (current line) \fBhl+ \fRHighlighted substrings (current line)
\fBinfo \fRInfo \fBinfo \fRInfo
\fBborder \fRBorder of the preview window and horizontal separators (\fB--border\fR)
\fBprompt \fRPrompt \fBprompt \fRPrompt
\fBpointer \fRPointer to the current line \fBpointer \fRPointer to the current line
\fBmarker \fRMulti-select marker \fBmarker \fRMulti-select marker
@@ -206,6 +247,9 @@ e.g. \fBfzf --color=bg+:24\fR
\fBheader \fRHeader \fBheader \fRHeader
.RE .RE
.TP .TP
.B "--no-bold"
Do not use bold text
.TP
.B "--black" .B "--black"
Use black background Use black background
.SS History .SS History
@@ -222,17 +266,38 @@ automatically truncated when the number of the lines exceeds the value.
.TP .TP
.BI "--preview=" "COMMAND" .BI "--preview=" "COMMAND"
Execute the given command for the current line and display the result on the Execute the given command for the current line and display the result on the
preview window. \fB{}\fR is the placeholder for the quoted string of the preview window. \fB{}\fR in the command is the placeholder that is replaced to
current line. the single-quoted string of the current line. To transform the replacement
string, specify field index expressions between the braces (See \fBFIELD INDEX
EXPRESSION\fR for the details).
.RS .RS
e.g. \fBfzf --preview="head -$LINES {}"\fR e.g. \fBfzf --preview='head -$LINES {}'\fR
\fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
fzf overrides \fB$LINES\fR and \fB$COLUMNS\fR so that they represent the exact
size of the preview window.
A placeholder expression starting with \fB+\fR flag will be replaced to the
space-separated list of the selected lines (or the current line if no selection
was made) individually quoted.
e.g. \fBfzf --multi --preview='head -10 {+}'\fR
\fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR
Also, \fB{q}\fR is replaced to the current query string.
Note that you can escape a placeholder pattern by prepending a backslash.
.RE .RE
.TP .TP
.BI "--preview-window=" "[POSITION][:SIZE[%]][:hidden]" .BI "--preview-window=" "[POSITION][:SIZE[%]][:wrap][:hidden]"
Determine the layout of the preview window. If the argument ends with Determine the layout of the preview window. If the argument ends with
\fB:hidden\fR, the preview window will be hidden by default until \fB:hidden\fR, the preview window will be hidden by default until
\fBtoggle-preview\fR action is triggered. \fBtoggle-preview\fR action is triggered. Long lines are truncated by default.
Line wrap can be enabled with \fB:wrap\fR flag.
If size is given as 0, preview window will not be visible, but fzf will still
execute the command in the background.
.RS .RS
.B POSITION: (default: right) .B POSITION: (default: right)
@@ -269,12 +334,26 @@ Comma-separated list of keys that can be used to complete fzf in addition to
the default enter key. When this option is set, fzf will print the name of the the default enter key. When this option is set, fzf will print the name of the
key pressed as the first line of its output (or as the second line if key pressed as the first line of its output (or as the second line if
\fB--print-query\fR is also used). The line will be empty if fzf is completed \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. If \fB--expect\fR option is specified multiple
times, fzf will expect the union of the keys. \fB--no-expect\fR will clear the
list.
.RS .RS
e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR
.RE .RE
.TP .TP
.B "--read0"
Read input delimited by ASCII NUL characters instead of newline characters
.TP
.B "--print0"
Print output delimited by ASCII NUL characters instead of newline characters
.TP
.B "--no-clear"
Do not clear finder interface on exit. If fzf was started in full screen mode,
it will not switch back to the original screen, so you'll have to manually run
\fBtput rmcup\fR to return. This option can be used to avoid flickering of the
screen when your application needs to start fzf multiple times in order.
.TP
.B "--sync" .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.
@@ -282,6 +361,12 @@ ncurses finder only after the input stream is complete.
.RS .RS
e.g. \fBfzf --multi | fzf --sync\fR e.g. \fBfzf --multi | fzf --sync\fR
.RE .RE
.TP
.B "--version"
Display version information and exit
.TP
Note that most options have the opposite versions with \fB--no-\fR prefix.
.SH ENVIRONMENT VARIABLES .SH ENVIRONMENT VARIABLES
.TP .TP
@@ -330,6 +415,9 @@ Unless specified otherwise, fzf will start in "extended-search mode". In this
mode, you can specify multiple patterns delimited by spaces, such as: \fB'wild mode, you can specify multiple patterns delimited by spaces, such as: \fB'wild
^music .mp3$ sbtrkt !rmx\fR ^music .mp3$ sbtrkt !rmx\fR
You can prepend a backslash to a space (\fB\\ \fR) to match a literal space
character.
.SS Exact-match (quoted) .SS Exact-match (quoted)
A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as
an "exact-match" (or "non-fuzzy") term. fzf will search for the exact an "exact-match" (or "non-fuzzy") term. fzf will search for the exact
@@ -342,7 +430,7 @@ with the given string. An anchored-match term is also an exact-match term.
.SS Negation .SS Negation
If a term is prefixed by \fB!\fR, fzf will exclude the lines that satisfy the If a term is prefixed by \fB!\fR, fzf will exclude the lines that satisfy the
term from the result. term from the result. In this case, fzf performs exact match by default.
.SS Exact-match by default .SS Exact-match by default
If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with
@@ -365,8 +453,11 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.B AVAILABLE KEYS: (SYNONYMS) .B AVAILABLE KEYS: (SYNONYMS)
\fIctrl-[a-z]\fR \fIctrl-[a-z]\fR
\fIctrl-space\fR
\fIctrl-alt-[a-z]\fR
\fIalt-[a-z]\fR \fIalt-[a-z]\fR
\fIf[1-10]\fR \fIalt-[0-9]\fR
\fIf[1-12]\fR
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR) \fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
\fIspace\fR \fIspace\fR
\fIbspace\fR (\fIbs\fR) \fIbspace\fR (\fIbs\fR)
@@ -388,18 +479,26 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fIpgdn\fR (\fIpage-down\fR) \fIpgdn\fR (\fIpage-down\fR)
\fIshift-left\fR \fIshift-left\fR
\fIshift-right\fR \fIshift-right\fR
\fIleft-click\fR
\fIright-click\fR
\fIdouble-click\fR \fIdouble-click\fR
or any single character or any single character
Additionally, a special event named \fIchange\fR is available which is
triggered whenever the query string is changed.
e.g. \fBfzf --bind change:top\fR
\fBACTION: DEFAULT BINDINGS (NOTES): \fBACTION: DEFAULT BINDINGS (NOTES):
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIenter double-click\fR \fBaccept\fR \fIenter double-click\fR
\fBaccept-non-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
\fBbackward-char\fR \fIctrl-b left\fR \fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR \fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-kill-word\fR \fIalt-bs\fR \fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR \fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR \fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR \fBcancel\fR (clears query string if not empty, aborts fzf otherwise)
\fBclear-screen\fR \fIctrl-l\fR \fBclear-screen\fR \fIctrl-l\fR
\fBdelete-char\fR \fIdel\fR \fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR \fBdelete-char/eof\fR \fIctrl-d\fR
@@ -407,7 +506,8 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBdown\fR \fIctrl-j ctrl-n down\fR \fBdown\fR \fIctrl-j ctrl-n down\fR
\fBend-of-line\fR \fIctrl-e end\fR \fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details) \fBexecute(...)\fR (see below for the details)
\fBexecute-multi(...)\fR (see below for the details) \fBexecute-silent(...)\fR (see below for the details)
\fRexecute-multi(...)\fR (deprecated in favor of \fB{+}\fR expression)
\fBforward-char\fR \fIctrl-f right\fR \fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR \fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR \fBignore\fR
@@ -418,31 +518,46 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR)
\fBpage-down\fR \fIpgdn\fR \fBpage-down\fR \fIpgdn\fR
\fBpage-up\fR \fIpgup\fR \fBpage-up\fR \fIpgup\fR
\fBhalf-page-down\fR
\fBhalf-page-up\fR
\fBpreview-down\fR
\fBpreview-up\fR
\fBpreview-page-down\fR
\fBpreview-page-up\fR
\fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR) \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR)
\fBprint-query\fR (print query and exit) \fBprint-query\fR (print query and exit)
\fBreplace-query\fR (replace query string with the current selection)
\fBselect-all\fR \fBselect-all\fR
\fBtoggle\fR \fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR \fBtoggle-all\fR
\fBtoggle-down\fR \fIctrl-i (tab)\fR \fBtoggle+down\fR \fIctrl-i (tab)\fR
\fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle-up\fR : \fBtoggle-down\fR) \fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle-down\fR : \fBtoggle-up\fR) \fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBtoggle-preview\fR \fBtoggle-preview\fR
\fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR) \fBtoggle-preview-wrap\fR
\fBtoggle-up\fR \fIbtab (shift-tab)\fR \fBtoggle-sort\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBtop\fR (move to the top result)
\fBunix-line-discard\fR \fIctrl-u\fR \fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR \fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR \fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR \fByank\fR \fIctrl-y\fR
Multiple actions can be chained using \fB+\fR separator.
\fBfzf --bind 'ctrl-a:select-all+accept'\fR
With \fBexecute(...)\fR action, you can execute arbitrary commands without With \fBexecute(...)\fR action, you can execute arbitrary commands without
leaving fzf. For example, you can turn fzf into a simple file browser by leaving fzf. For example, you can turn fzf into a simple file browser by
binding \fBenter\fR key to \fBless\fR command like follows. binding \fBenter\fR key to \fBless\fR command like follows.
\fBfzf --bind "enter:execute(less {})"\fR \fBfzf --bind "enter:execute(less {})"\fR
\fB{}\fR is the placeholder for the quoted string of the current line. You can use the same placeholder expressions as in \fB--preview\fR.
If the command contains parentheses, you can use any of the following
alternative notations to avoid parse errors. If the command contains parentheses, fzf may fail to parse the expression. In
that case, you can use any of the following alternative notations to avoid
parse errors.
\fBexecute[...]\fR \fBexecute[...]\fR
\fBexecute~...~\fR \fBexecute~...~\fR
@@ -461,13 +576,15 @@ alternative notations to avoid parse errors.
.RS .RS
This is the special form that frees you from parse errors as it does not expect 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 the closing character. The catch is that it should be the last one in the
comma-separated list. comma-separated list of key-action pairs.
.RE .RE
\fBexecute-multi(...)\fR is an alternative action that executes the command fzf switches to the alternate screen when executing a command. However, if the
with the selected entries when multi-select is enabled (\fB--multi\fR). With command is expected to complete quickly, and you are not interested in its
this action, \fB{}\fR is replaced with the quoted strings of the selected output, you might want to use \fBexecute-silent\fR instead, which silently
entries separated by spaces. executes the command without the switching. Note that fzf will not be
responsible until the command is complete. For asynchronous execution, start
your command as a background process (i.e. appending \fB&\fR).
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

View File

@@ -1,4 +1,4 @@
" Copyright (c) 2016 Junegunn Choi " Copyright (c) 2017 Junegunn Choi
" "
" MIT License " MIT License
" "
@@ -21,11 +21,89 @@
" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION " OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
" 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%' if exists('g:loaded_fzf')
let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf' finish
let s:install = expand('<sfile>:h:h').'/install' endif
let g:loaded_fzf = 1
let s:is_win = has('win32') || has('win64')
if s:is_win && &shellslash
set noshellslash
let s:base_dir = expand('<sfile>:h:h')
set shellslash
else
let s:base_dir = expand('<sfile>:h:h')
endif
if s:is_win
let s:term_marker = '&::FZF'
function! s:fzf_call(fn, ...)
let shellslash = &shellslash
try
set noshellslash
return call(a:fn, a:000)
finally
let &shellslash = shellslash
endtry
endfunction
" Use utf-8 for fzf.vim commands
" Return array of shell commands for cmd.exe
function! s:wrap_cmds(cmds)
return ['@echo off', 'for /f "tokens=4" %%a in (''chcp'') do set origchcp=%%a', 'chcp 65001 > nul'] +
\ (type(a:cmds) == type([]) ? a:cmds : [a:cmds]) +
\ ['chcp %origchcp% > nul']
endfunction
else
let s:term_marker = ";#FZF"
function! s:fzf_call(fn, ...)
return call(a:fn, a:000)
endfunction
function! s:wrap_cmds(cmds)
return a:cmds
endfunction
endif
function! s:shellesc_cmd(arg)
let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g')
let escaped = substitute(escaped, '%', '%%', 'g')
let escaped = substitute(escaped, '"', '\\^&', 'g')
let escaped = substitute(escaped, '\(\\\+\)\(\\^\)', '\1\1\2', 'g')
return '^"'.substitute(escaped, '\(\\\+\)$', '\1\1', '').'^"'
endfunction
function! fzf#shellescape(arg, ...)
let shell = get(a:000, 0, &shell)
if shell =~# 'cmd.exe$'
return s:shellesc_cmd(a:arg)
endif
return s:fzf_call('shellescape', a:arg)
endfunction
function! s:fzf_getcwd()
return s:fzf_call('getcwd')
endfunction
function! s:fzf_fnamemodify(fname, mods)
return s:fzf_call('fnamemodify', a:fname, a:mods)
endfunction
function! s:fzf_expand(fmt)
return s:fzf_call('expand', a:fmt, 1)
endfunction
function! s:fzf_tempname()
return s:fzf_call('tempname')
endfunction
let s:default_layout = { 'down': '~40%' }
let s:layout_keys = ['window', 'up', 'down', 'left', 'right']
let s:fzf_go = s:base_dir.'/bin/fzf'
let s:fzf_tmux = s:base_dir.'/bin/fzf-tmux'
let s:install = s:base_dir.'/install'
let s:installed = 0 let s:installed = 0
let s:fzf_tmux = expand('<sfile>:h:h').'/bin/fzf-tmux'
let s:cpo_save = &cpo let s:cpo_save = &cpo
set cpo&vim set cpo&vim
@@ -36,6 +114,11 @@ function! s:fzf_exec()
let s:exec = s:fzf_go let s:exec = s:fzf_go
elseif executable('fzf') elseif executable('fzf')
let s:exec = 'fzf' let s:exec = 'fzf'
elseif s:is_win && !has('win32unix')
call s:warn('fzf executable not found.')
call s:warn('Download fzf binary for Windows from https://github.com/junegunn/fzf-bin/releases/')
call s:warn('and place it as '.s:base_dir.'\bin\fzf.exe')
throw 'fzf executable not found'
elseif !s:installed && executable(s:install) && elseif !s:installed && executable(s:install) &&
\ input('fzf executable not found. Download binary? (y/n) ') =~? '^y' \ input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
redraw redraw
@@ -49,7 +132,7 @@ function! s:fzf_exec()
throw 'fzf executable not found' throw 'fzf executable not found'
endif endif
endif endif
return s:shellesc(s:exec) return fzf#shellescape(s:exec)
endfunction endfunction
function! s:tmux_enabled() function! s:tmux_enabled()
@@ -69,12 +152,9 @@ function! s:tmux_enabled()
return s:tmux return s:tmux
endfunction endfunction
function! s:shellesc(arg)
return '"'.substitute(a:arg, '"', '\\"', 'g').'"'
endfunction
function! s:escape(path) function! s:escape(path)
return escape(a:path, ' $%#''"\') let path = fnameescape(a:path)
return s:is_win ? escape(path, '$') : path
endfunction endfunction
" Upgrade legacy options " Upgrade legacy options
@@ -104,54 +184,238 @@ function! s:warn(msg)
echohl None echohl None
endfunction endfunction
function! s:has_any(dict, keys)
for key in a:keys
if has_key(a:dict, key)
return 1
endif
endfor
return 0
endfunction
function! s:open(cmd, target)
if stridx('edit', a:cmd) == 0 && s:fzf_fnamemodify(a:target, ':p') ==# s:fzf_expand('%:p')
return
endif
execute a:cmd s:escape(a:target)
endfunction
function! s:common_sink(action, lines) abort
if len(a:lines) < 2
return
endif
let key = remove(a:lines, 0)
let Cmd = get(a:action, key, 'e')
if type(Cmd) == type(function('call'))
return Cmd(a:lines)
endif
if len(a:lines) > 1
augroup fzf_swap
autocmd SwapExists * let v:swapchoice='o'
\| call s:warn('fzf: E325: swap file exists: '.s:fzf_expand('<afile>'))
augroup END
endif
try
let empty = empty(s:fzf_expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified
let autochdir = &autochdir
set noautochdir
for item in a:lines
if empty
execute 'e' s:escape(item)
let empty = 0
else
call s:open(Cmd, item)
endif
if !has('patch-8.0.0177') && !has('nvim-0.2') && exists('#BufEnter')
\ && isdirectory(item)
doautocmd BufEnter
endif
endfor
finally
let &autochdir = autochdir
silent! autocmd! fzf_swap
endtry
endfunction
function! s:get_color(attr, ...)
let gui = has('termguicolors') && &termguicolors
let fam = gui ? 'gui' : 'cterm'
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
for group in a:000
let code = synIDattr(synIDtrans(hlID(group)), a:attr, fam)
if code =~? pat
return code
endif
endfor
return ''
endfunction
function! s:defaults()
let rules = copy(get(g:, 'fzf_colors', {}))
let colors = join(map(items(filter(map(rules, 'call("s:get_color", v:val)'), '!empty(v:val)')), 'join(v:val, ":")'), ',')
return empty(colors) ? '' : ('--color='.colors)
endfunction
function! s:validate_layout(layout)
for key in keys(a:layout)
if index(s:layout_keys, key) < 0
throw printf('Invalid entry in g:fzf_layout: %s (allowed: %s)%s',
\ key, join(s:layout_keys, ', '), key == 'options' ? '. Use $FZF_DEFAULT_OPTS.' : '')
endif
endfor
return a:layout
endfunction
function! s:evaluate_opts(options)
return type(a:options) == type([]) ?
\ join(map(copy(a:options), 'fzf#shellescape(v:val)')) : a:options
endfunction
" [name string,] [opts dict,] [fullscreen boolean]
function! fzf#wrap(...)
let args = ['', {}, 0]
let expects = map(copy(args), 'type(v:val)')
let tidx = 0
for arg in copy(a:000)
let tidx = index(expects, type(arg), tidx)
if tidx < 0
throw 'Invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])'
endif
let args[tidx] = arg
let tidx += 1
unlet arg
endfor
let [name, opts, bang] = args
if len(name)
let opts.name = name
end
" Layout: g:fzf_layout (and deprecated g:fzf_height)
if bang
for key in s:layout_keys
if has_key(opts, key)
call remove(opts, key)
endif
endfor
elseif !s:has_any(opts, s:layout_keys)
if !exists('g:fzf_layout') && exists('g:fzf_height')
let opts.down = g:fzf_height
else
let opts = extend(opts, s:validate_layout(get(g:, 'fzf_layout', s:default_layout)))
endif
endif
" Colors: g:fzf_colors
let opts.options = s:defaults() .' '. s:evaluate_opts(get(opts, 'options', ''))
" History: g:fzf_history_dir
if len(name) && len(get(g:, 'fzf_history_dir', ''))
let dir = s:fzf_expand(g:fzf_history_dir)
if !isdirectory(dir)
call mkdir(dir, 'p')
endif
let history = fzf#shellescape(dir.'/'.name)
let opts.options = join(['--history', history, opts.options])
endif
" Action: g:fzf_action
if !s:has_any(opts, ['sink', 'sink*'])
let opts._action = get(g:, 'fzf_action', s:default_action)
let opts.options .= ' --expect='.join(keys(opts._action), ',')
function! opts.sink(lines) abort
return s:common_sink(self._action, a:lines)
endfunction
let opts['sink*'] = remove(opts, 'sink')
endif
return opts
endfunction
function! s:use_sh()
let [shell, shellslash] = [&shell, &shellslash]
if s:is_win
set shell=cmd.exe
set noshellslash
else
set shell=sh
endif
return [shell, shellslash]
endfunction
function! fzf#run(...) abort function! fzf#run(...) abort
try try
let oshell = &shell let [shell, shellslash] = s:use_sh()
set shell=sh
if has('nvim') && bufexists('term://*:FZF')
call s:warn('FZF is already running!')
return []
endif
let dict = exists('a:1') ? s:upgrade(a:1) : {} let dict = exists('a:1') ? s:upgrade(a:1) : {}
let temps = { 'result': tempname() } let temps = { 'result': s:fzf_tempname() }
let optstr = get(dict, 'options', '') let optstr = s:evaluate_opts(get(dict, 'options', ''))
try try
let fzf_exec = s:fzf_exec() let fzf_exec = s:fzf_exec()
catch catch
throw v:exception throw v:exception
endtry endtry
if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) if has('nvim') && !has_key(dict, 'dir')
let dict.source = $FZF_DEFAULT_COMMAND let dict.dir = s:fzf_getcwd()
endif
if has('win32unix') && has_key(dict, 'dir')
let dict.dir = fnamemodify(dict.dir, ':p')
endif
if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) && !s:is_win
let temps.source = s:fzf_tempname()
call writefile(s:wrap_cmds(split($FZF_DEFAULT_COMMAND, "\n")), temps.source)
let dict.source = (empty($SHELL) ? &shell : $SHELL).' '.fzf#shellescape(temps.source)
endif endif
if has_key(dict, 'source') if has_key(dict, 'source')
let source = dict.source let source = dict.source
let type = type(source) let type = type(source)
if type == 1 if type == 1
let prefix = source.'|' let prefix = '( '.source.' )|'
elseif type == 3 elseif type == 3
let temps.input = tempname() let temps.input = s:fzf_tempname()
call writefile(source, temps.input) call writefile(source, temps.input)
let prefix = 'cat '.s:shellesc(temps.input).'|' let prefix = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input).'|'
else else
throw 'Invalid source type' throw 'Invalid source type'
endif endif
else else
let prefix = '' let prefix = ''
endif endif
let tmux = (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict)
let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if has('nvim') && !tmux let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0)
let use_height = has_key(dict, 'down') && !has('gui_running') &&
\ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right', 'window')) &&
\ executable('tput') && filereadable('/dev/tty')
let has_vim8_term = has('terminal') && has('patch-8.0.995')
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
let use_term = has_nvim_term ||
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || !use_height && s:present(dict, 'down', 'up', 'left', 'right', 'window'))
let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict)
if prefer_tmux && use_tmux
let use_height = 0
let use_term = 0
endif
if use_height
let height = s:calc_size(&lines, dict.down, dict)
let optstr .= ' --height='.height
elseif use_term
let optstr .= ' --no-height'
endif
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if use_term
return s:execute_term(dict, command, temps) return s:execute_term(dict, command, temps)
endif endif
let ret = tmux ? s:execute_tmux(dict, command, temps) : s:execute(dict, command, temps) let lines = use_tmux ? s:execute_tmux(dict, command, temps)
call s:popd(dict, ret) \ : s:execute(dict, command, use_height, temps)
return ret call s:callback(dict, lines)
return lines
finally finally
let &shell = oshell let [&shell, &shellslash] = [shell, shellslash]
endtry endtry
endfunction endfunction
@@ -170,52 +434,48 @@ function! s:fzf_tmux(dict)
if s:present(a:dict, o) if s:present(a:dict, o)
let spec = a:dict[o] let spec = a:dict[o]
if (o == 'up' || o == 'down') && spec[0] == '~' if (o == 'up' || o == 'down') && spec[0] == '~'
let size = '-'.o[0].s:calc_size(&lines, spec[1:], a:dict) let size = '-'.o[0].s:calc_size(&lines, spec, a:dict)
else else
" Legacy boolean option " Legacy boolean option
let size = '-'.o[0].(spec == 1 ? '' : spec) let size = '-'.o[0].(spec == 1 ? '' : substitute(spec, '^\~', '', ''))
endif endif
break break
endif endif
endfor endfor
return printf('LINES=%d COLUMNS=%d %s %s %s --', return printf('LINES=%d COLUMNS=%d %s %s %s --',
\ &lines, &columns, s:shellesc(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-')) \ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
endfunction endfunction
function! s:splittable(dict) function! s:splittable(dict)
return s:present(a:dict, 'up', 'down', 'left', 'right') return s:present(a:dict, 'up', 'down') && &lines > 15 ||
\ s:present(a:dict, 'left', 'right') && &columns > 40
endfunction endfunction
function! s:pushd(dict) function! s:pushd(dict)
if s:present(a:dict, 'dir') if s:present(a:dict, 'dir')
let cwd = getcwd() let cwd = s:fzf_getcwd()
if get(a:dict, 'prev_dir', '') ==# cwd if get(a:dict, 'prev_dir', '') ==# cwd
return 1 return 1
endif endif
let a:dict.prev_dir = cwd let a:dict.prev_dir = cwd
execute 'lcd' s:escape(a:dict.dir) execute 'lcd' s:escape(a:dict.dir)
let a:dict.dir = getcwd() let a:dict.dir = s:fzf_getcwd()
return 1 return 1
endif endif
return 0 return 0
endfunction endfunction
function! s:popd(dict, lines) augroup fzf_popd
" Since anything can be done in the sink function, there is no telling that autocmd!
" the change of the working directory was made by &autochdir setting. autocmd WinEnter * call s:dopopd()
" augroup END
" We use the following heuristic to determine whether to restore CWD:
" - Always restore the current directory when &autochdir is disabled. function! s:dopopd()
" FIXME This makes it impossible to change directory from inside the sink if !exists('w:fzf_dir') || s:fzf_getcwd() != w:fzf_dir[1]
" function when &autochdir is not used. return
" - In case of an error or an interrupt, a:lines will be empty.
" And it will be an array of a single empty string when fzf was finished
" without a match. In these cases, we presume that the change of the
" directory is not expected and should be undone.
if has_key(a:dict, 'prev_dir') &&
\ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0])))
execute 'lcd' s:escape(remove(a:dict, 'prev_dir'))
endif endif
execute 'lcd' s:escape(w:fzf_dir[0])
unlet w:fzf_dir
endfunction endfunction
function! s:xterm_launcher() function! s:xterm_launcher()
@@ -228,7 +488,11 @@ function! s:xterm_launcher()
\ &columns, &lines/2, getwinposx(), getwinposy()) \ &columns, &lines/2, getwinposx(), getwinposy())
endfunction endfunction
unlet! s:launcher unlet! s:launcher
let s:launcher = function('s:xterm_launcher') if s:is_win || has('win32unix')
let s:launcher = '%s'
else
let s:launcher = function('s:xterm_launcher')
endif
function! s:exit_handler(code, command, ...) function! s:exit_handler(code, command, ...)
if a:code == 130 if a:code == 130
@@ -243,39 +507,78 @@ function! s:exit_handler(code, command, ...)
return 1 return 1
endfunction endfunction
function! s:execute(dict, command, temps) abort function! s:execute(dict, command, use_height, temps) abort
call s:pushd(a:dict) call s:pushd(a:dict)
silent! !clear 2> /dev/null if has('unix') && !a:use_height
let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#') silent! !clear 2> /dev/null
endif
let escaped = (a:use_height || s:is_win) ? a:command : escape(substitute(a:command, '\n', '\\n', 'g'), '%#!')
if has('gui_running') if has('gui_running')
let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher))) let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher)))
let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher
let command = printf(fmt, "'".substitute(escaped, "'", "'\"'\"'", 'g')."'") if has('unix')
let escaped = "'".substitute(escaped, "'", "'\"'\"'", 'g')."'"
endif
let command = printf(fmt, escaped)
else else
let command = escaped let command = escaped
endif endif
execute 'silent !'.command if s:is_win
let batchfile = s:fzf_tempname().'.bat'
call writefile(s:wrap_cmds(command), batchfile)
let command = batchfile
let a:temps.batchfile = batchfile
if has('nvim')
let fzf = {}
let fzf.dict = a:dict
let fzf.temps = a:temps
function! fzf.on_exit(job_id, exit_status, event) dict
if s:present(self.dict, 'dir')
execute 'lcd' s:escape(self.dict.dir)
endif
let lines = s:collect(self.temps)
call s:callback(self.dict, lines)
endfunction
let cmd = 'start /wait cmd /c '.command
call jobstart(cmd, fzf)
return []
endif
elseif has('win32unix') && $TERM !=# 'cygwin'
let shellscript = s:fzf_tempname()
call writefile([command], shellscript)
let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript)
let a:temps.shellscript = shellscript
endif
if a:use_height
let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty'
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin))
else
execute 'silent !'.command
endif
let exit_status = v:shell_error
redraw! redraw!
return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : [] return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
endfunction endfunction
function! s:execute_tmux(dict, command, temps) abort function! s:execute_tmux(dict, command, temps) abort
let command = a:command let command = a:command
if s:pushd(a:dict) if s:pushd(a:dict)
" -c '#{pane_current_path}' is only available on tmux 1.9 or above " -c '#{pane_current_path}' is only available on tmux 1.9 or above
let command = 'cd '.s:escape(a:dict.dir).' && '.command let command = join(['cd', fzf#shellescape(a:dict.dir), '&&', command])
endif endif
call system(command) call system(command)
let exit_status = v:shell_error
redraw! redraw!
return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : [] return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
endfunction endfunction
function! s:calc_size(max, val, dict) function! s:calc_size(max, val, dict)
if a:val =~ '%$' let val = substitute(a:val, '^\~', '', '')
let size = a:max * str2nr(a:val[:-2]) / 100 if val =~ '%$'
let size = a:max * str2nr(val[:-2]) / 100
else else
let size = min([a:max, str2nr(a:val)]) let size = min([a:max, str2nr(val)])
endif endif
let srcsz = -1 let srcsz = -1
@@ -285,6 +588,7 @@ function! s:calc_size(max, val, dict)
let opts = get(a:dict, 'options', '').$FZF_DEFAULT_OPTS let opts = get(a:dict, 'options', '').$FZF_DEFAULT_OPTS
let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2 let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2
let margin += stridx(opts, '--header') > stridx(opts, '--no-header')
return srcsz >= 0 ? min([srcsz + margin, size]) : size return srcsz >= 0 ? min([srcsz + margin, size]) : size
endfunction endfunction
@@ -300,24 +604,25 @@ function! s:split(dict)
\ 'right': ['vertical botright', 'vertical resize', &columns] } \ 'right': ['vertical botright', 'vertical resize', &columns] }
let ppos = s:getpos() let ppos = s:getpos()
try try
for [dir, triple] in items(directions)
let val = get(a:dict, dir, '')
if !empty(val)
let [cmd, resz, max] = triple
if (dir == 'up' || dir == 'down') && val[0] == '~'
let sz = s:calc_size(max, val[1:], a:dict)
else
let sz = s:calc_size(max, val, {})
endif
execute cmd sz.'new'
execute resz sz
return [ppos, {}]
endif
endfor
if s:present(a:dict, 'window') if s:present(a:dict, 'window')
execute a:dict.window execute 'keepalt' a:dict.window
else elseif !s:splittable(a:dict)
execute (tabpagenr()-1).'tabnew' execute (tabpagenr()-1).'tabnew'
else
for [dir, triple] in items(directions)
let val = get(a:dict, dir, '')
if !empty(val)
let [cmd, resz, max] = triple
if (dir == 'up' || dir == 'down') && val[0] == '~'
let sz = s:calc_size(max, val, a:dict)
else
let sz = s:calc_size(max, val, {})
endif
execute cmd sz.'new'
execute resz sz
return [ppos, {}]
endif
endfor
endif endif
return [ppos, { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh }] return [ppos, { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh }]
finally finally
@@ -326,21 +631,26 @@ function! s:split(dict)
endfunction endfunction
function! s:execute_term(dict, command, temps) abort function! s:execute_term(dict, command, temps) abort
let winrest = winrestcmd()
let pbuf = bufnr('')
let [ppos, winopts] = s:split(a:dict) let [ppos, winopts] = s:split(a:dict)
let fzf = { 'buf': bufnr('%'), 'ppos': ppos, 'dict': a:dict, 'temps': a:temps, call s:use_sh()
\ 'name': 'FZF', 'winopts': winopts, 'command': a:command } let b:fzf = a:dict
let fzf = { 'buf': bufnr(''), 'pbuf': pbuf, 'ppos': ppos, 'dict': a:dict, 'temps': a:temps,
\ 'winopts': winopts, 'winrest': winrest, 'lines': &lines,
\ 'columns': &columns, 'command': a:command }
function! fzf.switch_back(inplace) function! fzf.switch_back(inplace)
if a:inplace && bufnr('') == self.buf if a:inplace && bufnr('') == self.buf
" FIXME: Can't re-enter normal mode from terminal mode if bufexists(self.pbuf)
" execute "normal! \<c-^>" execute 'keepalt b' self.pbuf
b # endif
" No other listed buffer " No other listed buffer
if bufnr('') == self.buf if bufnr('') == self.buf
enew enew
endif endif
endif endif
endfunction endfunction
function! fzf.on_exit(id, code) function! fzf.on_exit(id, code, ...)
if s:getpos() == self.ppos " {'window': 'enew'} if s:getpos() == self.ppos " {'window': 'enew'}
for [opt, val] in items(self.winopts) for [opt, val] in items(self.winopts)
execute 'let' opt '=' val execute 'let' opt '=' val
@@ -356,36 +666,86 @@ function! s:execute_term(dict, command, temps) abort
execute self.ppos.win.'wincmd w' execute self.ppos.win.'wincmd w'
endif endif
if bufexists(self.buf)
execute 'bd!' self.buf
endif
if &lines == self.lines && &columns == self.columns && s:getpos() == self.ppos
execute self.winrest
endif
if !s:exit_handler(a:code, self.command, 1) if !s:exit_handler(a:code, self.command, 1)
return return
endif endif
call s:pushd(self.dict) call s:pushd(self.dict)
let ret = [] let lines = s:collect(self.temps)
try call s:callback(self.dict, lines)
let ret = s:callback(self.dict, self.temps) call self.switch_back(s:getpos() == self.ppos)
call self.switch_back(s:getpos() == self.ppos)
finally
call s:popd(self.dict, ret)
endtry
endfunction endfunction
call s:pushd(a:dict) try
call termopen(a:command, fzf) if s:present(a:dict, 'dir')
call s:popd(a:dict, []) execute 'lcd' s:escape(a:dict.dir)
setlocal nospell bufhidden=wipe nobuflisted endif
if s:is_win
let fzf.temps.batchfile = s:fzf_tempname().'.bat'
call writefile(s:wrap_cmds(a:command), fzf.temps.batchfile)
let command = fzf.temps.batchfile
else
let command = a:command
endif
let command .= s:term_marker
if has('nvim')
call termopen(command, fzf)
else
let t = term_start([&shell, &shellcmdflag, command], {'curwin': fzf.buf, 'exit_cb': function(fzf.on_exit)})
if !has('patch-8.0.1261') && !has('nvim') && !s:is_win
call term_wait(t, 20)
endif
endif
finally
if s:present(a:dict, 'dir')
lcd -
endif
endtry
setlocal nospell bufhidden=wipe nobuflisted nonumber
setf fzf setf fzf
startinsert startinsert
return [] return []
endfunction endfunction
function! s:callback(dict, temps) abort function! s:collect(temps) abort
let lines = [] try
try return filereadable(a:temps.result) ? readfile(a:temps.result) : []
if filereadable(a:temps.result) finally
let lines = readfile(a:temps.result) for tf in values(a:temps)
silent! call delete(tf)
endfor
endtry
endfunction
function! s:callback(dict, lines) abort
" Since anything can be done in the sink function, there is no telling that
" the change of the working directory was made by &autochdir setting.
"
" We use the following heuristic to determine whether to restore CWD:
" - Always restore the current directory when &autochdir is disabled.
" FIXME This makes it impossible to change directory from inside the sink
" function when &autochdir is not used.
" - In case of an error or an interrupt, a:lines will be empty.
" And it will be an array of a single empty string when fzf was finished
" without a match. In these cases, we presume that the change of the
" directory is not expected and should be undone.
let popd = has_key(a:dict, 'prev_dir') &&
\ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0])))
if popd
let w:fzf_dir = [a:dict.prev_dir, a:dict.dir]
endif
try
if has_key(a:dict, 'sink') if has_key(a:dict, 'sink')
for line in lines for line in a:lines
if type(a:dict.sink) == 2 if type(a:dict.sink) == 2
call a:dict.sink(line) call a:dict.sink(line)
else else
@@ -394,76 +754,54 @@ try
endfor endfor
endif endif
if has_key(a:dict, 'sink*') if has_key(a:dict, 'sink*')
call a:dict['sink*'](lines) call a:dict['sink*'](a:lines)
endif endif
endif catch
if stridx(v:exception, ':E325:') < 0
echoerr v:exception
endif
endtry
for tf in values(a:temps) " We may have opened a new window or tab
silent! call delete(tf) if popd
endfor let w:fzf_dir = [a:dict.prev_dir, a:dict.dir]
catch call s:dopopd()
if stridx(v:exception, ':E325:') < 0
echoerr v:exception
endif endif
finally
return lines
endtry
endfunction endfunction
let s:default_action = { let s:default_action = {
\ 'ctrl-m': 'e',
\ 'ctrl-t': 'tab split', \ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split', \ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' } \ 'ctrl-v': 'vsplit' }
function! s:cmd_callback(lines) abort function! s:shortpath()
if empty(a:lines) let short = fnamemodify(getcwd(), ':~:.')
return if !has('win32unix')
let short = pathshorten(short)
endif endif
let key = remove(a:lines, 0) let slash = (s:is_win && !&shellslash) ? '\' : '/'
let cmd = get(s:action, key, 'e') return empty(short) ? '~'.slash : short . (short =~ escape(slash, '\').'$' ? '' : slash)
if len(a:lines) > 1
augroup fzf_swap
autocmd SwapExists * let v:swapchoice='o'
\| call s:warn('fzf: E325: swap file exists: '.expand('<afile>'))
augroup END
endif
try
let empty = empty(expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified
let autochdir = &autochdir
set noautochdir
for item in a:lines
if empty
execute 'e' s:escape(item)
let empty = 0
else
execute cmd s:escape(item)
endif
if exists('#BufEnter') && isdirectory(item)
doautocmd BufEnter
endif
endfor
finally
let &autochdir = autochdir
silent! autocmd! fzf_swap
endtry
endfunction endfunction
function! s:cmd(bang, ...) abort function! s:cmd(bang, ...) abort
let s:action = get(g:, 'fzf_action', s:default_action) let args = copy(a:000)
let args = extend(['--expect='.join(keys(s:action), ',')], a:000) let opts = { 'options': ['--multi'] }
let opts = {} if len(args) && isdirectory(expand(args[-1]))
if len(args) > 0 && isdirectory(expand(args[-1])) let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g') if s:is_win && !&shellslash
let opts.dir = substitute(opts.dir, '/', '\\', 'g')
endif
let prompt = opts.dir
else
let prompt = s:shortpath()
endif endif
if !a:bang let prompt = strwidth(prompt) < &columns - 20 ? prompt : '> '
let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height)) call extend(opts.options, ['--prompt', prompt])
endif call extend(opts.options, args)
call fzf#run(extend({'options': join(args), 'sink*': function('<sid>cmd_callback')}, opts)) call fzf#run(fzf#wrap('FZF', opts, a:bang))
endfunction endfunction
command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>) command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>)
let &cpo = s:cpo_save let &cpo = s:cpo_save
unlet s:cpo_save unlet s:cpo_save

View File

@@ -5,7 +5,7 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/-completion.bash # /_/ /___/_/-completion.bash
# #
# - $FZF_TMUX (default: 1) # - $FZF_TMUX (default: 0)
# - $FZF_TMUX_HEIGHT (default: '40%') # - $FZF_TMUX_HEIGHT (default: '40%')
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
@@ -14,7 +14,7 @@
if ! declare -f _fzf_compgen_path > /dev/null; then if ! declare -f _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() { _fzf_compgen_path() {
echo "$1" echo "$1"
\find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ -name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
@@ -22,7 +22,7 @@ fi
if ! declare -f _fzf_compgen_dir > /dev/null; then if ! declare -f _fzf_compgen_dir > /dev/null; then
_fzf_compgen_dir() { _fzf_compgen_dir() {
\find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .svn -prune -o -type d \ -name .git -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
@@ -30,9 +30,17 @@ fi
########################################################### ###########################################################
# To redraw line after fzf closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line'
__fzfcmd_complete() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
}
_fzf_orig_completion_filter() { _fzf_orig_completion_filter() {
sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' | sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' |
awk -F= '{gsub(/[^a-z0-9_= ;]/, "_", $1); print $1"="$2}' awk -F= '{gsub(/[^A-Za-z0-9_= ;]/, "_", $1); print $1"="$2}'
} }
_fzf_opts_completion() { _fzf_opts_completion() {
@@ -43,35 +51,43 @@ _fzf_opts_completion() {
opts=" opts="
-x --extended -x --extended
-e --exact -e --exact
--algo
-i +i -i +i
-n --nth -n --nth
--with-nth
-d --delimiter -d --delimiter
+s --no-sort +s --no-sort
--tac --tac
--tiebreak --tiebreak
--bind
-m --multi -m --multi
--no-mouse --no-mouse
--color --bind
--black --cycle
--reverse
--no-hscroll --no-hscroll
--jump-labels
--height
--literal
--reverse
--margin
--inline-info --inline-info
--prompt --prompt
--header
--header-lines
--ansi
--tabstop
--color
--no-bold
--history
--history-size
--preview
--preview-window
-q --query -q --query
-1 --select-1 -1 --select-1
-0 --exit-0 -0 --exit-0
-f --filter -f --filter
--print-query --print-query
--expect --expect
--toggle-sort --sync"
--sync
--cycle
--history
--history-size
--header
--header-lines
--margin"
case "${prev}" in case "${prev}" in
--tiebreak) --tiebreak)
@@ -97,7 +113,7 @@ _fzf_opts_completion() {
} }
_fzf_handle_dynamic_completion() { _fzf_handle_dynamic_completion() {
local cmd orig_var orig ret orig_cmd local cmd orig_var orig ret orig_cmd orig_complete
cmd="$1" cmd="$1"
shift shift
orig_cmd="$1" orig_cmd="$1"
@@ -106,18 +122,22 @@ _fzf_handle_dynamic_completion() {
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
$orig "$@" $orig "$@"
elif [ -n "$_fzf_completion_loader" ]; then elif [ -n "$_fzf_completion_loader" ]; then
orig_complete=$(complete -p "$cmd")
_completion_loader "$@" _completion_loader "$@"
ret=$? ret=$?
eval "$(complete | \grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)" # _completion_loader may not have updated completion for the command
source "${BASH_SOURCE[0]}" if [ "$(complete -p "$cmd")" != "$orig_complete" ]; then
eval "$(complete | command grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)"
eval "$orig_complete"
fi
return $ret return $ret
fi fi
} }
__fzf_generic_path_completion() { __fzf_generic_path_completion() {
local cur base dir leftover matches trigger cmd fzf local cur base dir leftover matches trigger cmd fzf
[ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" fzf="$(__fzfcmd_complete)"
cmd=$(echo "${COMP_WORDS[0]}" | sed 's/[^a-z0-9_=]/_/g') cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
COMPREPLY=() COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
@@ -132,8 +152,7 @@ __fzf_generic_path_completion() {
leftover=${leftover/#\/} leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.' [ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [ "$dir" != "/" ] && dir="${dir/%\//}"
tput sc matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $2 -q "$leftover" | while read -r item; do
matches=$(eval "$1 $(printf %q "$dir")" | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read -r item; do
printf "%q$3 " "$item" printf "%q$3 " "$item"
done) done)
matches=${matches% } matches=${matches% }
@@ -142,7 +161,7 @@ __fzf_generic_path_completion() {
else else
COMPREPLY=( "$cur" ) COMPREPLY=( "$cur" )
fi fi
tput rc printf '\e[5n'
return 0 return 0
fi fi
dir=$(dirname "$dir") dir=$(dirname "$dir")
@@ -160,18 +179,17 @@ _fzf_complete() {
local cur selected trigger cmd fzf post local cur selected trigger cmd fzf post
post="$(caller 0 | awk '{print $2}')_post" post="$(caller 0 | awk '{print $2}')_post"
type -t "$post" > /dev/null 2>&1 || post=cat type -t "$post" > /dev/null 2>&1 || post=cat
[ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" fzf="$(__fzfcmd_complete)"
cmd=$(echo "${COMP_WORDS[0]}" | sed 's/[^a-z0-9_=]/_/g') cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
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}}
tput sc selected=$(cat | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ')
selected=$(cat | $fzf $FZF_COMPLETION_OPTS $1 -q "$cur" | $post | tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace" selected=${selected% } # Strip trailing space not to repeat "-o nospace"
tput rc printf '\e[5n'
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
COMPREPLY=("$selected") COMPREPLY=("$selected")
@@ -200,10 +218,9 @@ _fzf_complete_kill() {
[ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1 [ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1
local selected fzf local selected fzf
[ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" fzf="$(__fzfcmd_complete)"
tput sc selected=$(command ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ')
selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ') printf '\e[5n'
tput rc
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
COMPREPLY=( "$selected" ) COMPREPLY=( "$selected" )
@@ -213,16 +230,16 @@ _fzf_complete_kill() {
_fzf_complete_telnet() { _fzf_complete_telnet() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
awk '{if (length($2) > 0) {print $2}}' | sort -u awk '{if (length($2) > 0) {print $2}}' | sort -u
) )
} }
_fzf_complete_ssh() { _fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \ cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}') \
<(\grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u awk '{if (length($2) > 0) {print $2}}' | sort -u
) )
} }
@@ -261,12 +278,9 @@ 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.11.3' ]; then eval $(complete |
# Really wish I could use associative array but OSX comes with bash 3.2 :( sed -E '/-F/!d; / _fzf/d; '"/ ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g; s/+/\\+/g'))$/"'!d' |
eval $(complete | \grep '\-F' | \grep -v _fzf_ | _fzf_orig_completion_filter)
\grep -E " ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter)
export _fzf_completion_loaded=0.11.3
fi
if type _completion_loader > /dev/null 2>&1; then if type _completion_loader > /dev/null 2>&1; then
_fzf_completion_loader=1 _fzf_completion_loader=1
@@ -277,7 +291,7 @@ _fzf_defc() {
cmd="$1" cmd="$1"
func="$2" func="$2"
opts="$3" opts="$3"
orig_var="_fzf_orig_completion_$cmd" orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
orig="${!orig_var}" orig="${!orig_var}"
if [ -n "$orig" ]; then if [ -n "$orig" ]; then
printf -v def "$orig" "$func" printf -v def "$orig" "$func"
@@ -294,7 +308,7 @@ done
# Directory # Directory
for cmd in $d_cmds; do for cmd in $d_cmds; do
_fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o plusdirs" _fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
done done
unset _fzf_defc unset _fzf_defc

View File

@@ -5,7 +5,7 @@
# / __/ / /_/ __/ # / __/ / /_/ __/
# /_/ /___/_/-completion.zsh # /_/ /___/_/-completion.zsh
# #
# - $FZF_TMUX (default: 1) # - $FZF_TMUX (default: 0)
# - $FZF_TMUX_HEIGHT (default: '40%') # - $FZF_TMUX_HEIGHT (default: '40%')
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
@@ -14,7 +14,7 @@
if ! declare -f _fzf_compgen_path > /dev/null; then if ! declare -f _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() { _fzf_compgen_path() {
echo "$1" echo "$1"
\find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ -name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
@@ -22,7 +22,7 @@ fi
if ! declare -f _fzf_compgen_dir > /dev/null; then if ! declare -f _fzf_compgen_dir > /dev/null; then
_fzf_compgen_dir() { _fzf_compgen_dir() {
\find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .svn -prune -o -type d \ -name .git -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
@@ -30,6 +30,11 @@ fi
########################################################### ###########################################################
__fzfcmd_complete() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
}
__fzf_generic_path_completion() { __fzf_generic_path_completion() {
local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches
# (Q) flag removes a quoting level: "foo\ bar" => "foo bar" # (Q) flag removes a quoting level: "foo\ bar" => "foo bar"
@@ -39,18 +44,18 @@ __fzf_generic_path_completion() {
fzf_opts=$4 fzf_opts=$4
suffix=$5 suffix=$5
tail=$6 tail=$6
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" fzf="$(__fzfcmd_complete)"
setopt localoptions nonomatch setopt localoptions nonomatch
dir="$base" dir="$base"
while [ 1 ]; do while [ 1 ]; do
if [ -z "$dir" -o -d ${~dir} ]; then if [[ -z "$dir" || -d ${~dir} ]]; then
leftover=${base/#"$dir"} leftover=${base/#"$dir"}
leftover=${leftover/#\/} leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.' [ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [ "$dir" != "/" ] && dir="${dir/%\//}"
dir=${~dir} dir=${~dir}
matches=$(eval "$compgen $(printf %q "$dir")" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "$leftover" | while read item; do
echo -n "${(q)item}$suffix " echo -n "${(q)item}$suffix "
done) done)
matches=${matches% } matches=${matches% }
@@ -58,6 +63,7 @@ __fzf_generic_path_completion() {
LBUFFER="$lbuf$matches$tail" LBUFFER="$lbuf$matches$tail"
fi fi
zle redisplay zle redisplay
typeset -f zle-line-init >/dev/null && zle zle-line-init
break break
fi fi
dir=$(dirname "$dir") dir=$(dirname "$dir")
@@ -76,7 +82,7 @@ _fzf_dir_completion() {
} }
_fzf_feed_fifo() ( _fzf_feed_fifo() (
rm -f "$1" command rm -f "$1"
mkfifo "$1" mkfifo "$1"
cat <&0 > "$1" & cat <&0 > "$1" &
) )
@@ -89,29 +95,30 @@ _fzf_complete() {
post="${funcstack[2]}_post" post="${funcstack[2]}_post"
type $post > /dev/null 2>&1 || post=cat type $post > /dev/null 2>&1 || post=cat
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" fzf="$(__fzfcmd_complete)"
_fzf_feed_fifo "$fifo" _fzf_feed_fifo "$fifo"
matches=$(cat "$fifo" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') matches=$(cat "$fifo" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ')
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches" LBUFFER="$lbuf$matches"
fi fi
zle redisplay zle redisplay
rm -f "$fifo" typeset -f zle-line-init >/dev/null && zle zle-line-init
command rm -f "$fifo"
} }
_fzf_complete_telnet() { _fzf_complete_telnet() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
awk '{if (length($2) > 0) {print $2}}' | sort -u awk '{if (length($2) > 0) {print $2}}' | sort -u
) )
} }
_fzf_complete_ssh() { _fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \ command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}') \
<(\grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
<(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u awk '{if (length($2) > 0) {print $2}}' | sort -u
) )
} }
@@ -136,7 +143,7 @@ _fzf_complete_unalias() {
fzf-completion() { fzf-completion() {
local tokens cmd prefix trigger tail fzf matches lbuf d_cmds local tokens cmd prefix trigger tail fzf matches lbuf d_cmds
setopt localoptions noshwordsplit setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
# http://zsh.sourceforge.net/FAQ/zshfaq03.html # http://zsh.sourceforge.net/FAQ/zshfaq03.html
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
@@ -155,12 +162,13 @@ fzf-completion() {
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
# Kill completion (do not require trigger sequence) # Kill completion (do not require trigger sequence)
if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" fzf="$(__fzfcmd_complete)"
matches=$(ps -ef | sed 1d | ${=fzf} ${=FZF_COMPLETION_OPTS} -m | awk '{print $2}' | tr '\n' ' ') matches=$(command ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ')
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
LBUFFER="$LBUFFER$matches" LBUFFER="$LBUFFER$matches"
fi fi
zle redisplay zle redisplay
typeset -f zle-line-init >/dev/null && zle zle-line-init
# Trigger sequence given # Trigger sequence given
elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}) d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
@@ -181,8 +189,11 @@ fzf-completion() {
fi fi
} }
[ -z "$fzf_default_completion" ] && [ -z "$fzf_default_completion" ] && {
fzf_default_completion=$(bindkey '^I' | \grep -v undefined-key | awk '{print $2}') binding=$(bindkey '^I')
[[ $binding =~ 'undefined-key' ]] || fzf_default_completion=$binding[(s: :w)2]
unset binding
}
zle -N fzf-completion zle -N fzf-completion
bindkey '^I' fzf-completion bindkey '^I' fzf-completion

View File

@@ -1,11 +1,11 @@
# Key bindings # Key bindings
# ------------ # ------------
__fzf_select__() { __fzf_select__() {
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" -o -type l -print 2> /dev/null | cut -b3-"}"
eval "$cmd" | fzf -m | while read -r item; do eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" fzf -m "$@" | while read -r item; do
printf '%q ' "$item" printf '%q ' "$item"
done done
echo echo
@@ -13,8 +13,13 @@ __fzf_select__() {
if [[ $- =~ i ]]; then if [[ $- =~ i ]]; then
__fzf_use_tmux__() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
}
__fzfcmd() { __fzfcmd() {
[ "${FZF_TMUX:-1}" != 0 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" __fzf_use_tmux__ &&
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
} }
__fzf_select_tmux__() { __fzf_select_tmux__() {
@@ -26,7 +31,7 @@ __fzf_select_tmux__() {
height="-l $height" height="-l $height"
fi fi
tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") bash -c 'source \"${BASH_SOURCE[0]}\"; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__ --no-height)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'"
} }
fzf-file-widget() { fzf-file-widget() {
@@ -41,9 +46,9 @@ fzf-file-widget() {
__fzf_cd__() { __fzf_cd__() {
local cmd dir local cmd dir
cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" -o -type d -print 2> /dev/null | cut -b3-"}"
dir=$(eval "$cmd" | $(__fzfcmd) +m) && printf 'cd %q' "$dir" dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir"
} }
__fzf_history__() ( __fzf_history__() (
@@ -51,8 +56,8 @@ __fzf_history__() (
shopt -u nocaseglob nocasematch shopt -u nocaseglob nocasematch
line=$( line=$(
HISTTIMEFORMAT= history | HISTTIMEFORMAT= history |
eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" $(__fzfcmd) |
\grep '^ *[0-9]') && command grep '^ *[0-9]') &&
if [[ $- =~ H ]]; then if [[ $- =~ H ]]; then
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
else else
@@ -60,25 +65,18 @@ __fzf_history__() (
fi fi
) )
__fzf_use_tmux__() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-1}" != 0 ] && [ ${LINES:-40} -gt 15 ]
}
[ $BASH_VERSINFO -gt 3 ] && __use_bind_x=1 || __use_bind_x=0
__fzf_use_tmux__ && __use_tmux=1 || __use_tmux=0
if [[ ! -o vi ]]; then if [[ ! -o vi ]]; 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' 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_bind_x -eq 1 ]; then if [ $BASH_VERSINFO -gt 3 ]; then
bind -x '"\C-t": "fzf-file-widget"' bind -x '"\C-t": "fzf-file-widget"'
elif [ $__use_tmux -eq 1 ]; then elif __fzf_use_tmux__; then
bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select_tmux__)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"' 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$(__fzf_select__)\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
@@ -102,24 +100,22 @@ else
# 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_bind_x -eq 1 ]; then if [ $BASH_VERSINFO -gt 3 ]; then
bind -x '"\C-t": "fzf-file-widget"' bind -x '"\C-t": "fzf-file-widget"'
elif [ $__use_tmux -eq 1 ]; then elif __fzf_use_tmux__; then
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi$(__fzf_select_tmux__)\C-x\C-e\C-x\C-a0P$xa"' bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select_tmux__`\C-x\C-e\C-x\C-a0P$xa"'
else else
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi$(__fzf_select__)\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "' bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select__`\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "'
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": "\C-x\C-addi$(__fzf_history__)\C-x\C-e\C-x^\C-x\C-a$a\C-x\C-r"' bind '"\C-r": "\C-x\C-addi`__fzf_history__`\C-x\C-e\C-x^\C-x\C-a$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": "\C-x\C-addi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"' bind '"\ec": "\C-x\C-addi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
bind -m vi-command '"\ec": "ddi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"' bind -m vi-command '"\ec": "ddi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
fi fi
unset -v __use_tmux __use_bind_x
fi fi

View File

@@ -1,58 +1,91 @@
# Key bindings # Key bindings
# ------------ # ------------
function fzf_key_bindings function fzf_key_bindings
# Due to a bug of fish, we cannot use command substitution,
# so we use temporary file instead
if [ -z "$TMPDIR" ]
set -g TMPDIR /tmp
end
function __fzf_escape # Store current token in $dir as root for the 'find' command
while read item function fzf-file-widget -d "List files and folders"
echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' ' set -l commandline (__fzf_parse_commandline)
end set -l dir $commandline[1]
end set -l fzf_query $commandline[2]
function fzf-file-widget # "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
# $dir itself, even if hidden.
set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND " set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND "
command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -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-" -o -type l -print 2> /dev/null | sed 's@^\./@@'"
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m > $TMPDIR/fzf.result"
and for i in (seq 20); commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) 2> /dev/null; and break; sleep 0.1; end set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
end
if [ -z "$result" ]
commandline -f repaint
return
else
# Remove last token from commandline.
commandline -t ""
end
for i in $result
commandline -it -- (string escape $i)
commandline -it -- ' '
end
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result
end end
function fzf-history-widget function fzf-history-widget -d "Show command history"
history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS > $TMPDIR/fzf.result set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
and commandline (cat $TMPDIR/fzf.result) begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m"
set -l FISH_MAJOR (echo $FISH_VERSION | cut -f1 -d.)
set -l FISH_MINOR (echo $FISH_VERSION | cut -f2 -d.)
# history's -z flag is needed for multi-line support.
# history's -z flag was added in fish 2.4.0, so don't use it for versions
# before 2.4.0.
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
history -z | eval (__fzfcmd) --read0 -q '(commandline)' | perl -pe 'chomp if eof' | read -lz result
and commandline -- $result
else
history | eval (__fzfcmd) -q '(commandline)' | read -l result
and commandline -- $result
end
end
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result
end end
function fzf-cd-widget function fzf-cd-widget -d "Change directory"
set -l commandline (__fzf_parse_commandline)
set -l dir $commandline[1]
set -l fzf_query $commandline[2]
set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND " set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND "
command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3-" -o -type d -print 2> /dev/null | sed 's@^\./@@'"
# Fish hangs if the command before pipe redirects (2> /dev/null) set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m > $TMPDIR/fzf.result" begin
[ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ] set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
and cd (cat $TMPDIR/fzf.result) eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
if [ -n "$result" ]
cd $result
# Remove last token from commandline.
commandline -t ""
end
end
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result
end end
function __fzfcmd function __fzfcmd
set -q FZF_TMUX; or set FZF_TMUX 1 set -q FZF_TMUX; or set FZF_TMUX 0
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
if [ $FZF_TMUX -eq 1 ] if [ $FZF_TMUX -eq 1 ]
if set -q FZF_TMUX_HEIGHT echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
else
echo "fzf-tmux -d40%"
end
else else
echo "fzf" echo "fzf"
end end
@@ -67,5 +100,47 @@ function fzf_key_bindings
bind -M insert \cr fzf-history-widget bind -M insert \cr fzf-history-widget
bind -M insert \ec fzf-cd-widget bind -M insert \ec fzf-cd-widget
end end
end
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath and rest of token'
# eval is used to do shell expansion on paths
set -l commandline (eval "printf '%s' "(commandline -t))
if [ -z $commandline ]
# Default to current directory with no --query
set dir '.'
set fzf_query ''
else
set dir (__fzf_get_dir $commandline)
if [ "$dir" = "." -a (string sub -l 1 $commandline) != '.' ]
# if $dir is "." but commandline is not a relative path, this means no file path found
set fzf_query $commandline
else
# Also remove trailing slash after dir, to "split" input properly
set fzf_query (string replace -r "^$dir/?" '' "$commandline")
end
end
echo $dir
echo $fzf_query
end
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
set dir $argv
# Strip all trailing slashes. Ignore if $dir is root dir (/)
if [ (string length $dir) -gt 1 ]
set dir (string replace -r '/*$' '' $dir)
end
# Iteratively check if dir exists and strip tail end of path
while [ ! -d "$dir" ]
# If path is absolute, this can keep going until ends up at /
# If path is relative, this can keep going until entire input is consumed, dirname returns "."
set dir (dirname "$dir")
end
echo $dir
end
end

View File

@@ -4,33 +4,63 @@ 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() {
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" -o -type l -print 2> /dev/null | cut -b3-"}"
eval "$cmd" | $(__fzfcmd) -m | while read item; do setopt localoptions pipefail 2> /dev/null
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do
echo -n "${(q)item} " echo -n "${(q)item} "
done done
local ret=$?
echo echo
return $ret
}
__fzf_use_tmux__() {
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
} }
__fzfcmd() { __fzfcmd() {
[ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" __fzf_use_tmux__ &&
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
} }
fzf-file-widget() { fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)" LBUFFER="${LBUFFER}$(__fsel)"
local ret=$?
zle redisplay zle redisplay
typeset -f zle-line-init >/dev/null && zle zle-line-init
return $ret
} }
zle -N fzf-file-widget zle -N fzf-file-widget
bindkey '^T' fzf-file-widget bindkey '^T' fzf-file-widget
# Ensure precmds are run after cd
fzf-redraw-prompt() {
local precmd
for precmd in $precmd_functions; do
$precmd
done
zle reset-prompt
}
zle -N fzf-redraw-prompt
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" -o -type d -print 2> /dev/null | cut -b3-"}"
cd "${$(eval "$cmd" | $(__fzfcmd) +m):-.}" setopt localoptions pipefail 2> /dev/null
zle reset-prompt local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)"
if [[ -z "$dir" ]]; then
zle redisplay
return 0
fi
cd "$dir"
local ret=$?
zle fzf-redraw-prompt
typeset -f zle-line-init >/dev/null && zle zle-line-init
return $ret
} }
zle -N fzf-cd-widget zle -N fzf-cd-widget
bindkey '\ec' fzf-cd-widget bindkey '\ec' fzf-cd-widget
@@ -38,8 +68,10 @@ 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 num local selected num
setopt localoptions noglobsubst setopt localoptions noglobsubst noposixbuiltins pipefail 2> /dev/null
selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") ) selected=( $(fc -rl 1 |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )
local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$selected[1] num=$selected[1]
if [ -n "$num" ]; then if [ -n "$num" ]; then
@@ -47,9 +79,10 @@ fzf-history-widget() {
fi fi
fi fi
zle redisplay zle redisplay
typeset -f zle-line-init >/dev/null && zle zle-line-init
return $ret
} }
zle -N fzf-history-widget zle -N fzf-history-widget
bindkey '^R' fzf-history-widget bindkey '^R' fzf-history-widget
fi fi

View File

@@ -1,40 +0,0 @@
FROM ubuntu:14.04
MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# apt-get
RUN apt-get update && apt-get -y upgrade && \
apt-get install -y --force-yes git curl build-essential
# Install Go 1.4
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4 && \
sed -i 's@#define PTHREAD_KEYS_MAX 128@@' /go1.4/src/runtime/cgo/gcc_android_arm.c
ENV GOROOT /go1.4
ENV PATH /go1.4/bin:$PATH
RUN cd / && \
curl -O http://dl.google.com/android/ndk/android-ndk-r10e-linux-x86_64.bin && \
chmod 755 /android-ndk* && /android-ndk-r10e-linux-x86_64.bin && \
mv android-ndk-r10e /android-ndk
RUN cd /android-ndk && bash ./build/tools/make-standalone-toolchain.sh --platform=android-21 --install-dir=/ndk --arch=arm
ENV NDK_CC /ndk/bin/arm-linux-androideabi-gcc
RUN cd $GOROOT/src && \
CC_FOR_TARGET=$NDK_CC GOOS=android GOARCH=arm GOARM=7 ./make.bash
RUN cd / && curl \
http://ftp.gnu.org/gnu/ncurses/ncurses-5.9.tar.gz | \
tar -xz && cd /ncurses-5.9 && \
./configure CC=$NDK_CC CFLAGS="-fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch" LDFLAGS="-march=armv7-a -Wl,--no-warn-mismatch" --host=arm-linux --enable-overwrite --enable-const --without-cxx-binding --without-shared --without-debug --enable-widec --enable-ext-colors --enable-ext-mouse --enable-pc-files --with-pkg-config-libdir=$PKG_CONFIG_LIBDIR --without-manpages --without-ada --disable-shared --without-tests --prefix=/ndk/sysroot/usr --with-default-terminfo-dirs=/usr/share/terminfo --with-terminfo-dirs=/usr/share/terminfo ac_cv_header_locale_h=n ac_cv_func_getpwent=no ac_cv_func_getpwnam=no ac_cv_func_getpwuid=no && \
sed -i 's@#define HAVE_LOCALE_H 1@/* #undef HAVE_LOCALE_H */@' include/ncurses_cfg.h && \
make && \
sed -i '0,/echo.*/{s/echo.*/exit 0/}' misc/run_tic.sh && \
make install && \
mv /ndk/sysroot/usr/lib/libncursesw.a /ndk/sysroot/usr/lib/libncurses.a
# Default CMD
CMD cd /fzf/src && /bin/bash

View File

@@ -1,24 +0,0 @@
FROM base/archlinux:2014.07.03
MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# apt-get
RUN pacman-key --populate archlinux && pacman-key --refresh-keys
RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git
# Install Go 1.4
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4
ENV GOROOT /go1.4
ENV PATH /go1.4/bin:$PATH
# For i386 build
RUN echo '[multilib]' >> /etc/pacman.conf && \
echo 'Include = /etc/pacman.d/mirrorlist' >> /etc/pacman.conf && \
pacman-db-upgrade && yes | pacman -Sy gcc-multilib lib32-ncurses && \
cd $GOROOT/src && GOARCH=386 ./make.bash
# Default CMD
CMD cd /fzf/src && /bin/bash

View File

@@ -1,32 +0,0 @@
FROM centos:centos6
MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# yum
RUN yum install -y git gcc make tar glibc-devel glibc-devel.i686 \
ncurses-devel ncurses-static ncurses-devel.i686 \
gpm-devel gpm-static libgcc.i686
# Install Go 1.4
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4
# Install Go 1.5
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz | \
tar -xz && mv go go1.5
# Install RPMs for building static 32-bit binary
RUN curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.7/os/i386/Packages/ncurses-static-5.7-4.20090207.el6.i686.rpm -o rpm && rpm -i rpm && \
curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.7/os/i386/Packages/gpm-static-1.20.6-12.el6.i686.rpm -o rpm && rpm -i rpm
ENV GOROOT_BOOTSTRAP /go1.4
ENV GOROOT /go1.5
ENV PATH /go1.5/bin:$PATH
# For i386 build
RUN cd $GOROOT/src && GOARCH=386 ./make.bash
# Default CMD
CMD cd /fzf/src && /bin/bash

View File

@@ -1,22 +0,0 @@
FROM ubuntu:14.04
MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# apt-get
RUN apt-get update && apt-get -y upgrade && \
apt-get install -y --force-yes git curl build-essential libncurses-dev libgpm-dev
# Install Go 1.4
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4
ENV GOROOT /go1.4
ENV PATH /go1.4/bin:$PATH
# For i386 build
RUN apt-get install -y lib32ncurses5-dev && \
cd $GOROOT/src && GOARCH=386 ./make.bash
# Default CMD
CMD cd /fzf/src && /bin/bash

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Junegunn Choi Copyright (c) 2017 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,116 +0,0 @@
ifndef GOOS
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
GOOS := darwin
else ifeq ($(UNAME_S),Linux)
GOOS := linux
endif
endif
SOURCES := $(wildcard *.go */*.go)
ROOTDIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
BINDIR := $(shell dirname $(ROOTDIR))/bin
GOPATH := $(shell dirname $(ROOTDIR))/gopath
SRCDIR := $(GOPATH)/src/github.com/junegunn/fzf/src
DOCKEROPTS := -i -t -v $(ROOTDIR):/fzf/src
BINARY32 := fzf-$(GOOS)_386
BINARY64 := fzf-$(GOOS)_amd64
BINARYARM7 := fzf-$(GOOS)_arm7
VERSION := $(shell awk -F= '/version =/ {print $$2}' constants.go | tr -d "\" ")
RELEASE32 := fzf-$(VERSION)-$(GOOS)_386
RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64
RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7
export GOPATH
UNAME_M := $(shell uname -m)
ifeq ($(UNAME_M),x86_64)
BINARY := $(BINARY64)
else ifeq ($(UNAME_M),i686)
BINARY := $(BINARY32)
else
$(error "Build on $(UNAME_M) is not supported, yet.")
endif
all: fzf/$(BINARY)
release: test fzf/$(BINARY32) fzf/$(BINARY64)
-cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32)
cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \
rm -f $(RELEASE32) $(RELEASE64)
$(SRCDIR):
mkdir -p $(shell dirname $(SRCDIR))
ln -s $(ROOTDIR) $(SRCDIR)
deps: $(SRCDIR) $(SOURCES)
cd $(SRCDIR) && go get
android-build: $(SRCDIR)
cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get
cd $(SRCDIR)/fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-w -extldflags=-pie" -o $(BINARYARM7)
cd $(SRCDIR)/fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) && \
rm -f $(RELEASEARM7)
test: deps
SHELL=/bin/sh go test -v ./...
install: $(BINDIR)/fzf
uninstall:
rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY)
clean:
cd fzf && rm -f fzf-*
fzf/$(BINARY32): deps
cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -ldflags -w -tags "$(TAGS)" -o $(BINARY32)
fzf/$(BINARY64): deps
cd fzf && go build -a -ldflags -w -tags "$(TAGS)" -o $(BINARY64)
$(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR)
cp -f fzf/$(BINARY) $(BINDIR)
cd $(BINDIR) && ln -sf $(BINARY) fzf
$(BINDIR):
mkdir -p $@
docker-arch:
docker build -t junegunn/arch-sandbox - < Dockerfile.arch
docker-ubuntu:
docker build -t junegunn/ubuntu-sandbox - < Dockerfile.ubuntu
docker-centos:
docker build -t junegunn/centos-sandbox - < Dockerfile.centos
docker-android:
docker build -t junegunn/android-sandbox - < Dockerfile.android
arch: docker-arch
docker run $(DOCKEROPTS) junegunn/$@-sandbox \
sh -c 'cd /fzf/src; /bin/bash'
ubuntu: docker-ubuntu
docker run $(DOCKEROPTS) junegunn/$@-sandbox \
sh -c 'cd /fzf/src; /bin/bash'
centos: docker-centos
docker run $(DOCKEROPTS) junegunn/$@-sandbox \
sh -c 'cd /fzf/src; /bin/bash'
linux: docker-centos
docker run $(DOCKEROPTS) junegunn/centos-sandbox \
/bin/bash -ci 'cd /fzf/src; make TAGS=static release'
ubuntu-android: docker-android
docker run $(DOCKEROPTS) junegunn/android-sandbox \
sh -c 'cd /fzf/src; /bin/bash'
android: docker-android
docker run $(DOCKEROPTS) junegunn/android-sandbox \
/bin/bash -ci 'cd /fzf/src; GOOS=android make android-build'
.PHONY: all deps release test install uninstall clean \
linux arch ubuntu centos docker-arch docker-ubuntu docker-centos \
android-build docker-android ubuntu-android android

View File

@@ -1,121 +0,0 @@
fzf in Go
=========
<img src="https://cloud.githubusercontent.com/assets/700826/5725028/028ea834-9b93-11e4-9198-43088c3f295d.gif" height="463" alt="fzf in go">
This directory contains the source code for the new fzf implementation in
[Go][go].
Upgrade from Ruby version
-------------------------
The install script has been updated to download the right binary for your
system. If you already have installed fzf, simply git-pull the repository and
rerun the install script.
```sh
cd ~/.fzf
git pull
./install
```
Otherwise, follow [the instruction][install] as before. You can also install
fzf using Homebrew if you prefer that way.
Motivations
-----------
### No Ruby dependency
There have always been complaints about fzf being a Ruby script. To make
matters worse, Ruby 2.1 removed ncurses binding from its standard libary.
Because of the change, users running Ruby 2.1 or above are forced to build C
extensions of curses gem to meet the requirement of fzf. The new Go version
will be distributed as an executable binary so it will be much more accessible
and should be easier to setup.
### Performance
Many people have been surprised to see how fast fzf is even when it was
written in Ruby. It stays quite responsive even for 100k+ lines, which is
well above the size of the usual input.
The new Go version, of course, is significantly faster than that. It has all
the performance optimization techniques used in Ruby implementation and more.
It also doesn't suffer from [GIL][gil], so the search performance scales
proportional to the number of CPU cores. On my MacBook Pro (Mid 2012), the new
version was shown to be an order of magnitude faster on certain cases. It also
starts much faster though the difference may not be noticeable.
Differences with Ruby version
-----------------------------
The Go version is designed to be perfectly compatible with the previous Ruby
version. The only behavioral difference is that the new version ignores the
numeric argument to `--sort=N` option and always sorts the result regardless
of the number of matches. The value was introduced to limit the response time
of the query, but the Go version is blazingly fast (almost instant response
even for 1M+ items) so I decided that it's no longer required.
System requirements
-------------------
Currently, prebuilt binaries are provided only for OS X and Linux. The install
script will fall back to the legacy Ruby version on the other systems, but if
you have Go 1.4 installed, you can try building it yourself.
However, as pointed out in [golang.org/doc/install][req], the Go version may
not run on CentOS/RHEL 5.x, and if that's the case, the install script will
choose the Ruby version instead.
The Go version depends on [ncurses][ncurses] and some Unix system calls, so it
shouldn't run natively on Windows at the moment. But it won't be impossible to
support Windows by falling back to a cross-platform alternative such as
[termbox][termbox] only on Windows. If you're interested in making fzf work on
Windows, please let me know.
Build
-----
```sh
# Build fzf executables and tarballs
make release
# Install the executable to ../bin directory
make install
# Build executables and tarballs for Linux using Docker
make linux
```
Contribution
------------
For the time being, I will not add or accept any new features until we can be
sure that the implementation is stable and we have a sufficient number of test
cases. However, fixes for obvious bugs and new test cases are welcome.
I also care much about the performance of the implementation, so please make
sure that your change does not result in performance regression. And please be
noted that we don't have a quantitative measure of the performance yet.
Third-party libraries used
--------------------------
- [ncurses][ncurses]
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
- Licensed under [MIT](http://mattn.mit-license.org/2013)
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
- Licensed under [MIT](http://mattn.mit-license.org/2014)
License
-------
[MIT](LICENSE)
[install]: https://github.com/junegunn/fzf#installation
[go]: https://golang.org/
[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock
[ncurses]: https://www.gnu.org/software/ncurses/
[req]: http://golang.org/doc/install
[termbox]: https://github.com/nsf/termbox-go

View File

@@ -1,37 +1,145 @@
package algo package algo
/*
Algorithm
---------
FuzzyMatchV1 finds the first "fuzzy" occurrence of the pattern within the given
text in O(n) time where n is the length of the text. Once the position of the
last character is located, it traverses backwards to see if there's a shorter
substring that matches the pattern.
a_____b___abc__ To find "abc"
*-----*-----*> 1. Forward scan
<*** 2. Backward scan
The algorithm is simple and fast, but as it only sees the first occurrence,
it is not guaranteed to find the occurrence with the highest score.
a_____b__c__abc
*-----*--* ***
FuzzyMatchV2 implements a modified version of Smith-Waterman algorithm to find
the optimal solution (highest score) according to the scoring criteria. Unlike
the original algorithm, omission or mismatch of a character in the pattern is
not allowed.
Performance
-----------
The new V2 algorithm is slower than V1 as it examines all occurrences of the
pattern instead of stopping immediately after finding the first one. The time
complexity of the algorithm is O(nm) if a match is found and O(n) otherwise
where n is the length of the item and m is the length of the pattern. Thus, the
performance overhead may not be noticeable for a query with high selectivity.
However, if the performance is more important than the quality of the result,
you can still choose v1 algorithm with --algo=v1.
Scoring criteria
----------------
- We prefer matches at special positions, such as the start of a word, or
uppercase character in camelCase words.
- That is, we prefer an occurrence of the pattern with more characters
matching at special positions, even if the total match length is longer.
e.g. "fuzzyfinder" vs. "fuzzy-finder" on "ff"
````````````
- Also, if the first character in the pattern appears at one of the special
positions, the bonus point for the position is multiplied by a constant
as it is extremely likely that the first character in the typed pattern
has more significance than the rest.
e.g. "fo-bar" vs. "foob-r" on "br"
``````
- But since fzf is still a fuzzy finder, not an acronym finder, we should also
consider the total length of the matched substring. This is why we have the
gap penalty. The gap penalty increases as the length of the gap (distance
between the matching characters) increases, so the effect of the bonus is
eventually cancelled at some point.
e.g. "fuzzyfinder" vs. "fuzzy-blurry-finder" on "ff"
```````````
- Consequently, it is crucial to find the right balance between the bonus
and the gap penalty. The parameters were chosen that the bonus is cancelled
when the gap size increases beyond 8 characters.
- The bonus mechanism can have the undesirable side effect where consecutive
matches are ranked lower than the ones with gaps.
e.g. "foobar" vs. "foo-bar" on "foob"
```````
- To correct this anomaly, we also give extra bonus point to each character
in a consecutive matching chunk.
e.g. "foobar" vs. "foo-bar" on "foob"
``````
- The amount of consecutive bonus is primarily determined by the bonus of the
first character in the chunk.
e.g. "foobar" vs. "out-of-bound" on "oob"
````````````
*/
import ( import (
"bytes"
"fmt"
"strings" "strings"
"unicode" "unicode"
"unicode/utf8"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
/* var DEBUG bool
* String matching algorithms here do not use strings.ToLower to avoid
* performance penalty. And they assume pattern runes are given in lowercase
* letters when caseSensitive is false.
*
* In short: They try to do as little work as possible.
*/
func runeAt(runes []rune, index int, max int, forward bool) rune { func indexAt(index int, max int, forward bool) int {
if forward { if forward {
return runes[index] return index
} }
return runes[max-index-1] return max - index - 1
} }
// Result conatins the results of running a match function. // Result contains the results of running a match function.
type Result struct { type Result struct {
Start int32 // TODO int32 should suffice
End int32 Start int
End int
// Items are basically sorted by the lengths of matched substrings. Score int
// But we slightly adjust the score with bonus for better results.
Bonus int32
} }
const (
scoreMatch = 16
scoreGapStart = -3
scoreGapExtention = -1
// We prefer matches at the beginning of a word, but the bonus should not be
// too great to prevent the longer acronym matches from always winning over
// shorter fuzzy matches. The bonus point here was specifically chosen that
// the bonus is cancelled when the gap between the acronyms grows over
// 8 characters, which is approximately the average length of the words found
// in web2 dictionary and my file system.
bonusBoundary = scoreMatch / 2
// Although bonus point for non-word characters is non-contextual, we need it
// for computing bonus points for consecutive chunks starting with a non-word
// character.
bonusNonWord = scoreMatch / 2
// Edge-triggered bonus for matches in camelCase words.
// Compared to word-boundary case, they don't accompany single-character gaps
// (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly.
bonusCamel123 = bonusBoundary + scoreGapExtention
// Minimum bonus point given to characters in consecutive chunks.
// Note that bonus points for consecutive matches shouldn't have needed if we
// used fixed match score as in the original algorithm.
bonusConsecutive = -(scoreGapStart + scoreGapExtention)
// The first character in the typed pattern usually has more significance
// than the rest so it's important that it appears at special positions where
// bonus points are given. e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
// The amount of the extra bonus should be limited so that the gap penalty is
// still respected.
bonusFirstCharMultiplier = 2
)
type charClass int type charClass int
const ( const (
@@ -42,94 +150,485 @@ const (
charNumber charNumber
) )
func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, eidx int) int32 { func posArray(withPos bool, len int) *[]int {
var bonus int32 if withPos {
pidx := 0 pos := make([]int, 0, len)
lenPattern := len(pattern) return &pos
consecutive := false
prevClass := charNonWord
for index := 0; index < eidx; index++ {
char := runes[index]
var class charClass
if unicode.IsLower(char) {
class = charLower
} else if unicode.IsUpper(char) {
class = charUpper
} else if unicode.IsLetter(char) {
class = charLetter
} else if unicode.IsNumber(char) {
class = charNumber
} else {
class = charNonWord
}
var point int32
if prevClass == charNonWord && class != charNonWord {
// Word boundary
point = 2
} else if prevClass == charLower && class == charUpper ||
prevClass != charNumber && class == charNumber {
// camelCase letter123
point = 1
}
prevClass = class
if index >= sidx {
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
} else if char > unicode.MaxASCII {
char = unicode.To(unicode.LowerCase, char)
}
}
pchar := pattern[pidx]
if pchar == char {
// Boost bonus for the first character in the pattern
if pidx == 0 {
point *= 2
}
// Bonus to consecutive matching chars
if consecutive {
point++
}
bonus += point
if pidx++; pidx == lenPattern {
break
}
consecutive = true
} else {
consecutive = false
}
}
} }
return bonus return nil
} }
// FuzzyMatch performs fuzzy-match func alloc16(offset int, slab *util.Slab, size int) (int, []int16) {
func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { if slab != nil && cap(slab.I16) > offset+size {
if len(pattern) == 0 { slice := slab.I16[offset : offset+size]
return Result{0, 0, 0} return offset + size, slice
}
return offset, make([]int16, size)
}
func alloc32(offset int, slab *util.Slab, size int) (int, []int32) {
if slab != nil && cap(slab.I32) > offset+size {
slice := slab.I32[offset : offset+size]
return offset + size, slice
}
return offset, make([]int32, size)
}
func charClassOfAscii(char rune) charClass {
if char >= 'a' && char <= 'z' {
return charLower
} else if char >= 'A' && char <= 'Z' {
return charUpper
} else if char >= '0' && char <= '9' {
return charNumber
}
return charNonWord
}
func charClassOfNonAscii(char rune) charClass {
if unicode.IsLower(char) {
return charLower
} else if unicode.IsUpper(char) {
return charUpper
} else if unicode.IsNumber(char) {
return charNumber
} else if unicode.IsLetter(char) {
return charLetter
}
return charNonWord
}
func charClassOf(char rune) charClass {
if char <= unicode.MaxASCII {
return charClassOfAscii(char)
}
return charClassOfNonAscii(char)
}
func bonusFor(prevClass charClass, class charClass) int16 {
if prevClass == charNonWord && class != charNonWord {
// Word boundary
return bonusBoundary
} else if prevClass == charLower && class == charUpper ||
prevClass != charNumber && class == charNumber {
// camelCase letter123
return bonusCamel123
} else if class == charNonWord {
return bonusNonWord
}
return 0
}
func bonusAt(input *util.Chars, idx int) int16 {
if idx == 0 {
return bonusBoundary
}
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx)))
}
func normalizeRune(r rune) rune {
if r < 0x00C0 || r > 0x2184 {
return r
}
n := normalized[r]
if n > 0 {
return n
}
return r
}
// Algo functions make two assumptions
// 1. "pattern" is given in lowercase if "caseSensitive" is false
// 2. "pattern" is already normalized if "normalize" is true
type Algo func(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int)
func trySkip(input *util.Chars, caseSensitive bool, b byte, from int) int {
byteArray := input.Bytes()[from:]
idx := bytes.IndexByte(byteArray, b)
if idx == 0 {
// Can't skip any further
return from
}
// We may need to search for the uppercase letter again. We don't have to
// consider normalization as we can be sure that this is an ASCII string.
if !caseSensitive && b >= 'a' && b <= 'z' {
if idx > 0 {
byteArray = byteArray[:idx]
}
uidx := bytes.IndexByte(byteArray, b-32)
if uidx >= 0 {
idx = uidx
}
}
if idx < 0 {
return -1
}
return from + idx
}
func isAscii(runes []rune) bool {
for _, r := range runes {
if r >= utf8.RuneSelf {
return false
}
}
return true
}
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int {
// Can't determine
if !input.IsBytes() {
return 0
}
// Not possible
if !isAscii(pattern) {
return -1
}
firstIdx, idx := 0, 0
for pidx := 0; pidx < len(pattern); pidx++ {
idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx)
if idx < 0 {
return -1
}
if pidx == 0 && idx > 0 {
// Step back to find the right bonus point
firstIdx = idx - 1
}
idx++
}
return firstIdx
}
func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) {
width := lastIdx - int(F[0]) + 1
for i, f := range F {
I := i * width
if i == 0 {
fmt.Print(" ")
for j := int(f); j <= lastIdx; j++ {
fmt.Printf(" " + string(T[j]) + " ")
}
fmt.Println()
}
fmt.Print(string(pattern[i]) + " ")
for idx := int(F[0]); idx < int(f); idx++ {
fmt.Print(" 0 ")
}
for idx := int(f); idx <= lastIdx; idx++ {
fmt.Printf("%2d ", H[i*width+idx-int(F[0])])
}
fmt.Println()
fmt.Print(" ")
for idx, p := range C[I : I+width] {
if idx+int(F[0]) < int(F[i]) {
p = 0
}
if p > 0 {
fmt.Printf("%2d ", p)
} else {
fmt.Print(" ")
}
}
fmt.Println()
}
}
func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
// Assume that pattern is given in lowercase if case-insensitive.
// First check if there's a match and calculate bonus for each position.
// If the input string is too long, consider finding the matching chars in
// this phase as well (non-optimal alignment).
M := len(pattern)
if M == 0 {
return Result{0, 0, 0}, posArray(withPos, M)
}
N := input.Length()
// Since O(nm) algorithm can be prohibitively expensive for large input,
// we fall back to the greedy algorithm.
if slab != nil && N*M > cap(slab.I16) {
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab)
}
// Phase 1. Optimized search for ASCII string
idx := asciiFuzzyIndex(input, pattern, caseSensitive)
if idx < 0 {
return Result{-1, -1, 0}, nil
}
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
offset16 := 0
offset32 := 0
offset16, H0 := alloc16(offset16, slab, N)
offset16, C0 := alloc16(offset16, slab, N)
// Bonus point for each position
offset16, B := alloc16(offset16, slab, N)
// The first occurrence of each character in the pattern
offset32, F := alloc32(offset32, slab, M)
// Rune array
offset32, T := alloc32(offset32, slab, N)
input.CopyRunes(T)
// Phase 2. Calculate bonus for each point
maxScore, maxScorePos := int16(0), 0
pidx, lastIdx := 0, 0
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), charNonWord, false
Tsub := T[idx:]
H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)]
for off, char := range Tsub {
var class charClass
if char <= unicode.MaxASCII {
class = charClassOfAscii(char)
if !caseSensitive && class == charUpper {
char += 32
}
} else {
class = charClassOfNonAscii(char)
if !caseSensitive && class == charUpper {
char = unicode.To(unicode.LowerCase, char)
}
if normalize {
char = normalizeRune(char)
}
}
Tsub[off] = char
bonus := bonusFor(prevClass, class)
Bsub[off] = bonus
prevClass = class
if char == pchar {
if pidx < M {
F[pidx] = int32(idx + off)
pidx++
pchar = pattern[util.Min(pidx, M-1)]
}
lastIdx = idx + off
}
if char == pchar0 {
score := scoreMatch + bonus*bonusFirstCharMultiplier
H0sub[off] = score
C0sub[off] = 1
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
maxScore, maxScorePos = score, idx+off
if forward && bonus == bonusBoundary {
break
}
}
inGap = false
} else {
if inGap {
H0sub[off] = util.Max16(prevH0+scoreGapExtention, 0)
} else {
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0)
}
C0sub[off] = 0
inGap = true
}
prevH0 = H0sub[off]
}
if pidx != M {
return Result{-1, -1, 0}, nil
}
if M == 1 {
result := Result{maxScorePos, maxScorePos + 1, int(maxScore)}
if !withPos {
return result, nil
}
pos := []int{maxScorePos}
return result, &pos
}
// Phase 3. Fill in score matrix (H)
// Unlike the original algorithm, we do not allow omission.
f0 := int(F[0])
width := lastIdx - f0 + 1
offset16, H := alloc16(offset16, slab, width*M)
copy(H, H0[f0:lastIdx+1])
// Possible length of consecutive chunk at each position.
offset16, C := alloc16(offset16, slab, width*M)
copy(C, C0[f0:lastIdx+1])
Fsub := F[1:]
Psub := pattern[1:][:len(Fsub)]
for off, f := range Fsub {
f := int(f)
pchar := Psub[off]
pidx := off + 1
row := pidx * width
inGap := false
Tsub := T[f : lastIdx+1]
Bsub := B[f:][:len(Tsub)]
Csub := C[row+f-f0:][:len(Tsub)]
Cdiag := C[row+f-f0-1-width:][:len(Tsub)]
Hsub := H[row+f-f0:][:len(Tsub)]
Hdiag := H[row+f-f0-1-width:][:len(Tsub)]
Hleft := H[row+f-f0-1:][:len(Tsub)]
Hleft[0] = 0
for off, char := range Tsub {
col := off + f
var s1, s2, consecutive int16
if inGap {
s2 = Hleft[off] + scoreGapExtention
} else {
s2 = Hleft[off] + scoreGapStart
}
if pchar == char {
s1 = Hdiag[off] + scoreMatch
b := Bsub[off]
consecutive = Cdiag[off] + 1
// Break consecutive chunk
if b == bonusBoundary {
consecutive = 1
} else if consecutive > 1 {
b = util.Max16(b, util.Max16(bonusConsecutive, B[col-int(consecutive)+1]))
}
if s1+b < s2 {
s1 += Bsub[off]
consecutive = 0
} else {
s1 += b
}
}
Csub[off] = consecutive
inGap = s1 < s2
score := util.Max16(util.Max16(s1, s2), 0)
if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {
maxScore, maxScorePos = score, col
}
Hsub[off] = score
}
}
if DEBUG {
debugV2(T, pattern, F, lastIdx, H, C)
}
// Phase 4. (Optional) Backtrace to find character positions
pos := posArray(withPos, M)
j := f0
if withPos {
i := M - 1
j = maxScorePos
preferMatch := true
for {
I := i * width
j0 := j - f0
s := H[I+j0]
var s1, s2 int16
if i > 0 && j >= int(F[i]) {
s1 = H[I-width+j0-1]
}
if j > int(F[i]) {
s2 = H[I+j0-1]
}
if s > s1 && (s > s2 || s == s2 && preferMatch) {
*pos = append(*pos, j)
if i == 0 {
break
}
i--
}
preferMatch = C[I+j0] > 1 || I+width+j0+1 < len(C) && C[I+width+j0+1] > 0
j--
}
}
// Start offset we return here is only relevant when begin tiebreak is used.
// However finding the accurate offset requires backtracking, and we don't
// want to pay extra cost for the option that has lost its importance.
return Result{j, maxScorePos + 1, int(maxScore)}, pos
}
// Implement the same sorting criteria as V2
func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) {
pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0)
pos := posArray(withPos, len(pattern))
prevClass := charNonWord
if sidx > 0 {
prevClass = charClassOf(text.Get(sidx - 1))
}
for idx := sidx; idx < eidx; idx++ {
char := text.Get(idx)
class := charClassOf(char)
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
} else if char > unicode.MaxASCII {
char = unicode.To(unicode.LowerCase, char)
}
}
// pattern is already normalized
if normalize {
char = normalizeRune(char)
}
if char == pattern[pidx] {
if withPos {
*pos = append(*pos, idx)
}
score += scoreMatch
bonus := bonusFor(prevClass, class)
if consecutive == 0 {
firstBonus = bonus
} else {
// Break consecutive chunk
if bonus == bonusBoundary {
firstBonus = bonus
}
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
}
if pidx == 0 {
score += int(bonus * bonusFirstCharMultiplier)
} else {
score += int(bonus)
}
inGap = false
consecutive++
pidx++
} else {
if inGap {
score += scoreGapExtention
} else {
score += scoreGapStart
}
inGap = true
consecutive = 0
firstBonus = 0
}
prevClass = class
}
return score, pos
}
// FuzzyMatchV1 performs fuzzy-match
func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
if len(pattern) == 0 {
return Result{0, 0, 0}, nil
}
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
return Result{-1, -1, 0}, nil
} }
// 0. (FIXME) How to find the shortest match?
// a_____b__c__abc
// ^^^^^^^^^^ ^^^
// 1. forward scan (abc)
// *-----*-----*>
// a_____b___abc__
// 2. reverse scan (cba)
// a_____b___abc__
// <***
pidx := 0 pidx := 0
sidx := -1 sidx := -1
eidx := -1 eidx := -1
lenRunes := len(runes) lenRunes := text.Length()
lenPattern := len(pattern) lenPattern := len(pattern)
for index := range runes { for index := 0; index < lenRunes; index++ {
char := runeAt(runes, index, lenRunes, forward) char := text.Get(indexAt(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 {
@@ -142,7 +641,10 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
pchar := runeAt(pattern, pidx, lenPattern, forward) if normalize {
char = normalizeRune(char)
}
pchar := pattern[indexAt(pidx, lenPattern, forward)]
if char == pchar { if char == pchar {
if sidx < 0 { if sidx < 0 {
sidx = index sidx = index
@@ -157,7 +659,8 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
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 := runeAt(runes, index, lenRunes, forward) tidx := indexAt(index, lenRunes, forward)
char := text.Get(tidx)
if !caseSensitive { if !caseSensitive {
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
@@ -166,7 +669,8 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
} }
} }
pchar := runeAt(pattern, pidx, lenPattern, forward) pidx_ := indexAt(pidx, lenPattern, forward)
pchar := pattern[pidx_]
if char == pchar { if char == pchar {
if pidx--; pidx < 0 { if pidx--; pidx < 0 {
sidx = index sidx = index
@@ -175,16 +679,14 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
} }
} }
// Calculate the bonus. This can't be done at the same time as the
// pattern scan above because 'forward' may be false.
if !forward { if !forward {
sidx, eidx = lenRunes-eidx, lenRunes-sidx sidx, eidx = lenRunes-eidx, lenRunes-sidx
} }
return Result{int32(sidx), int32(eidx), score, pos := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, withPos)
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} return Result{sidx, eidx, score}, pos
} }
return Result{-1, -1, 0} return Result{-1, -1, 0}, nil
} }
// ExactMatchNaive is a basic string searching algorithm that handles case // ExactMatchNaive is a basic string searching algorithm that handles case
@@ -192,23 +694,32 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
// of strings.ToLower + strings.Index for typical fzf use cases where input // of strings.ToLower + strings.Index for typical fzf use cases where input
// strings and patterns are not very long. // strings and patterns are not very long.
// //
// We might try to implement better algorithms in the future: // Since 0.15.0, this function searches for the match with the highest
// http://en.wikipedia.org/wiki/String_searching_algorithm // bonus point, instead of stopping immediately after finding the first match.
func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { // The solution is much cheaper since there is only one possible alignment of
// the pattern.
func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
if len(pattern) == 0 { if len(pattern) == 0 {
return Result{0, 0, 0} return Result{0, 0, 0}, nil
} }
lenRunes := len(runes) lenRunes := text.Length()
lenPattern := len(pattern) lenPattern := len(pattern)
if lenRunes < lenPattern { if lenRunes < lenPattern {
return Result{-1, -1, 0} return Result{-1, -1, 0}, nil
} }
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
return Result{-1, -1, 0}, nil
}
// For simplicity, only look at the bonus at the first character position
pidx := 0 pidx := 0
bestPos, bonus, bestBonus := -1, int16(0), int16(-1)
for index := 0; index < lenRunes; index++ { for index := 0; index < lenRunes; index++ {
char := runeAt(runes, index, lenRunes, forward) index_ := indexAt(index, lenRunes, forward)
char := text.Get(index_)
if !caseSensitive { if !caseSensitive {
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
@@ -216,86 +727,133 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
pchar := runeAt(pattern, pidx, lenPattern, forward) if normalize {
char = normalizeRune(char)
}
pidx_ := indexAt(pidx, lenPattern, forward)
pchar := pattern[pidx_]
if pchar == char { if pchar == char {
if pidx_ == 0 {
bonus = bonusAt(text, index_)
}
pidx++ pidx++
if pidx == lenPattern { if pidx == lenPattern {
var sidx, eidx int if bonus > bestBonus {
if forward { bestPos, bestBonus = index, bonus
sidx = index - lenPattern + 1
eidx = index + 1
} else {
sidx = lenRunes - (index + 1)
eidx = lenRunes - (index - lenPattern + 1)
} }
return Result{int32(sidx), int32(eidx), if bonus == bonusBoundary {
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} break
}
index -= pidx - 1
pidx, bonus = 0, 0
} }
} else { } else {
index -= pidx index -= pidx
pidx = 0 pidx, bonus = 0, 0
} }
} }
return Result{-1, -1, 0} if bestPos >= 0 {
var sidx, eidx int
if forward {
sidx = bestPos - lenPattern + 1
eidx = bestPos + 1
} else {
sidx = lenRunes - (bestPos + 1)
eidx = lenRunes - (bestPos - lenPattern + 1)
}
score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
return Result{sidx, eidx, score}, nil
}
return Result{-1, -1, 0}, nil
} }
// PrefixMatch performs prefix-match // PrefixMatch performs prefix-match
func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
if len(runes) < len(pattern) { if len(pattern) == 0 {
return Result{-1, -1, 0} return Result{0, 0, 0}, nil
}
if text.Length() < len(pattern) {
return Result{-1, -1, 0}, nil
} }
for index, r := range pattern { for index, r := range pattern {
char := runes[index] char := text.Get(index)
if !caseSensitive { if !caseSensitive {
char = unicode.ToLower(char) char = unicode.ToLower(char)
} }
if normalize {
char = normalizeRune(char)
}
if char != r { if char != r {
return Result{-1, -1, 0} return Result{-1, -1, 0}, nil
} }
} }
lenPattern := len(pattern) lenPattern := len(pattern)
return Result{0, int32(lenPattern), score, _ := calculateScore(caseSensitive, normalize, text, pattern, 0, lenPattern, false)
evaluateBonus(caseSensitive, runes, pattern, 0, lenPattern)} return Result{0, lenPattern, score}, nil
} }
// SuffixMatch performs suffix-match // SuffixMatch performs suffix-match
func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) Result { func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
runes := util.TrimRight(input) lenRunes := text.Length()
trimmedLen := len(runes) trimmedLen := lenRunes - text.TrailingWhitespaces()
if len(pattern) == 0 {
return Result{trimmedLen, trimmedLen, 0}, nil
}
diff := trimmedLen - len(pattern) diff := trimmedLen - len(pattern)
if diff < 0 { if diff < 0 {
return Result{-1, -1, 0} return Result{-1, -1, 0}, nil
} }
for index, r := range pattern { for index, r := range pattern {
char := runes[index+diff] char := text.Get(index + diff)
if !caseSensitive { if !caseSensitive {
char = unicode.ToLower(char) char = unicode.ToLower(char)
} }
if normalize {
char = normalizeRune(char)
}
if char != r { if char != r {
return Result{-1, -1, 0} return Result{-1, -1, 0}, nil
} }
} }
lenPattern := len(pattern) lenPattern := len(pattern)
sidx := trimmedLen - lenPattern sidx := trimmedLen - lenPattern
eidx := trimmedLen eidx := trimmedLen
return Result{int32(sidx), int32(eidx), score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} return Result{sidx, eidx, score}, nil
} }
// EqualMatch performs equal-match // EqualMatch performs equal-match
func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
// Note: EqualMatch always return a zero bonus. lenPattern := len(pattern)
if len(runes) != len(pattern) { if text.Length() != lenPattern {
return Result{-1, -1, 0} return Result{-1, -1, 0}, nil
} }
runesStr := string(runes) match := true
if !caseSensitive { if normalize {
runesStr = strings.ToLower(runesStr) runes := text.ToRunes()
for idx, pchar := range pattern {
char := runes[idx]
if !caseSensitive {
char = unicode.To(unicode.LowerCase, char)
}
if normalizeRune(pchar) != normalizeRune(char) {
match = false
break
}
}
} else {
runesStr := text.ToString()
if !caseSensitive {
runesStr = strings.ToLower(runesStr)
}
match = runesStr == string(pattern)
} }
if runesStr == string(pattern) { if match {
return Result{0, int32(len(pattern)), 0} return Result{0, lenPattern, (scoreMatch+bonusBoundary)*lenPattern +
(bonusFirstCharMultiplier-1)*bonusBoundary}, nil
} }
return Result{-1, -1, 0} return Result{-1, -1, 0}, nil
} }

View File

@@ -1,95 +1,186 @@
package algo package algo
import ( import (
"math"
"sort"
"strings" "strings"
"testing" "testing"
"github.com/junegunn/fzf/src/util"
) )
func assertMatch(t *testing.T, fun func(bool, bool, []rune, []rune) Result, caseSensitive, forward bool, input, pattern string, sidx int32, eidx int32, bonus int32) { func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) {
assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score)
}
func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool, input, pattern string, sidx int, eidx int, score int) {
if !caseSensitive { if !caseSensitive {
pattern = strings.ToLower(pattern) pattern = strings.ToLower(pattern)
} }
res := fun(caseSensitive, forward, []rune(input), []rune(pattern)) chars := util.ToChars([]byte(input))
if res.Start != sidx { res, pos := fun(caseSensitive, normalize, forward, &chars, []rune(pattern), true, nil)
t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", res.Start, sidx, input, pattern) var start, end int
if pos == nil || len(*pos) == 0 {
start = res.Start
end = res.End
} else {
sort.Ints(*pos)
start = (*pos)[0]
end = (*pos)[len(*pos)-1] + 1
} }
if res.End != eidx { if start != sidx {
t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", res.End, eidx, input, pattern) t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", start, sidx, input, pattern)
} }
if res.Bonus != bonus { if end != eidx {
t.Errorf("Invalid bonus: %d (expected: %d, %s / %s)", res.Bonus, bonus, input, pattern) t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", end, eidx, input, pattern)
}
if res.Score != score {
t.Errorf("Invalid score: %d (expected: %d, %s / %s)", res.Score, score, input, pattern)
} }
} }
func TestFuzzyMatch(t *testing.T) { func TestFuzzyMatch(t *testing.T) {
assertMatch(t, FuzzyMatch, false, true, "fooBarbaz", "oBZ", 2, 9, 2) for _, fn := range []Algo{FuzzyMatchV1, FuzzyMatchV2} {
assertMatch(t, FuzzyMatch, false, true, "foo bar baz", "fbb", 0, 9, 8) for _, forward := range []bool{true, false} {
assertMatch(t, FuzzyMatch, false, true, "/AutomatorDocument.icns", "rdoc", 9, 13, 4) assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9,
assertMatch(t, FuzzyMatch, false, true, "/man1/zshcompctl.1", "zshc", 6, 10, 7) scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3)
assertMatch(t, FuzzyMatch, false, true, "/.oh-my-zsh/cache", "zshc", 8, 13, 8) assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9,
assertMatch(t, FuzzyMatch, false, true, "ab0123 456", "12356", 3, 10, 3) scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
assertMatch(t, FuzzyMatch, false, true, "abc123 456", "12356", 3, 10, 5) bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13,
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart)
assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10,
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtention)
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtention)
assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtention)
assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8,
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+
scoreGapStart*2+scoreGapExtention*3)
assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4,
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6,
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+
bonusNonWord+bonusBoundary)
assertMatch(t, FuzzyMatch, false, true, "foo/bar/baz", "fbb", 0, 9, 8) assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9,
assertMatch(t, FuzzyMatch, false, true, "fooBarBaz", "fbb", 0, 7, 6) scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3)
assertMatch(t, FuzzyMatch, false, true, "foo barbaz", "fbb", 0, 8, 6) assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9,
assertMatch(t, FuzzyMatch, false, true, "fooBar Baz", "foob", 0, 4, 8) scoreMatch*3+bonusBoundary*(bonusFirstCharMultiplier+2)+
assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBZ", -1, -1, 0) scoreGapStart*2+scoreGapExtention*4)
assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBz", 2, 9, 2) assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
assertMatch(t, FuzzyMatch, true, true, "Foo Bar Baz", "fbb", -1, -1, 0) scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+
assertMatch(t, FuzzyMatch, true, true, "Foo/Bar/Baz", "FBB", 0, 9, 8) scoreGapStart*2+scoreGapExtention*2)
assertMatch(t, FuzzyMatch, true, true, "FooBarBaz", "FBB", 0, 7, 6) assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
assertMatch(t, FuzzyMatch, true, true, "foo BarBaz", "fBB", 0, 8, 7) scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+
assertMatch(t, FuzzyMatch, true, true, "FooBar Baz", "FooB", 0, 4, 8) util.Max(bonusCamel123, bonusBoundary))
assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "fooBarbazz", -1, -1, 0)
// Consecutive bonus updated
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,
scoreMatch*4+bonusBoundary*3)
// Non-match
assertMatch(t, fn, true, forward, "fooBarbaz", "oBZ", -1, -1, 0)
assertMatch(t, fn, true, forward, "Foo Bar Baz", "fbb", -1, -1, 0)
assertMatch(t, fn, true, forward, "fooBarbaz", "fooBarbazz", -1, -1, 0)
}
}
} }
func TestFuzzyMatchBackward(t *testing.T) { func TestFuzzyMatchBackward(t *testing.T) {
assertMatch(t, FuzzyMatch, false, true, "foobar fb", "fb", 0, 4, 4) assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4,
assertMatch(t, FuzzyMatch, false, false, "foobar fb", "fb", 7, 9, 5) scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+
scoreGapStart+scoreGapExtention)
assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9,
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary)
} }
func TestExactMatchNaive(t *testing.T) { func TestExactMatchNaive(t *testing.T) {
for _, dir := range []bool{true, false} { for _, dir := range []bool{true, false} {
assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5, 3)
assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1, 0) assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1, 0)
assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1, 0) assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1, 0)
assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13, 4) assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5,
assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10, 7) scoreMatch*3+bonusCamel123+bonusConsecutive)
assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13, 10) assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13,
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10,
scoreMatch*4+bonusBoundary*(bonusFirstCharMultiplier+3))
assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13,
scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+4))
} }
} }
func TestExactMatchNaiveBackward(t *testing.T) { func TestExactMatchNaiveBackward(t *testing.T) {
assertMatch(t, ExactMatchNaive, false, true, "foobar foob", "oo", 1, 3, 1) assertMatch(t, ExactMatchNaive, false, true, "foobar foob", "oo", 1, 3,
assertMatch(t, ExactMatchNaive, false, false, "foobar foob", "oo", 8, 10, 1) scoreMatch*2+bonusConsecutive)
assertMatch(t, ExactMatchNaive, false, false, "foobar foob", "oo", 8, 10,
scoreMatch*2+bonusConsecutive)
} }
func TestPrefixMatch(t *testing.T) { func TestPrefixMatch(t *testing.T) {
score := (scoreMatch+bonusBoundary)*3 + bonusBoundary*(bonusFirstCharMultiplier-1)
for _, dir := range []bool{true, false} { for _, dir := range []bool{true, false} {
assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0) assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0)
assertMatch(t, PrefixMatch, false, dir, "fooBarBaz", "baz", -1, -1, 0) assertMatch(t, PrefixMatch, false, dir, "fooBarBaz", "baz", -1, -1, 0)
assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, 6) assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, score)
assertMatch(t, PrefixMatch, false, dir, "foOBarBaZ", "foo", 0, 3, 7) assertMatch(t, PrefixMatch, false, dir, "foOBarBaZ", "foo", 0, 3, score)
assertMatch(t, PrefixMatch, false, dir, "f-oBarbaz", "f-o", 0, 3, 8) assertMatch(t, PrefixMatch, false, dir, "f-oBarbaz", "f-o", 0, 3, score)
} }
} }
func TestSuffixMatch(t *testing.T) { func TestSuffixMatch(t *testing.T) {
for _, dir := range []bool{true, false} { for _, dir := range []bool{true, false} {
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1, 0)
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9, 2)
assertMatch(t, SuffixMatch, false, dir, "fooBarBaZ", "baz", 6, 9, 5)
assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1, 0) assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1, 0)
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1, 0)
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9,
scoreMatch*3+bonusConsecutive*2)
assertMatch(t, SuffixMatch, false, dir, "fooBarBaZ", "baz", 6, 9,
(scoreMatch+bonusCamel123)*3+bonusCamel123*(bonusFirstCharMultiplier-1))
} }
} }
func TestEmptyPattern(t *testing.T) { func TestEmptyPattern(t *testing.T) {
for _, dir := range []bool{true, false} { for _, dir := range []bool{true, false} {
assertMatch(t, FuzzyMatch, true, dir, "foobar", "", 0, 0, 0) assertMatch(t, FuzzyMatchV1, true, dir, "foobar", "", 0, 0, 0)
assertMatch(t, FuzzyMatchV2, true, dir, "foobar", "", 0, 0, 0)
assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0, 0) assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0, 0)
assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0, 0) assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0, 0)
assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6, 0) assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6, 0)
} }
} }
func TestNormalize(t *testing.T) {
caseSensitive := false
normalize := true
forward := true
test := func(input, pattern string, sidx, eidx, score int, funs ...Algo) {
for _, fun := range funs {
assertMatch2(t, fun, caseSensitive, normalize, forward,
input, pattern, sidx, eidx, score)
}
}
test("Só Danço Samba", "So", 0, 2, 56, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
test("Só Danço Samba", "sodc", 0, 7, 89, FuzzyMatchV1, FuzzyMatchV2)
test("Danço", "danco", 0, 5, 128, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
}
func TestLongString(t *testing.T) {
bytes := make([]byte, math.MaxUint16*2)
for i := range bytes {
bytes[i] = 'x'
}
bytes[math.MaxUint16] = 'z'
assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive)
}

424
src/algo/normalize.go Normal file
View File

@@ -0,0 +1,424 @@
// Normalization of latin script letters
// Reference: http://www.unicode.org/Public/UCD/latest/ucd/Index.txt
package algo
var normalized map[rune]rune = map[rune]rune{
0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER
0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER
0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER
0x00E2: 'a', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x00E4: 'a', // WITH DIAERESIS, LATIN SMALL LETTER
0x0227: 'a', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1EA1: 'a', // WITH DOT BELOW, LATIN SMALL LETTER
0x0201: 'a', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x00E0: 'a', // WITH GRAVE, LATIN SMALL LETTER
0x1EA3: 'a', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x0203: 'a', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x0101: 'a', // WITH MACRON, LATIN SMALL LETTER
0x0105: 'a', // WITH OGONEK, LATIN SMALL LETTER
0x1E9A: 'a', // WITH RIGHT HALF RING, LATIN SMALL LETTER
0x00E5: 'a', // WITH RING ABOVE, LATIN SMALL LETTER
0x1E01: 'a', // WITH RING BELOW, LATIN SMALL LETTER
0x00E3: 'a', // WITH TILDE, LATIN SMALL LETTER
0x0363: 'a', // , COMBINING LATIN SMALL LETTER
0x0250: 'a', // , LATIN SMALL LETTER TURNED
0x1E03: 'b', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E05: 'b', // WITH DOT BELOW, LATIN SMALL LETTER
0x0253: 'b', // WITH HOOK, LATIN SMALL LETTER
0x1E07: 'b', // WITH LINE BELOW, LATIN SMALL LETTER
0x0180: 'b', // WITH STROKE, LATIN SMALL LETTER
0x0183: 'b', // WITH TOPBAR, LATIN SMALL LETTER
0x0107: 'c', // WITH ACUTE, LATIN SMALL LETTER
0x010D: 'c', // WITH CARON, LATIN SMALL LETTER
0x00E7: 'c', // WITH CEDILLA, LATIN SMALL LETTER
0x0109: 'c', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x0255: 'c', // WITH CURL, LATIN SMALL LETTER
0x010B: 'c', // WITH DOT ABOVE, LATIN SMALL LETTER
0x0188: 'c', // WITH HOOK, LATIN SMALL LETTER
0x023C: 'c', // WITH STROKE, LATIN SMALL LETTER
0x0368: 'c', // , COMBINING LATIN SMALL LETTER
0x0297: 'c', // , LATIN LETTER STRETCHED
0x2184: 'c', // , LATIN SMALL LETTER REVERSED
0x010F: 'd', // WITH CARON, LATIN SMALL LETTER
0x1E11: 'd', // WITH CEDILLA, LATIN SMALL LETTER
0x1E13: 'd', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x0221: 'd', // WITH CURL, LATIN SMALL LETTER
0x1E0B: 'd', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E0D: 'd', // WITH DOT BELOW, LATIN SMALL LETTER
0x0257: 'd', // WITH HOOK, LATIN SMALL LETTER
0x1E0F: 'd', // WITH LINE BELOW, LATIN SMALL LETTER
0x0111: 'd', // WITH STROKE, LATIN SMALL LETTER
0x0256: 'd', // WITH TAIL, LATIN SMALL LETTER
0x018C: 'd', // WITH TOPBAR, LATIN SMALL LETTER
0x0369: 'd', // , COMBINING LATIN SMALL LETTER
0x00E9: 'e', // WITH ACUTE, LATIN SMALL LETTER
0x0115: 'e', // WITH BREVE, LATIN SMALL LETTER
0x011B: 'e', // WITH CARON, LATIN SMALL LETTER
0x0229: 'e', // WITH CEDILLA, LATIN SMALL LETTER
0x1E19: 'e', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x00EA: 'e', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x00EB: 'e', // WITH DIAERESIS, LATIN SMALL LETTER
0x0117: 'e', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1EB9: 'e', // WITH DOT BELOW, LATIN SMALL LETTER
0x0205: 'e', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x00E8: 'e', // WITH GRAVE, LATIN SMALL LETTER
0x1EBB: 'e', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x025D: 'e', // WITH HOOK, LATIN SMALL LETTER REVERSED OPEN
0x0207: 'e', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x0113: 'e', // WITH MACRON, LATIN SMALL LETTER
0x0119: 'e', // WITH OGONEK, LATIN SMALL LETTER
0x0247: 'e', // WITH STROKE, LATIN SMALL LETTER
0x1E1B: 'e', // WITH TILDE BELOW, LATIN SMALL LETTER
0x1EBD: 'e', // WITH TILDE, LATIN SMALL LETTER
0x0364: 'e', // , COMBINING LATIN SMALL LETTER
0x029A: 'e', // , LATIN SMALL LETTER CLOSED OPEN
0x025E: 'e', // , LATIN SMALL LETTER CLOSED REVERSED OPEN
0x025B: 'e', // , LATIN SMALL LETTER OPEN
0x0258: 'e', // , LATIN SMALL LETTER REVERSED
0x025C: 'e', // , LATIN SMALL LETTER REVERSED OPEN
0x01DD: 'e', // , LATIN SMALL LETTER TURNED
0x1D08: 'e', // , LATIN SMALL LETTER TURNED OPEN
0x1E1F: 'f', // WITH DOT ABOVE, LATIN SMALL LETTER
0x0192: 'f', // WITH HOOK, LATIN SMALL LETTER
0x01F5: 'g', // WITH ACUTE, LATIN SMALL LETTER
0x011F: 'g', // WITH BREVE, LATIN SMALL LETTER
0x01E7: 'g', // WITH CARON, LATIN SMALL LETTER
0x0123: 'g', // WITH CEDILLA, LATIN SMALL LETTER
0x011D: 'g', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x0121: 'g', // WITH DOT ABOVE, LATIN SMALL LETTER
0x0260: 'g', // WITH HOOK, LATIN SMALL LETTER
0x1E21: 'g', // WITH MACRON, LATIN SMALL LETTER
0x01E5: 'g', // WITH STROKE, LATIN SMALL LETTER
0x0261: 'g', // , LATIN SMALL LETTER SCRIPT
0x1E2B: 'h', // WITH BREVE BELOW, LATIN SMALL LETTER
0x021F: 'h', // WITH CARON, LATIN SMALL LETTER
0x1E29: 'h', // WITH CEDILLA, LATIN SMALL LETTER
0x0125: 'h', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x1E27: 'h', // WITH DIAERESIS, LATIN SMALL LETTER
0x1E23: 'h', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E25: 'h', // WITH DOT BELOW, LATIN SMALL LETTER
0x02AE: 'h', // WITH FISHHOOK, LATIN SMALL LETTER TURNED
0x0266: 'h', // WITH HOOK, LATIN SMALL LETTER
0x1E96: 'h', // WITH LINE BELOW, LATIN SMALL LETTER
0x0127: 'h', // WITH STROKE, LATIN SMALL LETTER
0x036A: 'h', // , COMBINING LATIN SMALL LETTER
0x0265: 'h', // , LATIN SMALL LETTER TURNED
0x2095: 'h', // , LATIN SUBSCRIPT SMALL LETTER
0x00ED: 'i', // WITH ACUTE, LATIN SMALL LETTER
0x012D: 'i', // WITH BREVE, LATIN SMALL LETTER
0x01D0: 'i', // WITH CARON, LATIN SMALL LETTER
0x00EE: 'i', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x00EF: 'i', // WITH DIAERESIS, LATIN SMALL LETTER
0x1ECB: 'i', // WITH DOT BELOW, LATIN SMALL LETTER
0x0209: 'i', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x00EC: 'i', // WITH GRAVE, LATIN SMALL LETTER
0x1EC9: 'i', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x020B: 'i', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x012B: 'i', // WITH MACRON, LATIN SMALL LETTER
0x012F: 'i', // WITH OGONEK, LATIN SMALL LETTER
0x0268: 'i', // WITH STROKE, LATIN SMALL LETTER
0x1E2D: 'i', // WITH TILDE BELOW, LATIN SMALL LETTER
0x0129: 'i', // WITH TILDE, LATIN SMALL LETTER
0x0365: 'i', // , COMBINING LATIN SMALL LETTER
0x0131: 'i', // , LATIN SMALL LETTER DOTLESS
0x1D09: 'i', // , LATIN SMALL LETTER TURNED
0x1D62: 'i', // , LATIN SUBSCRIPT SMALL LETTER
0x2071: 'i', // , SUPERSCRIPT LATIN SMALL LETTER
0x01F0: 'j', // WITH CARON, LATIN SMALL LETTER
0x0135: 'j', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x029D: 'j', // WITH CROSSED-TAIL, LATIN SMALL LETTER
0x0249: 'j', // WITH STROKE, LATIN SMALL LETTER
0x025F: 'j', // WITH STROKE, LATIN SMALL LETTER DOTLESS
0x0237: 'j', // , LATIN SMALL LETTER DOTLESS
0x1E31: 'k', // WITH ACUTE, LATIN SMALL LETTER
0x01E9: 'k', // WITH CARON, LATIN SMALL LETTER
0x0137: 'k', // WITH CEDILLA, LATIN SMALL LETTER
0x1E33: 'k', // WITH DOT BELOW, LATIN SMALL LETTER
0x0199: 'k', // WITH HOOK, LATIN SMALL LETTER
0x1E35: 'k', // WITH LINE BELOW, LATIN SMALL LETTER
0x029E: 'k', // , LATIN SMALL LETTER TURNED
0x2096: 'k', // , LATIN SUBSCRIPT SMALL LETTER
0x013A: 'l', // WITH ACUTE, LATIN SMALL LETTER
0x019A: 'l', // WITH BAR, LATIN SMALL LETTER
0x026C: 'l', // WITH BELT, LATIN SMALL LETTER
0x013E: 'l', // WITH CARON, LATIN SMALL LETTER
0x013C: 'l', // WITH CEDILLA, LATIN SMALL LETTER
0x1E3D: 'l', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x0234: 'l', // WITH CURL, LATIN SMALL LETTER
0x1E37: 'l', // WITH DOT BELOW, LATIN SMALL LETTER
0x1E3B: 'l', // WITH LINE BELOW, LATIN SMALL LETTER
0x0140: 'l', // WITH MIDDLE DOT, LATIN SMALL LETTER
0x026B: 'l', // WITH MIDDLE TILDE, LATIN SMALL LETTER
0x026D: 'l', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
0x0142: 'l', // WITH STROKE, LATIN SMALL LETTER
0x2097: 'l', // , LATIN SUBSCRIPT SMALL LETTER
0x1E3F: 'm', // WITH ACUTE, LATIN SMALL LETTER
0x1E41: 'm', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E43: 'm', // WITH DOT BELOW, LATIN SMALL LETTER
0x0271: 'm', // WITH HOOK, LATIN SMALL LETTER
0x0270: 'm', // WITH LONG LEG, LATIN SMALL LETTER TURNED
0x036B: 'm', // , COMBINING LATIN SMALL LETTER
0x1D1F: 'm', // , LATIN SMALL LETTER SIDEWAYS TURNED
0x026F: 'm', // , LATIN SMALL LETTER TURNED
0x2098: 'm', // , LATIN SUBSCRIPT SMALL LETTER
0x0144: 'n', // WITH ACUTE, LATIN SMALL LETTER
0x0148: 'n', // WITH CARON, LATIN SMALL LETTER
0x0146: 'n', // WITH CEDILLA, LATIN SMALL LETTER
0x1E4B: 'n', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x0235: 'n', // WITH CURL, LATIN SMALL LETTER
0x1E45: 'n', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E47: 'n', // WITH DOT BELOW, LATIN SMALL LETTER
0x01F9: 'n', // WITH GRAVE, LATIN SMALL LETTER
0x0272: 'n', // WITH LEFT HOOK, LATIN SMALL LETTER
0x1E49: 'n', // WITH LINE BELOW, LATIN SMALL LETTER
0x019E: 'n', // WITH LONG RIGHT LEG, LATIN SMALL LETTER
0x0273: 'n', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
0x00F1: 'n', // WITH TILDE, LATIN SMALL LETTER
0x2099: 'n', // , LATIN SUBSCRIPT SMALL LETTER
0x00F3: 'o', // WITH ACUTE, LATIN SMALL LETTER
0x014F: 'o', // WITH BREVE, LATIN SMALL LETTER
0x01D2: 'o', // WITH CARON, LATIN SMALL LETTER
0x00F4: 'o', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x00F6: 'o', // WITH DIAERESIS, LATIN SMALL LETTER
0x022F: 'o', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1ECD: 'o', // WITH DOT BELOW, LATIN SMALL LETTER
0x0151: 'o', // WITH DOUBLE ACUTE, LATIN SMALL LETTER
0x020D: 'o', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x00F2: 'o', // WITH GRAVE, LATIN SMALL LETTER
0x1ECF: 'o', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x01A1: 'o', // WITH HORN, LATIN SMALL LETTER
0x020F: 'o', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x014D: 'o', // WITH MACRON, LATIN SMALL LETTER
0x01EB: 'o', // WITH OGONEK, LATIN SMALL LETTER
0x00F8: 'o', // WITH STROKE, LATIN SMALL LETTER
0x1D13: 'o', // WITH STROKE, LATIN SMALL LETTER SIDEWAYS
0x00F5: 'o', // WITH TILDE, LATIN SMALL LETTER
0x0366: 'o', // , COMBINING LATIN SMALL LETTER
0x0275: 'o', // , LATIN SMALL LETTER BARRED
0x1D17: 'o', // , LATIN SMALL LETTER BOTTOM HALF
0x0254: 'o', // , LATIN SMALL LETTER OPEN
0x1D11: 'o', // , LATIN SMALL LETTER SIDEWAYS
0x1D12: 'o', // , LATIN SMALL LETTER SIDEWAYS OPEN
0x1D16: 'o', // , LATIN SMALL LETTER TOP HALF
0x1E55: 'p', // WITH ACUTE, LATIN SMALL LETTER
0x1E57: 'p', // WITH DOT ABOVE, LATIN SMALL LETTER
0x01A5: 'p', // WITH HOOK, LATIN SMALL LETTER
0x209A: 'p', // , LATIN SUBSCRIPT SMALL LETTER
0x024B: 'q', // WITH HOOK TAIL, LATIN SMALL LETTER
0x02A0: 'q', // WITH HOOK, LATIN SMALL LETTER
0x0155: 'r', // WITH ACUTE, LATIN SMALL LETTER
0x0159: 'r', // WITH CARON, LATIN SMALL LETTER
0x0157: 'r', // WITH CEDILLA, LATIN SMALL LETTER
0x1E59: 'r', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E5B: 'r', // WITH DOT BELOW, LATIN SMALL LETTER
0x0211: 'r', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x027E: 'r', // WITH FISHHOOK, LATIN SMALL LETTER
0x027F: 'r', // WITH FISHHOOK, LATIN SMALL LETTER REVERSED
0x027B: 'r', // WITH HOOK, LATIN SMALL LETTER TURNED
0x0213: 'r', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x1E5F: 'r', // WITH LINE BELOW, LATIN SMALL LETTER
0x027C: 'r', // WITH LONG LEG, LATIN SMALL LETTER
0x027A: 'r', // WITH LONG LEG, LATIN SMALL LETTER TURNED
0x024D: 'r', // WITH STROKE, LATIN SMALL LETTER
0x027D: 'r', // WITH TAIL, LATIN SMALL LETTER
0x036C: 'r', // , COMBINING LATIN SMALL LETTER
0x0279: 'r', // , LATIN SMALL LETTER TURNED
0x1D63: 'r', // , LATIN SUBSCRIPT SMALL LETTER
0x015B: 's', // WITH ACUTE, LATIN SMALL LETTER
0x0161: 's', // WITH CARON, LATIN SMALL LETTER
0x015F: 's', // WITH CEDILLA, LATIN SMALL LETTER
0x015D: 's', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x0219: 's', // WITH COMMA BELOW, LATIN SMALL LETTER
0x1E61: 's', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E9B: 's', // WITH DOT ABOVE, LATIN SMALL LETTER LONG
0x1E63: 's', // WITH DOT BELOW, LATIN SMALL LETTER
0x0282: 's', // WITH HOOK, LATIN SMALL LETTER
0x023F: 's', // WITH SWASH TAIL, LATIN SMALL LETTER
0x017F: 's', // , LATIN SMALL LETTER LONG
0x00DF: 's', // , LATIN SMALL LETTER SHARP
0x209B: 's', // , LATIN SUBSCRIPT SMALL LETTER
0x0165: 't', // WITH CARON, LATIN SMALL LETTER
0x0163: 't', // WITH CEDILLA, LATIN SMALL LETTER
0x1E71: 't', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x021B: 't', // WITH COMMA BELOW, LATIN SMALL LETTER
0x0236: 't', // WITH CURL, LATIN SMALL LETTER
0x1E97: 't', // WITH DIAERESIS, LATIN SMALL LETTER
0x1E6B: 't', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E6D: 't', // WITH DOT BELOW, LATIN SMALL LETTER
0x01AD: 't', // WITH HOOK, LATIN SMALL LETTER
0x1E6F: 't', // WITH LINE BELOW, LATIN SMALL LETTER
0x01AB: 't', // WITH PALATAL HOOK, LATIN SMALL LETTER
0x0288: 't', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
0x0167: 't', // WITH STROKE, LATIN SMALL LETTER
0x036D: 't', // , COMBINING LATIN SMALL LETTER
0x0287: 't', // , LATIN SMALL LETTER TURNED
0x209C: 't', // , LATIN SUBSCRIPT SMALL LETTER
0x0289: 'u', // BAR, LATIN SMALL LETTER
0x00FA: 'u', // WITH ACUTE, LATIN SMALL LETTER
0x016D: 'u', // WITH BREVE, LATIN SMALL LETTER
0x01D4: 'u', // WITH CARON, LATIN SMALL LETTER
0x1E77: 'u', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER
0x00FB: 'u', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x1E73: 'u', // WITH DIAERESIS BELOW, LATIN SMALL LETTER
0x00FC: 'u', // WITH DIAERESIS, LATIN SMALL LETTER
0x1EE5: 'u', // WITH DOT BELOW, LATIN SMALL LETTER
0x0171: 'u', // WITH DOUBLE ACUTE, LATIN SMALL LETTER
0x0215: 'u', // WITH DOUBLE GRAVE, LATIN SMALL LETTER
0x00F9: 'u', // WITH GRAVE, LATIN SMALL LETTER
0x1EE7: 'u', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x01B0: 'u', // WITH HORN, LATIN SMALL LETTER
0x0217: 'u', // WITH INVERTED BREVE, LATIN SMALL LETTER
0x016B: 'u', // WITH MACRON, LATIN SMALL LETTER
0x0173: 'u', // WITH OGONEK, LATIN SMALL LETTER
0x016F: 'u', // WITH RING ABOVE, LATIN SMALL LETTER
0x1E75: 'u', // WITH TILDE BELOW, LATIN SMALL LETTER
0x0169: 'u', // WITH TILDE, LATIN SMALL LETTER
0x0367: 'u', // , COMBINING LATIN SMALL LETTER
0x1D1D: 'u', // , LATIN SMALL LETTER SIDEWAYS
0x1D1E: 'u', // , LATIN SMALL LETTER SIDEWAYS DIAERESIZED
0x1D64: 'u', // , LATIN SUBSCRIPT SMALL LETTER
0x1E7F: 'v', // WITH DOT BELOW, LATIN SMALL LETTER
0x028B: 'v', // WITH HOOK, LATIN SMALL LETTER
0x1E7D: 'v', // WITH TILDE, LATIN SMALL LETTER
0x036E: 'v', // , COMBINING LATIN SMALL LETTER
0x028C: 'v', // , LATIN SMALL LETTER TURNED
0x1D65: 'v', // , LATIN SUBSCRIPT SMALL LETTER
0x1E83: 'w', // WITH ACUTE, LATIN SMALL LETTER
0x0175: 'w', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x1E85: 'w', // WITH DIAERESIS, LATIN SMALL LETTER
0x1E87: 'w', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E89: 'w', // WITH DOT BELOW, LATIN SMALL LETTER
0x1E81: 'w', // WITH GRAVE, LATIN SMALL LETTER
0x1E98: 'w', // WITH RING ABOVE, LATIN SMALL LETTER
0x028D: 'w', // , LATIN SMALL LETTER TURNED
0x1E8D: 'x', // WITH DIAERESIS, LATIN SMALL LETTER
0x1E8B: 'x', // WITH DOT ABOVE, LATIN SMALL LETTER
0x036F: 'x', // , COMBINING LATIN SMALL LETTER
0x00FD: 'y', // WITH ACUTE, LATIN SMALL LETTER
0x0177: 'y', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x00FF: 'y', // WITH DIAERESIS, LATIN SMALL LETTER
0x1E8F: 'y', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1EF5: 'y', // WITH DOT BELOW, LATIN SMALL LETTER
0x1EF3: 'y', // WITH GRAVE, LATIN SMALL LETTER
0x1EF7: 'y', // WITH HOOK ABOVE, LATIN SMALL LETTER
0x01B4: 'y', // WITH HOOK, LATIN SMALL LETTER
0x0233: 'y', // WITH MACRON, LATIN SMALL LETTER
0x1E99: 'y', // WITH RING ABOVE, LATIN SMALL LETTER
0x024F: 'y', // WITH STROKE, LATIN SMALL LETTER
0x1EF9: 'y', // WITH TILDE, LATIN SMALL LETTER
0x028E: 'y', // , LATIN SMALL LETTER TURNED
0x017A: 'z', // WITH ACUTE, LATIN SMALL LETTER
0x017E: 'z', // WITH CARON, LATIN SMALL LETTER
0x1E91: 'z', // WITH CIRCUMFLEX, LATIN SMALL LETTER
0x0291: 'z', // WITH CURL, LATIN SMALL LETTER
0x017C: 'z', // WITH DOT ABOVE, LATIN SMALL LETTER
0x1E93: 'z', // WITH DOT BELOW, LATIN SMALL LETTER
0x0225: 'z', // WITH HOOK, LATIN SMALL LETTER
0x1E95: 'z', // WITH LINE BELOW, LATIN SMALL LETTER
0x0290: 'z', // WITH RETROFLEX HOOK, LATIN SMALL LETTER
0x01B6: 'z', // WITH STROKE, LATIN SMALL LETTER
0x0240: 'z', // WITH SWASH TAIL, LATIN SMALL LETTER
0x0251: 'a', // , latin small letter script
0x00C1: 'A', // WITH ACUTE, LATIN CAPITAL LETTER
0x00C2: 'A', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
0x00C4: 'A', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x00C0: 'A', // WITH GRAVE, LATIN CAPITAL LETTER
0x00C5: 'A', // WITH RING ABOVE, LATIN CAPITAL LETTER
0x023A: 'A', // WITH STROKE, LATIN CAPITAL LETTER
0x00C3: 'A', // WITH TILDE, LATIN CAPITAL LETTER
0x1D00: 'A', // , LATIN LETTER SMALL CAPITAL
0x0181: 'B', // WITH HOOK, LATIN CAPITAL LETTER
0x0243: 'B', // WITH STROKE, LATIN CAPITAL LETTER
0x0299: 'B', // , LATIN LETTER SMALL CAPITAL
0x1D03: 'B', // , LATIN LETTER SMALL CAPITAL BARRED
0x00C7: 'C', // WITH CEDILLA, LATIN CAPITAL LETTER
0x023B: 'C', // WITH STROKE, LATIN CAPITAL LETTER
0x1D04: 'C', // , LATIN LETTER SMALL CAPITAL
0x018A: 'D', // WITH HOOK, LATIN CAPITAL LETTER
0x0189: 'D', // , LATIN CAPITAL LETTER AFRICAN
0x1D05: 'D', // , LATIN LETTER SMALL CAPITAL
0x00C9: 'E', // WITH ACUTE, LATIN CAPITAL LETTER
0x00CA: 'E', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
0x00CB: 'E', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x00C8: 'E', // WITH GRAVE, LATIN CAPITAL LETTER
0x0246: 'E', // WITH STROKE, LATIN CAPITAL LETTER
0x0190: 'E', // , LATIN CAPITAL LETTER OPEN
0x018E: 'E', // , LATIN CAPITAL LETTER REVERSED
0x1D07: 'E', // , LATIN LETTER SMALL CAPITAL
0x0193: 'G', // WITH HOOK, LATIN CAPITAL LETTER
0x029B: 'G', // WITH HOOK, LATIN LETTER SMALL CAPITAL
0x0262: 'G', // , LATIN LETTER SMALL CAPITAL
0x029C: 'H', // , LATIN LETTER SMALL CAPITAL
0x00CD: 'I', // WITH ACUTE, LATIN CAPITAL LETTER
0x00CE: 'I', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
0x00CF: 'I', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x0130: 'I', // WITH DOT ABOVE, LATIN CAPITAL LETTER
0x00CC: 'I', // WITH GRAVE, LATIN CAPITAL LETTER
0x0197: 'I', // WITH STROKE, LATIN CAPITAL LETTER
0x026A: 'I', // , LATIN LETTER SMALL CAPITAL
0x0248: 'J', // WITH STROKE, LATIN CAPITAL LETTER
0x1D0A: 'J', // , LATIN LETTER SMALL CAPITAL
0x1D0B: 'K', // , LATIN LETTER SMALL CAPITAL
0x023D: 'L', // WITH BAR, LATIN CAPITAL LETTER
0x1D0C: 'L', // WITH STROKE, LATIN LETTER SMALL CAPITAL
0x029F: 'L', // , LATIN LETTER SMALL CAPITAL
0x019C: 'M', // , LATIN CAPITAL LETTER TURNED
0x1D0D: 'M', // , LATIN LETTER SMALL CAPITAL
0x019D: 'N', // WITH LEFT HOOK, LATIN CAPITAL LETTER
0x0220: 'N', // WITH LONG RIGHT LEG, LATIN CAPITAL LETTER
0x00D1: 'N', // WITH TILDE, LATIN CAPITAL LETTER
0x0274: 'N', // , LATIN LETTER SMALL CAPITAL
0x1D0E: 'N', // , LATIN LETTER SMALL CAPITAL REVERSED
0x00D3: 'O', // WITH ACUTE, LATIN CAPITAL LETTER
0x00D4: 'O', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
0x00D6: 'O', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x00D2: 'O', // WITH GRAVE, LATIN CAPITAL LETTER
0x019F: 'O', // WITH MIDDLE TILDE, LATIN CAPITAL LETTER
0x00D8: 'O', // WITH STROKE, LATIN CAPITAL LETTER
0x00D5: 'O', // WITH TILDE, LATIN CAPITAL LETTER
0x0186: 'O', // , LATIN CAPITAL LETTER OPEN
0x1D0F: 'O', // , LATIN LETTER SMALL CAPITAL
0x1D10: 'O', // , LATIN LETTER SMALL CAPITAL OPEN
0x1D18: 'P', // , LATIN LETTER SMALL CAPITAL
0x024A: 'Q', // WITH HOOK TAIL, LATIN CAPITAL LETTER SMALL
0x024C: 'R', // WITH STROKE, LATIN CAPITAL LETTER
0x0280: 'R', // , LATIN LETTER SMALL CAPITAL
0x0281: 'R', // , LATIN LETTER SMALL CAPITAL INVERTED
0x1D19: 'R', // , LATIN LETTER SMALL CAPITAL REVERSED
0x1D1A: 'R', // , LATIN LETTER SMALL CAPITAL TURNED
0x023E: 'T', // WITH DIAGONAL STROKE, LATIN CAPITAL LETTER
0x01AE: 'T', // WITH RETROFLEX HOOK, LATIN CAPITAL LETTER
0x1D1B: 'T', // , LATIN LETTER SMALL CAPITAL
0x0244: 'U', // BAR, LATIN CAPITAL LETTER
0x00DA: 'U', // WITH ACUTE, LATIN CAPITAL LETTER
0x00DB: 'U', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER
0x00DC: 'U', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x00D9: 'U', // WITH GRAVE, LATIN CAPITAL LETTER
0x1D1C: 'U', // , LATIN LETTER SMALL CAPITAL
0x01B2: 'V', // WITH HOOK, LATIN CAPITAL LETTER
0x0245: 'V', // , LATIN CAPITAL LETTER TURNED
0x1D20: 'V', // , LATIN LETTER SMALL CAPITAL
0x1D21: 'W', // , LATIN LETTER SMALL CAPITAL
0x00DD: 'Y', // WITH ACUTE, LATIN CAPITAL LETTER
0x0178: 'Y', // WITH DIAERESIS, LATIN CAPITAL LETTER
0x024E: 'Y', // WITH STROKE, LATIN CAPITAL LETTER
0x028F: 'Y', // , LATIN LETTER SMALL CAPITAL
0x1D22: 'Z', // , LATIN LETTER SMALL CAPITAL
}
// NormalizeRunes normalizes latin script letters
func NormalizeRunes(runes []rune) []rune {
ret := make([]rune, len(runes))
copy(ret, runes)
for idx, r := range runes {
if r < 0x00C0 || r > 0x2184 {
continue
}
n := normalized[r]
if n > 0 {
ret[idx] = normalized[r]
}
}
return ret
}

View File

@@ -6,6 +6,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/junegunn/fzf/src/tui"
) )
type ansiOffset struct { type ansiOffset struct {
@@ -14,29 +16,52 @@ type ansiOffset struct {
} }
type ansiState struct { type ansiState struct {
fg int fg tui.Color
bg int bg tui.Color
bold bool attr tui.Attr
} }
func (s *ansiState) colored() bool { func (s *ansiState) colored() bool {
return s.fg != -1 || s.bg != -1 || s.bold return s.fg != -1 || s.bg != -1 || s.attr > 0
} }
func (s *ansiState) equals(t *ansiState) bool { func (s *ansiState) equals(t *ansiState) bool {
if t == nil { if t == nil {
return !s.colored() return !s.colored()
} }
return s.fg == t.fg && s.bg == t.bg && s.bold == t.bold return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr
} }
var ansiRegex *regexp.Regexp var ansiRegex *regexp.Regexp
func init() { func init() {
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]") /*
References:
- https://github.com/gnachman/iTerm2
- http://ascii-table.com/ansi-escape-sequences.php
- http://ascii-table.com/ansi-escape-sequences-vt-100.php
- http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
*/
// The following regular expression will include not all but most of the
// frequently used ANSI sequences
ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x0e\x0f]|.\x08)")
} }
func extractColor(str string, state *ansiState) (string, []ansiOffset, *ansiState) { func findAnsiStart(str string) int {
idx := 0
for ; idx < len(str); idx++ {
b := str[idx]
if b == 0x1b || b == 0x0e || b == 0x0f {
return idx
}
if b == 0x08 && idx > 0 {
return idx - 1
}
}
return idx
}
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
var offsets []ansiOffset var offsets []ansiOffset
var output bytes.Buffer var output bytes.Buffer
@@ -44,51 +69,87 @@ func extractColor(str string, state *ansiState) (string, []ansiOffset, *ansiStat
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state}) offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
} }
idx := 0 prevIdx := 0
for _, offset := range ansiRegex.FindAllStringIndex(str, -1) { runeCount := 0
output.WriteString(str[idx:offset[0]]) for idx := 0; idx < len(str); {
newState := interpretCode(str[offset[0]:offset[1]], state) idx += findAnsiStart(str[idx:])
if idx == len(str) {
break
}
// Make sure that we found an ANSI code
offset := ansiRegex.FindStringIndex(str[idx:])
if len(offset) < 2 {
idx++
continue
}
offset[0] += idx
offset[1] += idx
idx = offset[1]
// Check if we should continue
prev := str[prevIdx:offset[0]]
if proc != nil && !proc(prev, state) {
return "", nil, nil
}
prevIdx = offset[1]
runeCount += utf8.RuneCountInString(prev)
output.WriteString(prev)
newState := interpretCode(str[offset[0]:offset[1]], state)
if !newState.equals(state) { if !newState.equals(state) {
if state != nil { if state != nil {
// Update last offset // Update last offset
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes())) (&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
} }
if newState.colored() { if newState.colored() {
// Append new offset // Append new offset
state = newState state = newState
newLen := int32(utf8.RuneCount(output.Bytes())) offsets = append(offsets, ansiOffset{[2]int32{int32(runeCount), int32(runeCount)}, *state})
offsets = append(offsets, ansiOffset{[2]int32{newLen, newLen}, *state})
} else { } else {
// Discard state // Discard state
state = nil state = nil
} }
} }
idx = offset[1]
} }
rest := str[idx:] var rest string
if len(rest) > 0 { var trimmed string
if prevIdx == 0 {
// No ANSI code found
rest = str
trimmed = str
} else {
rest = str[prevIdx:]
output.WriteString(rest) output.WriteString(rest)
if state != nil { trimmed = output.String()
// Update last offset
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
}
} }
return output.String(), offsets, state if len(rest) > 0 && state != nil {
// Update last offset
runeCount += utf8.RuneCountInString(rest)
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
}
if proc != nil {
proc(rest, state)
}
if len(offsets) == 0 {
return trimmed, nil, state
}
return trimmed, &offsets, state
} }
func interpretCode(ansiCode string, prevState *ansiState) *ansiState { func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
// State // State
var state *ansiState var state *ansiState
if prevState == nil { if prevState == nil {
state = &ansiState{-1, -1, false} state = &ansiState{-1, -1, 0}
} else { } else {
state = &ansiState{prevState.fg, prevState.bg, prevState.bold} state = &ansiState{prevState.fg, prevState.bg, prevState.attr}
} }
if ansiCode[len(ansiCode)-1] == 'K' { if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
return state return state
} }
@@ -98,7 +159,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
init := func() { init := func() {
state.fg = -1 state.fg = -1
state.bg = -1 state.bg = -1
state.bold = false state.attr = 0
state256 = 0 state256 = 0
} }
@@ -122,28 +183,56 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
case 49: case 49:
state.bg = -1 state.bg = -1
case 1: case 1:
state.bold = true state.attr = state.attr | tui.Bold
case 2:
state.attr = state.attr | tui.Dim
case 3:
state.attr = state.attr | tui.Italic
case 4:
state.attr = state.attr | tui.Underline
case 5:
state.attr = state.attr | tui.Blink
case 7:
state.attr = state.attr | tui.Reverse
case 0: case 0:
init() init()
default: default:
if num >= 30 && num <= 37 { if num >= 30 && num <= 37 {
state.fg = num - 30 state.fg = tui.Color(num - 30)
} else if num >= 40 && num <= 47 { } else if num >= 40 && num <= 47 {
state.bg = num - 40 state.bg = tui.Color(num - 40)
} else if num >= 90 && num <= 97 {
state.fg = tui.Color(num - 90 + 8)
} else if num >= 100 && num <= 107 {
state.bg = tui.Color(num - 100 + 8)
} }
} }
case 1: case 1:
switch num { switch num {
case 2:
state256 = 10 // MAGIC
case 5: case 5:
state256++ state256++
default: default:
state256 = 0 state256 = 0
} }
case 2: case 2:
*ptr = num *ptr = tui.Color(num)
state256 = 0
case 10:
*ptr = tui.Color(1<<24) | tui.Color(num<<16)
state256++
case 11:
*ptr = *ptr | tui.Color(num<<8)
state256++
case 12:
*ptr = *ptr | tui.Color(num)
state256 = 0 state256 = 0
} }
} }
} }
if state256 > 0 {
*ptr = -1
}
return state return state
} }

View File

@@ -3,150 +3,156 @@ package fzf
import ( import (
"fmt" "fmt"
"testing" "testing"
"github.com/junegunn/fzf/src/tui"
) )
func TestExtractColor(t *testing.T) { func TestExtractColor(t *testing.T) {
assert := func(offset ansiOffset, b int32, e int32, fg int, bg int, bold bool) { assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) {
var attr tui.Attr
if bold {
attr = tui.Bold
}
if offset.offset[0] != b || offset.offset[1] != e || if offset.offset[0] != b || offset.offset[1] != e ||
offset.color.fg != fg || offset.color.bg != bg || offset.color.bold != bold { offset.color.fg != fg || offset.color.bg != bg || offset.color.attr != attr {
t.Error(offset, b, e, fg, bg, bold) t.Error(offset, b, e, fg, bg, attr)
} }
} }
src := "hello world" src := "hello world"
var state *ansiState var state *ansiState
clean := "\x1b[0m" clean := "\x1b[0m"
check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) { check := func(assertion func(ansiOffsets *[]ansiOffset, state *ansiState)) {
output, ansiOffsets, newState := extractColor(src, state) output, ansiOffsets, newState := extractColor(src, state, nil)
state = newState state = newState
if output != "hello world" { if output != "hello world" {
t.Errorf("Invalid output: {}", output) t.Errorf("Invalid output: %s %s", output, []rune(output))
} }
fmt.Println(src, ansiOffsets, clean) fmt.Println(src, ansiOffsets, clean)
assertion(ansiOffsets, state) assertion(ansiOffsets, state)
} }
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) > 0 { if offsets != nil {
t.Fail() t.Fail()
} }
}) })
state = nil state = nil
src = "\x1b[0mhello world" src = "\x1b[0mhello world"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) > 0 { if offsets != nil {
t.Fail() t.Fail()
} }
}) })
state = nil state = nil
src = "\x1b[1mhello world" src = "\x1b[1mhello world"
check(func(offsets []ansiOffset, state *ansiState) { 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 state = nil
src = "\x1b[1mhello \x1b[mworld" src = "\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d"
check(func(offsets []ansiOffset, state *ansiState) { 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 state = nil
src = "\x1b[1mhello \x1b[Kworld" src = "\x1b[1mhello \x1b[Kworld"
check(func(offsets []ansiOffset, state *ansiState) { 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 state = nil
src = "hello \x1b[34;45;1mworld" src = "hello \x1b[34;45;1mworld"
check(func(offsets []ansiOffset, state *ansiState) { 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 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, state *ansiState) { 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 state = nil
src = "hello \x1b[34;45;1mwor\x1b[0mld" src = "hello \x1b[34;45;1mwor\x1b[0mld"
check(func(offsets []ansiOffset, state *ansiState) { 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 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, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 3 { if len(*offsets) != 3 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 8, 4, 233, true) assert((*offsets)[0], 6, 8, 4, 233, true)
assert(offsets[1], 8, 9, 161, 233, true) assert((*offsets)[1], 8, 9, 161, 233, true)
assert(offsets[2], 10, 11, 161, -1, false) assert((*offsets)[2], 10, 11, 161, -1, false)
}) })
// {38,48};5;{38,48} // {38,48};5;{38,48}
state = nil 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, state *ansiState) { 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" src = "hello \x1b[32;1mworld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
if state.fg != 2 || state.bg != -1 || !state.bold { if state.fg != 2 || state.bg != -1 || state.attr == 0 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 11, 2, -1, true) assert((*offsets)[0], 6, 11, 2, -1, true)
}) })
src = "hello world" src = "hello world"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
if state.fg != 2 || state.bg != -1 || !state.bold { if state.fg != 2 || state.bg != -1 || state.attr == 0 {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 11, 2, -1, true) assert((*offsets)[0], 0, 11, 2, -1, true)
}) })
src = "hello \x1b[0;38;5;200;48;5;100mworld" src = "hello \x1b[0;38;5;200;48;5;100mworld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 2 { if len(*offsets) != 2 {
t.Fail() t.Fail()
} }
if state.fg != 200 || state.bg != 100 || state.bold { if state.fg != 200 || state.bg != 100 || state.attr > 0 {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 6, 2, -1, true) assert((*offsets)[0], 0, 6, 2, -1, true)
assert(offsets[1], 6, 11, 200, 100, false) assert((*offsets)[1], 6, 11, 200, 100, false)
}) })
} }

View File

@@ -3,7 +3,7 @@ package fzf
import "sync" import "sync"
// queryCache associates strings to lists of items // queryCache associates strings to lists of items
type queryCache map[string][]*Item type queryCache map[string][]Result
// ChunkCache associates Chunk and query string to lists of items // ChunkCache associates Chunk and query string to lists of items
type ChunkCache struct { type ChunkCache struct {
@@ -17,7 +17,7 @@ func NewChunkCache() ChunkCache {
} }
// Add adds the list to the cache // Add adds the list to the cache
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) { func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) {
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax { if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
return return
} }
@@ -33,10 +33,10 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) {
(*qc)[key] = list (*qc)[key] = list
} }
// Find is called to lookup ChunkCache // Lookup is called to lookup ChunkCache
func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Item, bool) { func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []Result {
if len(key) == 0 || !chunk.IsFull() { if len(key) == 0 || !chunk.IsFull() {
return nil, false return nil
} }
cc.mutex.Lock() cc.mutex.Lock()
@@ -46,8 +46,36 @@ func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Item, bool) {
if ok { if ok {
list, ok := (*qc)[key] list, ok := (*qc)[key]
if ok { if ok {
return list, true return list
} }
} }
return nil, false return nil
}
func (cc *ChunkCache) Search(chunk *Chunk, key string) []Result {
if len(key) == 0 || !chunk.IsFull() {
return nil
}
cc.mutex.Lock()
defer cc.mutex.Unlock()
qc, ok := cc.cache[chunk]
if !ok {
return nil
}
for idx := 1; idx < len(key); idx++ {
// [---------| ] | [ |---------]
// [--------| ] | [ |--------]
// [-------| ] | [ |-------]
prefix := key[:len(key)-idx]
suffix := key[idx:]
for _, substr := range [2]string{prefix, suffix} {
if cached, found := (*qc)[substr]; found {
return cached
}
}
}
return nil
} }

View File

@@ -4,37 +4,36 @@ import "testing"
func TestChunkCache(t *testing.T) { func TestChunkCache(t *testing.T) {
cache := NewChunkCache() cache := NewChunkCache()
chunk2 := make(Chunk, chunkSize)
chunk1p := &Chunk{} chunk1p := &Chunk{}
chunk2p := &chunk2 chunk2p := &Chunk{count: chunkSize}
items1 := []*Item{&Item{}} items1 := []Result{Result{}}
items2 := []*Item{&Item{}, &Item{}} items2 := []Result{Result{}, Result{}}
cache.Add(chunk1p, "foo", items1) cache.Add(chunk1p, "foo", items1)
cache.Add(chunk2p, "foo", items1) cache.Add(chunk2p, "foo", items1)
cache.Add(chunk2p, "bar", items2) cache.Add(chunk2p, "bar", items2)
{ // chunk1 is not full { // chunk1 is not full
cached, found := cache.Find(chunk1p, "foo") cached := cache.Lookup(chunk1p, "foo")
if found { if cached != nil {
t.Error("Cached disabled for non-empty chunks", found, cached) t.Error("Cached disabled for non-empty chunks", cached)
} }
} }
{ {
cached, found := cache.Find(chunk2p, "foo") cached := cache.Lookup(chunk2p, "foo")
if !found || len(cached) != 1 { if cached == nil || len(cached) != 1 {
t.Error("Expected 1 item cached", found, cached) t.Error("Expected 1 item cached", cached)
} }
} }
{ {
cached, found := cache.Find(chunk2p, "bar") cached := cache.Lookup(chunk2p, "bar")
if !found || len(cached) != 2 { if cached == nil || len(cached) != 2 {
t.Error("Expected 2 items cached", found, cached) t.Error("Expected 2 items cached", cached)
} }
} }
{ {
cached, found := cache.Find(chunk1p, "foobar") cached := cache.Lookup(chunk1p, "foobar")
if found { if cached != nil {
t.Error("Expected 0 item cached", found, cached) t.Error("Expected 0 item cached", cached)
} }
} }
} }

View File

@@ -2,17 +2,18 @@ package fzf
import "sync" import "sync"
// Chunk is a list of Item pointers whose size has the upper limit of chunkSize // Chunk is a list of Items whose size has the upper limit of chunkSize
type Chunk []*Item // >>> []Item type Chunk struct {
items [chunkSize]Item
count int
}
// ItemBuilder is a closure type that builds Item object from a pointer to a // ItemBuilder is a closure type that builds Item object from byte array
// string and an integer type ItemBuilder func(*Item, []byte) bool
type ItemBuilder func([]byte, int) *Item
// ChunkList is a list of Chunks // ChunkList is a list of Chunks
type ChunkList struct { type ChunkList struct {
chunks []*Chunk chunks []*Chunk
count int
mutex sync.Mutex mutex sync.Mutex
trans ItemBuilder trans ItemBuilder
} }
@@ -21,15 +22,13 @@ type ChunkList struct {
func NewChunkList(trans ItemBuilder) *ChunkList { func NewChunkList(trans ItemBuilder) *ChunkList {
return &ChunkList{ return &ChunkList{
chunks: []*Chunk{}, chunks: []*Chunk{},
count: 0,
mutex: sync.Mutex{}, mutex: sync.Mutex{},
trans: trans} trans: trans}
} }
func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool { func (c *Chunk) push(trans ItemBuilder, data []byte) bool {
item := trans(data, index) if trans(&c.items[c.count], data) {
if item != nil { c.count++
*c = append(*c, item)
return true return true
} }
return false return false
@@ -37,7 +36,7 @@ func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool {
// IsFull returns true if the Chunk is full // IsFull returns true if the Chunk is full
func (c *Chunk) IsFull() bool { func (c *Chunk) IsFull() bool {
return len(*c) == chunkSize return c.count == chunkSize
} }
func (cl *ChunkList) lastChunk() *Chunk { func (cl *ChunkList) lastChunk() *Chunk {
@@ -49,45 +48,35 @@ func CountItems(cs []*Chunk) int {
if len(cs) == 0 { if len(cs) == 0 {
return 0 return 0
} }
return chunkSize*(len(cs)-1) + len(*(cs[len(cs)-1])) return chunkSize*(len(cs)-1) + cs[len(cs)-1].count
} }
// Push adds the item to the list // Push adds the item to the list
func (cl *ChunkList) Push(data []byte) bool { func (cl *ChunkList) Push(data []byte) bool {
cl.mutex.Lock() cl.mutex.Lock()
defer cl.mutex.Unlock()
if len(cl.chunks) == 0 || cl.lastChunk().IsFull() { if len(cl.chunks) == 0 || cl.lastChunk().IsFull() {
newChunk := Chunk(make([]*Item, 0, chunkSize)) cl.chunks = append(cl.chunks, &Chunk{})
cl.chunks = append(cl.chunks, &newChunk)
} }
if cl.lastChunk().push(cl.trans, data, cl.count) { ret := cl.lastChunk().push(cl.trans, data)
cl.count++ cl.mutex.Unlock()
return true return ret
}
return false
} }
// Snapshot returns immutable snapshot of the ChunkList // Snapshot returns immutable snapshot of the ChunkList
func (cl *ChunkList) Snapshot() ([]*Chunk, int) { func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
cl.mutex.Lock() cl.mutex.Lock()
defer cl.mutex.Unlock()
ret := make([]*Chunk, len(cl.chunks)) ret := make([]*Chunk, len(cl.chunks))
copy(ret, cl.chunks) copy(ret, cl.chunks)
// Duplicate the last chunk // Duplicate the last chunk
if cnt := len(ret); cnt > 0 { if cnt := len(ret); cnt > 0 {
ret[cnt-1] = ret[cnt-1].dupe() newChunk := *ret[cnt-1]
ret[cnt-1] = &newChunk
} }
return ret, cl.count
}
func (c *Chunk) dupe() *Chunk { cl.mutex.Unlock()
newChunk := make(Chunk, len(*c)) return ret, CountItems(ret)
for idx, ptr := range *c {
newChunk[idx] = ptr
}
return &newChunk
} }

View File

@@ -3,14 +3,17 @@ package fzf
import ( import (
"fmt" "fmt"
"testing" "testing"
"github.com/junegunn/fzf/src/util"
) )
func TestChunkList(t *testing.T) { func TestChunkList(t *testing.T) {
// FIXME global // FIXME global
sortCriteria = []criterion{byMatchLen, byLength} sortCriteria = []criterion{byScore, byLength}
cl := NewChunkList(func(s []byte, i int) *Item { cl := NewChunkList(func(item *Item, s []byte) bool {
return &Item{text: []rune(string(s)), rank: buildEmptyRank(int32(i * 2))} item.text = util.ToChars(s)
return true
}) })
// Snapshot // Snapshot
@@ -36,14 +39,11 @@ func TestChunkList(t *testing.T) {
// Check the content of the ChunkList // Check the content of the ChunkList
chunk1 := snapshot[0] chunk1 := snapshot[0]
if len(*chunk1) != 2 { if chunk1.count != 2 {
t.Error("Snapshot should contain only two items") t.Error("Snapshot should contain only two items")
} }
last := func(arr [5]int32) int32 { if chunk1.items[0].text.ToString() != "hello" ||
return arr[len(arr)-1] chunk1.items[1].text.ToString() != "world" {
}
if string((*chunk1)[0].text) != "hello" || last((*chunk1)[0].rank) != 0 ||
string((*chunk1)[1].text) != "world" || last((*chunk1)[1].rank) != 2 {
t.Error("Invalid data") t.Error("Invalid data")
} }
if chunk1.IsFull() { if chunk1.IsFull() {
@@ -66,14 +66,14 @@ func TestChunkList(t *testing.T) {
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 { !snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
t.Error("Expected two full chunks and one more chunk") t.Error("Expected two full chunks and one more chunk")
} }
if len(*snapshot[2]) != 2 { if snapshot[2].count != 2 {
t.Error("Unexpected number of items") t.Error("Unexpected number of items")
} }
cl.Push([]byte("hello")) cl.Push([]byte("hello"))
cl.Push([]byte("world")) cl.Push([]byte("world"))
lastChunkCount := len(*snapshot[len(snapshot)-1]) lastChunkCount := snapshot[len(snapshot)-1].count
if lastChunkCount != 2 { if lastChunkCount != 2 {
t.Error("Unexpected number of items:", lastChunkCount) t.Error("Unexpected number of items:", lastChunkCount)
} }

View File

@@ -1,6 +1,7 @@
package fzf package fzf
import ( import (
"os"
"time" "time"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
@@ -8,14 +9,17 @@ import (
const ( const (
// Current version // Current version
version = "0.13.0" version = "0.17.3"
// 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 | sed s/^..//` readerBufferSize = 64 * 1024
readerPollIntervalMin = 10 * time.Millisecond
readerPollIntervalStep = 5 * time.Millisecond
readerPollIntervalMax = 50 * time.Millisecond
// Terminal // Terminal
initialDelay = 20 * time.Millisecond initialDelay = 20 * time.Millisecond
@@ -23,11 +27,17 @@ const (
spinnerDuration = 200 * time.Millisecond spinnerDuration = 200 * time.Millisecond
// Matcher // Matcher
progressMinDuration = 200 * time.Millisecond numPartitionsMultiplier = 8
maxPartitions = 32
progressMinDuration = 200 * time.Millisecond
// Capacity of each chunk // Capacity of each chunk
chunkSize int = 100 chunkSize int = 100
// Pre-allocated memory slices to minimize GC
slab16Size int = 100 * 1024 // 200KB * 32 = 12.8MB
slab32Size int = 2048 // 8KB * 32 = 256KB
// Do not cache results of low selectivity queries // Do not cache results of low selectivity queries
queryCacheMax int = chunkSize / 5 queryCacheMax int = chunkSize / 5
@@ -41,6 +51,18 @@ const (
defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+" defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+"
) )
var defaultCommand string
func init() {
if !util.IsWindows() {
defaultCommand = `set -o pipefail; command find -L . -mindepth 1 \( -path '*/\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-`
} else if os.Getenv("TERM") == "cygwin" {
defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"`
} else {
defaultCommand = `dir /s/b`
}
}
// fzf events // fzf events
const ( const (
EvtReadNew util.EventType = iota EvtReadNew util.EventType = iota
@@ -49,7 +71,7 @@ const (
EvtSearchProgress EvtSearchProgress
EvtSearchFin EvtSearchFin
EvtHeader EvtHeader
EvtClose EvtReady
) )
const ( const (

View File

@@ -3,7 +3,7 @@ Package fzf implements fzf, a command-line fuzzy finder.
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Junegunn Choi Copyright (c) 2017 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -28,16 +28,11 @@ package fzf
import ( import (
"fmt" "fmt"
"os" "os"
"runtime"
"time" "time"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
func initProcs() {
runtime.GOMAXPROCS(runtime.NumCPU())
}
/* /*
Reader -> EvtReadFin Reader -> EvtReadFin
Reader -> EvtReadNew -> Matcher (restart) Reader -> EvtReadNew -> Matcher (restart)
@@ -48,14 +43,16 @@ Matcher -> EvtHeader -> Terminal (update header)
*/ */
// Run starts fzf // Run starts fzf
func Run(opts *Options) { func Run(opts *Options, revision string) {
initProcs()
sort := opts.Sort > 0 sort := opts.Sort > 0
sortCriteria = opts.Criteria sortCriteria = opts.Criteria
if opts.Version { if opts.Version {
fmt.Println(version) if len(revision) > 0 {
fmt.Printf("%s (%s)\n", version, revision)
} else {
fmt.Println(version)
}
os.Exit(exitOk) os.Exit(exitOk)
} }
@@ -63,78 +60,67 @@ func Run(opts *Options) {
eventBox := util.NewEventBox() eventBox := util.NewEventBox()
// ANSI code processor // ANSI code processor
ansiProcessor := func(data []byte) ([]rune, []ansiOffset) { ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
return util.BytesToRunes(data), nil return util.ToChars(data), nil
}
ansiProcessorRunes := func(data []rune) ([]rune, []ansiOffset) {
return data, nil
} }
if opts.Ansi { if opts.Ansi {
if opts.Theme != nil { if opts.Theme != nil {
var state *ansiState var state *ansiState
ansiProcessor = func(data []byte) ([]rune, []ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, offsets, newState := extractColor(string(data), state) trimmed, offsets, newState := extractColor(string(data), state, nil)
state = newState state = newState
return []rune(trimmed), offsets return util.ToChars([]byte(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 []byte) ([]rune, []ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(string(data), nil) trimmed, _, _ := extractColor(string(data), nil, nil)
return []rune(trimmed), nil return util.ToChars([]byte(trimmed)), nil
} }
} }
ansiProcessorRunes = func(data []rune) ([]rune, []ansiOffset) {
return ansiProcessor([]byte(string(data)))
}
} }
// Chunk list // Chunk list
var chunkList *ChunkList var chunkList *ChunkList
var itemIndex int32
header := make([]string, 0, opts.HeaderLines) header := make([]string, 0, opts.HeaderLines)
if len(opts.WithNth) == 0 { if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(data []byte, index int) *Item { chunkList = NewChunkList(func(item *Item, data []byte) bool {
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, string(data)) header = append(header, string(data))
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return nil return false
} }
runes, colors := ansiProcessor(data) item.text, item.colors = ansiProcessor(data)
return &Item{ item.text.Index = itemIndex
text: runes, itemIndex++
colors: colors, return true
rank: buildEmptyRank(int32(index))}
}) })
} else { } else {
chunkList = NewChunkList(func(data []byte, index int) *Item { chunkList = NewChunkList(func(item *Item, data []byte) bool {
runes := util.BytesToRunes(data) tokens := Tokenize(string(data), opts.Delimiter)
tokens := Tokenize(runes, opts.Delimiter)
trans := Transform(tokens, opts.WithNth) trans := Transform(tokens, opts.WithNth)
transformed := joinTokens(trans)
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, string(joinTokens(trans))) header = append(header, transformed)
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return nil return false
} }
item := Item{ item.text, item.colors = ansiProcessor([]byte(transformed))
text: joinTokens(trans), item.text.Index = itemIndex
origText: &runes, item.origText = &data
colors: nil, itemIndex++
rank: buildEmptyRank(int32(index))} return true
trimmed, colors := ansiProcessorRunes(item.text)
item.text = trimmed
item.colors = colors
return &item
}) })
} }
// 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(data []byte) bool { reader := NewReader(func(data []byte) bool {
return chunkList.Push(data) return chunkList.Push(data)
}, eventBox, opts.ReadZero} }, eventBox, opts.ReadZero)
go reader.ReadSource() go reader.ReadSource()
} }
@@ -151,30 +137,33 @@ func Run(opts *Options) {
} }
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern( return BuildPattern(
opts.Fuzzy, opts.Extended, opts.Case, forward, opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward,
opts.Nth, opts.Delimiter, runes) opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
} }
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
// Filtering mode // Filtering mode
if opts.Filter != nil { if opts.Filter != nil {
if opts.PrintQuery { if opts.PrintQuery {
fmt.Println(*opts.Filter) opts.Printer(*opts.Filter)
} }
pattern := patternBuilder([]rune(*opts.Filter)) pattern := patternBuilder([]rune(*opts.Filter))
found := false found := false
if streamingFilter { if streamingFilter {
reader := Reader{ slab := util.MakeSlab(slab16Size, slab32Size)
reader := NewReader(
func(runes []byte) bool { func(runes []byte) bool {
item := chunkList.trans(runes, 0) item := Item{}
if item != nil && pattern.MatchItem(item) { if chunkList.trans(&item, runes) {
fmt.Println(string(item.text)) if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
found = true opts.Printer(item.text.ToString())
found = true
}
} }
return false return false
}, eventBox, opts.ReadZero} }, eventBox, opts.ReadZero)
reader.ReadSource() reader.ReadSource()
} else { } else {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
@@ -185,7 +174,7 @@ func Run(opts *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(opts.Ansi)) opts.Printer(merger.Get(i).item.AsString(opts.Ansi))
found = true found = true
} }
} }
@@ -220,14 +209,19 @@ func Run(opts *Options) {
delay := true delay := true
ticks++ ticks++
eventBox.Wait(func(events *util.Events) { eventBox.Wait(func(events *util.Events) {
defer events.Clear() if _, fin := (*events)[EvtReadFin]; fin {
delete(*events, EvtReadNew)
}
for evt, value := range *events { for evt, value := range *events {
switch evt { switch evt {
case EvtReadNew, EvtReadFin: case EvtReadNew, EvtReadFin:
reading = reading && evt == EvtReadNew reading = reading && evt == EvtReadNew
snapshot, count := chunkList.Snapshot() snapshot, count := chunkList.Snapshot()
terminal.UpdateCount(count, !reading) terminal.UpdateCount(count, !reading, value.(bool))
if opts.Sync {
terminal.UpdateList(PassMerger(&snapshot, opts.Tac))
}
matcher.Reset(snapshot, terminal.Input(), false, !reading, sort) matcher.Reset(snapshot, terminal.Input(), false, !reading, sort)
case EvtSearchNew: case EvtSearchNew:
@@ -259,13 +253,13 @@ func Run(opts *Options) {
} else if val.final { } else if val.final {
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 { if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
if opts.PrintQuery { if opts.PrintQuery {
fmt.Println(opts.Query) opts.Printer(opts.Query)
} }
if len(opts.Expect) > 0 { if len(opts.Expect) > 0 {
fmt.Println() opts.Printer("")
} }
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
fmt.Println(val.Get(i).AsString(opts.Ansi)) opts.Printer(val.Get(i).item.AsString(opts.Ansi))
} }
if count > 0 { if count > 0 {
os.Exit(exitOk) os.Exit(exitOk)
@@ -280,6 +274,7 @@ func Run(opts *Options) {
} }
} }
} }
events.Clear()
}) })
if delay && reading { if delay && reading {
dur := util.DurWithin( dur := util.DurWithin(

View File

@@ -1,704 +0,0 @@
package curses
/*
#include <ncurses.h>
#include <locale.h>
#cgo !static LDFLAGS: -lncurses
#cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl
#cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch
SCREEN *c_newterm () {
return newterm(NULL, stderr, stdin);
}
*/
import "C"
import (
"fmt"
"os"
"strings"
"syscall"
"time"
"unicode/utf8"
)
// Types of user action
const (
Rune = iota
CtrlA
CtrlB
CtrlC
CtrlD
CtrlE
CtrlF
CtrlG
CtrlH
Tab
CtrlJ
CtrlK
CtrlL
CtrlM
CtrlN
CtrlO
CtrlP
CtrlQ
CtrlR
CtrlS
CtrlT
CtrlU
CtrlV
CtrlW
CtrlX
CtrlY
CtrlZ
ESC
Invalid
Mouse
DoubleClick
BTab
BSpace
Del
PgUp
PgDn
Up
Down
Left
Right
Home
End
SLeft
SRight
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
AltEnter
AltSpace
AltSlash
AltBS
AltA
AltB
AltC
AltD
AltE
AltF
AltZ = AltA + 'z' - 'a'
)
// Pallete
const (
ColNormal = iota
ColPrompt
ColMatch
ColCurrent
ColCurrentMatch
ColSpinner
ColInfo
ColCursor
ColSelected
ColHeader
ColBorder
ColUser // Should be the last entry
)
const (
doubleClickDuration = 500 * time.Millisecond
colDefault = -1
colUndefined = -2
)
type ColorTheme struct {
UseDefault bool
Fg int16
Bg int16
DarkBg int16
Prompt int16
Match int16
Current int16
CurrentMatch int16
Spinner int16
Info int16
Cursor int16
Selected int16
Header int16
Border int16
}
type Event struct {
Type int
Char rune
MouseEvent *MouseEvent
}
type MouseEvent struct {
Y int
X int
S int
Down bool
Double bool
Mod bool
}
var (
_buf []byte
_in *os.File
_color func(int, bool) C.int
_colorMap map[int]int
_prevDownTime time.Time
_clickY []int
_screen *C.SCREEN
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
FG int
CurrentFG int
BG int
DarkBG int
)
type Window struct {
win *C.WINDOW
Top int
Left int
Width int
Height int
}
func NewWindow(top int, left int, width int, height int, border bool) *Window {
win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
if border {
attr := _color(ColBorder, false)
C.wattron(win, attr)
C.box(win, 0, 0)
C.wattroff(win, attr)
}
return &Window{
win: win,
Top: top,
Left: left,
Width: width,
Height: height,
}
}
func EmptyTheme() *ColorTheme {
return &ColorTheme{
UseDefault: true,
Fg: colUndefined,
Bg: colUndefined,
DarkBg: colUndefined,
Prompt: colUndefined,
Match: colUndefined,
Current: colUndefined,
CurrentMatch: colUndefined,
Spinner: colUndefined,
Info: colUndefined,
Cursor: colUndefined,
Selected: colUndefined,
Header: colUndefined,
Border: colUndefined}
}
func init() {
_prevDownTime = time.Unix(0, 0)
_clickY = []int{}
_colorMap = make(map[int]int)
Default16 = &ColorTheme{
UseDefault: true,
Fg: 15,
Bg: 0,
DarkBg: C.COLOR_BLACK,
Prompt: C.COLOR_BLUE,
Match: C.COLOR_GREEN,
Current: C.COLOR_YELLOW,
CurrentMatch: C.COLOR_GREEN,
Spinner: C.COLOR_GREEN,
Info: C.COLOR_WHITE,
Cursor: C.COLOR_RED,
Selected: C.COLOR_MAGENTA,
Header: C.COLOR_CYAN,
Border: C.COLOR_BLACK}
Dark256 = &ColorTheme{
UseDefault: true,
Fg: 15,
Bg: 0,
DarkBg: 236,
Prompt: 110,
Match: 108,
Current: 254,
CurrentMatch: 151,
Spinner: 148,
Info: 144,
Cursor: 161,
Selected: 168,
Header: 109,
Border: 59}
Light256 = &ColorTheme{
UseDefault: true,
Fg: 15,
Bg: 0,
DarkBg: 251,
Prompt: 25,
Match: 66,
Current: 237,
CurrentMatch: 23,
Spinner: 65,
Info: 101,
Cursor: 161,
Selected: 168,
Header: 31,
Border: 145}
}
func attrColored(pair int, bold bool) C.int {
var attr C.int
if pair > ColNormal {
attr = C.COLOR_PAIR(C.int(pair))
}
if bold {
attr = attr | C.A_BOLD
}
return attr
}
func attrMono(pair int, bold bool) C.int {
var attr C.int
switch pair {
case ColCurrent:
if bold {
attr = C.A_REVERSE
}
case ColMatch:
attr = C.A_UNDERLINE
case ColCurrentMatch:
attr = C.A_UNDERLINE | C.A_REVERSE
}
if bold {
attr = attr | C.A_BOLD
}
return attr
}
func MaxX() int {
return int(C.COLS)
}
func MaxY() int {
return int(C.LINES)
}
func getch(nonblock bool) int {
b := make([]byte, 1)
syscall.SetNonblock(int(_in.Fd()), nonblock)
_, err := _in.Read(b)
if err != nil {
return -1
}
return int(b[0])
}
func Init(theme *ColorTheme, black bool, mouse bool) {
{
in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0)
if err != nil {
panic("Failed to open /dev/tty")
}
_in = in
// Break STDIN
// syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd()))
}
C.setlocale(C.LC_ALL, C.CString(""))
_screen = C.c_newterm()
if _screen == nil {
fmt.Println("Invalid $TERM: " + os.Getenv("TERM"))
os.Exit(2)
}
C.set_term(_screen)
if mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil)
}
C.noecho()
C.raw() // stty dsusp undef
if theme != nil {
C.start_color()
var baseTheme *ColorTheme
if C.tigetnum(C.CString("colors")) >= 256 {
baseTheme = Dark256
} else {
baseTheme = Default16
}
initPairs(baseTheme, theme, black)
_color = attrColored
} else {
_color = attrMono
}
}
func override(a int16, b int16) C.short {
if b == colUndefined {
return C.short(a)
}
return C.short(b)
}
func initPairs(baseTheme *ColorTheme, theme *ColorTheme, black bool) {
fg := override(baseTheme.Fg, theme.Fg)
bg := override(baseTheme.Bg, theme.Bg)
if black {
bg = C.COLOR_BLACK
} else if theme.UseDefault {
fg = colDefault
bg = colDefault
C.use_default_colors()
}
if theme.UseDefault {
FG = colDefault
BG = colDefault
} else {
FG = int(fg)
BG = int(bg)
C.assume_default_colors(C.int(override(baseTheme.Fg, theme.Fg)), C.int(bg))
}
currentFG := override(baseTheme.Current, theme.Current)
darkBG := override(baseTheme.DarkBg, theme.DarkBg)
CurrentFG = int(currentFG)
DarkBG = int(darkBG)
C.init_pair(ColPrompt, override(baseTheme.Prompt, theme.Prompt), bg)
C.init_pair(ColMatch, override(baseTheme.Match, theme.Match), bg)
C.init_pair(ColCurrent, currentFG, darkBG)
C.init_pair(ColCurrentMatch, override(baseTheme.CurrentMatch, theme.CurrentMatch), darkBG)
C.init_pair(ColSpinner, override(baseTheme.Spinner, theme.Spinner), bg)
C.init_pair(ColInfo, override(baseTheme.Info, theme.Info), bg)
C.init_pair(ColCursor, override(baseTheme.Cursor, theme.Cursor), darkBG)
C.init_pair(ColSelected, override(baseTheme.Selected, theme.Selected), darkBG)
C.init_pair(ColHeader, override(baseTheme.Header, theme.Header), bg)
C.init_pair(ColBorder, override(baseTheme.Border, theme.Border), bg)
}
func Close() {
C.endwin()
C.delscreen(_screen)
}
func GetBytes() []byte {
c := getch(false)
_buf = append(_buf, byte(c))
for {
c = getch(true)
if c == -1 {
break
}
_buf = append(_buf, byte(c))
}
return _buf
}
// 27 (91 79) 77 type x y
func mouseSequence(sz *int) Event {
if len(_buf) < 6 {
return Event{Invalid, 0, nil}
}
*sz = 6
switch _buf[3] {
case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl
35, 39, 43, 51: // mouse-up / shift / cmd / ctrl
mod := _buf[3] >= 36
down := _buf[3]%2 == 0
x := int(_buf[4] - 33)
y := int(_buf[5] - 33)
double := false
if down {
now := time.Now()
if now.Sub(_prevDownTime) < doubleClickDuration {
_clickY = append(_clickY, y)
} else {
_clickY = []int{y}
}
_prevDownTime = now
} else {
if len(_clickY) > 1 && _clickY[0] == _clickY[1] &&
time.Now().Sub(_prevDownTime) < doubleClickDuration {
double = true
}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}}
case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
mod := _buf[3] >= 100
s := 1 - int(_buf[3]%2)*2
x := int(_buf[4] - 33)
y := int(_buf[5] - 33)
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}}
}
return Event{Invalid, 0, nil}
}
func escSequence(sz *int) Event {
if len(_buf) < 2 {
return Event{ESC, 0, nil}
}
*sz = 2
switch _buf[1] {
case 13:
return Event{AltEnter, 0, nil}
case 32:
return Event{AltSpace, 0, nil}
case 47:
return Event{AltSlash, 0, nil}
case 98:
return Event{AltB, 0, nil}
case 100:
return Event{AltD, 0, nil}
case 102:
return Event{AltF, 0, nil}
case 127:
return Event{AltBS, 0, nil}
case 91, 79:
if len(_buf) < 3 {
return Event{Invalid, 0, nil}
}
*sz = 3
switch _buf[2] {
case 68:
return Event{Left, 0, nil}
case 67:
return Event{Right, 0, nil}
case 66:
return Event{Down, 0, nil}
case 65:
return Event{Up, 0, nil}
case 90:
return Event{BTab, 0, nil}
case 72:
return Event{Home, 0, nil}
case 70:
return Event{End, 0, nil}
case 77:
return mouseSequence(sz)
case 80:
return Event{F1, 0, nil}
case 81:
return Event{F2, 0, nil}
case 82:
return Event{F3, 0, nil}
case 83:
return Event{F4, 0, nil}
case 49, 50, 51, 52, 53, 54:
if len(_buf) < 4 {
return Event{Invalid, 0, nil}
}
*sz = 4
switch _buf[2] {
case 50:
if len(_buf) == 5 && _buf[4] == 126 {
*sz = 5
switch _buf[3] {
case 48:
return Event{F9, 0, nil}
case 49:
return Event{F10, 0, nil}
}
}
// Bracketed paste mode \e[200~ / \e[201
if _buf[3] == 48 && (_buf[4] == 48 || _buf[4] == 49) && _buf[5] == 126 {
*sz = 6
return Event{Invalid, 0, nil}
}
return Event{Invalid, 0, nil} // INS
case 51:
return Event{Del, 0, nil}
case 52:
return Event{End, 0, nil}
case 53:
return Event{PgUp, 0, nil}
case 54:
return Event{PgDn, 0, nil}
case 49:
switch _buf[3] {
case 126:
return Event{Home, 0, nil}
case 53, 55, 56, 57:
if len(_buf) == 5 && _buf[4] == 126 {
*sz = 5
switch _buf[3] {
case 53:
return Event{F5, 0, nil}
case 55:
return Event{F6, 0, nil}
case 56:
return Event{F7, 0, nil}
case 57:
return Event{F8, 0, nil}
}
}
return Event{Invalid, 0, nil}
case 59:
if len(_buf) != 6 {
return Event{Invalid, 0, nil}
}
*sz = 6
switch _buf[4] {
case 50:
switch _buf[5] {
case 68:
return Event{Home, 0, nil}
case 67:
return Event{End, 0, nil}
}
case 53:
switch _buf[5] {
case 68:
return Event{SLeft, 0, nil}
case 67:
return Event{SRight, 0, nil}
}
} // _buf[4]
} // _buf[3]
} // _buf[2]
} // _buf[2]
} // _buf[1]
if _buf[1] >= 'a' && _buf[1] <= 'z' {
return Event{AltA + int(_buf[1]) - 'a', 0, nil}
}
return Event{Invalid, 0, nil}
}
func GetChar() Event {
if len(_buf) == 0 {
_buf = GetBytes()
}
if len(_buf) == 0 {
panic("Empty _buffer")
}
sz := 1
defer func() {
_buf = _buf[sz:]
}()
switch _buf[0] {
case CtrlC:
return Event{CtrlC, 0, nil}
case CtrlG:
return Event{CtrlG, 0, nil}
case CtrlQ:
return Event{CtrlQ, 0, nil}
case 127:
return Event{BSpace, 0, nil}
case ESC:
return escSequence(&sz)
}
// CTRL-A ~ CTRL-Z
if _buf[0] <= CtrlZ {
return Event{int(_buf[0]), 0, nil}
}
r, rsz := utf8.DecodeRune(_buf)
if r == utf8.RuneError {
return Event{ESC, 0, nil}
}
sz = rsz
return Event{Rune, r, nil}
}
func (w *Window) Close() {
C.delwin(w.win)
}
func (w *Window) Enclose(y int, x int) bool {
return bool(C.wenclose(w.win, C.int(y), C.int(x)))
}
func (w *Window) Move(y int, x int) {
C.wmove(w.win, C.int(y), C.int(x))
}
func (w *Window) MoveAndClear(y int, x int) {
w.Move(y, x)
C.wclrtoeol(w.win)
}
func (w *Window) Print(text string) {
C.waddstr(w.win, C.CString(strings.Map(func(r rune) rune {
if r < 32 {
return -1
}
return r
}, text)))
}
func (w *Window) CPrint(pair int, bold bool, text string) {
attr := _color(pair, bold)
C.wattron(w.win, attr)
w.Print(text)
C.wattroff(w.win, attr)
}
func Clear() {
C.clear()
}
func Endwin() {
C.endwin()
}
func Refresh() {
C.refresh()
}
func (w *Window) Erase() {
C.werase(w.win)
}
func (w *Window) Fill(str string) bool {
return C.waddstr(w.win, C.CString(str)) == C.OK
}
func (w *Window) CFill(str string, fg int, bg int, bold bool) bool {
attr := _color(PairFor(fg, bg), bold)
C.wattron(w.win, attr)
ret := w.Fill(str)
C.wattroff(w.win, attr)
return ret
}
func (w *Window) Refresh() {
C.wnoutrefresh(w.win)
}
func DoUpdate() {
C.doupdate()
}
func PairFor(fg int, bg int) int {
key := (fg << 8) + bg
if found, prs := _colorMap[key]; prs {
return found
}
id := len(_colorMap) + ColUser
C.init_pair(C.short(id), C.short(fg), C.short(bg))
_colorMap[key] = id
return id
}

View File

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

View File

@@ -1,7 +1,10 @@
package fzf package fzf
import ( import (
"io/ioutil"
"os"
"os/user" "os/user"
"runtime"
"testing" "testing"
) )
@@ -10,23 +13,34 @@ func TestHistory(t *testing.T) {
// Invalid arguments // Invalid arguments
user, _ := user.Current() user, _ := user.Current()
paths := []string{"/etc", "/proc"} var paths []string
if user.Name != "root" { if runtime.GOOS == "windows" {
paths = append(paths, "/etc/sudoers") // GOPATH should exist, so we shouldn't be able to override it
paths = []string{os.Getenv("GOPATH")}
} else {
paths = []string{"/etc", "/proc"}
if user.Name != "root" {
paths = append(paths, "/etc/sudoers")
}
} }
for _, path := range paths { for _, path := range paths {
if _, e := NewHistory(path, maxHistory); e == nil { if _, e := NewHistory(path, maxHistory); e == nil {
t.Error("Error expected for: " + path) t.Error("Error expected for: " + path)
} }
} }
f, _ := ioutil.TempFile("", "fzf-history")
f.Close()
{ // Append lines { // Append lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory) h, _ := NewHistory(f.Name(), maxHistory)
for i := 0; i < maxHistory+10; i++ { for i := 0; i < maxHistory+10; i++ {
h.append("foobar") h.append("foobar")
} }
} }
{ // Read lines { // Read lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory) h, _ := NewHistory(f.Name(), maxHistory)
if len(h.lines) != maxHistory+1 { if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
} }
@@ -37,13 +51,13 @@ func TestHistory(t *testing.T) {
} }
} }
{ // Append lines { // Append lines
h, _ := NewHistory("/tmp/fzf-history", maxHistory) h, _ := NewHistory(f.Name(), maxHistory)
h.append("barfoo") h.append("barfoo")
h.append("") h.append("")
h.append("foobarbaz") h.append("foobarbaz")
} }
{ // Read lines again { // Read lines again
h, _ := NewHistory("/tmp/fzf-history", maxHistory) h, _ := NewHistory(f.Name(), maxHistory)
if len(h.lines) != maxHistory+1 { if len(h.lines) != maxHistory+1 {
t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines))
} }

View File

@@ -1,291 +1,44 @@
package fzf package fzf
import ( import (
"math" "github.com/junegunn/fzf/src/util"
"github.com/junegunn/fzf/src/curses"
) )
// Offset holds three 32-bit integers denoting the offsets of a matched substring // Item represents each input line. 56 bytes.
type Offset [3]int32
type colorOffset struct {
offset [2]int32
color int
bold bool
}
// Item represents each input line
type Item struct { type Item struct {
text []rune text util.Chars // 32 = 24 + 1 + 1 + 2 + 4
origText *[]rune transformed *[]Token // 8
transformed []Token origText *[]byte // 8
offsets []Offset colors *[]ansiOffset // 8
colors []ansiOffset
rank [5]int32
bonus int32
}
// Sort criteria to use. Never changes once fzf is started.
var sortCriteria []criterion
func isRankValid(rank [5]int32) bool {
// Exclude ordinal index
for _, r := range rank[:4] {
if r > 0 {
return true
}
}
return false
}
func buildEmptyRank(index int32) [5]int32 {
return [5]int32{0, 0, 0, 0, index}
} }
// Index returns ordinal index of the Item
func (item *Item) Index() int32 { func (item *Item) Index() int32 {
return item.rank[4] return item.text.Index
} }
// Rank calculates rank of the Item var minItem = Item{text: util.Chars{Index: -1}}
func (item *Item) Rank(cache bool) [5]int32 {
if cache && isRankValid(item.rank) { func (item *Item) TrimLength() uint16 {
return item.rank return item.text.TrimLength()
}
// Colors returns ansiOffsets of the Item
func (item *Item) Colors() []ansiOffset {
if item.colors == nil {
return []ansiOffset{}
} }
matchlen := 0 return *item.colors
prevEnd := 0
lenSum := 0
minBegin := math.MaxInt32
for _, offset := range item.offsets {
begin := int(offset[0])
end := int(offset[1])
trimLen := int(offset[2])
lenSum += trimLen
if prevEnd > begin {
begin = prevEnd
}
if end > prevEnd {
prevEnd = end
}
if end > begin {
if begin < minBegin {
minBegin = begin
}
matchlen += end - begin
}
}
rank := buildEmptyRank(item.Index())
for idx, criterion := range sortCriteria {
var val int32
switch criterion {
case byMatchLen:
if matchlen == 0 {
val = math.MaxInt32
} else {
// It is extremely unlikely that bonus exceeds 128
val = 128*int32(matchlen) - item.bonus
}
case byLength:
// It is guaranteed that .transformed in not null in normal execution
if item.transformed != nil {
// If offsets is empty, lenSum will be 0, but we don't care
val = int32(lenSum)
} else {
val = int32(len(item.text))
}
case byBegin:
// We can't just look at item.offsets[0][0] because it can be an inverse term
whitePrefixLen := 0
for idx, r := range item.text {
whitePrefixLen = idx
if idx == minBegin || r != ' ' && r != '\t' {
break
}
}
val = int32(minBegin - whitePrefixLen)
case byEnd:
if prevEnd > 0 {
val = int32(1 + len(item.text) - prevEnd)
} else {
// Empty offsets due to inverse terms.
val = 1
}
}
rank[idx] = val
}
if cache {
item.rank = rank
}
return rank
} }
// AsString returns the original string // AsString returns the original string
func (item *Item) AsString(stripAnsi bool) string { func (item *Item) AsString(stripAnsi bool) string {
return *item.StringPtr(stripAnsi)
}
// StringPtr returns the pointer to the original string
func (item *Item) StringPtr(stripAnsi bool) *string {
if item.origText != nil { if item.origText != nil {
if stripAnsi { if stripAnsi {
trimmed, _, _ := extractColor(string(*item.origText), nil) trimmed, _, _ := extractColor(string(*item.origText), nil, nil)
return &trimmed return trimmed
} }
orig := string(*item.origText) return string(*item.origText)
return &orig
} }
str := string(item.text) return item.text.ToString()
return &str
}
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {
if len(item.colors) == 0 {
var offsets []colorOffset
for _, off := range item.offsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold})
}
return offsets
}
// Find max column
var maxCol int32
for _, off := range item.offsets {
if off[1] > maxCol {
maxCol = off[1]
}
}
for _, ansi := range item.colors {
if ansi.offset[1] > maxCol {
maxCol = ansi.offset[1]
}
}
cols := make([]int, maxCol)
for colorIndex, ansi := range item.colors {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = colorIndex + 1 // XXX
}
}
for _, off := range item.offsets {
for i := off[0]; i < off[1]; i++ {
cols[i] = -1
}
}
// sort.Sort(ByOrder(offsets))
// Merge offsets
// ------------ ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
curr := 0
start := 0
var offsets []colorOffset
add := func(idx int) {
if curr != 0 && idx > start {
if curr == -1 {
offsets = append(offsets, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold})
} else {
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
if bg == -1 {
if current {
bg = curses.DarkBG
} else {
bg = curses.BG
}
}
offsets = append(offsets, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
color: curses.PairFor(fg, bg),
bold: ansi.color.bold || bold})
}
}
}
for idx, col := range cols {
if col != curr {
add(idx)
start = idx
curr = col
}
}
add(int(maxCol))
return offsets
}
// ByOrder is for sorting substring offsets
type ByOrder []Offset
func (a ByOrder) Len() int {
return len(a)
}
func (a ByOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByOrder) Less(i, j int) bool {
ioff := a[i]
joff := a[j]
return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
}
// ByRelevance is for sorting Items
type ByRelevance []*Item
func (a ByRelevance) Len() int {
return len(a)
}
func (a ByRelevance) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevance) Less(i, j int) bool {
irank := a[i].Rank(true)
jrank := a[j].Rank(true)
return compareRanks(irank, jrank, false)
}
// ByRelevanceTac is for sorting Items
type ByRelevanceTac []*Item
func (a ByRelevanceTac) Len() int {
return len(a)
}
func (a ByRelevanceTac) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevanceTac) Less(i, j int) bool {
irank := a[i].Rank(true)
jrank := a[j].Rank(true)
return compareRanks(irank, jrank, true)
}
func compareRanks(irank [5]int32, jrank [5]int32, tac bool) bool {
for idx := 0; idx < 4; idx++ {
left := irank[idx]
right := jrank[idx]
if left < right {
return true
} else if left > right {
return false
}
}
return (irank[4] <= jrank[4]) != tac
} }

View File

@@ -1,108 +1,23 @@
package fzf package fzf
import ( import (
"math"
"sort"
"testing" "testing"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/util"
) )
func TestOffsetSort(t *testing.T) { func TestStringPtr(t *testing.T) {
offsets := []Offset{ orig := []byte("\x1b[34mfoo")
Offset{3, 5}, Offset{2, 7}, text := []byte("\x1b[34mbar")
Offset{1, 3}, Offset{2, 9}} item := Item{origText: &orig, text: util.ToChars(text)}
sort.Sort(ByOrder(offsets)) if item.AsString(true) != "foo" || item.AsString(false) != string(orig) {
t.Fail()
if offsets[0][0] != 1 || offsets[0][1] != 3 || }
offsets[1][0] != 2 || offsets[1][1] != 7 || if item.AsString(true) != "foo" {
offsets[2][0] != 2 || offsets[2][1] != 9 || t.Fail()
offsets[3][0] != 3 || offsets[3][1] != 5 { }
t.Error("Invalid order:", offsets) item.origText = nil
if item.AsString(true) != string(text) || item.AsString(false) != string(text) {
t.Fail()
} }
} }
func TestRankComparison(t *testing.T) {
if compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{2, 0, 0, 0, 7}, false) ||
!compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{3, 0, 0, 0, 6}, false) ||
!compareRanks([5]int32{1, 2, 0, 0, 3}, [5]int32{1, 3, 0, 0, 2}, false) ||
!compareRanks([5]int32{0, 0, 0, 0, 0}, [5]int32{0, 0, 0, 0, 0}, false) {
t.Error("Invalid order")
}
if compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{2, 0, 0, 0, 7}, true) ||
!compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{3, 0, 0, 0, 6}, false) ||
!compareRanks([5]int32{1, 2, 0, 0, 3}, [5]int32{1, 3, 0, 0, 2}, true) ||
!compareRanks([5]int32{0, 0, 0, 0, 0}, [5]int32{0, 0, 0, 0, 0}, false) {
t.Error("Invalid order (tac)")
}
}
// Match length, string length, index
func TestItemRank(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byMatchLen, byLength}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := Item{text: strs[0], offsets: []Offset{}, rank: [5]int32{0, 0, 0, 0, 1}}
rank1 := item1.Rank(true)
if rank1[0] != math.MaxInt32 || rank1[1] != 3 || rank1[4] != 1 {
t.Error(item1.Rank(true))
}
// Only differ in index
item2 := Item{text: strs[0], offsets: []Offset{}}
items := []*Item{&item1, &item2}
sort.Sort(ByRelevance(items))
if items[0] != &item2 || items[1] != &item1 {
t.Error(items)
}
items = []*Item{&item2, &item1, &item1, &item2}
sort.Sort(ByRelevance(items))
if items[0] != &item2 || items[1] != &item2 ||
items[2] != &item1 || items[3] != &item1 {
t.Error(items)
}
// Sort by relevance
item3 := Item{text: strs[1], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item4 := Item{text: strs[1], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
item5 := Item{text: strs[2], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item6 := Item{text: strs[2], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6}
sort.Sort(ByRelevance(items))
if items[0] != &item6 || items[1] != &item4 ||
items[2] != &item5 || items[3] != &item3 ||
items[4] != &item2 || items[5] != &item1 {
t.Error(items)
}
}
func TestColorOffset(t *testing.T) {
// ------------ 20 ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
item := Item{
offsets: []Offset{Offset{5, 15}, Offset{25, 35}},
colors: []ansiOffset{
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}},
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}},
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}},
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
offsets := item.colorOffsets(99, false, true)
assert := func(idx int, b int32, e int32, c int, bold bool) {
o := offsets[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold {
t.Error(o)
}
}
assert(0, 0, 5, curses.ColUser, false)
assert(1, 5, 15, 99, false)
assert(2, 15, 20, curses.ColUser, false)
assert(3, 22, 25, curses.ColUser+1, true)
assert(4, 25, 35, 99, false)
assert(5, 35, 40, curses.ColUser+2, true)
}

View File

@@ -26,6 +26,7 @@ type Matcher struct {
eventBox *util.EventBox eventBox *util.EventBox
reqBox *util.EventBox reqBox *util.EventBox
partitions int partitions int
slab []*util.Slab
mergerCache map[string]*Merger mergerCache map[string]*Merger
} }
@@ -37,13 +38,15 @@ const (
// NewMatcher returns a new Matcher // NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern, func NewMatcher(patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox) *Matcher { sort bool, tac bool, eventBox *util.EventBox) *Matcher {
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
return &Matcher{ return &Matcher{
patternBuilder: patternBuilder, patternBuilder: patternBuilder,
sort: sort, sort: sort,
tac: tac, tac: tac,
eventBox: eventBox, eventBox: eventBox,
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
partitions: runtime.NumCPU(), partitions: partitions,
slab: make([]*util.Slab, partitions),
mergerCache: make(map[string]*Merger)} mergerCache: make(map[string]*Merger)}
} }
@@ -106,18 +109,19 @@ func (m *Matcher) Loop() {
} }
func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk { func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
perSlice := len(chunks) / m.partitions partitions := m.partitions
perSlice := len(chunks) / partitions
// No need to parallelize
if perSlice == 0 { if perSlice == 0 {
return [][]*Chunk{chunks} partitions = len(chunks)
perSlice = 1
} }
slices := make([][]*Chunk, m.partitions) slices := make([][]*Chunk, partitions)
for i := 0; i < m.partitions; i++ { for i := 0; i < partitions; i++ {
start := i * perSlice start := i * perSlice
end := start + perSlice end := start + perSlice
if i == m.partitions-1 { if i == partitions-1 {
end = len(chunks) end = len(chunks)
} }
slices[i] = chunks[start:end] slices[i] = chunks[start:end]
@@ -127,7 +131,7 @@ func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
type partialResult struct { type partialResult struct {
index int index int
matches []*Item matches []Result
} }
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
@@ -152,17 +156,26 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
for idx, chunks := range slices { for idx, chunks := range slices {
waitGroup.Add(1) waitGroup.Add(1)
go func(idx int, chunks []*Chunk) { if m.slab[idx] == nil {
m.slab[idx] = util.MakeSlab(slab16Size, slab32Size)
}
go func(idx int, slab *util.Slab, chunks []*Chunk) {
defer func() { waitGroup.Done() }() defer func() { waitGroup.Done() }()
sliceMatches := []*Item{} count := 0
for _, chunk := range chunks { allMatches := make([][]Result, len(chunks))
matches := request.pattern.Match(chunk) for idx, chunk := range chunks {
sliceMatches = append(sliceMatches, matches...) matches := request.pattern.Match(chunk, slab)
allMatches[idx] = matches
count += len(matches)
if cancelled.Get() { if cancelled.Get() {
return return
} }
countChan <- len(matches) countChan <- len(matches)
} }
sliceMatches := make([]Result, 0, count)
for _, matches := range allMatches {
sliceMatches = append(sliceMatches, matches...)
}
if m.sort { if m.sort {
if m.tac { if m.tac {
sort.Sort(ByRelevanceTac(sliceMatches)) sort.Sort(ByRelevanceTac(sliceMatches))
@@ -171,7 +184,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
} }
} }
resultChan <- partialResult{idx, sliceMatches} resultChan <- partialResult{idx, sliceMatches}
}(idx, chunks) }(idx, m.slab[idx], chunks)
} }
wait := func() bool { wait := func() bool {
@@ -199,12 +212,12 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
} }
} }
partialResults := make([][]*Item, numSlices) partialResults := make([][]Result, numSlices)
for _, _ = range slices { for _ = range slices {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches partialResults[partialResult.index] = partialResult.matches
} }
return NewMerger(partialResults, m.sort, m.tac), false return NewMerger(pattern, partialResults, m.sort, m.tac), false
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search

View File

@@ -2,14 +2,15 @@ package fzf
import "fmt" import "fmt"
// Merger with no data // EmptyMerger is a Merger with no data
var EmptyMerger = NewMerger([][]*Item{}, false, false) var EmptyMerger = NewMerger(nil, [][]Result{}, false, false)
// Merger holds a set of locally sorted lists of items and provides the view of // Merger holds a set of locally sorted lists of items and provides the view of
// a single, globally-sorted list // a single, globally-sorted list
type Merger struct { type Merger struct {
lists [][]*Item pattern *Pattern
merged []*Item lists [][]Result
merged []Result
chunks *[]*Chunk chunks *[]*Chunk
cursors []int cursors []int
sorted bool sorted bool
@@ -22,21 +23,23 @@ type Merger struct {
// original order // original order
func PassMerger(chunks *[]*Chunk, tac bool) *Merger { func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
mg := Merger{ mg := Merger{
chunks: chunks, pattern: nil,
tac: tac, chunks: chunks,
count: 0} tac: tac,
count: 0}
for _, chunk := range *mg.chunks { for _, chunk := range *mg.chunks {
mg.count += len(*chunk) mg.count += chunk.count
} }
return &mg return &mg
} }
// NewMerger returns a new Merger // NewMerger returns a new Merger
func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger { func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger {
mg := Merger{ mg := Merger{
pattern: pattern,
lists: lists, lists: lists,
merged: []*Item{}, merged: []Result{},
chunks: nil, chunks: nil,
cursors: make([]int, len(lists)), cursors: make([]int, len(lists)),
sorted: sorted, sorted: sorted,
@@ -55,14 +58,14 @@ func (mg *Merger) Length() int {
return mg.count return mg.count
} }
// Get returns the pointer to the Item object indexed by the given integer // Get returns the pointer to the Result object indexed by the given integer
func (mg *Merger) Get(idx int) *Item { func (mg *Merger) Get(idx int) Result {
if mg.chunks != nil { if mg.chunks != nil {
if mg.tac { if mg.tac {
idx = mg.count - idx - 1 idx = mg.count - idx - 1
} }
chunk := (*mg.chunks)[idx/chunkSize] chunk := (*mg.chunks)[idx/chunkSize]
return (*chunk)[idx%chunkSize] return Result{item: &chunk.items[idx%chunkSize]}
} }
if mg.sorted { if mg.sorted {
@@ -86,9 +89,9 @@ func (mg *Merger) cacheable() bool {
return mg.count < mergerCacheMax return mg.count < mergerCacheMax
} }
func (mg *Merger) mergedGet(idx int) *Item { func (mg *Merger) mergedGet(idx int) Result {
for i := len(mg.merged); i <= idx; i++ { for i := len(mg.merged); i <= idx; i++ {
minRank := buildEmptyRank(0) minRank := minRank()
minIdx := -1 minIdx := -1
for listIdx, list := range mg.lists { for listIdx, list := range mg.lists {
cursor := mg.cursors[listIdx] cursor := mg.cursors[listIdx]
@@ -97,7 +100,7 @@ func (mg *Merger) mergedGet(idx int) *Item {
continue continue
} }
if cursor >= 0 { if cursor >= 0 {
rank := list[cursor].Rank(false) rank := list[cursor]
if minIdx < 0 || compareRanks(rank, minRank, mg.tac) { if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
minRank = rank minRank = rank
minIdx = listIdx minIdx = listIdx

View File

@@ -5,6 +5,8 @@ import (
"math/rand" "math/rand"
"sort" "sort"
"testing" "testing"
"github.com/junegunn/fzf/src/util"
) )
func assert(t *testing.T, cond bool, msg ...string) { func assert(t *testing.T, cond bool, msg ...string) {
@@ -13,18 +15,11 @@ func assert(t *testing.T, cond bool, msg ...string) {
} }
} }
func randItem() *Item { func randResult() Result {
str := fmt.Sprintf("%d", rand.Uint32()) str := fmt.Sprintf("%d", rand.Uint32())
offsets := make([]Offset, rand.Int()%3) chars := util.ToChars([]byte(str))
for idx := range offsets { chars.Index = rand.Int31()
sidx := int32(rand.Uint32() % 20) return Result{item: &Item{text: chars}}
eidx := sidx + int32(rand.Uint32()%20)
offsets[idx] = Offset{sidx, eidx}
}
return &Item{
text: []rune(str),
rank: buildEmptyRank(rand.Int31()),
offsets: offsets}
} }
func TestEmptyMerger(t *testing.T) { func TestEmptyMerger(t *testing.T) {
@@ -34,23 +29,23 @@ func TestEmptyMerger(t *testing.T) {
assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list") assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
} }
func buildLists(partiallySorted bool) ([][]*Item, []*Item) { func buildLists(partiallySorted bool) ([][]Result, []Result) {
numLists := 4 numLists := 4
lists := make([][]*Item, numLists) lists := make([][]Result, numLists)
cnt := 0 cnt := 0
for i := 0; i < numLists; i++ { for i := 0; i < numLists; i++ {
numItems := rand.Int() % 20 numResults := rand.Int() % 20
cnt += numItems cnt += numResults
lists[i] = make([]*Item, numItems) lists[i] = make([]Result, numResults)
for j := 0; j < numItems; j++ { for j := 0; j < numResults; j++ {
item := randItem() item := randResult()
lists[i][j] = item lists[i][j] = item
} }
if partiallySorted { if partiallySorted {
sort.Sort(ByRelevance(lists[i])) sort.Sort(ByRelevance(lists[i]))
} }
} }
items := []*Item{} items := []Result{}
for _, list := range lists { for _, list := range lists {
items = append(items, list...) items = append(items, list...)
} }
@@ -62,7 +57,7 @@ func TestMergerUnsorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Not sorted: same order // Not sorted: same order
mg := NewMerger(lists, false, false) mg := NewMerger(nil, lists, false, false)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
assert(t, items[i] == mg.Get(i), "Invalid Get") assert(t, items[i] == mg.Get(i), "Invalid Get")
@@ -74,7 +69,7 @@ func TestMergerSorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Sorted sorted order // Sorted sorted order
mg := NewMerger(lists, true, false) mg := NewMerger(nil, lists, true, false)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
@@ -84,7 +79,7 @@ func TestMergerSorted(t *testing.T) {
} }
// Inverse order // Inverse order
mg2 := NewMerger(lists, true, false) mg2 := NewMerger(nil, lists, true, false)
for i := cnt - 1; i >= 0; i-- { for i := cnt - 1; i >= 0; i-- {
if items[i] != mg2.Get(i) { if items[i] != mg2.Get(i) {
t.Error("Not sorted", items[i], mg2.Get(i)) t.Error("Not sorted", items[i], mg2.Get(i))

View File

@@ -8,9 +8,11 @@ import (
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
"github.com/junegunn/go-shellwords" "github.com/mattn/go-shellwords"
) )
const usage = `usage: fzf [options] const usage = `usage: fzf [options]
@@ -19,8 +21,10 @@ const usage = `usage: fzf [options]
-x, --extended Extended-search mode -x, --extended Extended-search mode
(enabled by default; +x or --no-extended to disable) (enabled by default; +x or --no-extended to disable)
-e, --exact Enable Exact-match -e, --exact Enable Exact-match
--algo=TYPE Fuzzy matching algorithm: [v1|v2] (default: v2)
-i Case-insensitive match (default: smart-case match) -i Case-insensitive match (default: smart-case match)
+i Case-sensitive match +i Case-sensitive match
--literal Do not normalize latin script letters before matching
-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]).
@@ -41,10 +45,16 @@ const usage = `usage: fzf [options]
--no-hscroll Disable horizontal scroll --no-hscroll Disable horizontal scroll
--hscroll-off=COL Number of screen columns to keep to the right of the --hscroll-off=COL Number of screen columns to keep to the right of the
highlighted substring (default: 10) highlighted substring (default: 10)
--filepath-word Make word-wise movements respect path separators
--jump-labels=CHARS Label characters for jump and jump-accept --jump-labels=CHARS Label characters for jump and jump-accept
Layout Layout
--height=HEIGHT[%] Display fzf window below the cursor with the given
height instead of using fullscreen
--min-height=HEIGHT Minimum height when --height is given in percent
(default: 10)
--reverse Reverse orientation --reverse Reverse orientation
--border Draw border above and below the finder
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--inline-info Display finder info inline with the query --inline-info Display finder info inline with the query
--prompt=STR Input prompt (default: '> ') --prompt=STR Input prompt (default: '> ')
@@ -55,6 +65,7 @@ const usage = `usage: fzf [options]
--ansi Enable processing of ANSI color codes --ansi Enable processing of ANSI color codes
--tabstop=SPACES Number of spaces for a tab character (default: 8) --tabstop=SPACES Number of spaces for a tab character (default: 8)
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
--no-bold Do not use bold text
History History
--history=FILE History file --history=FILE History file
@@ -63,7 +74,7 @@ const usage = `usage: fzf [options]
Preview Preview
--preview=COMMAND Command to preview highlighted line ({}) --preview=COMMAND Command to preview highlighted line ({})
--preview-window=OPT Preview window layout (default: right:50%) --preview-window=OPT Preview window layout (default: right:50%)
[up|down|left|right][:SIZE[%]][:hidden] [up|down|left|right][:SIZE[%]][:wrap][:hidden]
Scripting Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
@@ -72,7 +83,10 @@ const usage = `usage: fzf [options]
-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
--read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering --sync Synchronous search for multi-staged filtering
--version Display version information and exit
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
@@ -94,7 +108,7 @@ const (
type criterion int type criterion int
const ( const (
byMatchLen criterion = iota byScore criterion = iota
byLength byLength
byBegin byBegin
byEnd byEnd
@@ -123,13 +137,16 @@ type previewOpts struct {
position windowPosition position windowPosition
size sizeSpec size sizeSpec
hidden bool hidden bool
wrap bool
} }
// Options stores the values of command-line options // Options stores the values of command-line options
type Options struct { type Options struct {
Fuzzy bool Fuzzy bool
FuzzyAlgo algo.Algo
Extended bool Extended bool
Case Case Case Case
Normalize bool
Nth []Range Nth []Range
WithNth []Range WithNth []Range
Delimiter Delimiter Delimiter Delimiter
@@ -139,12 +156,16 @@ type Options struct {
Multi bool Multi bool
Ansi bool Ansi bool
Mouse bool Mouse bool
Theme *curses.ColorTheme Theme *tui.ColorTheme
Black bool Black bool
Bold bool
Height sizeSpec
MinHeight int
Reverse bool Reverse bool
Cycle bool Cycle bool
Hscroll bool Hscroll bool
HscrollOff int HscrollOff int
FileWord bool
InlineInfo bool InlineInfo bool
JumpLabels string JumpLabels string
Prompt string Prompt string
@@ -154,40 +175,47 @@ type Options struct {
Filter *string Filter *string
ToggleSort bool ToggleSort bool
Expect map[int]string Expect map[int]string
Keymap map[int]actionType Keymap map[int][]action
Execmap map[int]string
Preview previewOpts Preview previewOpts
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
Printer func(string)
Sync bool Sync bool
History *History History *History
Header []string Header []string
HeaderLines int HeaderLines int
Margin [4]sizeSpec Margin [4]sizeSpec
Bordered bool
Tabstop int Tabstop int
ClearOnExit bool
Version bool Version bool
} }
func defaultOptions() *Options { func defaultOptions() *Options {
return &Options{ return &Options{
Fuzzy: true, Fuzzy: true,
FuzzyAlgo: algo.FuzzyMatchV2,
Extended: true, Extended: true,
Case: CaseSmart, Case: CaseSmart,
Normalize: true,
Nth: make([]Range, 0), Nth: make([]Range, 0),
WithNth: make([]Range, 0), WithNth: make([]Range, 0),
Delimiter: Delimiter{}, Delimiter: Delimiter{},
Sort: 1000, Sort: 1000,
Tac: false, Tac: false,
Criteria: []criterion{byMatchLen, byLength}, Criteria: []criterion{byScore, byLength},
Multi: false, Multi: false,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
Theme: curses.EmptyTheme(), Theme: tui.EmptyTheme(),
Black: false, Black: false,
Bold: true,
MinHeight: 10,
Reverse: false, Reverse: false,
Cycle: false, Cycle: false,
Hscroll: true, Hscroll: true,
HscrollOff: 10, HscrollOff: 10,
FileWord: false,
InlineInfo: false, InlineInfo: false,
JumpLabels: defaultJumpLabels, JumpLabels: defaultJumpLabels,
Prompt: "> ", Prompt: "> ",
@@ -197,17 +225,18 @@ func defaultOptions() *Options {
Filter: nil, Filter: nil,
ToggleSort: false, ToggleSort: false,
Expect: make(map[int]string), Expect: make(map[int]string),
Keymap: make(map[int]actionType), Keymap: make(map[int][]action),
Execmap: make(map[int]string), Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false},
Preview: previewOpts{"", posRight, sizeSpec{50, true}, false},
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
Printer: func(str string) { fmt.Println(str) },
Sync: false, Sync: false,
History: nil, History: nil,
Header: make([]string, 0), Header: make([]string, 0),
HeaderLines: 0, HeaderLines: 0,
Margin: defaultMargin(), Margin: defaultMargin(),
Tabstop: 8, Tabstop: 8,
ClearOnExit: true,
Version: false} Version: false}
} }
@@ -240,7 +269,7 @@ func nextString(args []string, i *int, message string) string {
} }
func optionalNextString(args []string, i *int) string { func optionalNextString(args []string, i *int) string {
if len(args) > *i+1 { if len(args) > *i+1 && !strings.HasPrefix(args[*i+1], "-") {
*i++ *i++
return args[*i] return args[*i]
} }
@@ -321,6 +350,22 @@ func isAlphabet(char uint8) bool {
return char >= 'a' && char <= 'z' return char >= 'a' && char <= 'z'
} }
func isNumeric(char uint8) bool {
return char >= '0' && char <= '9'
}
func parseAlgo(str string) algo.Algo {
switch str {
case "v1":
return algo.FuzzyMatchV1
case "v2":
return algo.FuzzyMatchV2
default:
errorExit("invalid algorithm (expected: v1 or v2)")
}
return algo.FuzzyMatchV2
}
func parseKeyChords(str string, message string) map[int]string { func parseKeyChords(str string, message string) map[int]string {
if len(str) == 0 { if len(str) == 0 {
errorExit(message) errorExit(message)
@@ -340,60 +385,76 @@ func parseKeyChords(str string, message string) map[int]string {
chord := 0 chord := 0
switch lkey { switch lkey {
case "up": case "up":
chord = curses.Up chord = tui.Up
case "down": case "down":
chord = curses.Down chord = tui.Down
case "left": case "left":
chord = curses.Left chord = tui.Left
case "right": case "right":
chord = curses.Right chord = tui.Right
case "enter", "return": case "enter", "return":
chord = curses.CtrlM chord = tui.CtrlM
case "space": case "space":
chord = curses.AltZ + int(' ') chord = tui.AltZ + int(' ')
case "bspace", "bs": case "bspace", "bs":
chord = curses.BSpace chord = tui.BSpace
case "ctrl-space":
chord = tui.CtrlSpace
case "change":
chord = tui.Change
case "alt-enter", "alt-return": case "alt-enter", "alt-return":
chord = curses.AltEnter chord = tui.CtrlAltM
case "alt-space": case "alt-space":
chord = curses.AltSpace chord = tui.AltSpace
case "alt-/": case "alt-/":
chord = curses.AltSlash chord = tui.AltSlash
case "alt-bs", "alt-bspace": case "alt-bs", "alt-bspace":
chord = curses.AltBS chord = tui.AltBS
case "tab": case "tab":
chord = curses.Tab chord = tui.Tab
case "btab", "shift-tab": case "btab", "shift-tab":
chord = curses.BTab chord = tui.BTab
case "esc": case "esc":
chord = curses.ESC chord = tui.ESC
case "del": case "del":
chord = curses.Del chord = tui.Del
case "home": case "home":
chord = curses.Home chord = tui.Home
case "end": case "end":
chord = curses.End chord = tui.End
case "pgup", "page-up": case "pgup", "page-up":
chord = curses.PgUp chord = tui.PgUp
case "pgdn", "page-down": case "pgdn", "page-down":
chord = curses.PgDn chord = tui.PgDn
case "shift-left": case "shift-left":
chord = curses.SLeft chord = tui.SLeft
case "shift-right": case "shift-right":
chord = curses.SRight chord = tui.SRight
case "left-click":
chord = tui.LeftClick
case "right-click":
chord = tui.RightClick
case "double-click": case "double-click":
chord = curses.DoubleClick chord = tui.DoubleClick
case "f10": case "f10":
chord = curses.F10 chord = tui.F10
case "f11":
chord = tui.F11
case "f12":
chord = tui.F12
default: default:
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) {
chord = curses.CtrlA + int(lkey[5]) - 'a' chord = tui.CtrlAltA + int(lkey[9]) - 'a'
} else if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chord = tui.CtrlA + int(lkey[5]) - 'a'
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
chord = curses.AltA + int(lkey[4]) - 'a' chord = tui.AltA + int(lkey[4]) - 'a'
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isNumeric(lkey[4]) {
chord = tui.Alt0 + int(lkey[4]) - '0'
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' { } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' {
chord = curses.F1 + int(key[1]) - '1' chord = tui.F1 + int(key[1]) - '1'
} else if utf8.RuneCountInString(key) == 1 { } else if utf8.RuneCountInString(key) == 1 {
chord = curses.AltZ + int([]rune(key)[0]) chord = tui.AltZ + int([]rune(key)[0])
} else { } else {
errorExit("unsupported key: " + key) errorExit("unsupported key: " + key)
} }
@@ -406,7 +467,7 @@ func parseKeyChords(str string, message string) map[int]string {
} }
func parseTiebreak(str string) []criterion { func parseTiebreak(str string) []criterion {
criteria := []criterion{byMatchLen} criteria := []criterion{byScore}
hasIndex := false hasIndex := false
hasLength := false hasLength := false
hasBegin := false hasBegin := false
@@ -440,7 +501,7 @@ func parseTiebreak(str string) []criterion {
return criteria return criteria
} }
func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme {
if theme != nil { if theme != nil {
dupe := *theme dupe := *theme
return &dupe return &dupe
@@ -448,16 +509,17 @@ func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
return nil return nil
} }
func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme { func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
theme := dupeTheme(defaultTheme) theme := dupeTheme(defaultTheme)
rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$")
for _, str := range strings.Split(strings.ToLower(str), ",") { for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str { switch str {
case "dark": case "dark":
theme = dupeTheme(curses.Dark256) theme = dupeTheme(tui.Dark256)
case "light": case "light":
theme = dupeTheme(curses.Light256) theme = dupeTheme(tui.Light256)
case "16": case "16":
theme = dupeTheme(curses.Default16) theme = dupeTheme(tui.Default16)
case "bw", "no": case "bw", "no":
theme = nil theme = nil
default: default:
@@ -473,18 +535,22 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme
if len(pair) != 2 { if len(pair) != 2 {
fail() fail()
} }
ansi32, err := strconv.Atoi(pair[1])
if err != nil || ansi32 < -1 || ansi32 > 255 { var ansi tui.Color
fail() if rrggbb.MatchString(pair[1]) {
ansi = tui.HexToColor(pair[1])
} else {
ansi32, err := strconv.Atoi(pair[1])
if err != nil || ansi32 < -1 || ansi32 > 255 {
fail()
}
ansi = tui.Color(ansi32)
} }
ansi := int16(ansi32)
switch pair[0] { switch pair[0] {
case "fg": case "fg":
theme.Fg = ansi theme.Fg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "bg": case "bg":
theme.Bg = ansi theme.Bg = ansi
theme.UseDefault = theme.UseDefault && ansi < 0
case "fg+": case "fg+":
theme.Current = ansi theme.Current = ansi
case "bg+": case "bg+":
@@ -527,23 +593,32 @@ func firstKey(keymap map[int]string) int {
const ( const (
escapedColon = 0 escapedColon = 0
escapedComma = 1 escapedComma = 1
escapedPlus = 2
) )
func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) { func init() {
if executeRegexp == nil { // Backreferences are not supported.
// Backreferences are not supported. // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') executeRegexp = regexp.MustCompile(
executeRegexp = regexp.MustCompile( "(?si):(execute(?:-multi|-silent)?):.+|:(execute(?:-multi|-silent)?)(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
"(?s):execute(-multi)?:.*|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") }
}
func parseKeymap(keymap map[int][]action, str string) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
if strings.HasPrefix(src, ":execute-multi") { prefix := ":execute"
return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")" if src[len(prefix)] == '-' {
c := src[len(prefix)+1]
if c == 's' || c == 'S' {
prefix += "-silent"
} else {
prefix += "-multi"
}
} }
return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")" return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")"
}) })
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1)
idx := 0 idx := 0
for _, pairStr := range strings.Split(masked, ",") { for _, pairStr := range strings.Split(masked, ",") {
@@ -556,142 +631,185 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
} }
var key int var key int
if len(pair[0]) == 1 && pair[0][0] == escapedColon { if len(pair[0]) == 1 && pair[0][0] == escapedColon {
key = ':' + curses.AltZ key = ':' + tui.AltZ
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma { } else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
key = ',' + curses.AltZ key = ',' + tui.AltZ
} else if len(pair[0]) == 1 && pair[0][0] == escapedPlus {
key = '+' + tui.AltZ
} else { } else {
keys := parseKeyChords(pair[0], "key name required") keys := parseKeyChords(pair[0], "key name required")
key = firstKey(keys) key = firstKey(keys)
} }
act := origPairStr[len(pair[0])+1 : len(origPairStr)] idx2 := len(pair[0]) + 1
actLower := strings.ToLower(act) specs := strings.Split(pair[1], "+")
switch actLower { actions := make([]action, 0, len(specs))
case "ignore": appendAction := func(types ...actionType) {
keymap[key] = actIgnore actions = append(actions, toActions(types...)...)
case "beginning-of-line": }
keymap[key] = actBeginningOfLine prevSpec := ""
case "abort": for specIndex, maskedSpec := range specs {
keymap[key] = actAbort spec := origPairStr[idx2 : idx2+len(maskedSpec)]
case "accept": idx2 += len(maskedSpec) + 1
keymap[key] = actAccept spec = prevSpec + spec
case "print-query": specLower := strings.ToLower(spec)
keymap[key] = actPrintQuery switch specLower {
case "backward-char": case "ignore":
keymap[key] = actBackwardChar appendAction(actIgnore)
case "backward-delete-char": case "beginning-of-line":
keymap[key] = actBackwardDeleteChar appendAction(actBeginningOfLine)
case "backward-word": case "abort":
keymap[key] = actBackwardWord appendAction(actAbort)
case "clear-screen": case "accept":
keymap[key] = actClearScreen appendAction(actAccept)
case "delete-char": case "accept-non-empty":
keymap[key] = actDeleteChar appendAction(actAcceptNonEmpty)
case "delete-char/eof": case "print-query":
keymap[key] = actDeleteCharEOF appendAction(actPrintQuery)
case "end-of-line": case "replace-query":
keymap[key] = actEndOfLine appendAction(actReplaceQuery)
case "cancel": case "backward-char":
keymap[key] = actCancel appendAction(actBackwardChar)
case "forward-char": case "backward-delete-char":
keymap[key] = actForwardChar appendAction(actBackwardDeleteChar)
case "forward-word": case "backward-word":
keymap[key] = actForwardWord appendAction(actBackwardWord)
case "jump": case "clear-screen":
keymap[key] = actJump appendAction(actClearScreen)
case "jump-accept": case "delete-char":
keymap[key] = actJumpAccept appendAction(actDeleteChar)
case "kill-line": case "delete-char/eof":
keymap[key] = actKillLine appendAction(actDeleteCharEOF)
case "kill-word": case "end-of-line":
keymap[key] = actKillWord appendAction(actEndOfLine)
case "unix-line-discard", "line-discard": case "cancel":
keymap[key] = actUnixLineDiscard appendAction(actCancel)
case "unix-word-rubout", "word-rubout": case "forward-char":
keymap[key] = actUnixWordRubout appendAction(actForwardChar)
case "yank": case "forward-word":
keymap[key] = actYank appendAction(actForwardWord)
case "backward-kill-word": case "jump":
keymap[key] = actBackwardKillWord appendAction(actJump)
case "toggle-down": case "jump-accept":
keymap[key] = actToggleDown appendAction(actJumpAccept)
case "toggle-up": case "kill-line":
keymap[key] = actToggleUp appendAction(actKillLine)
case "toggle-in": case "kill-word":
keymap[key] = actToggleIn appendAction(actKillWord)
case "toggle-out": case "unix-line-discard", "line-discard":
keymap[key] = actToggleOut appendAction(actUnixLineDiscard)
case "toggle-all": case "unix-word-rubout", "word-rubout":
keymap[key] = actToggleAll appendAction(actUnixWordRubout)
case "select-all": case "yank":
keymap[key] = actSelectAll appendAction(actYank)
case "deselect-all": case "backward-kill-word":
keymap[key] = actDeselectAll appendAction(actBackwardKillWord)
case "toggle": case "toggle-down":
keymap[key] = actToggle appendAction(actToggle, actDown)
case "down": case "toggle-up":
keymap[key] = actDown appendAction(actToggle, actUp)
case "up": case "toggle-in":
keymap[key] = actUp appendAction(actToggleIn)
case "page-up": case "toggle-out":
keymap[key] = actPageUp appendAction(actToggleOut)
case "page-down": case "toggle-all":
keymap[key] = actPageDown appendAction(actToggleAll)
case "previous-history": case "select-all":
keymap[key] = actPreviousHistory appendAction(actSelectAll)
case "next-history": case "deselect-all":
keymap[key] = actNextHistory appendAction(actDeselectAll)
case "toggle-preview": case "toggle":
keymap[key] = actTogglePreview appendAction(actToggle)
case "toggle-sort": case "down":
keymap[key] = actToggleSort appendAction(actDown)
default: case "up":
if isExecuteAction(actLower) { appendAction(actUp)
var offset int case "top":
if strings.HasPrefix(actLower, "execute-multi") { appendAction(actTop)
keymap[key] = actExecuteMulti case "page-up":
offset = len("execute-multi") appendAction(actPageUp)
case "page-down":
appendAction(actPageDown)
case "half-page-up":
appendAction(actHalfPageUp)
case "half-page-down":
appendAction(actHalfPageDown)
case "previous-history":
appendAction(actPreviousHistory)
case "next-history":
appendAction(actNextHistory)
case "toggle-preview":
appendAction(actTogglePreview)
case "toggle-preview-wrap":
appendAction(actTogglePreviewWrap)
case "toggle-sort":
appendAction(actToggleSort)
case "preview-up":
appendAction(actPreviewUp)
case "preview-down":
appendAction(actPreviewDown)
case "preview-page-up":
appendAction(actPreviewPageUp)
case "preview-page-down":
appendAction(actPreviewPageDown)
default:
t := isExecuteAction(specLower)
if t == actIgnore {
errorExit("unknown action: " + spec)
} else { } else {
keymap[key] = actExecute var offset int
offset = len("execute") switch t {
case actExecuteSilent:
offset = len("execute-silent")
case actExecuteMulti:
offset = len("execute-multi")
default:
offset = len("execute")
}
if spec[offset] == ':' {
if specIndex == len(specs)-1 {
actions = append(actions, action{t: t, a: spec[offset+1:]})
} else {
prevSpec = spec + "+"
continue
}
} else {
actions = append(actions, action{t: t, a: spec[offset+1 : len(spec)-1]})
}
} }
if act[offset] == ':' {
execmap[key] = act[offset+1:]
} else {
execmap[key] = act[offset+1 : len(act)-1]
}
} else {
errorExit("unknown action: " + act)
} }
prevSpec = ""
} }
keymap[key] = actions
} }
} }
func isExecuteAction(str string) bool { func isExecuteAction(str string) actionType {
if !strings.HasPrefix(str, "execute") || len(str) < len("execute()") { matches := executeRegexp.FindAllStringSubmatch(":"+str, -1)
return false if matches == nil || len(matches) != 1 {
return actIgnore
} }
b := str[len("execute")] prefix := matches[0][1]
if strings.HasPrefix(str, "execute-multi") { if len(prefix) == 0 {
if len(str) < len("execute-multi()") { prefix = matches[0][2]
return false
}
b = str[len("execute-multi")]
} }
e := str[len(str)-1] switch prefix {
if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' || case "execute":
b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") { return actExecute
return true case "execute-silent":
return actExecuteSilent
case "execute-multi":
return actExecuteMulti
} }
return false return actIgnore
} }
func parseToggleSort(keymap map[int]actionType, str string) { func parseToggleSort(keymap map[int][]action, str string) {
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")
} }
keymap[firstKey(keys)] = actToggleSort keymap[firstKey(keys)] = toActions(actToggleSort)
} }
func strLines(str string) []string { func strLines(str string) []string {
@@ -722,39 +840,49 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec {
return sizeSpec{val, percent} return sizeSpec{val, percent}
} }
func parseHeight(str string) sizeSpec {
size := parseSize(str, 100, "height")
return size
}
func parsePreviewWindow(opts *previewOpts, input string) { func parsePreviewWindow(opts *previewOpts, input string) {
layout := input // Default
if strings.HasSuffix(layout, ":hidden") { opts.position = posRight
opts.hidden = true opts.size = sizeSpec{50, true}
layout = strings.TrimSuffix(layout, ":hidden") opts.hidden = false
} opts.wrap = false
tokens := strings.Split(layout, ":") tokens := strings.Split(input, ":")
if len(tokens) == 0 || len(tokens) > 2 { sizeRegex := regexp.MustCompile("^[0-9]+%?$")
errorExit("invalid window layout: " + input) for _, token := range tokens {
} switch token {
case "hidden":
if len(tokens) > 1 { opts.hidden = true
opts.size = parseSize(tokens[1], 99, "window size") case "wrap":
} else { opts.wrap = true
opts.size = sizeSpec{50, true} case "up", "top":
opts.position = posUp
case "down", "bottom":
opts.position = posDown
case "left":
opts.position = posLeft
case "right":
opts.position = posRight
default:
if sizeRegex.MatchString(token) {
opts.size = parseSize(token, 99, "window size")
} else {
errorExit("invalid preview window layout: " + input)
}
}
} }
if !opts.size.percent && opts.size.size > 0 { if !opts.size.percent && opts.size.size > 0 {
// Adjust size for border // Adjust size for border
opts.size.size += 2 opts.size.size += 2
} // And padding
if opts.position == posLeft || opts.position == posRight {
switch tokens[0] { opts.size.size += 2
case "up": }
opts.position = posUp
case "down":
opts.position = posDown
case "left":
opts.position = posLeft
case "right":
opts.position = posRight
default:
errorExit("invalid window position: " + input)
} }
} }
@@ -832,16 +960,26 @@ func parseOptions(opts *Options, allArgs []string) {
case "-f", "--filter": case "-f", "--filter":
filter := nextString(allArgs, &i, "query string required") filter := nextString(allArgs, &i, "query string required")
opts.Filter = &filter opts.Filter = &filter
case "--literal":
opts.Normalize = false
case "--no-literal":
opts.Normalize = true
case "--algo":
opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)"))
case "--expect": case "--expect":
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") for k, v := range parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") {
opts.Expect[k] = v
}
case "--no-expect":
opts.Expect = make(map[int]string)
case "--tiebreak": case "--tiebreak":
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind": case "--bind":
parseKeymap(opts.Keymap, opts.Execmap, nextString(allArgs, &i, "bind expression required")) parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"))
case "--color": case "--color":
spec := optionalNextString(allArgs, &i) spec := optionalNextString(allArgs, &i)
if len(spec) == 0 { if len(spec) == 0 {
opts.Theme = curses.EmptyTheme() opts.Theme = tui.EmptyTheme()
} else { } else {
opts.Theme = parseTheme(opts.Theme, spec) opts.Theme = parseTheme(opts.Theme, spec)
} }
@@ -878,11 +1016,15 @@ func parseOptions(opts *Options, allArgs []string) {
case "+c", "--no-color": case "+c", "--no-color":
opts.Theme = nil opts.Theme = nil
case "+2", "--no-256": case "+2", "--no-256":
opts.Theme = curses.Default16 opts.Theme = tui.Default16
case "--black": case "--black":
opts.Black = true opts.Black = true
case "--no-black": case "--no-black":
opts.Black = false opts.Black = false
case "--bold":
opts.Bold = true
case "--no-bold":
opts.Bold = false
case "--reverse": case "--reverse":
opts.Reverse = true opts.Reverse = true
case "--no-reverse": case "--no-reverse":
@@ -897,6 +1039,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Hscroll = false opts.Hscroll = false
case "--hscroll-off": case "--hscroll-off":
opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required") opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required")
case "--filepath-word":
opts.FileWord = true
case "--no-filepath-word":
opts.FileWord = false
case "--inline-info": case "--inline-info":
opts.InlineInfo = true opts.InlineInfo = true
case "--no-inline-info": case "--no-inline-info":
@@ -916,6 +1062,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.ReadZero = true opts.ReadZero = true
case "--no-read0": case "--no-read0":
opts.ReadZero = false opts.ReadZero = false
case "--print0":
opts.Printer = func(str string) { fmt.Print(str, "\x00") }
case "--no-print0":
opts.Printer = func(str string) { fmt.Println(str) }
case "--print-query": case "--print-query":
opts.PrintQuery = true opts.PrintQuery = true
case "--no-print-query": case "--no-print-query":
@@ -949,18 +1099,34 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Preview.command = "" opts.Preview.command = ""
case "--preview-window": case "--preview-window":
parsePreviewWindow(&opts.Preview, parsePreviewWindow(&opts.Preview,
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]]")) nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:wrap][:hidden]"))
case "--height":
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
case "--min-height":
opts.MinHeight = nextInt(allArgs, &i, "height required: HEIGHT")
case "--no-height":
opts.Height = sizeSpec{}
case "--no-margin": case "--no-margin":
opts.Margin = defaultMargin() opts.Margin = defaultMargin()
case "--no-border":
opts.Bordered = false
case "--border":
opts.Bordered = true
case "--margin": case "--margin":
opts.Margin = parseMargin( opts.Margin = parseMargin(
nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
case "--tabstop": case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required") opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
case "--clear":
opts.ClearOnExit = true
case "--no-clear":
opts.ClearOnExit = false
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, "--algo="); match {
opts.FuzzyAlgo = parseAlgo(value)
} else 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
@@ -974,16 +1140,22 @@ func parseOptions(opts *Options, allArgs []string) {
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, "--height="); match {
opts.Height = parseHeight(value)
} else if match, value := optString(arg, "--min-height="); match {
opts.MinHeight = atoi(value)
} else if match, value := optString(arg, "--toggle-sort="); match { } else if match, value := optString(arg, "--toggle-sort="); match {
parseToggleSort(opts.Keymap, value) parseToggleSort(opts.Keymap, value)
} else if match, value := optString(arg, "--expect="); match { } else if match, value := optString(arg, "--expect="); match {
opts.Expect = parseKeyChords(value, "key names required") for k, v := range parseKeyChords(value, "key names required") {
opts.Expect[k] = v
}
} else if match, value := optString(arg, "--tiebreak="); match { } else if match, value := optString(arg, "--tiebreak="); match {
opts.Criteria = parseTiebreak(value) opts.Criteria = parseTiebreak(value)
} else if match, value := optString(arg, "--color="); match { } else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(opts.Theme, value) opts.Theme = parseTheme(opts.Theme, value)
} else if match, value := optString(arg, "--bind="); match { } else if match, value := optString(arg, "--bind="); match {
parseKeymap(opts.Keymap, opts.Execmap, value) parseKeymap(opts.Keymap, value)
} else if match, value := optString(arg, "--history="); match { } else if match, value := optString(arg, "--history="); match {
setHistory(value) setHistory(value)
} else if match, value := optString(arg, "--history-size="); match { } else if match, value := optString(arg, "--history-size="); match {
@@ -1036,23 +1208,28 @@ func parseOptions(opts *Options, allArgs []string) {
} }
func postProcessOptions(opts *Options) { func postProcessOptions(opts *Options) {
if util.IsWindows() && opts.Height.size > 0 {
errorExit("--height option is currently not supported on Windows")
}
// Default actions for CTRL-N / CTRL-P when --history is set // Default actions for CTRL-N / CTRL-P when --history is set
if opts.History != nil { if opts.History != nil {
if _, prs := opts.Keymap[curses.CtrlP]; !prs { if _, prs := opts.Keymap[tui.CtrlP]; !prs {
opts.Keymap[curses.CtrlP] = actPreviousHistory opts.Keymap[tui.CtrlP] = toActions(actPreviousHistory)
} }
if _, prs := opts.Keymap[curses.CtrlN]; !prs { if _, prs := opts.Keymap[tui.CtrlN]; !prs {
opts.Keymap[curses.CtrlN] = actNextHistory opts.Keymap[tui.CtrlN] = toActions(actNextHistory)
} }
} }
// Extend the default key map // Extend the default key map
keymap := defaultKeymap() keymap := defaultKeymap()
for key, act := range opts.Keymap { for key, actions := range opts.Keymap {
if act == actToggleSort { for _, act := range actions {
opts.ToggleSort = true if act.t == actToggleSort {
opts.ToggleSort = true
}
} }
keymap[key] = act keymap[key] = actions
} }
opts.Keymap = keymap opts.Keymap = keymap

View File

@@ -2,9 +2,10 @@ package fzf
import ( import (
"fmt" "fmt"
"io/ioutil"
"testing" "testing"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/tui"
) )
func TestDelimiterRegex(t *testing.T) { func TestDelimiterRegex(t *testing.T) {
@@ -42,24 +43,24 @@ func TestDelimiterRegex(t *testing.T) {
func TestDelimiterRegexString(t *testing.T) { func TestDelimiterRegexString(t *testing.T) {
delim := delimiterRegexp("*") delim := delimiterRegexp("*")
tokens := Tokenize([]rune("-*--*---**---"), delim) tokens := Tokenize("-*--*---**---", delim)
if delim.regex != nil || if delim.regex != nil ||
string(tokens[0].text) != "-*" || tokens[0].text.ToString() != "-*" ||
string(tokens[1].text) != "--*" || tokens[1].text.ToString() != "--*" ||
string(tokens[2].text) != "---*" || tokens[2].text.ToString() != "---*" ||
string(tokens[3].text) != "*" || tokens[3].text.ToString() != "*" ||
string(tokens[4].text) != "---" { tokens[4].text.ToString() != "---" {
t.Errorf("%s %s %d", delim, tokens, len(tokens)) t.Errorf("%s %s %d", delim, tokens, len(tokens))
} }
} }
func TestDelimiterRegexRegex(t *testing.T) { func TestDelimiterRegexRegex(t *testing.T) {
delim := delimiterRegexp("--\\*") delim := delimiterRegexp("--\\*")
tokens := Tokenize([]rune("-*--*---**---"), delim) tokens := Tokenize("-*--*---**---", delim)
if delim.str != nil || if delim.str != nil ||
string(tokens[0].text) != "-*--*" || tokens[0].text.ToString() != "-*--*" ||
string(tokens[1].text) != "---*" || tokens[1].text.ToString() != "---*" ||
string(tokens[2].text) != "*---" { tokens[2].text.ToString() != "*---" {
t.Errorf("%s %d", tokens, len(tokens)) t.Errorf("%s %d", tokens, len(tokens))
} }
} }
@@ -123,57 +124,58 @@ func TestIrrelevantNth(t *testing.T) {
} }
func TestParseKeys(t *testing.T) { func TestParseKeys(t *testing.T) {
pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ALT-enter,alt-SPACE", "") pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
check := func(i int, s string) { check := func(i int, s string) {
if pairs[i] != s { if pairs[i] != s {
t.Errorf("%s != %s", pairs[i], s) t.Errorf("%s != %s", pairs[i], s)
} }
} }
if len(pairs) != 11 { if len(pairs) != 12 {
t.Error(11) t.Error(12)
} }
check(curses.CtrlZ, "ctrl-z") check(tui.CtrlZ, "ctrl-z")
check(curses.AltZ, "alt-z") check(tui.AltZ, "alt-z")
check(curses.F2, "f2") check(tui.F2, "f2")
check(curses.AltZ+'@', "@") check(tui.AltZ+'@', "@")
check(curses.AltA, "Alt-a") check(tui.AltA, "Alt-a")
check(curses.AltZ+'!', "!") check(tui.AltZ+'!', "!")
check(curses.CtrlA+'g'-'a', "ctrl-G") check(tui.CtrlA+'g'-'a', "ctrl-G")
check(curses.AltZ+'J', "J") check(tui.AltZ+'J', "J")
check(curses.AltZ+'g', "g") check(tui.AltZ+'g', "g")
check(curses.AltEnter, "ALT-enter") check(tui.CtrlAltA, "ctrl-alt-a")
check(curses.AltSpace, "alt-SPACE") check(tui.CtrlAltM, "ALT-enter")
check(tui.AltSpace, "alt-SPACE")
// Synonyms // Synonyms
pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
if len(pairs) != 9 { if len(pairs) != 9 {
t.Error(9) t.Error(9)
} }
check(curses.CtrlM, "Return") check(tui.CtrlM, "Return")
check(curses.AltZ+' ', "space") check(tui.AltZ+' ', "space")
check(curses.Tab, "tab") check(tui.Tab, "tab")
check(curses.BTab, "btab") check(tui.BTab, "btab")
check(curses.ESC, "esc") check(tui.ESC, "esc")
check(curses.Up, "up") check(tui.Up, "up")
check(curses.Down, "down") check(tui.Down, "down")
check(curses.Left, "left") check(tui.Left, "left")
check(curses.Right, "right") check(tui.Right, "right")
pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "") pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
if len(pairs) != 11 { if len(pairs) != 11 {
t.Error(11) t.Error(11)
} }
check(curses.Tab, "Ctrl-I") check(tui.Tab, "Ctrl-I")
check(curses.PgUp, "page-up") check(tui.PgUp, "page-up")
check(curses.PgDn, "Page-Down") check(tui.PgDn, "Page-Down")
check(curses.Home, "Home") check(tui.Home, "Home")
check(curses.End, "End") check(tui.End, "End")
check(curses.AltBS, "Alt-BSpace") check(tui.AltBS, "Alt-BSpace")
check(curses.SLeft, "shift-left") check(tui.SLeft, "shift-left")
check(curses.SRight, "shift-right") check(tui.SRight, "shift-right")
check(curses.BTab, "shift-tab") check(tui.BTab, "shift-tab")
check(curses.CtrlM, "Enter") check(tui.CtrlM, "Enter")
check(curses.BSpace, "bspace") check(tui.BSpace, "bspace")
} }
func TestParseKeysWithComma(t *testing.T) { func TestParseKeysWithComma(t *testing.T) {
@@ -190,86 +192,88 @@ func TestParseKeysWithComma(t *testing.T) {
pairs := parseKeyChords(",", "") pairs := parseKeyChords(",", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords(",,a,b", "") pairs = parseKeyChords(",,a,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, curses.AltZ+'a', "a") check(pairs, tui.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b") check(pairs, tui.AltZ+'b', "b")
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords("a,b,,", "") pairs = parseKeyChords("a,b,,", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, curses.AltZ+'a', "a") check(pairs, tui.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b") check(pairs, tui.AltZ+'b', "b")
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords("a,,,b", "") pairs = parseKeyChords("a,,,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, curses.AltZ+'a', "a") check(pairs, tui.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b") check(pairs, tui.AltZ+'b', "b")
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords("a,,,b,c", "") pairs = parseKeyChords("a,,,b,c", "")
checkN(len(pairs), 4) checkN(len(pairs), 4)
check(pairs, curses.AltZ+'a', "a") check(pairs, tui.AltZ+'a', "a")
check(pairs, curses.AltZ+'b', "b") check(pairs, tui.AltZ+'b', "b")
check(pairs, curses.AltZ+'c', "c") check(pairs, tui.AltZ+'c', "c")
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
pairs = parseKeyChords(",,,", "") pairs = parseKeyChords(",,,", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, curses.AltZ+',', ",") check(pairs, tui.AltZ+',', ",")
} }
func TestBind(t *testing.T) { 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() keymap := defaultKeymap()
execmap := make(map[int]string) check := func(keyName int, arg1 string, types ...actionType) {
check(actBeginningOfLine, keymap[curses.CtrlA]) if len(keymap[keyName]) != len(types) {
parseKeymap(keymap, execmap, t.Errorf("invalid number of actions (%d != %d)", len(types), len(keymap[keyName]))
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+ return
"f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ }
"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ for idx, action := range keymap[keyName] {
",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)") if types[idx] != action.t {
check(actKillLine, keymap[curses.CtrlA]) t.Errorf("invalid action type (%d != %d)", types[idx], action.t)
check(actToggleSort, keymap[curses.CtrlB]) }
check(actPageUp, keymap[curses.AltZ+'c']) }
check(actAbort, keymap[curses.AltZ+',']) if len(arg1) > 0 && keymap[keyName][0].a != arg1 {
check(actAccept, keymap[curses.AltZ+':']) t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[keyName][0].a)
check(actPageDown, keymap[curses.AltZ]) }
check(actExecute, keymap[curses.F1]) }
check(actExecute, keymap[curses.F2]) check(tui.CtrlA, "", actBeginningOfLine)
check(actExecute, keymap[curses.F3]) parseKeymap(keymap,
check(actExecute, keymap[curses.F4]) "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
checkString("ls {}", execmap[curses.F1]) "f1:execute(ls {})+abort,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
checkString("echo {}, {}, {}", execmap[curses.F2]) "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
checkString("echo '({})'", execmap[curses.F3]) "x:Execute(foo+bar),X:execute/bar+baz/"+
checkString("less {}", execmap[curses.F4]) ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA]) check(tui.CtrlA, "", actKillLine)
checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB]) check(tui.CtrlB, "", actToggleSort, actUp, actDown)
checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X']) check(tui.AltZ+'c', "", actPageUp)
check(tui.AltZ+',', "", actAbort)
check(tui.AltZ+':', "", actAccept)
check(tui.AltZ, "", actPageDown)
check(tui.F1, "ls {}", actExecute, actAbort)
check(tui.F2, "echo {}, {}, {}", actExecute)
check(tui.F3, "echo '({})'", actExecute)
check(tui.F4, "less {}", actExecute)
check(tui.AltZ+'x', "foo+bar", actExecute)
check(tui.AltZ+'X', "bar+baz", actExecute)
check(tui.AltA, "echo (,),[,],/,:,;,%,{}", actExecuteMulti)
check(tui.AltB, "echo (,),[,],/,:,@,%,{}", actExecute)
check(tui.AltZ+'+', "++\nfoobar,Y:execute(baz)+up", actExecute)
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) check(tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
} }
parseKeymap(keymap, execmap, "f1:abort") parseKeymap(keymap, "f1:abort")
check(actAbort, keymap[curses.F1]) check(tui.F1, "", actAbort)
} }
func TestColorSpec(t *testing.T) { func TestColorSpec(t *testing.T) {
theme := curses.Dark256 theme := tui.Dark256
dark := parseTheme(theme, "dark") dark := parseTheme(theme, "dark")
if *dark != *theme { if *dark != *theme {
t.Errorf("colors should be equivalent") t.Errorf("colors should be equivalent")
@@ -282,7 +286,7 @@ func TestColorSpec(t *testing.T) {
if *light == *theme { if *light == *theme {
t.Errorf("should not be equivalent") t.Errorf("should not be equivalent")
} }
if *light != *curses.Light256 { if *light != *tui.Light256 {
t.Errorf("colors should be equivalent") t.Errorf("colors should be equivalent")
} }
if light == theme { if light == theme {
@@ -293,29 +297,23 @@ func TestColorSpec(t *testing.T) {
if customized.Fg != 231 || customized.Bg != 232 { if customized.Fg != 231 || customized.Bg != 232 {
t.Errorf("color not customized") t.Errorf("color not customized")
} }
if *curses.Dark256 == *customized { if *tui.Dark256 == *customized {
t.Errorf("colors should not be equivalent") t.Errorf("colors should not be equivalent")
} }
customized.Fg = curses.Dark256.Fg customized.Fg = tui.Dark256.Fg
customized.Bg = curses.Dark256.Bg customized.Bg = tui.Dark256.Bg
if *curses.Dark256 == *customized { if *tui.Dark256 != *customized {
t.Errorf("colors should now be equivalent") t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
} }
customized = parseTheme(theme, "fg:231,dark,bg:232") customized = parseTheme(theme, "fg:231,dark,bg:232")
if customized.Fg != curses.Dark256.Fg || customized.Bg == curses.Dark256.Bg { if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
t.Errorf("color not customized") t.Errorf("color not customized")
} }
if customized.UseDefault {
t.Errorf("not using default colors")
}
if !curses.Dark256.UseDefault {
t.Errorf("using default colors")
}
} }
func TestParseNilTheme(t *testing.T) { func TestParseNilTheme(t *testing.T) {
var theme *curses.ColorTheme var theme *tui.ColorTheme
newTheme := parseTheme(theme, "prompt:12") newTheme := parseTheme(theme, "prompt:12")
if newTheme != nil { if newTheme != nil {
t.Errorf("color is disabled. keep it that way.") t.Errorf("color is disabled. keep it that way.")
@@ -331,35 +329,37 @@ func TestDefaultCtrlNP(t *testing.T) {
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if opts.Keymap[key] != expected { if opts.Keymap[key][0].t != expected {
t.Error() t.Error()
} }
} }
check([]string{}, curses.CtrlN, actDown) check([]string{}, tui.CtrlN, actDown)
check([]string{}, curses.CtrlP, actUp) check([]string{}, tui.CtrlP, actUp)
check([]string{"--bind=ctrl-n:accept"}, curses.CtrlN, actAccept) check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
check([]string{"--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
hist := "--history=/tmp/foo" f, _ := ioutil.TempFile("", "fzf-history")
check([]string{hist}, curses.CtrlN, actNextHistory) f.Close()
check([]string{hist}, curses.CtrlP, actPreviousHistory) hist := "--history=" + f.Name()
check([]string{hist}, tui.CtrlN, actNextHistory)
check([]string{hist}, tui.CtrlP, actPreviousHistory)
check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlN, actAccept) check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlP, actPreviousHistory) check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPreviousHistory)
check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlN, actNextHistory) check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory)
check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
}
func optsFor(words ...string) *Options {
opts := defaultOptions()
parseOptions(opts, words)
postProcessOptions(opts)
return opts
} }
func TestToggle(t *testing.T) { func TestToggle(t *testing.T) {
optsFor := func(words ...string) *Options {
opts := defaultOptions()
parseOptions(opts, words)
postProcessOptions(opts)
return opts
}
opts := optsFor() opts := optsFor()
if opts.ToggleSort { if opts.ToggleSort {
t.Error() t.Error()
@@ -375,3 +375,49 @@ func TestToggle(t *testing.T) {
t.Error() t.Error()
} }
} }
func TestPreviewOpts(t *testing.T) {
opts := optsFor()
if !(opts.Preview.command == "" &&
opts.Preview.hidden == false &&
opts.Preview.wrap == false &&
opts.Preview.position == posRight &&
opts.Preview.size.percent == true &&
opts.Preview.size.size == 50) {
t.Error()
}
opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden:wrap")
if !(opts.Preview.command == "cat {}" &&
opts.Preview.hidden == true &&
opts.Preview.wrap == true &&
opts.Preview.position == posLeft &&
opts.Preview.size.percent == false &&
opts.Preview.size.size == 15+2+2) {
t.Error(opts.Preview)
}
opts = optsFor("--preview-window=up:15:wrap:hidden", "--preview-window=down")
if !(opts.Preview.command == "" &&
opts.Preview.hidden == false &&
opts.Preview.wrap == false &&
opts.Preview.position == posDown &&
opts.Preview.size.percent == true &&
opts.Preview.size.size == 50) {
t.Error(opts.Preview)
}
opts = optsFor("--preview-window=up:15:wrap:hidden")
if !(opts.Preview.command == "" &&
opts.Preview.hidden == true &&
opts.Preview.wrap == true &&
opts.Preview.position == posUp &&
opts.Preview.size.percent == false &&
opts.Preview.size.size == 15+2) {
t.Error(opts.Preview)
}
}
func TestAdditiveExpect(t *testing.T) {
opts := optsFor("--expect=a", "--expect", "b", "--expect=c")
if len(opts.Expect) != 3 {
t.Error(opts.Expect)
}
}

View File

@@ -2,7 +2,6 @@ package fzf
import ( import (
"regexp" "regexp"
"sort"
"strings" "strings"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
@@ -11,12 +10,12 @@ import (
// fuzzy // fuzzy
// 'exact // 'exact
// ^exact-prefix // ^prefix-exact
// exact-suffix$ // suffix-exact$
// !not-fuzzy // !inverse-exact
// !'not-exact // !'inverse-fuzzy
// !^not-exact-prefix // !^inverse-prefix-exact
// !not-exact-suffix$ // !inverse-suffix-exact$
type termType int type termType int
@@ -33,7 +32,6 @@ type term struct {
inv bool inv bool
text []rune text []rune
caseSensitive bool caseSensitive bool
origText []rune
} }
type termSet []term type termSet []term
@@ -41,15 +39,18 @@ type termSet []term
// Pattern represents search pattern // Pattern represents search pattern
type Pattern struct { type Pattern struct {
fuzzy bool fuzzy bool
fuzzyAlgo algo.Algo
extended bool extended bool
caseSensitive bool caseSensitive bool
normalize bool
forward bool forward bool
text []rune text []rune
termSets []termSet termSets []termSet
cacheable bool cacheable bool
cacheKey string
delimiter Delimiter delimiter Delimiter
nth []Range nth []Range
procFun map[termType]func(bool, bool, []rune, []rune) algo.Result procFun map[termType]algo.Algo
} }
var ( var (
@@ -59,7 +60,7 @@ var (
) )
func init() { func init() {
_splitRegex = regexp.MustCompile("\\s+") _splitRegex = regexp.MustCompile(" +")
clearPatternCache() clearPatternCache()
clearChunkCache() clearChunkCache()
} }
@@ -75,12 +76,15 @@ func clearChunkCache() {
} }
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool, func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
nth []Range, delimiter Delimiter, runes []rune) *Pattern { cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string var asString string
if extended { if extended {
asString = strings.Trim(string(runes), " ") asString = strings.TrimLeft(string(runes), " ")
for strings.HasSuffix(asString, " ") && !strings.HasSuffix(asString, "\\ ") {
asString = asString[:len(asString)-1]
}
} else { } else {
asString = string(runes) asString = string(runes)
} }
@@ -90,17 +94,17 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
return cached return cached
} }
caseSensitive, cacheable := true, true caseSensitive := true
termSets := []termSet{} termSets := []termSet{}
if extended { if extended {
termSets = parseTerms(fuzzy, caseMode, asString) termSets = parseTerms(fuzzy, caseMode, normalize, asString)
Loop: Loop:
for _, termSet := range termSets { for _, termSet := range termSets {
for idx, term := range termSet { for idx, term := range termSet {
// If the query contains inverse search terms or OR operators, // If the query contains inverse search terms or OR operators,
// we cannot cache the search scope // we cannot cache the search scope
if idx > 0 || term.inv { if !cacheable || idx > 0 || term.inv || fuzzy && term.typ != termFuzzy || !fuzzy && term.typ != termExact {
cacheable = false cacheable = false
break Loop break Loop
} }
@@ -117,17 +121,20 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
ptr := &Pattern{ ptr := &Pattern{
fuzzy: fuzzy, fuzzy: fuzzy,
fuzzyAlgo: fuzzyAlgo,
extended: extended, extended: extended,
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
normalize: normalize,
forward: forward, forward: forward,
text: []rune(asString), text: []rune(asString),
termSets: termSets, termSets: termSets,
cacheable: cacheable, cacheable: cacheable,
nth: nth, nth: nth,
delimiter: delimiter, delimiter: delimiter,
procFun: make(map[termType]func(bool, bool, []rune, []rune) algo.Result)} procFun: make(map[termType]algo.Algo)}
ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.cacheKey = ptr.buildCacheKey()
ptr.procFun[termFuzzy] = fuzzyAlgo
ptr.procFun[termEqual] = algo.EqualMatch 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
@@ -137,37 +144,46 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
return ptr return ptr
} }
func parseTerms(fuzzy bool, caseMode Case, str string) []termSet { func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
str = strings.Replace(str, "\\ ", "\t", -1)
tokens := _splitRegex.Split(str, -1) tokens := _splitRegex.Split(str, -1)
sets := []termSet{} sets := []termSet{}
set := termSet{} set := termSet{}
switchSet := false switchSet := false
afterBar := false
for _, token := range tokens { for _, token := range tokens {
typ, inv, text := termFuzzy, false, token typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1)
lowerText := strings.ToLower(text) lowerText := strings.ToLower(text)
caseSensitive := caseMode == CaseRespect || caseSensitive := caseMode == CaseRespect ||
caseMode == CaseSmart && text != lowerText caseMode == CaseSmart && text != lowerText
if !caseSensitive { if !caseSensitive {
text = lowerText text = lowerText
} }
origText := []rune(text)
if !fuzzy { if !fuzzy {
typ = termExact typ = termExact
} }
if text == "|" { if len(set) > 0 && !afterBar && text == "|" {
switchSet = false switchSet = false
afterBar = true
continue continue
} }
afterBar = false
if strings.HasPrefix(text, "!") { if strings.HasPrefix(text, "!") {
inv = true inv = true
typ = termExact
text = text[1:] text = text[1:]
} }
if text != "$" && strings.HasSuffix(text, "$") {
typ = termSuffix
text = text[:len(text)-1]
}
if strings.HasPrefix(text, "'") { if strings.HasPrefix(text, "'") {
// Flip exactness // Flip exactness
if fuzzy { if fuzzy && !inv {
typ = termExact typ = termExact
text = text[1:] text = text[1:]
} else { } else {
@@ -175,16 +191,12 @@ func parseTerms(fuzzy bool, caseMode Case, str string) []termSet {
text = text[1:] text = text[1:]
} }
} else if strings.HasPrefix(text, "^") { } else if strings.HasPrefix(text, "^") {
if strings.HasSuffix(text, "$") { if typ == termSuffix {
typ = termEqual typ = termEqual
text = text[1 : len(text)-1]
} else { } else {
typ = termPrefix typ = termPrefix
text = text[1:]
} }
} else if strings.HasSuffix(text, "$") { text = text[1:]
typ = termSuffix
text = text[:len(text)-1]
} }
if len(text) > 0 { if len(text) > 0 {
@@ -192,12 +204,15 @@ func parseTerms(fuzzy bool, caseMode Case, str string) []termSet {
sets = append(sets, set) sets = append(sets, set)
set = termSet{} set = termSet{}
} }
textRunes := []rune(text)
if normalize {
textRunes = algo.NormalizeRunes(textRunes)
}
set = append(set, term{ set = append(set, term{
typ: typ, typ: typ,
inv: inv, inv: inv,
text: []rune(text), text: textRunes,
caseSensitive: caseSensitive, caseSensitive: caseSensitive})
origText: origText})
switchSet = true switchSet = true
} }
} }
@@ -220,50 +235,38 @@ func (p *Pattern) AsString() string {
return string(p.text) return string(p.text)
} }
// CacheKey is used to build string to be used as the key of result cache func (p *Pattern) buildCacheKey() string {
func (p *Pattern) CacheKey() string {
if !p.extended { if !p.extended {
return p.AsString() return p.AsString()
} }
cacheableTerms := []string{} cacheableTerms := []string{}
for _, termSet := range p.termSets { for _, termSet := range p.termSets {
if len(termSet) == 1 && !termSet[0].inv && (p.fuzzy || termSet[0].typ == termExact) { if len(termSet) == 1 && !termSet[0].inv && (p.fuzzy || termSet[0].typ == termExact) {
cacheableTerms = append(cacheableTerms, string(termSet[0].origText)) cacheableTerms = append(cacheableTerms, string(termSet[0].text))
} }
} }
return strings.Join(cacheableTerms, " ") return strings.Join(cacheableTerms, "\t")
}
// CacheKey is used to build string to be used as the key of result cache
func (p *Pattern) CacheKey() string {
return p.cacheKey
} }
// Match returns the list of matches Items in the given Chunk // Match returns the list of matches Items in the given Chunk
func (p *Pattern) Match(chunk *Chunk) []*Item { func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
space := chunk
// ChunkCache: Exact match // ChunkCache: Exact match
cacheKey := p.CacheKey() cacheKey := p.CacheKey()
if p.cacheable { if p.cacheable {
if cached, found := _cache.Find(chunk, cacheKey); found { if cached := _cache.Lookup(chunk, cacheKey); cached != nil {
return cached return cached
} }
} }
// ChunkCache: Prefix/suffix match // Prefix/suffix cache
Loop: space := _cache.Search(chunk, cacheKey)
for idx := 1; idx < len(cacheKey); idx++ {
// [---------| ] | [ |---------]
// [--------| ] | [ |--------]
// [-------| ] | [ |-------]
prefix := cacheKey[:len(cacheKey)-idx]
suffix := cacheKey[idx:]
for _, substr := range [2]*string{&prefix, &suffix} {
if cached, found := _cache.Find(chunk, *substr); found {
cachedChunk := Chunk(cached)
space = &cachedChunk
break Loop
}
}
}
matches := p.matchChunk(space) matches := p.matchChunk(chunk, space, slab)
if p.cacheable { if p.cacheable {
_cache.Add(chunk, cacheKey, matches) _cache.Add(chunk, cacheKey, matches)
@@ -271,20 +274,19 @@ Loop:
return matches return matches
} }
func (p *Pattern) matchChunk(chunk *Chunk) []*Item { func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
matches := []*Item{} matches := []Result{}
if !p.extended {
for _, item := range *chunk { if space == nil {
offset, bonus := p.basicMatch(item) for idx := 0; idx < chunk.count; idx++ {
if sidx := offset[0]; sidx >= 0 { if match, _, _ := p.MatchItem(&chunk.items[idx], false, slab); match != nil {
matches = append(matches, matches = append(matches, *match)
dupItem(item, []Offset{offset}, bonus))
} }
} }
} else { } else {
for _, item := range *chunk { for _, result := range space {
if offsets, bonus := p.extendedMatch(item); len(offsets) == len(p.termSets) { if match, _, _ := p.MatchItem(result.item, false, slab); match != nil {
matches = append(matches, dupItem(item, offsets, bonus)) matches = append(matches, *match)
} }
} }
} }
@@ -292,91 +294,109 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
} }
// MatchItem returns true if the Item is a match // MatchItem returns true if the Item is a match
func (p *Pattern) MatchItem(item *Item) bool { func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
if !p.extended { if p.extended {
offset, _ := p.basicMatch(item) if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
sidx := offset[0] result := buildResult(item, offsets, bonus)
return sidx >= 0 return &result, offsets, pos
}
return nil, nil, nil
} }
offsets, _ := p.extendedMatch(item) offset, bonus, pos := p.basicMatch(item, withPos, slab)
return len(offsets) == len(p.termSets) if sidx := offset[0]; sidx >= 0 {
offsets := []Offset{offset}
result := buildResult(item, offsets, bonus)
return &result, offsets, pos
}
return nil, nil, nil
} }
func dupItem(item *Item, offsets []Offset, bonus int32) *Item { func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
sort.Sort(ByOrder(offsets)) var input []Token
return &Item{ if len(p.nth) == 0 {
text: item.text, input = []Token{Token{text: &item.text, prefixLength: 0}}
origText: item.origText, } else {
transformed: item.transformed, input = p.transformInput(item)
offsets: offsets, }
bonus: bonus,
colors: item.colors,
rank: buildEmptyRank(item.Index())}
}
func (p *Pattern) basicMatch(item *Item) (Offset, int32) {
input := p.prepareInput(item)
if p.fuzzy { if p.fuzzy {
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
} }
return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text) return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
} }
func (p *Pattern) extendedMatch(item *Item) ([]Offset, int32) { func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) {
input := p.prepareInput(item) var input []Token
if len(p.nth) == 0 {
input = []Token{Token{text: &item.text, prefixLength: 0}}
} else {
input = p.transformInput(item)
}
offsets := []Offset{} offsets := []Offset{}
var totalBonus int32 var totalScore int
var allPos *[]int
if withPos {
allPos = &[]int{}
}
for _, termSet := range p.termSets { for _, termSet := range p.termSets {
var offset *Offset var offset Offset
var bonus int32 var currentScore int
matched := false
for _, term := range termSet { for _, term := range termSet {
pfun := p.procFun[term.typ] pfun := p.procFun[term.typ]
off, pen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text) off, score, pos := p.iter(pfun, input, term.caseSensitive, p.normalize, p.forward, term.text, withPos, slab)
if sidx := off[0]; sidx >= 0 { if sidx := off[0]; sidx >= 0 {
if term.inv { if term.inv {
continue continue
} }
offset, bonus = &off, pen offset, currentScore = off, score
matched = true
if withPos {
if pos != nil {
*allPos = append(*allPos, *pos...)
} else {
for idx := off[0]; idx < off[1]; idx++ {
*allPos = append(*allPos, int(idx))
}
}
}
break break
} else if term.inv { } else if term.inv {
offset, bonus = &Offset{0, 0, 0}, 0 offset, currentScore = Offset{0, 0}, 0
matched = true
continue continue
} }
} }
if offset != nil { if matched {
offsets = append(offsets, *offset) offsets = append(offsets, offset)
totalBonus += bonus totalScore += currentScore
} }
} }
return offsets, totalBonus return offsets, totalScore, allPos
} }
func (p *Pattern) prepareInput(item *Item) []Token { func (p *Pattern) transformInput(item *Item) []Token {
if item.transformed != nil { if item.transformed != nil {
return item.transformed return *item.transformed
} }
var ret []Token tokens := Tokenize(item.text.ToString(), p.delimiter)
if len(p.nth) > 0 { ret := Transform(tokens, p.nth)
tokens := Tokenize(item.text, p.delimiter) item.transformed = &ret
ret = Transform(tokens, p.nth)
} else {
ret = []Token{Token{text: item.text, prefixLength: 0, trimLength: util.TrimLen(item.text)}}
}
item.transformed = ret
return ret return ret
} }
func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) algo.Result, func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int32) {
for _, part := range tokens { for _, part := range tokens {
prefixLength := int32(part.prefixLength) if res, pos := pfun(caseSensitive, normalize, forward, part.text, pattern, withPos, slab); res.Start >= 0 {
if res := pfun(caseSensitive, forward, part.text, pattern); res.Start >= 0 { sidx := int32(res.Start) + part.prefixLength
var sidx int32 = res.Start + prefixLength eidx := int32(res.End) + part.prefixLength
var eidx int32 = res.End + prefixLength if pos != nil {
return Offset{sidx, eidx, int32(part.trimLength)}, res.Bonus for idx := range *pos {
(*pos)[idx] += int(part.prefixLength)
}
}
return Offset{sidx, eidx}, res.Score, pos
} }
} }
// TODO: math.MaxUint16 return Offset{-1, -1}, 0, nil
return Offset{-1, -1, -1}, 0.0
} }

View File

@@ -5,45 +5,44 @@ import (
"testing" "testing"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/util"
) )
var slab *util.Slab
func init() {
slab = util.MakeSlab(slab16Size, slab32Size)
}
func TestParseTermsExtended(t *testing.T) { func TestParseTermsExtended(t *testing.T) {
terms := parseTerms(true, CaseSmart, terms := parseTerms(true, CaseSmart, false,
"| aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | | zzz$ | !ZZZ |") "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | zzz$ | !ZZZ |")
if len(terms) != 9 || if len(terms) != 9 ||
terms[0][0].typ != termFuzzy || terms[0][0].inv || terms[0][0].typ != termFuzzy || terms[0][0].inv ||
terms[1][0].typ != termExact || terms[1][0].inv || terms[1][0].typ != termExact || terms[1][0].inv ||
terms[2][0].typ != termPrefix || terms[2][0].inv || terms[2][0].typ != termPrefix || terms[2][0].inv ||
terms[3][0].typ != termSuffix || terms[3][0].inv || terms[3][0].typ != termSuffix || terms[3][0].inv ||
terms[4][0].typ != termFuzzy || !terms[4][0].inv || terms[4][0].typ != termExact || !terms[4][0].inv ||
terms[5][0].typ != termExact || !terms[5][0].inv || terms[5][0].typ != termFuzzy || !terms[5][0].inv ||
terms[6][0].typ != termPrefix || !terms[6][0].inv || terms[6][0].typ != termPrefix || !terms[6][0].inv ||
terms[7][0].typ != termSuffix || !terms[7][0].inv || terms[7][0].typ != termSuffix || !terms[7][0].inv ||
terms[7][1].typ != termEqual || terms[7][1].inv || terms[7][1].typ != termEqual || terms[7][1].inv ||
terms[8][0].typ != termPrefix || terms[8][0].inv || terms[8][0].typ != termPrefix || terms[8][0].inv ||
terms[8][1].typ != termExact || terms[8][1].inv || terms[8][1].typ != termExact || terms[8][1].inv ||
terms[8][2].typ != termSuffix || terms[8][2].inv || terms[8][2].typ != termSuffix || terms[8][2].inv ||
terms[8][3].typ != termFuzzy || !terms[8][3].inv { terms[8][3].typ != termExact || !terms[8][3].inv {
t.Errorf("%s", terms) t.Errorf("%s", terms)
} }
for idx, termSet := range terms[:8] { for _, termSet := range terms[:8] {
term := termSet[0] term := termSet[0]
if len(term.text) != 3 { if len(term.text) != 3 {
t.Errorf("%s", term) t.Errorf("%s", term)
} }
if idx > 0 && len(term.origText) != 4+idx/5 {
t.Errorf("%s", term)
}
}
for _, term := range terms[8] {
if len(term.origText) != 4 {
t.Errorf("%s", term)
}
} }
} }
func TestParseTermsExtendedExact(t *testing.T) { func TestParseTermsExtendedExact(t *testing.T) {
terms := parseTerms(false, CaseSmart, terms := parseTerms(false, CaseSmart, false,
"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][0].typ != termExact || terms[0][0].inv || len(terms[0][0].text) != 3 || terms[0][0].typ != termExact || terms[0][0].inv || len(terms[0][0].text) != 3 ||
@@ -59,7 +58,7 @@ func TestParseTermsExtendedExact(t *testing.T) {
} }
func TestParseTermsEmpty(t *testing.T) { func TestParseTermsEmpty(t *testing.T) {
terms := parseTerms(true, CaseSmart, "' $ ^ !' !^ !$") terms := parseTerms(true, CaseSmart, false, "' ^ !' !^")
if len(terms) != 0 { if len(terms) != 0 {
t.Errorf("%s", terms) t.Errorf("%s", terms)
} }
@@ -68,26 +67,34 @@ func TestParseTermsEmpty(t *testing.T) {
func TestExact(t *testing.T) { func TestExact(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pattern := BuildPattern(true, true, CaseSmart, true, pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true,
[]Range{}, Delimiter{}, []rune("'abc")) []Range{}, Delimiter{}, []rune("'abc"))
res := algo.ExactMatchNaive( chars := util.ToChars([]byte("aabbcc abc"))
pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.termSets[0][0].text) res, pos := algo.ExactMatchNaive(
pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil)
if res.Start != 7 || res.End != 10 { if res.Start != 7 || res.End != 10 {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
} }
if pos != nil {
t.Errorf("pos is expected to be nil")
}
} }
func TestEqual(t *testing.T) { func TestEqual(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$")) pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int32, eidxExpected int32) { match := func(str string, sidxExpected int, eidxExpected int) {
res := algo.EqualMatch( chars := util.ToChars([]byte(str))
pattern.caseSensitive, pattern.forward, []rune(str), pattern.termSets[0][0].text) res, pos := algo.EqualMatch(
pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil)
if res.Start != sidxExpected || res.End != eidxExpected { if res.Start != sidxExpected || res.End != eidxExpected {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
} }
if pos != nil {
t.Errorf("pos is expected to be nil")
}
} }
match("ABC", -1, -1) match("ABC", -1, -1)
match("AbC", 0, 3) match("AbC", 0, 3)
@@ -96,17 +103,17 @@ func TestEqual(t *testing.T) {
func TestCaseSensitivity(t *testing.T) { func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pat1 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc")) pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat2 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc")) pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat3 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc")) pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat4 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc")) pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat5 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc")) pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat6 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc")) pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, 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 ||
@@ -119,46 +126,81 @@ func TestCaseSensitivity(t *testing.T) {
} }
func TestOrigTextAndTransformed(t *testing.T) { func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg")) pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize([]rune("junegunn"), Delimiter{}) tokens := Tokenize("junegunn", Delimiter{})
trans := Transform(tokens, []Range{Range{1, 1}}) trans := Transform(tokens, []Range{Range{1, 1}})
origRunes := []rune("junegunn.choi") origBytes := []byte("junegunn.choi")
for _, extended := range []bool{false, true} { for _, extended := range []bool{false, true} {
chunk := Chunk{ chunk := Chunk{count: 1}
&Item{ chunk.items[0] = Item{
text: []rune("junegunn"), text: util.ToChars([]byte("junegunn")),
origText: &origRunes, origText: &origBytes,
transformed: trans}, transformed: &trans}
}
pattern.extended = extended pattern.extended = extended
matches := pattern.matchChunk(&chunk) matches := pattern.matchChunk(&chunk, nil, slab) // No cache
if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" || if !(matches[0].item.text.ToString() == "junegunn" &&
matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || string(*matches[0].item.origText) == "junegunn.choi" &&
!reflect.DeepEqual(matches[0].transformed, trans) { reflect.DeepEqual(*matches[0].item.transformed, trans)) {
t.Error("Invalid match result", matches) t.Error("Invalid match result", matches)
} }
match, offsets, pos := pattern.MatchItem(&chunk.items[0], true, slab)
if !(match.item.text.ToString() == "junegunn" &&
string(*match.item.origText) == "junegunn.choi" &&
offsets[0][0] == 0 && offsets[0][1] == 5 &&
reflect.DeepEqual(*match.item.transformed, trans)) {
t.Error("Invalid match result", match, offsets, extended)
}
if !((*pos)[0] == 4 && (*pos)[1] == 0) {
t.Error("Invalid pos array", *pos)
}
} }
} }
func TestCacheKey(t *testing.T) { func TestCacheKey(t *testing.T) {
test := func(extended bool, patStr string, expected string, cacheable bool) { test := func(extended bool, patStr string, expected string, cacheable bool) {
pat := BuildPattern(true, extended, CaseSmart, true, []Range{}, Delimiter{}, []rune(patStr)) clearPatternCache()
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune(patStr))
if pat.CacheKey() != expected { if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
} }
if pat.cacheable != cacheable { if pat.cacheable != cacheable {
t.Errorf("Expected: %s, actual: %s (%s)", cacheable, pat.cacheable, patStr) t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr)
} }
clearPatternCache() clearPatternCache()
} }
test(false, "foo !bar", "foo !bar", true) test(false, "foo !bar", "foo !bar", true)
test(false, "foo | bar !baz", "foo | bar !baz", true) test(false, "foo | bar !baz", "foo | bar !baz", true)
test(true, "foo bar baz", "foo bar baz", true) test(true, "foo bar baz", "foo\tbar\tbaz", true)
test(true, "foo !bar", "foo", false) test(true, "foo !bar", "foo", false)
test(true, "foo !bar baz", "foo baz", false) test(true, "foo !bar baz", "foo\tbaz", false)
test(true, "foo | bar baz", "baz", false) test(true, "foo | bar baz", "baz", false)
test(true, "foo | bar | baz", "", false) test(true, "foo | bar | baz", "", false)
test(true, "foo | bar !baz", "", false) test(true, "foo | bar !baz", "", false)
test(true, "| | | foo", "foo", true) test(true, "| | foo", "", false)
test(true, "| | | foo", "foo", false)
}
func TestCacheable(t *testing.T) {
test := func(fuzzy bool, str string, expected string, cacheable bool) {
clearPatternCache()
pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, true, []Range{}, Delimiter{}, []rune(str))
if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
}
if cacheable != pat.cacheable {
t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable)
}
clearPatternCache()
}
test(true, "foo bar", "foo\tbar", true)
test(true, "foo 'bar", "foo\tbar", false)
test(true, "foo !bar", "foo", false)
test(false, "foo bar", "foo\tbar", true)
test(false, "foo 'bar", "foo", false)
test(false, "foo '", "foo", true)
test(false, "foo 'bar", "foo", false)
test(false, "foo !bar", "foo", false)
} }

View File

@@ -4,6 +4,8 @@ import (
"bufio" "bufio"
"io" "io"
"os" "os"
"sync/atomic"
"time"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -13,20 +15,56 @@ type Reader struct {
pusher func([]byte) bool pusher func([]byte) bool
eventBox *util.EventBox eventBox *util.EventBox
delimNil bool delimNil bool
event int32
}
// NewReader returns new Reader object
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool) *Reader {
return &Reader{pusher, eventBox, delimNil, int32(EvtReady)}
}
func (r *Reader) startEventPoller() {
go func() {
ptr := &r.event
pollInterval := readerPollIntervalMin
for {
if atomic.CompareAndSwapInt32(ptr, int32(EvtReadNew), int32(EvtReady)) {
r.eventBox.Set(EvtReadNew, true)
pollInterval = readerPollIntervalMin
} else if atomic.LoadInt32(ptr) == int32(EvtReadFin) {
return
} else {
pollInterval += readerPollIntervalStep
if pollInterval > readerPollIntervalMax {
pollInterval = readerPollIntervalMax
}
}
time.Sleep(pollInterval)
}
}()
}
func (r *Reader) fin(success bool) {
atomic.StoreInt32(&r.event, int32(EvtReadFin))
r.eventBox.Set(EvtReadFin, success)
} }
// ReadSource reads data from the default command or from standard input // ReadSource reads data from the default command or from standard input
func (r *Reader) ReadSource() { func (r *Reader) ReadSource() {
r.startEventPoller()
var success bool
if util.IsTty() { if util.IsTty() {
cmd := os.Getenv("FZF_DEFAULT_COMMAND") cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 { if len(cmd) == 0 {
cmd = defaultCommand // The default command for *nix requires bash
success = r.readFromCommand("bash", defaultCommand)
} else {
success = r.readFromCommand("sh", cmd)
} }
r.readFromCommand(cmd)
} else { } else {
r.readFromStdin() success = r.readFromStdin()
} }
r.eventBox.Set(EvtReadFin, nil) r.fin(success)
} }
func (r *Reader) feed(src io.Reader) { func (r *Reader) feed(src io.Reader) {
@@ -34,17 +72,23 @@ func (r *Reader) feed(src io.Reader) {
if r.delimNil { if r.delimNil {
delim = '\000' delim = '\000'
} }
reader := bufio.NewReader(src) reader := bufio.NewReaderSize(src, readerBufferSize)
for { for {
// ReadBytes returns err != nil if and only if the returned data does not // ReadBytes returns err != nil if and only if the returned data does not
// end in delim. // end in delim.
bytea, err := reader.ReadBytes(delim) bytea, err := reader.ReadBytes(delim)
if len(bytea) > 0 { byteaLen := len(bytea)
if byteaLen > 0 {
if err == nil { if err == nil {
bytea = bytea[:len(bytea)-1] // get rid of carriage return if under Windows:
if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') {
bytea = bytea[:byteaLen-2]
} else {
bytea = bytea[:byteaLen-1]
}
} }
if r.pusher(bytea) { if r.pusher(bytea) {
r.eventBox.Set(EvtReadNew, nil) atomic.StoreInt32(&r.event, int32(EvtReadNew))
} }
} }
if err != nil { if err != nil {
@@ -53,20 +97,21 @@ func (r *Reader) feed(src io.Reader) {
} }
} }
func (r *Reader) readFromStdin() { func (r *Reader) readFromStdin() bool {
r.feed(os.Stdin) r.feed(os.Stdin)
return true
} }
func (r *Reader) readFromCommand(cmd string) { func (r *Reader) readFromCommand(shell string, cmd string) bool {
listCommand := util.ExecCommand(cmd) listCommand := util.ExecCommandWith(shell, cmd)
out, err := listCommand.StdoutPipe() out, err := listCommand.StdoutPipe()
if err != nil { if err != nil {
return return false
} }
err = listCommand.Start() err = listCommand.Start()
if err != nil { if err != nil {
return return false
} }
defer listCommand.Wait()
r.feed(out) r.feed(out)
return listCommand.Wait() == nil
} }

View File

@@ -2,6 +2,7 @@ package fzf
import ( import (
"testing" "testing"
"time"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -11,7 +12,10 @@ func TestReadFromCommand(t *testing.T) {
eb := util.NewEventBox() eb := util.NewEventBox()
reader := Reader{ reader := Reader{
pusher: func(s []byte) bool { strs = append(strs, string(s)); return true }, pusher: func(s []byte) bool { strs = append(strs, string(s)); return true },
eventBox: eb} eventBox: eb,
event: int32(EvtReady)}
reader.startEventPoller()
// Check EventBox // Check EventBox
if eb.Peek(EvtReadNew) { if eb.Peek(EvtReadNew) {
@@ -19,21 +23,16 @@ func TestReadFromCommand(t *testing.T) {
} }
// Normal command // Normal command
reader.readFromCommand(`echo abc && echo def`) reader.fin(reader.readFromCommand("sh", `echo abc && echo def`))
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" { if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
t.Errorf("%s", strs) t.Errorf("%s", strs)
} }
// Check EventBox again // Check EventBox again
if !eb.Peek(EvtReadNew) { eb.WaitFor(EvtReadFin)
t.Error("EvtReadNew should be set yet")
}
// Wait should return immediately // Wait should return immediately
eb.Wait(func(events *util.Events) { eb.Wait(func(events *util.Events) {
if _, found := (*events)[EvtReadNew]; !found {
t.Errorf("%s", events)
}
events.Clear() events.Clear()
}) })
@@ -42,8 +41,14 @@ func TestReadFromCommand(t *testing.T) {
t.Error("EvtReadNew should not be set yet") t.Error("EvtReadNew should not be set yet")
} }
// Make sure that event poller is finished
time.Sleep(readerPollIntervalMax)
// Restart event poller
reader.startEventPoller()
// Failing command // Failing command
reader.readFromCommand(`no-such-command`) reader.fin(reader.readFromCommand("sh", `no-such-command`))
strs = []string{} strs = []string{}
if len(strs) > 0 { if len(strs) > 0 {
t.Errorf("%s", strs) t.Errorf("%s", strs)
@@ -51,6 +56,9 @@ func TestReadFromCommand(t *testing.T) {
// Check EventBox again // Check EventBox again
if eb.Peek(EvtReadNew) { if eb.Peek(EvtReadNew) {
t.Error("Command failed. EvtReadNew should be set") t.Error("Command failed. EvtReadNew should not be set")
}
if !eb.Peek(EvtReadFin) {
t.Error("EvtReadFin should be set")
} }
} }

226
src/result.go Normal file
View File

@@ -0,0 +1,226 @@
package fzf
import (
"math"
"sort"
"unicode"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)
// Offset holds two 32-bit integers denoting the offsets of a matched substring
type Offset [2]int32
type colorOffset struct {
offset [2]int32
color tui.ColorPair
attr tui.Attr
index int32
}
type Result struct {
item *Item
points [4]uint16
}
func buildResult(item *Item, offsets []Offset, score int) Result {
if len(offsets) > 1 {
sort.Sort(ByOrder(offsets))
}
result := Result{item: item}
numChars := item.text.Length()
minBegin := math.MaxUint16
minEnd := math.MaxUint16
maxEnd := 0
validOffsetFound := false
for _, offset := range offsets {
b, e := int(offset[0]), int(offset[1])
if b < e {
minBegin = util.Min(b, minBegin)
minEnd = util.Min(e, minEnd)
maxEnd = util.Max(e, maxEnd)
validOffsetFound = true
}
}
for idx, criterion := range sortCriteria {
val := uint16(math.MaxUint16)
switch criterion {
case byScore:
// Higher is better
val = math.MaxUint16 - util.AsUint16(score)
case byLength:
val = item.TrimLength()
case byBegin, byEnd:
if validOffsetFound {
whitePrefixLen := 0
for idx := 0; idx < numChars; idx++ {
r := item.text.Get(idx)
whitePrefixLen = idx
if idx == minBegin || !unicode.IsSpace(r) {
break
}
}
if criterion == byBegin {
val = util.AsUint16(minEnd - whitePrefixLen)
} else {
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()))
}
}
}
result.points[3-idx] = val
}
return result
}
// Sort criteria to use. Never changes once fzf is started.
var sortCriteria []criterion
// Index returns ordinal index of the Item
func (result *Result) Index() int32 {
return result.item.Index()
}
func minRank() Result {
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
}
func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset {
itemColors := result.item.Colors()
// No ANSI code, or --color=no
if len(itemColors) == 0 {
var offsets []colorOffset
for _, off := range matchOffsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, attr: attr})
}
return offsets
}
// Find max column
var maxCol int32
for _, off := range matchOffsets {
if off[1] > maxCol {
maxCol = off[1]
}
}
for _, ansi := range itemColors {
if ansi.offset[1] > maxCol {
maxCol = ansi.offset[1]
}
}
cols := make([]int, maxCol)
for colorIndex, ansi := range itemColors {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = colorIndex + 1 // XXX
}
}
for _, off := range matchOffsets {
for i := off[0]; i < off[1]; i++ {
cols[i] = -1
}
}
// sort.Sort(ByOrder(offsets))
// Merge offsets
// ------------ ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
curr := 0
start := 0
var colors []colorOffset
add := func(idx int) {
if curr != 0 && idx > start {
if curr == -1 {
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color, attr: attr})
} else {
ansi := itemColors[curr-1]
fg := ansi.color.fg
bg := ansi.color.bg
if theme != nil {
if fg == -1 {
if current {
fg = theme.Current
} else {
fg = theme.Fg
}
}
if bg == -1 {
if current {
bg = theme.DarkBg
} else {
bg = theme.Bg
}
}
}
colors = append(colors, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
color: tui.NewColorPair(fg, bg),
attr: ansi.color.attr.Merge(attr)})
}
}
}
for idx, col := range cols {
if col != curr {
add(idx)
start = idx
curr = col
}
}
add(int(maxCol))
return colors
}
// ByOrder is for sorting substring offsets
type ByOrder []Offset
func (a ByOrder) Len() int {
return len(a)
}
func (a ByOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByOrder) Less(i, j int) bool {
ioff := a[i]
joff := a[j]
return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
}
// ByRelevance is for sorting Items
type ByRelevance []Result
func (a ByRelevance) Len() int {
return len(a)
}
func (a ByRelevance) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevance) Less(i, j int) bool {
return compareRanks(a[i], a[j], false)
}
// ByRelevanceTac is for sorting Items
type ByRelevanceTac []Result
func (a ByRelevanceTac) Len() int {
return len(a)
}
func (a ByRelevanceTac) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevanceTac) Less(i, j int) bool {
return compareRanks(a[i], a[j], true)
}

16
src/result_others.go Normal file
View File

@@ -0,0 +1,16 @@
// +build !386,!amd64
package fzf
func compareRanks(irank Result, jrank Result, tac bool) bool {
for idx := 3; idx >= 0; idx-- {
left := irank.points[idx]
right := jrank.points[idx]
if left < right {
return true
} else if left > right {
return false
}
}
return (irank.item.Index() <= jrank.item.Index()) != tac
}

136
src/result_test.go Normal file
View File

@@ -0,0 +1,136 @@
// +build !tcell
package fzf
import (
"math"
"sort"
"testing"
"github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
)
func withIndex(i *Item, index int) *Item {
(*i).text.Index = int32(index)
return i
}
func TestOffsetSort(t *testing.T) {
offsets := []Offset{
Offset{3, 5}, Offset{2, 7},
Offset{1, 3}, Offset{2, 9}}
sort.Sort(ByOrder(offsets))
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
offsets[1][0] != 2 || offsets[1][1] != 7 ||
offsets[2][0] != 2 || offsets[2][1] != 9 ||
offsets[3][0] != 3 || offsets[3][1] != 5 {
t.Error("Invalid order:", offsets)
}
}
func TestRankComparison(t *testing.T) {
rank := func(vals ...uint16) Result {
return Result{
points: [4]uint16{vals[0], vals[1], vals[2], vals[3]},
item: &Item{text: util.Chars{Index: int32(vals[4])}}}
}
if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) ||
!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
!compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), false) ||
!compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) {
t.Error("Invalid order")
}
if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), true) ||
!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
!compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), true) ||
!compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) {
t.Error("Invalid order (tac)")
}
}
// Match length, string length, index
func TestResultRank(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byScore, byLength}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := buildResult(
withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2)
if item1.points[3] != math.MaxUint16-2 || // Bonus
item1.points[2] != 3 || // Length
item1.points[1] != 0 || // Unused
item1.points[0] != 0 || // Unused
item1.item.Index() != 1 {
t.Error(item1)
}
// Only differ in index
item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2)
items := []Result{item1, item2}
sort.Sort(ByRelevance(items))
if items[0] != item2 || items[1] != item1 {
t.Error(items)
}
items = []Result{item2, item1, item1, item2}
sort.Sort(ByRelevance(items))
if items[0] != item2 || items[1] != item2 ||
items[2] != item1 || items[3] != item1 {
t.Error(items, item1, item1.item.Index(), item2, item2.item.Index())
}
// Sort by relevance
item3 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 3)
item4 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 4)
item5 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 5)
item6 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 6)
items = []Result{item1, item2, item3, item4, item5, item6}
sort.Sort(ByRelevance(items))
if !(items[0] == item6 && items[1] == item5 &&
items[2] == item4 && items[3] == item3 &&
items[4] == item2 && items[5] == item1) {
t.Error(items, item1, item2, item3, item4, item5, item6)
}
}
func TestColorOffset(t *testing.T) {
// ------------ 20 ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
offsets := []Offset{Offset{5, 15}, Offset{25, 35}}
item := Result{
item: &Item{
colors: &[]ansiOffset{
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, 0}},
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, tui.Bold}},
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, 0}},
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, tui.Bold}}}}}
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
pair := tui.NewColorPair(99, 199)
colors := item.colorOffsets(offsets, tui.Dark256, pair, tui.AttrRegular, true)
assert := func(idx int, b int32, e int32, c tui.ColorPair, bold bool) {
var attr tui.Attr
if bold {
attr = tui.Bold
}
o := colors[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.attr != attr {
t.Error(o)
}
}
assert(0, 0, 5, tui.NewColorPair(1, 5), false)
assert(1, 5, 15, pair, false)
assert(2, 15, 20, tui.NewColorPair(1, 5), false)
assert(3, 22, 25, tui.NewColorPair(2, 6), true)
assert(4, 25, 35, pair, false)
assert(5, 35, 40, tui.NewColorPair(4, 8), true)
}

16
src/result_x86.go Normal file
View File

@@ -0,0 +1,16 @@
// +build 386 amd64
package fzf
import "unsafe"
func compareRanks(irank Result, jrank Result, tac bool) bool {
left := *(*uint64)(unsafe.Pointer(&irank.points[0]))
right := *(*uint64)(unsafe.Pointer(&jrank.points[0]))
if left < right {
return true
} else if left > right {
return false
}
return (irank.item.Index() <= jrank.item.Index()) != tac
}

File diff suppressed because it is too large Load Diff

112
src/terminal_test.go Normal file
View File

@@ -0,0 +1,112 @@
package fzf
import (
"regexp"
"testing"
"github.com/junegunn/fzf/src/util"
)
func newItem(str string) *Item {
bytes := []byte(str)
trimmed, _, _ := extractColor(str, nil, nil)
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
}
func TestReplacePlaceholder(t *testing.T) {
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
items1 := []*Item{item1, item1}
items2 := []*Item{
newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("foo'bar \x1b[31mbaz\x1b[m"),
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
var result string
check := func(expected string) {
if result != expected {
t.Errorf("expected: %s, actual: %s", expected, result)
}
}
// {}, preserve ansi
result = replacePlaceholder("echo {}", false, Delimiter{}, false, "query", items1)
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
// {}, strip ansi
result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items1)
check("echo ' foo'\\''bar baz'")
// {}, with multiple items
result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items2)
check("echo 'foo'\\''bar baz'")
// {..}, strip leading whitespaces, preserve ansi
result = replacePlaceholder("echo {..}", false, Delimiter{}, false, "query", items1)
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
// {..}, strip leading whitespaces, strip ansi
result = replacePlaceholder("echo {..}", true, Delimiter{}, false, "query", items1)
check("echo 'foo'\\''bar baz'")
// {q}
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, false, "query", items1)
check("echo ' foo'\\''bar baz' 'query'")
// {q}, multiple items
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, false, "query 'string'", items2)
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, false, "query 'string'", items2)
check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'")
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items1)
check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items2)
check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, false, "query", items2)
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
// forcePlus
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, true, "query", items2)
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
// No match
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, nil})
check("echo /")
// No match, but with selections
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, item1})
check("echo /' foo'\\''bar baz'")
// String delimiter
delim := "'"
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, false, "query", items1)
check("echo ' foo'\\''bar baz'/'foo'/'bar baz'")
// Regex delimiter
regex := regexp.MustCompile("[oa]+")
// foo'bar baz
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1)
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
}
func TestQuoteEntryCmd(t *testing.T) {
tests := map[string]string{
`"`: `^"\^"^"`,
`\`: `^"\\^"`,
`\"`: `^"\\\^"^"`,
`"\\\"`: `^"\^"\\\\\\\^"^"`,
`&|<>()@^%!`: `^"^&^|^<^>^(^)^@^^^%^!^"`,
`%USERPROFILE%`: `^"^%USERPROFILE^%^"`,
`C:\Program Files (x86)\`: `^"C:\\Program Files ^(x86^)\\^"`,
}
for input, expected := range tests {
escaped := quoteEntryCmd(input)
if escaped != expected {
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
}
}
}

21
src/terminal_unix.go Normal file
View File

@@ -0,0 +1,21 @@
// +build !windows
package fzf
import (
"os"
"os/signal"
"syscall"
)
func notifyOnResize(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGWINCH)
}
func notifyStop(p *os.Process) {
p.Signal(syscall.SIGSTOP)
}
func notifyOnCont(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGCONT)
}

19
src/terminal_windows.go Normal file
View File

@@ -0,0 +1,19 @@
// +build windows
package fzf
import (
"os"
)
func notifyOnResize(resizeChan chan<- os.Signal) {
// TODO
}
func notifyStop(p *os.Process) {
// NOOP
}
func notifyOnCont(resizeChan chan<- os.Signal) {
// NOOP
}

View File

@@ -1,6 +1,7 @@
package fzf package fzf
import ( import (
"bytes"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@@ -18,9 +19,8 @@ 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 *util.Chars
prefixLength int prefixLength int32
trimLength int
} }
// Delimiter for tokenizing the input // Delimiter for tokenizing the input
@@ -75,15 +75,14 @@ func ParseRange(str *string) (Range, bool) {
return newRange(n, n), true return newRange(n, n), true
} }
func withPrefixLengths(tokens [][]rune, begin int) []Token { func withPrefixLengths(tokens []string, begin int) []Token {
ret := make([]Token, len(tokens)) ret := make([]Token, len(tokens))
prefixLength := begin prefixLength := begin
for idx, token := range tokens { for idx := range tokens {
// Need to define a new local variable instead of the reused token to take chars := util.ToChars([]byte(tokens[idx]))
// the pointer to it ret[idx] = Token{&chars, int32(prefixLength)}
ret[idx] = Token{token, prefixLength, util.TrimLen(token)} prefixLength += chars.Length()
prefixLength += len(token)
} }
return ret return ret
} }
@@ -94,90 +93,77 @@ const (
awkWhite awkWhite
) )
func awkTokenizer(input []rune) ([][]rune, int) { func awkTokenizer(input string) ([]string, int) {
// 9, 32 // 9, 32
ret := [][]rune{} ret := []string{}
str := []rune{}
prefixLength := 0 prefixLength := 0
state := awkNil state := awkNil
for _, r := range input { begin := 0
end := 0
for idx := 0; idx < len(input); idx++ {
r := input[idx]
white := r == 9 || r == 32 white := r == 9 || r == 32
switch state { switch state {
case awkNil: case awkNil:
if white { if white {
prefixLength++ prefixLength++
} else { } else {
state = awkBlack state, begin, end = awkBlack, idx, idx+1
str = append(str, r)
} }
case awkBlack: case awkBlack:
str = append(str, r) end = idx + 1
if white { if white {
state = awkWhite state = awkWhite
} }
case awkWhite: case awkWhite:
if white { if white {
str = append(str, r) end = idx + 1
} else { } else {
ret = append(ret, str) ret = append(ret, input[begin:end])
state = awkBlack state, begin, end = awkBlack, idx, idx+1
str = []rune{r}
} }
} }
} }
if len(str) > 0 { if begin < end {
ret = append(ret, str) ret = append(ret, input[begin:end])
} }
return ret, prefixLength return ret, prefixLength
} }
// Tokenize tokenizes the given string with the delimiter // Tokenize tokenizes the given string with the delimiter
func Tokenize(runes []rune, delimiter Delimiter) []Token { func Tokenize(text string, delimiter Delimiter) []Token {
if delimiter.str == nil && delimiter.regex == nil { if delimiter.str == nil && delimiter.regex == nil {
// AWK-style (\S+\s*) // AWK-style (\S+\s*)
tokens, prefixLength := awkTokenizer(runes) tokens, prefixLength := awkTokenizer(text)
return withPrefixLengths(tokens, prefixLength) return withPrefixLengths(tokens, prefixLength)
} }
var tokens []string
if delimiter.str != nil { if delimiter.str != nil {
tokens = strings.Split(string(runes), *delimiter.str) return withPrefixLengths(strings.SplitAfter(text, *delimiter.str), 0)
for i := 0; i < len(tokens)-1; i++ { }
tokens[i] = tokens[i] + *delimiter.str
} // FIXME performance
} else if delimiter.regex != nil { var tokens []string
str := string(runes) if delimiter.regex != nil {
for len(str) > 0 { for len(text) > 0 {
loc := delimiter.regex.FindStringIndex(str) loc := delimiter.regex.FindStringIndex(text)
if loc == nil { if len(loc) < 2 {
loc = []int{0, len(str)} loc = []int{0, len(text)}
} }
last := util.Max(loc[1], 1) last := util.Max(loc[1], 1)
tokens = append(tokens, str[:last]) tokens = append(tokens, text[:last])
str = str[last:] text = text[last:]
} }
} }
asRunes := make([][]rune, len(tokens)) return withPrefixLengths(tokens, 0)
for i, token := range tokens {
asRunes[i] = []rune(token)
}
return withPrefixLengths(asRunes, 0)
} }
func joinTokens(tokens []Token) []rune { func joinTokens(tokens []Token) string {
ret := []rune{} var output bytes.Buffer
for _, token := range tokens { for _, token := range tokens {
ret = append(ret, token.text...) output.WriteString(token.text.ToString())
} }
return ret return output.String()
}
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
@@ -185,19 +171,20 @@ 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 {
part := []rune{} parts := []*util.Chars{}
minIdx := 0 minIdx := 0
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)...) chars := util.ToChars([]byte(joinTokens(tokens)))
parts = append(parts, &chars)
} 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...) parts = append(parts, tokens[idx-1].text)
} }
} }
} else { } else {
@@ -224,17 +211,32 @@ 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...) parts = append(parts, tokens[idx-1].text)
} }
} }
} }
var prefixLength int // Merge multiple parts
var merged util.Chars
switch len(parts) {
case 0:
merged = util.ToChars([]byte{})
case 1:
merged = *parts[0]
default:
var output bytes.Buffer
for _, part := range parts {
output.WriteString(part.ToString())
}
merged = util.ToChars([]byte(output.String()))
}
var prefixLength int32
if minIdx < numTokens { if minIdx < numTokens {
prefixLength = tokens[minIdx].prefixLength prefixLength = tokens[minIdx].prefixLength
} else { } else {
prefixLength = 0 prefixLength = 0
} }
transTokens[idx] = Token{part, prefixLength, util.TrimLen(part)} transTokens[idx] = Token{&merged, prefixLength}
} }
return transTokens return transTokens
} }

View File

@@ -1,6 +1,8 @@
package fzf package fzf
import "testing" import (
"testing"
)
func TestParseRange(t *testing.T) { func TestParseRange(t *testing.T) {
{ {
@@ -43,23 +45,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([]rune(input), Delimiter{}) tokens := Tokenize(input, Delimiter{})
if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 { if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
// With delimiter // With delimiter
tokens = Tokenize([]rune(input), delimiterRegexp(":")) tokens = Tokenize(input, delimiterRegexp(":"))
if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 { if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 {
t.Errorf("%s", tokens) t.Error(tokens[0].text.ToString(), tokens[0].prefixLength)
} }
// With delimiter regex // With delimiter regex
tokens = Tokenize([]rune(input), delimiterRegexp("\\s+")) tokens = Tokenize(input, delimiterRegexp("\\s+"))
if string(tokens[0].text) != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 || if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 ||
string(tokens[1].text) != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 || tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 ||
string(tokens[2].text) != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 || tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 ||
string(tokens[3].text) != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 { tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 14 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
} }
@@ -67,7 +69,7 @@ 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([]rune(input), Delimiter{}) tokens := Tokenize(input, Delimiter{})
{ {
ranges := splitNth("1,2,3") ranges := splitNth("1,2,3")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
@@ -80,25 +82,25 @@ func TestTransform(t *testing.T) {
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if string(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 || tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
string(tx[1].text) != "ghi: " || tx[1].prefixLength != 14 || tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
string(tx[2].text) != "def: ghi: jkl" || tx[2].prefixLength != 8 || tx[2].text.ToString() != "def: ghi: jkl" || tx[2].prefixLength != 8 ||
string(tx[3].text) != "abc: " || tx[3].prefixLength != 2 { tx[3].text.ToString() != "abc: " || tx[3].prefixLength != 2 {
t.Errorf("%s", tx) t.Errorf("%s", tx)
} }
} }
} }
{ {
tokens := Tokenize([]rune(input), delimiterRegexp(":")) tokens := Tokenize(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 string(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 || tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
string(tx[1].text) != " ghi:" || tx[1].prefixLength != 12 || tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
string(tx[2].text) != " def: ghi: jkl" || tx[2].prefixLength != 6 || tx[2].text.ToString() != " def: ghi: jkl" || tx[2].prefixLength != 6 ||
string(tx[3].text) != " abc:" || tx[3].prefixLength != 0 { tx[3].text.ToString() != " abc:" || tx[3].prefixLength != 0 {
t.Errorf("%s", tx) t.Errorf("%s", tx)
} }
} }

44
src/tui/dummy.go Normal file
View File

@@ -0,0 +1,44 @@
// +build !ncurses
// +build !tcell
// +build !windows
package tui
type Attr int
func HasFullscreenRenderer() bool {
return false
}
func (a Attr) Merge(b Attr) Attr {
return a | b
}
const (
AttrRegular Attr = Attr(0)
Bold = Attr(1)
Dim = Attr(1 << 1)
Italic = Attr(1 << 2)
Underline = Attr(1 << 3)
Blink = Attr(1 << 4)
Blink2 = Attr(1 << 5)
Reverse = Attr(1 << 6)
)
func (r *FullscreenRenderer) Init() {}
func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Resume(bool) {}
func (r *FullscreenRenderer) Clear() {}
func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {}
func (r *FullscreenRenderer) DoesAutoWrap() bool { return false }
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
func (r *FullscreenRenderer) MaxX() int { return 0 }
func (r *FullscreenRenderer) MaxY() int { return 0 }
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
return nil
}

910
src/tui/light.go Normal file
View File

@@ -0,0 +1,910 @@
package tui
import (
"fmt"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"unicode/utf8"
"github.com/junegunn/fzf/src/util"
"golang.org/x/crypto/ssh/terminal"
)
const (
defaultWidth = 80
defaultHeight = 24
defaultEscDelay = 100
escPollInterval = 5
offsetPollTries = 10
)
const consoleDevice string = "/dev/tty"
var offsetRegexp *regexp.Regexp = regexp.MustCompile("\x1b\\[([0-9]+);([0-9]+)R")
func openTtyIn() *os.File {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
if err != nil {
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
os.Exit(2)
}
return in
}
func (r *LightRenderer) stderr(str string) {
r.stderrInternal(str, true)
}
// FIXME: Need better handling of non-displayable characters
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) {
bytes := []byte(str)
runes := []rune{}
for len(bytes) > 0 {
r, sz := utf8.DecodeRune(bytes)
if r == utf8.RuneError || r < 32 &&
r != '\x1b' && (!allowNLCR || r != '\n' && r != '\r') {
runes = append(runes, '?')
} else {
runes = append(runes, r)
}
bytes = bytes[sz:]
}
r.queued += string(runes)
}
func (r *LightRenderer) csi(code string) {
r.stderr("\x1b[" + code)
}
func (r *LightRenderer) flush() {
if len(r.queued) > 0 {
fmt.Fprint(os.Stderr, r.queued)
r.queued = ""
}
}
// Light renderer
type LightRenderer struct {
theme *ColorTheme
mouse bool
forceBlack bool
clearOnExit bool
prevDownTime time.Time
clickY []int
ttyin *os.File
buffer []byte
origState *terminal.State
width int
height int
yoffset int
tabstop int
escDelay int
fullscreen bool
upOneLine bool
queued string
y int
x int
maxHeightFunc func(int) int
}
type LightWindow struct {
renderer *LightRenderer
colored bool
border BorderStyle
top int
left int
width int
height int
posx int
posy int
tabstop int
fg Color
bg Color
}
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer {
r := LightRenderer{
theme: theme,
forceBlack: forceBlack,
mouse: mouse,
clearOnExit: clearOnExit,
ttyin: openTtyIn(),
yoffset: 0,
tabstop: tabstop,
fullscreen: fullscreen,
upOneLine: false,
maxHeightFunc: maxHeightFunc}
return &r
}
func (r *LightRenderer) fd() int {
return int(r.ttyin.Fd())
}
func (r *LightRenderer) defaultTheme() *ColorTheme {
if strings.Contains(os.Getenv("TERM"), "256") {
return Dark256
}
colors, err := exec.Command("tput", "colors").Output()
if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 {
return Dark256
}
return Default16
}
func (r *LightRenderer) findOffset() (row int, col int) {
r.csi("6n")
r.flush()
bytes := []byte{}
for tries := 0; tries < offsetPollTries; tries++ {
bytes = r.getBytesInternal(bytes, tries > 0)
offsets := offsetRegexp.FindSubmatch(bytes)
if len(offsets) > 2 {
return atoi(string(offsets[1]), 0) - 1, atoi(string(offsets[2]), 0) - 1
}
}
return -1, -1
}
func repeat(s string, times int) string {
if times > 0 {
return strings.Repeat(s, times)
}
return ""
}
func atoi(s string, defaultValue int) int {
value, err := strconv.Atoi(s)
if err != nil {
return defaultValue
}
return value
}
func (r *LightRenderer) Init() {
r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
fd := r.fd()
origState, err := terminal.GetState(fd)
if err != nil {
errorExit(err.Error())
}
r.origState = origState
terminal.MakeRaw(fd)
r.updateTerminalSize()
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
if r.fullscreen {
r.smcup()
} else {
// We assume that --no-clear is used for repetitive relaunching of fzf.
// So we do not clear the lower bottom of the screen.
if r.clearOnExit {
r.csi("J")
}
y, x := r.findOffset()
r.mouse = r.mouse && y >= 0
// When --no-clear is used for repetitive relaunching, there is a small
// time frame between fzf processes where the user keystrokes are not
// captured by either of fzf process which can cause x offset to be
// increased and we're left with unwanted extra new line.
if x > 0 && r.clearOnExit {
r.upOneLine = true
r.makeSpace()
}
for i := 1; i < r.MaxY(); i++ {
r.makeSpace()
}
}
if r.mouse {
r.csi("?1000h")
}
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
r.csi("G")
r.csi("K")
if !r.clearOnExit && !r.fullscreen {
r.csi("s")
}
if !r.fullscreen && r.mouse {
r.yoffset, _ = r.findOffset()
}
}
func (r *LightRenderer) makeSpace() {
r.stderr("\n")
r.csi("G")
}
func (r *LightRenderer) move(y int, x int) {
// w.csi("u")
if r.y < y {
r.csi(fmt.Sprintf("%dB", y-r.y))
} else if r.y > y {
r.csi(fmt.Sprintf("%dA", r.y-y))
}
r.stderr("\r")
if x > 0 {
r.csi(fmt.Sprintf("%dC", x))
}
r.y = y
r.x = x
}
func (r *LightRenderer) origin() {
r.move(0, 0)
}
func getEnv(name string, defaultValue int) int {
env := os.Getenv(name)
if len(env) == 0 {
return defaultValue
}
return atoi(env, defaultValue)
}
func (r *LightRenderer) updateTerminalSize() {
width, height, err := terminal.GetSize(r.fd())
if err == nil {
r.width = width
r.height = r.maxHeightFunc(height)
} else {
r.width = getEnv("COLUMNS", defaultWidth)
r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
}
}
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
b := make([]byte, 1)
fd := r.fd()
util.SetNonblock(r.ttyin, nonblock)
_, err := util.Read(fd, b)
if err != nil {
return 0, false
}
return int(b[0]), true
}
func (r *LightRenderer) getBytes() []byte {
return r.getBytesInternal(r.buffer, false)
}
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
c, ok := r.getch(nonblock)
if !nonblock && !ok {
r.Close()
errorExit("Failed to read " + consoleDevice)
}
retries := 0
if c == ESC || nonblock {
retries = r.escDelay / escPollInterval
}
buffer = append(buffer, byte(c))
for {
c, ok = r.getch(true)
if !ok {
if retries > 0 {
retries--
time.Sleep(escPollInterval * time.Millisecond)
continue
}
break
}
retries = 0
buffer = append(buffer, byte(c))
}
return buffer
}
func (r *LightRenderer) GetChar() Event {
if len(r.buffer) == 0 {
r.buffer = r.getBytes()
}
if len(r.buffer) == 0 {
panic("Empty buffer")
}
sz := 1
defer func() {
r.buffer = r.buffer[sz:]
}()
switch r.buffer[0] {
case CtrlC:
return Event{CtrlC, 0, nil}
case CtrlG:
return Event{CtrlG, 0, nil}
case CtrlQ:
return Event{CtrlQ, 0, nil}
case 127:
return Event{BSpace, 0, nil}
case 0:
return Event{CtrlSpace, 0, nil}
case ESC:
ev := r.escSequence(&sz)
// Second chance
if ev.Type == Invalid {
r.buffer = r.getBytes()
ev = r.escSequence(&sz)
}
return ev
}
// CTRL-A ~ CTRL-Z
if r.buffer[0] <= CtrlZ {
return Event{int(r.buffer[0]), 0, nil}
}
char, rsz := utf8.DecodeRune(r.buffer)
if char == utf8.RuneError {
return Event{ESC, 0, nil}
}
sz = rsz
return Event{Rune, char, nil}
}
func (r *LightRenderer) escSequence(sz *int) Event {
if len(r.buffer) < 2 {
return Event{ESC, 0, nil}
}
*sz = 2
if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 {
return Event{int(CtrlAltA + r.buffer[1] - 1), 0, nil}
}
switch r.buffer[1] {
case 32:
return Event{AltSpace, 0, nil}
case 47:
return Event{AltSlash, 0, nil}
case 98:
return Event{AltB, 0, nil}
case 100:
return Event{AltD, 0, nil}
case 102:
return Event{AltF, 0, nil}
case 127:
return Event{AltBS, 0, nil}
case 91, 79:
if len(r.buffer) < 3 {
return Event{Invalid, 0, nil}
}
*sz = 3
switch r.buffer[2] {
case 68:
return Event{Left, 0, nil}
case 67:
return Event{Right, 0, nil}
case 66:
return Event{Down, 0, nil}
case 65:
return Event{Up, 0, nil}
case 90:
return Event{BTab, 0, nil}
case 72:
return Event{Home, 0, nil}
case 70:
return Event{End, 0, nil}
case 77:
return r.mouseSequence(sz)
case 80:
return Event{F1, 0, nil}
case 81:
return Event{F2, 0, nil}
case 82:
return Event{F3, 0, nil}
case 83:
return Event{F4, 0, nil}
case 49, 50, 51, 52, 53, 54:
if len(r.buffer) < 4 {
return Event{Invalid, 0, nil}
}
*sz = 4
switch r.buffer[2] {
case 50:
if len(r.buffer) == 5 && r.buffer[4] == 126 {
*sz = 5
switch r.buffer[3] {
case 48:
return Event{F9, 0, nil}
case 49:
return Event{F10, 0, nil}
case 51:
return Event{F11, 0, nil}
case 52:
return Event{F12, 0, nil}
}
}
// Bracketed paste mode: \e[200~ ... \e[201~
if r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' {
// Immediately discard the sequence from the buffer and reread input
r.buffer = r.buffer[6:]
*sz = 0
return r.GetChar()
}
return Event{Invalid, 0, nil} // INS
case 51:
return Event{Del, 0, nil}
case 52:
return Event{End, 0, nil}
case 53:
return Event{PgUp, 0, nil}
case 54:
return Event{PgDn, 0, nil}
case 49:
switch r.buffer[3] {
case 126:
return Event{Home, 0, nil}
case 53, 55, 56, 57:
if len(r.buffer) == 5 && r.buffer[4] == 126 {
*sz = 5
switch r.buffer[3] {
case 53:
return Event{F5, 0, nil}
case 55:
return Event{F6, 0, nil}
case 56:
return Event{F7, 0, nil}
case 57:
return Event{F8, 0, nil}
}
}
return Event{Invalid, 0, nil}
case 59:
if len(r.buffer) != 6 {
return Event{Invalid, 0, nil}
}
*sz = 6
switch r.buffer[4] {
case 50:
switch r.buffer[5] {
case 68:
return Event{Home, 0, nil}
case 67:
return Event{End, 0, nil}
}
case 53:
switch r.buffer[5] {
case 68:
return Event{SLeft, 0, nil}
case 67:
return Event{SRight, 0, nil}
}
} // r.buffer[4]
} // r.buffer[3]
} // r.buffer[2]
} // r.buffer[2]
} // r.buffer[1]
if r.buffer[1] >= 'a' && r.buffer[1] <= 'z' {
return Event{AltA + int(r.buffer[1]) - 'a', 0, nil}
}
return Event{Invalid, 0, nil}
}
func (r *LightRenderer) mouseSequence(sz *int) Event {
if len(r.buffer) < 6 || !r.mouse {
return Event{Invalid, 0, nil}
}
*sz = 6
switch r.buffer[3] {
case 32, 34, 36, 40, 48, // mouse-down / shift / cmd / ctrl
35, 39, 43, 51: // mouse-up / shift / cmd / ctrl
mod := r.buffer[3] >= 36
left := r.buffer[3] == 32
down := r.buffer[3]%2 == 0
x := int(r.buffer[4] - 33)
y := int(r.buffer[5]-33) - r.yoffset
double := false
if down {
now := time.Now()
if !left { // Right double click is not allowed
r.clickY = []int{}
} else if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, y)
} else {
r.clickY = []int{y}
}
r.prevDownTime = now
} else {
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
double = true
}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
mod := r.buffer[3] >= 100
s := 1 - int(r.buffer[3]%2)*2
x := int(r.buffer[4] - 33)
y := int(r.buffer[5]-33) - r.yoffset
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, false, mod}}
}
return Event{Invalid, 0, nil}
}
func (r *LightRenderer) smcup() {
r.csi("?1049h")
}
func (r *LightRenderer) rmcup() {
r.csi("?1049l")
}
func (r *LightRenderer) Pause(clear bool) {
terminal.Restore(r.fd(), r.origState)
if clear {
if r.fullscreen {
r.rmcup()
} else {
r.smcup()
r.csi("H")
}
r.flush()
}
}
func (r *LightRenderer) Resume(clear bool) {
terminal.MakeRaw(r.fd())
if clear {
if r.fullscreen {
r.smcup()
} else {
r.rmcup()
}
r.flush()
} else if !r.fullscreen && r.mouse {
// NOTE: Resume(false) is only called on SIGCONT after SIGSTOP.
// And It's highly likely that the offset we obtained at the beginning will
// no longer be correct, so we simply disable mouse input.
r.csi("?1000l")
r.mouse = false
}
}
func (r *LightRenderer) Clear() {
if r.fullscreen {
r.csi("H")
}
// r.csi("u")
r.origin()
r.csi("J")
r.flush()
}
func (r *LightRenderer) RefreshWindows(windows []Window) {
r.flush()
}
func (r *LightRenderer) Refresh() {
r.updateTerminalSize()
}
func (r *LightRenderer) Close() {
// r.csi("u")
if r.clearOnExit {
if r.fullscreen {
r.rmcup()
} else {
r.origin()
if r.upOneLine {
r.csi("A")
}
r.csi("J")
}
} else if !r.fullscreen {
r.csi("u")
}
if r.mouse {
r.csi("?1000l")
}
r.flush()
terminal.Restore(r.fd(), r.origState)
}
func (r *LightRenderer) MaxX() int {
return r.width
}
func (r *LightRenderer) MaxY() int {
return r.height
}
func (r *LightRenderer) DoesAutoWrap() bool {
return false
}
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
w := &LightWindow{
renderer: r,
colored: r.theme != nil,
border: borderStyle,
top: top,
left: left,
width: width,
height: height,
tabstop: r.tabstop,
fg: colDefault,
bg: colDefault}
if r.theme != nil {
w.fg = r.theme.Fg
w.bg = r.theme.Bg
}
w.drawBorder()
return w
}
func (w *LightWindow) drawBorder() {
switch w.border {
case BorderAround:
w.drawBorderAround()
case BorderHorizontal:
w.drawBorderHorizontal()
}
}
func (w *LightWindow) drawBorderHorizontal() {
w.Move(0, 0)
w.CPrint(ColBorder, AttrRegular, repeat("─", w.width))
w.Move(w.height-1, 0)
w.CPrint(ColBorder, AttrRegular, repeat("─", w.width))
}
func (w *LightWindow) drawBorderAround() {
w.Move(0, 0)
w.CPrint(ColBorder, AttrRegular, "┌"+repeat("─", w.width-2)+"┐")
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(ColBorder, AttrRegular, "│")
w.cprint2(colDefault, w.bg, AttrRegular, repeat(" ", w.width-2))
w.CPrint(ColBorder, AttrRegular, "│")
}
w.Move(w.height-1, 0)
w.CPrint(ColBorder, AttrRegular, "└"+repeat("─", w.width-2)+"┘")
}
func (w *LightWindow) csi(code string) {
w.renderer.csi(code)
}
func (w *LightWindow) stderr(str string) {
w.renderer.stderr(str)
}
func (w *LightWindow) stderrInternal(str string, allowNLCR bool) {
w.renderer.stderrInternal(str, allowNLCR)
}
func (w *LightWindow) Top() int {
return w.top
}
func (w *LightWindow) Left() int {
return w.left
}
func (w *LightWindow) Width() int {
return w.width
}
func (w *LightWindow) Height() int {
return w.height
}
func (w *LightWindow) Refresh() {
}
func (w *LightWindow) Close() {
}
func (w *LightWindow) X() int {
return w.posx
}
func (w *LightWindow) Y() int {
return w.posy
}
func (w *LightWindow) Enclose(y int, x int) bool {
return x >= w.left && x < (w.left+w.width) &&
y >= w.top && y < (w.top+w.height)
}
func (w *LightWindow) Move(y int, x int) {
w.posx = x
w.posy = y
w.renderer.move(w.Top()+y, w.Left()+x)
}
func (w *LightWindow) MoveAndClear(y int, x int) {
w.Move(y, x)
// We should not delete preview window on the right
// csi("K")
w.Print(repeat(" ", w.width-x))
w.Move(y, x)
}
func attrCodes(attr Attr) []string {
codes := []string{}
if (attr & Bold) > 0 {
codes = append(codes, "1")
}
if (attr & Dim) > 0 {
codes = append(codes, "2")
}
if (attr & Italic) > 0 {
codes = append(codes, "3")
}
if (attr & Underline) > 0 {
codes = append(codes, "4")
}
if (attr & Blink) > 0 {
codes = append(codes, "5")
}
if (attr & Reverse) > 0 {
codes = append(codes, "7")
}
return codes
}
func colorCodes(fg Color, bg Color) []string {
codes := []string{}
appendCode := func(c Color, offset int) {
if c == colDefault {
return
}
if c.is24() {
r := (c >> 16) & 0xff
g := (c >> 8) & 0xff
b := (c) & 0xff
codes = append(codes, fmt.Sprintf("%d;2;%d;%d;%d", 38+offset, r, g, b))
} else if c >= colBlack && c <= colWhite {
codes = append(codes, fmt.Sprintf("%d", int(c)+30+offset))
} else if c > colWhite && c < 16 {
codes = append(codes, fmt.Sprintf("%d", int(c)+90+offset-8))
} else if c >= 16 && c < 256 {
codes = append(codes, fmt.Sprintf("%d;5;%d", 38+offset, c))
}
}
appendCode(fg, 0)
appendCode(bg, 10)
return codes
}
func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool {
codes := append(attrCodes(attr), colorCodes(fg, bg)...)
w.csi(";" + strings.Join(codes, ";") + "m")
return len(codes) > 0
}
func (w *LightWindow) Print(text string) {
w.cprint2(colDefault, w.bg, AttrRegular, text)
}
func cleanse(str string) string {
return strings.Replace(str, "\x1b", "?", -1)
}
func (w *LightWindow) CPrint(pair ColorPair, attr Attr, text string) {
if !w.colored {
w.csiColor(colDefault, colDefault, attrFor(pair, attr))
} else {
w.csiColor(pair.Fg(), pair.Bg(), attr)
}
w.stderrInternal(cleanse(text), false)
w.csi("m")
}
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
if w.csiColor(fg, bg, attr) {
defer w.csi("m")
}
w.stderrInternal(cleanse(text), false)
}
type wrappedLine struct {
text string
displayWidth int
}
func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine {
lines := []wrappedLine{}
width := 0
line := ""
for _, r := range input {
w := util.Max(util.RuneWidth(r, prefixLength+width, 8), 1)
width += w
str := string(r)
if r == '\t' {
str = repeat(" ", w)
}
if prefixLength+width <= max {
line += str
} else {
lines = append(lines, wrappedLine{string(line), width - w})
line = str
prefixLength = 0
width = util.RuneWidth(r, prefixLength, 8)
}
}
lines = append(lines, wrappedLine{string(line), width})
return lines
}
func (w *LightWindow) fill(str string, onMove func()) FillReturn {
allLines := strings.Split(str, "\n")
for i, line := range allLines {
lines := wrapLine(line, w.posx, w.width, w.tabstop)
for j, wl := range lines {
if w.posx >= w.Width()-1 && wl.displayWidth == 0 {
if w.posy < w.height-1 {
w.Move(w.posy+1, 0)
}
return FillNextLine
}
w.stderrInternal(wl.text, false)
w.posx += wl.displayWidth
// Wrap line
if j < len(lines)-1 || i < len(allLines)-1 {
if w.posy+1 >= w.height {
return FillSuspend
}
w.MoveAndClear(w.posy, w.posx)
w.Move(w.posy+1, 0)
onMove()
}
}
}
return FillContinue
}
func (w *LightWindow) setBg() {
if w.bg != colDefault {
w.csiColor(colDefault, w.bg, AttrRegular)
}
}
func (w *LightWindow) Fill(text string) FillReturn {
w.Move(w.posy, w.posx)
w.setBg()
return w.fill(text, w.setBg)
}
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
w.Move(w.posy, w.posx)
if fg == colDefault {
fg = w.fg
}
if bg == colDefault {
bg = w.bg
}
if w.csiColor(fg, bg, attr) {
defer w.csi("m")
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
}
return w.fill(text, w.setBg)
}
func (w *LightWindow) FinishFill() {
w.MoveAndClear(w.posy, w.posx)
for y := w.posy + 1; y < w.height; y++ {
w.MoveAndClear(y, 0)
}
}
func (w *LightWindow) Erase() {
w.drawBorder()
// We don't erase the window here to avoid flickering during scroll
w.Move(0, 0)
}

586
src/tui/tcell.go Normal file
View File

@@ -0,0 +1,586 @@
// +build tcell windows
package tui
import (
"os"
"time"
"unicode/utf8"
"runtime"
"github.com/gdamore/tcell"
"github.com/gdamore/tcell/encoding"
"github.com/mattn/go-runewidth"
)
func HasFullscreenRenderer() bool {
return true
}
func (p ColorPair) style() tcell.Style {
style := tcell.StyleDefault
return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg()))
}
type Attr tcell.Style
type TcellWindow struct {
color bool
top int
left int
width int
height int
lastX int
lastY int
moveCursor bool
borderStyle BorderStyle
}
func (w *TcellWindow) Top() int {
return w.top
}
func (w *TcellWindow) Left() int {
return w.left
}
func (w *TcellWindow) Width() int {
return w.width
}
func (w *TcellWindow) Height() int {
return w.height
}
func (w *TcellWindow) Refresh() {
if w.moveCursor {
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
w.moveCursor = false
}
w.lastX = 0
w.lastY = 0
switch w.borderStyle {
case BorderAround:
w.drawBorder(true)
case BorderHorizontal:
w.drawBorder(false)
}
}
func (w *TcellWindow) FinishFill() {
// NO-OP
}
const (
Bold Attr = Attr(tcell.AttrBold)
Dim = Attr(tcell.AttrDim)
Blink = Attr(tcell.AttrBlink)
Reverse = Attr(tcell.AttrReverse)
Underline = Attr(tcell.AttrUnderline)
Italic = Attr(tcell.AttrNone) // Not supported
)
const (
AttrRegular Attr = 0
)
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
if _screen.Colors() >= 256 {
return Dark256
}
return Default16
}
var (
_colorToAttribute = []tcell.Color{
tcell.ColorBlack,
tcell.ColorRed,
tcell.ColorGreen,
tcell.ColorYellow,
tcell.ColorBlue,
tcell.ColorDarkMagenta,
tcell.ColorLightCyan,
tcell.ColorWhite,
}
)
func (c Color) Style() tcell.Color {
if c <= colDefault {
return tcell.ColorDefault
} else if c >= colBlack && c <= colWhite {
return _colorToAttribute[int(c)]
} else {
return tcell.Color(c)
}
}
func (a Attr) Merge(b Attr) Attr {
return a | b
}
var (
_screen tcell.Screen
)
func (r *FullscreenRenderer) initScreen() {
s, e := tcell.NewScreen()
if e != nil {
errorExit(e.Error())
}
if e = s.Init(); e != nil {
errorExit(e.Error())
}
if r.mouse {
s.EnableMouse()
} else {
s.DisableMouse()
}
_screen = s
}
func (r *FullscreenRenderer) Init() {
if os.Getenv("TERM") == "cygwin" {
os.Setenv("TERM", "")
}
encoding.Register()
r.initScreen()
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
}
func (r *FullscreenRenderer) MaxX() int {
ncols, _ := _screen.Size()
return int(ncols)
}
func (r *FullscreenRenderer) MaxY() int {
_, nlines := _screen.Size()
return int(nlines)
}
func (w *TcellWindow) X() int {
return w.lastX
}
func (w *TcellWindow) Y() int {
return w.lastY
}
func (r *FullscreenRenderer) DoesAutoWrap() bool {
return false
}
func (r *FullscreenRenderer) Clear() {
_screen.Sync()
_screen.Clear()
}
func (r *FullscreenRenderer) Refresh() {
// noop
}
func (r *FullscreenRenderer) GetChar() Event {
ev := _screen.PollEvent()
switch ev := ev.(type) {
case *tcell.EventResize:
return Event{Resize, 0, nil}
// process mouse events:
case *tcell.EventMouse:
x, y := ev.Position()
button := ev.Buttons()
mod := ev.Modifiers() != 0
if button&tcell.WheelDown != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
} else if button&tcell.WheelUp != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
} else if runtime.GOOS != "windows" {
// double and single taps on Windows don't quite work due to
// the console acting on the events and not allowing us
// to consume them.
left := button&tcell.Button1 != 0
down := left || button&tcell.Button3 != 0
double := false
if down {
now := time.Now()
if !left {
r.clickY = []int{}
} else if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, x)
} else {
r.clickY = []int{x}
r.prevDownTime = now
}
} else {
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
double = true
}
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
}
// process keyboard:
case *tcell.EventKey:
alt := (ev.Modifiers() & tcell.ModAlt) > 0
keyfn := func(r rune) int {
if alt {
return CtrlAltA - 'a' + int(r)
}
return CtrlA - 'a' + int(r)
}
switch ev.Key() {
case tcell.KeyCtrlA:
return Event{keyfn('a'), 0, nil}
case tcell.KeyCtrlB:
return Event{keyfn('b'), 0, nil}
case tcell.KeyCtrlC:
return Event{keyfn('c'), 0, nil}
case tcell.KeyCtrlD:
return Event{keyfn('d'), 0, nil}
case tcell.KeyCtrlE:
return Event{keyfn('e'), 0, nil}
case tcell.KeyCtrlF:
return Event{keyfn('f'), 0, nil}
case tcell.KeyCtrlG:
return Event{keyfn('g'), 0, nil}
case tcell.KeyCtrlH:
return Event{keyfn('h'), 0, nil}
case tcell.KeyCtrlI:
return Event{keyfn('i'), 0, nil}
case tcell.KeyCtrlJ:
return Event{keyfn('j'), 0, nil}
case tcell.KeyCtrlK:
return Event{keyfn('k'), 0, nil}
case tcell.KeyCtrlL:
return Event{keyfn('l'), 0, nil}
case tcell.KeyCtrlM:
return Event{keyfn('m'), 0, nil}
case tcell.KeyCtrlN:
return Event{keyfn('n'), 0, nil}
case tcell.KeyCtrlO:
return Event{keyfn('o'), 0, nil}
case tcell.KeyCtrlP:
return Event{keyfn('p'), 0, nil}
case tcell.KeyCtrlQ:
return Event{keyfn('q'), 0, nil}
case tcell.KeyCtrlR:
return Event{keyfn('r'), 0, nil}
case tcell.KeyCtrlS:
return Event{keyfn('s'), 0, nil}
case tcell.KeyCtrlT:
return Event{keyfn('t'), 0, nil}
case tcell.KeyCtrlU:
return Event{keyfn('u'), 0, nil}
case tcell.KeyCtrlV:
return Event{keyfn('v'), 0, nil}
case tcell.KeyCtrlW:
return Event{keyfn('w'), 0, nil}
case tcell.KeyCtrlX:
return Event{keyfn('x'), 0, nil}
case tcell.KeyCtrlY:
return Event{keyfn('y'), 0, nil}
case tcell.KeyCtrlZ:
return Event{keyfn('z'), 0, nil}
case tcell.KeyCtrlSpace:
return Event{CtrlSpace, 0, nil}
case tcell.KeyBackspace2:
if alt {
return Event{AltBS, 0, nil}
}
return Event{BSpace, 0, nil}
case tcell.KeyUp:
return Event{Up, 0, nil}
case tcell.KeyDown:
return Event{Down, 0, nil}
case tcell.KeyLeft:
return Event{Left, 0, nil}
case tcell.KeyRight:
return Event{Right, 0, nil}
case tcell.KeyHome:
return Event{Home, 0, nil}
case tcell.KeyDelete:
return Event{Del, 0, nil}
case tcell.KeyEnd:
return Event{End, 0, nil}
case tcell.KeyPgUp:
return Event{PgUp, 0, nil}
case tcell.KeyPgDn:
return Event{PgDn, 0, nil}
case tcell.KeyBacktab:
return Event{BTab, 0, nil}
case tcell.KeyF1:
return Event{F1, 0, nil}
case tcell.KeyF2:
return Event{F2, 0, nil}
case tcell.KeyF3:
return Event{F3, 0, nil}
case tcell.KeyF4:
return Event{F4, 0, nil}
case tcell.KeyF5:
return Event{F5, 0, nil}
case tcell.KeyF6:
return Event{F6, 0, nil}
case tcell.KeyF7:
return Event{F7, 0, nil}
case tcell.KeyF8:
return Event{F8, 0, nil}
case tcell.KeyF9:
return Event{F9, 0, nil}
case tcell.KeyF10:
return Event{F10, 0, nil}
case tcell.KeyF11:
return Event{F11, 0, nil}
case tcell.KeyF12:
return Event{F12, 0, nil}
// ev.Ch doesn't work for some reason for space:
case tcell.KeyRune:
r := ev.Rune()
if alt {
switch r {
case ' ':
return Event{AltSpace, 0, nil}
case '/':
return Event{AltSlash, 0, nil}
}
if r >= 'a' && r <= 'z' {
return Event{AltA + int(r) - 'a', 0, nil}
}
if r >= '0' && r <= '9' {
return Event{Alt0 + int(r) - '0', 0, nil}
}
}
return Event{Rune, r, nil}
case tcell.KeyEsc:
return Event{ESC, 0, nil}
}
}
return Event{Invalid, 0, nil}
}
func (r *FullscreenRenderer) Pause(bool) {
_screen.Fini()
}
func (r *FullscreenRenderer) Resume(bool) {
r.initScreen()
}
func (r *FullscreenRenderer) Close() {
_screen.Fini()
}
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
// TODO
for _, w := range windows {
w.Refresh()
}
_screen.Show()
}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
// TODO
return &TcellWindow{
color: r.theme != nil,
top: top,
left: left,
width: width,
height: height,
borderStyle: borderStyle}
}
func (w *TcellWindow) Close() {
// TODO
}
func fill(x, y, w, h int, r rune) {
for ly := 0; ly <= h; ly++ {
for lx := 0; lx <= w; lx++ {
_screen.SetContent(x+lx, y+ly, r, nil, ColNormal.style())
}
}
}
func (w *TcellWindow) Erase() {
fill(w.left-1, w.top, w.width+1, w.height, ' ')
}
func (w *TcellWindow) Enclose(y int, x int) bool {
return x >= w.left && x < (w.left+w.width) &&
y >= w.top && y < (w.top+w.height)
}
func (w *TcellWindow) Move(y int, x int) {
w.lastX = x
w.lastY = y
w.moveCursor = true
}
func (w *TcellWindow) MoveAndClear(y int, x int) {
w.Move(y, x)
for i := w.lastX; i < w.width; i++ {
_screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, ColNormal.style())
}
w.lastX = x
}
func (w *TcellWindow) Print(text string) {
w.printString(text, ColNormal, 0)
}
func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) {
t := text
lx := 0
var style tcell.Style
if w.color {
style = pair.style().
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0)
} else {
style = ColNormal.style().
Reverse(a&Attr(tcell.AttrReverse) != 0 || pair == ColCurrent || pair == ColCurrentMatch).
Underline(a&Attr(tcell.AttrUnderline) != 0 || pair == ColMatch || pair == ColCurrentMatch)
}
style = style.
Blink(a&Attr(tcell.AttrBlink) != 0).
Bold(a&Attr(tcell.AttrBold) != 0).
Dim(a&Attr(tcell.AttrDim) != 0)
for {
if len(t) == 0 {
break
}
r, size := utf8.DecodeRuneInString(t)
t = t[size:]
if r < rune(' ') { // ignore control characters
continue
}
if r == '\n' {
w.lastY++
lx = 0
} else {
if r == '\u000D' { // skip carriage return
continue
}
var xPos = w.left + w.lastX + lx
var yPos = w.top + w.lastY
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
_screen.SetContent(xPos, yPos, r, nil, style)
}
lx += runewidth.RuneWidth(r)
}
}
w.lastX += lx
}
func (w *TcellWindow) CPrint(pair ColorPair, attr Attr, text string) {
w.printString(text, pair, attr)
}
func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn {
lx := 0
var style tcell.Style
if w.color {
style = pair.style()
} else {
style = ColNormal.style()
}
style = style.
Blink(a&Attr(tcell.AttrBlink) != 0).
Bold(a&Attr(tcell.AttrBold) != 0).
Dim(a&Attr(tcell.AttrDim) != 0).
Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0)
for _, r := range text {
if r == '\n' {
w.lastY++
w.lastX = 0
lx = 0
} else {
var xPos = w.left + w.lastX + lx
// word wrap:
if xPos >= (w.left + w.width) {
w.lastY++
w.lastX = 0
lx = 0
xPos = w.left
}
var yPos = w.top + w.lastY
if yPos >= (w.top + w.height) {
return FillSuspend
}
_screen.SetContent(xPos, yPos, r, nil, style)
lx += runewidth.RuneWidth(r)
}
}
w.lastX += lx
return FillContinue
}
func (w *TcellWindow) Fill(str string) FillReturn {
return w.fillString(str, ColNormal, 0)
}
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
if fg == colDefault {
fg = ColNormal.Fg()
}
if bg == colDefault {
bg = ColNormal.Bg()
}
return w.fillString(str, NewColorPair(fg, bg), a)
}
func (w *TcellWindow) drawBorder(around bool) {
left := w.left
right := left + w.width
top := w.top
bot := top + w.height
var style tcell.Style
if w.color {
style = ColBorder.style()
} else {
style = ColNormal.style()
}
for x := left; x < right; x++ {
_screen.SetContent(x, top, tcell.RuneHLine, nil, style)
_screen.SetContent(x, bot-1, tcell.RuneHLine, nil, style)
}
if around {
for y := top; y < bot; y++ {
_screen.SetContent(left, y, tcell.RuneVLine, nil, style)
_screen.SetContent(right-1, y, tcell.RuneVLine, nil, style)
}
_screen.SetContent(left, top, tcell.RuneULCorner, nil, style)
_screen.SetContent(right-1, top, tcell.RuneURCorner, nil, style)
_screen.SetContent(left, bot-1, tcell.RuneLLCorner, nil, style)
_screen.SetContent(right-1, bot-1, tcell.RuneLRCorner, nil, style)
}
}

424
src/tui/tui.go Normal file
View File

@@ -0,0 +1,424 @@
package tui
import (
"fmt"
"os"
"strconv"
"time"
)
// Types of user action
const (
Rune = iota
CtrlA
CtrlB
CtrlC
CtrlD
CtrlE
CtrlF
CtrlG
CtrlH
Tab
CtrlJ
CtrlK
CtrlL
CtrlM
CtrlN
CtrlO
CtrlP
CtrlQ
CtrlR
CtrlS
CtrlT
CtrlU
CtrlV
CtrlW
CtrlX
CtrlY
CtrlZ
ESC
CtrlSpace
Invalid
Resize
Mouse
DoubleClick
LeftClick
RightClick
BTab
BSpace
Del
PgUp
PgDn
Up
Down
Left
Right
Home
End
SLeft
SRight
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
F11
F12
Change
AltSpace
AltSlash
AltBS
Alt0
)
const ( // Reset iota
AltA = Alt0 + 'a' - '0' + iota
AltB
AltC
AltD
AltE
AltF
AltZ = AltA + 'z' - 'a'
CtrlAltA = AltZ + 1
CtrlAltM = CtrlAltA + 'm' - 'a'
)
const (
doubleClickDuration = 500 * time.Millisecond
)
type Color int32
func (c Color) is24() bool {
return c > 0 && (c&(1<<24)) > 0
}
const (
colUndefined Color = -2
colDefault = -1
)
const (
colBlack Color = iota
colRed
colGreen
colYellow
colBlue
colMagenta
colCyan
colWhite
)
type FillReturn int
const (
FillContinue FillReturn = iota
FillNextLine
FillSuspend
)
type ColorPair struct {
fg Color
bg Color
id int
}
func HexToColor(rrggbb string) Color {
r, _ := strconv.ParseInt(rrggbb[1:3], 16, 0)
g, _ := strconv.ParseInt(rrggbb[3:5], 16, 0)
b, _ := strconv.ParseInt(rrggbb[5:7], 16, 0)
return Color((1 << 24) + (r << 16) + (g << 8) + b)
}
func NewColorPair(fg Color, bg Color) ColorPair {
return ColorPair{fg, bg, -1}
}
func (p ColorPair) Fg() Color {
return p.fg
}
func (p ColorPair) Bg() Color {
return p.bg
}
func (p ColorPair) is24() bool {
return p.fg.is24() || p.bg.is24()
}
type ColorTheme struct {
Fg Color
Bg Color
DarkBg Color
Prompt Color
Match Color
Current Color
CurrentMatch Color
Spinner Color
Info Color
Cursor Color
Selected Color
Header Color
Border Color
}
type Event struct {
Type int
Char rune
MouseEvent *MouseEvent
}
type MouseEvent struct {
Y int
X int
S int
Left bool
Down bool
Double bool
Mod bool
}
type BorderStyle int
const (
BorderNone BorderStyle = iota
BorderAround
BorderHorizontal
)
type Renderer interface {
Init()
Pause(clear bool)
Resume(clear bool)
Clear()
RefreshWindows(windows []Window)
Refresh()
Close()
GetChar() Event
MaxX() int
MaxY() int
DoesAutoWrap() bool
NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window
}
type Window interface {
Top() int
Left() int
Width() int
Height() int
Refresh()
FinishFill()
Close()
X() int
Y() int
Enclose(y int, x int) bool
Move(y int, x int)
MoveAndClear(y int, x int)
Print(text string)
CPrint(color ColorPair, attr Attr, text string)
Fill(text string) FillReturn
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
Erase()
}
type FullscreenRenderer struct {
theme *ColorTheme
mouse bool
forceBlack bool
prevDownTime time.Time
clickY []int
}
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
r := &FullscreenRenderer{
theme: theme,
mouse: mouse,
forceBlack: forceBlack,
prevDownTime: time.Unix(0, 0),
clickY: []int{}}
return r
}
var (
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
ColNormal ColorPair
ColPrompt ColorPair
ColMatch ColorPair
ColCurrent ColorPair
ColCurrentMatch ColorPair
ColSpinner ColorPair
ColInfo ColorPair
ColCursor ColorPair
ColSelected ColorPair
ColHeader ColorPair
ColBorder ColorPair
)
func EmptyTheme() *ColorTheme {
return &ColorTheme{
Fg: colUndefined,
Bg: colUndefined,
DarkBg: colUndefined,
Prompt: colUndefined,
Match: colUndefined,
Current: colUndefined,
CurrentMatch: colUndefined,
Spinner: colUndefined,
Info: colUndefined,
Cursor: colUndefined,
Selected: colUndefined,
Header: colUndefined,
Border: colUndefined}
}
func errorExit(message string) {
fmt.Fprintln(os.Stderr, message)
os.Exit(2)
}
func init() {
Default16 = &ColorTheme{
Fg: colDefault,
Bg: colDefault,
DarkBg: colBlack,
Prompt: colBlue,
Match: colGreen,
Current: colYellow,
CurrentMatch: colGreen,
Spinner: colGreen,
Info: colWhite,
Cursor: colRed,
Selected: colMagenta,
Header: colCyan,
Border: colBlack}
Dark256 = &ColorTheme{
Fg: colDefault,
Bg: colDefault,
DarkBg: 236,
Prompt: 110,
Match: 108,
Current: 254,
CurrentMatch: 151,
Spinner: 148,
Info: 144,
Cursor: 161,
Selected: 168,
Header: 109,
Border: 59}
Light256 = &ColorTheme{
Fg: colDefault,
Bg: colDefault,
DarkBg: 251,
Prompt: 25,
Match: 66,
Current: 237,
CurrentMatch: 23,
Spinner: 65,
Info: 101,
Cursor: 161,
Selected: 168,
Header: 31,
Border: 145}
}
func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
if theme == nil {
initPalette(theme)
return
}
if forceBlack {
theme.Bg = colBlack
}
o := func(a Color, b Color) Color {
if b == colUndefined {
return a
}
return b
}
theme.Fg = o(baseTheme.Fg, theme.Fg)
theme.Bg = o(baseTheme.Bg, theme.Bg)
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
theme.Match = o(baseTheme.Match, theme.Match)
theme.Current = o(baseTheme.Current, theme.Current)
theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch)
theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
theme.Info = o(baseTheme.Info, theme.Info)
theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
theme.Selected = o(baseTheme.Selected, theme.Selected)
theme.Header = o(baseTheme.Header, theme.Header)
theme.Border = o(baseTheme.Border, theme.Border)
initPalette(theme)
}
func initPalette(theme *ColorTheme) {
idx := 0
pair := func(fg, bg Color) ColorPair {
idx++
return ColorPair{fg, bg, idx}
}
if theme != nil {
ColNormal = pair(theme.Fg, theme.Bg)
ColPrompt = pair(theme.Prompt, theme.Bg)
ColMatch = pair(theme.Match, theme.Bg)
ColCurrent = pair(theme.Current, theme.DarkBg)
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
ColSpinner = pair(theme.Spinner, theme.Bg)
ColInfo = pair(theme.Info, theme.Bg)
ColCursor = pair(theme.Cursor, theme.DarkBg)
ColSelected = pair(theme.Selected, theme.DarkBg)
ColHeader = pair(theme.Header, theme.Bg)
ColBorder = pair(theme.Border, theme.Bg)
} else {
ColNormal = pair(colDefault, colDefault)
ColPrompt = pair(colDefault, colDefault)
ColMatch = pair(colDefault, colDefault)
ColCurrent = pair(colDefault, colDefault)
ColCurrentMatch = pair(colDefault, colDefault)
ColSpinner = pair(colDefault, colDefault)
ColInfo = pair(colDefault, colDefault)
ColCursor = pair(colDefault, colDefault)
ColSelected = pair(colDefault, colDefault)
ColHeader = pair(colDefault, colDefault)
ColBorder = pair(colDefault, colDefault)
}
}
func attrFor(color ColorPair, attr Attr) Attr {
switch color {
case ColCurrent:
return attr | Reverse
case ColMatch:
return attr | Underline
case ColCurrentMatch:
return attr | Underline | Reverse
}
return attr
}

20
src/tui/tui_test.go Normal file
View File

@@ -0,0 +1,20 @@
package tui
import "testing"
func TestHexToColor(t *testing.T) {
assert := func(expr string, r, g, b int) {
color := HexToColor(expr)
if !color.is24() ||
int((color>>16)&0xff) != r ||
int((color>>8)&0xff) != g ||
int((color)&0xff) != b {
t.Fail()
}
}
assert("#ff0000", 255, 0, 0)
assert("#010203", 1, 2, 3)
assert("#102030", 16, 32, 48)
assert("#ffffff", 255, 255, 255)
}

View File

@@ -2,6 +2,7 @@
# http://www.rubydoc.info/github/rest-client/rest-client/RestClient # http://www.rubydoc.info/github/rest-client/rest-client/RestClient
require 'rest_client' require 'rest_client'
require 'json'
if ARGV.length < 3 if ARGV.length < 3
puts "usage: #$0 <token> <version> <files...>" puts "usage: #$0 <token> <version> <files...>"
@@ -22,21 +23,23 @@ end
# List assets # List assets
assets = Hash[rel['assets'].map { |a| a.values_at *%w[name id] }] assets = Hash[rel['assets'].map { |a| a.values_at *%w[name id] }]
files.select { |f| File.exists? f }.each do |file| files.select { |f| File.exists? f }.map do |file|
name = File.basename file Thread.new do
name = File.basename file
if asset_id = assets[name] if asset_id = assets[name]
puts "#{name} found. Deleting asset id #{asset_id}." puts "#{name} found. Deleting asset id #{asset_id}."
RestClient.delete "#{base}/assets/#{asset_id}", RestClient.delete "#{base}/assets/#{asset_id}",
:authorization => "token #{token}" :authorization => "token #{token}"
else else
puts "#{name} not found" puts "#{name} not found"
end
puts "Uploading #{name}"
RestClient.post(
"#{base.sub 'api', 'uploads'}/#{rel['id']}/assets?name=#{name}",
File.read(file),
:authorization => "token #{token}",
:content_type => "application/octet-stream")
end end
end.each(&:join)
puts "Uploading #{name}"
RestClient.post(
"#{base.sub 'api', 'uploads'}/#{rel['id']}/assets?name=#{name}",
File.read(file),
:authorization => "token #{token}",
:content_type => "application/octet-stream")
end

167
src/util/chars.go Normal file
View File

@@ -0,0 +1,167 @@
package util
import (
"unicode"
"unicode/utf8"
"unsafe"
)
const (
overflow64 uint64 = 0x8080808080808080
overflow32 uint32 = 0x80808080
)
type Chars struct {
slice []byte // or []rune
inBytes bool
trimLengthKnown bool
trimLength uint16
// XXX Piggybacking item index here is a horrible idea. But I'm trying to
// minimize the memory footprint by not wasting padded spaces.
Index int32
}
func checkAscii(bytes []byte) (bool, int) {
i := 0
for ; i <= len(bytes)-8; i += 8 {
if (overflow64 & *(*uint64)(unsafe.Pointer(&bytes[i]))) > 0 {
return false, i
}
}
for ; i <= len(bytes)-4; i += 4 {
if (overflow32 & *(*uint32)(unsafe.Pointer(&bytes[i]))) > 0 {
return false, i
}
}
for ; i < len(bytes); i++ {
if bytes[i] >= utf8.RuneSelf {
return false, i
}
}
return true, 0
}
// ToChars converts byte array into rune array
func ToChars(bytes []byte) Chars {
inBytes, bytesUntil := checkAscii(bytes)
if inBytes {
return Chars{slice: bytes, inBytes: inBytes}
}
runes := make([]rune, bytesUntil, len(bytes))
for i := 0; i < bytesUntil; i++ {
runes[i] = rune(bytes[i])
}
for i := bytesUntil; i < len(bytes); {
r, sz := utf8.DecodeRune(bytes[i:])
i += sz
runes = append(runes, r)
}
return RunesToChars(runes)
}
func RunesToChars(runes []rune) Chars {
return Chars{slice: *(*[]byte)(unsafe.Pointer(&runes)), inBytes: false}
}
func (chars *Chars) IsBytes() bool {
return chars.inBytes
}
func (chars *Chars) Bytes() []byte {
return chars.slice
}
func (chars *Chars) optionalRunes() []rune {
if chars.inBytes {
return nil
}
return *(*[]rune)(unsafe.Pointer(&chars.slice))
}
func (chars *Chars) Get(i int) rune {
if runes := chars.optionalRunes(); runes != nil {
return runes[i]
}
return rune(chars.slice[i])
}
func (chars *Chars) Length() int {
if runes := chars.optionalRunes(); runes != nil {
return len(runes)
}
return len(chars.slice)
}
// TrimLength returns the length after trimming leading and trailing whitespaces
func (chars *Chars) TrimLength() uint16 {
if chars.trimLengthKnown {
return chars.trimLength
}
chars.trimLengthKnown = true
var i int
len := chars.Length()
for i = len - 1; i >= 0; i-- {
char := chars.Get(i)
if !unicode.IsSpace(char) {
break
}
}
// Completely empty
if i < 0 {
return 0
}
var j int
for j = 0; j < len; j++ {
char := chars.Get(j)
if !unicode.IsSpace(char) {
break
}
}
chars.trimLength = AsUint16(i - j + 1)
return chars.trimLength
}
func (chars *Chars) TrailingWhitespaces() int {
whitespaces := 0
for i := chars.Length() - 1; i >= 0; i-- {
char := chars.Get(i)
if !unicode.IsSpace(char) {
break
}
whitespaces++
}
return whitespaces
}
func (chars *Chars) ToString() string {
if runes := chars.optionalRunes(); runes != nil {
return string(runes)
}
return string(chars.slice)
}
func (chars *Chars) ToRunes() []rune {
if runes := chars.optionalRunes(); runes != nil {
return runes
}
bytes := chars.slice
runes := make([]rune, len(bytes))
for idx, b := range bytes {
runes[idx] = rune(b)
}
return runes
}
func (chars *Chars) CopyRunes(dest []rune) {
if runes := chars.optionalRunes(); runes != nil {
copy(dest, runes)
return
}
for idx, b := range chars.slice[:len(dest)] {
dest[idx] = rune(b)
}
return
}

46
src/util/chars_test.go Normal file
View File

@@ -0,0 +1,46 @@
package util
import "testing"
func TestToCharsAscii(t *testing.T) {
chars := ToChars([]byte("foobar"))
if !chars.inBytes || chars.ToString() != "foobar" || !chars.inBytes {
t.Error()
}
}
func TestCharsLength(t *testing.T) {
chars := ToChars([]byte("\tabc한글 "))
if chars.inBytes || chars.Length() != 8 || chars.TrimLength() != 5 {
t.Error()
}
}
func TestCharsToString(t *testing.T) {
text := "\tabc한글 "
chars := ToChars([]byte(text))
if chars.ToString() != text {
t.Error()
}
}
func TestTrimLength(t *testing.T) {
check := func(str string, exp uint16) {
chars := ToChars([]byte(str))
trimmed := chars.TrimLength()
if trimmed != exp {
t.Errorf("Invalid TrimLength result for '%s': %d (expected %d)",
str, trimmed, exp)
}
}
check("hello", 5)
check("hello ", 5)
check("hello ", 5)
check(" hello", 5)
check(" hello", 5)
check(" hello ", 5)
check(" hello ", 5)
check("h o", 5)
check(" h o ", 5)
check(" ", 0)
}

View File

@@ -26,23 +26,23 @@ func NewEventBox() *EventBox {
// Wait blocks the goroutine until signaled // Wait blocks the goroutine until signaled
func (b *EventBox) Wait(callback func(*Events)) { func (b *EventBox) Wait(callback func(*Events)) {
b.cond.L.Lock() b.cond.L.Lock()
defer b.cond.L.Unlock()
if len(b.events) == 0 { if len(b.events) == 0 {
b.cond.Wait() b.cond.Wait()
} }
callback(&b.events) callback(&b.events)
b.cond.L.Unlock()
} }
// Set turns on the event type on the box // Set turns on the event type on the box
func (b *EventBox) Set(event EventType, value interface{}) { func (b *EventBox) Set(event EventType, value interface{}) {
b.cond.L.Lock() b.cond.L.Lock()
defer b.cond.L.Unlock()
b.events[event] = value b.events[event] = value
if _, found := b.ignore[event]; !found { if _, found := b.ignore[event]; !found {
b.cond.Broadcast() b.cond.Broadcast()
} }
b.cond.L.Unlock()
} }
// Clear clears the events // Clear clears the events
@@ -56,27 +56,27 @@ func (events *Events) Clear() {
// Peek peeks at the event box if the given event is set // Peek peeks at the event box if the given event is set
func (b *EventBox) Peek(event EventType) bool { func (b *EventBox) Peek(event EventType) bool {
b.cond.L.Lock() b.cond.L.Lock()
defer b.cond.L.Unlock()
_, ok := b.events[event] _, ok := b.events[event]
b.cond.L.Unlock()
return ok return ok
} }
// Watch deletes the events from the ignore list // Watch deletes the events from the ignore list
func (b *EventBox) Watch(events ...EventType) { func (b *EventBox) Watch(events ...EventType) {
b.cond.L.Lock() b.cond.L.Lock()
defer b.cond.L.Unlock()
for _, event := range events { for _, event := range events {
delete(b.ignore, event) delete(b.ignore, event)
} }
b.cond.L.Unlock()
} }
// Unwatch adds the events to the ignore list // Unwatch adds the events to the ignore list
func (b *EventBox) Unwatch(events ...EventType) { func (b *EventBox) Unwatch(events ...EventType) {
b.cond.L.Lock() b.cond.L.Lock()
defer b.cond.L.Unlock()
for _, event := range events { for _, event := range events {
b.ignore[event] = true b.ignore[event] = true
} }
b.cond.L.Unlock()
} }
// WaitFor blocks the execution until the event is received // WaitFor blocks the execution until the event is received

12
src/util/slab.go Normal file
View File

@@ -0,0 +1,12 @@
package util
type Slab struct {
I16 []int16
I32 []int32
}
func MakeSlab(size16 int, size32 int) *Slab {
return &Slab{
I16: make([]int16, size16),
I32: make([]int32, size32)}
}

View File

@@ -1,24 +1,51 @@
package util package util
// #include <unistd.h>
import "C"
import ( import (
"math"
"os" "os"
"os/exec"
"time" "time"
"unicode/utf8"
"github.com/mattn/go-isatty"
"github.com/mattn/go-runewidth"
) )
// Max returns the largest integer var _runeWidths = make(map[rune]int)
func Max(first int, items ...int) int {
max := first // RuneWidth returns rune width
for _, item := range items { func RuneWidth(r rune, prefixWidth int, tabstop int) int {
if item > max { if r == '\t' {
max = item return tabstop - prefixWidth%tabstop
} } else if w, found := _runeWidths[r]; found {
return w
} else {
w := Max(runewidth.RuneWidth(r), 1)
_runeWidths[r] = w
return w
} }
return max }
// Max returns the largest integer
func Max(first int, second int) int {
if first >= second {
return first
}
return second
}
// Max16 returns the largest integer
func Max16(first int16, second int16) int16 {
if first >= second {
return first
}
return second
}
// Max32 returns the largest 32-bit integer
func Max32(first int32, second int32) int32 {
if first > second {
return first
}
return second
} }
// Min returns the smallest integer // Min returns the smallest integer
@@ -37,14 +64,6 @@ func Min32(first int32, second int32) int32 {
return second return second
} }
// Max32 returns the largest 32-bit integer
func Max32(first int32, second int32) int32 {
if first > second {
return first
}
return second
}
// Constrain32 limits the given 32-bit integer with the upper and lower bounds // Constrain32 limits the given 32-bit integer with the upper and lower bounds
func Constrain32(val int32, min int32, max int32) int32 { func Constrain32(val int32, min int32, max int32) int32 {
if val < min { if val < min {
@@ -67,6 +86,15 @@ func Constrain(val int, min int, max int) int {
return val return val
} }
func AsUint16(val int) uint16 {
if val > math.MaxUint16 {
return math.MaxUint16
} else if val < 0 {
return 0
}
return uint16(val)
}
// DurWithin limits the given time.Duration with the upper and lower bounds // DurWithin limits the given time.Duration with the upper and lower bounds
func DurWithin( func DurWithin(
val time.Duration, min time.Duration, max time.Duration) time.Duration { val time.Duration, min time.Duration, max time.Duration) time.Duration {
@@ -81,66 +109,5 @@ func DurWithin(
// 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 isatty.IsTerminal(os.Stdin.Fd())
}
// TrimRight returns rune array with trailing white spaces cut off
func TrimRight(runes []rune) []rune {
var i int
for i = len(runes) - 1; i >= 0; i-- {
char := runes[i]
if char != ' ' && char != '\t' {
break
}
}
return runes[0 : i+1]
}
// BytesToRunes converts byte array into rune array
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
}
// TrimLen returns the length of trimmed rune array
func TrimLen(runes []rune) int {
var i int
for i = len(runes) - 1; i >= 0; i-- {
char := runes[i]
if char != ' ' && char != '\t' {
break
}
}
// Completely empty
if i < 0 {
return 0
}
var j int
for j = 0; j < len(runes); j++ {
char := runes[j]
if char != ' ' && char != '\t' {
break
}
}
return i - j + 1
}
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string) *exec.Cmd {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "sh"
}
return exec.Command(shell, "-c", command)
} }

View File

@@ -3,7 +3,7 @@ package util
import "testing" import "testing"
func TestMax(t *testing.T) { func TestMax(t *testing.T) {
if Max(-2, 5, 1, 4, 3) != 5 { if Max(-2, 5) != 5 {
t.Error("Invalid result") t.Error("Invalid result")
} }
} }
@@ -20,23 +20,3 @@ func TestContrain(t *testing.T) {
t.Error("Expected", 3) t.Error("Expected", 3)
} }
} }
func TestTrimLen(t *testing.T) {
check := func(str string, exp int) {
trimmed := TrimLen([]rune(str))
if trimmed != exp {
t.Errorf("Invalid TrimLen result for '%s': %d (expected %d)",
str, trimmed, exp)
}
}
check("hello", 5)
check("hello ", 5)
check("hello ", 5)
check(" hello", 5)
check(" hello", 5)
check(" hello ", 5)
check(" hello ", 5)
check("h o", 5)
check(" h o ", 5)
check(" ", 0)
}

38
src/util/util_unix.go Normal file
View File

@@ -0,0 +1,38 @@
// +build !windows
package util
import (
"os"
"os/exec"
"syscall"
)
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string) *exec.Cmd {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "sh"
}
return ExecCommandWith(shell, command)
}
// ExecCommandWith executes the given command with the specified shell
func ExecCommandWith(shell string, command string) *exec.Cmd {
return exec.Command(shell, "-c", command)
}
// IsWindows returns true on Windows
func IsWindows() bool {
return false
}
// SetNonBlock executes syscall.SetNonblock on file descriptor
func SetNonblock(file *os.File, nonblock bool) {
syscall.SetNonblock(int(file.Fd()), nonblock)
}
// Read executes syscall.Read on file descriptor
func Read(fd int, b []byte) (int, error) {
return syscall.Read(int(fd), b)
}

42
src/util/util_windows.go Normal file
View File

@@ -0,0 +1,42 @@
// +build windows
package util
import (
"fmt"
"os"
"os/exec"
"syscall"
)
// ExecCommand executes the given command with cmd
func ExecCommand(command string) *exec.Cmd {
return ExecCommandWith("cmd", command)
}
// ExecCommandWith executes the given command with cmd. _shell parameter is
// ignored on Windows.
func ExecCommandWith(_shell string, command string) *exec.Cmd {
cmd := exec.Command("cmd")
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false,
CmdLine: fmt.Sprintf(` /s /c "%s"`, command),
CreationFlags: 0,
}
return cmd
}
// IsWindows returns true on Windows
func IsWindows() bool {
return true
}
// SetNonBlock executes syscall.SetNonblock on file descriptor
func SetNonblock(file *os.File, nonblock bool) {
syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock)
}
// Read executes syscall.Read on file descriptor
func Read(fd int, b []byte) (int, error) {
return syscall.Read(syscall.Handle(fd), b)
}

View File

@@ -1,15 +1,16 @@
Execute (Setup): Execute (Setup):
let g:dir = fnamemodify(g:vader_file, ':p:h') let g:dir = fnamemodify(g:vader_file, ':p:h')
unlet! g:fzf_layout g:fzf_action g:fzf_history_dir
Log 'Test directory: ' . g:dir Log 'Test directory: ' . g:dir
Save &acd Save &acd
Execute (fzf#run with dir option): Execute (fzf#run with dir option):
let cwd = getcwd() let cwd = getcwd()
let result = fzf#run({ 'options': '--filter=vdr', 'dir': g:dir }) let result = fzf#run({ 'source': 'git ls-files', 'options': '--filter=vdr', 'dir': g:dir })
AssertEqual ['fzf.vader'], result AssertEqual ['fzf.vader'], result
AssertEqual getcwd(), cwd AssertEqual getcwd(), cwd
let result = sort(fzf#run({ 'options': '--filter e', 'dir': g:dir })) let result = sort(fzf#run({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_go.rb'], result AssertEqual ['fzf.vader', 'test_go.rb'], result
AssertEqual getcwd(), cwd AssertEqual getcwd(), cwd
@@ -18,7 +19,7 @@ Execute (fzf#run with Funcref command):
function! g:FzfTest(e) function! g:FzfTest(e)
call add(g:ret, a:e) call add(g:ret, a:e)
endfunction endfunction
let result = sort(fzf#run({ 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir })) let result = sort(fzf#run({ 'source': 'git ls-files', 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_go.rb'], result AssertEqual ['fzf.vader', 'test_go.rb'], result
AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret) AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret)
@@ -43,6 +44,11 @@ Execute (fzf#run with dir option and noautochdir):
" No change in working directory " No change in working directory
AssertEqual cwd, getcwd() AssertEqual cwd, getcwd()
call fzf#run({'source': ['/foobar'], 'sink': 'tabe', 'dir': '/tmp', 'options': '-1'})
AssertEqual cwd, getcwd()
tabclose
AssertEqual cwd, getcwd()
Execute (Incomplete fzf#run with dir option and autochdir): Execute (Incomplete fzf#run with dir option and autochdir):
set acd set acd
let cwd = getcwd() let cwd = getcwd()
@@ -64,6 +70,103 @@ Execute (fzf#run with dir option and autochdir when final cwd is same as dir):
" Working directory changed due to &acd " Working directory changed due to &acd
AssertEqual '/', getcwd() AssertEqual '/', getcwd()
Execute (fzf#wrap):
AssertThrows fzf#wrap({'foo': 'bar'})
let opts = fzf#wrap('foobar')
Log opts
AssertEqual '~40%', opts.down
Assert opts.options =~ '--expect='
Assert !has_key(opts, 'sink')
Assert has_key(opts, 'sink*')
let opts = fzf#wrap('foobar', {}, 0)
Log opts
AssertEqual '~40%', opts.down
let opts = fzf#wrap('foobar', {}, 1)
Log opts
Assert !has_key(opts, 'down')
let opts = fzf#wrap('foobar', {'down': '50%'})
Log opts
AssertEqual '50%', opts.down
let opts = fzf#wrap('foobar', {'down': '50%'}, 1)
Log opts
Assert !has_key(opts, 'down')
let opts = fzf#wrap('foobar', {'sink': 'e'})
Log opts
AssertEqual 'e', opts.sink
Assert !has_key(opts, 'sink*')
let opts = fzf#wrap('foobar', {'options': '--reverse'})
Log opts
Assert opts.options =~ '--expect='
Assert opts.options =~ '--reverse'
let g:fzf_layout = {'window': 'enew'}
let opts = fzf#wrap('foobar')
Log opts
AssertEqual 'enew', opts.window
let opts = fzf#wrap('foobar', {}, 1)
Log opts
Assert !has_key(opts, 'window')
let opts = fzf#wrap('foobar', {'right': '50%'})
Log opts
Assert !has_key(opts, 'window')
AssertEqual '50%', opts.right
let opts = fzf#wrap('foobar', {'right': '50%'}, 1)
Log opts
Assert !has_key(opts, 'window')
Assert !has_key(opts, 'right')
let g:fzf_action = {'a': 'tabe'}
let opts = fzf#wrap('foobar')
Log opts
Assert opts.options =~ '--expect=a'
Assert !has_key(opts, 'sink')
Assert has_key(opts, 'sink*')
let opts = fzf#wrap('foobar', {'sink': 'e'})
Log opts
AssertEqual 'e', opts.sink
Assert !has_key(opts, 'sink*')
let g:fzf_history_dir = '/tmp'
let opts = fzf#wrap('foobar', {'options': '--color light'})
Log opts
Assert opts.options =~ "--history '/tmp/foobar'"
Assert opts.options =~ '--color light'
let g:fzf_colors = { 'fg': ['fg', 'Error'] }
let opts = fzf#wrap({})
Assert opts.options =~ '^--color=fg:'
Execute (fzf#shellescape with sh):
AssertEqual '''''', fzf#shellescape('', 'sh')
AssertEqual '''\''', fzf#shellescape('\', 'sh')
AssertEqual '''""''', fzf#shellescape('""', 'sh')
AssertEqual '''foobar>''', fzf#shellescape('foobar>', 'sh')
AssertEqual '''\\\"\\\''', fzf#shellescape('\\\"\\\', 'sh')
AssertEqual '''echo ''\''''a''\'''' && echo ''\''''b''\''''''', fzf#shellescape('echo ''a'' && echo ''b''', 'sh')
Execute (fzf#shellescape with cmd.exe):
AssertEqual '^"^"', fzf#shellescape('', 'cmd.exe')
AssertEqual '^"\\^"', fzf#shellescape('\', 'cmd.exe')
AssertEqual '^"\^"\^"^"', fzf#shellescape('""', 'cmd.exe')
AssertEqual '^"foobar^>^"', fzf#shellescape('foobar>', 'cmd.exe')
AssertEqual '^"\\\\\\\^"\\\\\\^"', fzf#shellescape('\\\"\\\', 'cmd.exe')
AssertEqual '^"echo ''a'' ^&^& echo ''b''^"', fzf#shellescape('echo ''a'' && echo ''b''', 'cmd.exe')
AssertEqual '^"C:\Program Files ^(x86^)\\^"', fzf#shellescape('C:\Program Files (x86)\', 'cmd.exe')
AssertEqual '^"C:/Program Files ^(x86^)/^"', fzf#shellescape('C:/Program Files (x86)/', 'cmd.exe')
AssertEqual '^"%%USERPROFILE%%^"', fzf#shellescape('%USERPROFILE%', 'cmd.exe')
Execute (Cleanup): Execute (Cleanup):
unlet g:dir unlet g:dir
Restore Restore

File diff suppressed because it is too large Load Diff