Compare commits

...

64 Commits

Author SHA1 Message Date
Junegunn Choi
305ec3b3ce [fish] Remove buffering delay by not using subroutines
Close #169
2015-04-22 14:33:03 +09:00
Junegunn Choi
f4fe93338b Update README 2015-04-22 02:09:16 +09:00
Junegunn Choi
3b84c80d56 Update README 2015-04-22 02:07:27 +09:00
Junegunn Choi
5e120e7ab5 Update man page 2015-04-22 01:44:56 +09:00
Junegunn Choi
a4cf5510e3 0.9.11 2015-04-22 01:42:38 +09:00
Junegunn Choi
edb5ab5622 Update test cases for #203 2015-04-22 00:57:25 +09:00
Junegunn Choi
06b4f75680 Fix broken FZF_TMUX switch and update test cases (#203) 2015-04-22 00:55:39 +09:00
Junegunn Choi
318edc8c35 Apply fzf-tmux to key bindings (#203)
Note that CTRL-T on bash is still using the old trick of send-keys.
2015-04-22 00:32:18 +09:00
Junegunn Choi
651a8f8cc2 Add --inline-info option
Close #202
2015-04-21 23:50:53 +09:00
Junegunn Choi
9f64a00549 Fix double-click result when scroll offset is positive 2015-04-21 23:23:39 +09:00
Junegunn Choi
a88bf87e2a Update test case 2015-04-21 22:36:40 +09:00
Junegunn Choi
e82eb27787 Smart-case for each term in extended-search mode
Close #208
2015-04-21 22:18:05 +09:00
Junegunn Choi
3f0e6a5806 Fix #209 - Invalid mutation of input on case conversion 2015-04-21 22:10:14 +09:00
Junegunn Choi
917b1759b0 [fzf-tmux/vim] Fixes for fish (#204) 2015-04-20 22:42:12 +09:00
Junegunn Choi
16ca9c688b Revert "[fzf-tmux] Fix #204 - Escape command substitution"
This reverts commit 7b6a27cb5e.
2015-04-20 16:23:15 +09:00
Junegunn Choi
7b6a27cb5e [fzf-tmux] Fix #204 - Escape command substitution 2015-04-20 15:22:59 +09:00
Junegunn Choi
869a234938 [fzf-tmux] Use bash instead of sh (#204)
The default shell can be a non-standard shell (e.g. fish)
2015-04-20 14:58:27 +09:00
Junegunn Choi
537d07c1e5 [vim] Use "system" fzf when available
1. Go binary: ../bin/fzf
2. System fzf: $(which fzf)
3. Download fzf from GitHub or create wrapper script to Ruby version (../fzf)
   when the binary for the platform is not available
4. If install script is not found or for some reason failed, try to use Ruby
   version in its expected location (../fzf)
5. If fzf is found to be a shell function, use it (type fzf)
2015-04-19 17:13:07 +09:00
Junegunn Choi
d091a2c4bb [fzf-tmux] Minor adjustment 2015-04-18 16:27:40 +09:00
Junegunn Choi
d2f95d69fb [fzf-tmux] Fix #200 - Double-quote handling
Related #199
2015-04-18 16:24:57 +09:00
Junegunn Choi
1169cc8653 0.9.10 2015-04-18 10:43:40 +09:00
Junegunn Choi
f66d94c6b0 Add --color=[dark|light|16|bw] option
- dark:  the current default for 256-color terminal
- light: color scheme for 256-color terminal with light background
- 16:    the default color scheme for 16-color terminal (`+2`)
- bw:    no colors (`+c`)
2015-04-18 02:55:17 +09:00
Junegunn Choi
2fe1e28220 Improvements in performance and memory usage
I profiled fzf and it turned out that it was spending significant amount
of time repeatedly converting character arrays into Unicode codepoints.
This commit greatly improves search performance after the initial scan
by memoizing the converted results.

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

    :FZF
    <esc>
    :FZF

The main problem being that `:file [FZF]` can be used only once.
2015-04-10 22:18:46 +02:00
Junegunn Choi
622e69ff54 [vim] Neovim compatibility (#137)
Use terminal emulator of Neovim to open fzf
2015-04-10 23:23:47 +09:00
Junegunn Choi
68503d32df [vim] Code cleanup 2015-04-04 11:55:57 +09:00
Junegunn Choi
57319f8c58 [vim] Fix #177 - :FZF with relative paths 2015-04-04 09:18:04 +09:00
Junegunn Choi
dd4d465305 Update Homebrew instruction
Close #175
2015-04-03 01:22:16 +09:00
35 changed files with 954 additions and 553 deletions

View File

@@ -12,17 +12,13 @@ install:
- sudo apt-get install -y zsh fish
script: |
export GOROOT=~/go1.4
export GOPATH=~/go
export FZF_BASE=~/go/src/github.com/junegunn/fzf
export FZF_BASE=$GOPATH/src/github.com/junegunn/fzf
mkdir -p ~/go/src/github.com/junegunn
mkdir -p $GOPATH/src/github.com/junegunn
ln -s $(pwd) $FZF_BASE
curl https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | tar -xz
mv go $GOROOT
cd $FZF_BASE/src && make test fzf/fzf-linux_amd64 install &&
cd $FZF_BASE/bin && ln -sf fzf-linux_amd64 fzf-$(./fzf --version)-linux_amd64 &&
cd $FZF_BASE && yes | ./install &&
cd $FZF_BASE && yes | ./install && rm -f fzf &&
tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ]

View File

@@ -1,6 +1,50 @@
CHANGELOG
=========
0.9.11
------
### New features
- Added `--inline-info` option for saving screen estate (#202)
- Useful inside Neovim
- e.g. `let $FZF_DEFAULT_OPTS = $FZF_DEFAULT_OPTS.' --inline-info'`
### Bug fixes
- Invalid mutation of input on case conversion (#209)
- Smart-case for each term in extended-search mode (#208)
- Fixed double-click result when scroll offset is positive
0.9.10
------
### Improvements
- Performance optimization
- Less aggressive memoization to limit memory usage
### New features
- Added color scheme for light background: `--color=light`
0.9.9
-----
### New features
- Added `--tiebreak` option (#191)
- Added `--no-hscroll` option (#193)
- Visual indication of `--toggle-sort` (#194)
0.9.8
-----
### Bug fixes
- Fixed Unicode case handling (#186)
- Fixed to terminate on RuneError (#185)
0.9.7
-----

146
README.md
View File

@@ -1,4 +1,4 @@
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf.png" height="170" alt="fzf - a command-line fuzzy finder"> [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf)
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf.png" height="170" alt="fzf - a command-line fuzzy finder"> [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) <a href="http://flattr.com/thing/3115381/junegunnfzf-on-GitHub" target="_blank"><img src="http://api.flattr.com/button/flattr-badge-large.png" alt="Flattr this" title="Flattr this" border="0" /></a>
===
fzf is a general-purpose command-line fuzzy finder.
@@ -16,7 +16,7 @@ Pros
- The most comprehensive feature set
- Try `fzf --help` and be surprised
- Batteries included
- Vim plugin, key bindings and fuzzy auto-completion
- Vim/Neovim plugin, key bindings and fuzzy auto-completion
Installation
------------
@@ -28,6 +28,7 @@ fzf project consists of the followings:
- Shell extensions
- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
- Fuzzy auto-completion (bash only)
- Vim/Neovim plugin
You can [download fzf executable][bin] alone, but it's recommended that you
install the extra stuff using the attached install script.
@@ -44,26 +45,15 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install
```
#### Using curl
In case you don't have git installed:
```sh
mkdir -p ~/.fzf
curl -L https://github.com/junegunn/fzf/archive/master.tar.gz |
tar xz --strip-components 1 -C ~/.fzf
~/.fzf/install
```
#### Using Homebrew
On OS X, you can use [Homebrew](http://brew.sh/) to install fzf.
```sh
brew install fzf
brew reinstall --HEAD fzf
# Install shell extensions - this should be done whenever fzf is updated
$(brew info fzf | grep /install)
# Install shell extensions
/usr/local/Cellar/fzf/HEAD/install
```
#### Install as Vim plugin
@@ -88,7 +78,7 @@ while. Please follow the instruction below depending on the installation
method.
- git: `cd ~/.fzf && git pull && ./install`
- brew: `brew update && brew upgrade fzf && $(brew info fzf | grep /install)`
- brew: `brew reinstall --HEAD fzf`
- vim-plug: `:PlugUpdate fzf`
Usage
@@ -152,21 +142,18 @@ fish.
- `CTRL-T` - Paste the selected file path(s) into the command line
- `CTRL-R` - Paste the selected command from history into the command line
- Sort is disabled by default. Press `CTRL-R` again to toggle sort.
- Sort is disabled by default to respect chronological ordering
- Press `CTRL-R` again to toggle sort
- `ALT-C` - cd into the selected directory
If you're on a tmux session, `CTRL-T` will launch fzf in a new split-window. You
may disable this tmux integration by setting `FZF_TMUX` to 0, or change the
height of the window with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`).
If you're on a tmux session, fzf will start in a split pane. You may disable
this tmux integration by setting `FZF_TMUX` to 0, or change the height of the
pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`).
If you use vi mode on bash, you need to add `set -o vi` *before* `source
~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi
mode.
If you want to customize the key bindings, consider editing the
installer-generated source code: `~/.fzf.bash`, `~/.fzf.zsh`, and
`~/.config/fish/functions/fzf_key_bindings.fish`.
`fzf-tmux` script
-----------------
@@ -274,35 +261,20 @@ If you have set up fzf for Vim, `:FZF` command will be added.
" With options
:FZF --no-sort -m /tmp
" Bang version starts in fullscreen instead of using tmux pane or Neovim split
:FZF!
```
Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key,
`CTRL-T`, `CTRL-X` or `CTRL-V` to open selected files in the current window,
in new tabs, in horizontal splits, or in vertical splits respectively.
Note that the environment variables `FZF_DEFAULT_COMMAND` and `FZF_DEFAULT_OPTS`
also apply here.
Note that the environment variables `FZF_DEFAULT_COMMAND` and
`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for
customization.
If you're on a tmux session, `:FZF` will launch fzf in a new split-window whose
height can be adjusted with `g:fzf_tmux_height` (default: '40%'). However, the
bang version (`:FZF!`) will always start in fullscreen.
In GVim, you need an external terminal emulator to start fzf with. `xterm`
command is used by default, but you can customize it with `g:fzf_launcher`.
```vim
" This is the default. %s is replaced with fzf command
let g:fzf_launcher = 'xterm -e bash -ic %s'
" Use urxvt instead
let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s'
```
If you're running MacVim on OSX, I recommend you to use iTerm2 as the launcher.
Refer to the [this wiki
page](https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2) to see
how to set up.
[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim)
#### `fzf#run([options])`
@@ -317,11 +289,16 @@ of the selected items.
| `source` | list | Vim list as input to fzf |
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
| `sink` | funcref | Reference to function to process each selected item |
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
| `options` | string | Options to fzf |
| `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) |
| `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `launcher` | string | External terminal emulator to start fzf with (Only used in GVim) |
_However on Neovim `fzf#run` is asynchronous and does not return values so you
should use `sink` or `sink*` to process the output from fzf._
##### Examples
If `sink` option is not given, `fzf#run` will simply return the list.
@@ -358,32 +335,28 @@ handy mapping that selects an open buffer.
```vim
" List of buffers
function! BufList()
function! s:buflist()
redir => ls
silent ls
redir END
return split(ls, '\n')
endfunction
function! BufOpen(e)
execute 'buffer '. matchstr(a:e, '^[ 0-9]*')
function! s:bufopen(e)
execute 'buffer' matchstr(a:e, '^[ 0-9]*')
endfunction
nnoremap <silent> <Leader><Enter> :call fzf#run({
\ 'source': reverse(BufList()),
\ 'sink': function('BufOpen'),
\ 'source': reverse(<sid>buflist()),
\ 'sink': function('<sid>bufopen'),
\ 'options': '+m',
\ 'down': '40%'
\ 'down': len(<sid>buflist()) + 2
\ })<CR>
```
More examples can be found on [the wiki
page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
#### Articles
- [fzf+vim+tmux](http://junegunn.kr/2014/04/fzf+vim+tmux)
Tips
----
@@ -392,13 +365,13 @@ Tips
If you have any rendering issues, check the followings:
1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it
contains `256` (e.g. `xterm-256color`)
contains `256` (e.g. `xterm-256color`)
2. If you're on screen or tmux, `$TERM` should be either `screen` or
`screen-256color`
`screen-256color`
3. Some terminal emulators (e.g. mintty) have problem displaying default
background color and make some text unable to read. In that case, try `--black`
option. And if it solves your problem, I recommend including it in
`FZF_DEFAULT_OPTS` for further convenience.
background color and make some text unable to read. In that case, try
`--black` option. And if it solves your problem, I recommend including it
in `FZF_DEFAULT_OPTS` for further convenience.
4. If you still have problem, try `--no-256` option or even `--no-color`.
#### Respecting `.gitignore`, `.hgignore`, and `svn:ignore`
@@ -429,41 +402,6 @@ export FZF_DEFAULT_COMMAND='
find * -name ".*" -prune -o -type f -print -o -type l -print) 2> /dev/null'
```
#### Using fzf with tmux panes
The supplied [fzf-tmux](bin/fzf-tmux) script should suffice in most of the
cases, but if you want to be able to update command line like the default
`CTRL-T` key binding, you'll have to use `send-keys` command of tmux. The
following example will show you how it can be done.
```sh
# This is a helper function that splits the current pane to start the given
# command ($1) and sends its output back to the original pane with any number of
# optional keys (shift; $*).
fzf_tmux_helper() {
[ -n "$TMUX_PANE" ] || return
local cmd=$1
shift
tmux split-window -p 40 \
"bash -c \"\$(tmux send-keys -t $TMUX_PANE \"\$(source ~/.fzf.bash; $cmd)\" $*)\""
}
# This is the function we are going to run in the split pane.
# - "find" to list the directories
# - "sed" will escape spaces in the paths.
# - "paste" will join the selected paths into a single line
fzf_tmux_dir() {
fzf_tmux_helper \
'find * -path "*/\.*" -prune -o -type d -print 2> /dev/null |
fzf --multi |
sed "s/ /\\\\ /g" |
paste -sd" " -' Space
}
# Bind CTRL-X-CTRL-D to fzf_tmux_dir
bind '"\C-x\C-d": "$(fzf_tmux_dir)\e\C-e"'
```
#### Fish shell
It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362)
@@ -472,19 +410,7 @@ simple `vim (fzf)` won't work as expected. The workaround is to store the result
of fzf to a temporary file.
```sh
function vimf
if fzf > $TMPDIR/fzf.result
vim (cat $TMPDIR/fzf.result)
end
end
function fe
set tmp $TMPDIR/fzf.result
fzf --query="$argv[1]" --select-1 --exit-0 > $tmp
if [ (cat $tmp | wc -l) -gt 0 ]
vim (cat $tmp)
end
end
fzf > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result)
```
#### Handling UTF-8 NFD paths on OSX

View File

@@ -89,16 +89,14 @@ fi
set -e
# Build arguments to fzf
[ ${#args[@]} -gt 0 ] && fzf_args=$(printf '\\"%s\\" ' "${args[@]}"; echo '')
# Clean up named pipes on exit
id=$RANDOM
argsf=/tmp/fzf-args-$id
fifo1=/tmp/fzf-fifo1-$id
fifo2=/tmp/fzf-fifo2-$id
fifo3=/tmp/fzf-fifo3-$id
cleanup() {
rm -f $fifo1 $fifo2 $fifo3
rm -f $argsf $fifo1 $fifo2 $fifo3
}
trap cleanup EXIT SIGINT SIGTERM
@@ -106,20 +104,31 @@ fail() {
>&2 echo "$1"
exit 1
}
fzf=$(which fzf 2> /dev/null) || fail "fzf executable not found"
fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
[ -x "$fzf" ] || fail "fzf executable not found"
envs=""
[ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
[ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
mkfifo $fifo2
mkfifo $fifo3
# Build arguments to fzf
opts=""
for arg in "${args[@]}"; do
opts="$opts \"${arg//\"/\\\"}\""
done
if [ -n "$term" -o -t 0 ]; then
cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf
tmux set-window-option -q synchronize-panes off \;\
split-window $opt "cd $(printf %q "$PWD");$envs"' sh -c "'$fzf' '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap
split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap
else
mkfifo $fifo1
cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf
tmux set-window-option -q synchronize-panes off \;\
split-window $opt "$envs"' sh -c "'$fzf' '"$fzf_args"' < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap
split-window $opt "$envs bash $argsf" $swap
cat <&0 > $fifo1 &
fi
cat $fifo2

6
fzf
View File

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

13
install
View File

@@ -1,12 +1,19 @@
#!/usr/bin/env bash
version=0.9.7
version=0.9.11
cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd)
# If stdin is a tty, we are "interactive".
[ -t 0 ] && interactive=yes
ask() {
read -p "$1 ([y]/n) " -n 1 -r
# non-interactive shell: wait for a linefeed
# interactive shell: continue after a single keypress
[ -n "$interactive" ] && read_n='-n 1' || read_n=
read -p "$1 ([y]/n) " $read_n -r
echo
[[ ! $REPLY =~ ^[Nn]$ ]]
}
@@ -154,6 +161,8 @@ if [ -n "$binary_error" ]; then
echo "OK"
fi
[[ "$*" =~ "--bin" ]] && exit 0
# Auto-completion
ask "Do you want to add auto-completion support?"
auto_completion=$?

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
THE SOFTWARE.
..
.TH fzf 1 "March 2015" "fzf 0.9.6" "fzf - a command-line fuzzy finder"
.TH fzf 1 "April 2015" "fzf 0.9.11" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -66,6 +66,20 @@ Reverse the order of the input
.RS
e.g. \fBhistory | fzf --tac --no-sort\fR
.RE
.TP
.BI "--tiebreak=" "CRI"
Sort criterion to use when the scores are tied
.br
.R ""
.br
.BR length " Prefers item with shorter length"
.br
.BR begin " Prefers item with matched substring closer to the beginning"
.br
.BR end " Prefers item with matched substring closer to the end"
.br
.BR index " Prefers item that appeared earlier in the input stream"
.br
.SS Interface
.TP
.B "-m, --multi"
@@ -77,11 +91,21 @@ Enable processing of ANSI color codes
.B "--no-mouse"
Disable mouse
.TP
.B "+c, --no-color"
Disable colors
.TP
.B "+2, --no-256"
Disable 256-color
.B "--color=COL"
Color scheme: [dark|light|16|bw]
.br
(default: dark on 256-color terminal, otherwise 16)
.br
.R ""
.br
.BR dark " Color scheme for dark 256-color terminal"
.br
.BR light " Color scheme for light 256-color terminal"
.br
.BR 16 " Color scheme for 16-color terminal"
.br
.BR bw " No colors"
.br
.TP
.B "--black"
Use black background
@@ -89,6 +113,12 @@ Use black background
.B "--reverse"
Reverse orientation
.TP
.B "--no-hscroll"
Disable horizontal scroll
.TP
.B "--inline-info"
Display finder info inline with the query
.TP
.BI "--prompt=" "STR"
Input prompt (default: '> ')
.SS Scripting

View File

@@ -21,12 +21,13 @@
" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
let s:default_tmux_height = '40%'
let s:default_height = '40%'
let s:launcher = 'xterm -e bash -ic %s'
let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf'
let s:install = expand('<sfile>:h:h').'/install'
let s:installed = 0
let s:fzf_rb = expand('<sfile>:h:h').'/fzf'
let s:fzf_tmux = expand('<sfile>:h:h').'/bin/fzf-tmux'
let s:legacy = 0
let s:cpo_save = &cpo
set cpo&vim
@@ -39,9 +40,15 @@ function! s:fzf_exec()
let path = split(system('which fzf 2> /dev/null'), '\n')
if !v:shell_error && !empty(path)
let s:exec = path[0]
elseif !s:installed && executable(s:install)
echohl WarningMsg
echo 'Downloading fzf binary. Please wait ...'
echohl None
let s:installed = 1
call system(s:install.' --bin')
return s:fzf_exec()
elseif executable(s:fzf_rb)
let s:exec = s:fzf_rb
let s:legacy = 1
else
call system('type fzf')
if v:shell_error
@@ -98,6 +105,15 @@ function! s:upgrade(dict)
endfunction
function! fzf#run(...) abort
try
let oshell = &shell
set shell=sh
if has('nvim') && bufexists('[FZF]')
echohl WarningMsg
echomsg 'FZF is already running!'
echohl None
return []
endif
let dict = exists('a:1') ? s:upgrade(a:1) : {}
let temps = { 'result': tempname() }
let optstr = get(dict, 'options', '')
@@ -122,14 +138,23 @@ function! fzf#run(...) abort
else
let prefix = ''
endif
let split = s:tmux_enabled() && s:tmux_splittable(dict)
let command = prefix.(split ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
let tmux = !has('nvim') && s:tmux_enabled() && s:splittable(dict)
let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if split
return s:execute_tmux(dict, command, temps)
else
return s:execute(dict, command, temps)
endif
try
if tmux
return s:execute_tmux(dict, command, temps)
elseif has('nvim')
return s:execute_term(dict, command, temps)
else
return s:execute(dict, command, temps)
endif
finally
call s:popd(dict)
endtry
finally
let &shell = oshell
endtry
endfunction
function! s:present(dict, ...)
@@ -146,20 +171,26 @@ function! s:fzf_tmux(dict)
for o in ['up', 'down', 'left', 'right']
if s:present(a:dict, o)
let size = '-'.o[0].(a:dict[o] == 1 ? '' : a:dict[o])
break
endif
endfor
return printf('LINES=%d COLUMNS=%d %s %s %s --',
\ &lines, &columns, s:fzf_tmux, size, (has_key(a:dict, 'source') ? '' : '-'))
endfunction
function! s:tmux_splittable(dict)
function! s:splittable(dict)
return s:present(a:dict, 'up', 'down', 'left', 'right')
endfunction
function! s:pushd(dict)
if s:present(a:dict, 'dir')
let a:dict.prev_dir = getcwd()
let cwd = getcwd()
if get(a:dict, 'prev_dir', '') ==# cwd
return 1
endif
let a:dict.prev_dir = cwd
execute 'chdir '.s:escape(a:dict.dir)
let a:dict.dir = getcwd()
return 1
endif
return 0
@@ -194,14 +225,6 @@ function! s:execute(dict, command, temps)
endif
endfunction
function! s:env_var(name)
if exists('$'.a:name)
return a:name . "='". substitute(expand('$'.a:name), "'", "'\\\\''", 'g') . "' "
else
return ''
endif
endfunction
function! s:execute_tmux(dict, command, temps)
let command = a:command
if s:pushd(a:dict)
@@ -213,6 +236,67 @@ function! s:execute_tmux(dict, command, temps)
return s:callback(a:dict, a:temps)
endfunction
function! s:calc_size(max, val)
if a:val =~ '%$'
return a:max * str2nr(a:val[:-2]) / 100
else
return min([a:max, a:val])
endif
endfunction
function! s:split(dict)
let directions = {
\ 'up': ['topleft', 'resize', &lines],
\ 'down': ['botright', 'resize', &lines],
\ 'left': ['vertical topleft', 'vertical resize', &columns],
\ 'right': ['vertical botright', 'vertical resize', &columns] }
let s:ptab = tabpagenr()
try
for [dir, triple] in items(directions)
let val = get(a:dict, dir, '')
if !empty(val)
let [cmd, resz, max] = triple
let sz = s:calc_size(max, val)
execute cmd sz.'new'
execute resz sz
return
endif
endfor
if s:present(a:dict, 'window')
execute a:dict.window
else
tabnew
endif
finally
setlocal winfixwidth winfixheight
endtry
endfunction
function! s:execute_term(dict, command, temps)
call s:split(a:dict)
call s:pushd(a:dict)
let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps }
function! fzf.on_exit(id, code)
let tab = tabpagenr()
execute 'bd!' self.buf
if s:ptab == tab
wincmd p
endif
call s:pushd(self.dict)
try
call s:callback(self.dict, self.temps)
finally
call s:popd(self.dict)
endtry
endfunction
call termopen(a:command, fzf)
silent file [FZF]
startinsert
return []
endfunction
function! s:callback(dict, temps)
if !filereadable(a:temps.result)
let lines = []
@@ -227,55 +311,49 @@ function! s:callback(dict, temps)
endif
endfor
endif
if has_key(a:dict, 'sink*')
call a:dict['sink*'](lines)
endif
endif
for tf in values(a:temps)
silent! call delete(tf)
endfor
call s:popd(a:dict)
return lines
endfunction
function! s:cmd(bang, ...) abort
let args = copy(a:000)
if !s:legacy
let args = insert(args, '--expect=ctrl-t,ctrl-x,ctrl-v', 0)
let s:default_action = {
\ 'ctrl-m': 'e',
\ 'ctrl-t': 'tabedit',
\ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' }
function! s:cmd_callback(lines) abort
if empty(a:lines)
return
endif
let key = remove(a:lines, 0)
let cmd = get(s:action, key, 'e')
for item in a:lines
execute cmd s:escape(item)
endfor
endfunction
function! s:cmd(bang, ...) abort
let s:action = get(g:, 'fzf_action', s:default_action)
let args = extend(['--expect='.join(keys(s:action), ',')], a:000)
let opts = {}
if len(args) > 0 && isdirectory(expand(args[-1]))
let opts.dir = remove(args, -1)
endif
if !a:bang
let opts.down = get(g:, 'fzf_tmux_height', s:default_tmux_height)
endif
if s:legacy
call fzf#run(extend({ 'sink': 'e', 'options': join(args) }, opts))
else
let output = fzf#run(extend({ 'options': join(args) }, opts))
if empty(output)
return
endif
let key = remove(output, 0)
if key == 'ctrl-t' | let cmd = 'tabedit'
elseif key == 'ctrl-x' | let cmd = 'split'
elseif key == 'ctrl-v' | let cmd = 'vsplit'
else | let cmd = 'e'
endif
try
call s:pushd(opts)
for item in output
execute cmd s:escape(item)
endfor
finally
call s:popd(opts)
endtry
let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height))
endif
call fzf#run(extend({'options': join(args), 'sink*': function('<sid>cmd_callback')}, opts))
endfunction
command! -nargs=* -complete=dir -bang FZF call s:cmd('<bang>' == '!', <f-args>)
command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>)
let &cpo = s:cpo_save
unlet s:cpo_save

View File

@@ -5,6 +5,8 @@
# / __/ / /_/ __/
# /_/ /___/_/-completion.bash
#
# - $FZF_TMUX (default: 1)
# - $FZF_TMUX_HEIGHT (default: '40%')
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
@@ -14,9 +16,10 @@ _fzf_orig_completion_filter() {
}
_fzf_opts_completion() {
local cur opts
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="
-x --extended
-e --extended-exact
@@ -25,20 +28,31 @@ _fzf_opts_completion() {
-d --delimiter
+s --no-sort
--tac
--tiebreak
-m --multi
--no-mouse
+c --no-color
+2 --no-256
--black
--reverse
--no-hscroll
--prompt
-q --query
-1 --select-1
-0 --exit-0
-f --filter
--print-query
--expect
--toggle-sort
--sync"
case "${prev}" in
--tiebreak)
COMPREPLY=( $(compgen -W "length begin end index" -- ${cur}) )
return 0
;;
esac
if [[ ${cur} =~ ^-|\+ ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
@@ -65,7 +79,8 @@ _fzf_handle_dynamic_completion() {
}
_fzf_path_completion() {
local cur base dir leftover matches trigger cmd
local cur base dir leftover matches trigger cmd fzf
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g')
COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER:-**}
@@ -81,7 +96,7 @@ _fzf_path_completion() {
leftover=${leftover/#\/}
[ "$dir" = './' ] && dir=''
tput sc
matches=$(find -L "$dir"* $1 2> /dev/null | fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do
matches=$(find -L "$dir"* $1 2> /dev/null | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do
printf "%q$3 " "$item"
done)
matches=${matches% }
@@ -105,7 +120,8 @@ _fzf_path_completion() {
}
_fzf_list_completion() {
local cur selected trigger cmd src
local cur selected trigger cmd src fzf
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
read -r src
cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g')
trigger=${FZF_COMPLETION_TRIGGER:-**}
@@ -114,7 +130,7 @@ _fzf_list_completion() {
cur=${cur:0:${#cur}-${#trigger}}
tput sc
selected=$(eval "$src | fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ')
selected=$(eval "$src | $fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ')
selected=${selected% }
tput rc
@@ -149,9 +165,10 @@ _fzf_dir_completion() {
_fzf_kill_completion() {
[ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1
local selected
local selected fzf
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
tput sc
selected=$(ps -ef | sed 1d | fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ')
selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ')
tput rc
if [ -n "$selected" ]; then

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,10 @@
package algo
import "strings"
import (
"unicode"
"github.com/junegunn/fzf/src/util"
)
/*
* String matching algorithms here do not use strings.ToLower to avoid
@@ -11,13 +15,11 @@ import "strings"
*/
// FuzzyMatch performs fuzzy-match
func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
if len(pattern) == 0 {
return 0, 0
}
runes := []rune(*input)
// 0. (FIXME) How to find the shortest match?
// a_____b__c__abc
// ^^^^^^^^^^ ^^^
@@ -31,11 +33,18 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
sidx := -1
eidx := -1
for index, char := range runes {
for index, char := range *runes {
// This is considerably faster than blindly applying strings.ToLower to the
// whole string
if !caseSensitive && char >= 65 && char <= 90 {
char += 32
if !caseSensitive {
// Partially inlining `unicode.ToLower`. Ugly, but makes a noticeable
// difference in CPU cost. (Measured on Go 1.4.1. Also note that the Go
// compiler as of now does not inline non-leaf functions.)
if char >= 'A' && char <= 'Z' {
char += 32
} else if char > unicode.MaxASCII {
char = unicode.To(unicode.LowerCase, char)
}
}
if char == pattern[pidx] {
if sidx < 0 {
@@ -51,9 +60,13 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
if sidx >= 0 && eidx >= 0 {
pidx--
for index := eidx - 1; index >= sidx; index-- {
char := runes[index]
if !caseSensitive && char >= 65 && char <= 90 {
char += 32
char := (*runes)[index]
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
} else if char > unicode.MaxASCII {
char = unicode.To(unicode.LowerCase, char)
}
}
if char == pattern[pidx] {
if pidx--; pidx < 0 {
@@ -67,27 +80,6 @@ func FuzzyMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
return -1, -1
}
// ExactMatchStrings performs exact-match using strings package.
// Currently not used.
func ExactMatchStrings(caseSensitive bool, input *string, pattern []rune) (int, int) {
if len(pattern) == 0 {
return 0, 0
}
var str string
if caseSensitive {
str = *input
} else {
str = strings.ToLower(*input)
}
if idx := strings.Index(str, string(pattern)); idx >= 0 {
prefixRuneLen := len([]rune((*input)[:idx]))
return prefixRuneLen, prefixRuneLen + len(pattern)
}
return -1, -1
}
// ExactMatchNaive is a basic string searching algorithm that handles case
// sensitivity. Although naive, it still performs better than the combination
// of strings.ToLower + strings.Index for typical fzf use cases where input
@@ -95,13 +87,12 @@ func ExactMatchStrings(caseSensitive bool, input *string, pattern []rune) (int,
//
// We might try to implement better algorithms in the future:
// http://en.wikipedia.org/wiki/String_searching_algorithm
func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, int) {
func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
if len(pattern) == 0 {
return 0, 0
}
runes := []rune(*input)
numRunes := len(runes)
numRunes := len(*runes)
plen := len(pattern)
if numRunes < plen {
return -1, -1
@@ -109,9 +100,13 @@ func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, in
pidx := 0
for index := 0; index < numRunes; index++ {
char := runes[index]
if !caseSensitive && char >= 65 && char <= 90 {
char += 32
char := (*runes)[index]
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
} else if char > unicode.MaxASCII {
char = unicode.To(unicode.LowerCase, char)
}
}
if pattern[pidx] == char {
pidx++
@@ -127,16 +122,15 @@ func ExactMatchNaive(caseSensitive bool, input *string, pattern []rune) (int, in
}
// PrefixMatch performs prefix-match
func PrefixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
runes := []rune(*input)
if len(runes) < len(pattern) {
func PrefixMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
if len(*runes) < len(pattern) {
return -1, -1
}
for index, r := range pattern {
char := runes[index]
if !caseSensitive && char >= 65 && char <= 90 {
char += 32
char := (*runes)[index]
if !caseSensitive {
char = unicode.ToLower(char)
}
if char != r {
return -1, -1
@@ -146,8 +140,8 @@ func PrefixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
}
// SuffixMatch performs suffix-match
func SuffixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
runes := []rune(strings.TrimRight(*input, " "))
func SuffixMatch(caseSensitive bool, input *[]rune, pattern []rune) (int, int) {
runes := util.TrimRight(input)
trimmedLen := len(runes)
diff := trimmedLen - len(pattern)
if diff < 0 {
@@ -156,8 +150,8 @@ func SuffixMatch(caseSensitive bool, input *string, pattern []rune) (int, int) {
for index, r := range pattern {
char := runes[index+diff]
if !caseSensitive && char >= 65 && char <= 90 {
char += 32
if !caseSensitive {
char = unicode.ToLower(char)
}
if char != r {
return -1, -1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,9 +34,6 @@ import (
"github.com/junegunn/fzf/src/util"
)
const coordinatorDelayMax time.Duration = 100 * time.Millisecond
const coordinatorDelayStep time.Duration = 10 * time.Millisecond
func initProcs() {
runtime.GOMAXPROCS(runtime.NumCPU())
}
@@ -55,6 +52,7 @@ func Run(options *Options) {
opts := ParseOptions()
sort := opts.Sort > 0
rankTiebreak = opts.Tiebreak
if opts.Version {
fmt.Println(Version)
@@ -70,7 +68,7 @@ func Run(options *Options) {
return data, nil
}
if opts.Ansi {
if opts.Color {
if opts.Theme != nil {
ansiProcessor = func(data *string) (*string, []ansiOffset) {
return extractColor(data)
}
@@ -98,8 +96,9 @@ func Run(options *Options) {
} else {
chunkList = NewChunkList(func(data *string, index int) *Item {
tokens := Tokenize(data, opts.Delimiter)
trans := Transform(tokens, opts.WithNth)
item := Item{
text: Transform(tokens, opts.WithNth).whole,
text: joinTokens(trans),
origText: data,
index: uint32(index),
colors: nil,
@@ -194,8 +193,9 @@ func Run(options *Options) {
matcher.Reset(snapshot, terminal.Input(), false, !reading, sort)
case EvtSearchNew:
if value.(bool) {
sort = !sort
switch val := value.(type) {
case bool:
sort = val
}
snapshot, _ := chunkList.Snapshot()
matcher.Reset(snapshot, terminal.Input(), true, !reading, sort)

View File

@@ -95,6 +95,18 @@ const (
doubleClickDuration = 500 * time.Millisecond
)
type ColorTheme struct {
darkBg C.short
prompt C.short
match C.short
current C.short
currentMatch C.short
spinner C.short
info C.short
cursor C.short
selected C.short
}
type Event struct {
Type int
Char rune
@@ -116,8 +128,10 @@ var (
_color func(int, bool) C.int
_colorMap map[int]int
_prevDownTime time.Time
_prevDownY int
_clickY []int
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
DarkBG C.short
)
@@ -125,6 +139,36 @@ func init() {
_prevDownTime = time.Unix(0, 0)
_clickY = []int{}
_colorMap = make(map[int]int)
Default16 = &ColorTheme{
darkBg: C.COLOR_BLACK,
prompt: C.COLOR_BLUE,
match: C.COLOR_GREEN,
current: C.COLOR_YELLOW,
currentMatch: C.COLOR_GREEN,
spinner: C.COLOR_GREEN,
info: C.COLOR_WHITE,
cursor: C.COLOR_RED,
selected: C.COLOR_MAGENTA}
Dark256 = &ColorTheme{
darkBg: 236,
prompt: 110,
match: 108,
current: 254,
currentMatch: 151,
spinner: 148,
info: 144,
cursor: 161,
selected: 168}
Light256 = &ColorTheme{
darkBg: 251,
prompt: 25,
match: 66,
current: 237,
currentMatch: 23,
spinner: 65,
info: 101,
cursor: 161,
selected: 168}
}
func attrColored(pair int, bold bool) C.int {
@@ -174,7 +218,7 @@ func getch(nonblock bool) int {
return int(b[0])
}
func Init(color bool, color256 bool, black bool, mouse bool) {
func Init(theme *ColorTheme, black bool, mouse bool) {
{
in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0)
if err != nil {
@@ -204,42 +248,35 @@ func Init(color bool, color256 bool, black bool, mouse bool) {
os.Exit(1)
}()
if color {
if theme != nil {
C.start_color()
var bg C.short
if black {
bg = C.COLOR_BLACK
} else {
C.use_default_colors()
bg = -1
}
if color256 {
DarkBG = 236
C.init_pair(ColPrompt, 110, bg)
C.init_pair(ColMatch, 108, bg)
C.init_pair(ColCurrent, 254, DarkBG)
C.init_pair(ColCurrentMatch, 151, DarkBG)
C.init_pair(ColSpinner, 148, bg)
C.init_pair(ColInfo, 144, bg)
C.init_pair(ColCursor, 161, DarkBG)
C.init_pair(ColSelected, 168, DarkBG)
} else {
DarkBG = C.COLOR_BLACK
C.init_pair(ColPrompt, C.COLOR_BLUE, bg)
C.init_pair(ColMatch, C.COLOR_GREEN, bg)
C.init_pair(ColCurrent, C.COLOR_YELLOW, DarkBG)
C.init_pair(ColCurrentMatch, C.COLOR_GREEN, DarkBG)
C.init_pair(ColSpinner, C.COLOR_GREEN, bg)
C.init_pair(ColInfo, C.COLOR_WHITE, bg)
C.init_pair(ColCursor, C.COLOR_RED, DarkBG)
C.init_pair(ColSelected, C.COLOR_MAGENTA, DarkBG)
}
initPairs(theme, black)
_color = attrColored
} else {
_color = attrMono
}
}
func initPairs(theme *ColorTheme, black bool) {
var bg C.short
if black {
bg = C.COLOR_BLACK
} else {
C.use_default_colors()
bg = -1
}
DarkBG = theme.darkBg
C.init_pair(ColPrompt, theme.prompt, bg)
C.init_pair(ColMatch, theme.match, bg)
C.init_pair(ColCurrent, theme.current, DarkBG)
C.init_pair(ColCurrentMatch, theme.currentMatch, DarkBG)
C.init_pair(ColSpinner, theme.spinner, bg)
C.init_pair(ColInfo, theme.info, bg)
C.init_pair(ColCursor, theme.cursor, DarkBG)
C.init_pair(ColSelected, theme.selected, DarkBG)
}
func Close() {
C.endwin()
C.swapOutput()
@@ -420,6 +457,9 @@ func GetChar() Event {
return Event{int(_buf[0]), 0, nil}
}
r, rsz := utf8.DecodeRune(_buf)
if r == utf8.RuneError {
return Event{ESC, 0, nil}
}
sz = rsz
return Event{Rune, r, nil}
}

View File

@@ -1,6 +1,8 @@
package fzf
import (
"math"
"github.com/junegunn/fzf/src/curses"
)
@@ -17,7 +19,7 @@ type colorOffset struct {
type Item struct {
text *string
origText *string
transformed *Transformed
transformed *[]Token
index uint32
offsets []Offset
colors []ansiOffset
@@ -27,17 +29,21 @@ type Item struct {
// Rank is used to sort the search result
type Rank struct {
matchlen uint16
strlen uint16
tiebreak uint16
index uint32
}
// Tiebreak criterion to use. Never changes once fzf is started.
var rankTiebreak tiebreak
// Rank calculates rank of the Item
func (i *Item) Rank(cache bool) Rank {
if cache && (i.rank.matchlen > 0 || i.rank.strlen > 0) {
if cache && (i.rank.matchlen > 0 || i.rank.tiebreak > 0) {
return i.rank
}
matchlen := 0
prevEnd := 0
minBegin := math.MaxUint16
for _, offset := range i.offsets {
begin := int(offset[0])
end := int(offset[1])
@@ -48,10 +54,30 @@ func (i *Item) Rank(cache bool) Rank {
prevEnd = end
}
if end > begin {
if begin < minBegin {
minBegin = begin
}
matchlen += end - begin
}
}
rank := Rank{uint16(matchlen), uint16(len(*i.text)), i.index}
var tiebreak uint16
switch rankTiebreak {
case byLength:
tiebreak = uint16(len(*i.text))
case byBegin:
// We can't just look at i.offsets[0][0] because it can be an inverse term
tiebreak = uint16(minBegin)
case byEnd:
if prevEnd > 0 {
tiebreak = uint16(1 + len(*i.text) - prevEnd)
} else {
// Empty offsets due to inverse terms.
tiebreak = 1
}
case byIndex:
tiebreak = 1
}
rank := Rank{uint16(matchlen), tiebreak, i.index}
if cache {
i.rank = rank
}
@@ -199,9 +225,9 @@ func compareRanks(irank Rank, jrank Rank, tac bool) bool {
return false
}
if irank.strlen < jrank.strlen {
if irank.tiebreak < jrank.tiebreak {
return true
} else if irank.strlen > jrank.strlen {
} else if irank.tiebreak > jrank.tiebreak {
return false
}

View File

@@ -42,7 +42,7 @@ func TestItemRank(t *testing.T) {
strs := []string{"foo", "foobar", "bar", "baz"}
item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}}
rank1 := item1.Rank(true)
if rank1.matchlen != 0 || rank1.strlen != 3 || rank1.index != 1 {
if rank1.matchlen != 0 || rank1.tiebreak != 3 || rank1.index != 1 {
t.Error(item1.Rank(true))
}
// Only differ in index

View File

@@ -34,10 +34,6 @@ const (
reqReset
)
const (
progressMinDuration = 200 * time.Millisecond
)
// NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox) *Matcher {
@@ -100,7 +96,9 @@ func (m *Matcher) Loop() {
}
if !cancelled {
m.mergerCache[patternString] = merger
if merger.Cacheable() {
m.mergerCache[patternString] = merger
}
merger.final = request.final
m.eventBox.Set(EvtSearchFin, merger)
}

View File

@@ -61,8 +61,8 @@ func (mg *Merger) Get(idx int) *Item {
if mg.tac {
idx = mg.count - idx - 1
}
chunk := (*mg.chunks)[idx/ChunkSize]
return (*chunk)[idx%ChunkSize]
chunk := (*mg.chunks)[idx/chunkSize]
return (*chunk)[idx%chunkSize]
}
if mg.sorted {
@@ -82,6 +82,10 @@ func (mg *Merger) Get(idx int) *Item {
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
}
func (mg *Merger) Cacheable() bool {
return mg.count < mergerCacheMax
}
func (mg *Merger) mergedGet(idx int) *Item {
for i := len(mg.merged); i <= idx; i++ {
minRank := Rank{0, 0, 0}

View File

@@ -28,16 +28,19 @@ const usage = `usage: fzf [options]
Search result
+s, --no-sort Do not sort the result
--tac Reverse the order of the input
(e.g. 'history | fzf --tac --no-sort')
--tiebreak=CRI Sort criterion when the scores are tied;
[length|begin|end|index] (default: length)
Interface
-m, --multi Enable multi-select with tab/shift-tab
--ansi Enable processing of ANSI color codes
--no-mouse Disable mouse
+c, --no-color Disable colors
+2, --no-256 Disable 256-color
--color=COL Color scheme; [dark|light|16|bw]
(default: dark on 256-color terminal, otherwise 16)
--black Use black background
--reverse Reverse orientation
--no-hscroll Disable horizontal scroll
--inline-info Display finder info inline with the query
--prompt=STR Input prompt (default: '> ')
Scripting
@@ -49,7 +52,6 @@ const usage = `usage: fzf [options]
--expect=KEYS Comma-separated list of keys to complete fzf
--toggle-sort=KEY Key to toggle sort
--sync Synchronous search for multi-staged filtering
(e.g. 'fzf --multi | fzf --sync')
Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty
@@ -77,6 +79,16 @@ const (
CaseRespect
)
// Sort criteria
type tiebreak int
const (
byLength tiebreak = iota
byBegin
byEnd
byIndex
)
// Options stores the values of command-line options
type Options struct {
Mode Mode
@@ -86,13 +98,15 @@ type Options struct {
Delimiter *regexp.Regexp
Sort int
Tac bool
Tiebreak tiebreak
Multi bool
Ansi bool
Mouse bool
Color bool
Color256 bool
Theme *curses.ColorTheme
Black bool
Reverse bool
Hscroll bool
InlineInfo bool
Prompt string
Query string
Select1 bool
@@ -106,6 +120,13 @@ type Options struct {
}
func defaultOptions() *Options {
var defaultTheme *curses.ColorTheme
if strings.Contains(os.Getenv("TERM"), "256") {
defaultTheme = curses.Dark256
} else {
defaultTheme = curses.Default16
}
return &Options{
Mode: ModeFuzzy,
Case: CaseSmart,
@@ -114,13 +135,15 @@ func defaultOptions() *Options {
Delimiter: nil,
Sort: 1000,
Tac: false,
Tiebreak: byLength,
Multi: false,
Ansi: false,
Mouse: true,
Color: true,
Color256: strings.Contains(os.Getenv("TERM"), "256"),
Theme: defaultTheme,
Black: false,
Reverse: false,
Hscroll: true,
InlineInfo: false,
Prompt: "> ",
Query: "",
Select1: false,
@@ -235,6 +258,38 @@ func parseKeyChords(str string, message string) []int {
return chords
}
func parseTiebreak(str string) tiebreak {
switch strings.ToLower(str) {
case "length":
return byLength
case "index":
return byIndex
case "begin":
return byBegin
case "end":
return byEnd
default:
errorExit("invalid sort criterion: " + str)
}
return byLength
}
func parseTheme(str string) *curses.ColorTheme {
switch strings.ToLower(str) {
case "dark":
return curses.Dark256
case "light":
return curses.Light256
case "16":
return curses.Default16
case "bw", "no":
return nil
default:
errorExit("invalid color scheme: " + str)
}
return nil
}
func checkToggleSort(str string) int {
keys := parseKeyChords(str, "key name required")
if len(keys) != 1 {
@@ -262,6 +317,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Filter = &filter
case "--expect":
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
case "--tiebreak":
opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--color":
opts.Theme = parseTheme(nextString(allArgs, &i, "color scheme name required"))
case "--toggle-sort":
opts.ToggleSort = checkToggleSort(nextString(allArgs, &i, "key name required"))
case "-d", "--delimiter":
@@ -293,9 +352,9 @@ func parseOptions(opts *Options, allArgs []string) {
case "--no-mouse":
opts.Mouse = false
case "+c", "--no-color":
opts.Color = false
opts.Theme = nil
case "+2", "--no-256":
opts.Color256 = false
opts.Theme = curses.Default16
case "--black":
opts.Black = true
case "--no-black":
@@ -304,6 +363,14 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Reverse = true
case "--no-reverse":
opts.Reverse = false
case "--hscroll":
opts.Hscroll = true
case "--no-hscroll":
opts.Hscroll = false
case "--inline-info":
opts.InlineInfo = true
case "--no-inline-info":
opts.InlineInfo = false
case "-1", "--select-1":
opts.Select1 = true
case "+1", "--no-select-1":
@@ -345,6 +412,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.ToggleSort = checkToggleSort(value)
} else if match, value := optString(arg, "--expect="); match {
opts.Expect = parseKeyChords(value, "key names required")
} else if match, value := optString(arg, "--tiebreak="); match {
opts.Tiebreak = parseTiebreak(value)
} else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(value)
} else {
errorExit("unknown option: " + arg)
}

View File

@@ -8,8 +8,6 @@ import (
"github.com/junegunn/fzf/src/algo"
)
const uppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// fuzzy
// 'exact
// ^exact-prefix
@@ -29,10 +27,11 @@ const (
)
type term struct {
typ termType
inv bool
text []rune
origText []rune
typ termType
inv bool
text []rune
caseSensitive bool
origText []rune
}
// Pattern represents search pattern
@@ -44,7 +43,7 @@ type Pattern struct {
hasInvTerm bool
delimiter *regexp.Regexp
nth []Range
procFun map[termType]func(bool, *string, []rune) (int, int)
procFun map[termType]func(bool, *[]rune, []rune) (int, int)
}
var (
@@ -89,34 +88,32 @@ func BuildPattern(mode Mode, caseMode Case,
caseSensitive, hasInvTerm := true, false
terms := []term{}
switch caseMode {
case CaseSmart:
if !strings.ContainsAny(asString, uppercaseLetters) {
runes, caseSensitive = []rune(strings.ToLower(asString)), false
}
case CaseIgnore:
runes, caseSensitive = []rune(strings.ToLower(asString)), false
}
switch mode {
case ModeExtended, ModeExtendedExact:
terms = parseTerms(mode, string(runes))
terms = parseTerms(mode, caseMode, asString)
for _, term := range terms {
if term.inv {
hasInvTerm = true
}
}
default:
lowerString := strings.ToLower(asString)
caseSensitive = caseMode == CaseRespect ||
caseMode == CaseSmart && lowerString != asString
if !caseSensitive {
asString = lowerString
}
}
ptr := &Pattern{
mode: mode,
caseSensitive: caseSensitive,
text: runes,
text: []rune(asString),
terms: terms,
hasInvTerm: hasInvTerm,
nth: nth,
delimiter: delimiter,
procFun: make(map[termType]func(bool, *string, []rune) (int, int))}
procFun: make(map[termType]func(bool, *[]rune, []rune) (int, int))}
ptr.procFun[termFuzzy] = algo.FuzzyMatch
ptr.procFun[termExact] = algo.ExactMatchNaive
@@ -127,11 +124,17 @@ func BuildPattern(mode Mode, caseMode Case,
return ptr
}
func parseTerms(mode Mode, str string) []term {
func parseTerms(mode Mode, caseMode Case, str string) []term {
tokens := _splitRegex.Split(str, -1)
terms := []term{}
for _, token := range tokens {
typ, inv, text := termFuzzy, false, token
lowerText := strings.ToLower(text)
caseSensitive := caseMode == CaseRespect ||
caseMode == CaseSmart && text != lowerText
if !caseSensitive {
text = lowerText
}
origText := []rune(text)
if mode == ModeExtendedExact {
typ = termExact
@@ -157,10 +160,11 @@ func parseTerms(mode Mode, str string) []term {
if len(text) > 0 {
terms = append(terms, term{
typ: typ,
inv: inv,
text: []rune(text),
origText: origText})
typ: typ,
inv: inv,
text: []rune(text),
caseSensitive: caseSensitive,
origText: origText})
}
}
return terms
@@ -274,7 +278,7 @@ func dupItem(item *Item, offsets []Offset) *Item {
func (p *Pattern) fuzzyMatch(item *Item) (int, int) {
input := p.prepareInput(item)
return p.iter(algo.FuzzyMatch, input, p.text)
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.text)
}
func (p *Pattern) extendedMatch(item *Item) []Offset {
@@ -282,7 +286,7 @@ func (p *Pattern) extendedMatch(item *Item) []Offset {
offsets := []Offset{}
for _, term := range p.terms {
pfun := p.procFun[term.typ]
if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 {
if sidx, eidx := p.iter(pfun, input, term.caseSensitive, term.text); sidx >= 0 {
if term.inv {
break
}
@@ -294,30 +298,29 @@ func (p *Pattern) extendedMatch(item *Item) []Offset {
return offsets
}
func (p *Pattern) prepareInput(item *Item) *Transformed {
func (p *Pattern) prepareInput(item *Item) *[]Token {
if item.transformed != nil {
return item.transformed
}
var ret *Transformed
var ret *[]Token
if len(p.nth) > 0 {
tokens := Tokenize(item.text, p.delimiter)
ret = Transform(tokens, p.nth)
} else {
trans := Transformed{
whole: item.text,
parts: []Token{Token{text: item.text, prefixLength: 0}}}
runes := []rune(*item.text)
trans := []Token{Token{text: &runes, prefixLength: 0}}
ret = &trans
}
item.transformed = ret
return ret
}
func (p *Pattern) iter(pfun func(bool, *string, []rune) (int, int),
inputs *Transformed, pattern []rune) (int, int) {
for _, part := range inputs.parts {
func (p *Pattern) iter(pfun func(bool, *[]rune, []rune) (int, int),
tokens *[]Token, caseSensitive bool, pattern []rune) (int, int) {
for _, part := range *tokens {
prefixLength := part.prefixLength
if sidx, eidx := pfun(p.caseSensitive, part.text, pattern); sidx >= 0 {
if sidx, eidx := pfun(caseSensitive, part.text, pattern); sidx >= 0 {
return sidx + prefixLength, eidx + prefixLength
}
}

View File

@@ -7,7 +7,7 @@ import (
)
func TestParseTermsExtended(t *testing.T) {
terms := parseTerms(ModeExtended,
terms := parseTerms(ModeExtended, CaseSmart,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
if len(terms) != 8 ||
terms[0].typ != termFuzzy || terms[0].inv ||
@@ -31,7 +31,7 @@ func TestParseTermsExtended(t *testing.T) {
}
func TestParseTermsExtendedExact(t *testing.T) {
terms := parseTerms(ModeExtendedExact,
terms := parseTerms(ModeExtendedExact, CaseSmart,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
if len(terms) != 8 ||
terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 ||
@@ -47,7 +47,7 @@ func TestParseTermsExtendedExact(t *testing.T) {
}
func TestParseTermsEmpty(t *testing.T) {
terms := parseTerms(ModeExtended, "' $ ^ !' !^ !$")
terms := parseTerms(ModeExtended, CaseSmart, "' $ ^ !' !^ !$")
if len(terms) != 0 {
t.Errorf("%s", terms)
}
@@ -58,8 +58,8 @@ func TestExact(t *testing.T) {
clearPatternCache()
pattern := BuildPattern(ModeExtended, CaseSmart,
[]Range{}, nil, []rune("'abc"))
str := "aabbcc abc"
sidx, eidx := algo.ExactMatchNaive(pattern.caseSensitive, &str, pattern.terms[0].text)
runes := []rune("aabbcc abc")
sidx, eidx := algo.ExactMatchNaive(pattern.caseSensitive, &runes, pattern.terms[0].text)
if sidx != 7 || eidx != 10 {
t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
}

View File

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

View File

@@ -20,14 +20,17 @@ import (
// Terminal represents terminal input/output
type Terminal struct {
inlineInfo bool
prompt string
reverse bool
hscroll bool
cx int
cy int
offset int
yanked []rune
input []rune
multi bool
sort bool
toggleSort int
expect []int
pressed int
@@ -36,7 +39,7 @@ type Terminal struct {
progress int
reading bool
merger *Merger
selected map[*string]selectedItem
selected map[uint32]selectedItem
reqBox *util.EventBox
eventBox *util.EventBox
mutex sync.Mutex
@@ -77,36 +80,34 @@ const (
reqQuit
)
const (
initialDelay = 100 * time.Millisecond
spinnerDuration = 200 * time.Millisecond
)
// NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := []rune(opts.Query)
return &Terminal{
inlineInfo: opts.InlineInfo,
prompt: opts.Prompt,
reverse: opts.Reverse,
hscroll: opts.Hscroll,
cx: len(input),
cy: 0,
offset: 0,
yanked: []rune{},
input: input,
multi: opts.Multi,
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
expect: opts.Expect,
pressed: 0,
printQuery: opts.PrintQuery,
merger: EmptyMerger,
selected: make(map[*string]selectedItem),
selected: make(map[uint32]selectedItem),
reqBox: util.NewEventBox(),
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
startChan: make(chan bool, 1),
initFunc: func() {
C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse)
C.Init(opts.Theme, opts.Black, opts.Mouse)
}}
}
@@ -230,15 +231,31 @@ func (t *Terminal) printPrompt() {
}
func (t *Terminal) printInfo() {
t.move(1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
C.CPrint(C.ColSpinner, true, _spinner[idx])
if t.inlineInfo {
t.move(0, len(t.prompt)+displayWidth(t.input)+1, true)
if t.reading {
C.CPrint(C.ColSpinner, true, " < ")
} else {
C.CPrint(C.ColPrompt, true, " < ")
}
} else {
t.move(1, 0, true)
if t.reading {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
C.CPrint(C.ColSpinner, true, _spinner[idx])
}
t.move(1, 2, false)
}
t.move(1, 2, false)
output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count)
if t.toggleSort > 0 {
if t.sort {
output += "/S"
} else {
output += " "
}
}
if t.multi && len(t.selected) > 0 {
output += fmt.Sprintf(" (%d)", len(t.selected))
}
@@ -251,10 +268,16 @@ func (t *Terminal) printInfo() {
func (t *Terminal) printList() {
t.constrain()
maxy := maxItems()
maxy := t.maxItems()
count := t.merger.Length() - t.offset
for i := 0; i < maxy; i++ {
t.move(i+2, 0, true)
var line int
if t.inlineInfo {
line = i + 1
} else {
line = i + 2
}
t.move(line, 0, true)
if i < count {
t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset)
}
@@ -262,7 +285,7 @@ func (t *Terminal) printList() {
}
func (t *Terminal) printItem(item *Item, current bool) {
_, selected := t.selected[item.text]
_, selected := t.selected[item.index]
if current {
C.CPrint(C.ColCursor, true, ">")
if selected {
@@ -318,7 +341,7 @@ func trimLeft(runes []rune, width int) ([]rune, int32) {
return runes, trimmed
}
func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) {
func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) {
var maxe int32
for _, offset := range item.offsets {
if offset[1] > maxe {
@@ -332,30 +355,40 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, cur
maxWidth := C.MaxX() - 3
fullWidth := displayWidth(text)
if fullWidth > maxWidth {
// Stri..
matchEndWidth := displayWidth(text[:maxe])
if matchEndWidth <= maxWidth-2 {
if t.hscroll {
// Stri..
matchEndWidth := displayWidth(text[:maxe])
if matchEndWidth <= maxWidth-2 {
text, _ = trimRight(text, maxWidth-2)
text = append(text, []rune("..")...)
} else {
// Stri..
if matchEndWidth < fullWidth-2 {
text = append(text[:maxe], []rune("..")...)
}
// ..ri..
var diff int32
text, diff = trimLeft(text, maxWidth-2)
// Transform offsets
for idx, offset := range offsets {
b, e := offset.offset[0], offset.offset[1]
b += 2 - diff
e += 2 - diff
b = util.Max32(b, 2)
offsets[idx].offset[0] = b
offsets[idx].offset[1] = util.Max32(b, e)
}
text = append([]rune(".."), text...)
}
} else {
text, _ = trimRight(text, maxWidth-2)
text = append(text, []rune("..")...)
} else {
// Stri..
if matchEndWidth < fullWidth-2 {
text = append(text[:maxe], []rune("..")...)
}
// ..ri..
var diff int32
text, diff = trimLeft(text, maxWidth-2)
// Transform offsets
for idx, offset := range offsets {
b, e := offset.offset[0], offset.offset[1]
b += 2 - diff
e += 2 - diff
b = util.Max32(b, 2)
offsets[idx].offset[0] = b
offsets[idx].offset[1] = util.Max32(b, e)
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-2))
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
}
text = append([]rune(".."), text...)
}
}
@@ -499,6 +532,9 @@ func (t *Terminal) Loop() {
switch req {
case reqPrompt:
t.printPrompt()
if t.inlineInfo {
t.printInfo()
}
case reqInfo:
t.printInfo()
case reqList:
@@ -544,16 +580,16 @@ func (t *Terminal) Loop() {
toggle := func() {
if t.cy < t.merger.Length() {
item := t.merger.Get(t.cy)
if _, found := t.selected[item.text]; !found {
if _, found := t.selected[item.index]; !found {
var strptr *string
if item.origText != nil {
strptr = item.origText
} else {
strptr = item.text
}
t.selected[item.text] = selectedItem{time.Now(), strptr}
t.selected[item.index] = selectedItem{time.Now(), strptr}
} else {
delete(t.selected, item.text)
delete(t.selected, item.index)
}
req(reqInfo)
}
@@ -567,7 +603,8 @@ func (t *Terminal) Loop() {
}
if t.toggleSort > 0 {
if keyMatch(t.toggleSort, event) {
t.eventBox.Set(EvtSearchNew, true)
t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock()
continue
}
@@ -642,10 +679,10 @@ func (t *Terminal) Loop() {
case C.Del:
t.delChar()
case C.PgUp:
t.vmove(maxItems() - 1)
t.vmove(t.maxItems() - 1)
req(reqList)
case C.PgDn:
t.vmove(-(maxItems() - 1))
t.vmove(-(t.maxItems() - 1))
req(reqList)
case C.AltB:
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
@@ -668,6 +705,10 @@ func (t *Terminal) Loop() {
if !t.reverse {
my = C.MaxY() - my - 1
}
min := 2
if t.inlineInfo {
min = 1
}
if me.S != 0 {
// Scroll
if t.merger.Length() > 0 {
@@ -679,8 +720,8 @@ func (t *Terminal) Loop() {
}
} else if me.Double {
// Double-click
if my >= 2 {
if t.vset(my-2) && t.cy < t.merger.Length() {
if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
req(reqClose)
}
}
@@ -688,9 +729,9 @@ func (t *Terminal) Loop() {
if my == 0 && mx >= 0 {
// Prompt
t.cx = mx
} else if my >= 2 {
} else if my >= min {
// List
if t.vset(t.offset+my-2) && t.multi && me.Mod {
if t.vset(t.offset+my-min) && t.multi && me.Mod {
toggle()
}
req(reqList)
@@ -701,7 +742,7 @@ func (t *Terminal) Loop() {
t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed {
t.eventBox.Set(EvtSearchNew, false)
t.eventBox.Set(EvtSearchNew, t.sort)
}
for _, event := range events {
t.reqBox.Set(event, nil)
@@ -711,7 +752,7 @@ func (t *Terminal) Loop() {
func (t *Terminal) constrain() {
count := t.merger.Length()
height := C.MaxY() - 2
height := t.maxItems()
diffpos := t.cy - t.offset
t.cy = util.Constrain(t.cy, 0, count-1)
@@ -744,6 +785,10 @@ func (t *Terminal) vset(o int) bool {
return t.cy == o
}
func maxItems() int {
return C.MaxY() - 2
func (t *Terminal) maxItems() int {
if t.inlineInfo {
return C.MaxY() - 1
} else {
return C.MaxY() - 2
}
}

View File

@@ -16,15 +16,9 @@ type Range struct {
end int
}
// Transformed holds the result of tokenization and transformation
type Transformed struct {
whole *string
parts []Token
}
// Token contains the tokenized part of the strings and its prefix length
type Token struct {
text *string
text *[]rune
prefixLength int
}
@@ -81,8 +75,8 @@ func withPrefixLengths(tokens []string, begin int) []Token {
for idx, token := range tokens {
// Need to define a new local variable instead of the reused token to take
// the pointer to it
str := token
ret[idx] = Token{text: &str, prefixLength: prefixLength}
runes := []rune(token)
ret[idx] = Token{text: &runes, prefixLength: prefixLength}
prefixLength += len([]rune(token))
}
return ret
@@ -142,33 +136,40 @@ func Tokenize(str *string, delimiter *regexp.Regexp) []Token {
return withPrefixLengths(tokens, 0)
}
func joinTokens(tokens []Token) string {
func joinTokens(tokens *[]Token) *string {
ret := ""
for _, token := range tokens {
ret += *token.text
for _, token := range *tokens {
ret += string(*token.text)
}
return ret
return &ret
}
func joinTokensAsRunes(tokens *[]Token) *[]rune {
ret := []rune{}
for _, token := range *tokens {
ret = append(ret, *token.text...)
}
return &ret
}
// Transform is used to transform the input when --with-nth option is given
func Transform(tokens []Token, withNth []Range) *Transformed {
func Transform(tokens []Token, withNth []Range) *[]Token {
transTokens := make([]Token, len(withNth))
numTokens := len(tokens)
whole := ""
for idx, r := range withNth {
part := ""
part := []rune{}
minIdx := 0
if r.begin == r.end {
idx := r.begin
if idx == rangeEllipsis {
part += joinTokens(tokens)
part = append(part, *joinTokensAsRunes(&tokens)...)
} else {
if idx < 0 {
idx += numTokens + 1
}
if idx >= 1 && idx <= numTokens {
minIdx = idx - 1
part += *tokens[idx-1].text
part = append(part, *tokens[idx-1].text...)
}
}
} else {
@@ -195,11 +196,10 @@ func Transform(tokens []Token, withNth []Range) *Transformed {
minIdx = util.Max(0, begin-1)
for idx := begin; idx <= end; idx++ {
if idx >= 1 && idx <= numTokens {
part += *tokens[idx-1].text
part = append(part, *tokens[idx-1].text...)
}
}
}
whole += part
var prefixLength int
if minIdx < numTokens {
prefixLength = tokens[minIdx].prefixLength
@@ -208,7 +208,5 @@ func Transform(tokens []Token, withNth []Range) *Transformed {
}
transTokens[idx] = Token{&part, prefixLength}
}
return &Transformed{
whole: &whole,
parts: transTokens}
return &transTokens
}

View File

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

View File

@@ -19,6 +19,14 @@ func Max(first int, items ...int) int {
return max
}
// Max32 returns the smallest 32-bit integer
func Min32(first int32, second int32) int32 {
if first <= second {
return first
}
return second
}
// Max32 returns the largest 32-bit integer
func Max32(first int32, second int32) int32 {
if first > second {
@@ -69,3 +77,14 @@ func Between(val int, min int, max int) bool {
func IsTty() bool {
return int(C.isatty(C.int(os.Stdin.Fd()))) != 0
}
func TrimRight(runes *[]rune) []rune {
var i int
for i = len(*runes) - 1; i >= 0; i-- {
char := (*runes)[i]
if char != ' ' && char != '\t' {
break
}
}
return (*runes)[0 : i+1]
}

View File

@@ -3,11 +3,14 @@ Execute (Setup):
Log 'Test directory: ' . g:dir
Execute (fzf#run with dir option):
let cwd = getcwd()
let result = fzf#run({ 'options': '--filter=vdr', 'dir': g:dir })
AssertEqual ['fzf.vader'], result
AssertEqual getcwd(), cwd
let result = sort(fzf#run({ 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], result
AssertEqual ['fzf.vader', 'test_go.rb'], result
AssertEqual getcwd(), cwd
Execute (fzf#run with Funcref command):
let g:ret = []
@@ -15,8 +18,8 @@ Execute (fzf#run with Funcref command):
call add(g:ret, a:e)
endfunction
let result = sort(fzf#run({ 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], result
AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], sort(g:ret)
AssertEqual ['fzf.vader', 'test_go.rb'], result
AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret)
Execute (fzf#run with string source):
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))

View File

@@ -4,7 +4,9 @@
require 'minitest/autorun'
require 'fileutils'
Dir.chdir File.expand_path('../../', __FILE__)
base = File.expand_path('../../', __FILE__)
Dir.chdir base
FZF = "#{base}/bin/fzf"
class NilClass
def include? str
@@ -26,7 +28,8 @@ module Temp
waited = 0
while waited < 5
begin
data = `cat #{name}`
system 'sync'
data = File.read(name)
return data unless data.empty?
rescue
sleep 0.1
@@ -195,7 +198,7 @@ class TestBase < Minitest::Test
nil
end
}.compact
"fzf #{opts.join ' '}"
"#{FZF} #{opts.join ' '}"
end
end
@@ -243,7 +246,7 @@ class TestGoFZF < TestBase
end
def test_key_bindings
tmux.send_keys "fzf -q 'foo bar foo-bar'", :Enter
tmux.send_keys "#{FZF} -q 'foo bar foo-bar'", :Enter
tmux.until { |lines| lines.last =~ /^>/ }
# CTRL-A
@@ -462,14 +465,91 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 1 111 | #{fzf '-m +s --tac --toggle-sort=ctrl-r -q11'}", :Enter
tmux.until { |lines| lines[-3].include? '> 111' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include? '4/111 (1)' }
tmux.until { |lines| lines[-2].include? '4/111 (1)' }
tmux.send_keys 'C-R'
tmux.until { |lines| lines[-3].include? '> 11' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include? '4/111 (2)' }
tmux.until { |lines| lines[-2].include? '4/111/S (2)' }
tmux.send_keys :Enter
assert_equal ['111', '11'], readonce.split($/)
end
def test_unicode_case
tempname = TEMPNAME + Time.now.to_f.to_s
writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4]
assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/)
assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/)
rescue
File.unlink tempname
end
def test_tiebreak
tempname = TEMPNAME + Time.now.to_f.to_s
input = %w[
--foobar--------
-----foobar---
----foobar--
-------foobar-
]
writelines tempname, input
assert_equal input, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=index`.split($/)
by_length = %w[
----foobar--
-----foobar---
-------foobar-
--foobar--------
]
assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar`.split($/)
assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=length`.split($/)
by_begin = %w[
--foobar--------
----foobar--
-----foobar---
-------foobar-
]
assert_equal by_begin, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=begin`.split($/)
assert_equal by_begin, `cat #{tempname} | #{FZF} -f"!z foobar" -x --tiebreak begin`.split($/)
assert_equal %w[
-------foobar-
----foobar--
-----foobar---
--foobar--------
], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/)
assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/)
rescue
File.unlink tempname
end
def test_invalid_cache
tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter
tmux.until { |lines| lines[-2].include? '2/3' }
tmux.send_keys :BSpace
tmux.until { |lines| lines[-2].include? '3/3' }
tmux.send_keys :D
tmux.until { |lines| lines[-2].include? '1/3' }
tmux.send_keys :Enter
end
def test_smart_case_for_each_term
assert_equal 1, `echo Foo bar | #{FZF} -x -f "foo Fbar" | wc -l`.to_i
end
private
def writelines path, lines, timeout = 10
File.open(path, 'w') do |f|
f << lines.join($/)
f.sync
end
since = Time.now
while `cat #{path}`.split($/).length != lines.length && (Time.now - since) < 10
sleep 0.1
end
end
end
module TestShell
@@ -502,11 +582,11 @@ module TestShell
def test_alt_c
tmux.prepare
tmux.send_keys :Escape, :c
lines = tmux.until { |lines| lines[-1].start_with? '>' }
tmux.send_keys :Escape, :c, pane: 0
lines = tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' }
expected = lines[-3][2..-1]
p expected
tmux.send_keys :Enter
tmux.send_keys :Enter, pane: 1
tmux.prepare
tmux.send_keys :pwd, :Enter
tmux.until { |lines| p lines; lines[-1].end_with?(expected) }
@@ -519,11 +599,11 @@ module TestShell
tmux.send_keys 'echo 3d', :Enter; tmux.prepare
tmux.send_keys 'echo 3rd', :Enter; tmux.prepare
tmux.send_keys 'echo 4th', :Enter; tmux.prepare
tmux.send_keys 'C-r'
tmux.until { |lines| lines[-1].start_with? '>' }
tmux.send_keys '3d'
tmux.until { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort
tmux.send_keys :Enter
tmux.send_keys 'C-r', pane: 0
tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' }
tmux.send_keys '3d', pane: 1
tmux.until(pane: 1) { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort
tmux.send_keys :Enter, pane: 1
tmux.until { |lines| lines[-1] == 'echo 3rd' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == '3rd' }
@@ -546,25 +626,29 @@ class TestBash < TestBase
def test_file_completion
tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}', :Enter
tmux.prepare
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab
tmux.until { |lines| lines[-1].start_with? '>' }
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0
tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' }
tmux.send_keys :BTab, :BTab, :Enter
tmux.until { |lines|
tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1].include?('/tmp/fzf-test/10') &&
lines[-1].include?('/tmp/fzf-test/100')
}
end
end
def test_dir_completion
tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab
tmux.until { |lines| lines[-1].start_with? '>' }
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0
tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' }
tmux.send_keys :BTab, :BTab # BTab does not work here
tmux.send_keys 55
tmux.until { |lines| lines[-2].start_with? ' 1/' }
tmux.until(pane: 1) { |lines| lines[-2].start_with? ' 1/' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' }
tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1] == 'cd /tmp/fzf-test/d55/'
end
tmux.send_keys :xx
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
@@ -584,12 +668,15 @@ class TestBash < TestBase
lines = tmux.until { |lines| lines[-1].start_with? '[1]' }
pid = lines[-1].split.last
tmux.prepare
tmux.send_keys 'kill ', :Tab
tmux.until { |lines| lines[-1].start_with? '>' }
tmux.send_keys 'kill ', :Tab, pane: 0
tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' }
tmux.send_keys 'sleep12345'
tmux.until { |lines| lines[-3].include? 'sleep 12345' }
tmux.until(pane: 1) { |lines| lines[-3].include? 'sleep 12345' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == "kill #{pid}" }
tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1] == "kill #{pid}"
end
end
end