Compare commits

...

102 Commits
0.7.0 ... 0.8.4

Author SHA1 Message Date
Junegunn Choi
f6b1a6278f Add --reverse option (top-to-bottom layout) 2014-05-17 22:07:18 +09:00
Junegunn Choi
db58182483 Customization of key bindings (#40) 2014-05-06 15:39:44 +09:00
Junegunn Choi
6e9f0882da Update README (missing link to fish key bindings file) 2014-05-04 12:56:43 +09:00
Junegunn Choi
7ed18579dc set -o vi is required for vi-mode bash key bindings (#39) 2014-05-04 12:52:33 +09:00
Junegunn Choi
f250fc8f86 Fix #41: [CTRL-T] long file paths causing wrapping artifacts 2014-05-04 11:28:34 +09:00
Junegunn Choi
6eea9603c2 Fix bug in install script 2014-05-04 00:49:29 +09:00
Junegunn Choi
20915529b7 Add link to examples wiki page 2014-05-02 23:38:36 +09:00
Junegunn Choi
b3efccca81 [fish] Remove temporary file after use 2014-05-02 16:35:36 +09:00
Junegunn Choi
809d465de5 Tip on using git ls-tree (#31) 2014-05-02 12:52:06 +09:00
Junegunn Choi
7d15071c63 Fish shell support - installer / key bindings (#33) 2014-05-02 11:27:32 +09:00
Junegunn Choi
89eb1575e7 Simpler check for curses 2014-05-02 04:51:35 +09:00
Junegunn Choi
5d6ed935a4 Update README: master.tar.gz 2014-04-25 19:45:07 +09:00
Junegunn Choi
0528435386 Update README 2014-04-25 19:16:33 +09:00
Junegunn Choi
fe22213b51 Remove gif link 2014-04-14 16:48:54 +09:00
Junegunn Choi
aab42eaaba Update Vim plugin example 2014-04-12 20:02:04 +09:00
Junegunn Choi
16031b0d54 [Vim] Allow vertical split of tmux window 2014-04-12 19:53:39 +09:00
Junegunn Choi
ded184daaf Typo 2014-04-12 19:52:25 +09:00
Junegunn Choi
ecf90bd25b Update README 2014-04-06 15:25:58 +09:00
Junegunn Choi
d82e38adc1 0.8.3 2014-04-05 12:56:10 +09:00
Junegunn Choi
af677e7e35 Vim plugin: do not enable tmux-integration if version < 1.7 2014-04-04 12:43:29 +09:00
Junegunn Choi
6ad38bdad3 Update example: suppress error message from fc on bash (#37)
`'fc' -l 1` generated an error message on bash
2014-04-04 10:26:38 +09:00
Junegunn Choi
8b80136a87 Merge pull request #37 from wellle/zsh-history
Feed all zsh history into fzf
2014-04-03 23:14:28 +09:00
Christian Wellenbrock
97de919152 Feed all zsh history into fzf 2014-04-03 16:11:51 +02:00
Junegunn Choi
0eafa725b9 Fix test code indentation 2014-04-03 14:53:47 +09:00
Junegunn Choi
fa212efe5f Fix ranking when multiple regions overlap
e.g.
  Match region #1: [-----------]
  Match region #2:       [---]
  Match region #3:         [------]
2014-04-03 14:51:01 +09:00
Junegunn Choi
a9056ce90c Add gif showing tmux integration 2014-04-03 01:29:21 +09:00
Junegunn Choi
16682a3f92 Update fe example as the exit status from -0 has changed (#36) 2014-04-03 01:20:22 +09:00
Junegunn Choi
02c01c81a0 Improve -0 and -1 as suggested in #36
- Make -0 and -1 work without -q
- Change exit status to 0 when exiting with -0
2014-04-03 01:06:40 +09:00
Junegunn Choi
22d3929ae3 Implement --select-1 and --exit-0 (#27, #36) 2014-04-02 21:41:57 +09:00
Junegunn Choi
ab9fbf1967 Allow --nth option to take multiple indexes (comma-separated) 2014-04-02 01:49:07 +09:00
Junegunn Choi
608ec2b806 set -o nonomatch for zsh (#34)
Avoid error message in an empty directory
2014-04-01 21:39:40 +09:00
Junegunn Choi
e5ae4f0ef6 Do not load interactive parts when not required (#34) 2014-04-01 20:55:26 +09:00
Junegunn Choi
67ba87d390 Avoid CTRL-T error when default shell != zsh (#34) 2014-04-01 20:49:54 +09:00
Junegunn Choi
77d45cb173 Avoid starting interactive bash (#34) 2014-04-01 20:48:15 +09:00
Junegunn Choi
d83febea46 Merge pull request #35 from junegunn/fix-tmux-on-linux
Fix #34: tmux integration on Linux
2014-04-01 17:58:19 +09:00
Junegunn Choi
546a315884 Fix #34: tmux integration on Linux 2014-04-01 08:55:16 +00:00
Junegunn Choi
af616457e3 Use -p option of split-window instead of manual calculation 2014-03-31 13:48:53 +09:00
Junegunn Choi
1a100a2919 No need for screenrow() 2014-03-31 13:36:58 +09:00
Junegunn Choi
a85bb93b69 Fix use of screenrow when tmux height is given in % 2014-03-31 13:22:52 +09:00
Junegunn Choi
057eda060c Installation on other shells 2014-03-31 10:15:56 +09:00
Junegunn Choi
48f9ee6763 Update install script 2014-03-31 01:01:23 +09:00
Junegunn Choi
52b74abb99 Merge pull request #32 from junegunn/nth
Add --nth and --delimiter option
2014-03-30 15:19:05 +09:00
Junegunn Choi
ec4b8a59fa Implement --nth and --delimiter option 2014-03-30 15:12:04 +09:00
Junegunn Choi
cf8dbf8047 Allow setting tmux split height in % 2014-03-28 17:15:45 +09:00
Junegunn Choi
995d380200 Merge pull request #30 from junegunn/keybinding-tmux-split
Make CTRL-T use tmux split when possible
2014-03-28 15:32:23 +09:00
Junegunn Choi
ae86cdf09a Make CTRL-T use tmux split when possible 2014-03-28 15:28:10 +09:00
Junegunn Choi
2b346659a0 Vim plugin: tmux integration 2014-03-28 00:58:07 +09:00
Junegunn Choi
49081711a9 Execute clear before fzf 2014-03-26 01:34:59 +09:00
Junegunn Choi
e7439ce193 Major update to Vim plugin 2014-03-25 19:55:52 +09:00
Junegunn Choi
b8e438b6be Prefer pre-existing function/alias in Vim plugin 2014-03-25 12:05:57 +09:00
Junegunn Choi
678e950b6d Use --reverse option in fco example (#29) 2014-03-20 10:38:53 +09:00
Junegunn Choi
9ea651f1cd Merge pull request #29 from wellle/fix/fco
Fix small typo in Readme
2014-03-20 10:33:47 +09:00
Christian Wellenbrock
bd98a08b89 Fix small typo in Readme 2014-03-20 00:07:47 +01:00
Junegunn Choi
f02bb4fdac Add fe command to examples section as suggested in #27 2014-03-20 01:57:57 +09:00
Junegunn Choi
0a8352a5cd Quote $1 in vimf example (#26) 2014-03-19 21:57:03 +09:00
Junegunn Choi
737423995d Merge pull request #28 from wellle/ignore-dsstore
Add .DS_Store to .gitignore
2014-03-19 21:19:28 +09:00
Christian Wellenbrock
2916bf7ee4 Add .DS_Store to .gitignore 2014-03-19 13:14:20 +01:00
Junegunn Choi
fa54c5d9b0 Merge pull request #26 from wellle/vimf-query
Add --query parameter to fzf invocation in vimf function
2014-03-19 21:09:52 +09:00
Christian Wellenbrock
693b6651b4 Add --query parameter to fzf invocation in vimf function 2014-03-19 12:30:42 +01:00
Junegunn Choi
5c71ecb267 Implement C-Y (yank) 2014-03-15 16:55:20 +09:00
Junegunn Choi
1ba50eba98 Fix gemspec
Reference:
16ead977fa
2014-03-14 18:25:55 +09:00
Junegunn Choi
2c8a256b13 Update README and install
- Unset multi-select option with +m
2014-03-14 17:53:23 +09:00
Junegunn Choi
f4c5aa03d7 Update README and install script
- Added examples: fbr and fco
- Always use local variables
2014-03-14 17:46:55 +09:00
Junegunn Choi
c6acb2a639 Update README 2014-03-13 15:28:01 +09:00
Junegunn Choi
2296013174 Add ALT-C keybinding for bash 2014-03-13 14:29:27 +09:00
Junegunn Choi
8a3e8c2d81 Install curses gem in user's home directory 2014-03-13 11:01:35 +09:00
Junegunn Choi
ae84d8c7a4 Update README 2014-03-09 11:52:35 +09:00
Junegunn Choi
dbd627c38a Update README: Remove section on --disable-gems
This is automatically set in install script. It may only cause unnecessary
confusion.
2014-03-09 10:46:34 +09:00
Junegunn Choi
d172c3ce03 Update README 2014-03-09 10:43:59 +09:00
Junegunn Choi
9904f5354e Add --black for terminals incapable of use_default_colors
See the discussion in #18.

Use --black option to use black background regardless of the default
background color of the terminal. Also, this option can be used to fix
rendering issues on terminals that don't support use_default_colors (man
3 default_colors). Depending on the terminal, use_default_colors may or
may not succeed, but the Ruby version of it always returns nil, it's
currently not possible to automatically enable this option.
2014-03-09 04:09:09 +09:00
Junegunn Choi
f345bf7983 Shift-left/right on OSX 2014-03-08 01:55:48 +09:00
Junegunn Choi
875f9b6534 Reduce timeout to 0.1 sec 2014-03-08 01:55:11 +09:00
Junegunn Choi
871dfb709d Introduce escape time-out for better handling of escape sequences 2014-03-08 01:28:32 +09:00
Junegunn Choi
19e24bd644 Home/End/PgUp/PgDn/Del/(Ins) 2014-03-08 01:02:32 +09:00
Junegunn Choi
457a240457 Add option to disable 256-color output (related #18) 2014-03-07 17:34:11 +09:00
Junegunn Choi
bbf4567dd8 Allow command/control-click/wheel
e.g. urxvt
2014-03-07 17:30:44 +09:00
Junegunn Choi
27d3b52843 Update gem version 2014-03-07 01:37:33 +09:00
Junegunn Choi
dcb4694ec1 Reimplement mouse input without using Curses.getch 2014-03-06 20:52:46 +09:00
Junegunn Choi
2fb8ae010f Completely remove mouse support
Since the version 0.7.0, fzf internally used Curses.getch() call to take user
input, which allowed it to support mouse input as well. However it has turned
out that Curses.getch() has introduced glitches that cannot be easily handled
(e.g. Try resize the terminal). So I finally decided that it's not worth the
trouble and drop the mouse support.
2014-03-06 12:21:09 +09:00
Junegunn Choi
65ae6cabb5 Rename variables 2014-03-05 22:41:45 +09:00
Junegunn Choi
86a66da04d Synchronize getch calls to reduce screen glitches 2014-03-05 19:07:59 +09:00
Junegunn Choi
d66b02b0cd Disable typeahead optimization in Ruby 1.8 2014-03-05 18:00:20 +09:00
Junegunn Choi
b3182c3304 Performance optimization: batch application of input chars 2014-03-05 11:21:20 +09:00
Junegunn Choi
2dbca00bfb Implement --extended-exact option (#24) 2014-03-04 21:29:45 +09:00
Junegunn Choi
b22fd6de6d Fix #22. Keybindings for vi-mode bash. 2014-03-04 18:53:29 +09:00
Junegunn Choi
245ee42763 Update installation instruction 2014-03-04 11:25:50 +09:00
Junegunn Choi
98bef4600c Merge pull request #20 from wellle/zsh-history-fc
Use `fc` instead of `history` to avoid `oh-my-zsh` alias
2014-02-26 19:03:01 +09:00
Christian Wellenbrock
f5d53b94fe Use fc instead of history to avoid omz alias 2014-02-26 10:56:44 +01:00
Junegunn Choi
00c8a68430 Unalias history on zsh (related #19) 2014-02-26 11:46:30 +09:00
Junegunn Choi
c1be834ff9 Merge pull request #19 from wellle/zsh_history
Feed all zsh history into fzf (not only most recent)
2014-02-25 23:41:54 +09:00
Christian Wellenbrock
2c0dc2f3b1 Feed all zsh history into fzf (not only most recent) 2014-02-25 15:40:52 +01:00
Junegunn Choi
1c94fef720 Update version number 2014-02-20 15:17:29 +09:00
Junegunn Choi
b711d76b8e Choose to use 256-colors when $TERM includes 256 (related: #18)
It turned out that Curses.can_change_color? returns false when $TERM is
set to screen-256color, which is perfectly capable of rendering 256
colors.
2014-02-20 13:38:04 +09:00
Junegunn Choi
4396ab7548 Do not set key bindings in non-interactive shell 2014-02-15 01:29:16 +09:00
Junegunn Choi
2b8c2b9f2a CTRL-R for bash: Unset $HISTTIMEFORMAT 2014-02-13 16:47:53 +09:00
Junegunn Choi
426284c87e Change CTRL-T binding to include directories 2014-02-07 18:41:05 +09:00
Junegunn Choi
089691faaf Cache the result as sorted 2014-02-02 21:41:08 +09:00
Junegunn Choi
301290663d Add -f (--filter) option (#15)
This commit adds --filter option so that fzf can be used as a simple unix
filter instead of being an interactive fuzzy finder.
2014-02-02 01:45:44 +09:00
Junegunn Choi
1155da7e1c Install curses 1.0.0 2014-02-02 01:42:04 +09:00
Junegunn Choi
eca0a99fb4 Proper handling of typeahead arrow keys
To reproduce: `sleep 2; fzf` and press arrow keys
2014-02-01 10:07:59 +09:00
Junegunn Choi
96215c4619 CTRL-L to clear and redraw the screen 2014-02-01 02:05:58 +09:00
Junegunn Choi
b2d2be55ef init_screen must be called within render block 2014-01-31 15:56:37 +09:00
9 changed files with 1446 additions and 344 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
pkg
Gemfile.lock
.DS_Store

339
README.md
View File

@@ -24,26 +24,35 @@ git clone https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install
```
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
```
The script will setup:
- `fzf` executable
- Key bindings (`CTRL-T`, `CTRL-R`, etc.)
- Fuzzy auto-completion for bash
- `fzf` function (bash, zsh, fish)
- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
- Fuzzy auto-completion (bash)
If you don't use any of the aforementioned shells, you have to manually place
fzf executable in a directory included in `$PATH`. Key bindings and
auto-completion will not be available in that case.
### Install as Vim plugin
You can use any Vim plugin manager to install fzf for Vim. If you don't use one,
I recommend you try [vim-plug](https://github.com/junegunn/vim-plug).
Once you have cloned the repository, add the following line to your .vimrc.
1. [Install vim-plug](https://github.com/junegunn/vim-plug#usage)
2. Edit your .vimrc
```vim
set rtp+=~/.fzf
```
call plug#begin()
Plug 'junegunn/fzf'
" ...
call plug#end()
3. Run `:PlugInstall`
Or you may use any Vim plugin manager, such as
[vim-plug](https://github.com/junegunn/vim-plug).
Usage
-----
@@ -51,16 +60,32 @@ Usage
```
usage: fzf [options]
Options
-m, --multi Enable multi-select
Search
-x, --extended Extended-search mode
-q, --query=STR Initial query
-s, --sort=MAX Maximum number of matched items to sort (default: 1000)
+s, --no-sort Do not sort the result. Keep the sequence unchanged.
-e, --extended-exact Extended-search mode (exact match)
-i Case-insensitive match (default: smart-case match)
+i Case-sensitive match
+c, --no-color Disable colors
-n, --nth=[-]N[,..] Comma-separated list of field indexes for limiting
search scope (positive or negative integers)
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result
-s, --sort=MAX Maximum number of matched items to sort (default: 1000)
+s, --no-sort Do not sort the result. Keep the sequence unchanged.
Interface
-m, --multi Enable multi-select with tab/shift-tab
--no-mouse Disable mouse
+c, --no-color Disable colors
+2, --no-256 Disable 256-color
--black Use black background
--reverse Reverse orientation
Scripting
-q, --query=STR Start the finder with the given query
-1, --select-1 Automatically select the only match
-0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder.
Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty
@@ -89,7 +114,7 @@ If you want to preserve the exact sequence of the input, provide `--no-sort` (or
history | fzf +s
```
### Key binding
### Keys
Use CTRL-J and CTRL-K (or CTRL-N and CTRL-P) to change the selection, press
enter key to select the item. CTRL-C, CTRL-G, or ESC will terminate the finder.
@@ -98,14 +123,15 @@ The following readline key bindings should also work as expected.
- CTRL-A / CTRL-E
- CTRL-B / CTRL-F
- CTRL-W / CTRL-U
- CTRL-W / CTRL-U / CTRL-Y
- ALT-B / ALT-F
If you enable multi-select mode with `-m` option, you can select multiple items
with TAB or Shift-TAB key.
You can also use mouse. Click on an item to select it or shift-click to select
multiple items. Use mouse wheel to move the cursor up and down.
You can also use mouse. Double-click on an item to select it or shift-click (or
ctrl-click) to select multiple items. Use mouse wheel to move the cursor up and
down.
### Extended-search mode
@@ -123,55 +149,98 @@ such as: `^music .mp3$ sbtrkt !rmx`
| `'wild` | Items that include `wild` | exact-match (quoted) |
| `!'fire` | Items that do not include `fire` | inverse-exact-match |
If you don't need fuzzy matching and do not wish to "quote" every word, start
fzf with `-e` or `--extended-exact` option.
Useful examples
---------------
```sh
# vimf - Open selected file in Vim
vimf() {
FILE=$(fzf) && vim "$FILE"
# fe [FUZZY PATTERN] - Open the selected file with the default editor
# - Bypass fuzzy finder if there's only one match (--select-1)
# - Exit if there's no match (--exit-0)
fe() {
local file
file=$(fzf --query="$1" --select-1 --exit-0)
[ -n "$file" ] && ${EDITOR:-vim} "$file"
}
# fd - cd to selected directory
fd() {
DIR=$(find ${1:-*} -path '*/\.*' -prune -o -type d -print 2> /dev/null | fzf) && cd "$DIR"
local dir
dir=$(find ${1:-*} -path '*/\.*' -prune \
-o -type d -print 2> /dev/null | fzf +m) &&
cd "$dir"
}
# fda - including hidden directories
fda() {
DIR=$(find ${1:-.} -type d 2> /dev/null | fzf) && cd "$DIR"
local dir
dir=$(find ${1:-.} -type d 2> /dev/null | fzf +m) && cd "$dir"
}
# fh - repeat history
fh() {
eval $(history | fzf +s | sed 's/ *[0-9]* *//')
eval $(([ -n "$ZSH_NAME" ] && fc -l 1 || history) | fzf +s | sed 's/ *[0-9]* *//')
}
# fkill - kill process
fkill() {
ps -ef | sed 1d | fzf -m | awk '{print $2}' | xargs kill -${1:-9}
}
# fbr - checkout git branch
fbr() {
local branches branch
branches=$(git branch) &&
branch=$(echo "$branches" | fzf +s +m) &&
git checkout $(echo "$branch" | sed "s/.* //")
}
# fco - checkout git commit
fco() {
local commits commit
commits=$(git log --pretty=oneline --abbrev-commit --reverse) &&
commit=$(echo "$commits" | fzf +s +m -e) &&
git checkout $(echo "$commit" | sed "s/ .*//")
}
# ftags - search ctags
ftags() {
local line
[ -e tags ] &&
line=$(
awk 'BEGIN { FS="\t" } !/^!/ {print toupper($4)"\t"$1"\t"$2"\t"$3}' tags |
cut -c1-80 | fzf --nth=1,2
) && $EDITOR $(cut -f3 <<< "$line") -c "set nocst" \
-c "silent tag $(cut -f2 <<< "$line")"
}
```
For more examples, see [the wiki
page](https://github.com/junegunn/fzf/wiki/examples).
Key bindings for command line
-----------------------------
The install script will setup the following key bindings.
### bash
- `CTRL-T` - Paste the selected file path(s) into the command line
- `CTRL-R` - Paste the selected command from history into the command line
The source code can be found in `~/.fzf.bash`.
### zsh
The install script will setup the following key bindings for bash, zsh, and
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
- `ALT-C` - cd into the selected directory
The source code can be found in `~/.fzf.zsh`.
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 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`.
Auto-completion
---------------
@@ -249,7 +318,11 @@ TODO :smiley:
Usage as Vim plugin
-------------------
If you install fzf as a Vim plugin, `:FZF` command will be added.
(fzf is a command-line utility, naturally it is only accessible in terminal Vim)
### `:FZF[!]`
If you have set up fzf for Vim, `:FZF` command will be added.
```vim
" Look for files under current directory
@@ -262,59 +335,106 @@ If you install fzf as a Vim plugin, `:FZF` command will be added.
:FZF --no-sort -m /tmp
```
You can override the source command which produces input to fzf.
Note that the environment variables `FZF_DEFAULT_COMMAND` and `FZF_DEFAULT_OPTS`
also apply here.
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.
### `fzf#run([options])`
For more advanced uses, you can call `fzf#run()` function which returns the list
of the selected items.
`fzf#run()` may take an options-dictionary:
| Option name | Type | Description |
| ------------- | ------------- | ------------------------------------------------------------------ |
| `source` | string | External command to generate input to fzf (e.g. `find .`) |
| `source` | list | Vim list as input to fzf |
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
| `sink` | funcref | Reference to function to process each selected item |
| `options` | string | Options to fzf |
| `dir` | string | Working directory |
| `tmux_width` | number/string | Use tmux vertical split with the given height (e.g. `20`, `50%`) |
| `tmux_height` | number/string | Use tmux horizontal split with the given height (e.g. `20`, `50%`) |
#### Examples
If `sink` option is not given, `fzf#run` will simply return the list.
```vim
let g:fzf_source = 'find . -type f'
let items = fzf#run({ 'options': '-m +c', 'dir': '~', 'source': 'ls' })
```
And you can predefine default options to fzf command.
But if `sink` is given as a string, the command will be executed for each
selected item.
```vim
let g:fzf_options = '--no-color --extended'
" Each selected item will be opened in a new tab
let items = fzf#run({ 'sink': 'tabe', 'options': '-m +c', 'dir': '~', 'source': 'ls' })
```
For more advanced uses, you can call `fzf#run` function as follows.
We can also use a Vim list as the source as follows:
```vim
:call fzf#run('tabedit', '-m +c')
" Choose a color scheme with fzf
nnoremap <silent> <Leader>C :call fzf#run({
\ 'source':
\ map(split(globpath(&rtp, "colors/*.vim"), "\n"),
\ "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"),
\ 'sink': 'colo',
\ 'options': '+m',
\ 'tmux_width': 20
\ })<CR>
```
Most of the time, you will prefer native Vim plugins with better integration
with Vim. The only reason one might consider using fzf in Vim is its speed. For
a very large list of files, fzf is significantly faster and it does not block.
`sink` option can be a function reference. The following example creates a
handy mapping that selects an open buffer.
```vim
" List of buffers
function! g:buflist()
redir => ls
silent ls
redir END
return split(ls, '\n')
endfunction
function! g:bufopen(e)
execute 'buffer '. matchstr(a:e, '^[ 0-9]*')
endfunction
nnoremap <silent> <Leader><Enter> :call fzf#run({
\ 'source': reverse(g:buflist()),
\ 'sink': function('g:bufopen'),
\ 'options': '+m',
\ 'tmux_height': '40%'
\ })<CR>
```
### Articles
- [fzf+vim+tmux](http://junegunn.kr/2014/04/fzf+vim+tmux)
Tips
----
### Faster startup with `--disable-gems` options
### Rendering issues
If you're running Ruby 1.9 or above, you can improve the startup time with
`--disable-gems` option to Ruby.
If you have any rendering issues, check the followings:
- `time ruby ~/bin/fzf -h`
- 0.077 sec
- `time ruby --disable-gems ~/bin/fzf -h`
- 0.025 sec
You can define fzf function with the option as follows:
```sh
fzf() {
ruby --disable-gems ~/bin/fzf "$@"
}
export -f fzf
```
However, this is automatically set up in your .bashrc and .zshrc if you use the
bundled [install](https://github.com/junegunn/fzf/blob/master/install) script.
### Incorrect display on Ruby 1.8
It is reported that the output of fzf can become unreadable on some terminals
when it's running on Ruby 1.8. If you experience the problem, upgrade your Ruby
to 1.9 or above. Ruby 1.9 or above is also required for displaying Unicode
characters.
1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it
contains `256` (e.g. `xterm-256color`)
2. If you're on screen or tmux, `$TERM` should be either `screen` or
`screen-256color`
3. Some terminal emulators (e.g. mintty) have problem displaying default
background color and make some text unable to read. In that case, try `--black`
option. And if it solves your problem, I recommend including it in
`FZF_DEFAULT_OPTS` for further convenience.
4. If you still have problem, try `--no-256` option or even `--no-color`.
5. Ruby 1.9 or above is required for correctly displaying unicode characters.
### Ranking algorithm
@@ -330,6 +450,73 @@ This limit can be adjusted with `-s` option, or with the environment variable
export FZF_DEFAULT_OPTS="--sort 20000"
```
### Respecting `.gitignore`, `.hgignore`, and `svn:ignore`
[ag](https://github.com/ggreer/the_silver_searcher) or
[pt](https://github.com/monochromegane/the_platinum_searcher) will do the
filtering:
```sh
# Feed the output of ag into fzf
ag -l -g "" | fzf
# Setting ag as the default source for fzf
export FZF_DEFAULT_COMMAND='ag -l -g ""'
# Now fzf (w/o pipe) will use ag instead of find
fzf
```
### `git ls-tree` for fast traversal
If you're running fzf in a large git repository, `git ls-tree` can boost up the
speed of the traversal.
```sh
# Copy the original fzf function to __fzf
declare -f __fzf > /dev/null ||
eval "$(echo "__fzf() {"; declare -f fzf | grep -v '^{' | tail -n +2)"
# Use git ls-tree when possible
fzf() {
if [ -n "$(git rev-parse HEAD 2> /dev/null)" ]; then
FZF_DEFAULT_COMMAND="git ls-tree -r --name-only HEAD" __fzf "$@"
else
__fzf "$@"
fi
}
```
### Fish shell
It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362)
that it doesn't allow reading from STDIN in command substitution, which means
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
```
### Windows
fzf works on [Cygwin](http://www.cygwin.com/) and
[MSYS2](http://sourceforge.net/projects/msys2/). You may need to use `--black`
option on MSYS2 to avoid rendering issues.
License
-------

9
ext/mkrf_conf.rb Normal file
View File

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

645
fzf
View File

@@ -7,13 +7,13 @@
# / __/ / /_/ __/
# /_/ /___/_/ Fuzzy finder for your shell
#
# Version: 0.7.0 (January 30, 2014)
# Version: 0.8.4 (May 17, 2014)
#
# Author: Junegunn Choi
# URL: https://github.com/junegunn/fzf
# License: MIT
#
# Copyright (c) 2013 Junegunn Choi
# Copyright (c) 2014 Junegunn Choi
#
# MIT License
#
@@ -50,7 +50,8 @@ end
class FZF
C = Curses
attr_reader :rxflag, :sort, :color, :mouse, :multi, :query, :extended
attr_reader :rxflag, :sort, :nth, :color, :black, :ansi256, :reverse,
:mouse, :multi, :query, :select1, :exit0, :filter, :extended
class AtomicVar
def initialize value
@@ -63,10 +64,8 @@ class FZF
end
def set value = nil
if block_given?
@mutex.synchronize { @value = yield @value }
else
@mutex.synchronize { @value = value }
@mutex.synchronize do
@value = block_given? ? yield(@value) : value
end
end
@@ -79,9 +78,17 @@ class FZF
@rxflag = nil
@sort = ENV.fetch('FZF_DEFAULT_SORT', 1000).to_i
@color = true
@ansi256 = true
@black = false
@multi = false
@extended = false
@mouse = true
@extended = nil
@select1 = false
@exit0 = false
@filter = nil
@nth = nil
@delim = nil
@reverse = false
argv =
if opts = ENV['FZF_DEFAULT_OPTS']
@@ -92,29 +99,58 @@ class FZF
end
while o = argv.shift
case o
when '--version' then version
when '--version' then FZF.version
when '-h', '--help' then usage 0
when '-m', '--multi' then @multi = true
when '+m', '--no-multi' then @multi = false
when '-x', '--extended' then @extended = true
when '+x', '--no-extended' then @extended = false
when '-x', '--extended' then @extended = :fuzzy
when '+x', '--no-extended' then @extended = nil
when '-i' then @rxflag = Regexp::IGNORECASE
when '+i' then @rxflag = 0
when '-c', '--color' then @color = true
when '+c', '--no-color' then @color = false
when '-2', '--256' then @ansi256 = true
when '+2', '--no-256' then @ansi256 = false
when '--black' then @black = true
when '--no-black' then @black = false
when '--mouse' then @mouse = true
when '--no-mouse' then @mouse = false
when '--reverse' then @reverse = true
when '--no-reverse' then @reverse = false
when '+s', '--no-sort' then @sort = nil
when '-1', '--select-1' then @select1 = true
when '+1', '--no-select-1' then @select1 = false
when '-0', '--exit-0' then @exit0 = true
when '+0', '--no-exit-0' then @exit0 = false
when '-q', '--query'
usage 1, 'query string required' unless query = argv.shift
@query = AtomicVar.new query.dup
when /^-q(.*)$/, /^--query=(.*)$/
@query = AtomicVar.new($1)
when '-f', '--filter'
usage 1, 'query string required' unless query = argv.shift
@filter = query
when /^-f(.*)$/, /^--filter=(.*)$/
@filter = $1
when '-n', '--nth'
usage 1, 'field number required' unless nth = argv.shift
usage 1, 'invalid field number' if nth.to_i == 0
@nth = parse_nth nth
when /^-n([0-9,-]+)$/, /^--nth=([0-9,-]+)$/
@nth = parse_nth $1
when '-d', '--delimiter'
usage 1, 'delimiter required' unless delim = argv.shift
@delim = FZF.build_delim_regex delim
when /^-d(.+)$/, /^--delimiter=(.+)$/
@delim = FZF.build_delim_regex $1
when '-s', '--sort'
usage 1, 'sort size required' unless sort = argv.shift
usage 1, 'invalid sort size' unless sort =~ /^[0-9]+$/
@sort = sort.to_i
when /^-s([0-9]+)$/, /^--sort=([0-9]+)$/
@sort = $1.to_i
when '-e', '--extended-exact' then @extended = :exact
when '+e', '--no-extended-exact' then @extended = nil
else
usage 1, "illegal option: #{o}"
end
@@ -126,54 +162,146 @@ class FZF
@events = {}
@new = []
@queue = Queue.new
@query ||= AtomicVar.new('')
@cursor_x = AtomicVar.new(@query.length)
@matches = AtomicVar.new([])
@count = AtomicVar.new(0)
@vcursor = AtomicVar.new(0)
@vcursors = AtomicVar.new(Set.new)
@spinner = AtomicVar.new('-\|/-\|/'.split(//))
@selects = AtomicVar.new({}) # ordered >= 1.9
@main = Thread.current
@stdout = $stdout.clone
@plcount = 0
@pending = nil
unless @filter
@query ||= AtomicVar.new('')
@cursor_x = AtomicVar.new(@query.length)
@matches = AtomicVar.new([])
@count = AtomicVar.new(0)
@vcursor = AtomicVar.new(0)
@vcursors = AtomicVar.new(Set.new)
@spinner = AtomicVar.new('-\|/-\|/'.split(//))
@selects = AtomicVar.new({}) # ordered >= 1.9
@main = Thread.current
@plcount = 0
end
end
def parse_nth nth
nth.split(',').map { |n|
ni = n.to_i
usage 1, "invalid field number: #{n}" if ni == 0
ni
}
end
def FZF.build_delim_regex delim
Regexp.compile(delim) rescue (delim = Regexp.escape(delim))
Regexp.compile "(?:.*?#{delim})|(?:.+?$)"
end
def start
$stdout.reopen($stderr)
if @filter
start_reader.join
filter_list @new
else
start_reader
emit(:key) { q = @query.get; [q, q.length] } unless empty = @query.empty?
if @select1 || @exit0
start_search do |loaded, matches|
len = empty ? @count.get : matches.length
if loaded
if @select1 && len == 1
puts empty ? matches.first : matches.first.first
exit 0
elsif @exit0 && len == 0
exit 0
end
end
init_screen
start_reader
start_renderer
start_search
start_loop
if loaded || len > 1
start_renderer
Thread.new { start_loop }
end
end
sleep
else
start_search
start_renderer
start_loop
end
end
end
def version
File.open(__FILE__, 'r') do |f|
f.each_line do |line|
if line =~ /Version: (.*)/
$stdout.puts "fzf " << $1
exit
def filter_list list
matches = matcher.match(list, @filter, '', '')
if @sort && matches.length <= @sort
matches = FZF.sort(matches)
end
matches.each { |m| puts m.first }
end
def matcher
@matcher ||=
if @extended
ExtendedFuzzyMatcher.new @rxflag, @extended, @nth, @delim
else
FuzzyMatcher.new @rxflag, @nth, @delim
end
end
class << self
def version
File.open(__FILE__, 'r') do |f|
f.each_line do |line|
if line =~ /Version: (.*)/
$stdout.puts 'fzf ' << $1
exit
end
end
end
end
def sort list
list.sort_by { |tuple| rank tuple }
end
def rank tuple
line, offsets = tuple
matchlen = 0
pe = 0
offsets.sort.each do |pair|
b, e = pair
b = pe if pe > b
pe = e if e > pe
matchlen += e - b if e > b
end
[matchlen, line.length, line]
end
end
def usage x, message = nil
$stderr.puts message if message
$stderr.puts %[usage: fzf [options]
Options
-m, --multi Enable multi-select
Search
-x, --extended Extended-search mode
-q, --query=STR Initial query
-s, --sort=MAX Maximum number of matched items to sort (default: 1000)
+s, --no-sort Do not sort the result. Keep the sequence unchanged.
-e, --extended-exact Extended-search mode (exact match)
-i Case-insensitive match (default: smart-case match)
+i Case-sensitive match
+c, --no-color Disable colors
-n, --nth=[-]N[,..] Comma-separated list of field indexes for limiting
search scope (positive or negative integers)
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result
-s, --sort=MAX Maximum number of matched items to sort (default: 1000)
+s, --no-sort Do not sort the result. Keep the sequence unchanged.
Interface
-m, --multi Enable multi-select with tab/shift-tab
--no-mouse Disable mouse
+c, --no-color Disable colors
+2, --no-256 Disable 256-color
--black Use black background
--reverse Reverse orientation
Scripting
-q, --query=STR Start the finder with the given query
-1, --select-1 Automatically select the only match
-0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder.
Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty
@@ -304,7 +432,11 @@ class FZF
end
def max_items; C.lines - 2; end
def cursor_y; C.lines - 1; end
def cursor_y offset = 0
@reverse ? (offset) : (C.lines - 1 - offset)
end
def cprint str, col
C.attron(col) do
addstr_safe str
@@ -324,7 +456,7 @@ class FZF
end
def print_info msg = nil
C.setpos cursor_y - 1, 0
C.setpos cursor_y(1), 0
C.clrtoeol
prefix =
if spinner = @spinner.first
@@ -414,22 +546,9 @@ class FZF
C.attroff color(:chosen, true) if chosen
end
def sort_by_rank list
list.sort_by { |tuple|
line, offsets = tuple
matchlen = 0
pe = nil
offsets.sort.each do |pair|
b, e = pair
b = pe if pe && pe > b
pe = e
matchlen += e - b
end
[matchlen, line.length, line]
}
end
AFTER_1_9 = RUBY_VERSION.split('.').map { |e| e.rjust(3, '0') }.join >= '001009'
if RUBY_VERSION.split('.').map { |e| e.rjust(3, '0') }.join > '001009'
if AFTER_1_9
@@wrx = Regexp.new '\p{Han}|\p{Katakana}|\p{Hiragana}|\p{Hangul}'
def width str
str.gsub(@@wrx, ' ').length rescue str.length
@@ -473,26 +592,24 @@ class FZF
end
def init_screen
@stdout = $stdout.clone
$stdout.reopen($stderr)
C.init_screen
if @mouse
C.mouseinterval 0
C.mousemask C::ALL_MOUSE_EVENTS
end
C.stdscr.keypad(true)
C.mousemask C::ALL_MOUSE_EVENTS if @mouse
C.start_color
dbg =
if C.respond_to?(:use_default_colors)
if !@black && C.respond_to?(:use_default_colors)
C.use_default_colors
-1
else
C::COLOR_BLACK
end
C.raw
C.nonl
C.noecho
if @color
if C.can_change_color? && ENV['TERM'].to_s =~ /256/
if @ansi256 && ENV['TERM'].to_s =~ /256/
C.init_pair 1, 110, dbg
C.init_pair 2, 108, dbg
C.init_pair 3, 254, 236
@@ -531,6 +648,8 @@ class FZF
end | (bold ? C::A_BOLD : 0)
end
end
C.refresh
end
def start_reader
@@ -544,7 +663,6 @@ class FZF
exit 1
end
else
$stdin.reopen IO.open(IO.sysopen('/dev/tty'), 'r')
@source
end
@@ -553,13 +671,12 @@ class FZF
emit(:new) { @new << line.chomp }
end
emit(:loaded) { true }
@spinner.clear
@spinner.clear if @spinner
end
end
def start_search
matcher = (@extended ? ExtendedFuzzyMatcher : FuzzyMatcher).new @rxflag
searcher = Thread.new {
def start_search &callback
Thread.new do
lists = []
events = {}
fcache = {}
@@ -598,7 +715,7 @@ class FZF
progress = 0
started_at = Time.now
if new_search && !lists.empty?
if updated = new_search && !lists.empty?
q, cx = events.delete(:key) || [q, 0]
empty = matcher.empty?(q)
unless matches = fcache[q]
@@ -618,17 +735,21 @@ class FZF
matcher.match(list, q, q[0, cx], q[cx..-1]))
end
next if skip
matches = fcache[q] = @sort ? found : found.reverse
end
if !empty && @sort && matches.length <= @sort
matches = sort_by_rank(matches)
matches = @sort ? found : found.reverse
if !empty && @sort && matches.length <= @sort
matches = FZF.sort(matches)
end
fcache[q] = matches
end
# Atomic update
@matches.set matches
end#new_search
callback = nil if callback &&
(updated || events[:loaded]) &&
callback.call(events[:loaded], matches)
# This small delay reduces the number of partial lists
sleep((delay = [20, delay + 5].min) * 0.01) unless user_input
@@ -637,7 +758,7 @@ class FZF
rescue Exception => e
@main.raise e
end
}
end
end
def pick
@@ -653,7 +774,7 @@ class FZF
# Wipe
if items.length < @plcount
@plcount.downto(items.length) do |idx|
C.setpos cursor_y - idx - 2, 0
C.setpos cursor_y(idx + 2), 0
C.clrtoeol
end
end
@@ -668,7 +789,7 @@ class FZF
}
items.each_with_index do |item, idx|
next unless wipe || cleanse.include?(idx)
row = cursor_y - idx - 2
row = cursor_y(idx + 2)
chosen = idx == vcursor
selected = @selects.include?([*item][0])
line, offsets = convert_item item
@@ -681,9 +802,10 @@ class FZF
end
def start_renderer
init_screen
Thread.new do
begin
refresh
while blk = @queue.shift
blk.call
refresh
@@ -718,15 +840,157 @@ class FZF
end
end
def test_mouse st, *states
states.any? { |s| s & st > 0 }
def read_nb chars = 1, default = nil, tries = 10
tries.times do |_|
begin
return @tty.read_nonblock(chars).ord
rescue Exception
sleep 0.01
end
end
default
end
def read_nbs
ords = []
while ord = read_nb
ords << ord
end
ords
end
def get_mouse
case ord = read_nb
when 32, 36, 40, 48, # mouse-down / shift / cmd / ctrl
35, 39, 43, 51 # mouse-up / shift / cmd / ctrl
x = read_nb - 33
y = read_nb - 33
{ :event => (ord % 2 == 0 ? :click : :release),
:x => x, :y => y, :shift => ord >= 36 }
when 96, 100, 104, 112, # scroll-up / shift / cmd / ctrl
97, 101, 105, 113 # scroll-down / shift / cmd / ctrl
read_nb(2)
{ :event => :scroll, :diff => (ord % 2 == 0 ? -1 : 1), :shift => ord >= 100 }
else
# e.g. 40, 43, 104, 105
read_nb(2)
nil
end
end
def get_input actions
@tty ||= IO.open(IO.sysopen('/dev/tty'), 'r')
if pending = @pending
@pending = nil
return pending
end
str = ''
while true
ord =
if str.empty?
@tty.getc.ord
else
begin
ord = @tty.read_nonblock(1).ord
if (nb = num_unicode_bytes(ord)) > 1
ords = [ord]
(nb - 1).times do |_|
ords << @tty.read_nonblock(1).ord
end
# UTF-8 TODO Ruby 1.8
ords.pack('C*').force_encoding('UTF-8')
else
ord
end
rescue Exception
return str
end
end
ord =
case read_nb(1, :esc)
when 91
case read_nb(1, nil)
when 68 then ctrl(:b)
when 67 then ctrl(:f)
when 66 then ctrl(:j)
when 65 then ctrl(:k)
when 90 then :stab
when 50 then read_nb; :ins
when 51 then read_nb; :del
when 53 then read_nb; :pgup
when 54 then read_nb; :pgdn
when 49
case read_nbs
when [59, 50, 68] then ctrl(:a)
when [59, 50, 67] then ctrl(:e)
when [126] then ctrl(:a)
end
when 52 then read_nb; ctrl(:e)
when 72 then ctrl(:a)
when 70 then ctrl(:e)
when 77
get_mouse
end
when 'b', 98 then :alt_b
when 'f', 102 then :alt_f
when :esc then :esc
else next
end if ord == 27
return ord if ord.nil? || ord.is_a?(Hash)
if actions.has_key?(ord)
if str.empty?
return ord
else
@pending = ord
return str
end
else
unless ord.is_a? String
ord = [ord].pack('U*')
end
str << ord if ord =~ /[[:print:]]/
end
end
end
class MouseEvent
DOUBLE_CLICK_INTERVAL = 0.5
attr_reader :v
def initialize v = nil
@c = 0
@v = v
@t = Time.at 0
end
def v= v
@c = (@v == v && within?) ? @c + 1 : 0
@v = v
@t = Time.now
end
def double? v
@c == 1 && @v == v && within?
end
def within?
(Time.now - @t) < DOUBLE_CLICK_INTERVAL
end
end
def start_loop
got = nil
begin
input = @query.get.dup
cursor = input.length
input = @query.get.dup
cursor = input.length
yanked = ''
mouse_event = MouseEvent.new
backword = proc {
cursor = (input[0, cursor].rindex(/\s\S/) || -1) + 1
}
@@ -737,16 +1001,22 @@ class FZF
got = pick
exit 0
},
ctrl(:u) => proc { input = input[cursor..-1]; cursor = 0 },
ctrl(:u) => proc {
yanked = input[0...cursor] if cursor > 0
input = input[cursor..-1]
cursor = 0
},
ctrl(:a) => proc { cursor = 0; nil },
ctrl(:e) => proc { cursor = input.length; nil },
ctrl(:j) => proc { vselect { |v| v - 1 } },
ctrl(:k) => proc { vselect { |v| v + 1 } },
ctrl(:j) => proc { vselect { |v| v - (@reverse ? -1 : 1) } },
ctrl(:k) => proc { vselect { |v| v + (@reverse ? -1 : 1) } },
ctrl(:w) => proc {
pcursor = cursor
backword.call
yanked = input[cursor...pcursor] if pcursor > cursor
input = input[0...cursor] + input[pcursor..-1]
},
ctrl(:y) => proc { actions[:default].call yanked },
ctrl(:h) => proc { input[cursor -= 1] = '' if cursor > 0 },
ctrl(:i) => proc { |o|
if @multi && sel = pick
@@ -755,103 +1025,74 @@ class FZF
else
@selects[sel] = 1
end
vselect { |v|
v + case o
when :select then 0
when C::KEY_BTAB then 1
else -1
end
}
vselect { |v| v + case o
when :stab then 1
when :sclick then 0
else -1
end * (@reverse ? -1 : 1) }
end
},
ctrl(:b) => proc { cursor = [0, cursor - 1].max; nil },
ctrl(:f) => proc { cursor = [input.length, cursor + 1].min; nil },
ctrl(:l) => proc { render { C.clear; C.refresh }; update_list true },
:del => proc { input[cursor] = '' if input.length > cursor },
:pgup => proc { vselect { |_| max_items } },
:pgdn => proc { vselect { |_| 0 } },
:alt_b => proc { backword.call; nil },
:alt_f => proc {
cursor += (input[cursor..-1].index(/(\S\s)|(.$)/) || -1) + 1
nil
},
:default => proc { |val|
case val
when String
input.insert cursor, val
cursor += val.length
when Hash
event = val[:event]
case event
when :click, :release
x, y, shift = val.values_at :x, :y, :shift
if y == cursor_y
cursor = [0, [input.length, x - 2].min].max
elsif x > 1 && y <= max_items
tv = max_items - y - 1
case event
when :click
vselect { |_| tv }
actions[ctrl(:i)].call(:sclick) if shift
mouse_event.v = tv
when :release
if !shift && mouse_event.double?(tv)
actions[ctrl(:m)].call
end
end
end
when :scroll
diff, shift = val.values_at :diff, :shift
actions[ctrl(:i)].call(:sclick) if shift
actions[ctrl(diff > 0 ? :j : :k)].call
end
end
}
}
actions[C::KEY_UP] = actions[ctrl(:p)] = actions[ctrl(:k)]
actions[C::KEY_DOWN] = actions[ctrl(:n)] = actions[ctrl(:j)]
actions[C::KEY_LEFT] = actions[ctrl(:b)]
actions[C::KEY_RIGHT] = actions[ctrl(:f)]
actions[C::KEY_BTAB] = actions[:select] = actions[ctrl(:i)]
actions[C::KEY_BACKSPACE] = actions[127] = actions[ctrl(:h)]
actions[ctrl(:p)] = actions[ctrl(:k)]
actions[ctrl(:n)] = actions[ctrl(:j)]
actions[:stab] = actions[ctrl(:i)]
actions[127] = actions[ctrl(:h)]
actions[ctrl(:q)] = actions[ctrl(:g)] = actions[ctrl(:c)] = actions[:esc]
emit(:key) { [@query.get, cursor] } unless @query.empty?
pmv = nil
while true
@cursor_x.set cursor
render { print_input }
C.stdscr.timeout = -1
ch = C.getch
if key = get_input(actions)
upd = actions.fetch(key, actions[:default]).call(key)
case ch
when C::KEY_MOUSE
if m = C.getmouse
st = m.bstate
if test_mouse(st, C::BUTTON1_PRESSED, C::BUTTON1_RELEASED)
if m.y == cursor_y
# TODO Wide-characters
cursor = [0, [input.length, m.x - 2].min].max
elsif m.x > 1 && m.y <= max_items
vselect { |v|
tv = max_items - m.y - 1
if test_mouse(st, C::BUTTON1_RELEASED)
if test_mouse(st, C::BUTTON_SHIFT)
ch = :select
elsif pmv == tv
ch = ctrl(:m)
end
pmv = tv
end
tv
}
end
elsif test_mouse(st, 0x8000000, C::BUTTON2_PRESSED)
ch = C::KEY_DOWN
elsif test_mouse(st, C::BUTTON4_PRESSED)
ch = C::KEY_UP
end
end
when 27
C.stdscr.timeout = 0
ch =
case ch2 = C.getch
when 'b' then :alt_b
when 'f' then :alt_f
when nil then :esc
else
ch2
end
# Dispatch key event
emit(:key) { [@query.set(input.dup), cursor] } if upd
end
upd = actions.fetch(ch, proc { |ch|
if ch.is_a?(Fixnum)
# Ruby 1.8
if (ch.chr rescue '') =~ /[[:print:]]/
ch = ch.chr
elsif (nch = num_unicode_bytes(ch)) > 1
chs = [ch]
(nch - 1).times do |i|
chs << C.getch
end
# UTF-8 TODO Ruby 1.8
ch = chs.pack('C*').force_encoding('UTF-8')
end
end
if ch.is_a?(String) && ch =~ /[[:print:]]/
input.insert cursor, ch
cursor += 1
end
}).call(ch)
# Dispatch key event
emit(:key) { [@query.set(input.dup), cursor] } if upd
end
ensure
C.close_screen
@@ -867,10 +1108,58 @@ class FZF
end
end
class Matcher
class MatchData
def initialize n
@n = n
end
def offset _
@n
end
end
def initialize nth, delim
@nth = nth && nth.map { |n| n > 0 ? n - 1 : n }
@delim = delim
@tokens_cache = {}
end
def tokenize str
@tokens_cache[str] ||=
unless @delim
# AWK default
prefix_length = str[/^\s+/].length rescue 0
[prefix_length, (str.strip.scan(/\S+\s*/) rescue [])]
else
prefix_length = 0
[prefix_length, (str.scan(@delim) rescue [])]
end
end
def do_match str, pat
if @nth
prefix_length, tokens = tokenize str
@nth.each do |n|
if (token = tokens[n]) && (md = token.match(pat) rescue nil)
prefix_length += (tokens[0...n] || []).join.length
offset = md.offset(0).map { |o| o + prefix_length }
return MatchData.new(offset)
end
end
nil
else
str.match(pat) rescue nil
end
end
end
class FuzzyMatcher < Matcher
attr_reader :caches, :rxflag
def initialize rxflag
def initialize rxflag, nth = nil, delim = nil
super nth, delim
@caches = Hash.new { |h, k| h[k] = {} }
@regexp = {}
@rxflag = rxflag
@@ -914,16 +1203,17 @@ class FZF
cache[q] ||= (partial_cache ?
partial_cache.map { |e| e.first } : list).map { |line|
# Ignore errors: e.g. invalid byte sequence in UTF-8
md = line.match(regexp) rescue nil
md = do_match(line, regexp)
md && [line, [md.offset(0)]]
}.compact
end
end
class ExtendedFuzzyMatcher < FuzzyMatcher
def initialize rxflag
super
def initialize rxflag, mode = :fuzzy, nth = nil, delim = nil
super rxflag, nth, delim
@regexps = {}
@mode = mode
end
def empty? q
@@ -946,8 +1236,11 @@ class FZF
when /^\^(.*)\$$/
Regexp.new('^' << sanitize(Regexp.escape($1)) << '$', rxflag_for(w))
when /^'/
w.length > 1 ?
Regexp.new(sanitize(Regexp.escape(w[1..-1])), rxflag_for(w)) : nil
if @mode == :fuzzy && w.length > 1
exact_regex w[1..-1]
elsif @mode == :exact
exact_regex w
end
when /^\^/
w.length > 1 ?
Regexp.new('^' << sanitize(Regexp.escape(w[1..-1])), rxflag_for(w)) : nil
@@ -955,11 +1248,15 @@ class FZF
w.length > 1 ?
Regexp.new(sanitize(Regexp.escape(w[0..-2])) << '$', rxflag_for(w)) : nil
else
fuzzy_regex w
@mode == :fuzzy ? fuzzy_regex(w) : exact_regex(w)
end, invert ]
}.select { |pair| pair.first }
end
def exact_regex w
Regexp.new(sanitize(Regexp.escape(w)), rxflag_for(w))
end
def match list, q, prefix, suffix
regexps = parse q
# Look for prefix cache
@@ -976,7 +1273,7 @@ class FZF
offsets = []
regexps.all? { |pair|
regexp, invert = pair
md = line.match(regexp) rescue nil
md = do_match(line, regexp)
if md && !invert
offsets << md.offset(0)
elsif !md && invert

View File

@@ -1,7 +1,7 @@
# coding: utf-8
Gem::Specification.new do |spec|
spec.name = 'fzf'
spec.version = '0.7.0'
spec.version = '0.8.4'
spec.authors = ['Junegunn Choi']
spec.email = ['junegunn.c@gmail.com']
spec.description = %q{Fuzzy finder for your shell}
@@ -13,5 +13,5 @@ Gem::Specification.new do |spec|
spec.files = %w[fzf.gemspec]
spec.executables = 'fzf'
spec.add_runtime_dependency 'curses', '~> 1.0.0'
spec.extensions += ['ext/mkrf_conf.rb']
end

252
install
View File

@@ -12,10 +12,9 @@ if [ $? -ne 0 ]; then
fi
# System ruby is preferred
curses_check="begin; require 'curses'; rescue Exception; exit 1; end"
system_ruby=/usr/bin/ruby
if [ -x $system_ruby -a $system_ruby != "$ruby" ]; then
$system_ruby --disable-gems -e "$curses_check" 2> /dev/null
$system_ruby --disable-gems -rcurses -e0 2> /dev/null
[ $? -eq 0 ] && ruby=$system_ruby
fi
@@ -23,16 +22,19 @@ echo "OK ($ruby)"
# Curses-support
echo -n "Checking Curses support ... "
"$ruby" -e "$curses_check"
"$ruby" -rcurses -e0 2> /dev/null
if [ $? -eq 0 ]; then
echo "OK"
else
echo "Not found"
echo "Installing 'curses' gem ... "
/usr/bin/env gem install curses
/usr/bin/env gem install curses -v 1.0.0 --user-install
if [ $? -ne 0 ]; then
echo
echo "Failed to install 'curses' gem."
echo "Try installing it as root: sudo gem install curses"
if [[ $(uname -r) =~ 'ARCH' ]]; then
echo "Make sure that base-devel package group is installed."
fi
exit 1
fi
fi
@@ -42,7 +44,7 @@ echo -n "Checking Ruby version ... "
"$ruby" -e 'exit RUBY_VERSION >= "1.9"'
if [ $? -eq 0 ]; then
echo ">= 1.9"
"$ruby" --disable-gems -e "$curses_check"
"$ruby" --disable-gems -rcurses -e0 2> /dev/null
if [ $? -eq 0 ]; then
fzf_cmd="$ruby --disable-gems $fzf_base/fzf"
else
@@ -70,7 +72,7 @@ for shell in bash zsh; do
echo -n "Generate ~/.fzf.$shell ... "
src=~/.fzf.${shell}
fzf_completion="source $fzf_base/fzf-completion.${shell}"
fzf_completion="[[ \$- =~ i ]] && source $fzf_base/fzf-completion.${shell}"
if [ $auto_completion -ne 0 ]; then
fzf_completion="# $fzf_completion"
fi
@@ -92,51 +94,120 @@ EOF
if [ $key_bindings -eq 0 ]; then
if [ $shell = bash ]; then
cat >> $src << "EOF"
cat >> $src << "EOFZF"
# Key bindings
# ------------
# Required to refresh the prompt after fzf
bind '"\er": redraw-current-line'
# CTRL-T - Paste the selected file path into the command line
__fsel() {
find * -path '*/\.*' -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | fzf -m | while read item; do
printf '%q ' "$item"
done
echo
}
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"'
# CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": " \C-e\C-u$(history | fzf +s | sed \"s/ *[0-9]* *//\")\e\C-e\er"'
if [[ $- =~ i ]]; then
EOF
__fsel_tmux() {
local height
height=${FZF_TMUX_HEIGHT:-40%}
if [[ $height =~ %$ ]]; then
height="-p ${height%\%}"
else
height="-l $height"
fi
tmux split-window $height "bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'"
}
__fcd() {
local dir
dir=$(find ${1:-*} -path '*/\.*' -prune -o -type d -print 2> /dev/null | fzf +m) && printf 'cd %q' "$dir"
}
__use_tmux=0
[ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1
if [ -z "$(set -o | grep '^vi.*on')" ]; then
# Required to refresh the prompt after fzf
bind '"\er": redraw-current-line'
# CTRL-T - Paste the selected file path into the command line
if [ $__use_tmux -eq 1 ]; then
bind '"\C-t": " \C-u \C-a\C-k$(__fsel_tmux)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"'
else
bind '"\C-t": " \C-u \C-a\C-k$(__fsel)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"'
fi
# CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s | sed \"s/ *[0-9]* *//\")\e\C-e\er"'
# ALT-C - cd into the selected directory
bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"'
else
bind '"\C-x\C-e": shell-expand-line'
bind '"\C-x\C-r": redraw-current-line'
# CTRL-T - Paste the selected file path into the command line
# - FIXME: Selected items are attached to the end regardless of cursor position
if [ $__use_tmux -eq 1 ]; then
bind '"\C-t": "\e$a \eddi$(__fsel_tmux)\C-x\C-e\e0P$xa"'
else
bind '"\C-t": "\e$a \eddi$(__fsel)\C-x\C-e\e0Px$a \C-x\C-r\exa "'
fi
# CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"'
# ALT-C - cd into the selected directory
bind '"\ec": "\eddi$(__fcd)\C-x\C-e\C-x\C-r\C-m"'
fi
unset __use_tmux
fi
EOFZF
else
cat >> $src << "EOF"
cat >> $src << "EOFZF"
# Key bindings
# ------------
# CTRL-T - Paste the selected file path(s) into the command line
fzf-file-widget() {
local FILES
local IFS="
"
FILES=($(
find * -path '*/\.*' -prune \
__fsel() {
set -o nonomatch
find * -path '*/\.*' -prune \
-o -type f -print \
-o -type l -print 2> /dev/null | fzf -m))
unset IFS
FILES=$FILES:q
LBUFFER="${LBUFFER%% #} $FILES"
zle redisplay
-o -type d -print \
-o -type l -print 2> /dev/null | fzf -m | while read item; do
printf '%q ' "$item"
done
echo
}
if [[ $- =~ i ]]; then
if [ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ]; then
fzf-file-widget() {
local height
height=${FZF_TMUX_HEIGHT:-40%}
if [[ $height =~ %$ ]]; then
height="-p ${height%\%}"
else
height="-l $height"
fi
tmux split-window $height "zsh -c 'source ~/.fzf.zsh; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'"
}
else
fzf-file-widget() {
LBUFFER="${LBUFFER%% #}$(__fsel)"
zle redisplay
}
fi
zle -N fzf-file-widget
bindkey '^T' fzf-file-widget
# ALT-C - cd into the selected directory
fzf-cd-widget() {
cd "${$(find * -path '*/\.*' -prune \
cd "${$(set -o nonomatch; find * -path '*/\.*' -prune \
-o -type d -print 2> /dev/null | fzf):-.}"
zle reset-prompt
}
@@ -145,40 +216,137 @@ bindkey '\ec' fzf-cd-widget
# CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() {
LBUFFER=$(history | fzf +s | sed "s/ *[0-9]* *//")
LBUFFER=$(fc -l 1 | fzf +s | sed "s/ *[0-9]* *//")
zle redisplay
}
zle -N fzf-history-widget
bindkey '^R' fzf-history-widget
EOF
fi
EOFZF
fi
fi
echo "OK"
done
echo
for shell in bash zsh; do
rc=~/.${shell}rc
src="source ~/.fzf.${shell}"
# fish
has_fish=0
if [ -n "$(which fish)" ]; then
has_fish=1
echo -n "Generate ~/.config/fish/functions/fzf.fish ... "
mkdir -p ~/.config/fish/functions
cat > ~/.config/fish/functions/fzf.fish << EOFZF
function fzf
$fzf_cmd \$argv
end
EOFZF
echo "ok"
echo "Update $rc:"
echo " - $src"
if [ $(grep -F "$src" $rc | wc -l) -gt 0 ]; then
echo " - Not added (already being sourced)"
if [ $key_bindings -eq 0 ]; then
echo -n "Generate ~/.config/fish/functions/fzf_key_bindings.fish ... "
cat > ~/.config/fish/functions/fzf_key_bindings.fish << "EOFZF"
function fzf_key_bindings
function __fzf_select
find * -path '*/\.*' -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | fzf -m | while read item
echo -n (echo -n "$item" | sed 's/ /\\\\ /g')' '
end
echo
end
function __fzf_ctrl_t
if [ -n "$TMUX_PANE" -a "$FZF_TMUX" != "0" ]
tmux split-window (__fzf_tmux_height) "fish -c 'fzf_key_bindings; __fzf_ctrl_t_tmux \\$TMUX_PANE'"
else
__fzf_select > $TMPDIR/fzf.result
and commandline -i (cat $TMPDIR/fzf.result)
rm -f $TMPDIR/fzf.result
end
end
function __fzf_ctrl_t_tmux
__fzf_select > $TMPDIR/fzf.result
and tmux send-keys -t $argv[1] (cat $TMPDIR/fzf.result)
rm -f $TMPDIR/fzf.result
end
function __fzf_ctrl_r
if history | fzf +s +m > $TMPDIR/fzf.result
commandline (cat $TMPDIR/fzf.result)
else
commandline -f repaint
end
rm -f $TMPDIR/fzf.result
end
function __fzf_alt_c
find * -path '*/\.*' -prune -o -type d -print 2> /dev/null | fzf +m > $TMPDIR/fzf.result
if [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ]
cd (cat $TMPDIR/fzf.result)
end
commandline -f repaint
rm -f $TMPDIR/fzf.result
end
function __fzf_tmux_height
if set -q FZF_TMUX_HEIGHT
set height $FZF_TMUX_HEIGHT
else
set height 40%
end
if echo $height | grep -q -E '%$'
echo "-p "(echo $height | sed 's/%$//')
else
echo "-l $height"
end
set -e height
end
bind \ct '__fzf_ctrl_t'
bind \cr '__fzf_ctrl_r'
bind \ec '__fzf_alt_c'
end
EOFZF
echo "ok"
fi
fi
append_line() {
echo "Update $2:"
echo " - $1"
[ -f "$2" ] || touch "$2"
line=$(grep -nF "$1" "$2" | sed 's/:.*//')
if [ -n "$line" ]; then
echo " - Already exists (line #$line)"
else
echo $src >> $rc
echo "$1" >> "$2"
echo " - Added"
fi
echo
}
echo
for shell in bash zsh; do
append_line "source ~/.fzf.${shell}" ~/.${shell}rc
done
if [ $key_bindings -eq 0 -a $has_fish -eq 1 ]; then
bind_file=~/.config/fish/functions/fish_user_key_bindings.fish
append_line "fzf_key_bindings" "$bind_file"
fi
cat << EOF
Finished. Reload your .bashrc or .zshrc.
Finished. Restart your shell or reload config file.
source ~/.bashrc # bash
source ~/.zshrc # zsh
EOF
[ $has_fish -eq 1 ] && echo " fzf_key_bindings # fish"; cat << EOF
To uninstall fzf, simply remove the added line.
To uninstall fzf, simply remove the added lines.
For more information, see: https://github.com/junegunn/fzf
EOF

View File

@@ -1,4 +1,4 @@
" Copyright (c) 2013 Junegunn Choi
" Copyright (c) 2014 Junegunn Choi
"
" MIT License
"
@@ -21,39 +21,201 @@
" 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:exec = expand('<sfile>:h:h').'/fzf'
let s:min_tmux_width = 10
let s:min_tmux_height = 3
let s:default_tmux_height = '40%'
let s:cpo_save = &cpo
set cpo&vim
call system('type fzf')
if v:shell_error
let s:fzf_rb = expand('<sfile>:h:h').'/fzf'
if executable(s:fzf_rb)
let s:exec = s:fzf_rb
else
echoerr 'fzf executable not found'
finish
endif
else
let s:exec = 'fzf'
endif
function! s:tmux_enabled()
if exists('s:tmux')
return s:tmux
endif
let s:tmux = 0
if exists('$TMUX')
let output = system('tmux -V')
let s:tmux = !v:shell_error && output >= 'tmux 1.7'
endif
return s:tmux
endfunction
function! s:shellesc(arg)
return '"'.substitute(a:arg, '"', '\\"', 'g').'"'
endfunction
function! s:escape(path)
return substitute(a:path, ' ', '\\ ', 'g')
endfunction
function! fzf#run(command, ...)
let cwd = getcwd()
try
let args = copy(a:000)
if len(args) > 0 && isdirectory(expand(args[-1]))
let dir = remove(args, -1)
execute 'chdir '.s:escape(dir)
function! fzf#run(...) abort
if has('gui_running')
echohl Error
echo 'GVim is not supported'
return []
endif
let dict = exists('a:1') ? a:1 : {}
let temps = { 'result': tempname() }
let optstr = get(dict, 'options', '')
if has_key(dict, 'source')
let source = dict.source
let type = type(source)
if type == 1
let prefix = source.'|'
elseif type == 3
let temps.input = tempname()
call writefile(source, temps.input)
let prefix = 'cat '.s:shellesc(temps.input).'|'
else
throw 'Invalid source type'
endif
let argstr = join(args)
let tf = tempname()
let prefix = exists('g:fzf_source') ? g:fzf_source.'|' : ''
let fzf = executable(s:exec) ? s:exec : 'fzf'
let options = empty(argstr) ? get(g:, 'fzf_options', '') : argstr
execute "silent !".prefix.fzf.' '.options." > ".tf
if !v:shell_error
for line in readfile(tf)
if !empty(line)
execute a:command.' '.s:escape(line)
else
let prefix = ''
endif
let command = prefix.s:exec.' '.optstr.' > '.temps.result
if s:tmux_enabled() && s:tmux_splittable(dict)
return s:execute_tmux(dict, command, temps)
else
return s:execute(dict, command, temps)
endif
endfunction
function! s:tmux_splittable(dict)
return
\ min([&columns, get(a:dict, 'tmux_width', 0)]) >= s:min_tmux_width ||
\ min([&lines, get(a:dict, 'tmux_height', get(a:dict, 'tmux', 0))]) >= s:min_tmux_height
endfunction
function! s:pushd(dict)
if has_key(a:dict, 'dir')
let a:dict.prev_dir = getcwd()
execute 'chdir '.s:escape(a:dict.dir)
endif
endfunction
function! s:popd(dict)
if has_key(a:dict, 'prev_dir')
execute 'chdir '.s:escape(remove(a:dict, 'prev_dir'))
endif
endfunction
function! s:execute(dict, command, temps)
call s:pushd(a:dict)
silent !clear
execute 'silent !'.a:command
redraw!
if v:shell_error
return []
else
return s:callback(a:dict, a:temps, 0)
endif
endfunction
function! s:execute_tmux(dict, command, temps)
if has_key(a:dict, 'dir')
let command = 'cd '.s:escape(a:dict.dir).' && '.a:command
else
let command = a:command
endif
let splitopt = '-v'
if has_key(a:dict, 'tmux_width')
let splitopt = '-h'
let size = a:dict.tmux_width
else
let size = get(a:dict, 'tmux_height', get(a:dict, 'tmux'))
endif
if type(size) == 1 && size =~ '%$'
let sizeopt = '-p '.size[0:-2]
else
let sizeopt = '-l '.size
endif
let s:pane = substitute(
\ system(
\ printf(
\ 'tmux split-window %s %s -P -F "#{pane_id}" %s',
\ splitopt, sizeopt, s:shellesc(command))), '\n', '', 'g')
let s:dict = a:dict
let s:temps = a:temps
augroup fzf_tmux
autocmd!
autocmd VimResized * nested call s:tmux_check()
augroup END
endfunction
function! s:tmux_check()
let panes = split(system('tmux list-panes -a -F "#{pane_id}"'), '\n')
if index(panes, s:pane) < 0
augroup fzf_tmux
autocmd!
augroup END
call s:callback(s:dict, s:temps, 1)
redraw
endif
endfunction
function! s:callback(dict, temps, cd)
if !filereadable(a:temps.result)
let lines = []
else
if a:cd | call s:pushd(a:dict) | endif
let lines = readfile(a:temps.result)
if has_key(a:dict, 'sink')
for line in lines
if type(a:dict.sink) == 2
call a:dict.sink(line)
else
execute a:dict.sink.' '.s:escape(line)
endif
endfor
endif
finally
execute 'chdir '.s:escape(cwd)
redraw!
endif
for tf in values(a:temps)
silent! call delete(tf)
endtry
endfor
call s:popd(a:dict)
return lines
endfunction
command! -nargs=* -complete=dir FZF call fzf#run('silent e', <f-args>)
function! s:cmd(bang, ...) abort
let args = copy(a:000)
let opts = {}
if len(args) > 0 && isdirectory(expand(args[-1]))
let opts.dir = remove(args, -1)
endif
if !a:bang
let opts.tmux = get(g:, 'fzf_tmux_height', s:default_tmux_height)
endif
call fzf#run(extend({ 'sink': 'e', 'options': join(args) }, opts))
endfunction
command! -nargs=* -complete=dir -bang FZF call s:cmd('<bang>' == '!', <f-args>)
let &cpo = s:cpo_save
unlet s:cpo_save

37
test/fzf.vader Normal file
View File

@@ -0,0 +1,37 @@
Execute (Setup):
let g:dir = fnamemodify(g:vader_file, ':p:h')
Log 'Test directory: ' . g:dir
Execute (fzf#run with dir option):
let result = fzf#run({ 'options': '--filter=vdr', 'dir': g:dir })
AssertEqual ['fzf.vader'], result
let result = sort(fzf#run({ 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_fzf.rb'], result
Execute (fzf#run with Funcref command):
let g:ret = []
function! g:proc(e)
call add(g:ret, a:e)
endfunction
let result = sort(fzf#run({ 'sink': function('g:proc'), 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_fzf.rb'], result
AssertEqual ['fzf.vader', 'test_fzf.rb'], sort(g:ret)
Execute (fzf#run with string source):
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))
AssertEqual ['hi'], result
Execute (fzf#run with list source):
let result = sort(fzf#run({ 'source': ['hello', 'world'], 'options': '-f e' }))
AssertEqual ['hello'], result
let result = sort(fzf#run({ 'source': ['hello', 'world'], 'options': '-f o' }))
AssertEqual ['hello', 'world'], result
Execute (fzf#run with string source):
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))
AssertEqual ['hi'], result
Execute (Cleanup):
unlet g:dir
Restore

View File

@@ -1,13 +1,15 @@
#!/usr/bin/env ruby
# encoding: utf-8
require 'curses'
require 'timeout'
require 'stringio'
require 'minitest/autorun'
$LOAD_PATH.unshift File.expand_path('../..', __FILE__)
ENV['FZF_EXECUTABLE'] = '0'
load 'fzf'
class TestFZF < MiniTest::Unit::TestCase
def setup
ENV.delete 'FZF_DEFAULT_SORT'
ENV.delete 'FZF_DEFAULT_OPTS'
@@ -21,6 +23,16 @@ class TestFZF < MiniTest::Unit::TestCase
assert_equal true, fzf.color
assert_equal nil, fzf.rxflag
assert_equal true, fzf.mouse
assert_equal nil, fzf.nth
assert_equal true, fzf.color
assert_equal false, fzf.black
assert_equal true, fzf.ansi256
assert_equal '', fzf.query.get
assert_equal false, fzf.select1
assert_equal false, fzf.exit0
assert_equal nil, fzf.filter
assert_equal nil, fzf.extended
assert_equal false, fzf.reverse
end
def test_environment_variables
@@ -28,61 +40,101 @@ class TestFZF < MiniTest::Unit::TestCase
ENV['FZF_DEFAULT_SORT'] = '20000'
fzf = FZF.new []
assert_equal 20000, fzf.sort
assert_equal nil, fzf.nth
ENV['FZF_DEFAULT_OPTS'] = '-x -m -s 10000 -q " hello world " +c --no-mouse'
ENV['FZF_DEFAULT_OPTS'] =
'-x -m -s 10000 -q " hello world " +c +2 --select-1 -0 ' +
'--no-mouse -f "goodbye world" --black --nth=3,-1,2 --reverse'
fzf = FZF.new []
assert_equal 10000, fzf.sort
assert_equal ' hello world ',
fzf.query.get
assert_equal true, fzf.extended
assert_equal 'goodbye world',
fzf.filter
assert_equal :fuzzy, fzf.extended
assert_equal true, fzf.multi
assert_equal false, fzf.color
assert_equal false, fzf.ansi256
assert_equal true, fzf.black
assert_equal false, fzf.mouse
assert_equal true, fzf.select1
assert_equal true, fzf.exit0
assert_equal true, fzf.reverse
assert_equal [3, -1, 2], fzf.nth
end
def test_option_parser
# Long opts
fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello --extended --no-mouse]
fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello --select-1
--exit-0 --filter=howdy --extended-exact
--no-mouse --no-256 --nth=1 --reverse]
assert_equal 2000, fzf.sort
assert_equal true, fzf.multi
assert_equal false, fzf.color
assert_equal false, fzf.ansi256
assert_equal false, fzf.black
assert_equal false, fzf.mouse
assert_equal 0, fzf.rxflag
assert_equal 'hello', fzf.query.get
assert_equal true, fzf.extended
assert_equal true, fzf.select1
assert_equal true, fzf.exit0
assert_equal 'howdy', fzf.filter
assert_equal :exact, fzf.extended
assert_equal [1], fzf.nth
assert_equal true, fzf.reverse
fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello
--no-sort -i --color --no-multi]
# Long opts (left-to-right)
fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query=hello
--filter a --filter b --no-256 --black --nth -1 --nth -2
--select-1 --exit-0 --no-select-1 --no-exit-0
--no-sort -i --color --no-multi --256
--reverse --no-reverse]
assert_equal nil, fzf.sort
assert_equal false, fzf.multi
assert_equal true, fzf.color
assert_equal true, fzf.ansi256
assert_equal true, fzf.black
assert_equal true, fzf.mouse
assert_equal 1, fzf.rxflag
assert_equal 'b', fzf.filter
assert_equal 'hello', fzf.query.get
assert_equal false, fzf.extended
assert_equal false, fzf.select1
assert_equal false, fzf.exit0
assert_equal nil, fzf.extended
assert_equal [-2], fzf.nth
assert_equal false, fzf.reverse
# Short opts
fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x]
fzf = FZF.new %w[-s2000 +c -m +i -qhello -x -fhowdy +2 -n3 -1 -0]
assert_equal 2000, fzf.sort
assert_equal true, fzf.multi
assert_equal false, fzf.color
assert_equal false, fzf.ansi256
assert_equal 0, fzf.rxflag
assert_equal 'hello', fzf.query.get
assert_equal true, fzf.extended
assert_equal 'howdy', fzf.filter
assert_equal :fuzzy, fzf.extended
assert_equal [3], fzf.nth
assert_equal true, fzf.select1
assert_equal true, fzf.exit0
# Left-to-right
fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x
-s 3000 -c +m -i -q world +x]
fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x -fgoodbye +2 -n3 -n4,5
-s 3000 -c +m -i -q world +x -fworld -2 --black --no-black
-1 -0 +1 +0
]
assert_equal 3000, fzf.sort
assert_equal false, fzf.multi
assert_equal true, fzf.color
assert_equal true, fzf.ansi256
assert_equal false, fzf.black
assert_equal 1, fzf.rxflag
assert_equal 'world', fzf.query.get
assert_equal false, fzf.extended
fzf = FZF.new %w[--query hello +s -s 2000 --query=world]
assert_equal 2000, fzf.sort
assert_equal 'world', fzf.query.get
assert_equal false, fzf.select1
assert_equal false, fzf.exit0
assert_equal 'world', fzf.filter
assert_equal nil, fzf.extended
assert_equal [4, 5], fzf.nth
rescue SystemExit => e
assert false, "Exited"
end
@@ -93,6 +145,12 @@ class TestFZF < MiniTest::Unit::TestCase
fzf = FZF.new argv
end
end
assert_raises(SystemExit) do
fzf = FZF.new %w[--nth=0]
end
assert_raises(SystemExit) do
fzf = FZF.new %w[-n 0]
end
end
# FIXME Only on 1.9 or above
@@ -347,7 +405,7 @@ class TestFZF < MiniTest::Unit::TestCase
["0____1", [[0, 6]]],
["0_____1", [[0, 7]]],
["0______1", [[0, 8]]]],
FZF.new([]).sort_by_rank(matcher.match(list, '01', '', '')))
FZF.sort(matcher.match(list, '01', '', '')))
assert_equal(
[["01", [[0, 1], [1, 2]]],
@@ -358,16 +416,38 @@ class TestFZF < MiniTest::Unit::TestCase
["____0_1", [[4, 5], [6, 7]]],
["0______1", [[0, 1], [7, 8]]],
["___01___", [[3, 4], [4, 5]]]],
FZF.new([]).sort_by_rank(xmatcher.match(list, '0 1', '', '')))
FZF.sort(xmatcher.match(list, '0 1', '', '')))
assert_equal(
[["_01_", [[1, 3], [0, 4]]],
["0____1", [[0, 6], [1, 3]]],
["0_____1", [[0, 7], [1, 3]]],
["0______1", [[0, 8], [1, 3]]],
["___01___", [[3, 5], [0, 2]]],
["____0_1", [[4, 7], [0, 2]]]],
FZF.new([]).sort_by_rank(xmatcher.match(list, '01 __', '', '')))
[["_01_", [[1, 3], [0, 4]], [4, 4, "_01_"]],
["___01___", [[3, 5], [0, 2]], [4, 8, "___01___"]],
["____0_1", [[4, 7], [0, 2]], [5, 7, "____0_1"]],
["0____1", [[0, 6], [1, 3]], [6, 6, "0____1"]],
["0_____1", [[0, 7], [1, 3]], [7, 7, "0_____1"]],
["0______1", [[0, 8], [1, 3]], [8, 8, "0______1"]]],
FZF.sort(xmatcher.match(list, '01 __', '', '')).map { |tuple|
tuple << FZF.rank(tuple)
}
)
end
def test_extended_exact_mode
exact = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :exact
fuzzy = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy
list = %w[
extended-exact-mode-not-fuzzy
extended'-fuzzy-mode
]
assert_equal 2, fuzzy.match(list, 'extended', '', '').length
assert_equal 2, fuzzy.match(list, 'mode extended', '', '').length
assert_equal 2, fuzzy.match(list, 'xtndd', '', '').length
assert_equal 2, fuzzy.match(list, "'-fuzzy", '', '').length
assert_equal 2, exact.match(list, 'extended', '', '').length
assert_equal 2, exact.match(list, 'mode extended', '', '').length
assert_equal 0, exact.match(list, 'xtndd', '', '').length
assert_equal 1, exact.match(list, "'-fuzzy", '', '').length
assert_equal 2, exact.match(list, "-fuzzy", '', '').length
end
if RUBY_PLATFORM =~ /darwin/
@@ -425,5 +505,166 @@ class TestFZF < MiniTest::Unit::TestCase
tokens = fzf.format line, 80, offsets
assert_equal [], tokens
end
def test_mouse_event
interval = FZF::MouseEvent::DOUBLE_CLICK_INTERVAL
me = FZF::MouseEvent.new nil
me.v = 10
assert_equal false, me.double?(10)
assert_equal false, me.double?(20)
me.v = 20
assert_equal false, me.double?(10)
assert_equal false, me.double?(20)
me.v = 20
assert_equal false, me.double?(10)
assert_equal true, me.double?(20)
sleep interval
assert_equal false, me.double?(20)
end
def test_nth_match
list = [
' first second third',
'fourth fifth sixth',
]
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE
assert_equal list, matcher.match(list, 'f', '', '').map(&:first)
assert_equal [
[list[0], [[2, 5]]],
[list[1], [[9, 17]]]], matcher.match(list, 'is', '', '')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2]
assert_equal [[list[1], [[8, 9]]]], matcher.match(list, 'f', '', '')
assert_equal [[list[0], [[8, 9]]]], matcher.match(list, 's', '', '')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [3]
assert_equal [[list[0], [[19, 20]]]], matcher.match(list, 'r', '', '')
# Comma-separated
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [3, 1]
assert_equal [[list[0], [[19, 20]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '')
# Ordered
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1, 3]
assert_equal [[list[0], [[3, 4]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '')
regex = FZF.build_delim_regex "\t"
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1], regex
assert_equal [[list[0], [[3, 10]]]], matcher.match(list, 're', '', '')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2], regex
assert_equal [], matcher.match(list, 'r', '', '')
assert_equal [[list[1], [[9, 17]]]], matcher.match(list, 'is', '', '')
# Negative indexing
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [-1], regex
assert_equal [[list[0], [[3, 6]]]], matcher.match(list, 'rt', '', '')
assert_equal [[list[0], [[2, 5]]], [list[1], [[9, 17]]]], matcher.match(list, 'is', '', '')
# Regex delimiter
regex = FZF.build_delim_regex "[ \t]+"
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1], regex
assert_equal [list[1]], matcher.match(list, 'f', '', '').map(&:first)
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2], regex
assert_equal [[list[0], [[1, 2]]], [list[1], [[8, 9]]]], matcher.match(list, 'f', '', '')
end
def stream_for str
StringIO.new(str).tap do |sio|
sio.instance_eval do
alias org_gets gets
def gets
org_gets.tap { |e| sleep 0.5 unless e.nil? }
end
end
end
end
def test_select_1
stream = stream_for "Hello\nWorld"
output = StringIO.new
begin
$stdout = output
FZF.new(%w[--query=ol --select-1], stream).start
rescue SystemExit => e
assert_equal 0, e.status
assert_equal 'World', output.string.chomp
ensure
$stdout = STDOUT
end
end
def test_select_1_without_query
stream = stream_for "Hello World"
output = StringIO.new
begin
$stdout = output
FZF.new(%w[--select-1], stream).start
rescue SystemExit => e
assert_equal 0, e.status
assert_equal 'Hello World', output.string.chomp
ensure
$stdout = STDOUT
end
end
def test_select_1_ambiguity
stream = stream_for "Hello\nWorld"
begin
Timeout::timeout(3) do
FZF.new(%w[--query=o --select-1], stream).start
end
flunk 'Should not reach here'
rescue Exception => e
Curses.close_screen
assert_instance_of Timeout::Error, e
end
end
def test_exit_0
stream = stream_for "Hello\nWorld"
output = StringIO.new
begin
$stdout = output
FZF.new(%w[--query=zz --exit-0], stream).start
rescue SystemExit => e
assert_equal 0, e.status
assert_equal '', output.string
ensure
$stdout = STDOUT
end
end
def test_exit_0_without_query
stream = stream_for ""
output = StringIO.new
begin
$stdout = output
FZF.new(%w[--exit-0], stream).start
rescue SystemExit => e
assert_equal 0, e.status
assert_equal '', output.string
ensure
$stdout = STDOUT
end
end
def test_ranking_overlap_match_regions
list = [
'1 3 4 2',
'1 2 3 4'
]
assert_equal [
['1 2 3 4', [[0, 13], [16, 22]]],
['1 3 4 2', [[0, 24], [12, 17]]],
], FZF.sort(FZF::ExtendedFuzzyMatcher.new(nil).match(list, '12 34', '', ''))
end
end