m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-15 23:03:47 -05:00

Compare commits

..

46 Commits

Author SHA1 Message Date
Junegunn Choi
49c752b1f7 [vim] up/down/left/right options to take boolean values
When 1 is given, 50% of the screen width or height will be used as the
default size of the pane.
2015-03-10 12:13:11 +09:00
Junegunn Choi
daa79a6df2 [vim] fzf#run with tmux panes can now return values to the caller
As they're made synchronous with the use of fzf-tmux script
2015-03-10 12:07:32 +09:00
Junegunn Choi
48e0c1e721 Ignore new options in legacy Ruby version 2015-03-10 02:16:32 +09:00
Junegunn Choi
12d81e212f [vim] Use fzf-tmux script for tmux integration 2015-03-10 01:41:35 +09:00
Junegunn Choi
c22e729d9c [fzf-tmux] Apply environment variables 2015-03-09 23:57:17 +09:00
Junegunn Choi
2b8a1c0d70 Update README - Homebrew instruction and fzf-tmux options 2015-03-09 23:40:43 +09:00
Junegunn Choi
e4b56b9702 Merge pull request #138 from junegunn/fzf-tmux-swap-pane
[fzf-tmux] Allow opening fzf on any position (up/down/left/right)
2015-03-09 23:28:53 +09:00
Junegunn Choi
789a474b28 [fzf-tmux] Allow opening fzf on any position (-u/-d/-l/-r)
The previous -w and -h will be synonyms for -r and -d respectively.
2015-03-09 12:49:26 +09:00
Junegunn Choi
fb2959c514 [fzf-tmux] Fix duplicate arguments to fzf
fzf-tmux -w -q q
fzf-tmux -w -- -q q
2015-03-08 16:40:48 +09:00
Junegunn Choi
62a28468a7 [fzf-tmux] Fix -- 2015-03-08 16:36:37 +09:00
Junegunn Choi
23dba99eda [fzf-tmux] Allow -w / -h without size argument 2015-03-08 15:08:27 +09:00
Junegunn Choi
5f62d224b0 Fix fzf-tmux script (bash 3.2 compatibility) 2015-03-07 10:07:36 +09:00
Junegunn Choi
6728870071 Merge pull request #136 from junegunn/fzf-tmux
Add fzf-tmux script
2015-03-07 10:01:23 +09:00
Junegunn Choi
87c71a3ea6 Increase timeout in test cases 2015-03-07 09:53:54 +09:00
Junegunn Choi
06ab399497 Improve how vim plugin finds fzf executable
This avoids the problem in which :FZF command silently fails when fzf
executable cannot be found in $PATH of the hosting tmux server.
2015-03-07 09:48:56 +09:00
Junegunn Choi
f7b52d2541 Use absolute path of fzf when splitting tmux window 2015-03-07 09:29:16 +09:00
Junegunn Choi
c111af0ed2 Use the term pane instead of split when not ambiguous
/cc @Tranquility
2015-03-07 09:08:41 +09:00
Junegunn Choi
07e2bd673e Update README 2015-03-06 18:57:36 +09:00
Junegunn Choi
e4ce64d10b Add fzf-tmux script 2015-03-06 18:51:50 +09:00
Junegunn Choi
5f3326a888 Deprecation alert 2015-03-06 13:21:55 +09:00
Junegunn Choi
1304428003 Update bash completion *for* fzf 2015-03-06 10:42:38 +09:00
Junegunn Choi
55828f389a Add test case for 7e2c18a 2015-03-04 13:13:11 +09:00
Junegunn Choi
7e2c18a1f6 Fix directory completion matching regular files
Related: #135
2015-03-04 13:03:54 +09:00
Junegunn Choi
79c147ed78 Fix #135 - Directory completion to append / 2015-03-04 12:59:23 +09:00
Junegunn Choi
d4b41c5e03 Merge pull request #134 from junegunn/devel
0.9.4
2015-03-01 12:35:08 +09:00
Junegunn Choi
b15a0e9650 Update CHANGELOG 2015-03-01 12:31:49 +09:00
Junegunn Choi
fe09559ee9 Build with Go 1.4.2 2015-03-01 11:49:11 +09:00
Junegunn Choi
94e8e6419f Make --filter non-blocking when --no-sort (#132)
When fzf works in filtering mode (--filter) and sorting is disabled
(--no-sort), there's no need to block until input is complete. This
commit makes fzf print the matches on-the-fly when the following
condition is met:

    --filter FILTER --no-sort [--no-tac --no-sync]

or simply:

    -f FILTER +s

This removes unnecessary delay in use cases like the following:

    fzf -f xxx +s | head -5

However, in this case, fzf processes the input lines sequentially, so it
cannot utilize multiple cores, which makes it slightly slower than the
previous mode of execution where filtering is done in parallel after the
entire input is loaded. If the user is concerned about the performance
problem, one can add --sync option to re-enable buffering.
2015-03-01 11:16:38 +09:00
Junegunn Choi
4d2d18649c Add basic test cases for shell extensions (#83)
- Key bindings for bash, zsh, and fish
- Fuzzy completion for bash (file, dir, process)
2015-03-01 03:33:56 +09:00
Junegunn Choi
c1aa5c5f33 Add --tac option and reverse display order of --no-sort
DISCLAIMER: This is a backward incompatible change
2015-02-26 01:42:15 +09:00
Junegunn Choi
4a1752d3fc 0.9.3 2015-02-18 13:19:20 +09:00
Junegunn Choi
b9b1eeffce Update Vader tests 2015-02-18 12:12:59 +09:00
Junegunn Choi
5667667d1f Add test case for --sync option 2015-02-18 12:07:54 +09:00
Junegunn Choi
f5b034095a Fix race condition in asynchronous -1 and -0 2015-02-18 00:51:44 +09:00
Junegunn Choi
95e5beb34e Update Homebrew instruction 2015-02-18 00:22:17 +09:00
Junegunn Choi
e808151c28 Make --select-1 and --exit-0 asynchronous 2015-02-18 00:08:17 +09:00
Junegunn Choi
d760b790b3 Fix typo in code 2015-02-17 19:28:10 +09:00
Junegunn Choi
1b5599972a Update installation instruction 2015-02-17 13:15:16 +09:00
Junegunn Choi
6c2ce28d0d Add --sync option 2015-02-13 12:25:19 +09:00
Junegunn Choi
ff09c275d4 Fix bash script when fzf_base contains spaces 2015-02-12 10:14:05 +09:00
Junegunn Choi
93dcd932e8 Merge pull request #123 from junegunn/fix-travis-ci
Fix Travis CI build
2015-01-29 17:44:11 +09:00
Junegunn Choi
e6a0de4094 Fix Travis CI build 2015-01-29 17:41:28 +09:00
Junegunn Choi
9f39671e65 Update README.md
Update outdated --help output
2015-01-28 01:45:34 +09:00
Junegunn Choi
423317b82a Update README.md 2015-01-28 01:18:20 +09:00
Junegunn Choi
47201c2c4d Merge pull request #122 from blueyed/improve-find-cdwidget
Improve `find` command for ALT-C: exclude proc/dev
2015-01-25 11:20:20 +09:00
Daniel Hahler
53d5d9d162 Improve find command for cd widgets: exclude proc/dev etc
When using the widget in "/", it would descend into 'dev/'.
Using '*' for the starting path would do so also with the new '-fstype'
excludes.

`cut -b3-` and `sed 1d` have been added to massage the different format
of the list.

This also uses `-L` with all calls to find, especially for the file
finders.

Ref: https://github.com/junegunn/fzf/pull/122
2015-01-25 03:09:02 +01:00
27 changed files with 951 additions and 435 deletions

View File

@@ -1,10 +1,15 @@
language: ruby
rvm:
- 2.2.0
install:
- sudo apt-get update
- sudo apt-get install -y libncurses-dev lib32ncurses5-dev
- sudo add-apt-repository -y ppa:pi-rho/dev
- sudo apt-add-repository -y ppa:fish-shell/release-2
- sudo apt-get update
- sudo apt-get install -y tmux=1.9a-1~ppa1~p
- sudo apt-get install -y zsh fish
script: |
export GOROOT=~/go1.4

60
CHANGELOG.md Normal file
View File

@@ -0,0 +1,60 @@
CHANGELOG
=========
0.9.4
-----
### New features
#### Added `--tac` option to reverse the order of the input.
One might argue that this option is unnecessary since we can already put `tac`
or `tail -r` in the command pipeline to achieve the same result. However, the
advantage of `--tac` is that it does not block until the input is complete.
### *Backward incompatible changes*
#### Changed behavior on `--no-sort`
`--no-sort` option will no longer reverse the display order within finder. You
may want to use the new `--tac` option with `--no-sort`.
```
history | fzf +s --tac
```
### Improvements
#### `--filter` will not block when sort is disabled
When fzf works in filtering mode (`--filter`) and sort is disabled
(`--no-sort`), there's no need to block until input is complete. The new
version of fzf will print the matches on-the-fly when the following condition
is met:
--filter TERM --no-sort [--no-tac --no-sync]
or simply:
-f TERM +s
This change removes unnecessary delay in the use cases like the following:
fzf -f xxx +s | head -5
However, in this case, fzf processes the lines sequentially, so it cannot
utilize multiple cores, and fzf will run slightly slower than the previous
mode of execution where filtering is done in parallel after the entire input
is loaded. If the user is concerned about this performance problem, one can
add `--sync` option to re-enable buffering.
0.9.3
-----
### New features
- Added `--sync` option for multi-staged filtering
### Improvements
- `--select-1` and `--exit-0` will start finder immediately when the condition
cannot be met

246
README.md
View File

@@ -11,6 +11,21 @@ the likes.
Installation
------------
fzf project consists of the followings:
- `fzf` executable
- `fzf-tmux` script for launching fzf in a tmux pane
- Shell extensions
- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
- Fuzzy auto-completion (bash only)
You can [download fzf executable][bin] alone, but it's recommended that you
install the extra stuff using the attached install script.
[bin]: https://github.com/junegunn/fzf-bin/releases
#### Using git (recommended)
Clone this repository and run
[install](https://github.com/junegunn/fzf/blob/master/install) script.
@@ -19,6 +34,8 @@ git clone https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install
```
#### Using curl
In case you don't have git installed:
```sh
@@ -28,17 +45,18 @@ curl -L https://github.com/junegunn/fzf/archive/master.tar.gz |
~/.fzf/install
```
The script will setup:
#### Using Homebrew
- `fzf` function (bash, zsh, fish)
- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
- Fuzzy auto-completion (bash)
On OS X, you can use [Homebrew](http://brew.sh/) to install fzf.
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.
```sh
brew install fzf
### Install as Vim plugin
# Install shell extensions - this should be done whenever fzf is updated
$(brew info fzf | grep /install)
```
#### Install as Vim plugin
Once you have cloned the repository, add the following line to your .vimrc.
@@ -46,8 +64,8 @@ Once you have cloned the repository, add the following line to your .vimrc.
set rtp+=~/.fzf
```
Or you may use [vim-plug](https://github.com/junegunn/vim-plug) to manage fzf
inside Vim:
Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf
(recommended):
```vim
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' }
@@ -56,45 +74,6 @@ Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' }
Usage
-----
```
usage: fzf [options]
Search
-x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match)
-i Case-insensitive match (default: smart-case match)
+i Case-sensitive match
-n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END])
--with-nth=N[,..] Transform the item using index expressions for search
-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
--prompt=STR Input prompt (default: '> ')
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.
--print-query Print query as the first line
Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000")
```
fzf will launch curses-based finder, read the list from STDIN, and write the
selected item to STDOUT.
@@ -110,34 +89,16 @@ files excluding hidden ones. (You can override the default command with
vim $(fzf)
```
If you want to preserve the exact sequence of the input, provide `--no-sort` (or
`+s`) option.
#### Using the finder
```sh
history | fzf +s
```
- `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P)` to move cursor up and down
- `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit
- On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items
- Emacs style key bindings
- Mouse: scroll, click, double-click; shift-click and shift-scroll on
multi-select mode
### 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.
The following readline key bindings should also work as expected.
- CTRL-A / CTRL-E
- CTRL-B / CTRL-F
- CTRL-H / CTRL-D
- 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. 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
#### Extended-search mode
With `-x` or `--extended` option, fzf will start in "extended-search mode".
@@ -156,40 +117,12 @@ such as: `^music .mp3$ sbtrkt !rmx`
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
---------------
Examples
--------
```sh
# 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() {
local dir
dir=$(find ${1:-*} -path '*/\.*' -prune \
-o -type d -print 2> /dev/null | fzf +m) &&
cd "$dir"
}
# fh - repeat history
fh() {
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}
}
```
For more examples, see [the wiki
page](https://github.com/junegunn/fzf/wiki/examples).
Many useful examples can be found on [the wiki
page](https://github.com/junegunn/fzf/wiki/examples). Feel free to add your
own as well.
Key bindings for command line
-----------------------------
@@ -213,13 +146,27 @@ 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
---------------
`fzf-tmux` script
-----------------
Disclaimer: *Auto-completion feature is currently experimental, it can change
over time*
[fzf-tmux](bin/fzf-tmux) is a bash script that opens fzf in a tmux pane.
### bash
```sh
# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
# (-[udlr]: up/down/left/right)
# select git branches in horizontal split below (15 lines)
git branch | fzf-tmux -d 15
# select multiple words in vertical split on the left (20% of screen width)
cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse
```
It will still work even when you're not on tmux, silently ignoring `-[udlr]`
options, so you can invariably use `fzf-tmux` in your scripts.
Fuzzy completion for bash
-------------------------
#### Files and directories
@@ -288,18 +235,12 @@ export FZF_COMPLETION_TRIGGER='~~'
export FZF_COMPLETION_OPTS='+c -x'
```
### zsh
TODO :smiley:
(Pull requests are appreciated.)
Usage as Vim plugin
-------------------
(Note: To use fzf in GVim, an external terminal emulator is required.)
### `:FZF[!]`
#### `:FZF[!]`
If you have set up fzf for Vim, `:FZF` command will be added.
@@ -337,26 +278,25 @@ Refer to the [this wiki
page](https://github.com/junegunn/fzf/wiki/fzf-with-MacVim-and-iTerm2) to see
how to set up.
### `fzf#run([options])`
#### `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%`) |
| `launcher` | string | External terminal emulator to start fzf with (Only used in GVim) |
| 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 |
| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) |
| `launcher` | string | External terminal emulator to start fzf with (Only used in GVim) |
#### Examples
##### Examples
If `sink` option is not given, `fzf#run` will simply return the list.
@@ -380,10 +320,10 @@ 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,
\ 'launcher': 'xterm -geometry 20x30 -e bash -ic %s'
\ 'sink': 'colo',
\ 'options': '+m',
\ 'left': 20,
\ 'launcher': 'xterm -geometry 20x30 -e bash -ic %s'
\ })<CR>
```
@@ -404,21 +344,24 @@ function! BufOpen(e)
endfunction
nnoremap <silent> <Leader><Enter> :call fzf#run({
\ 'source': reverse(BufList()),
\ 'sink': function('BufOpen'),
\ 'options': '+m',
\ 'tmux_height': '40%'
\ 'source': reverse(BufList()),
\ 'sink': function('BufOpen'),
\ 'options': '+m',
\ 'down': '40%'
\ })<CR>
```
### Articles
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
----
### Rendering issues
#### Rendering issues
If you have any rendering issues, check the followings:
@@ -432,7 +375,7 @@ If you have any rendering issues, check the followings:
`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`
#### 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
@@ -449,7 +392,7 @@ export FZF_DEFAULT_COMMAND='ag -l -g ""'
fzf
```
### `git ls-tree` for fast traversal
#### `git ls-tree` for fast traversal
If you're running fzf in a large git repository, `git ls-tree` can boost up the
speed of the traversal.
@@ -469,10 +412,12 @@ fzf() {
}
```
### Using fzf with tmux splits
#### Using fzf with tmux panes
It isn't too hard to write your own fzf-tmux combo like the default
CTRL-T key binding. (Or is it?)
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
@@ -502,7 +447,7 @@ fzf_tmux_dir() {
bind '"\C-x\C-d": "$(fzf_tmux_dir)\e\C-e"'
```
### Fish shell
#### Fish shell
It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362)
that it doesn't allow reading from STDIN in command substitution, which means
@@ -525,7 +470,7 @@ function fe
end
```
### Handling UTF-8 NFD paths on OSX
#### Handling UTF-8 NFD paths on OSX
Use iconv to convert NFD paths to NFC:
@@ -542,4 +487,3 @@ Author
------
Junegunn Choi

125
bin/fzf-tmux Executable file
View File

@@ -0,0 +1,125 @@
#!/usr/bin/env bash
# fzf-tmux: starts fzf in a tmux pane
# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
args=()
opt=""
skip=""
swap=""
close=""
term=""
while [ $# -gt 0 ]; do
arg="$1"
case "$arg" in
-)
term=1
;;
-w*|-h*|-d*|-u*|-r*|-l*)
if [ -n "$skip" ]; then
args+=("$1")
shift
continue
fi
if [[ "$arg" =~ ^.[lrw] ]]; then
opt="-h"
if [[ "$arg" =~ ^.l ]]; then
opt="$opt -d"
swap="; swap-pane -D ; select-pane -L"
close="; tmux swap-pane -D"
fi
else
opt=""
if [[ "$arg" =~ ^.u ]]; then
opt="$opt -d"
swap="; swap-pane -D ; select-pane -U"
close="; tmux swap-pane -D"
fi
fi
if [ ${#arg} -gt 2 ]; then
size="${arg:2}"
else
shift
if [[ "$1" =~ ^[0-9]+%?$ ]]; then
size="$1"
else
[ -n "$1" -a "$1" != "--" ] && args+=("$1")
shift
continue
fi
fi
if [[ "$size" =~ %$ ]]; then
size=${size:0:((${#size}-1))}
if [ -n "$swap" ]; then
opt="$opt -p $(( 100 - size ))"
else
opt="$opt -p $size"
fi
else
if [ -n "$swap" ]; then
if [[ "$arg" =~ ^.l ]]; then
[ -n "$COLUMNS" ] && max=$COLUMNS || max=$(tput cols)
else
[ -n "$LINES" ] && max=$LINES || max=$(tput lines)
fi
size=$(( max - size ))
[ $size -lt 0 ] && size=0
opt="$opt -l $size"
else
opt="$opt -l $size"
fi
fi
;;
--)
# "--" can be used to separate fzf-tmux options from fzf options to
# avoid conflicts
skip=1
;;
*)
args+=("$1")
;;
esac
shift
done
if [ -z "$TMUX_PANE" ]; then
fzf "${args[@]}"
exit $?
fi
set -e
# Build arguments to fzf
[ ${#args[@]} -gt 0 ] && fzf_args=$(printf '\\"%s\\" ' "${args[@]}"; echo '')
# Clean up named pipes on exit
id=$RANDOM
fifo1=/tmp/fzf-fifo1-$id
fifo2=/tmp/fzf-fifo2-$id
fifo3=/tmp/fzf-fifo3-$id
cleanup() {
rm -f $fifo1 $fifo2 $fifo3
}
trap cleanup EXIT SIGINT SIGTERM
fail() {
>&2 echo "$1"
exit 1
}
fzf=$(which fzf 2> /dev/null) || fail "fzf executable not found"
envs="FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
mkfifo $fifo2
mkfifo $fifo3
if [ -n "$term" -o -t 0 ]; then
tmux set-window-option -q synchronize-panes off \;\
split-window $opt "$envs"' sh -c "'$fzf' '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap
else
mkfifo $fifo1
tmux set-window-option -q synchronize-panes off \;\
split-window $opt "$envs"' sh -c "'$fzf' '"$fzf_args"' < '$fifo1' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap
cat <&0 > $fifo1 &
fi
cat $fifo2
[ "$(cat $fifo3)" = '0' ]

4
fzf
View File

@@ -8,6 +8,8 @@
# /_/ /___/_/ Fuzzy finder for your shell
#
# Version: 0.8.9 (Dec 24, 2014)
# Deprecation alert:
# This script is no longer maintained. Use the new Go version.
#
# Author: Junegunn Choi
# URL: https://github.com/junegunn/fzf
@@ -198,6 +200,8 @@ class FZF
when '--no-print-query' then @print_query = false
when '-e', '--extended-exact' then @extended = :exact
when '+e', '--no-extended-exact' then @extended = nil
when '--tac', '--sync'
# XXX
else
usage 1, "illegal option: #{o}"
end

View File

@@ -14,17 +14,17 @@ _fzf_orig_completion_filter() {
}
_fzf_opts_completion() {
local cur prev opts
local cur opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="
-x --extended
-e --extended-exact
-i +i
-n --nth
-d --delimiter
-s --sort +s
+s --no-sort
--tac
-m --multi
--no-mouse
+c --no-color
@@ -36,14 +36,8 @@ _fzf_opts_completion() {
-1 --select-1
-0 --exit-0
-f --filter
--print-query"
case "${prev}" in
--sort|-s)
COMPREPLY=( $(compgen -W "$(seq 2000 1000 10000)" -- ${cur}) )
return 0
;;
esac
--print-query
--sync"
if [[ ${cur} =~ ^-|\+ ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
@@ -88,7 +82,7 @@ _fzf_path_completion() {
[ "$dir" = './' ] && dir=''
tput sc
matches=$(find -L "$dir"* $1 2> /dev/null | fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do
printf '%q ' "$item"
printf "%q$3 " "$item"
done)
matches=${matches% }
if [ -n "$matches" ]; then
@@ -103,6 +97,7 @@ _fzf_path_completion() {
[[ "$dir" =~ /$ ]] || dir="$dir"/
done
else
shift
shift
shift
_fzf_handle_dynamic_completion "$cmd" "$@"
@@ -136,19 +131,19 @@ _fzf_list_completion() {
_fzf_all_completion() {
_fzf_path_completion \
"-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \
"-m" "$@"
"-m" "" "$@"
}
_fzf_file_completion() {
_fzf_path_completion \
"-name .git -prune -o -name .svn -prune -o -type f -print -o -type l -print" \
"-m" "$@"
"-m" "" "$@"
}
_fzf_dir_completion() {
_fzf_path_completion \
"-name .git -prune -o -name .svn -prune -o -type d -print" \
"" "$@"
"" "/" "$@"
}
_fzf_kill_completion() {
@@ -219,7 +214,7 @@ fi
# Directory
for cmd in $d_cmds; do
complete -F _fzf_dir_completion -o default -o bashdefault $cmd
complete -F _fzf_dir_completion -o nospace -o plusdirs $cmd
done
# File

37
install
View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
version=0.9.2
version=0.9.4
cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd)
@@ -159,7 +159,7 @@ for shell in bash zsh; do
echo -n "Generate ~/.fzf.$shell ... "
src=~/.fzf.${shell}
fzf_completion="[[ \$- =~ i ]] && 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
@@ -202,10 +202,10 @@ EOF
# Key bindings
# ------------
__fsel() {
command find * -path '*/\.*' -prune \
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 | fzf -m | while read item; do
-o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do
printf '%q ' "$item"
done
echo
@@ -226,7 +226,8 @@ __fsel_tmux() {
__fcd() {
local dir
dir=$(command find -L ${1:-*} -path '*/\.*' -prune -o -type d -print 2> /dev/null | fzf +m) && printf 'cd %q' "$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"
}
__use_tmux=0
@@ -244,7 +245,7 @@ if [ -z "$(set -o | \grep '^vi.*on')" ]; then
fi
# CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s +m -n2..,.. | sed \"s/ *[0-9]* *//\")\e\C-e\er"'
bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. | 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"'
@@ -262,7 +263,7 @@ else
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 +m -n2..,.. | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"'
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 -m vi-command '"\C-r": "i\C-r"'
# ALT-C - cd into the selected directory
@@ -274,17 +275,16 @@ unset __use_tmux
fi
EOFZF
else
else # zsh
cat >> $src << "EOFZF"
# Key bindings
# ------------
# CTRL-T - Paste the selected file path(s) into the command line
__fsel() {
set -o nonomatch
command find * -path '*/\.*' -prune \
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 | fzf -m | while read item; do
-o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do
printf '%q ' "$item"
done
echo
@@ -314,8 +314,8 @@ bindkey '^T' fzf-file-widget
# ALT-C - cd into the selected directory
fzf-cd-widget() {
cd "${$(set -o nonomatch; command find -L * -path '*/\.*' -prune \
-o -type d -print 2> /dev/null | fzf):-.}"
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):-.}"
zle reset-prompt
}
zle -N fzf-cd-widget
@@ -323,7 +323,7 @@ bindkey '\ec' fzf-cd-widget
# CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() {
LBUFFER=$(fc -l 1 | fzf +s +m -n2..,.. | sed "s/ *[0-9*]* *//")
LBUFFER=$(fc -l 1 | fzf +s --tac +m -n2..,.. | sed "s/ *[0-9*]* *//")
zle redisplay
}
zle -N fzf-history-widget
@@ -369,14 +369,15 @@ function fzf_key_bindings
end
function __fzf_list
command find * -path '*/\.*' -prune \
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
-o -type l -print 2> /dev/null | sed 1d | cut -b3-
end
function __fzf_list_dir
command find -L * -path '*/\.*' -prune -o -type d -print 2> /dev/null
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
@@ -411,7 +412,7 @@ function fzf_key_bindings
end
function __fzf_ctrl_r
history | __fzf_reverse | fzf +s +m > $TMPDIR/fzf.result
history | __fzf_reverse | fzf +s --tac +m > $TMPDIR/fzf.result
and commandline (cat $TMPDIR/fzf.result)
commandline -f repaint
rm -f $TMPDIR/fzf.result

View File

@@ -21,29 +21,35 @@
" 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:min_tmux_width = 10
let s:min_tmux_height = 3
let s:default_tmux_height = '40%'
let s:launcher = 'xterm -e bash -ic %s'
let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf'
let s:fzf_rb = expand('<sfile>:h:h').'/fzf'
let s:fzf_tmux = expand('<sfile>:h:h').'/bin/fzf-tmux'
let s:cpo_save = &cpo
set cpo&vim
function! s:fzf_exec()
if !exists('s:exec')
call system('type fzf')
if v:shell_error
let s:exec = executable(s:fzf_go) ?
\ s:fzf_go : (executable(s:fzf_rb) ? s:fzf_rb : '')
if executable(s:fzf_go)
let s:exec = s:fzf_go
else
let s:exec = 'fzf'
let path = split(system('which fzf 2> /dev/null'), '\n')
if !v:shell_error && !empty(path)
let s:exec = path[0]
elseif executable(s:fzf_rb)
let s:exec = s:fzf_rb
else
call system('type fzf')
if v:shell_error
throw 'fzf executable not found'
else
let s:exec = 'fzf'
endif
endif
endif
return s:fzf_exec()
elseif empty(s:exec)
unlet s:exec
throw 'fzf executable not found'
return s:exec
else
return s:exec
endif
@@ -59,7 +65,7 @@ function! s:tmux_enabled()
endif
let s:tmux = 0
if exists('$TMUX')
if exists('$TMUX') && executable(s:fzf_tmux)
let output = system('tmux -V')
let s:tmux = !v:shell_error && output >= 'tmux 1.7'
endif
@@ -74,8 +80,23 @@ function! s:escape(path)
return substitute(a:path, ' ', '\\ ', 'g')
endfunction
" Upgrade legacy options
function! s:upgrade(dict)
let copy = copy(a:dict)
if has_key(copy, 'tmux')
let copy.down = remove(copy, 'tmux')
endif
if has_key(copy, 'tmux_height')
let copy.down = remove(copy, 'tmux_height')
endif
if has_key(copy, 'tmux_width')
let copy.right = remove(copy, 'tmux_width')
endif
return copy
endfunction
function! fzf#run(...) abort
let dict = exists('a:1') ? a:1 : {}
let dict = exists('a:1') ? s:upgrade(a:1) : {}
let temps = { 'result': tempname() }
let optstr = get(dict, 'options', '')
try
@@ -99,26 +120,47 @@ function! fzf#run(...) abort
else
let prefix = ''
endif
let command = prefix.fzf_exec.' '.optstr.' > '.temps.result
let split = s:tmux_enabled() && s:tmux_splittable(dict)
let command = prefix.(split ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if s:tmux_enabled() && s:tmux_splittable(dict)
if split
return s:execute_tmux(dict, command, temps)
else
return s:execute(dict, command, temps)
endif
endfunction
function! s:present(dict, ...)
for key in a:000
if !empty(get(a:dict, key, ''))
return 1
endif
endfor
return 0
endfunction
function! s:fzf_tmux(dict)
let size = ''
for o in ['up', 'down', 'left', 'right']
if s:present(a:dict, o)
let size = '-'.o[0].(a:dict[o] == 1 ? '' : a:dict[o])
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)
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
return s:present(a:dict, 'up', 'down', 'left', 'right')
endfunction
function! s:pushd(dict)
if !empty(get(a:dict, 'dir', ''))
if s:present(a:dict, 'dir')
let a:dict.prev_dir = getcwd()
execute 'chdir '.s:escape(a:dict.dir)
return 1
endif
return 0
endfunction
function! s:popd(dict)
@@ -146,7 +188,7 @@ function! s:execute(dict, command, temps)
endif
return []
else
return s:callback(a:dict, a:temps, 0)
return s:callback(a:dict, a:temps)
endif
endfunction
@@ -159,58 +201,20 @@ function! s:env_var(name)
endfunction
function! s:execute_tmux(dict, command, temps)
let command = s:env_var('FZF_DEFAULT_OPTS').s:env_var('FZF_DEFAULT_COMMAND').a:command
if !empty(get(a:dict, 'dir', ''))
let command = a:command
if s:pushd(a:dict)
" -c '#{pane_current_path}' is only available on tmux 1.9 or above
let command = 'cd '.s:escape(a:dict.dir).' && '.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
call system(command)
return s:callback(a:dict, a:temps)
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)
function! s:callback(dict, temps)
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
@@ -239,7 +243,7 @@ function! s:cmd(bang, ...) abort
let opts.dir = remove(args, -1)
endif
if !a:bang
let opts.tmux = get(g:, 'fzf_tmux_height', s:default_tmux_height)
let opts.down = get(g:, 'fzf_tmux_height', s:default_tmux_height)
endif
call fzf#run(extend({ 'sink': 'e', 'options': join(args) }, opts))
endfunction

View File

@@ -6,7 +6,7 @@ RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git
# Install Go 1.4
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4
ENV GOPATH /go

View File

@@ -6,7 +6,7 @@ RUN yum install -y git gcc make tar ncurses-devel
# Install Go 1.4
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4
ENV GOPATH /go

View File

@@ -7,7 +7,7 @@ RUN apt-get update && apt-get -y upgrade && \
# Install Go 1.4
RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4
ENV GOPATH /go

View File

@@ -5,7 +5,7 @@ import (
)
// Current version
const Version = "0.9.2"
const Version = "0.9.4"
// fzf events
const (

View File

@@ -85,61 +85,55 @@ func Run(options *Options) {
}
// Reader
reader := Reader{func(str string) { chunkList.Push(str) }, eventBox}
go reader.ReadSource()
streamingFilter := opts.Filter != nil && opts.Sort == 0 && !opts.Tac && !opts.Sync
if !streamingFilter {
reader := Reader{func(str string) { chunkList.Push(str) }, eventBox}
go reader.ReadSource()
}
// Matcher
patternBuilder := func(runes []rune) *Pattern {
return BuildPattern(
opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes)
}
matcher := NewMatcher(patternBuilder, opts.Sort > 0, eventBox)
matcher := NewMatcher(patternBuilder, opts.Sort > 0, opts.Tac, eventBox)
// Defered-interactive / Non-interactive
// --select-1 | --exit-0 | --filter
if filtering := opts.Filter != nil; filtering || opts.Select1 || opts.Exit0 {
limit := 0
var patternString string
if filtering {
patternString = *opts.Filter
} else {
if opts.Select1 || opts.Exit0 {
limit = 1
}
patternString = opts.Query
// Filtering mode
if opts.Filter != nil {
if opts.PrintQuery {
fmt.Println(*opts.Filter)
}
pattern := patternBuilder([]rune(patternString))
looping := true
eventBox.Unwatch(EvtReadNew)
for looping {
eventBox.Wait(func(events *util.Events) {
for evt := range *events {
switch evt {
case EvtReadFin:
looping = false
return
pattern := patternBuilder([]rune(*opts.Filter))
if streamingFilter {
reader := Reader{
func(str string) {
item := chunkList.trans(&str, 0)
if pattern.MatchItem(item) {
fmt.Println(*item.text)
}
}
})
}
}, eventBox}
reader.ReadSource()
} else {
eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin)
snapshot, _ := chunkList.Snapshot()
merger, cancelled := matcher.scan(MatchRequest{
chunks: snapshot,
pattern: pattern}, limit)
if !cancelled && (filtering ||
opts.Exit0 && merger.Length() == 0 ||
opts.Select1 && merger.Length() == 1) {
if opts.PrintQuery {
fmt.Println(patternString)
}
snapshot, _ := chunkList.Snapshot()
merger, _ := matcher.scan(MatchRequest{
chunks: snapshot,
pattern: pattern})
for i := 0; i < merger.Length(); i++ {
fmt.Println(merger.Get(i).AsString())
}
os.Exit(0)
}
os.Exit(0)
}
// Synchronous search
if opts.Sync {
eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin)
}
// Go interactive
@@ -147,7 +141,11 @@ func Run(options *Options) {
// Terminal I/O
terminal := NewTerminal(opts, eventBox)
deferred := opts.Select1 || opts.Exit0
go terminal.Loop()
if !deferred {
terminal.startChan <- true
}
// Event coordination
reading := true
@@ -165,11 +163,11 @@ func Run(options *Options) {
reading = reading && evt == EvtReadNew
snapshot, count := chunkList.Snapshot()
terminal.UpdateCount(count, !reading)
matcher.Reset(snapshot, terminal.Input(), false)
matcher.Reset(snapshot, terminal.Input(), false, !reading)
case EvtSearchNew:
snapshot, _ := chunkList.Snapshot()
matcher.Reset(snapshot, terminal.Input(), true)
matcher.Reset(snapshot, terminal.Input(), true, !reading)
delay = false
case EvtSearchProgress:
@@ -181,6 +179,25 @@ func Run(options *Options) {
case EvtSearchFin:
switch val := value.(type) {
case *Merger:
if deferred {
count := val.Length()
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
deferred = false
terminal.startChan <- true
} else if val.final {
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
if opts.PrintQuery {
fmt.Println(opts.Query)
}
for i := 0; i < count; i++ {
fmt.Println(val.Get(i).AsString())
}
os.Exit(0)
}
deferred = false
terminal.startChan <- true
}
}
terminal.UpdateList(val)
}
}

View File

@@ -87,10 +87,28 @@ func (a ByRelevance) Less(i, j int) bool {
irank := a[i].Rank(true)
jrank := a[j].Rank(true)
return compareRanks(irank, jrank)
return compareRanks(irank, jrank, false)
}
func compareRanks(irank Rank, jrank Rank) bool {
// ByRelevanceTac is for sorting Items
type ByRelevanceTac []*Item
func (a ByRelevanceTac) Len() int {
return len(a)
}
func (a ByRelevanceTac) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevanceTac) Less(i, j int) bool {
irank := a[i].Rank(true)
jrank := a[j].Rank(true)
return compareRanks(irank, jrank, true)
}
func compareRanks(irank Rank, jrank Rank, tac bool) bool {
if irank.matchlen < jrank.matchlen {
return true
} else if irank.matchlen > jrank.matchlen {
@@ -103,8 +121,5 @@ func compareRanks(irank Rank, jrank Rank) bool {
return false
}
if irank.index <= jrank.index {
return true
}
return false
return (irank.index <= jrank.index) != tac
}

View File

@@ -20,12 +20,19 @@ func TestOffsetSort(t *testing.T) {
}
func TestRankComparison(t *testing.T) {
if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}) ||
!compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}) ||
!compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}) ||
!compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}) {
if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, false) ||
!compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) ||
!compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, false) ||
!compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) {
t.Error("Invalid order")
}
if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, true) ||
!compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) ||
!compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, true) ||
!compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) {
t.Error("Invalid order (tac)")
}
}
// Match length, string length, index

View File

@@ -14,12 +14,14 @@ import (
type MatchRequest struct {
chunks []*Chunk
pattern *Pattern
final bool
}
// Matcher is responsible for performing search
type Matcher struct {
patternBuilder func([]rune) *Pattern
sort bool
tac bool
eventBox *util.EventBox
reqBox *util.EventBox
partitions int
@@ -37,10 +39,11 @@ const (
// NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern,
sort bool, eventBox *util.EventBox) *Matcher {
sort bool, tac bool, eventBox *util.EventBox) *Matcher {
return &Matcher{
patternBuilder: patternBuilder,
sort: sort,
tac: tac,
eventBox: eventBox,
reqBox: util.NewEventBox(),
partitions: runtime.NumCPU(),
@@ -86,11 +89,12 @@ func (m *Matcher) Loop() {
}
if !foundCache {
merger, cancelled = m.scan(request, 0)
merger, cancelled = m.scan(request)
}
if !cancelled {
m.mergerCache[patternString] = merger
merger.final = request.final
m.eventBox.Set(EvtSearchFin, merger)
}
}
@@ -121,7 +125,7 @@ type partialResult struct {
matches []*Item
}
func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
startedAt := time.Now()
numChunks := len(request.chunks)
@@ -157,7 +161,11 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
countChan <- len(matches)
}
if !empty && m.sort {
sort.Sort(ByRelevance(sliceMatches))
if m.tac {
sort.Sort(ByRelevanceTac(sliceMatches))
} else {
sort.Sort(ByRelevance(sliceMatches))
}
}
resultChan <- partialResult{idx, sliceMatches}
}(idx, chunks)
@@ -175,15 +183,11 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
count++
matchCount += matchesInChunk
if limit > 0 && matchCount > limit {
return nil, wait() // For --select-1 and --exit-0
}
if count == numChunks {
break
}
if !empty && m.reqBox.Peak(reqReset) {
if !empty && m.reqBox.Peek(reqReset) {
return nil, wait()
}
@@ -197,11 +201,11 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches
}
return NewMerger(partialResults, !empty && m.sort), false
return NewMerger(partialResults, !empty && m.sort, m.tac), false
}
// Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) {
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool) {
pattern := m.patternBuilder(patternRunes)
var event util.EventType
@@ -210,5 +214,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) {
} else {
event = reqRetry
}
m.reqBox.Set(event, MatchRequest{chunks, pattern})
m.reqBox.Set(event, MatchRequest{chunks, pattern, final})
}

View File

@@ -3,7 +3,7 @@ package fzf
import "fmt"
// Merger with no data
var EmptyMerger = NewMerger([][]*Item{}, false)
var EmptyMerger = NewMerger([][]*Item{}, false, false)
// Merger holds a set of locally sorted lists of items and provides the view of
// a single, globally-sorted list
@@ -12,16 +12,20 @@ type Merger struct {
merged []*Item
cursors []int
sorted bool
tac bool
final bool
count int
}
// NewMerger returns a new Merger
func NewMerger(lists [][]*Item, sorted bool) *Merger {
func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger {
mg := Merger{
lists: lists,
merged: []*Item{},
cursors: make([]int, len(lists)),
sorted: sorted,
tac: tac,
final: false,
count: 0}
for _, list := range mg.lists {
@@ -37,19 +41,21 @@ func (mg *Merger) Length() int {
// Get returns the pointer to the Item object indexed by the given integer
func (mg *Merger) Get(idx int) *Item {
if len(mg.lists) == 1 {
return mg.lists[0][idx]
} else if !mg.sorted {
for _, list := range mg.lists {
numItems := len(list)
if idx < numItems {
return list[idx]
}
idx -= numItems
}
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
if mg.sorted {
return mg.mergedGet(idx)
}
return mg.mergedGet(idx)
if mg.tac {
idx = mg.Length() - idx - 1
}
for _, list := range mg.lists {
numItems := len(list)
if idx < numItems {
return list[idx]
}
idx -= numItems
}
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
}
func (mg *Merger) mergedGet(idx int) *Item {
@@ -64,7 +70,7 @@ func (mg *Merger) mergedGet(idx int) *Item {
}
if cursor >= 0 {
rank := list[cursor].Rank(false)
if minIdx < 0 || compareRanks(rank, minRank) {
if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
minRank = rank
minIdx = listIdx
}

View File

@@ -62,7 +62,7 @@ func TestMergerUnsorted(t *testing.T) {
cnt := len(items)
// Not sorted: same order
mg := NewMerger(lists, false)
mg := NewMerger(lists, false, false)
assert(t, cnt == mg.Length(), "Invalid Length")
for i := 0; i < cnt; i++ {
assert(t, items[i] == mg.Get(i), "Invalid Get")
@@ -74,7 +74,7 @@ func TestMergerSorted(t *testing.T) {
cnt := len(items)
// Sorted sorted order
mg := NewMerger(lists, true)
mg := NewMerger(lists, true, false)
assert(t, cnt == mg.Length(), "Invalid Length")
sort.Sort(ByRelevance(items))
for i := 0; i < cnt; i++ {
@@ -84,7 +84,7 @@ func TestMergerSorted(t *testing.T) {
}
// Inverse order
mg2 := NewMerger(lists, true)
mg2 := NewMerger(lists, true, false)
for i := cnt - 1; i >= 0; i-- {
if items[i] != mg2.Get(i) {
t.Error("Not sorted", items[i], mg2.Get(i))

View File

@@ -11,7 +11,7 @@ import (
const usage = `usage: fzf [options]
Search
Search mode
-x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match)
-i Case-insensitive match (default: smart-case match)
@@ -23,8 +23,9 @@ const usage = `usage: fzf [options]
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result
-s, --sort Sort the result
+s, --no-sort Do not sort the result. Keep the sequence unchanged.
+s, --no-sort Do not sort the result
--tac Reverse the order of the input
(e.g. 'history | fzf --tac --no-sort')
Interface
-m, --multi Enable multi-select with tab/shift-tab
@@ -41,10 +42,12 @@ const usage = `usage: fzf [options]
-0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line
--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
FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m")
FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m')
`
@@ -76,6 +79,7 @@ type Options struct {
WithNth []Range
Delimiter *regexp.Regexp
Sort int
Tac bool
Multi bool
Mouse bool
Color bool
@@ -88,6 +92,7 @@ type Options struct {
Exit0 bool
Filter *string
PrintQuery bool
Sync bool
Version bool
}
@@ -99,6 +104,7 @@ func defaultOptions() *Options {
WithNth: make([]Range, 0),
Delimiter: nil,
Sort: 1000,
Tac: false,
Multi: false,
Mouse: true,
Color: true,
@@ -111,6 +117,7 @@ func defaultOptions() *Options {
Exit0: false,
Filter: nil,
PrintQuery: false,
Sync: false,
Version: false}
}
@@ -208,6 +215,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Sort = optionalNumeric(allArgs, &i)
case "+s", "--no-sort":
opts.Sort = 0
case "--tac":
opts.Tac = true
case "--no-tac":
opts.Tac = false
case "-i":
opts.Case = CaseIgnore
case "+i":
@@ -244,6 +255,12 @@ func parseOptions(opts *Options, allArgs []string) {
opts.PrintQuery = false
case "--prompt":
opts.Prompt = nextString(allArgs, &i, "prompt string required")
case "--sync":
opts.Sync = true
case "--no-sync":
opts.Sync = false
case "--async":
opts.Sync = false
case "--version":
opts.Version = true
default:

View File

@@ -219,12 +219,7 @@ Loop:
}
}
var matches []*Item
if p.mode == ModeFuzzy {
matches = p.fuzzyMatch(space)
} else {
matches = p.extendedMatch(space)
}
matches := p.matchChunk(space)
if !p.hasInvTerm {
_cache.Add(chunk, cacheKey, matches)
@@ -232,6 +227,35 @@ Loop:
return matches
}
func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
matches := []*Item{}
if p.mode == ModeFuzzy {
for _, item := range *chunk {
if sidx, eidx := p.fuzzyMatch(item); sidx >= 0 {
matches = append(matches,
dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}}))
}
}
} else {
for _, item := range *chunk {
if offsets := p.extendedMatch(item); len(offsets) == len(p.terms) {
matches = append(matches, dupItem(item, offsets))
}
}
}
return matches
}
// MatchItem returns true if the Item is a match
func (p *Pattern) MatchItem(item *Item) bool {
if p.mode == ModeFuzzy {
sidx, _ := p.fuzzyMatch(item)
return sidx >= 0
}
offsets := p.extendedMatch(item)
return len(offsets) == len(p.terms)
}
func dupItem(item *Item, offsets []Offset) *Item {
sort.Sort(ByOrder(offsets))
return &Item{
@@ -243,39 +267,26 @@ func dupItem(item *Item, offsets []Offset) *Item {
rank: Rank{0, 0, item.index}}
}
func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item {
matches := []*Item{}
for _, item := range *chunk {
input := p.prepareInput(item)
if sidx, eidx := p.iter(algo.FuzzyMatch, input, p.text); sidx >= 0 {
matches = append(matches,
dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}}))
}
}
return matches
func (p *Pattern) fuzzyMatch(item *Item) (int, int) {
input := p.prepareInput(item)
return p.iter(algo.FuzzyMatch, input, p.text)
}
func (p *Pattern) extendedMatch(chunk *Chunk) []*Item {
matches := []*Item{}
for _, item := range *chunk {
input := p.prepareInput(item)
offsets := []Offset{}
for _, term := range p.terms {
pfun := p.procFun[term.typ]
if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 {
if term.inv {
break
}
offsets = append(offsets, Offset{int32(sidx), int32(eidx)})
} else if term.inv {
offsets = append(offsets, Offset{0, 0})
func (p *Pattern) extendedMatch(item *Item) []Offset {
input := p.prepareInput(item)
offsets := []Offset{}
for _, term := range p.terms {
pfun := p.procFun[term.typ]
if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 {
if term.inv {
break
}
}
if len(offsets) == len(p.terms) {
matches = append(matches, dupItem(item, offsets))
offsets = append(offsets, Offset{int32(sidx), int32(eidx)})
} else if term.inv {
offsets = append(offsets, Offset{0, 0})
}
}
return matches
return offsets
}
func (p *Pattern) prepareInput(item *Item) *Transformed {

View File

@@ -98,14 +98,15 @@ func TestOrigTextAndTransformed(t *testing.T) {
tokens := Tokenize(strptr("junegunn"), nil)
trans := Transform(tokens, []Range{Range{1, 1}})
for _, fun := range []func(*Chunk) []*Item{pattern.fuzzyMatch, pattern.extendedMatch} {
for _, mode := range []Mode{ModeFuzzy, ModeExtended} {
chunk := Chunk{
&Item{
text: strptr("junegunn"),
origText: strptr("junegunn.choi"),
transformed: trans},
}
matches := fun(&chunk)
pattern.mode = mode
matches := pattern.matchChunk(&chunk)
if *matches[0].text != "junegunn" || *matches[0].origText != "junegunn.choi" ||
matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 ||
matches[0].transformed != trans {

View File

@@ -14,7 +14,7 @@ func TestReadFromCommand(t *testing.T) {
eventBox: eb}
// Check EventBox
if eb.Peak(EvtReadNew) {
if eb.Peek(EvtReadNew) {
t.Error("EvtReadNew should not be set yet")
}
@@ -25,7 +25,7 @@ func TestReadFromCommand(t *testing.T) {
}
// Check EventBox again
if !eb.Peak(EvtReadNew) {
if !eb.Peek(EvtReadNew) {
t.Error("EvtReadNew should be set yet")
}
@@ -38,7 +38,7 @@ func TestReadFromCommand(t *testing.T) {
})
// EventBox is cleared
if eb.Peak(EvtReadNew) {
if eb.Peek(EvtReadNew) {
t.Error("EvtReadNew should not be set yet")
}
@@ -50,7 +50,7 @@ func TestReadFromCommand(t *testing.T) {
}
// Check EventBox again
if eb.Peak(EvtReadNew) {
if eb.Peek(EvtReadNew) {
t.Error("Command failed. EvtReadNew should be set")
}
}

View File

@@ -22,7 +22,6 @@ import (
type Terminal struct {
prompt string
reverse bool
tac bool
cx int
cy int
offset int
@@ -40,6 +39,7 @@ type Terminal struct {
mutex sync.Mutex
initFunc func()
suppress bool
startChan chan bool
}
type selectedItem struct {
@@ -84,7 +84,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := []rune(opts.Query)
return &Terminal{
prompt: opts.Prompt,
tac: opts.Sort == 0,
reverse: opts.Reverse,
cx: len(input),
cy: 0,
@@ -99,6 +98,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
startChan: make(chan bool, 1),
initFunc: func() {
C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse)
}}
@@ -146,13 +146,6 @@ func (t *Terminal) UpdateList(merger *Merger) {
t.reqBox.Set(reqList, nil)
}
func (t *Terminal) listIndex(y int) int {
if t.tac {
return t.merger.Length() - y - 1
}
return y
}
func (t *Terminal) output() {
if t.printQuery {
fmt.Println(string(t.input))
@@ -160,7 +153,7 @@ func (t *Terminal) output() {
if len(t.selected) == 0 {
cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy {
fmt.Println(t.merger.Get(t.listIndex(t.cy)).AsString())
fmt.Println(t.merger.Get(t.cy).AsString())
}
} else {
sels := make([]selectedItem, 0, len(t.selected))
@@ -244,7 +237,7 @@ func (t *Terminal) printList() {
for i := 0; i < maxy; i++ {
t.move(i+2, 0, true)
if i < count {
t.printItem(t.merger.Get(t.listIndex(i+t.offset)), i == t.cy-t.offset)
t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset)
}
}
}
@@ -446,6 +439,7 @@ func (t *Terminal) rubout(pattern string) {
// Loop is called to start Terminal I/O
func (t *Terminal) Loop() {
<-t.startChan
{ // Late initialization
t.mutex.Lock()
t.initFunc()
@@ -522,9 +516,8 @@ func (t *Terminal) Loop() {
}
}
toggle := func() {
idx := t.listIndex(t.cy)
if idx < t.merger.Length() {
item := t.merger.Get(idx)
if t.cy < t.merger.Length() {
item := t.merger.Get(t.cy)
if _, found := t.selected[item.text]; !found {
var strptr *string
if item.origText != nil {
@@ -647,7 +640,7 @@ func (t *Terminal) Loop() {
} else if me.Double {
// Double-click
if my >= 2 {
if t.vset(my-2) && t.listIndex(t.cy) < t.merger.Length() {
if t.vset(my-2) && t.cy < t.merger.Length() {
req(reqClose)
}
}

View File

@@ -53,8 +53,8 @@ func (events *Events) Clear() {
}
}
// Peak peaks at the event box if the given event is set
func (b *EventBox) Peak(event EventType) bool {
// Peek peeks at the event box if the given event is set
func (b *EventBox) Peek(event EventType) bool {
b.cond.L.Lock()
defer b.cond.L.Unlock()
_, ok := b.events[event]
@@ -78,3 +78,18 @@ func (b *EventBox) Unwatch(events ...EventType) {
b.ignore[event] = true
}
}
func (b *EventBox) WaitFor(event EventType) {
looping := true
for looping {
b.Wait(func(events *Events) {
for evt := range *events {
switch evt {
case event:
looping = false
return
}
}
})
}
}

View File

@@ -7,16 +7,16 @@ Execute (fzf#run with dir option):
AssertEqual ['fzf.vader'], result
let result = sort(fzf#run({ 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_fzf.rb'], result
AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], result
Execute (fzf#run with Funcref command):
let g:ret = []
function! g:proc(e)
function! g:FzfTest(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)
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)
Execute (fzf#run with string source):
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))

View File

@@ -2,11 +2,20 @@
# encoding: utf-8
require 'minitest/autorun'
require 'fileutils'
class NilClass
def include? str
false
end
def start_with? str
false
end
def end_with? str
false
end
end
module Temp
@@ -15,7 +24,7 @@ module Temp
waited = 0
while waited < 5
begin
data = File.read(name)
data = `cat #{name}`
return data unless data.empty?
rescue
sleep 0.1
@@ -30,6 +39,20 @@ module Temp
end
end
class Shell
class << self
def bash
'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.bash'
end
def zsh
FileUtils.mkdir_p '/tmp/fzf-zsh'
FileUtils.cp File.expand_path('~/.fzf.zsh'), '/tmp/fzf-zsh/.zshrc'
'PS1= PROMPT_COMMAND= HISTSIZE=100 ZDOTDIR=/tmp/fzf-zsh zsh'
end
end
end
class Tmux
include Temp
@@ -37,18 +60,33 @@ class Tmux
attr_reader :win
def initialize shell = 'bash'
@win = go("new-window -d -P -F '#I' 'PS1=FIN PROMPT_COMMAND= bash --rcfile ~/.fzf.#{shell}'").first
def initialize shell = :bash
@win =
case shell
when :bash
go("new-window -d -P -F '#I' '#{Shell.bash}'").first
when :zsh
go("new-window -d -P -F '#I' '#{Shell.zsh}'").first
when :fish
go("new-window -d -P -F '#I' 'fish'").first
else
raise "Unknown shell: #{shell}"
end
@lines = `tput lines`.chomp.to_i
if shell == :fish
send_keys('function fish_prompt; end; clear', :Enter)
self.until { |lines| lines.empty? }
end
end
def closed?
!go("list-window -F '#I'").include?(win)
end
def close timeout = 1
def close
send_keys 'C-c', 'C-u', 'exit', :Enter
wait(timeout) { closed? }
wait { closed? }
end
def kill
@@ -56,35 +94,68 @@ class Tmux
end
def send_keys *args
target =
if args.last.is_a?(Hash)
hash = args.pop
go("select-window -t #{win}")
"#{win}.#{hash[:pane]}"
else
win
end
args = args.map { |a| %{"#{a}"} }.join ' '
go("send-keys -t #{win} #{args}")
go("send-keys -t #{target} #{args}")
end
def capture
go("capture-pane -t #{win} \\; save-buffer #{TEMPNAME}")
raise "Window not found" if $?.exitstatus != 0
def capture opts = {}
timeout, pane = defaults(opts).values_at(:timeout, :pane)
waited = 0
loop do
go("capture-pane -t #{win}.#{pane} \\; save-buffer #{TEMPNAME}")
break if $?.exitstatus == 0
if waited > timeout
raise "Window not found"
end
waited += 0.1
sleep 0.1
end
readonce.split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
end
def until timeout = 1
wait(timeout) { yield capture }
def until opts = {}
lines = nil
wait(opts) do
yield lines = capture(opts)
end
lines
end
def prepare
self.send_keys 'echo hello', :Enter
self.until { |lines| lines[-1].start_with?('hello') }
self.send_keys 'clear', :Enter
self.until { |lines| lines.empty? }
end
private
def wait timeout = 1
def defaults opts
{ timeout: 10, pane: 0 }.merge(opts)
end
def wait opts = {}
timeout, pane = defaults(opts).values_at(:timeout, :pane)
waited = 0
until yield
waited += 0.1
sleep 0.1
if waited > timeout
hl = '=' * 10
puts hl
capture.each_with_index do |line, idx|
capture(opts).each_with_index do |line, idx|
puts [idx.to_s.rjust(2), line].join(': ')
end
puts hl
raise "timeout"
end
waited += 0.1
sleep 0.1
end
end
@@ -93,9 +164,10 @@ private
end
end
class TestGoFZF < MiniTest::Unit::TestCase
class TestBase < Minitest::Test
include Temp
FIN = 'FIN'
TEMPNAME = '/tmp/output'
attr_reader :tmux
@@ -103,6 +175,31 @@ class TestGoFZF < MiniTest::Unit::TestCase
def setup
ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_DEFAULT_COMMAND'
end
def fzf(*opts)
fzf!(*opts) + " > #{TEMPNAME} && echo #{FIN}"
end
def fzf!(*opts)
opts = opts.map { |o|
case o
when Symbol
o = o.to_s
o.length > 1 ? "--#{o.gsub('_', '-')}" : "-#{o}"
when String, Numeric
o.to_s
else
nil
end
}.compact
"fzf #{opts.join ' '}"
end
end
class TestGoFZF < TestBase
def setup
super
@tmux = Tmux.new
end
@@ -111,8 +208,9 @@ class TestGoFZF < MiniTest::Unit::TestCase
end
def test_vanilla
tmux.send_keys "seq 1 100000 | fzf > #{TEMPNAME}", :Enter
tmux.until(10) { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ }
tmux.send_keys "seq 1 100000 | #{fzf}", :Enter
tmux.until(timeout: 20) { |lines|
lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ }
lines = tmux.capture
assert_equal ' 2', lines[-4]
assert_equal '> 1', lines[-3]
@@ -134,7 +232,7 @@ class TestGoFZF < MiniTest::Unit::TestCase
end
def test_fzf_default_command
tmux.send_keys "FZF_DEFAULT_COMMAND='echo hello' fzf > #{TEMPNAME}", :Enter
tmux.send_keys "FZF_DEFAULT_COMMAND='echo hello' #{fzf}", :Enter
tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Enter
@@ -209,7 +307,7 @@ class TestGoFZF < MiniTest::Unit::TestCase
end
def test_multi_order
tmux.send_keys "seq 1 10 | fzf --multi > #{TEMPNAME}", :Enter
tmux.send_keys "seq 1 10 | #{fzf :multi}", :Enter
tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Tab, :Up, :Up, :Tab, :Tab, :Tab, # 3, 2
@@ -217,7 +315,7 @@ class TestGoFZF < MiniTest::Unit::TestCase
:PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7
tmux.until { |lines| lines[-2].include? '(6)' }
tmux.send_keys "C-M"
tmux.until { |lines| lines[-1].include?('FIN') }
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal %w[3 2 5 6 8 7], readonce.split($/)
tmux.close
end
@@ -226,7 +324,7 @@ class TestGoFZF < MiniTest::Unit::TestCase
[true, false].each do |multi|
tmux.send_keys "(echo ' 1st 2nd 3rd/';
echo ' first second third/') |
fzf #{"--multi" if multi} -x --nth 2 --with-nth 2,-1,1 > #{TEMPNAME}",
#{fzf multi && :multi, :x, :nth, 2, :with_nth, '2,-1,1'}",
:Enter
tmux.until { |lines| lines[-2].include?('2/2') }
@@ -238,13 +336,13 @@ class TestGoFZF < MiniTest::Unit::TestCase
# However, the output must not be transformed
if multi
tmux.send_keys :BTab, :BTab, :Enter
tmux.until { |lines| lines[-1].include?('FIN') }
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.split($/)
else
tmux.send_keys '^', '3'
tmux.until { |lines| lines[-2].include?('1/2') }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?('FIN') }
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal [' 1st 2nd 3rd/'], readonce.split($/)
end
end
@@ -252,34 +350,228 @@ class TestGoFZF < MiniTest::Unit::TestCase
def test_scroll
[true, false].each do |rev|
tmux.send_keys "seq 1 100 | fzf #{'--reverse' if rev} > #{TEMPNAME}", :Enter
tmux.send_keys "seq 1 100 | #{fzf rev && :reverse}", :Enter
tmux.until { |lines| lines.include? ' 100/100' }
tmux.send_keys *110.times.map { rev ? :Down : :Up }
tmux.until { |lines| lines.include? '> 100' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?('FIN') }
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal '100', readonce.chomp
end
end
def test_select_1
tmux.send_keys "seq 1 100 | fzf --with-nth ..,.. --print-query -q 5555 -1 > #{TEMPNAME}", :Enter
tmux.until { |lines| lines[-1].include?('FIN') }
tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 5555, :'1'}", :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['5555', '55'], readonce.split($/)
end
def test_exit_0
tmux.send_keys "seq 1 100 | fzf --with-nth ..,.. --print-query -q 555555 -0 > #{TEMPNAME}", :Enter
tmux.until { |lines| lines[-1].include?('FIN') }
tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 555555, :'0'}", :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['555555'], readonce.split($/)
end
def test_select_1_exit_0_fail
[:'0', :'1', [:'1', :'0']].each do |opt|
tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter
tmux.until { |lines| lines.last =~ /^> 5/ }
tmux.send_keys :BTab, :BTab, :BTab, :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['5', '5', '15', '25'], readonce.split($/)
end
end
def test_query_unicode
tmux.send_keys "(echo abc; echo 가나다) | fzf --query 가다 > #{TEMPNAME}", :Enter
tmux.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter
tmux.until { |lines| lines.last.start_with? '>' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?('FIN') }
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['가나다'], readonce.split($/)
end
def test_sync
tmux.send_keys "seq 1 100 | #{fzf! :multi} | awk '{print \\$1 \\$1}' | #{fzf :sync}", :Enter
tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 9
tmux.until { |lines| lines[-2] == ' 19/100' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter
tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 'C-K', :Enter
assert_equal ['1919'], readonce.split($/)
end
def test_tac
tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter
assert_equal %w[1000 999 998], readonce.split($/)
end
def test_tac_sort
tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys '99'
tmux.send_keys :BTab, :BTab, :BTab, :Enter
assert_equal %w[99 999 998], readonce.split($/)
end
def test_tac_nosort
tmux.send_keys "seq 1 1000 | #{fzf :tac, :no_sort, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys '00'
tmux.send_keys :BTab, :BTab, :BTab, :Enter
assert_equal %w[1000 900 800], readonce.split($/)
end
end
module TestShell
def setup
super
end
def teardown
@tmux.kill
end
def test_ctrl_t
tmux.prepare
tmux.send_keys 'C-t', pane: 0
lines = tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, :Enter, pane: 1
tmux.until(pane: 0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c'
# FZF_TMUX=0
new_shell
tmux.send_keys 'C-t', pane: 0
lines = tmux.until(pane: 0) { |lines| lines[-1].start_with? '>' }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, :Enter, pane: 0
tmux.until(pane: 0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c', 'C-d'
end
def test_alt_c
tmux.prepare
tmux.send_keys :Escape, :c
lines = tmux.until { |lines| lines[-1].start_with? '>' }
expected = lines[-3][2..-1]
p expected
tmux.send_keys :Enter
tmux.prepare
tmux.send_keys :pwd, :Enter
tmux.until { |lines| p lines; lines[-1].end_with?(expected) }
end
def test_ctrl_r
tmux.prepare
tmux.send_keys 'echo 1st', :Enter; tmux.prepare
tmux.send_keys 'echo 2nd', :Enter; tmux.prepare
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.until { |lines| lines[-1] == 'echo 3rd' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == '3rd' }
end
end
class TestBash < TestBase
include TestShell
def new_shell
tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter
tmux.prepare
end
def setup
super
@tmux = Tmux.new :bash
end
def test_file_completion
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 :BTab, :BTab, :Enter
tmux.until { |lines|
lines[-1].include?('/tmp/fzf-test/10') &&
lines[-1].include?('/tmp/fzf-test/100')
}
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 :BTab, :BTab # BTab does not work here
tmux.send_keys 55
tmux.until { |lines| lines[-2].start_with? ' 1/' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' }
tmux.send_keys :xx
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
# Should not match regular files
tmux.send_keys :Tab
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
# Fail back to plusdirs
tmux.send_keys :BSpace, :BSpace, :BSpace
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' }
end
def test_process_completion
tmux.send_keys 'sleep 12345 &', :Enter
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 'sleep12345'
tmux.until { |lines| lines[-3].include? 'sleep 12345' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == "kill #{pid}" }
end
end
class TestZsh < TestBase
include TestShell
def new_shell
tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter
tmux.prepare
end
def setup
super
@tmux = Tmux.new :zsh
end
end
class TestFish < TestBase
include TestShell
def new_shell
tmux.send_keys 'env FZF_TMUX=0 fish', :Enter
tmux.send_keys 'function fish_prompt; end; clear', :Enter
tmux.until { |lines| lines.empty? }
end
def setup
super
@tmux = Tmux.new :fish
end
end

View File

@@ -54,7 +54,7 @@ class MockTTY
end
end
class TestRubyFZF < MiniTest::Unit::TestCase
class TestRubyFZF < Minitest::Test
def setup
ENV.delete 'FZF_DEFAULT_SORT'
ENV.delete 'FZF_DEFAULT_OPTS'