Compare commits

...

163 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 https://github.com/vim/vim/commit/1232624ae5b56c167c5982a2620f736c4bbc19ef
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 https://github.com/junegunn/fzf.vim/commit/e7928d154aad71e530daf5c17733275da5732680

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: https://github.com/junegunn/fzf/commit/a9bf29b65ec36d9caa7298aad0322fba8cb3e130
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:

https://github.com/junegunn/fzf.vim/blob/13b27c45c8bdf6c3a41376bb83e4895edadf8c7e/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
56 changed files with 2242 additions and 1694 deletions
+2
View File
@@ -1,7 +1,9 @@
bin/fzf bin/fzf
bin/fzf.exe
target target
pkg pkg
Gemfile.lock Gemfile.lock
.DS_Store .DS_Store
doc/tags doc/tags
vendor vendor
gopath
+4 -5
View File
@@ -1,19 +1,18 @@
language: ruby language: ruby
dist: trusty
sudo: required
matrix: matrix:
include: include:
- env: TAGS= - env: TAGS=
rvm: 2.3.3 rvm: 2.3.3
# - env: TAGS=tcell # - env: TAGS=tcell
# rvm: 2.2.0 # 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: |
make test install && make test install &&
+69 -2
View File
@@ -1,13 +1,80 @@
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 0.16.8
------ ------
- New `change` event and `top` action for `--bind` - New `change` event and `top` action for `--bind`
- `fzf --bind change:top` - `fzf --bind change:top`
- Move cursor to the top result whenever the query string is changed - Move cursor to the top result whenever the query string is changed
- `fzf --bind ctrl-u:unix-word-rubout+top` - `fzf --bind 'ctrl-w:unix-word-rubout+top,ctrl-u:unix-line-discard+top'`
- `top` combined with `unix-word-rubout` - `top` combined with `unix-word-rubout` and `unix-line-discard`
- Fixed inconsistent tiebreak scores when `--nth` is used - Fixed inconsistent tiebreak scores when `--nth` is used
- Proper display of tab characters in `--prompt` - Proper display of tab characters in `--prompt`
- Fixed not to `--cycle` on page-up/page-down to prevent overshoot - Fixed not to `--cycle` on page-up/page-down to prevent overshoot
+46 -5
View File
@@ -37,7 +37,7 @@ Note that the environment variables `FZF_DEFAULT_COMMAND` and
- `g:fzf_action` - `g:fzf_action`
- Customizable extra key bindings for opening selected files in different ways - Customizable extra key bindings for opening selected files in different ways
- `g:fzf_layout` - `g:fzf_layout`
- Determines the size and position of fzf window (tmux pane or Neovim split) - Determines the size and position of fzf window
- `g:fzf_colors` - `g:fzf_colors`
- Customizes fzf colors to match the current color scheme - Customizes fzf colors to match the current color scheme
- `g:fzf_history_dir` - `g:fzf_history_dir`
@@ -55,11 +55,24 @@ let g:fzf_action = {
\ 'ctrl-x': 'split', \ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' } \ '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 " Default fzf layout
" - down / up / left / right " - down / up / left / right
let g:fzf_layout = { 'down': '~40%' } let g:fzf_layout = { 'down': '~40%' }
" In Neovim, you can set up fzf window using a Vim command " 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': 'enew' }
let g:fzf_layout = { 'window': '-tabnew' } let g:fzf_layout = { 'window': '-tabnew' }
let g:fzf_layout = { 'window': '10split enew' } let g:fzf_layout = { 'window': '10split enew' }
@@ -73,6 +86,7 @@ let g:fzf_colors =
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
\ 'hl+': ['fg', 'Statement'], \ 'hl+': ['fg', 'Statement'],
\ 'info': ['fg', 'PreProc'], \ 'info': ['fg', 'PreProc'],
\ 'border': ['fg', 'Ignore'],
\ 'prompt': ['fg', 'Conditional'], \ 'prompt': ['fg', 'Conditional'],
\ 'pointer': ['fg', 'Exception'], \ 'pointer': ['fg', 'Exception'],
\ 'marker': ['fg', 'Keyword'], \ 'marker': ['fg', 'Keyword'],
@@ -102,7 +116,7 @@ following options.
| `options` | string/list | Options to fzf | | `options` | string/list | Options to fzf |
| `dir` | string | Working directory | | `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) | | `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) |
| `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) | | `window` (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` | string | External terminal emulator to start fzf with (GVim only) |
| `launcher` | funcref | Function for generating `launcher` string (GVim only) | | `launcher` | funcref | Function for generating `launcher` string (GVim only) |
@@ -128,11 +142,38 @@ command! -bang MyStuff
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0)) \ 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 GVim
---- ----
In GVim, you need an external terminal emulator to start fzf with. `xterm` With the latest version of GVim, fzf will start inside the builtin terminal
command is used by default, but you can customize it with `g:fzf_launcher`. 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 ```vim
" This is the default. %s is replaced with fzf command " This is the default. %s is replaced with fzf command
+178 -47
View File
@@ -3,15 +3,19 @@
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
- The most comprehensive feature set - The most comprehensive feature set
- Flexible layout using tmux panes - Flexible layout
- Batteries included - Batteries included
- Vim/Neovim plugin, key bindings and fuzzy auto-completion - Vim/Neovim plugin, key bindings and fuzzy auto-completion
@@ -19,9 +23,10 @@ Table of Contents
----------------- -----------------
* [Installation](#installation) * [Installation](#installation)
* [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew)
* [Using git](#using-git) * [Using git](#using-git)
* [Using Homebrew](#using-homebrew)
* [As Vim plugin](#as-vim-plugin) * [As Vim plugin](#as-vim-plugin)
* [Fedora](#fedora)
* [Windows](#windows) * [Windows](#windows)
* [Upgrading fzf](#upgrading-fzf) * [Upgrading fzf](#upgrading-fzf)
* [Building fzf](#building-fzf) * [Building fzf](#building-fzf)
@@ -42,8 +47,12 @@ Table of Contents
* [Settings](#settings) * [Settings](#settings)
* [Supported commands](#supported-commands) * [Supported commands](#supported-commands)
* [Vim plugin](#vim-plugin) * [Vim plugin](#vim-plugin)
* [Advanced topics](#advanced-topics)
* [Performance](#performance)
* [Executing external programs](#executing-external-programs)
* [Preview window](#preview-window)
* [Tips](#tips) * [Tips](#tips)
* [Respecting .gitignore, <code>.hgignore</code>, and <code>svn:ignore</code>](#respecting-gitignore-hgignore-and-svnignore) * [Respecting .gitignore](#respecting-gitignore)
* [git ls-tree for fast traversal](#git-ls-tree-for-fast-traversal) * [git ls-tree for fast traversal](#git-ls-tree-for-fast-traversal)
* [Fish shell](#fish-shell) * [Fish shell](#fish-shell)
* [<a href="LICENSE">License</a>](#license) * [<a href="LICENSE">License</a>](#license)
@@ -65,9 +74,21 @@ stuff.
[bin]: https://github.com/junegunn/fzf-bin/releases [bin]: https://github.com/junegunn/fzf-bin/releases
### Using Homebrew or Linuxbrew
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 ### Using git
Clone this repository and run 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
@@ -75,36 +96,55 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install ~/.fzf/install
``` ```
### Using Homebrew
On OS X, you can use [Homebrew](http://brew.sh/) to install fzf.
```sh
brew install fzf
# Install shell extensions
/usr/local/opt/fzf/install
```
### As Vim plugin ### As Vim plugin
You can manually add the directory to `&runtimepath` as follows, Once you have fzf installed, you can enable it inside Vim simply by adding the
directory to `&runtimepath` as follows:
```vim ```vim
" If installed using git
set rtp+=~/.fzf
" If installed using Homebrew " If installed using Homebrew
set rtp+=/usr/local/opt/fzf set rtp+=/usr/local/opt/fzf
" If installed using git
set rtp+=~/.fzf
``` ```
But it's recommended that you use a plugin manager like If you use [vim-plug](https://github.com/junegunn/vim-plug), the same can be
[vim-plug](https://github.com/junegunn/vim-plug). 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'
``` ```
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 ### Windows
Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also
@@ -116,10 +156,12 @@ available as a [Chocolatey package][choco].
choco install fzf choco install fzf
``` ```
However, other components of the project may not work on Windows. You might However, other components of the project may not work on Windows. Known issues
want to consider installing fzf on [Windows Subsystem for Linux][wsl] where 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. everything runs flawlessly.
[windows-wiki]: https://github.com/junegunn/fzf/wiki/Windows
[wsl]: https://blogs.msdn.microsoft.com/wsl/ [wsl]: https://blogs.msdn.microsoft.com/wsl/
Upgrading fzf Upgrading fzf
@@ -220,7 +262,7 @@ 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"`
@@ -358,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"
} }
``` ```
@@ -383,33 +430,117 @@ Vim plugin
See [README-VIM.md](README-VIM.md). See [README-VIM.md](README-VIM.md).
Advanced topics
---------------
### Performance
fzf is fast, and is [getting even faster][perf]. Performance should not be
a problem in most use cases. However, you might want to be aware of the
options that affect the performance.
- `--ansi` tells fzf to extract and parse ANSI color codes in the input and it
makes the initial scanning slower. So it's not recommended that you add it
to your `$FZF_DEFAULT_OPTS`.
- `--nth` makes fzf slower as fzf has to tokenize each line.
- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each
line.
- If you absolutely need better performance, you can consider using
`--algo=v1` (the default being `v2`) to make fzf use faster greedy
algorithm. However, this algorithm is not guaranteed to find the optimal
ordering of the matches and is not recommended.
[perf]: https://junegunn.kr/images/fzf-0.17.0.png
### Executing external programs
You can set up key bindings for starting external processes without leaving
fzf (`execute`, `execute-silent`).
```bash
# Press F1 to open the file with less without leaving fzf
# Press CTRL-Y to copy the line to clipboard and aborts fzf (requires pbcopy)
fzf --bind 'f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort'
```
See *KEY BINDINGS* section of the man page for details.
### Preview window
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.
```bash
# {} is replaced to the single-quoted string of the focused line
fzf --preview 'cat {}'
```
Since preview window is updated only after the process is complete, it's
important that the command finishes quickly.
```bash
# Use head instead of cat so that the command doesn't take too long to finish
fzf --preview 'head -100 {}'
```
Preview window supports ANSI colors, so you can use programs that
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
---- ----
#### Respecting `.gitignore`, `.hgignore`, and `svn:ignore` #### Respecting `.gitignore`
[ag](https://github.com/ggreer/the_silver_searcher) or You can use [fd](https://github.com/sharkdp/fd),
[pt](https://github.com/monochromegane/the_platinum_searcher) will do the [ripgrep](https://github.com/BurntSushi/ripgrep), or [the silver
filtering: searcher](https://github.com/ggreer/the_silver_searcher) instead of the
default find command to traverse the file system while respecting
`.gitignore`.
```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 don't want to exclude hidden files, use the following command: If you want the command to follow symbolic links, and don't want it to exclude
hidden files, use the following command:
```sh ```sh
export FZF_DEFAULT_COMMAND='ag --hidden --ignore .git -g ""' export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git'
``` ```
#### `git ls-tree` for fast traversal #### `git ls-tree` for fast traversal
@@ -426,10 +557,10 @@ 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)
(will be fixed in 2.6.0) that it doesn't allow reading from STDIN in command reading from STDIN in command substitution, which means simple `vim (fzf)`
substitution, which means simple `vim (fzf)` won't work as expected. The doesn't work as expected. The workaround for fish 2.5.0 and earlier is to use
workaround is to use the `read` fish command: the `read` fish command:
```sh ```sh
fzf | read -l result; and vim $result fzf | read -l result; and vim $result
@@ -457,7 +588,7 @@ make use of this feature. `$dir` defaults to `.` when the last token is not a
valid directory. Example: valid directory. Example:
```sh ```sh
set -l FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'" set -g FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'"
``` ```
[License](LICENSE) [License](LICENSE)
+9 -8
View File
@@ -16,8 +16,8 @@ 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}")
[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) [[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
help() { help() {
>&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] >&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
@@ -146,6 +146,7 @@ cleanup() {
fi fi
if [ $# -gt 0 ]; then if [ $# -gt 0 ]; then
trap - EXIT
exit 130 exit 130
fi fi
} }
@@ -170,21 +171,21 @@ for arg in "${args[@]}"; do
done done
pppid=$$ pppid=$$
trap_set="trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM" echo -n "trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM;" > $argsf
trap_unset="trap - EXIT SIGINT SIGTERM" close="; trap - EXIT SIGINT SIGTERM $close"
if [[ -n "$term" ]] || [[ -t 0 ]]; then if [[ -n "$term" ]] || [[ -t 0 ]]; then
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf
TMUX=$(echo $TMUX | cut -d , -f 1,2) 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 "$trap_set;cd $(printf %q "$PWD");$envs bash $argsf;$trap_unset" $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=$(echo $TMUX | cut -d , -f 1,2) 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 "$trap_set;$envs bash $argsf;$trap_unset" $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
+50 -6
View File
@@ -1,4 +1,4 @@
fzf.txt fzf Last change: April 28 2017 fzf.txt fzf Last change: November 19 2017
FZF - TABLE OF CONTENTS *fzf* *fzf-toc* FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
============================================================================== ==============================================================================
@@ -8,6 +8,8 @@ FZF - TABLE OF CONTENTS *fzf* *fzf-to
Examples Examples
fzf#run fzf#run
fzf#wrap fzf#wrap
fzf inside terminal buffer
Hide statusline
GVim GVim
License License
@@ -61,7 +63,7 @@ Note that the environment variables `FZF_DEFAULT_COMMAND` and
- Customizable extra key bindings for opening selected files in different - Customizable extra key bindings for opening selected files in different
ways ways
- `g:fzf_layout` - `g:fzf_layout`
- Determines the size and position of fzf window (tmux pane or Neovim split) - Determines the size and position of fzf window
- `g:fzf_colors` - `g:fzf_colors`
- Customizes fzf colors to match the current color scheme - Customizes fzf colors to match the current color scheme
- `g:fzf_history_dir` - `g:fzf_history_dir`
@@ -80,11 +82,24 @@ Examples~
\ 'ctrl-x': 'split', \ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' } \ '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 " Default fzf layout
" - down / up / left / right " - down / up / left / right
let g:fzf_layout = { 'down': '~40%' } let g:fzf_layout = { 'down': '~40%' }
" In Neovim, you can set up fzf window using a Vim command " 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': 'enew' }
let g:fzf_layout = { 'window': '-tabnew' } let g:fzf_layout = { 'window': '-tabnew' }
let g:fzf_layout = { 'window': '10split enew' } let g:fzf_layout = { 'window': '10split enew' }
@@ -98,6 +113,7 @@ Examples~
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
\ 'hl+': ['fg', 'Statement'], \ 'hl+': ['fg', 'Statement'],
\ 'info': ['fg', 'PreProc'], \ 'info': ['fg', 'PreProc'],
\ 'border': ['fg', 'Ignore'],
\ 'prompt': ['fg', 'Conditional'], \ 'prompt': ['fg', 'Conditional'],
\ 'pointer': ['fg', 'Exception'], \ 'pointer': ['fg', 'Exception'],
\ 'marker': ['fg', 'Keyword'], \ 'marker': ['fg', 'Keyword'],
@@ -128,7 +144,7 @@ following options.
`options` | string/list | Options to fzf `options` | string/list | Options to fzf
`dir` | string | Working directory `dir` | string | Working directory
`up` / `down` / `left` / `right` | number/string | Use tmux pane with the given size (e.g. `20` , `50%` ) `up` / `down` / `left` / `right` | number/string | Use tmux pane with the given size (e.g. `20` , `50%` )
`window` (Neovim only) | string | Command to open fzf window (e.g. `verticalaboveleft30new` ) `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` | string | External terminal emulator to start fzf with (GVim only)
`launcher` | funcref | Function for generating `launcher` string (GVim only) `launcher` | funcref | Function for generating `launcher` string (GVim only)
---------------------------+---------------+-------------------------------------------------------------- ---------------------------+---------------+--------------------------------------------------------------
@@ -153,11 +169,39 @@ function that decorates the options dictionary so that it understands
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0)) \ 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* GVIM *fzf-gvim*
============================================================================== ==============================================================================
In GVim, you need an external terminal emulator to start fzf with. `xterm` With the latest version of GVim, fzf will start inside the builtin terminal
command is used by default, but you can customize it with `g:fzf_launcher`. 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 " This is the default. %s is replaced with fzf command
let g:fzf_launcher = 'xterm -e bash -ic %s' let g:fzf_launcher = 'xterm -e bash -ic %s'
Generated
+100 -4
View File
@@ -1,24 +1,68 @@
hash: d68dd0bd779ac4ffca1e0c49ca38d85f90d5d68fa8e2d5d7db70a8ce8c662ec1 hash: 92a208bfbaecdf8d1ccaf99a465884c49f9cd91f44f1756d7bbf3290795c781b
updated: 2017-06-01T15:48:41.653745249-07:00 updated: 2017-12-03T13:37:23.420874333+09:00
imports: imports:
- name: github.com/bjwbell/gensimd
version: 06eb18285485c0d572cc7f024050fc6cb652ed4c
subpackages:
- simd
- name: github.com/codegangsta/cli
version: c6af8847eb2b7b297d07c3ede98903e95e680ef9
- name: github.com/gdamore/encoding - name: github.com/gdamore/encoding
version: b23993cbb6353f0e6aa98d0ee318a34728f628b9 version: b23993cbb6353f0e6aa98d0ee318a34728f628b9
- name: github.com/gdamore/tcell - name: github.com/gdamore/tcell
version: 44772c121bb7838819d3ba4a7e84c0c2d617328e version: 0a0db94084dfe181108c18508ebd312f12d331fb
subpackages: subpackages:
- encoding - encoding
- name: github.com/lucasb-eyer/go-colorful - name: github.com/lucasb-eyer/go-colorful
version: c900de9dbbc73129068f5af6a823068fc5f2308c version: c900de9dbbc73129068f5af6a823068fc5f2308c
- name: github.com/Masterminds/semver
version: 15d8430ab86497c5c0da827b748823945e1cf1e1
- name: github.com/Masterminds/vcs
version: 6f1c6d150500e452704e9863f68c2559f58616bf
- name: github.com/mattn/go-isatty - name: github.com/mattn/go-isatty
version: 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8 version: 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8
- name: github.com/mattn/go-runewidth - name: github.com/mattn/go-runewidth
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50 version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
- name: github.com/mattn/go-shellwords - name: github.com/mattn/go-shellwords
version: 02e3cf038dcea8290e44424da473dd12be796a8a version: 02e3cf038dcea8290e44424da473dd12be796a8a
- name: github.com/mengzhuo/intrinsic
version: 34b800838e0bcd9c5b6abd414e3ad03dc1a686b8
subpackages:
- sse2
- name: github.com/mitchellh/go-homedir
version: b8bc1bf767474819792c23f32d8286a45736f1c6
- name: golang.org/x/crypto - name: golang.org/x/crypto
version: e1a4589e7d3ea14a3352255d04b6f1a418845e5e version: e1a4589e7d3ea14a3352255d04b6f1a418845e5e
subpackages: 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/terminal
- ssh/testdata
- name: golang.org/x/net
version: a8b9294777976932365dabb6640cf1468d95c70f
subpackages:
- context
- context/ctxhttp
- name: golang.org/x/sys - name: golang.org/x/sys
version: b90f89a1e7a9c1f6b918820b3daa7f08488c8594 version: b90f89a1e7a9c1f6b918820b3daa7f08488c8594
subpackages: subpackages:
@@ -26,13 +70,65 @@ imports:
- name: golang.org/x/text - name: golang.org/x/text
version: 4ee4af566555f5fbe026368b75596286a312663a version: 4ee4af566555f5fbe026368b75596286a312663a
subpackages: subpackages:
- cases
- collate
- collate/build
- currency
- encoding - encoding
- encoding/charmap - encoding/charmap
- encoding/ianaindex
- encoding/internal - encoding/internal
- encoding/internal/identifier - encoding/internal/identifier
- encoding/japanese - encoding/japanese
- encoding/korean - encoding/korean
- encoding/simplifiedchinese - encoding/simplifiedchinese
- encoding/traditionalchinese - 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 - transform
testImports: [] - 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
+1 -1
View File
@@ -7,7 +7,7 @@ import:
- package: github.com/mattn/go-shellwords - package: github.com/mattn/go-shellwords
version: 02e3cf038dcea8290e44424da473dd12be796a8a version: 02e3cf038dcea8290e44424da473dd12be796a8a
- package: github.com/gdamore/tcell - package: github.com/gdamore/tcell
version: 44772c121bb7838819d3ba4a7e84c0c2d617328e version: 0a0db94084dfe181108c18508ebd312f12d331fb
subpackages: subpackages:
- encoding - encoding
- package: golang.org/x/crypto - package: golang.org/x/crypto
+49 -26
View File
@@ -2,12 +2,13 @@
set -u set -u
version=0.16.8 version=0.17.3
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
binary_arch= binary_arch=
allow_legacy= allow_legacy=
shells="bash zsh fish"
help() { help() {
cat << EOF cat << EOF
@@ -21,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
@@ -47,6 +52,9 @@ for opt in "$@"; do
--32) binary_arch=386 ;; --32) binary_arch=386 ;;
--64) binary_arch=amd64 ;; --64) binary_arch=amd64 ;;
--bin) ;; --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,14 +67,14 @@ 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() {
@@ -160,17 +168,20 @@ binary_error=""
case "$archi" in case "$archi" in
Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64}.tgz ;; Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64}.tgz ;;
Darwin\ *86) download fzf-$version-darwin_${binary_arch:-386}.tgz ;; Darwin\ *86) download fzf-$version-darwin_${binary_arch:-386}.tgz ;;
Linux\ *64) download fzf-$version-linux_${binary_arch:-amd64}.tgz ;;
Linux\ *86) download fzf-$version-linux_${binary_arch:-386}.tgz ;;
Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5}.tgz ;; Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5}.tgz ;;
Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6}.tgz ;; Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6}.tgz ;;
Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7}.tgz ;; Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7}.tgz ;;
Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8}.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\ *64) download fzf-$version-freebsd_${binary_arch:-amd64}.tgz ;;
FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386}.tgz ;; FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386}.tgz ;;
OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64}.tgz ;; OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64}.tgz ;;
OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386}.tgz ;; OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386}.tgz ;;
CYGWIN*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;; 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 ;; *) binary_available=0 binary_error=1 ;;
esac esac
@@ -202,6 +213,17 @@ 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?"
@@ -215,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}
@@ -251,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"
@@ -286,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
@@ -328,11 +350,12 @@ 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
if [ ! -e "$bind_file" ]; then if [ ! -e "$bind_file" ]; then
create_file "$bind_file" \ create_file "$bind_file" \
@@ -346,9 +369,9 @@ 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
+1 -1
View File
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Jun 2017" "fzf 0.16.8" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "Dec 2017" "fzf 0.17.3" "fzf-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane fzf-tmux - open fzf in tmux split pane
+26 -7
View File
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Jun 2017" "fzf 0.16.8" "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
@@ -111,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
@@ -236,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
@@ -268,14 +272,17 @@ string, specify field index expressions between the braces (See \fBFIELD INDEX
EXPRESSION\fR for the details). 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 \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 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 space-separated list of the selected lines (or the current line if no selection
was made) individually quoted. was made) individually quoted.
e.g. \fBfzf --multi --preview="head -10 {+}"\fR e.g. \fBfzf --multi --preview='head -10 {+}'\fR
\fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR \fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR
Also, \fB{q}\fR is replaced to the current query string. Also, \fB{q}\fR is replaced to the current query string.
@@ -327,10 +334,12 @@ 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" .B "--read0"
@@ -356,6 +365,9 @@ e.g. \fBfzf --multi | fzf --sync\fR
.B "--version" .B "--version"
Display version information and exit 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
.B FZF_DEFAULT_COMMAND .B FZF_DEFAULT_COMMAND
@@ -403,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
@@ -464,6 +479,8 @@ 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
@@ -475,12 +492,13 @@ triggered whenever the query string is changed.
\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
@@ -508,8 +526,9 @@ triggered whenever the query string is changed.
\fBpreview-page-up\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)
+72 -48
View File
@@ -35,6 +35,8 @@ else
let s:base_dir = expand('<sfile>:h:h') let s:base_dir = expand('<sfile>:h:h')
endif endif
if s:is_win if s:is_win
let s:term_marker = '&::FZF'
function! s:fzf_call(fn, ...) function! s:fzf_call(fn, ...)
let shellslash = &shellslash let shellslash = &shellslash
try try
@@ -53,6 +55,8 @@ if s:is_win
\ ['chcp %origchcp% > nul'] \ ['chcp %origchcp% > nul']
endfunction endfunction
else else
let s:term_marker = ";#FZF"
function! s:fzf_call(fn, ...) function! s:fzf_call(fn, ...)
return call(a:fn, a:000) return call(a:fn, a:000)
endfunction endfunction
@@ -66,8 +70,8 @@ function! s:shellesc_cmd(arg)
let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g') let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g')
let escaped = substitute(escaped, '%', '%%', 'g') let escaped = substitute(escaped, '%', '%%', 'g')
let escaped = substitute(escaped, '"', '\\^&', 'g') let escaped = substitute(escaped, '"', '\\^&', 'g')
let escaped = substitute(escaped, '\\\+\(\\^\)', '\\\\\1', 'g') let escaped = substitute(escaped, '\(\\\+\)\(\\^\)', '\1\1\2', 'g')
return '^"'.substitute(escaped, '[^\\]\zs\\$', '\\\\', '').'^"' return '^"'.substitute(escaped, '\(\\\+\)$', '\1\1', '').'^"'
endfunction endfunction
function! fzf#shellescape(arg, ...) function! fzf#shellescape(arg, ...)
@@ -149,13 +153,8 @@ function! s:tmux_enabled()
endfunction endfunction
function! s:escape(path) function! s:escape(path)
let escaped_chars = '$%#''"' let path = fnameescape(a:path)
return s:is_win ? escape(path, '$') : path
if has('unix')
let escaped_chars .= ' \'
endif
return escape(a:path, escaped_chars)
endfunction endfunction
" Upgrade legacy options " Upgrade legacy options
@@ -206,7 +205,10 @@ function! s:common_sink(action, lines) abort
return return
endif endif
let key = remove(a:lines, 0) let key = remove(a:lines, 0)
let cmd = get(a:action, key, 'e') let Cmd = get(a:action, key, 'e')
if type(Cmd) == type(function('call'))
return Cmd(a:lines)
endif
if len(a:lines) > 1 if len(a:lines) > 1
augroup fzf_swap augroup fzf_swap
autocmd SwapExists * let v:swapchoice='o' autocmd SwapExists * let v:swapchoice='o'
@@ -222,7 +224,7 @@ function! s:common_sink(action, lines) abort
execute 'e' s:escape(item) execute 'e' s:escape(item)
let empty = 0 let empty = 0
else else
call s:open(cmd, item) call s:open(Cmd, item)
endif endif
if !has('patch-8.0.0177') && !has('nvim-0.2') && exists('#BufEnter') if !has('patch-8.0.0177') && !has('nvim-0.2') && exists('#BufEnter')
\ && isdirectory(item) \ && isdirectory(item)
@@ -330,25 +332,21 @@ function! fzf#wrap(...)
return opts return opts
endfunction endfunction
function! fzf#run(...) abort function! s:use_sh()
try let [shell, shellslash] = [&shell, &shellslash]
let oshell = &shell
let useshellslash = &shellslash
if s:is_win if s:is_win
set shell=cmd.exe set shell=cmd.exe
set noshellslash set noshellslash
else else
set shell=sh set shell=sh
endif endif
return [shell, shellslash]
endfunction
function! fzf#run(...) abort
try
let [shell, shellslash] = s:use_sh()
if has('nvim')
let running = filter(range(1, bufnr('$')), "bufname(v:val) =~# ';#FZF'")
if len(running)
call s:warn('FZF is already running (in buffer '.join(running, ', ').')!')
return []
endif
endif
let dict = exists('a:1') ? s:upgrade(a:1) : {} let dict = exists('a:1') ? s:upgrade(a:1) : {}
let temps = { 'result': s:fzf_tempname() } let temps = { 'result': s:fzf_tempname() }
let optstr = s:evaluate_opts(get(dict, 'options', '')) let optstr = s:evaluate_opts(get(dict, 'options', ''))
@@ -361,18 +359,21 @@ try
if has('nvim') && !has_key(dict, 'dir') if has('nvim') && !has_key(dict, 'dir')
let dict.dir = s:fzf_getcwd() let dict.dir = s:fzf_getcwd()
endif 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) if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) && !s:is_win
let temps.source = s:fzf_tempname().(s:is_win ? '.bat' : '') let temps.source = s:fzf_tempname()
call writefile(s:wrap_cmds(split($FZF_DEFAULT_COMMAND, "\n")), temps.source) call writefile(s:wrap_cmds(split($FZF_DEFAULT_COMMAND, "\n")), temps.source)
let dict.source = (empty($SHELL) ? &shell : $SHELL) . (s:is_win ? ' /c ' : ' ') . fzf#shellescape(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 = s:fzf_tempname() let temps.input = s:fzf_tempname()
call writefile(source, temps.input) call writefile(source, temps.input)
@@ -385,10 +386,13 @@ try
endif endif
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0)
let use_height = has_key(dict, 'down') && let use_height = has_key(dict, 'down') && !has('gui_running') &&
\ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right')) && \ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right', 'window')) &&
\ executable('tput') && filereadable('/dev/tty') \ executable('tput') && filereadable('/dev/tty')
let use_term = has('nvim') && !s:is_win let has_vim8_term = has('terminal') && has('patch-8.0.995')
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
let use_term = has_nvim_term ||
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || !use_height && s:present(dict, 'down', 'up', 'left', 'right', 'window'))
let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict) let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict)
if prefer_tmux && use_tmux if prefer_tmux && use_tmux
let use_height = 0 let use_height = 0
@@ -411,8 +415,7 @@ try
call s:callback(dict, lines) call s:callback(dict, lines)
return lines return lines
finally finally
let &shell = oshell let [&shell, &shellslash] = [shell, shellslash]
let &shellslash = useshellslash
endtry endtry
endfunction endfunction
@@ -468,11 +471,11 @@ augroup fzf_popd
augroup END augroup END
function! s:dopopd() function! s:dopopd()
if !exists('w:fzf_prev_dir') || exists('*haslocaldir') && !haslocaldir() if !exists('w:fzf_dir') || s:fzf_getcwd() != w:fzf_dir[1]
return return
endif endif
execute 'lcd' s:escape(w:fzf_prev_dir) execute 'lcd' s:escape(w:fzf_dir[0])
unlet w:fzf_prev_dir unlet w:fzf_dir
endfunction endfunction
function! s:xterm_launcher() function! s:xterm_launcher()
@@ -526,12 +529,15 @@ function! s:execute(dict, command, use_height, temps) abort
let command = batchfile let command = batchfile
let a:temps.batchfile = batchfile let a:temps.batchfile = batchfile
if has('nvim') if has('nvim')
let s:dict = a:dict
let s:temps = a:temps
let fzf = {} let fzf = {}
let fzf.dict = a:dict
let fzf.temps = a:temps
function! fzf.on_exit(job_id, exit_status, event) dict function! fzf.on_exit(job_id, exit_status, event) dict
let lines = s:collect(s:temps) if s:present(self.dict, 'dir')
call s:callback(s:dict, lines) execute 'lcd' s:escape(self.dict.dir)
endif
let lines = s:collect(self.temps)
call s:callback(self.dict, lines)
endfunction endfunction
let cmd = 'start /wait cmd /c '.command let cmd = 'start /wait cmd /c '.command
call jobstart(cmd, fzf) call jobstart(cmd, fzf)
@@ -628,6 +634,7 @@ function! s:execute_term(dict, command, temps) abort
let winrest = winrestcmd() let winrest = winrestcmd()
let pbuf = bufnr('') let pbuf = bufnr('')
let [ppos, winopts] = s:split(a:dict) let [ppos, winopts] = s:split(a:dict)
call s:use_sh()
let b:fzf = a:dict let b:fzf = a:dict
let fzf = { 'buf': bufnr(''), 'pbuf': pbuf, 'ppos': ppos, 'dict': a:dict, 'temps': a:temps, let fzf = { 'buf': bufnr(''), 'pbuf': pbuf, 'ppos': ppos, 'dict': a:dict, 'temps': a:temps,
\ 'winopts': winopts, 'winrest': winrest, 'lines': &lines, \ 'winopts': winopts, 'winrest': winrest, 'lines': &lines,
@@ -643,7 +650,7 @@ function! s:execute_term(dict, command, temps) abort
endif endif
endif endif
endfunction endfunction
function! fzf.on_exit(id, code, _event) 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
@@ -681,13 +688,28 @@ function! s:execute_term(dict, command, temps) abort
if s:present(a:dict, 'dir') if s:present(a:dict, 'dir')
execute 'lcd' s:escape(a:dict.dir) execute 'lcd' s:escape(a:dict.dir)
endif endif
call termopen(a:command . ';#FZF', fzf) 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 finally
if s:present(a:dict, 'dir') if s:present(a:dict, 'dir')
lcd - lcd -
endif endif
endtry endtry
setlocal nospell bufhidden=wipe nobuflisted setlocal nospell bufhidden=wipe nobuflisted nonumber
setf fzf setf fzf
startinsert startinsert
return [] return []
@@ -718,7 +740,7 @@ function! s:callback(dict, lines) abort
let popd = has_key(a:dict, 'prev_dir') && let popd = has_key(a:dict, 'prev_dir') &&
\ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0]))) \ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0])))
if popd if popd
let w:fzf_prev_dir = a:dict.prev_dir let w:fzf_dir = [a:dict.prev_dir, a:dict.dir]
endif endif
try try
@@ -742,7 +764,7 @@ function! s:callback(dict, lines) abort
" We may have opened a new window or tab " We may have opened a new window or tab
if popd if popd
let w:fzf_prev_dir = a:dict.prev_dir let w:fzf_dir = [a:dict.prev_dir, a:dict.dir]
call s:dopopd() call s:dopopd()
endif endif
endfunction endfunction
@@ -753,9 +775,12 @@ let s:default_action = {
\ 'ctrl-v': 'vsplit' } \ 'ctrl-v': 'vsplit' }
function! s:shortpath() function! s:shortpath()
let short = pathshorten(fnamemodify(getcwd(), ':~:.')) let short = fnamemodify(getcwd(), ':~:.')
if !has('win32unix')
let short = pathshorten(short)
endif
let slash = (s:is_win && !&shellslash) ? '\' : '/' let slash = (s:is_win && !&shellslash) ? '\' : '/'
return empty(short) ? '~'.slash : short . (short =~ slash.'$' ? '' : slash) return empty(short) ? '~'.slash : short . (short =~ escape(slash, '\').'$' ? '' : slash)
endfunction endfunction
function! s:cmd(bang, ...) abort function! s:cmd(bang, ...) abort
@@ -765,13 +790,12 @@ function! s:cmd(bang, ...) abort
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '') let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
if s:is_win && !&shellslash if s:is_win && !&shellslash
let opts.dir = substitute(opts.dir, '/', '\\', 'g') let opts.dir = substitute(opts.dir, '/', '\\', 'g')
elseif has('win32unix')
let opts.dir = fnamemodify(opts.dir, ':p')
endif endif
let prompt = opts.dir let prompt = opts.dir
else else
let prompt = s:shortpath() let prompt = s:shortpath()
endif endif
let prompt = strwidth(prompt) < &columns - 20 ? prompt : '> '
call extend(opts.options, ['--prompt', prompt]) call extend(opts.options, ['--prompt', prompt])
call extend(opts.options, args) call extend(opts.options, args)
call fzf#run(fzf#wrap('FZF', opts, a:bang)) call fzf#run(fzf#wrap('FZF', opts, a:bang))
+10 -6
View File
@@ -113,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"
@@ -122,10 +122,14 @@ _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 | command 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
} }
@@ -215,7 +219,7 @@ _fzf_complete_kill() {
local selected fzf local selected fzf
fzf="$(__fzfcmd_complete)" fzf="$(__fzfcmd_complete)"
selected=$(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=$(command ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ')
printf '\e[5n' printf '\e[5n'
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
@@ -233,8 +237,8 @@ _fzf_complete_telnet() {
_fzf_complete_ssh() { _fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command 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}') \
<(command grep -oE '^[a-z0-9.,:-]+' ~/.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 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command 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
) )
+3 -3
View File
@@ -116,8 +116,8 @@ _fzf_complete_telnet() {
_fzf_complete_ssh() { _fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command 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}') \
<(command grep -oE '^[a-z0-9.,:-]+' ~/.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 }') \
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command 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
) )
@@ -163,7 +163,7 @@ fzf-completion() {
# 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="$(__fzfcmd_complete)" fzf="$(__fzfcmd_complete)"
matches=$(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' ' ') 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
+78 -19
View File
@@ -4,14 +4,9 @@ function fzf_key_bindings
# Store current token in $dir as root for the 'find' command # Store current token in $dir as root for the 'find' command
function fzf-file-widget -d "List files and folders" function fzf-file-widget -d "List files and folders"
set -l dir (commandline -t) set -l commandline (__fzf_parse_commandline)
# The commandline token might be escaped, we need to unescape it. set -l dir $commandline[1]
set dir (eval "printf '%s' $dir") set -l fzf_query $commandline[2]
if [ ! -d "$dir" ]
set dir .
end
# Some 'find' versions print undesired duplicated slashes if the path ends with slashes.
set dir (string replace --regex '(.)/+$' '$1' "$dir")
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not # "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
# $dir itself, even if hidden. # $dir itself, even if hidden.
@@ -19,19 +14,17 @@ function fzf_key_bindings
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -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 | cut -b3-" -o -type l -print 2> /dev/null | sed 's@^\./@@'"
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m" | while read -l r; set result $result $r; end eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
end end
if [ -z "$result" ] if [ -z "$result" ]
commandline -f repaint commandline -f repaint
return return
end else
if [ "$dir" != . ]
# Remove last token from commandline. # Remove last token from commandline.
commandline -t "" commandline -t ""
end end
@@ -46,22 +39,45 @@ function fzf_key_bindings
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
begin 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 -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m"
history | eval (__fzfcmd) -q '(commandline)' | read -l result
and commandline -- $result 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 end
commandline -f repaint commandline -f repaint
end end
function fzf-cd-widget -d "Change directory" 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 . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -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 | cut -b3-" -o -type d -print 2> /dev/null | sed 's@^\./@@'"
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m" | read -l result eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
[ "$result" ]; and cd $result
if [ -n "$result" ]
cd $result
# Remove last token from commandline.
commandline -t ""
end
end end
commandline -f repaint commandline -f repaint
end end
@@ -84,4 +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
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 end
+13 -3
View File
@@ -36,6 +36,16 @@ fzf-file-widget() {
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 . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -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 \
@@ -48,7 +58,7 @@ fzf-cd-widget() {
fi fi
cd "$dir" cd "$dir"
local ret=$? local ret=$?
zle reset-prompt zle fzf-redraw-prompt
typeset -f zle-line-init >/dev/null && zle zle-line-init typeset -f zle-line-init >/dev/null && zle zle-line-init
return $ret return $ret
} }
@@ -59,8 +69,8 @@ bindkey '\ec' fzf-cd-widget
fzf-history-widget() { fzf-history-widget() {
local selected num local selected num
setopt localoptions noglobsubst noposixbuiltins pipefail 2> /dev/null setopt localoptions noglobsubst noposixbuiltins pipefail 2> /dev/null
selected=( $(fc -l 1 | selected=( $(fc -rl 1 |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) ) 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=$? local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$selected[1] num=$selected[1]
+229 -121
View File
@@ -78,9 +78,11 @@ Scoring criteria
*/ */
import ( import (
"bytes"
"fmt" "fmt"
"strings" "strings"
"unicode" "unicode"
"unicode/utf8"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
@@ -156,27 +158,17 @@ func posArray(withPos bool, len int) *[]int {
return nil return nil
} }
func alloc16(offset int, slab *util.Slab, size int, clear bool) (int, []int16) { func alloc16(offset int, slab *util.Slab, size int) (int, []int16) {
if slab != nil && cap(slab.I16) > offset+size { if slab != nil && cap(slab.I16) > offset+size {
slice := slab.I16[offset : offset+size] slice := slab.I16[offset : offset+size]
if clear {
for idx := range slice {
slice[idx] = 0
}
}
return offset + size, slice return offset + size, slice
} }
return offset, make([]int16, size) return offset, make([]int16, size)
} }
func alloc32(offset int, slab *util.Slab, size int, clear bool) (int, []int32) { func alloc32(offset int, slab *util.Slab, size int) (int, []int32) {
if slab != nil && cap(slab.I32) > offset+size { if slab != nil && cap(slab.I32) > offset+size {
slice := slab.I32[offset : offset+size] slice := slab.I32[offset : offset+size]
if clear {
for idx := range slice {
slice[idx] = 0
}
}
return offset + size, slice return offset + size, slice
} }
return offset, make([]int32, size) return offset, make([]int32, size)
@@ -227,7 +219,7 @@ func bonusFor(prevClass charClass, class charClass) int16 {
return 0 return 0
} }
func bonusAt(input util.Chars, idx int) int16 { func bonusAt(input *util.Chars, idx int) int16 {
if idx == 0 { if idx == 0 {
return bonusBoundary return bonusBoundary
} }
@@ -249,21 +241,113 @@ func normalizeRune(r rune) rune {
// Algo functions make two assumptions // Algo functions make two assumptions
// 1. "pattern" is given in lowercase if "caseSensitive" is false // 1. "pattern" is given in lowercase if "caseSensitive" is false
// 2. "pattern" is already normalized if "normalize" is true // 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) type Algo func(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int)
func FuzzyMatchV2(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. // Assume that pattern is given in lowercase if case-insensitive.
// First check if there's a match and calculate bonus for each position. // 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 // If the input string is too long, consider finding the matching chars in
// this phase as well (non-optimal alignment). // this phase as well (non-optimal alignment).
N := input.Length()
M := len(pattern) M := len(pattern)
switch M { if M == 0 {
case 0:
return Result{0, 0, 0}, posArray(withPos, M) return Result{0, 0, 0}, posArray(withPos, M)
case 1:
return ExactMatchNaive(caseSensitive, normalize, forward, input, pattern[0:1], withPos, slab)
} }
N := input.Length()
// Since O(nm) algorithm can be prohibitively expensive for large input, // Since O(nm) algorithm can be prohibitively expensive for large input,
// we fall back to the greedy algorithm. // we fall back to the greedy algorithm.
@@ -271,158 +355,175 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab) 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 // Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
offset16 := 0 offset16 := 0
offset32 := 0 offset32 := 0
offset16, H0 := alloc16(offset16, slab, N)
offset16, C0 := alloc16(offset16, slab, N)
// Bonus point for each position // Bonus point for each position
offset16, B := alloc16(offset16, slab, N, false) offset16, B := alloc16(offset16, slab, N)
// The first occurrence of each character in the pattern // The first occurrence of each character in the pattern
offset32, F := alloc32(offset32, slab, M, false) offset32, F := alloc32(offset32, slab, M)
// Rune array // Rune array
offset32, T := alloc32(offset32, slab, N, false) offset32, T := alloc32(offset32, slab, N)
input.CopyRunes(T)
// Phase 1. Check if there's a match and calculate bonus for each point // Phase 2. Calculate bonus for each point
pidx, lastIdx, prevClass := 0, 0, charNonWord maxScore, maxScorePos := int16(0), 0
for idx := 0; idx < N; idx++ { pidx, lastIdx := 0, 0
char := input.Get(idx) 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 var class charClass
if char <= unicode.MaxASCII { if char <= unicode.MaxASCII {
class = charClassOfAscii(char) class = charClassOfAscii(char)
if !caseSensitive && class == charUpper {
char += 32
}
} else { } else {
class = charClassOfNonAscii(char) class = charClassOfNonAscii(char)
} if !caseSensitive && class == charUpper {
if !caseSensitive && class == charUpper {
if char <= unicode.MaxASCII {
char += 32
} else {
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
if normalize {
char = normalizeRune(char)
}
} }
if normalize { Tsub[off] = char
char = normalizeRune(char) bonus := bonusFor(prevClass, class)
} Bsub[off] = bonus
T[idx] = char
B[idx] = bonusFor(prevClass, class)
prevClass = class prevClass = class
if pidx < M { if char == pchar {
if char == pattern[pidx] { if pidx < M {
lastIdx = idx F[pidx] = int32(idx + off)
F[pidx] = int32(idx)
pidx++ pidx++
pchar = pattern[util.Min(pidx, M-1)]
} }
} else { lastIdx = idx + off
if char == pattern[M-1] {
lastIdx = idx
}
} }
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 { if pidx != M {
return Result{-1, -1, 0}, nil 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 2. Fill in score matrix (H) // Phase 3. Fill in score matrix (H)
// Unlike the original algorithm, we do not allow omission. // Unlike the original algorithm, we do not allow omission.
width := lastIdx - int(F[0]) + 1 f0 := int(F[0])
offset16, H := alloc16(offset16, slab, width*M, false) 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. // Possible length of consecutive chunk at each position.
offset16, C := alloc16(offset16, slab, width*M, false) offset16, C := alloc16(offset16, slab, width*M)
copy(C, C0[f0:lastIdx+1])
maxScore, maxScorePos := int16(0), 0 Fsub := F[1:]
for i := 0; i < M; i++ { Psub := pattern[1:][:len(Fsub)]
I := i * width for off, f := range Fsub {
f := int(f)
pchar := Psub[off]
pidx := off + 1
row := pidx * width
inGap := false inGap := false
for j := int(F[i]); j <= lastIdx; j++ { Tsub := T[f : lastIdx+1]
j0 := j - int(F[0]) 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 var s1, s2, consecutive int16
if j > int(F[i]) { if inGap {
if inGap { s2 = Hleft[off] + scoreGapExtention
s2 = H[I+j0-1] + scoreGapExtention } else {
} else { s2 = Hleft[off] + scoreGapStart
s2 = H[I+j0-1] + scoreGapStart
}
} }
if pattern[i] == T[j] { if pchar == char {
var diag int16 s1 = Hdiag[off] + scoreMatch
if i > 0 && j0 > 0 { b := Bsub[off]
diag = H[I-width+j0-1] consecutive = Cdiag[off] + 1
} // Break consecutive chunk
s1 = diag + scoreMatch if b == bonusBoundary {
b := B[j]
if i > 0 {
// j > 0 if i > 0
consecutive = C[I-width+j0-1] + 1
// Break consecutive chunk
if b == bonusBoundary {
consecutive = 1
} else if consecutive > 1 {
b = util.Max16(b, util.Max16(bonusConsecutive, B[j-int(consecutive)+1]))
}
} else {
consecutive = 1 consecutive = 1
b *= bonusFirstCharMultiplier } else if consecutive > 1 {
b = util.Max16(b, util.Max16(bonusConsecutive, B[col-int(consecutive)+1]))
} }
if s1+b < s2 { if s1+b < s2 {
s1 += B[j] s1 += Bsub[off]
consecutive = 0 consecutive = 0
} else { } else {
s1 += b s1 += b
} }
} }
C[I+j0] = consecutive Csub[off] = consecutive
inGap = s1 < s2 inGap = s1 < s2
score := util.Max16(util.Max16(s1, s2), 0) score := util.Max16(util.Max16(s1, s2), 0)
if i == M-1 && (forward && score > maxScore || !forward && score >= maxScore) { if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {
maxScore, maxScorePos = score, j maxScore, maxScorePos = score, col
} }
H[I+j0] = score Hsub[off] = score
}
if DEBUG {
if i == 0 {
fmt.Print(" ")
for j := int(F[i]); j <= lastIdx; j++ {
fmt.Printf(" " + string(input.Get(j)) + " ")
}
fmt.Println()
}
fmt.Print(string(pattern[i]) + " ")
for idx := int(F[0]); idx < int(F[i]); idx++ {
fmt.Print(" 0 ")
}
for idx := int(F[i]); 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
}
fmt.Printf("%2d ", p)
}
fmt.Println()
} }
} }
// Phase 3. (Optional) Backtrace to find character positions if DEBUG {
debugV2(T, pattern, F, lastIdx, H, C)
}
// Phase 4. (Optional) Backtrace to find character positions
pos := posArray(withPos, M) pos := posArray(withPos, M)
j := int(F[0]) j := f0
if withPos { if withPos {
i := M - 1 i := M - 1
j = maxScorePos j = maxScorePos
preferMatch := true preferMatch := true
for { for {
I := i * width I := i * width
j0 := j - int(F[0]) j0 := j - f0
s := H[I+j0] s := H[I+j0]
var s1, s2 int16 var s1, s2 int16
@@ -451,7 +552,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C
} }
// Implement the same sorting criteria as V2 // 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) { 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) pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0)
pos := posArray(withPos, len(pattern)) pos := posArray(withPos, len(pattern))
prevClass := charNonWord prevClass := charNonWord
@@ -511,10 +612,13 @@ func calculateScore(caseSensitive bool, normalize bool, text util.Chars, pattern
} }
// FuzzyMatchV1 performs fuzzy-match // FuzzyMatchV1 performs fuzzy-match
func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { func FuzzyMatchV1(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}, nil return Result{0, 0, 0}, nil
} }
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
return Result{-1, -1, 0}, nil
}
pidx := 0 pidx := 0
sidx := -1 sidx := -1
@@ -594,7 +698,7 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text util.Ch
// bonus point, instead of stopping immediately after finding the first match. // bonus point, instead of stopping immediately after finding the first match.
// The solution is much cheaper since there is only one possible alignment of // The solution is much cheaper since there is only one possible alignment of
// the pattern. // the pattern.
func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { 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}, nil return Result{0, 0, 0}, nil
} }
@@ -606,6 +710,10 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util
return Result{-1, -1, 0}, nil 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 // For simplicity, only look at the bonus at the first character position
pidx := 0 pidx := 0
bestPos, bonus, bestBonus := -1, int16(0), int16(-1) bestPos, bonus, bestBonus := -1, int16(0), int16(-1)
@@ -660,7 +768,7 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util
} }
// PrefixMatch performs prefix-match // PrefixMatch performs prefix-match
func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { func PrefixMatch(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}, nil return Result{0, 0, 0}, nil
} }
@@ -687,7 +795,7 @@ func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text util.Cha
} }
// SuffixMatch performs suffix-match // SuffixMatch performs suffix-match
func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
lenRunes := text.Length() lenRunes := text.Length()
trimmedLen := lenRunes - text.TrailingWhitespaces() trimmedLen := lenRunes - text.TrailingWhitespaces()
if len(pattern) == 0 { if len(pattern) == 0 {
@@ -718,7 +826,7 @@ func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text util.Cha
} }
// EqualMatch performs equal-match // EqualMatch performs equal-match
func EqualMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
lenPattern := len(pattern) lenPattern := len(pattern)
if text.Length() != lenPattern { if text.Length() != lenPattern {
return Result{-1, -1, 0}, nil return Result{-1, -1, 0}, nil
+2 -1
View File
@@ -17,7 +17,8 @@ func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool
if !caseSensitive { if !caseSensitive {
pattern = strings.ToLower(pattern) pattern = strings.ToLower(pattern)
} }
res, pos := fun(caseSensitive, normalize, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil) chars := util.ToChars([]byte(input))
res, pos := fun(caseSensitive, normalize, forward, &chars, []rune(pattern), true, nil)
var start, end int var start, end int
if pos == nil || len(*pos) == 0 { if pos == nil || len(*pos) == 0 {
start = res.Start start = res.Start
+59 -19
View File
@@ -44,7 +44,21 @@ func init() {
*/ */
// The following regular expression will include not all but most of the // The following regular expression will include not all but most of the
// frequently used ANSI sequences // frequently used ANSI sequences
ansiRegex = regexp.MustCompile("\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x0e\x0f]|.\x08") ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x0e\x0f]|.\x08)")
}
func findAnsiStart(str string) int {
idx := 0
for ; idx < len(str); idx++ {
b := str[idx]
if b == 0x1b || b == 0x0e || b == 0x0f {
return idx
}
if b == 0x08 && idx > 0 {
return idx - 1
}
}
return idx
} }
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) { func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
@@ -55,50 +69,76 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
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
prev := str[idx:offset[0]] for idx := 0; idx < len(str); {
output.WriteString(prev) 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) { if proc != nil && !proc(prev, state) {
return "", nil, nil return "", nil, nil
} }
newState := interpretCode(str[offset[0]:offset[1]], state)
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())) if len(rest) > 0 && state != nil {
} // Update last offset
runeCount += utf8.RuneCountInString(rest)
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
} }
if proc != nil { if proc != nil {
proc(rest, state) proc(rest, state)
} }
if len(offsets) == 0 { if len(offsets) == 0 {
return output.String(), nil, state return trimmed, nil, state
} }
return output.String(), &offsets, state return trimmed, &offsets, state
} }
func interpretCode(ansiCode string, prevState *ansiState) *ansiState { func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
+35 -7
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][]*Result 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 []*Result) { 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 []*Result) {
(*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) ([]*Result, 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) ([]*Result, 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
} }
+15 -16
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 := []*Result{&Result{}} items1 := []Result{Result{}}
items2 := []*Result{&Result{}, &Result{}} 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)
} }
} }
} }
+20 -31
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
} }
+8 -7
View File
@@ -11,8 +11,9 @@ func TestChunkList(t *testing.T) {
// FIXME global // FIXME global
sortCriteria = []criterion{byScore, byLength} sortCriteria = []criterion{byScore, byLength}
cl := NewChunkList(func(s []byte, i int) *Item { cl := NewChunkList(func(item *Item, s []byte) bool {
return &Item{text: util.ToChars(s), index: int32(i * 2)} item.text = util.ToChars(s)
return true
}) })
// Snapshot // Snapshot
@@ -38,11 +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")
} }
if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].index != 0 || if chunk1.items[0].text.ToString() != "hello" ||
(*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].index != 2 { chunk1.items[1].text.ToString() != "world" {
t.Error("Invalid data") t.Error("Invalid data")
} }
if chunk1.IsFull() { if chunk1.IsFull() {
@@ -65,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)
} }
+7 -4
View File
@@ -9,14 +9,17 @@ import (
const ( const (
// Current version // Current version
version = "0.16.8" 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
readerBufferSize = 64 * 1024 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
@@ -52,7 +55,7 @@ var defaultCommand string
func init() { func init() {
if !util.IsWindows() { if !util.IsWindows() {
defaultCommand = `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-` 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" { } 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-"` defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"`
} else { } else {
@@ -68,7 +71,7 @@ const (
EvtSearchProgress EvtSearchProgress
EvtSearchFin EvtSearchFin
EvtHeader EvtHeader
EvtClose EvtReady
) )
const ( const (
+34 -40
View File
@@ -63,76 +63,64 @@ func Run(opts *Options, revision string) {
ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) { ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
return util.ToChars(data), nil return util.ToChars(data), nil
} }
ansiProcessorRunes := func(data []rune) (util.Chars, *[]ansiOffset) {
return util.RunesToChars(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) (util.Chars, *[]ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, offsets, newState := extractColor(string(data), state, nil) trimmed, offsets, newState := extractColor(string(data), state, nil)
state = newState state = newState
return util.RunesToChars([]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) (util.Chars, *[]ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(string(data), nil, nil) trimmed, _, _ := extractColor(string(data), nil, nil)
return util.RunesToChars([]rune(trimmed)), nil return util.ToChars([]byte(trimmed)), nil
} }
} }
ansiProcessorRunes = func(data []rune) (util.Chars, *[]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
} }
chars, colors := ansiProcessor(data) item.text, item.colors = ansiProcessor(data)
return &Item{ item.text.Index = itemIndex
index: int32(index), itemIndex++
trimLength: -1, return true
text: chars,
colors: colors}
}) })
} else { } else {
chunkList = NewChunkList(func(data []byte, index int) *Item { chunkList = NewChunkList(func(item *Item, data []byte) bool {
tokens := Tokenize(util.ToChars(data), opts.Delimiter) tokens := Tokenize(string(data), 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
} }
textRunes := joinTokens(trans) item.text, item.colors = ansiProcessor([]byte(transformed))
item := Item{ item.text.Index = itemIndex
index: int32(index), item.origText = &data
trimLength: -1, itemIndex++
origText: &data, return true
colors: nil}
trimmed, colors := ansiProcessorRunes(textRunes)
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()
} }
@@ -165,17 +153,17 @@ func Run(opts *Options, revision string) {
found := false found := false
if streamingFilter { if streamingFilter {
slab := util.MakeSlab(slab16Size, slab32Size) slab := util.MakeSlab(slab16Size, slab32Size)
reader := Reader{ reader := NewReader(
func(runes []byte) bool { func(runes []byte) bool {
item := chunkList.trans(runes, 0) item := Item{}
if item != nil { if chunkList.trans(&item, runes) {
if result, _, _ := pattern.MatchItem(item, false, slab); result != nil { if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
opts.Printer(item.text.ToString()) opts.Printer(item.text.ToString())
found = true found = true
} }
} }
return false return false
}, eventBox, opts.ReadZero} }, eventBox, opts.ReadZero)
reader.ReadSource() reader.ReadSource()
} else { } else {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
@@ -221,14 +209,19 @@ func Run(opts *Options, revision string) {
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:
@@ -281,6 +274,7 @@ func Run(opts *Options, revision string) {
} }
} }
} }
events.Clear()
}) })
if delay && reading { if delay && reading {
dur := util.DurWithin( dur := util.DurWithin(
+10 -14
View File
@@ -4,27 +4,23 @@ import (
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
// Item represents each input line // Item represents each input line. 56 bytes.
type Item struct { type Item struct {
index int32 text util.Chars // 32 = 24 + 1 + 1 + 2 + 4
trimLength int32 transformed *[]Token // 8
text util.Chars origText *[]byte // 8
origText *[]byte colors *[]ansiOffset // 8
colors *[]ansiOffset
transformed []Token
} }
// Index returns ordinal index of the Item // Index returns ordinal index of the Item
func (item *Item) Index() int32 { func (item *Item) Index() int32 {
return item.index return item.text.Index
} }
func (item *Item) TrimLength() int32 { var minItem = Item{text: util.Chars{Index: -1}}
if item.trimLength >= 0 {
return item.trimLength func (item *Item) TrimLength() uint16 {
} return item.text.TrimLength()
item.trimLength = int32(item.text.TrimLength())
return item.trimLength
} }
// Colors returns ansiOffsets of the Item // Colors returns ansiOffsets of the Item
+4 -4
View File
@@ -131,7 +131,7 @@ func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
type partialResult struct { type partialResult struct {
index int index int
matches []*Result matches []Result
} }
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
@@ -162,7 +162,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
go func(idx int, slab *util.Slab, chunks []*Chunk) { go func(idx int, slab *util.Slab, chunks []*Chunk) {
defer func() { waitGroup.Done() }() defer func() { waitGroup.Done() }()
count := 0 count := 0
allMatches := make([][]*Result, len(chunks)) allMatches := make([][]Result, len(chunks))
for idx, chunk := range chunks { for idx, chunk := range chunks {
matches := request.pattern.Match(chunk, slab) matches := request.pattern.Match(chunk, slab)
allMatches[idx] = matches allMatches[idx] = matches
@@ -172,7 +172,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
} }
countChan <- len(matches) countChan <- len(matches)
} }
sliceMatches := make([]*Result, 0, count) sliceMatches := make([]Result, 0, count)
for _, matches := range allMatches { for _, matches := range allMatches {
sliceMatches = append(sliceMatches, matches...) sliceMatches = append(sliceMatches, matches...)
} }
@@ -212,7 +212,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
} }
} }
partialResults := make([][]*Result, 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
+10 -10
View File
@@ -3,14 +3,14 @@ package fzf
import "fmt" import "fmt"
// EmptyMerger is a Merger with no data // EmptyMerger is a Merger with no data
var EmptyMerger = NewMerger(nil, [][]*Result{}, 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 {
pattern *Pattern pattern *Pattern
lists [][]*Result lists [][]Result
merged []*Result merged []Result
chunks *[]*Chunk chunks *[]*Chunk
cursors []int cursors []int
sorted bool sorted bool
@@ -29,17 +29,17 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
count: 0} 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(pattern *Pattern, lists [][]*Result, sorted bool, tac bool) *Merger { func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger {
mg := Merger{ mg := Merger{
pattern: pattern, pattern: pattern,
lists: lists, lists: lists,
merged: []*Result{}, merged: []Result{},
chunks: nil, chunks: nil,
cursors: make([]int, len(lists)), cursors: make([]int, len(lists)),
sorted: sorted, sorted: sorted,
@@ -59,13 +59,13 @@ func (mg *Merger) Length() int {
} }
// Get returns the pointer to the Result 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) *Result { 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 &Result{item: (*chunk)[idx%chunkSize]} return Result{item: &chunk.items[idx%chunkSize]}
} }
if mg.sorted { if mg.sorted {
@@ -89,7 +89,7 @@ func (mg *Merger) cacheable() bool {
return mg.count < mergerCacheMax return mg.count < mergerCacheMax
} }
func (mg *Merger) mergedGet(idx int) *Result { func (mg *Merger) mergedGet(idx int) Result {
for i := len(mg.merged); i <= idx; i++ { for i := len(mg.merged); i <= idx; i++ {
minRank := minRank() minRank := minRank()
minIdx := -1 minIdx := -1
@@ -100,7 +100,7 @@ func (mg *Merger) mergedGet(idx int) *Result {
continue continue
} }
if cursor >= 0 { if cursor >= 0 {
rank := list[cursor].rank 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
+8 -8
View File
@@ -15,11 +15,11 @@ func assert(t *testing.T, cond bool, msg ...string) {
} }
} }
func randResult() *Result { func randResult() Result {
str := fmt.Sprintf("%d", rand.Uint32()) str := fmt.Sprintf("%d", rand.Uint32())
return &Result{ chars := util.ToChars([]byte(str))
item: &Item{text: util.RunesToChars([]rune(str))}, chars.Index = rand.Int31()
rank: rank{index: rand.Int31()}} return Result{item: &Item{text: chars}}
} }
func TestEmptyMerger(t *testing.T) { func TestEmptyMerger(t *testing.T) {
@@ -29,14 +29,14 @@ 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) ([][]*Result, []*Result) { func buildLists(partiallySorted bool) ([][]Result, []Result) {
numLists := 4 numLists := 4
lists := make([][]*Result, numLists) lists := make([][]Result, numLists)
cnt := 0 cnt := 0
for i := 0; i < numLists; i++ { for i := 0; i < numLists; i++ {
numResults := rand.Int() % 20 numResults := rand.Int() % 20
cnt += numResults cnt += numResults
lists[i] = make([]*Result, numResults) lists[i] = make([]Result, numResults)
for j := 0; j < numResults; j++ { for j := 0; j < numResults; j++ {
item := randResult() item := randResult()
lists[i][j] = item lists[i][j] = item
@@ -45,7 +45,7 @@ func buildLists(partiallySorted bool) ([][]*Result, []*Result) {
sort.Sort(ByRelevance(lists[i])) sort.Sort(ByRelevance(lists[i]))
} }
} }
items := []*Result{} items := []Result{}
for _, list := range lists { for _, list := range lists {
items = append(items, list...) items = append(items, list...)
} }
+19 -5
View File
@@ -430,6 +430,10 @@ func parseKeyChords(str string, message string) map[int]string {
chord = tui.SLeft chord = tui.SLeft
case "shift-right": case "shift-right":
chord = tui.SRight chord = tui.SRight
case "left-click":
chord = tui.LeftClick
case "right-click":
chord = tui.RightClick
case "double-click": case "double-click":
chord = tui.DoubleClick chord = tui.DoubleClick
case "f10": case "f10":
@@ -658,8 +662,12 @@ func parseKeymap(keymap map[int][]action, str string) {
appendAction(actAbort) appendAction(actAbort)
case "accept": case "accept":
appendAction(actAccept) appendAction(actAccept)
case "accept-non-empty":
appendAction(actAcceptNonEmpty)
case "print-query": case "print-query":
appendAction(actPrintQuery) appendAction(actPrintQuery)
case "replace-query":
appendAction(actReplaceQuery)
case "backward-char": case "backward-char":
appendAction(actBackwardChar) appendAction(actBackwardChar)
case "backward-delete-char": case "backward-delete-char":
@@ -833,9 +841,6 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec {
} }
func parseHeight(str string) sizeSpec { func parseHeight(str string) sizeSpec {
if util.IsWindows() {
errorExit("--height options is currently not supported on Windows")
}
size := parseSize(str, 100, "height") size := parseSize(str, 100, "height")
return size return size
} }
@@ -962,7 +967,11 @@ func parseOptions(opts *Options, allArgs []string) {
case "--algo": case "--algo":
opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)")) 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":
@@ -1138,7 +1147,9 @@ func parseOptions(opts *Options, allArgs []string) {
} 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 {
@@ -1197,6 +1208,9 @@ 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[tui.CtrlP]; !prs { if _, prs := opts.Keymap[tui.CtrlP]; !prs {
+9 -3
View File
@@ -6,7 +6,6 @@ import (
"testing" "testing"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
) )
func TestDelimiterRegex(t *testing.T) { func TestDelimiterRegex(t *testing.T) {
@@ -44,7 +43,7 @@ func TestDelimiterRegex(t *testing.T) {
func TestDelimiterRegexString(t *testing.T) { func TestDelimiterRegexString(t *testing.T) {
delim := delimiterRegexp("*") delim := delimiterRegexp("*")
tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim) tokens := Tokenize("-*--*---**---", delim)
if delim.regex != nil || if delim.regex != nil ||
tokens[0].text.ToString() != "-*" || tokens[0].text.ToString() != "-*" ||
tokens[1].text.ToString() != "--*" || tokens[1].text.ToString() != "--*" ||
@@ -57,7 +56,7 @@ func TestDelimiterRegexString(t *testing.T) {
func TestDelimiterRegexRegex(t *testing.T) { func TestDelimiterRegexRegex(t *testing.T) {
delim := delimiterRegexp("--\\*") delim := delimiterRegexp("--\\*")
tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim) tokens := Tokenize("-*--*---**---", delim)
if delim.str != nil || if delim.str != nil ||
tokens[0].text.ToString() != "-*--*" || tokens[0].text.ToString() != "-*--*" ||
tokens[1].text.ToString() != "---*" || tokens[1].text.ToString() != "---*" ||
@@ -415,3 +414,10 @@ func TestPreviewOpts(t *testing.T) {
t.Error(opts.Preview) 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)
}
}
+67 -63
View File
@@ -10,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
@@ -32,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
@@ -48,6 +47,7 @@ type Pattern struct {
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]algo.Algo procFun map[termType]algo.Algo
@@ -60,7 +60,7 @@ var (
) )
func init() { func init() {
_splitRegex = regexp.MustCompile("\\s+") _splitRegex = regexp.MustCompile(" +")
clearPatternCache() clearPatternCache()
clearChunkCache() clearChunkCache()
} }
@@ -81,7 +81,10 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
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)
} }
@@ -101,7 +104,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
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 !cacheable || idx > 0 || term.inv || !fuzzy && term.typ != termExact { if !cacheable || idx > 0 || term.inv || fuzzy && term.typ != termFuzzy || !fuzzy && term.typ != termExact {
cacheable = false cacheable = false
break Loop break Loop
} }
@@ -130,6 +133,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
delimiter: delimiter, delimiter: delimiter,
procFun: make(map[termType]algo.Algo)} procFun: make(map[termType]algo.Algo)}
ptr.cacheKey = ptr.buildCacheKey()
ptr.procFun[termFuzzy] = fuzzyAlgo ptr.procFun[termFuzzy] = fuzzyAlgo
ptr.procFun[termEqual] = algo.EqualMatch ptr.procFun[termEqual] = algo.EqualMatch
ptr.procFun[termExact] = algo.ExactMatchNaive ptr.procFun[termExact] = algo.ExactMatchNaive
@@ -141,27 +145,30 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
} }
func parseTerms(fuzzy bool, caseMode Case, normalize bool, 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
@@ -169,6 +176,11 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
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 && !inv { if fuzzy && !inv {
@@ -179,16 +191,12 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, 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 {
@@ -204,8 +212,7 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
typ: typ, typ: typ,
inv: inv, inv: inv,
text: textRunes, text: textRunes,
caseSensitive: caseSensitive, caseSensitive: caseSensitive})
origText: origText})
switchSet = true switchSet = true
} }
} }
@@ -228,46 +235,36 @@ 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, slab *util.Slab) []*Result { func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
// 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
} }
} }
// Prefix/suffix cache // Prefix/suffix cache
var space []*Result space := _cache.Search(chunk, cacheKey)
Loop:
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 {
space = cached
break Loop
}
}
}
matches := p.matchChunk(chunk, space, slab) matches := p.matchChunk(chunk, space, slab)
@@ -277,19 +274,19 @@ Loop:
return matches return matches
} }
func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []*Result { func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
matches := []*Result{} matches := []Result{}
if space == nil { if space == nil {
for _, item := range *chunk { for idx := 0; idx < chunk.count; idx++ {
if match, _, _ := p.MatchItem(item, false, slab); match != nil { if match, _, _ := p.MatchItem(&chunk.items[idx], false, slab); match != nil {
matches = append(matches, match) matches = append(matches, *match)
} }
} }
} else { } else {
for _, result := range space { for _, result := range space {
if match, _, _ := p.MatchItem(result.item, false, slab); match != nil { if match, _, _ := p.MatchItem(result.item, false, slab); match != nil {
matches = append(matches, match) matches = append(matches, *match)
} }
} }
} }
@@ -300,20 +297,27 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []*
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) { func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
if p.extended { if p.extended {
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) { if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
return buildResult(item, offsets, bonus), offsets, pos result := buildResult(item, offsets, bonus)
return &result, offsets, pos
} }
return nil, nil, nil return nil, nil, nil
} }
offset, bonus, pos := p.basicMatch(item, withPos, slab) offset, bonus, pos := p.basicMatch(item, withPos, slab)
if sidx := offset[0]; sidx >= 0 { if sidx := offset[0]; sidx >= 0 {
offsets := []Offset{offset} offsets := []Offset{offset}
return buildResult(item, offsets, bonus), offsets, pos result := buildResult(item, offsets, bonus)
return &result, offsets, pos
} }
return nil, nil, nil return nil, nil, nil
} }
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) { func (p *Pattern) basicMatch(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)
}
if p.fuzzy { if p.fuzzy {
return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab) return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
} }
@@ -321,7 +325,12 @@ func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset,
} }
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) { 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 totalScore int var totalScore int
var allPos *[]int var allPos *[]int
@@ -365,25 +374,20 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
return offsets, totalScore, allPos 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)
ret = []Token{Token{text: &item.text, prefixLength: 0}} item.transformed = &ret
} else {
tokens := Tokenize(item.text, p.delimiter)
ret = Transform(tokens, p.nth)
}
item.transformed = ret
return ret return ret
} }
func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, *[]int) { func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
for _, part := range tokens { for _, part := range tokens {
if res, pos := pfun(caseSensitive, normalize, forward, *part.text, pattern, withPos, slab); res.Start >= 0 { if res, pos := pfun(caseSensitive, normalize, forward, part.text, pattern, withPos, slab); res.Start >= 0 {
sidx := int32(res.Start) + part.prefixLength sidx := int32(res.Start) + part.prefixLength
eidx := int32(res.End) + part.prefixLength eidx := int32(res.End) + part.prefixLength
if pos != nil { if pos != nil {
+35 -35
View File
@@ -16,7 +16,7 @@ func init() {
func TestParseTermsExtended(t *testing.T) { func TestParseTermsExtended(t *testing.T) {
terms := parseTerms(true, CaseSmart, false, 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 ||
@@ -33,19 +33,11 @@ func TestParseTermsExtended(t *testing.T) {
terms[8][3].typ != termExact || !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)
}
} }
} }
@@ -66,7 +58,7 @@ func TestParseTermsExtendedExact(t *testing.T) {
} }
func TestParseTermsEmpty(t *testing.T) { func TestParseTermsEmpty(t *testing.T) {
terms := parseTerms(true, CaseSmart, false, "' $ ^ !' !^ !$") terms := parseTerms(true, CaseSmart, false, "' ^ !' !^")
if len(terms) != 0 { if len(terms) != 0 {
t.Errorf("%s", terms) t.Errorf("%s", terms)
} }
@@ -77,8 +69,9 @@ func TestExact(t *testing.T) {
clearPatternCache() clearPatternCache()
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true,
[]Range{}, Delimiter{}, []rune("'abc")) []Range{}, Delimiter{}, []rune("'abc"))
chars := util.ToChars([]byte("aabbcc abc"))
res, pos := algo.ExactMatchNaive( res, pos := algo.ExactMatchNaive(
pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text, true, nil) 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)
} }
@@ -93,8 +86,9 @@ func TestEqual(t *testing.T) {
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("^AbC$")) pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int, eidxExpected int) { match := func(str string, sidxExpected int, eidxExpected int) {
chars := util.ToChars([]byte(str))
res, pos := algo.EqualMatch( res, pos := algo.EqualMatch(
pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text, true, nil) 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)
} }
@@ -133,30 +127,29 @@ func TestCaseSensitivity(t *testing.T) {
func TestOrigTextAndTransformed(t *testing.T) { func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg")) pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize(util.RunesToChars([]rune("junegunn")), Delimiter{}) tokens := Tokenize("junegunn", Delimiter{})
trans := Transform(tokens, []Range{Range{1, 1}}) trans := Transform(tokens, []Range{Range{1, 1}})
origBytes := []byte("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: util.RunesToChars([]rune("junegunn")), text: util.ToChars([]byte("junegunn")),
origText: &origBytes, origText: &origBytes,
transformed: trans}, transformed: &trans}
}
pattern.extended = extended pattern.extended = extended
matches := pattern.matchChunk(&chunk, nil, slab) // No cache matches := pattern.matchChunk(&chunk, nil, slab) // No cache
if !(matches[0].item.text.ToString() == "junegunn" && if !(matches[0].item.text.ToString() == "junegunn" &&
string(*matches[0].item.origText) == "junegunn.choi" && string(*matches[0].item.origText) == "junegunn.choi" &&
reflect.DeepEqual(matches[0].item.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[0], true, slab) match, offsets, pos := pattern.MatchItem(&chunk.items[0], true, slab)
if !(match.item.text.ToString() == "junegunn" && if !(match.item.text.ToString() == "junegunn" &&
string(*match.item.origText) == "junegunn.choi" && string(*match.item.origText) == "junegunn.choi" &&
offsets[0][0] == 0 && offsets[0][1] == 5 && offsets[0][0] == 0 && offsets[0][1] == 5 &&
reflect.DeepEqual(match.item.transformed, trans)) { reflect.DeepEqual(*match.item.transformed, trans)) {
t.Error("Invalid match result", match, offsets, extended) t.Error("Invalid match result", match, offsets, extended)
} }
if !((*pos)[0] == 4 && (*pos)[1] == 0) { if !((*pos)[0] == 4 && (*pos)[1] == 0) {
@@ -167,40 +160,47 @@ func TestOrigTextAndTransformed(t *testing.T) {
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) {
clearPatternCache()
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune(patStr)) 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) { func TestCacheable(t *testing.T) {
test := func(fuzzy bool, str string, cacheable bool) { test := func(fuzzy bool, str string, expected string, cacheable bool) {
clearPatternCache() clearPatternCache()
pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, true, []Range{}, Delimiter{}, []rune(str)) 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 { if cacheable != pat.cacheable {
t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable) t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable)
} }
clearPatternCache()
} }
test(true, "foo bar", true) test(true, "foo bar", "foo\tbar", true)
test(true, "foo 'bar", true) test(true, "foo 'bar", "foo\tbar", false)
test(true, "foo !bar", false) test(true, "foo !bar", "foo", false)
test(false, "foo bar", true) test(false, "foo bar", "foo\tbar", true)
test(false, "foo '", true) test(false, "foo 'bar", "foo", false)
test(false, "foo 'bar", false) test(false, "foo '", "foo", true)
test(false, "foo !bar", false) test(false, "foo 'bar", "foo", false)
test(false, "foo !bar", "foo", false)
} }
+51 -12
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) {
@@ -40,7 +78,7 @@ func (r *Reader) feed(src io.Reader) {
// end in delim. // end in delim.
bytea, err := reader.ReadBytes(delim) bytea, err := reader.ReadBytes(delim)
byteaLen := len(bytea) byteaLen := len(bytea)
if len(bytea) > 0 { if byteaLen > 0 {
if err == nil { if err == nil {
// get rid of carriage return if under Windows: // get rid of carriage return if under Windows:
if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') { if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') {
@@ -50,7 +88,7 @@ func (r *Reader) feed(src io.Reader) {
} }
} }
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 {
@@ -59,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
} }
+18 -10
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")
} }
} }
+14 -32
View File
@@ -19,22 +19,17 @@ type colorOffset struct {
index int32 index int32
} }
type rank struct {
points [4]uint16
index int32
}
type Result struct { type Result struct {
item *Item item *Item
rank rank points [4]uint16
} }
func buildResult(item *Item, offsets []Offset, score int) *Result { func buildResult(item *Item, offsets []Offset, score int) Result {
if len(offsets) > 1 { if len(offsets) > 1 {
sort.Sort(ByOrder(offsets)) sort.Sort(ByOrder(offsets))
} }
result := Result{item: item, rank: rank{index: item.index}} result := Result{item: item}
numChars := item.text.Length() numChars := item.text.Length()
minBegin := math.MaxUint16 minBegin := math.MaxUint16
minEnd := math.MaxUint16 minEnd := math.MaxUint16
@@ -57,7 +52,7 @@ func buildResult(item *Item, offsets []Offset, score int) *Result {
// Higher is better // Higher is better
val = math.MaxUint16 - util.AsUint16(score) val = math.MaxUint16 - util.AsUint16(score)
case byLength: case byLength:
val = util.AsUint16(int(item.TrimLength())) val = item.TrimLength()
case byBegin, byEnd: case byBegin, byEnd:
if validOffsetFound { if validOffsetFound {
whitePrefixLen := 0 whitePrefixLen := 0
@@ -75,10 +70,10 @@ func buildResult(item *Item, offsets []Offset, score int) *Result {
} }
} }
} }
result.rank.points[idx] = val result.points[3-idx] = val
} }
return &result return result
} }
// Sort criteria to use. Never changes once fzf is started. // Sort criteria to use. Never changes once fzf is started.
@@ -86,11 +81,11 @@ var sortCriteria []criterion
// Index returns ordinal index of the Item // Index returns ordinal index of the Item
func (result *Result) Index() int32 { func (result *Result) Index() int32 {
return result.item.index return result.item.Index()
} }
func minRank() rank { func minRank() Result {
return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}} return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
} }
func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset { func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset {
@@ -201,7 +196,7 @@ func (a ByOrder) Less(i, j int) bool {
} }
// ByRelevance is for sorting Items // ByRelevance is for sorting Items
type ByRelevance []*Result type ByRelevance []Result
func (a ByRelevance) Len() int { func (a ByRelevance) Len() int {
return len(a) return len(a)
@@ -212,11 +207,11 @@ func (a ByRelevance) Swap(i, j int) {
} }
func (a ByRelevance) Less(i, j int) bool { func (a ByRelevance) Less(i, j int) bool {
return compareRanks((*a[i]).rank, (*a[j]).rank, false) return compareRanks(a[i], a[j], false)
} }
// ByRelevanceTac is for sorting Items // ByRelevanceTac is for sorting Items
type ByRelevanceTac []*Result type ByRelevanceTac []Result
func (a ByRelevanceTac) Len() int { func (a ByRelevanceTac) Len() int {
return len(a) return len(a)
@@ -227,18 +222,5 @@ func (a ByRelevanceTac) Swap(i, j int) {
} }
func (a ByRelevanceTac) Less(i, j int) bool { func (a ByRelevanceTac) Less(i, j int) bool {
return compareRanks((*a[i]).rank, (*a[j]).rank, true) return compareRanks(a[i], a[j], true)
}
func compareRanks(irank rank, jrank rank, tac bool) bool {
for idx := 0; idx < 4; idx++ {
left := irank.points[idx]
right := jrank.points[idx]
if left < right {
return true
} else if left > right {
return false
}
}
return (irank.index <= jrank.index) != tac
} }
+16
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
}
+28 -18
View File
@@ -11,6 +11,11 @@ import (
"github.com/junegunn/fzf/src/util" "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) { func TestOffsetSort(t *testing.T) {
offsets := []Offset{ offsets := []Offset{
Offset{3, 5}, Offset{2, 7}, Offset{3, 5}, Offset{2, 7},
@@ -26,10 +31,10 @@ func TestOffsetSort(t *testing.T) {
} }
func TestRankComparison(t *testing.T) { func TestRankComparison(t *testing.T) {
rank := func(vals ...uint16) rank { rank := func(vals ...uint16) Result {
return rank{ return Result{
points: [4]uint16{vals[0], vals[1], vals[2], vals[3]}, points: [4]uint16{vals[0], vals[1], vals[2], vals[3]},
index: int32(vals[4])} item: &Item{text: util.Chars{Index: int32(vals[4])}}}
} }
if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) || 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(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
@@ -52,36 +57,41 @@ func TestResultRank(t *testing.T) {
sortCriteria = []criterion{byScore, byLength} sortCriteria = []criterion{byScore, byLength}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := buildResult(&Item{text: util.RunesToChars(strs[0]), index: 1, trimLength: -1}, []Offset{}, 2) item1 := buildResult(
if item1.rank.points[0] != math.MaxUint16-2 || // Bonus withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2)
item1.rank.points[1] != 3 || // Length if item1.points[3] != math.MaxUint16-2 || // Bonus
item1.rank.points[2] != 0 || // Unused item1.points[2] != 3 || // Length
item1.rank.points[3] != 0 || // Unused item1.points[1] != 0 || // Unused
item1.item.index != 1 { item1.points[0] != 0 || // Unused
t.Error(item1.rank) item1.item.Index() != 1 {
t.Error(item1)
} }
// Only differ in index // Only differ in index
item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2) item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2)
items := []*Result{item1, item2} items := []Result{item1, item2}
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
if items[0] != item2 || items[1] != item1 { if items[0] != item2 || items[1] != item1 {
t.Error(items) t.Error(items)
} }
items = []*Result{item2, item1, item1, item2} items = []Result{item2, item1, item1, item2}
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
if items[0] != item2 || items[1] != item2 || if items[0] != item2 || items[1] != item2 ||
items[2] != item1 || items[3] != item1 { items[2] != item1 || items[3] != item1 {
t.Error(items, item1, item1.item.index, item2, item2.item.index) t.Error(items, item1, item1.item.Index(), item2, item2.item.Index())
} }
// Sort by relevance // Sort by relevance
item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 3) item3 := buildResult(
item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 4) withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 3)
item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 5) item4 := buildResult(
item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 6) withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 4)
items = []*Result{item1, item2, item3, item4, item5, item6} 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)) sort.Sort(ByRelevance(items))
if !(items[0] == item6 && items[1] == item5 && if !(items[0] == item6 && items[1] == item5 &&
items[2] == item4 && items[3] == item3 && items[2] == item4 && items[3] == item3 &&
+16
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
}
+115 -60
View File
@@ -87,6 +87,7 @@ type Terminal struct {
margin [4]sizeSpec margin [4]sizeSpec
strong tui.Attr strong tui.Attr
bordered bool bordered bool
cleanExit bool
border tui.Window border tui.Window
window tui.Window window tui.Window
pborder tui.Window pborder tui.Window
@@ -94,11 +95,13 @@ type Terminal struct {
count int count int
progress int progress int
reading bool reading bool
success bool
jumping jumpMode jumping jumpMode
jumpLabels string jumpLabels string
printer func(string) printer func(string)
merger *Merger merger *Merger
selected map[int32]selectedItem selected map[int32]selectedItem
version int64
reqBox *util.EventBox reqBox *util.EventBox
preview previewOpts preview previewOpts
previewer previewer previewer previewer
@@ -167,6 +170,7 @@ const (
actBeginningOfLine actBeginningOfLine
actAbort actAbort
actAccept actAccept
actAcceptNonEmpty
actBackwardChar actBackwardChar
actBackwardDeleteChar actBackwardDeleteChar
actBackwardWord actBackwardWord
@@ -200,6 +204,7 @@ const (
actJump actJump
actJumpAccept actJumpAccept
actPrintQuery actPrintQuery
actReplaceQuery
actToggleSort actToggleSort
actTogglePreview actTogglePreview
actTogglePreviewWrap actTogglePreviewWrap
@@ -275,12 +280,18 @@ func defaultKeymap() map[int][]action {
keymap[tui.Rune] = toActions(actRune) keymap[tui.Rune] = toActions(actRune)
keymap[tui.Mouse] = toActions(actMouse) keymap[tui.Mouse] = toActions(actMouse)
keymap[tui.DoubleClick] = toActions(actAccept) keymap[tui.DoubleClick] = toActions(actAccept)
keymap[tui.LeftClick] = toActions(actIgnore)
keymap[tui.RightClick] = toActions(actToggle)
return keymap return keymap
} }
func trimQuery(query string) []rune {
return []rune(strings.Replace(query, "\t", " ", -1))
}
// NewTerminal returns new Terminal object // NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := []rune(opts.Query) input := trimQuery(opts.Query)
var header []string var header []string
if opts.Reverse { if opts.Reverse {
header = opts.Header header = opts.Header
@@ -365,6 +376,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
history: opts.History, history: opts.History,
margin: opts.Margin, margin: opts.Margin,
bordered: opts.Bordered, bordered: opts.Bordered,
cleanExit: opts.ClearOnExit,
strong: strongAttr, strong: strongAttr,
cycle: opts.Cycle, cycle: opts.Cycle,
header: header, header: header,
@@ -372,6 +384,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
ansi: opts.Ansi, ansi: opts.Ansi,
tabstop: opts.Tabstop, tabstop: opts.Tabstop,
reading: true, reading: true,
success: true,
jumping: jumpDisabled, jumping: jumpDisabled,
jumpLabels: opts.JumpLabels, jumpLabels: opts.JumpLabels,
printer: opts.Printer, printer: opts.Printer,
@@ -401,10 +414,11 @@ func (t *Terminal) Input() []rune {
} }
// UpdateCount updates the count information // UpdateCount updates the count information
func (t *Terminal) UpdateCount(cnt int, final bool) { func (t *Terminal) UpdateCount(cnt int, final bool, success bool) {
t.mutex.Lock() t.mutex.Lock()
t.count = cnt t.count = cnt
t.reading = !final t.reading = !final
t.success = success
t.mutex.Unlock() t.mutex.Unlock()
t.reqBox.Set(reqInfo, nil) t.reqBox.Set(reqInfo, nil)
if final { if final {
@@ -613,10 +627,8 @@ func (t *Terminal) resizeWindows() {
width, width,
height, tui.BorderNone) height, tui.BorderNone)
} }
if !t.tui.IsOptimized() { for i := 0; i < t.window.Height(); i++ {
for i := 0; i < t.window.Height(); i++ { t.window.MoveAndClear(i, 0)
t.window.MoveAndClear(i, 0)
}
} }
t.truncateQuery() t.truncateQuery()
} }
@@ -682,6 +694,13 @@ func (t *Terminal) printInfo() {
if t.progress > 0 && t.progress < 100 { if t.progress > 0 && t.progress < 100 {
output += fmt.Sprintf(" (%d%%)", t.progress) output += fmt.Sprintf(" (%d%%)", t.progress)
} }
if !t.success && t.count == 0 {
if len(os.Getenv("FZF_DEFAULT_COMMAND")) > 0 {
output = "[$FZF_DEFAULT_COMMAND failed]"
} else {
output = "[default command failed - $FZF_DEFAULT_COMMAND required]"
}
}
if pos+len(output) <= t.window.Width() { if pos+len(output) <= t.window.Width() {
t.window.CPrint(tui.ColInfo, 0, output) t.window.CPrint(tui.ColInfo, 0, output)
} }
@@ -704,12 +723,12 @@ func (t *Terminal) printHeader() {
trimmed, colors, newState := extractColor(lineStr, state, nil) trimmed, colors, newState := extractColor(lineStr, state, nil)
state = newState state = newState
item := &Item{ item := &Item{
text: util.RunesToChars([]rune(trimmed)), text: util.ToChars([]byte(trimmed)),
colors: colors} colors: colors}
t.move(line, 2, true) t.move(line, 2, true)
t.printHighlighted(&Result{item: item}, t.printHighlighted(Result{item: item},
tui.AttrRegular, tui.ColHeader, tui.ColDefault, false, false) tui.AttrRegular, tui.ColHeader, tui.ColHeader, false, false)
} }
} }
@@ -736,7 +755,7 @@ func (t *Terminal) printList() {
} }
} }
func (t *Terminal) printItem(result *Result, line int, i int, current bool) { func (t *Terminal) printItem(result Result, line int, i int, current bool) {
item := result.item item := result.item
_, selected := t.selected[item.Index()] _, selected := t.selected[item.Index()]
label := " " label := " "
@@ -752,7 +771,7 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) {
// Avoid unnecessary redraw // Avoid unnecessary redraw
newLine := itemLine{current: current, selected: selected, label: label, newLine := itemLine{current: current, selected: selected, label: label,
result: *result, queryLen: len(t.input), width: 0} result: result, queryLen: len(t.input), width: 0}
prevLine := t.prevLines[i] prevLine := t.prevLines[i]
if prevLine.current == newLine.current && if prevLine.current == newLine.current &&
prevLine.selected == newLine.selected && prevLine.selected == newLine.selected &&
@@ -762,8 +781,7 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) {
return return
} }
// Optimized renderer can simply erase to the end of the window t.move(line, 0, false)
t.move(line, 0, t.tui.IsOptimized())
t.window.CPrint(tui.ColCursor, t.strong, label) t.window.CPrint(tui.ColCursor, t.strong, label)
if current { if current {
if selected { if selected {
@@ -780,11 +798,9 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) {
} }
newLine.width = t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false, true) newLine.width = t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false, true)
} }
if !t.tui.IsOptimized() { fillSpaces := prevLine.width - newLine.width
fillSpaces := prevLine.width - newLine.width if fillSpaces > 0 {
if fillSpaces > 0 { t.window.Print(strings.Repeat(" ", fillSpaces))
t.window.Print(strings.Repeat(" ", fillSpaces))
}
} }
t.prevLines[i] = newLine t.prevLines[i] = newLine
} }
@@ -834,7 +850,7 @@ func (t *Terminal) overflow(runes []rune, max int) bool {
return t.displayWidthWithLimit(runes, 0, max) > max return t.displayWidthWithLimit(runes, 0, max) > max
} }
func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) int { func (t *Terminal) printHighlighted(result Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) int {
item := result.item item := result.item
// Overflow // Overflow
@@ -954,6 +970,7 @@ func (t *Terminal) printPreview() {
} }
reader := bufio.NewReader(strings.NewReader(t.previewer.text)) reader := bufio.NewReader(strings.NewReader(t.previewer.text))
lineNo := -t.previewer.offset lineNo := -t.previewer.offset
height := t.pwindow.Height()
var ansi *ansiState var ansi *ansiState
for { for {
line, err := reader.ReadString('\n') line, err := reader.ReadString('\n')
@@ -962,7 +979,8 @@ func (t *Terminal) printPreview() {
line = line[:len(line)-1] line = line[:len(line)-1]
} }
lineNo++ lineNo++
if lineNo > t.pwindow.Height() { if lineNo > height ||
t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
break break
} else if lineNo > 0 { } else if lineNo > 0 {
var fillRet tui.FillReturn var fillRet tui.FillReturn
@@ -975,7 +993,7 @@ func (t *Terminal) printPreview() {
if t.theme != nil && ansi != nil && ansi.colored() { if t.theme != nil && ansi != nil && ansi.colored() {
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str) fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
} else { } else {
fillRet = t.pwindow.Fill(str) fillRet = t.pwindow.CFill(tui.ColNormal.Fg(), tui.ColNormal.Bg(), tui.AttrRegular, str)
} }
return fillRet == tui.FillContinue return fillRet == tui.FillContinue
}) })
@@ -992,7 +1010,7 @@ func (t *Terminal) printPreview() {
} }
} }
t.pwindow.FinishFill() t.pwindow.FinishFill()
if t.previewer.lines > t.pwindow.Height() { if t.previewer.lines > height {
offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines) offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines)
pos := t.pwindow.Width() - len(offset) pos := t.pwindow.Width() - len(offset)
if t.tui.DoesAutoWrap() { if t.tui.DoesAutoWrap() {
@@ -1093,9 +1111,18 @@ func keyMatch(key int, event tui.Event) bool {
event.Type == tui.Mouse && key == tui.DoubleClick && event.MouseEvent.Double event.Type == tui.Mouse && key == tui.DoubleClick && event.MouseEvent.Double
} }
func quoteEntryCmd(entry string) string {
escaped := strings.Replace(entry, `\`, `\\`, -1)
escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
r, _ := regexp.Compile(`[&|<>()@^%!"]`)
return r.ReplaceAllStringFunc(escaped, func(match string) string {
return "^" + match
})
}
func quoteEntry(entry string) string { func quoteEntry(entry string) string {
if util.IsWindows() { if util.IsWindows() {
return strconv.Quote(strings.Replace(entry, "\"", "\\\"", -1)) return quoteEntryCmd(entry)
} }
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
} }
@@ -1163,8 +1190,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo
} }
for idx, item := range items { for idx, item := range items {
chars := util.RunesToChars([]rune(item.AsString(stripAnsi))) tokens := Tokenize(item.AsString(stripAnsi), delimiter)
tokens := Tokenize(chars, delimiter)
trans := Transform(tokens, ranges) trans := Transform(tokens, ranges)
str := string(joinTokens(trans)) str := string(joinTokens(trans))
if delimiter.str != nil { if delimiter.str != nil {
@@ -1248,6 +1274,24 @@ func (t *Terminal) truncateQuery() {
t.cx = util.Constrain(t.cx, 0, len(t.input)) t.cx = util.Constrain(t.cx, 0, len(t.input))
} }
func (t *Terminal) selectItem(item *Item) {
t.selected[item.Index()] = selectedItem{time.Now(), item}
t.version++
}
func (t *Terminal) deselectItem(item *Item) {
delete(t.selected, item.Index())
t.version++
}
func (t *Terminal) toggleItem(item *Item) {
if _, found := t.selected[item.Index()]; !found {
t.selectItem(item)
} else {
t.deselectItem(item)
}
}
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
func (t *Terminal) Loop() { func (t *Terminal) Loop() {
// prof := profile.Start(profile.ProfilePath("/tmp/")) // prof := profile.Start(profile.ProfilePath("/tmp/"))
@@ -1326,6 +1370,12 @@ func (t *Terminal) Loop() {
command := replacePlaceholder(t.preview.command, command := replacePlaceholder(t.preview.command,
t.ansi, t.delimiter, false, string(t.input), request) t.ansi, t.delimiter, false, string(t.input), request)
cmd := util.ExecCommand(command) cmd := util.ExecCommand(command)
if t.pwindow != nil {
env := os.Environ()
env = append(env, fmt.Sprintf("LINES=%d", t.pwindow.Height()))
env = append(env, fmt.Sprintf("COLUMNS=%d", t.pwindow.Width()))
cmd.Env = env
}
out, _ := cmd.CombinedOutput() out, _ := cmd.CombinedOutput()
t.reqBox.Set(reqPreviewDisplay, string(out)) t.reqBox.Set(reqPreviewDisplay, string(out))
} else { } else {
@@ -1335,7 +1385,12 @@ func (t *Terminal) Loop() {
}() }()
} }
exit := func(code int) { exit := func(getCode func() int) {
if !t.cleanExit && t.fullscreen && t.inlineInfo {
t.placeCursor()
}
t.tui.Close()
code := getCode()
if code <= exitNoMatch && t.history != nil { if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input)) t.history.append(string(t.input))
} }
@@ -1345,6 +1400,7 @@ func (t *Terminal) Loop() {
go func() { go func() {
var focused *Item var focused *Item
var version int64
for { for {
t.reqBox.Wait(func(events *util.Events) { t.reqBox.Wait(func(events *util.Events) {
defer events.Clear() defer events.Clear()
@@ -1361,7 +1417,8 @@ func (t *Terminal) Loop() {
case reqList: case reqList:
t.printList() t.printList()
currentFocus := t.currentItem() currentFocus := t.currentItem()
if currentFocus != focused { if currentFocus != focused || version != t.version {
version = t.version
focused = currentFocus focused = currentFocus
if t.isPreviewEnabled() { if t.isPreviewEnabled() {
_, list := t.buildPlusList(t.preview.command, false) _, list := t.buildPlusList(t.preview.command, false)
@@ -1383,11 +1440,12 @@ func (t *Terminal) Loop() {
case reqRedraw: case reqRedraw:
t.redraw() t.redraw()
case reqClose: case reqClose:
t.tui.Close() exit(func() int {
if t.output() { if t.output() {
exit(exitOk) return exitOk
} }
exit(exitNoMatch) return exitNoMatch
})
case reqPreviewDisplay: case reqPreviewDisplay:
t.previewer.text = value.(string) t.previewer.text = value.(string)
t.previewer.lines = strings.Count(t.previewer.text, "\n") t.previewer.lines = strings.Count(t.previewer.text, "\n")
@@ -1396,12 +1454,12 @@ func (t *Terminal) Loop() {
case reqPreviewRefresh: case reqPreviewRefresh:
t.printPreview() t.printPreview()
case reqPrintQuery: case reqPrintQuery:
t.tui.Close() exit(func() int {
t.printer(string(t.input)) t.printer(string(t.input))
exit(exitOk) return exitOk
})
case reqQuit: case reqQuit:
t.tui.Close() exit(func() int { return exitInterrupt })
exit(exitInterrupt)
} }
} }
t.placeCursor() t.placeCursor()
@@ -1426,22 +1484,9 @@ func (t *Terminal) Loop() {
} }
} }
} }
selectItem := func(item *Item) bool {
if _, found := t.selected[item.Index()]; !found {
t.selected[item.Index()] = selectedItem{time.Now(), item}
return true
}
return false
}
toggleY := func(y int) {
item := t.merger.Get(y).item
if !selectItem(item) {
delete(t.selected, item.Index())
}
}
toggle := func() { toggle := func() {
if t.cy < t.merger.Length() { if t.cy < t.merger.Length() {
toggleY(t.cy) t.toggleItem(t.merger.Get(t.cy).item)
req(reqInfo) req(reqInfo)
} }
} }
@@ -1525,6 +1570,11 @@ func (t *Terminal) Loop() {
} }
case actPrintQuery: case actPrintQuery:
req(reqPrintQuery) req(reqPrintQuery)
case actReplaceQuery:
if t.cy >= 0 && t.cy < t.merger.Length() {
t.input = t.merger.Get(t.cy).item.text.ToRunes()
t.cx = len(t.input)
}
case actAbort: case actAbort:
req(reqQuit) req(reqQuit)
case actDeleteChar: case actDeleteChar:
@@ -1555,17 +1605,14 @@ func (t *Terminal) Loop() {
case actSelectAll: case actSelectAll:
if t.multi { if t.multi {
for i := 0; i < t.merger.Length(); i++ { for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i).item t.selectItem(t.merger.Get(i).item)
selectItem(item)
} }
req(reqList, reqInfo) req(reqList, reqInfo)
} }
case actDeselectAll: case actDeselectAll:
if t.multi { if t.multi {
for i := 0; i < t.merger.Length(); i++ { t.selected = make(map[int32]selectedItem)
item := t.merger.Get(i) t.version++
delete(t.selected, item.Index())
}
req(reqList, reqInfo) req(reqList, reqInfo)
} }
case actToggle: case actToggle:
@@ -1576,7 +1623,7 @@ func (t *Terminal) Loop() {
case actToggleAll: case actToggleAll:
if t.multi { if t.multi {
for i := 0; i < t.merger.Length(); i++ { for i := 0; i < t.merger.Length(); i++ {
toggleY(i) t.toggleItem(t.merger.Get(i).item)
} }
req(reqList, reqInfo) req(reqList, reqInfo)
} }
@@ -1610,6 +1657,10 @@ func (t *Terminal) Loop() {
req(reqList) req(reqList)
case actAccept: case actAccept:
req(reqClose) req(reqClose)
case actAcceptNonEmpty:
if len(t.selected) > 0 || t.merger.Length() > 0 || !t.reading && t.count == 0 {
req(reqClose)
}
case actClearScreen: case actClearScreen:
req(reqRedraw) req(reqRedraw)
case actTop: case actTop:
@@ -1674,13 +1725,13 @@ func (t *Terminal) Loop() {
case actPreviousHistory: case actPreviousHistory:
if t.history != nil { if t.history != nil {
t.history.override(string(t.input)) t.history.override(string(t.input))
t.input = []rune(t.history.previous()) t.input = trimQuery(t.history.previous())
t.cx = len(t.input) t.cx = len(t.input)
} }
case actNextHistory: case actNextHistory:
if t.history != nil { if t.history != nil {
t.history.override(string(t.input)) t.history.override(string(t.input))
t.input = []rune(t.history.next()) t.input = trimQuery(t.history.next())
t.cx = len(t.input) t.cx = len(t.input)
} }
case actSigStop: case actSigStop:
@@ -1734,6 +1785,10 @@ func (t *Terminal) Loop() {
toggle() toggle()
} }
req(reqList) req(reqList)
if me.Left {
return doActions(t.keymap[tui.LeftClick], tui.LeftClick)
}
return doActions(t.keymap[tui.RightClick], tui.RightClick)
} }
} }
} }
+20 -1
View File
@@ -10,7 +10,7 @@ import (
func newItem(str string) *Item { func newItem(str string) *Item {
bytes := []byte(str) bytes := []byte(str)
trimmed, _, _ := extractColor(str, nil, nil) trimmed, _, _ := extractColor(str, nil, nil)
return &Item{origText: &bytes, text: util.RunesToChars([]rune(trimmed))} return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
} }
func TestReplacePlaceholder(t *testing.T) { func TestReplacePlaceholder(t *testing.T) {
@@ -91,3 +91,22 @@ func TestReplacePlaceholder(t *testing.T) {
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1) result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1)
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'") 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)
}
}
}
+30 -35
View File
@@ -1,6 +1,7 @@
package fzf package fzf
import ( import (
"bytes"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@@ -74,14 +75,14 @@ func ParseRange(str *string) (Range, bool) {
return newRange(n, n), true return newRange(n, n), true
} }
func withPrefixLengths(tokens []util.Chars, 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 {
// NOTE: &tokens[idx] instead of &tokens chars := util.ToChars([]byte(tokens[idx]))
ret[idx] = Token{&tokens[idx], int32(prefixLength)} ret[idx] = Token{&chars, int32(prefixLength)}
prefixLength += token.Length() prefixLength += chars.Length()
} }
return ret return ret
} }
@@ -92,16 +93,15 @@ const (
awkWhite awkWhite
) )
func awkTokenizer(input util.Chars) ([]util.Chars, int) { func awkTokenizer(input string) ([]string, int) {
// 9, 32 // 9, 32
ret := []util.Chars{} ret := []string{}
prefixLength := 0 prefixLength := 0
state := awkNil state := awkNil
numChars := input.Length()
begin := 0 begin := 0
end := 0 end := 0
for idx := 0; idx < numChars; idx++ { for idx := 0; idx < len(input); idx++ {
r := input.Get(idx) r := input[idx]
white := r == 9 || r == 32 white := r == 9 || r == 32
switch state { switch state {
case awkNil: case awkNil:
@@ -119,19 +119,19 @@ func awkTokenizer(input util.Chars) ([]util.Chars, int) {
if white { if white {
end = idx + 1 end = idx + 1
} else { } else {
ret = append(ret, input.Slice(begin, end)) ret = append(ret, input[begin:end])
state, begin, end = awkBlack, idx, idx+1 state, begin, end = awkBlack, idx, idx+1
} }
} }
} }
if begin < end { if begin < end {
ret = append(ret, input.Slice(begin, end)) 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(text util.Chars, 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(text) tokens, prefixLength := awkTokenizer(text)
@@ -139,36 +139,31 @@ func Tokenize(text util.Chars, delimiter Delimiter) []Token {
} }
if delimiter.str != nil { if delimiter.str != nil {
return withPrefixLengths(text.Split(*delimiter.str), 0) return withPrefixLengths(strings.SplitAfter(text, *delimiter.str), 0)
} }
// FIXME performance // FIXME performance
var tokens []string var tokens []string
if delimiter.regex != nil { if delimiter.regex != nil {
str := text.ToString() for len(text) > 0 {
for len(str) > 0 { loc := delimiter.regex.FindStringIndex(text)
loc := delimiter.regex.FindStringIndex(str) if len(loc) < 2 {
if loc == nil { loc = []int{0, len(text)}
loc = []int{0, len(str)}
} }
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([]util.Chars, len(tokens)) return withPrefixLengths(tokens, 0)
for i, token := range tokens {
asRunes[i] = util.RunesToChars([]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.ToRunes()...) output.WriteString(token.text.ToString())
} }
return ret return output.String()
} }
// 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
@@ -181,7 +176,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
if r.begin == r.end { if r.begin == r.end {
idx := r.begin idx := r.begin
if idx == rangeEllipsis { if idx == rangeEllipsis {
chars := util.RunesToChars(joinTokens(tokens)) chars := util.ToChars([]byte(joinTokens(tokens)))
parts = append(parts, &chars) parts = append(parts, &chars)
} else { } else {
if idx < 0 { if idx < 0 {
@@ -224,15 +219,15 @@ func Transform(tokens []Token, withNth []Range) []Token {
var merged util.Chars var merged util.Chars
switch len(parts) { switch len(parts) {
case 0: case 0:
merged = util.RunesToChars([]rune{}) merged = util.ToChars([]byte{})
case 1: case 1:
merged = *parts[0] merged = *parts[0]
default: default:
runes := []rune{} var output bytes.Buffer
for _, part := range parts { for _, part := range parts {
runes = append(runes, part.ToRunes()...) output.WriteString(part.ToString())
} }
merged = util.RunesToChars(runes) merged = util.ToChars([]byte(output.String()))
} }
var prefixLength int32 var prefixLength int32
+6 -8
View File
@@ -2,8 +2,6 @@ package fzf
import ( import (
"testing" "testing"
"github.com/junegunn/fzf/src/util"
) )
func TestParseRange(t *testing.T) { func TestParseRange(t *testing.T) {
@@ -47,19 +45,19 @@ 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(util.RunesToChars([]rune(input)), Delimiter{}) tokens := Tokenize(input, Delimiter{})
if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 { if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
// With delimiter // With delimiter
tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":")) tokens = Tokenize(input, delimiterRegexp(":"))
if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 { 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(util.RunesToChars([]rune(input)), delimiterRegexp("\\s+")) tokens = Tokenize(input, delimiterRegexp("\\s+"))
if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 || if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 ||
tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 || tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 ||
tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 || tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 ||
@@ -71,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(util.RunesToChars([]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)
@@ -93,7 +91,7 @@ func TestTransform(t *testing.T) {
} }
} }
{ {
tokens := Tokenize(util.RunesToChars([]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)
-1
View File
@@ -33,7 +33,6 @@ func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {} func (r *FullscreenRenderer) Close() {}
func (r *FullscreenRenderer) DoesAutoWrap() bool { return false } func (r *FullscreenRenderer) DoesAutoWrap() bool { return false }
func (r *FullscreenRenderer) IsOptimized() bool { return false }
func (r *FullscreenRenderer) GetChar() Event { return Event{} } func (r *FullscreenRenderer) GetChar() Event { return Event{} }
func (r *FullscreenRenderer) MaxX() int { return 0 } func (r *FullscreenRenderer) MaxX() int { return 0 }
func (r *FullscreenRenderer) MaxY() int { return 0 } func (r *FullscreenRenderer) MaxY() int { return 0 }
+49 -25
View File
@@ -32,7 +32,8 @@ var offsetRegexp *regexp.Regexp = regexp.MustCompile("\x1b\\[([0-9]+);([0-9]+)R"
func openTtyIn() *os.File { func openTtyIn() *os.File {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
if err != nil { if err != nil {
panic("Failed to open " + consoleDevice) fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
os.Exit(2)
} }
return in return in
} }
@@ -104,6 +105,7 @@ type LightWindow struct {
posx int posx int
posy int posy int
tabstop int tabstop int
fg Color
bg Color bg Color
} }
@@ -182,10 +184,18 @@ func (r *LightRenderer) Init() {
if r.fullscreen { if r.fullscreen {
r.smcup() r.smcup()
} else { } else {
r.csi("J") // 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() y, x := r.findOffset()
r.mouse = r.mouse && y >= 0 r.mouse = r.mouse && y >= 0
if x > 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.upOneLine = true
r.makeSpace() r.makeSpace()
} }
@@ -200,7 +210,9 @@ func (r *LightRenderer) Init() {
r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
r.csi("G") r.csi("G")
r.csi("K") r.csi("K")
// r.csi("s") if !r.clearOnExit && !r.fullscreen {
r.csi("s")
}
if !r.fullscreen && r.mouse { if !r.fullscreen && r.mouse {
r.yoffset, _ = r.findOffset() r.yoffset, _ = r.findOffset()
} }
@@ -411,10 +423,12 @@ func (r *LightRenderer) escSequence(sz *int) Event {
return Event{F12, 0, nil} return Event{F12, 0, nil}
} }
} }
// Bracketed paste mode \e[200~ / \e[201 // Bracketed paste mode: \e[200~ ... \e[201~
if r.buffer[3] == 48 && (r.buffer[4] == 48 || r.buffer[4] == 49) && r.buffer[5] == 126 { if r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' {
*sz = 6 // Immediately discard the sequence from the buffer and reread input
return Event{Invalid, 0, nil} r.buffer = r.buffer[6:]
*sz = 0
return r.GetChar()
} }
return Event{Invalid, 0, nil} // INS return Event{Invalid, 0, nil} // INS
case 51: case 51:
@@ -481,16 +495,19 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
} }
*sz = 6 *sz = 6
switch r.buffer[3] { switch r.buffer[3] {
case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl case 32, 34, 36, 40, 48, // mouse-down / shift / cmd / ctrl
35, 39, 43, 51: // mouse-up / shift / cmd / ctrl 35, 39, 43, 51: // mouse-up / shift / cmd / ctrl
mod := r.buffer[3] >= 36 mod := r.buffer[3] >= 36
left := r.buffer[3] == 32
down := r.buffer[3]%2 == 0 down := r.buffer[3]%2 == 0
x := int(r.buffer[4] - 33) x := int(r.buffer[4] - 33)
y := int(r.buffer[5]-33) - r.yoffset y := int(r.buffer[5]-33) - r.yoffset
double := false double := false
if down { if down {
now := time.Now() now := time.Now()
if now.Sub(r.prevDownTime) < doubleClickDuration { if !left { // Right double click is not allowed
r.clickY = []int{}
} else if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, y) r.clickY = append(r.clickY, y)
} else { } else {
r.clickY = []int{y} r.clickY = []int{y}
@@ -503,14 +520,14 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
} }
} }
return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl
97, 101, 105, 113: // scroll-down / shift / cmd / ctrl 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl
mod := r.buffer[3] >= 100 mod := r.buffer[3] >= 100
s := 1 - int(r.buffer[3]%2)*2 s := 1 - int(r.buffer[3]%2)*2
x := int(r.buffer[4] - 33) x := int(r.buffer[4] - 33)
y := int(r.buffer[5]-33) - r.yoffset y := int(r.buffer[5]-33) - r.yoffset
return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, false, mod}}
} }
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
@@ -584,10 +601,8 @@ func (r *LightRenderer) Close() {
} }
r.csi("J") r.csi("J")
} }
} else if r.fullscreen { } else if !r.fullscreen {
r.csi("G") r.csi("u")
} else {
r.move(r.height, 0)
} }
if r.mouse { if r.mouse {
r.csi("?1000l") r.csi("?1000l")
@@ -608,10 +623,6 @@ func (r *LightRenderer) DoesAutoWrap() bool {
return false return false
} }
func (r *LightRenderer) IsOptimized() bool {
return false
}
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window { func (r *LightRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
w := &LightWindow{ w := &LightWindow{
renderer: r, renderer: r,
@@ -622,8 +633,10 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, bord
width: width, width: width,
height: height, height: height,
tabstop: r.tabstop, tabstop: r.tabstop,
fg: colDefault,
bg: colDefault} bg: colDefault}
if r.theme != nil { if r.theme != nil {
w.fg = r.theme.Fg
w.bg = r.theme.Bg w.bg = r.theme.Bg
} }
w.drawBorder() w.drawBorder()
@@ -697,6 +710,10 @@ func (w *LightWindow) X() int {
return w.posx return w.posx
} }
func (w *LightWindow) Y() int {
return w.posy
}
func (w *LightWindow) Enclose(y int, x int) bool { func (w *LightWindow) Enclose(y int, x int) bool {
return x >= w.left && x < (w.left+w.width) && return x >= w.left && x < (w.left+w.width) &&
y >= w.top && y < (w.top+w.height) y >= w.top && y < (w.top+w.height)
@@ -831,17 +848,20 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
for j, wl := range lines { for j, wl := range lines {
if w.posx >= w.Width()-1 && wl.displayWidth == 0 { if w.posx >= w.Width()-1 && wl.displayWidth == 0 {
if w.posy < w.height-1 { if w.posy < w.height-1 {
w.MoveAndClear(w.posy+1, 0) w.Move(w.posy+1, 0)
} }
return FillNextLine return FillNextLine
} }
w.stderrInternal(wl.text, false) w.stderrInternal(wl.text, false)
w.posx += wl.displayWidth w.posx += wl.displayWidth
// Wrap line
if j < len(lines)-1 || i < len(allLines)-1 { if j < len(lines)-1 || i < len(allLines)-1 {
if w.posy+1 >= w.height { if w.posy+1 >= w.height {
return FillSuspend return FillSuspend
} }
w.MoveAndClear(w.posy+1, 0) w.MoveAndClear(w.posy, w.posx)
w.Move(w.posy+1, 0)
onMove() onMove()
} }
} }
@@ -856,24 +876,28 @@ func (w *LightWindow) setBg() {
} }
func (w *LightWindow) Fill(text string) FillReturn { func (w *LightWindow) Fill(text string) FillReturn {
w.MoveAndClear(w.posy, w.posx) w.Move(w.posy, w.posx)
w.setBg() w.setBg()
return w.fill(text, w.setBg) return w.fill(text, w.setBg)
} }
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn { func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
w.MoveAndClear(w.posy, w.posx) w.Move(w.posy, w.posx)
if fg == colDefault {
fg = w.fg
}
if bg == colDefault { if bg == colDefault {
bg = w.bg bg = w.bg
} }
if w.csiColor(fg, bg, attr) { if w.csiColor(fg, bg, attr) {
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
defer w.csi("m") defer w.csi("m")
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
} }
return w.fill(text, w.setBg) return w.fill(text, w.setBg)
} }
func (w *LightWindow) FinishFill() { func (w *LightWindow) FinishFill() {
w.MoveAndClear(w.posy, w.posx)
for y := w.posy + 1; y < w.height; y++ { for y := w.posy + 1; y < w.height; y++ {
w.MoveAndClear(y, 0) w.MoveAndClear(y, 0)
} }
-505
View File
@@ -1,505 +0,0 @@
// +build ncurses
// +build !windows
// +build !tcell
package tui
/*
#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
FILE* c_tty() {
return fopen("/dev/tty", "r");
}
SCREEN* c_newterm(FILE* tty) {
return newterm(NULL, stderr, tty);
}
int c_getcurx(WINDOW* win) {
return getcurx(win);
}
*/
import "C"
import (
"os"
"strconv"
"strings"
"time"
"unicode/utf8"
)
func HasFullscreenRenderer() bool {
return true
}
type Attr C.uint
type CursesWindow struct {
impl *C.WINDOW
top int
left int
width int
height int
}
func (w *CursesWindow) Top() int {
return w.top
}
func (w *CursesWindow) Left() int {
return w.left
}
func (w *CursesWindow) Width() int {
return w.width
}
func (w *CursesWindow) Height() int {
return w.height
}
func (w *CursesWindow) Refresh() {
C.wnoutrefresh(w.impl)
}
func (w *CursesWindow) FinishFill() {
// NO-OP
}
const (
Bold Attr = C.A_BOLD
Dim = C.A_DIM
Blink = C.A_BLINK
Reverse = C.A_REVERSE
Underline = C.A_UNDERLINE
)
var Italic Attr = C.A_VERTICAL << 1 // FIXME
const (
AttrRegular Attr = 0
)
var (
_screen *C.SCREEN
_colorMap map[int]int16
_colorFn func(ColorPair, Attr) (C.short, C.int)
)
func init() {
_colorMap = make(map[int]int16)
if strings.HasPrefix(C.GoString(C.curses_version()), "ncurses 5") {
Italic = C.A_NORMAL
}
}
func (a Attr) Merge(b Attr) Attr {
return a | b
}
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
if C.tigetnum(C.CString("colors")) >= 256 {
return Dark256
}
return Default16
}
func (r *FullscreenRenderer) Init() {
C.setlocale(C.LC_ALL, C.CString(""))
tty := C.c_tty()
if tty == nil {
errorExit("Failed to open /dev/tty")
}
_screen = C.c_newterm(tty)
if _screen == nil {
errorExit("Invalid $TERM: " + os.Getenv("TERM"))
}
C.set_term(_screen)
if r.mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil)
C.mouseinterval(0)
}
C.noecho()
C.raw() // stty dsusp undef
C.nonl()
C.keypad(C.stdscr, true)
delay := 50
delayEnv := os.Getenv("ESCDELAY")
if len(delayEnv) > 0 {
num, err := strconv.Atoi(delayEnv)
if err == nil && num >= 0 {
delay = num
}
}
C.set_escdelay(C.int(delay))
if r.theme != nil {
C.start_color()
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
initPairs(r.theme)
C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
_colorFn = attrColored
} else {
initTheme(r.theme, nil, r.forceBlack)
_colorFn = attrMono
}
C.nodelay(C.stdscr, true)
ch := C.getch()
if ch != C.ERR {
C.ungetch(ch)
}
C.nodelay(C.stdscr, false)
}
func initPairs(theme *ColorTheme) {
C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg))
for _, pair := range []ColorPair{
ColNormal,
ColPrompt,
ColMatch,
ColCurrent,
ColCurrentMatch,
ColSpinner,
ColInfo,
ColCursor,
ColSelected,
ColHeader,
ColBorder} {
C.init_pair(C.short(pair.index()), C.short(pair.Fg()), C.short(pair.Bg()))
}
}
func (r *FullscreenRenderer) Pause(bool) {
C.endwin()
}
func (r *FullscreenRenderer) Resume(bool) {
}
func (r *FullscreenRenderer) Close() {
C.endwin()
C.delscreen(_screen)
}
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
if r.theme != nil {
C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
}
// FIXME Does not implement BorderHorizontal
if borderStyle != BorderNone {
pair, attr := _colorFn(ColBorder, 0)
C.wcolor_set(win, pair, nil)
C.wattron(win, attr)
C.box(win, 0, 0)
C.wattroff(win, attr)
C.wcolor_set(win, 0, nil)
}
return &CursesWindow{
impl: win,
top: top,
left: left,
width: width,
height: height,
}
}
func attrColored(color ColorPair, a Attr) (C.short, C.int) {
return C.short(color.index()), C.int(a)
}
func attrMono(color ColorPair, a Attr) (C.short, C.int) {
return 0, C.int(attrFor(color, a))
}
func (r *FullscreenRenderer) MaxX() int {
return int(C.COLS)
}
func (r *FullscreenRenderer) MaxY() int {
return int(C.LINES)
}
func (w *CursesWindow) Close() {
C.delwin(w.impl)
}
func (w *CursesWindow) Enclose(y int, x int) bool {
return bool(C.wenclose(w.impl, C.int(y), C.int(x)))
}
func (w *CursesWindow) Move(y int, x int) {
C.wmove(w.impl, C.int(y), C.int(x))
}
func (w *CursesWindow) MoveAndClear(y int, x int) {
w.Move(y, x)
C.wclrtoeol(w.impl)
}
func (w *CursesWindow) Print(text string) {
C.waddstr(w.impl, C.CString(strings.Map(func(r rune) rune {
if r < 32 {
return -1
}
return r
}, text)))
}
func (w *CursesWindow) CPrint(color ColorPair, attr Attr, text string) {
p, a := _colorFn(color, attr)
C.wcolor_set(w.impl, p, nil)
C.wattron(w.impl, a)
w.Print(text)
C.wattroff(w.impl, a)
C.wcolor_set(w.impl, 0, nil)
}
func (r *FullscreenRenderer) Clear() {
C.clear()
C.endwin()
}
func (r *FullscreenRenderer) Refresh() {
C.refresh()
}
func (w *CursesWindow) Erase() {
C.werase(w.impl)
}
func (w *CursesWindow) X() int {
return int(C.c_getcurx(w.impl))
}
func (r *FullscreenRenderer) DoesAutoWrap() bool {
return true
}
func (r *FullscreenRenderer) IsOptimized() bool {
return true
}
func (w *CursesWindow) Fill(str string) FillReturn {
if C.waddstr(w.impl, C.CString(str)) == C.OK {
return FillContinue
}
return FillSuspend
}
func (w *CursesWindow) CFill(fg Color, bg Color, attr Attr, str string) FillReturn {
index := ColorPair{fg, bg, -1}.index()
C.wcolor_set(w.impl, C.short(index), nil)
C.wattron(w.impl, C.int(attr))
ret := w.Fill(str)
C.wattroff(w.impl, C.int(attr))
C.wcolor_set(w.impl, 0, nil)
return ret
}
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
for _, w := range windows {
w.Refresh()
}
C.doupdate()
}
func (p ColorPair) index() int16 {
if p.id >= 0 {
return p.id
}
// ncurses does not support 24-bit colors
if p.is24() {
return ColDefault.index()
}
key := p.key()
if found, prs := _colorMap[key]; prs {
return found
}
id := int16(len(_colorMap)) + ColUser.id
C.init_pair(C.short(id), C.short(p.Fg()), C.short(p.Bg()))
_colorMap[key] = id
return id
}
func consume(expects ...rune) bool {
for _, r := range expects {
if int(C.getch()) != int(r) {
return false
}
}
return true
}
func escSequence() Event {
C.nodelay(C.stdscr, true)
defer func() {
C.nodelay(C.stdscr, false)
}()
c := C.getch()
switch c {
case C.ERR:
return Event{ESC, 0, nil}
case CtrlM:
return Event{CtrlAltM, 0, nil}
case '/':
return Event{AltSlash, 0, nil}
case ' ':
return Event{AltSpace, 0, nil}
case 127, C.KEY_BACKSPACE:
return Event{AltBS, 0, nil}
case '[':
// Bracketed paste mode (printf "\e[?2004h")
// \e[200~ TEXT \e[201~
if consume('2', '0', '0', '~') {
return Event{Invalid, 0, nil}
}
}
if c >= 'a' && c <= 'z' {
return Event{AltA + int(c) - 'a', 0, nil}
}
if c >= '0' && c <= '9' {
return Event{Alt0 + int(c) - '0', 0, nil}
}
// Don't care. Ignore the rest.
for ; c != C.ERR; c = C.getch() {
}
return Event{Invalid, 0, nil}
}
func (r *FullscreenRenderer) GetChar() Event {
c := C.getch()
switch c {
case C.ERR:
// Unexpected error from blocking read
r.Close()
errorExit("Failed to read /dev/tty")
case C.KEY_UP:
return Event{Up, 0, nil}
case C.KEY_DOWN:
return Event{Down, 0, nil}
case C.KEY_LEFT:
return Event{Left, 0, nil}
case C.KEY_RIGHT:
return Event{Right, 0, nil}
case C.KEY_HOME:
return Event{Home, 0, nil}
case C.KEY_END:
return Event{End, 0, nil}
case C.KEY_BACKSPACE:
return Event{BSpace, 0, nil}
case C.KEY_F0 + 1:
return Event{F1, 0, nil}
case C.KEY_F0 + 2:
return Event{F2, 0, nil}
case C.KEY_F0 + 3:
return Event{F3, 0, nil}
case C.KEY_F0 + 4:
return Event{F4, 0, nil}
case C.KEY_F0 + 5:
return Event{F5, 0, nil}
case C.KEY_F0 + 6:
return Event{F6, 0, nil}
case C.KEY_F0 + 7:
return Event{F7, 0, nil}
case C.KEY_F0 + 8:
return Event{F8, 0, nil}
case C.KEY_F0 + 9:
return Event{F9, 0, nil}
case C.KEY_F0 + 10:
return Event{F10, 0, nil}
case C.KEY_F0 + 11:
return Event{F11, 0, nil}
case C.KEY_F0 + 12:
return Event{F12, 0, nil}
case C.KEY_DC:
return Event{Del, 0, nil}
case C.KEY_PPAGE:
return Event{PgUp, 0, nil}
case C.KEY_NPAGE:
return Event{PgDn, 0, nil}
case C.KEY_BTAB:
return Event{BTab, 0, nil}
case C.KEY_ENTER:
return Event{CtrlM, 0, nil}
case C.KEY_SLEFT:
return Event{SLeft, 0, nil}
case C.KEY_SRIGHT:
return Event{SRight, 0, nil}
case C.KEY_MOUSE:
var me C.MEVENT
if C.getmouse(&me) != C.ERR {
mod := ((me.bstate & C.BUTTON_SHIFT) | (me.bstate & C.BUTTON_CTRL) | (me.bstate & C.BUTTON_ALT)) > 0
x := int(me.x)
y := int(me.y)
/* Cannot use BUTTON1_DOUBLE_CLICKED due to mouseinterval(0) */
if (me.bstate & C.BUTTON1_PRESSED) > 0 {
now := time.Now()
if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, y)
} else {
r.clickY = []int{y}
r.prevDownTime = now
}
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, false, mod}}
} else if (me.bstate & C.BUTTON1_RELEASED) > 0 {
double := false
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, false, double, mod}}
} else if (me.bstate&0x8000000) > 0 || (me.bstate&0x80) > 0 {
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}}
} else if (me.bstate & C.BUTTON4_PRESSED) > 0 {
return Event{Mouse, 0, &MouseEvent{y, x, 1, false, false, mod}}
}
}
return Event{Invalid, 0, nil}
case C.KEY_RESIZE:
return Event{Resize, 0, nil}
case ESC:
return escSequence()
case 127:
return Event{BSpace, 0, nil}
case 0:
return Event{CtrlSpace, 0, nil}
}
// CTRL-A ~ CTRL-Z
if c >= CtrlA && c <= CtrlZ {
return Event{int(c), 0, nil}
}
// Multi-byte character
buffer := []byte{byte(c)}
for {
r, _ := utf8.DecodeRune(buffer)
if r != utf8.RuneError {
return Event{Rune, r, nil}
}
c := C.getch()
if c == C.ERR {
break
}
if c >= C.KEY_CODE_YES {
C.ungetch(c)
break
}
buffer = append(buffer, byte(c))
}
return Event{Invalid, 0, nil}
}
+26 -18
View File
@@ -164,11 +164,11 @@ func (w *TcellWindow) X() int {
return w.lastX return w.lastX
} }
func (r *FullscreenRenderer) DoesAutoWrap() bool { func (w *TcellWindow) Y() int {
return false return w.lastY
} }
func (r *FullscreenRenderer) IsOptimized() bool { func (r *FullscreenRenderer) DoesAutoWrap() bool {
return false return false
} }
@@ -193,19 +193,22 @@ func (r *FullscreenRenderer) GetChar() Event {
button := ev.Buttons() button := ev.Buttons()
mod := ev.Modifiers() != 0 mod := ev.Modifiers() != 0
if button&tcell.WheelDown != 0 { if button&tcell.WheelDown != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
} else if button&tcell.WheelUp != 0 { } else if button&tcell.WheelUp != 0 {
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, mod}} return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
} else if runtime.GOOS != "windows" { } else if runtime.GOOS != "windows" {
// double and single taps on Windows don't quite work due to // double and single taps on Windows don't quite work due to
// the console acting on the events and not allowing us // the console acting on the events and not allowing us
// to consume them. // to consume them.
down := button&tcell.Button1 != 0 // left left := button&tcell.Button1 != 0
down := left || button&tcell.Button3 != 0
double := false double := false
if down { if down {
now := time.Now() now := time.Now()
if now.Sub(r.prevDownTime) < doubleClickDuration { if !left {
r.clickY = []int{}
} else if now.Sub(r.prevDownTime) < doubleClickDuration {
r.clickY = append(r.clickY, x) r.clickY = append(r.clickY, x)
} else { } else {
r.clickY = []int{x} r.clickY = []int{x}
@@ -218,7 +221,7 @@ func (r *FullscreenRenderer) GetChar() Event {
} }
} }
return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}} return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
} }
// process keyboard: // process keyboard:
@@ -405,14 +408,13 @@ func (w *TcellWindow) Close() {
func fill(x, y, w, h int, r rune) { func fill(x, y, w, h int, r rune) {
for ly := 0; ly <= h; ly++ { for ly := 0; ly <= h; ly++ {
for lx := 0; lx <= w; lx++ { for lx := 0; lx <= w; lx++ {
_screen.SetContent(x+lx, y+ly, r, nil, ColDefault.style()) _screen.SetContent(x+lx, y+ly, r, nil, ColNormal.style())
} }
} }
} }
func (w *TcellWindow) Erase() { func (w *TcellWindow) Erase() {
// TODO fill(w.left-1, w.top, w.width+1, w.height, ' ')
fill(w.left, w.top, w.width, w.height, ' ')
} }
func (w *TcellWindow) Enclose(y int, x int) bool { func (w *TcellWindow) Enclose(y int, x int) bool {
@@ -429,13 +431,13 @@ func (w *TcellWindow) Move(y int, x int) {
func (w *TcellWindow) MoveAndClear(y int, x int) { func (w *TcellWindow) MoveAndClear(y int, x int) {
w.Move(y, x) w.Move(y, x)
for i := w.lastX; i < w.width; i++ { for i := w.lastX; i < w.width; i++ {
_screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, ColDefault.style()) _screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, ColNormal.style())
} }
w.lastX = x w.lastX = x
} }
func (w *TcellWindow) Print(text string) { func (w *TcellWindow) Print(text string) {
w.printString(text, ColDefault, 0) w.printString(text, ColNormal, 0)
} }
func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) { func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) {
@@ -448,7 +450,7 @@ func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) {
Reverse(a&Attr(tcell.AttrReverse) != 0). Reverse(a&Attr(tcell.AttrReverse) != 0).
Underline(a&Attr(tcell.AttrUnderline) != 0) Underline(a&Attr(tcell.AttrUnderline) != 0)
} else { } else {
style = ColDefault.style(). style = ColNormal.style().
Reverse(a&Attr(tcell.AttrReverse) != 0 || pair == ColCurrent || pair == ColCurrentMatch). Reverse(a&Attr(tcell.AttrReverse) != 0 || pair == ColCurrent || pair == ColCurrentMatch).
Underline(a&Attr(tcell.AttrUnderline) != 0 || pair == ColMatch || pair == ColCurrentMatch) Underline(a&Attr(tcell.AttrUnderline) != 0 || pair == ColMatch || pair == ColCurrentMatch)
} }
@@ -499,7 +501,7 @@ func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn
if w.color { if w.color {
style = pair.style() style = pair.style()
} else { } else {
style = ColDefault.style() style = ColNormal.style()
} }
style = style. style = style.
Blink(a&Attr(tcell.AttrBlink) != 0). Blink(a&Attr(tcell.AttrBlink) != 0).
@@ -539,11 +541,17 @@ func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn
} }
func (w *TcellWindow) Fill(str string) FillReturn { func (w *TcellWindow) Fill(str string) FillReturn {
return w.fillString(str, ColDefault, 0) return w.fillString(str, ColNormal, 0)
} }
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn { func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
return w.fillString(str, ColorPair{fg, bg, -1}, a) 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) { func (w *TcellWindow) drawBorder(around bool) {
@@ -556,7 +564,7 @@ func (w *TcellWindow) drawBorder(around bool) {
if w.color { if w.color {
style = ColBorder.style() style = ColBorder.style()
} else { } else {
style = ColDefault.style() style = ColNormal.style()
} }
for x := left; x < right; x++ { for x := left; x < right; x++ {
+35 -39
View File
@@ -44,6 +44,8 @@ const (
Resize Resize
Mouse Mouse
DoubleClick DoubleClick
LeftClick
RightClick
BTab BTab
BSpace BSpace
@@ -133,7 +135,7 @@ const (
type ColorPair struct { type ColorPair struct {
fg Color fg Color
bg Color bg Color
id int16 id int
} }
func HexToColor(rrggbb string) Color { func HexToColor(rrggbb string) Color {
@@ -155,12 +157,8 @@ func (p ColorPair) Bg() Color {
return p.bg return p.bg
} }
func (p ColorPair) key() int {
return (int(p.Fg()) << 8) + int(p.Bg())
}
func (p ColorPair) is24() bool { func (p ColorPair) is24() bool {
return p.Fg().is24() || p.Bg().is24() return p.fg.is24() || p.bg.is24()
} }
type ColorTheme struct { type ColorTheme struct {
@@ -179,10 +177,6 @@ type ColorTheme struct {
Border Color Border Color
} }
func (t *ColorTheme) HasBg() bool {
return t.Bg != colDefault
}
type Event struct { type Event struct {
Type int Type int
Char rune Char rune
@@ -193,6 +187,7 @@ type MouseEvent struct {
Y int Y int
X int X int
S int S int
Left bool
Down bool Down bool
Double bool Double bool
Mod bool Mod bool
@@ -220,7 +215,6 @@ type Renderer interface {
MaxX() int MaxX() int
MaxY() int MaxY() int
DoesAutoWrap() bool DoesAutoWrap() bool
IsOptimized() bool
NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window
} }
@@ -236,6 +230,7 @@ type Window interface {
Close() Close()
X() int X() int
Y() int
Enclose(y int, x int) bool Enclose(y int, x int) bool
Move(y int, x int) Move(y int, x int)
@@ -270,7 +265,6 @@ var (
Dark256 *ColorTheme Dark256 *ColorTheme
Light256 *ColorTheme Light256 *ColorTheme
ColDefault ColorPair
ColNormal ColorPair ColNormal ColorPair
ColPrompt ColorPair ColPrompt ColorPair
ColMatch ColorPair ColMatch ColorPair
@@ -282,7 +276,6 @@ var (
ColSelected ColorPair ColSelected ColorPair
ColHeader ColorPair ColHeader ColorPair
ColBorder ColorPair ColBorder ColorPair
ColUser ColorPair
) )
func EmptyTheme() *ColorTheme { func EmptyTheme() *ColorTheme {
@@ -386,33 +379,36 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
} }
func initPalette(theme *ColorTheme) { func initPalette(theme *ColorTheme) {
ColDefault = ColorPair{colDefault, colDefault, 0} idx := 0
if theme != nil { pair := func(fg, bg Color) ColorPair {
ColNormal = ColorPair{theme.Fg, theme.Bg, 1} idx++
ColPrompt = ColorPair{theme.Prompt, theme.Bg, 2} return ColorPair{fg, bg, idx}
ColMatch = ColorPair{theme.Match, theme.Bg, 3} }
ColCurrent = ColorPair{theme.Current, theme.DarkBg, 4} if theme != nil {
ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg, 5} ColNormal = pair(theme.Fg, theme.Bg)
ColSpinner = ColorPair{theme.Spinner, theme.Bg, 6} ColPrompt = pair(theme.Prompt, theme.Bg)
ColInfo = ColorPair{theme.Info, theme.Bg, 7} ColMatch = pair(theme.Match, theme.Bg)
ColCursor = ColorPair{theme.Cursor, theme.DarkBg, 8} ColCurrent = pair(theme.Current, theme.DarkBg)
ColSelected = ColorPair{theme.Selected, theme.DarkBg, 9} ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
ColHeader = ColorPair{theme.Header, theme.Bg, 10} ColSpinner = pair(theme.Spinner, theme.Bg)
ColBorder = ColorPair{theme.Border, theme.Bg, 11} ColInfo = pair(theme.Info, theme.Bg)
} else { ColCursor = pair(theme.Cursor, theme.DarkBg)
ColNormal = ColorPair{colDefault, colDefault, 1} ColSelected = pair(theme.Selected, theme.DarkBg)
ColPrompt = ColorPair{colDefault, colDefault, 2} ColHeader = pair(theme.Header, theme.Bg)
ColMatch = ColorPair{colDefault, colDefault, 3} ColBorder = pair(theme.Border, theme.Bg)
ColCurrent = ColorPair{colDefault, colDefault, 4} } else {
ColCurrentMatch = ColorPair{colDefault, colDefault, 5} ColNormal = pair(colDefault, colDefault)
ColSpinner = ColorPair{colDefault, colDefault, 6} ColPrompt = pair(colDefault, colDefault)
ColInfo = ColorPair{colDefault, colDefault, 7} ColMatch = pair(colDefault, colDefault)
ColCursor = ColorPair{colDefault, colDefault, 8} ColCurrent = pair(colDefault, colDefault)
ColSelected = ColorPair{colDefault, colDefault, 9} ColCurrentMatch = pair(colDefault, colDefault)
ColHeader = ColorPair{colDefault, colDefault, 10} ColSpinner = pair(colDefault, colDefault)
ColBorder = ColorPair{colDefault, colDefault, 11} ColInfo = pair(colDefault, colDefault)
ColCursor = pair(colDefault, colDefault)
ColSelected = pair(colDefault, colDefault)
ColHeader = pair(colDefault, colDefault)
ColBorder = pair(colDefault, colDefault)
} }
ColUser = ColorPair{colDefault, colDefault, 12}
} }
func attrFor(color ColorPair, attr Attr) Attr { func attrFor(color ColorPair, attr Attr) Attr {
+93 -83
View File
@@ -3,63 +3,103 @@ package util
import ( import (
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
"unsafe"
)
const (
overflow64 uint64 = 0x8080808080808080
overflow32 uint32 = 0x80808080
) )
type Chars struct { type Chars struct {
runes []rune slice []byte // or []rune
bytes []byte 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 // ToChars converts byte array into rune array
func ToChars(bytea []byte) Chars { func ToChars(bytes []byte) Chars {
var runes []rune inBytes, bytesUntil := checkAscii(bytes)
ascii := true if inBytes {
numBytes := len(bytea) return Chars{slice: bytes, inBytes: inBytes}
for i := 0; i < numBytes; {
if bytea[i] < utf8.RuneSelf {
if !ascii {
runes = append(runes, rune(bytea[i]))
}
i++
} else {
if ascii {
ascii = false
runes = make([]rune, i, numBytes)
for j := 0; j < i; j++ {
runes[j] = rune(bytea[j])
}
}
r, sz := utf8.DecodeRune(bytea[i:])
i += sz
runes = append(runes, r)
}
} }
if ascii {
return Chars{bytes: bytea} runes := make([]rune, bytesUntil, len(bytes))
for i := 0; i < bytesUntil; i++ {
runes[i] = rune(bytes[i])
} }
return Chars{runes: runes} 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 { func RunesToChars(runes []rune) Chars {
return Chars{runes: runes} 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 { func (chars *Chars) Get(i int) rune {
if chars.runes != nil { if runes := chars.optionalRunes(); runes != nil {
return chars.runes[i] return runes[i]
} }
return rune(chars.bytes[i]) return rune(chars.slice[i])
} }
func (chars *Chars) Length() int { func (chars *Chars) Length() int {
if chars.runes != nil { if runes := chars.optionalRunes(); runes != nil {
return len(chars.runes) return len(runes)
} }
return len(chars.bytes) return len(chars.slice)
} }
// TrimLength returns the length after trimming leading and trailing whitespaces // TrimLength returns the length after trimming leading and trailing whitespaces
func (chars *Chars) TrimLength() int { func (chars *Chars) TrimLength() uint16 {
if chars.trimLengthKnown {
return chars.trimLength
}
chars.trimLengthKnown = true
var i int var i int
len := chars.Length() len := chars.Length()
for i = len - 1; i >= 0; i-- { for i = len - 1; i >= 0; i-- {
@@ -80,7 +120,8 @@ func (chars *Chars) TrimLength() int {
break break
} }
} }
return i - j + 1 chars.trimLength = AsUint16(i - j + 1)
return chars.trimLength
} }
func (chars *Chars) TrailingWhitespaces() int { func (chars *Chars) TrailingWhitespaces() int {
@@ -96,62 +137,31 @@ func (chars *Chars) TrailingWhitespaces() int {
} }
func (chars *Chars) ToString() string { func (chars *Chars) ToString() string {
if chars.runes != nil { if runes := chars.optionalRunes(); runes != nil {
return string(chars.runes) return string(runes)
} }
return string(chars.bytes) return string(chars.slice)
} }
func (chars *Chars) ToRunes() []rune { func (chars *Chars) ToRunes() []rune {
if chars.runes != nil { if runes := chars.optionalRunes(); runes != nil {
return chars.runes return runes
} }
runes := make([]rune, len(chars.bytes)) bytes := chars.slice
for idx, b := range chars.bytes { runes := make([]rune, len(bytes))
for idx, b := range bytes {
runes[idx] = rune(b) runes[idx] = rune(b)
} }
return runes return runes
} }
func (chars *Chars) Slice(b int, e int) Chars { func (chars *Chars) CopyRunes(dest []rune) {
if chars.runes != nil { if runes := chars.optionalRunes(); runes != nil {
return Chars{runes: chars.runes[b:e]} copy(dest, runes)
return
} }
return Chars{bytes: chars.bytes[b:e]} for idx, b := range chars.slice[:len(dest)] {
} dest[idx] = rune(b)
}
func (chars *Chars) Split(delimiter string) []Chars { return
delim := []rune(delimiter)
numChars := chars.Length()
numDelim := len(delim)
begin := 0
ret := make([]Chars, 0, 1)
for index := 0; index < numChars; {
if index+numDelim <= numChars {
match := true
for off, d := range delim {
if chars.Get(index+off) != d {
match = false
break
}
}
// Found the delimiter
if match {
incr := Max(numDelim, 1)
ret = append(ret, chars.Slice(begin, index+incr))
index += incr
begin = index
continue
}
} else {
// Impossible to find the delimiter in the remaining substring
break
}
index++
}
if begin < numChars || len(ret) == 0 {
ret = append(ret, chars.Slice(begin, numChars))
}
return ret
} }
+3 -39
View File
@@ -2,27 +2,16 @@ package util
import "testing" import "testing"
func TestToCharsNil(t *testing.T) {
bs := Chars{bytes: []byte{}}
if bs.bytes == nil || bs.runes != nil {
t.Error()
}
rs := RunesToChars([]rune{})
if rs.bytes != nil || rs.runes == nil {
t.Error()
}
}
func TestToCharsAscii(t *testing.T) { func TestToCharsAscii(t *testing.T) {
chars := ToChars([]byte("foobar")) chars := ToChars([]byte("foobar"))
if chars.ToString() != "foobar" || chars.runes != nil { if !chars.inBytes || chars.ToString() != "foobar" || !chars.inBytes {
t.Error() t.Error()
} }
} }
func TestCharsLength(t *testing.T) { func TestCharsLength(t *testing.T) {
chars := ToChars([]byte("\tabc한글 ")) chars := ToChars([]byte("\tabc한글 "))
if chars.Length() != 8 || chars.TrimLength() != 5 { if chars.inBytes || chars.Length() != 8 || chars.TrimLength() != 5 {
t.Error() t.Error()
} }
} }
@@ -36,7 +25,7 @@ func TestCharsToString(t *testing.T) {
} }
func TestTrimLength(t *testing.T) { func TestTrimLength(t *testing.T) {
check := func(str string, exp int) { check := func(str string, exp uint16) {
chars := ToChars([]byte(str)) chars := ToChars([]byte(str))
trimmed := chars.TrimLength() trimmed := chars.TrimLength()
if trimmed != exp { if trimmed != exp {
@@ -55,28 +44,3 @@ func TestTrimLength(t *testing.T) {
check(" h o ", 5) check(" h o ", 5)
check(" ", 0) check(" ", 0)
} }
func TestSplit(t *testing.T) {
check := func(str string, delim string, tokens ...string) {
input := ToChars([]byte(str))
result := input.Split(delim)
if len(result) != len(tokens) {
t.Errorf("Invalid Split result for '%s': %d tokens found (expected %d): %s",
str, len(result), len(tokens), result)
}
for idx, token := range tokens {
if result[idx].ToString() != token {
t.Errorf("Invalid Split result for '%s': %s (expected %s)",
str, result[idx].ToString(), token)
}
}
}
check("abc:def::", ":", "abc:", "def:", ":")
check("abc:def::", "-", "abc:def::")
check("abc", "", "a", "b", "c")
check("abc", "a", "a", "bc")
check("abc", "ab", "ab", "c")
check("abc", "abc", "abc")
check("abc", "abcd", "abc")
check("", "abcd", "")
}
+5 -5
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
+5
View File
@@ -14,6 +14,11 @@ func ExecCommand(command string) *exec.Cmd {
if len(shell) == 0 { if len(shell) == 0 {
shell = "sh" 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) return exec.Command(shell, "-c", command)
} }
+15 -8
View File
@@ -3,20 +3,27 @@
package util package util
import ( import (
"fmt"
"os" "os"
"os/exec" "os/exec"
"syscall" "syscall"
"github.com/mattn/go-shellwords"
) )
// ExecCommand executes the given command with $SHELL // ExecCommand executes the given command with cmd
func ExecCommand(command string) *exec.Cmd { func ExecCommand(command string) *exec.Cmd {
args, _ := shellwords.Parse(command) return ExecCommandWith("cmd", command)
allArgs := make([]string, len(args)+1) }
allArgs[0] = "/c"
copy(allArgs[1:], args) // ExecCommandWith executes the given command with cmd. _shell parameter is
return exec.Command("cmd", allArgs...) // 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 // IsWindows returns true on Windows
+9 -7
View File
@@ -6,11 +6,11 @@ Execute (Setup):
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
@@ -19,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)
@@ -140,7 +140,7 @@ Execute (fzf#wrap):
let g:fzf_history_dir = '/tmp' let g:fzf_history_dir = '/tmp'
let opts = fzf#wrap('foobar', {'options': '--color light'}) let opts = fzf#wrap('foobar', {'options': '--color light'})
Log opts Log opts
Assert opts.options =~ '--history /tmp/foobar' Assert opts.options =~ "--history '/tmp/foobar'"
Assert opts.options =~ '--color light' Assert opts.options =~ '--color light'
let g:fzf_colors = { 'fg': ['fg', 'Error'] } let g:fzf_colors = { 'fg': ['fg', 'Error'] }
@@ -149,21 +149,23 @@ Execute (fzf#wrap):
Execute (fzf#shellescape with sh): Execute (fzf#shellescape with sh):
AssertEqual '''''', fzf#shellescape('', 'sh') AssertEqual '''''', fzf#shellescape('', 'sh')
AssertEqual '''\''', fzf#shellescape('\', 'sh')
AssertEqual '''""''', fzf#shellescape('""', 'sh') AssertEqual '''""''', fzf#shellescape('""', 'sh')
AssertEqual '''foobar>''', fzf#shellescape('foobar>', 'sh') AssertEqual '''foobar>''', fzf#shellescape('foobar>', 'sh')
AssertEqual '''\"''', fzf#shellescape('\"', 'sh') AssertEqual '''\\\"\\\''', fzf#shellescape('\\\"\\\', 'sh')
AssertEqual '''echo ''\''''a''\'''' && echo ''\''''b''\''''''', fzf#shellescape('echo ''a'' && echo ''b''', 'sh') AssertEqual '''echo ''\''''a''\'''' && echo ''\''''b''\''''''', fzf#shellescape('echo ''a'' && echo ''b''', 'sh')
Execute (fzf#shellescape with cmd.exe): Execute (fzf#shellescape with cmd.exe):
AssertEqual '^"^"', fzf#shellescape('', 'cmd.exe') AssertEqual '^"^"', fzf#shellescape('', '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 '^"foobar^>^"', fzf#shellescape('foobar>', 'cmd.exe')
AssertEqual '^"\\\^"\\^"', fzf#shellescape('\\\\\\\\"\', 'cmd.exe') AssertEqual '^"\\\\\\\^"\\\\\\^"', fzf#shellescape('\\\"\\\', 'cmd.exe')
AssertEqual '^"echo ''a'' ^&^& echo ''b''^"', fzf#shellescape('echo ''a'' && echo ''b''', '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 '^"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') AssertEqual '^"%%USERPROFILE%%^"', fzf#shellescape('%USERPROFILE%', 'cmd.exe')
Execute (Cleanup): Execute (Cleanup):
unlet g:dir unlet g:dir
+406 -209
View File
File diff suppressed because it is too large Load Diff