Compare commits

..

30 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
24 changed files with 787 additions and 362 deletions

View File

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

197
README.md
View File

@@ -14,16 +14,17 @@ Installation
fzf project consists of the followings: fzf project consists of the followings:
- `fzf` executable - `fzf` executable
- `fzf-tmux` script for launching fzf in a tmux pane
- Shell extensions - Shell extensions
- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish) - Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
- Fuzzy auto-completion (bash) - Fuzzy auto-completion (bash only)
You can [download fzf executable][bin] alone, but it's recommended that you You can [download fzf executable][bin] alone, but it's recommended that you
install the extra stuff using the attached install script. install the extra stuff using the attached install script.
[bin]: https://github.com/junegunn/fzf-bin/releases [bin]: https://github.com/junegunn/fzf-bin/releases
### Using git (recommended) #### Using git (recommended)
Clone this repository and run Clone this repository and run
[install](https://github.com/junegunn/fzf/blob/master/install) script. [install](https://github.com/junegunn/fzf/blob/master/install) script.
@@ -33,7 +34,7 @@ git clone https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install ~/.fzf/install
``` ```
### Using curl #### Using curl
In case you don't have git installed: In case you don't have git installed:
@@ -44,7 +45,7 @@ curl -L https://github.com/junegunn/fzf/archive/master.tar.gz |
~/.fzf/install ~/.fzf/install
``` ```
### Using Homebrew #### Using Homebrew
On OS X, you can use [Homebrew](http://brew.sh/) to install fzf. On OS X, you can use [Homebrew](http://brew.sh/) to install fzf.
@@ -52,10 +53,10 @@ On OS X, you can use [Homebrew](http://brew.sh/) to install fzf.
brew install fzf brew install fzf
# Install shell extensions - this should be done whenever fzf is updated # Install shell extensions - this should be done whenever fzf is updated
/usr/local/Cellar/fzf/$(fzf --version)/install $(brew info fzf | grep /install)
``` ```
### Install as Vim plugin #### Install as Vim plugin
Once you have cloned the repository, add the following line to your .vimrc. Once you have cloned the repository, add the following line to your .vimrc.
@@ -63,7 +64,8 @@ Once you have cloned the repository, add the following line to your .vimrc.
set rtp+=~/.fzf set rtp+=~/.fzf
``` ```
Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf: Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf
(recommended):
```vim ```vim
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' } Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' }
@@ -72,47 +74,6 @@ Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' }
Usage 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 Sort the result
+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
--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 will launch curses-based finder, read the list from STDIN, and write the fzf will launch curses-based finder, read the list from STDIN, and write the
selected item to STDOUT. selected item to STDOUT.
@@ -128,34 +89,16 @@ files excluding hidden ones. (You can override the default command with
vim $(fzf) vim $(fzf)
``` ```
If you want to preserve the exact sequence of the input, provide `--no-sort` (or #### Using the finder
`+s`) option.
```sh - `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P)` to move cursor up and down
history | fzf +s - `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 #### Extended-search mode
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
With `-x` or `--extended` option, fzf will start in "extended-search mode". With `-x` or `--extended` option, fzf will start in "extended-search mode".
@@ -174,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 If you don't need fuzzy matching and do not wish to "quote" every word, start
fzf with `-e` or `--extended-exact` option. fzf with `-e` or `--extended-exact` option.
Useful examples Examples
--------------- --------
```sh Many useful examples can be found on [the wiki
# fe [FUZZY PATTERN] - Open the selected file with the default editor page](https://github.com/junegunn/fzf/wiki/examples). Feel free to add your
# - Bypass fuzzy finder if there's only one match (--select-1) own as well.
# - 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).
Key bindings for command line Key bindings for command line
----------------------------- -----------------------------
@@ -231,13 +146,27 @@ If you want to customize the key bindings, consider editing the
installer-generated source code: `~/.fzf.bash`, `~/.fzf.zsh`, and installer-generated source code: `~/.fzf.bash`, `~/.fzf.zsh`, and
`~/.config/fish/functions/fzf_key_bindings.fish`. `~/.config/fish/functions/fzf_key_bindings.fish`.
Auto-completion `fzf-tmux` script
--------------- -----------------
Disclaimer: *Auto-completion feature is currently experimental, it can change [fzf-tmux](bin/fzf-tmux) is a bash script that opens fzf in a tmux pane.
over time*
### 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 #### Files and directories
@@ -306,18 +235,12 @@ export FZF_COMPLETION_TRIGGER='~~'
export FZF_COMPLETION_OPTS='+c -x' export FZF_COMPLETION_OPTS='+c -x'
``` ```
### zsh
TODO :smiley:
(Pull requests are appreciated.)
Usage as Vim plugin Usage as Vim plugin
------------------- -------------------
(Note: To use fzf in GVim, an external terminal emulator is required.) (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. If you have set up fzf for Vim, `:FZF` command will be added.
@@ -355,7 +278,7 @@ Refer to the [this wiki
page](https://github.com/junegunn/fzf/wiki/fzf-with-MacVim-and-iTerm2) to see page](https://github.com/junegunn/fzf/wiki/fzf-with-MacVim-and-iTerm2) to see
how to set up. how to set up.
### `fzf#run([options])` #### `fzf#run([options])`
For more advanced uses, you can call `fzf#run()` function which returns the list For more advanced uses, you can call `fzf#run()` function which returns the list
of the selected items. of the selected items.
@@ -363,18 +286,17 @@ of the selected items.
`fzf#run()` may take an options-dictionary: `fzf#run()` may take an options-dictionary:
| Option name | Type | Description | | Option name | Type | Description |
| --------------- | ------------- | ------------------------------------------------------------------ | | -------------------------- | ------------- | ---------------------------------------------------------------- |
| `source` | string | External command to generate input to fzf (e.g. `find .`) | | `source` | string | External command to generate input to fzf (e.g. `find .`) |
| `source` | list | Vim list as input to fzf | | `source` | list | Vim list as input to fzf |
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) | | `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
| `sink` | funcref | Reference to function to process each selected item | | `sink` | funcref | Reference to function to process each selected item |
| `options` | string | Options to fzf | | `options` | string | Options to fzf |
| `dir` | string | Working directory | | `dir` | string | Working directory |
| `tmux_width` | number/string | Use tmux vertical split with the given height (e.g. `20`, `50%`) | | `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (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) | | `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. If `sink` option is not given, `fzf#run` will simply return the list.
@@ -400,7 +322,7 @@ nnoremap <silent> <Leader>C :call fzf#run({
\ "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"), \ "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"),
\ 'sink': 'colo', \ 'sink': 'colo',
\ 'options': '+m', \ 'options': '+m',
\ 'tmux_width': 20, \ 'left': 20,
\ 'launcher': 'xterm -geometry 20x30 -e bash -ic %s' \ 'launcher': 'xterm -geometry 20x30 -e bash -ic %s'
\ })<CR> \ })<CR>
``` ```
@@ -425,18 +347,21 @@ nnoremap <silent> <Leader><Enter> :call fzf#run({
\ 'source': reverse(BufList()), \ 'source': reverse(BufList()),
\ 'sink': function('BufOpen'), \ 'sink': function('BufOpen'),
\ 'options': '+m', \ 'options': '+m',
\ 'tmux_height': '40%' \ 'down': '40%'
\ })<CR> \ })<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) - [fzf+vim+tmux](http://junegunn.kr/2014/04/fzf+vim+tmux)
Tips Tips
---- ----
### Rendering issues #### Rendering issues
If you have any rendering issues, check the followings: If you have any rendering issues, check the followings:
@@ -450,7 +375,7 @@ If you have any rendering issues, check the followings:
`FZF_DEFAULT_OPTS` for further convenience. `FZF_DEFAULT_OPTS` for further convenience.
4. If you still have problem, try `--no-256` option or even `--no-color`. 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 [ag](https://github.com/ggreer/the_silver_searcher) or
[pt](https://github.com/monochromegane/the_platinum_searcher) will do the [pt](https://github.com/monochromegane/the_platinum_searcher) will do the
@@ -467,7 +392,7 @@ export FZF_DEFAULT_COMMAND='ag -l -g ""'
fzf 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 If you're running fzf in a large git repository, `git ls-tree` can boost up the
speed of the traversal. speed of the traversal.
@@ -487,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 The supplied [fzf-tmux](bin/fzf-tmux) script should suffice in most of the
CTRL-T key binding. (Or is it?) 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 ```sh
# This is a helper function that splits the current pane to start the given # This is a helper function that splits the current pane to start the given
@@ -520,7 +447,7 @@ fzf_tmux_dir() {
bind '"\C-x\C-d": "$(fzf_tmux_dir)\e\C-e"' 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) 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 that it doesn't allow reading from STDIN in command substitution, which means
@@ -543,7 +470,7 @@ function fe
end end
``` ```
### Handling UTF-8 NFD paths on OSX #### Handling UTF-8 NFD paths on OSX
Use iconv to convert NFD paths to NFC: Use iconv to convert NFD paths to NFC:

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

View File

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

10
install
View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
version=0.9.3 version=0.9.4
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base=$(pwd)
@@ -245,7 +245,7 @@ if [ -z "$(set -o | \grep '^vi.*on')" ]; then
fi fi
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
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 # ALT-C - cd into the selected directory
bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"' bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"'
@@ -263,7 +263,7 @@ else
bind -m vi-command '"\C-t": "i\C-t"' bind -m vi-command '"\C-t": "i\C-t"'
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": "\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"' bind -m vi-command '"\C-r": "i\C-r"'
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
@@ -323,7 +323,7 @@ bindkey '\ec' fzf-cd-widget
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() { fzf-history-widget() {
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 redisplay
} }
zle -N fzf-history-widget zle -N fzf-history-widget
@@ -412,7 +412,7 @@ function fzf_key_bindings
end end
function __fzf_ctrl_r 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) and commandline (cat $TMPDIR/fzf.result)
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result rm -f $TMPDIR/fzf.result

View File

@@ -21,29 +21,35 @@
" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION " OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
let s:min_tmux_width = 10
let s:min_tmux_height = 3
let s:default_tmux_height = '40%' let s:default_tmux_height = '40%'
let s:launcher = 'xterm -e bash -ic %s' let s:launcher = 'xterm -e bash -ic %s'
let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf' let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf'
let s:fzf_rb = expand('<sfile>:h:h').'/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 let s:cpo_save = &cpo
set cpo&vim set cpo&vim
function! s:fzf_exec() function! s:fzf_exec()
if !exists('s:exec') if !exists('s:exec')
if executable(s:fzf_go)
let s:exec = s:fzf_go
else
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') call system('type fzf')
if v:shell_error if v:shell_error
let s:exec = executable(s:fzf_go) ? throw 'fzf executable not found'
\ s:fzf_go : (executable(s:fzf_rb) ? s:fzf_rb : '')
else else
let s:exec = 'fzf' let s:exec = 'fzf'
endif endif
return s:fzf_exec() endif
elseif empty(s:exec) endif
unlet s:exec return s:exec
throw 'fzf executable not found'
else else
return s:exec return s:exec
endif endif
@@ -59,7 +65,7 @@ function! s:tmux_enabled()
endif endif
let s:tmux = 0 let s:tmux = 0
if exists('$TMUX') if exists('$TMUX') && executable(s:fzf_tmux)
let output = system('tmux -V') let output = system('tmux -V')
let s:tmux = !v:shell_error && output >= 'tmux 1.7' let s:tmux = !v:shell_error && output >= 'tmux 1.7'
endif endif
@@ -74,8 +80,23 @@ function! s:escape(path)
return substitute(a:path, ' ', '\\ ', 'g') return substitute(a:path, ' ', '\\ ', 'g')
endfunction 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 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 temps = { 'result': tempname() }
let optstr = get(dict, 'options', '') let optstr = get(dict, 'options', '')
try try
@@ -99,26 +120,47 @@ function! fzf#run(...) abort
else else
let prefix = '' let prefix = ''
endif 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) return s:execute_tmux(dict, command, temps)
else else
return s:execute(dict, command, temps) return s:execute(dict, command, temps)
endif endif
endfunction 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) function! s:tmux_splittable(dict)
return return s:present(a:dict, 'up', 'down', 'left', 'right')
\ 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 endfunction
function! s:pushd(dict) function! s:pushd(dict)
if !empty(get(a:dict, 'dir', '')) if s:present(a:dict, 'dir')
let a:dict.prev_dir = getcwd() let a:dict.prev_dir = getcwd()
execute 'chdir '.s:escape(a:dict.dir) execute 'chdir '.s:escape(a:dict.dir)
return 1
endif endif
return 0
endfunction endfunction
function! s:popd(dict) function! s:popd(dict)
@@ -146,7 +188,7 @@ function! s:execute(dict, command, temps)
endif endif
return [] return []
else else
return s:callback(a:dict, a:temps, 0) return s:callback(a:dict, a:temps)
endif endif
endfunction endfunction
@@ -159,58 +201,20 @@ function! s:env_var(name)
endfunction endfunction
function! s:execute_tmux(dict, command, temps) function! s:execute_tmux(dict, command, temps)
let command = s:env_var('FZF_DEFAULT_OPTS').s:env_var('FZF_DEFAULT_COMMAND').a:command let command = a:command
if !empty(get(a:dict, 'dir', '')) 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 let command = 'cd '.s:escape(a:dict.dir).' && '.command
endif endif
let splitopt = '-v' call system(command)
if has_key(a:dict, 'tmux_width') return s:callback(a:dict, a:temps)
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 endfunction
function! s:tmux_check() function! s:callback(dict, temps)
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) if !filereadable(a:temps.result)
let lines = [] let lines = []
else else
if a:cd | call s:pushd(a:dict) | endif
let lines = readfile(a:temps.result) let lines = readfile(a:temps.result)
if has_key(a:dict, 'sink') if has_key(a:dict, 'sink')
for line in lines for line in lines
@@ -239,7 +243,7 @@ function! s:cmd(bang, ...) abort
let opts.dir = remove(args, -1) let opts.dir = remove(args, -1)
endif endif
if !a:bang 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 endif
call fzf#run(extend({ 'sink': 'e', 'options': join(args) }, opts)) call fzf#run(extend({ 'sink': 'e', 'options': join(args) }, opts))
endfunction endfunction

View File

@@ -6,7 +6,7 @@ RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git
# Install Go 1.4 # Install Go 1.4
RUN cd / && curl \ 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 tar -xz && mv go go1.4
ENV GOPATH /go ENV GOPATH /go

View File

@@ -6,7 +6,7 @@ RUN yum install -y git gcc make tar ncurses-devel
# Install Go 1.4 # Install Go 1.4
RUN cd / && curl \ 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 tar -xz && mv go go1.4
ENV GOPATH /go ENV GOPATH /go

View File

@@ -7,7 +7,7 @@ RUN apt-get update && apt-get -y upgrade && \
# Install Go 1.4 # Install Go 1.4
RUN cd / && curl \ 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 tar -xz && mv go go1.4
ENV GOPATH /go ENV GOPATH /go

View File

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

View File

@@ -85,20 +85,37 @@ func Run(options *Options) {
} }
// Reader // Reader
streamingFilter := opts.Filter != nil && opts.Sort == 0 && !opts.Tac && !opts.Sync
if !streamingFilter {
reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} reader := Reader{func(str string) { chunkList.Push(str) }, eventBox}
go reader.ReadSource() go reader.ReadSource()
}
// Matcher // Matcher
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern( return BuildPattern(
opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes) 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)
// Filtering mode // Filtering mode
if opts.Filter != nil { if opts.Filter != nil {
if opts.PrintQuery {
fmt.Println(*opts.Filter)
}
pattern := patternBuilder([]rune(*opts.Filter)) 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.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin) eventBox.WaitFor(EvtReadFin)
@@ -106,13 +123,10 @@ func Run(options *Options) {
merger, _ := matcher.scan(MatchRequest{ merger, _ := matcher.scan(MatchRequest{
chunks: snapshot, chunks: snapshot,
pattern: pattern}) pattern: pattern})
if opts.PrintQuery {
fmt.Println(*opts.Filter)
}
for i := 0; i < merger.Length(); i++ { for i := 0; i < merger.Length(); i++ {
fmt.Println(merger.Get(i).AsString()) fmt.Println(merger.Get(i).AsString())
} }
}
os.Exit(0) os.Exit(0)
} }

View File

@@ -87,10 +87,28 @@ func (a ByRelevance) Less(i, j int) bool {
irank := a[i].Rank(true) irank := a[i].Rank(true)
jrank := a[j].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 { if irank.matchlen < jrank.matchlen {
return true return true
} else if irank.matchlen > jrank.matchlen { } else if irank.matchlen > jrank.matchlen {
@@ -103,8 +121,5 @@ func compareRanks(irank Rank, jrank Rank) bool {
return false return false
} }
if irank.index <= jrank.index { return (irank.index <= jrank.index) != tac
return true
}
return false
} }

View File

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

View File

@@ -21,6 +21,7 @@ type MatchRequest struct {
type Matcher struct { type Matcher struct {
patternBuilder func([]rune) *Pattern patternBuilder func([]rune) *Pattern
sort bool sort bool
tac bool
eventBox *util.EventBox eventBox *util.EventBox
reqBox *util.EventBox reqBox *util.EventBox
partitions int partitions int
@@ -38,10 +39,11 @@ const (
// NewMatcher returns a new Matcher // NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern, func NewMatcher(patternBuilder func([]rune) *Pattern,
sort bool, eventBox *util.EventBox) *Matcher { sort bool, tac bool, eventBox *util.EventBox) *Matcher {
return &Matcher{ return &Matcher{
patternBuilder: patternBuilder, patternBuilder: patternBuilder,
sort: sort, sort: sort,
tac: tac,
eventBox: eventBox, eventBox: eventBox,
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
partitions: runtime.NumCPU(), partitions: runtime.NumCPU(),
@@ -159,8 +161,12 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
countChan <- len(matches) countChan <- len(matches)
} }
if !empty && m.sort { if !empty && m.sort {
if m.tac {
sort.Sort(ByRelevanceTac(sliceMatches))
} else {
sort.Sort(ByRelevance(sliceMatches)) sort.Sort(ByRelevance(sliceMatches))
} }
}
resultChan <- partialResult{idx, sliceMatches} resultChan <- partialResult{idx, sliceMatches}
}(idx, chunks) }(idx, chunks)
} }
@@ -195,7 +201,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches 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 // Reset is called to interrupt/signal the ongoing search

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ import (
const usage = `usage: fzf [options] const usage = `usage: fzf [options]
Search Search mode
-x, --extended Extended-search mode -x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match) -e, --extended-exact Extended-search mode (exact match)
-i Case-insensitive match (default: smart-case 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) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result Search result
-s, --sort Sort the result +s, --no-sort Do not sort the result
+s, --no-sort Do not sort the result. Keep the sequence unchanged. --tac Reverse the order of the input
(e.g. 'history | fzf --tac --no-sort')
Interface Interface
-m, --multi Enable multi-select with tab/shift-tab -m, --multi Enable multi-select with tab/shift-tab
@@ -78,6 +79,7 @@ type Options struct {
WithNth []Range WithNth []Range
Delimiter *regexp.Regexp Delimiter *regexp.Regexp
Sort int Sort int
Tac bool
Multi bool Multi bool
Mouse bool Mouse bool
Color bool Color bool
@@ -102,6 +104,7 @@ func defaultOptions() *Options {
WithNth: make([]Range, 0), WithNth: make([]Range, 0),
Delimiter: nil, Delimiter: nil,
Sort: 1000, Sort: 1000,
Tac: false,
Multi: false, Multi: false,
Mouse: true, Mouse: true,
Color: true, Color: true,
@@ -212,6 +215,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Sort = optionalNumeric(allArgs, &i) opts.Sort = optionalNumeric(allArgs, &i)
case "+s", "--no-sort": case "+s", "--no-sort":
opts.Sort = 0 opts.Sort = 0
case "--tac":
opts.Tac = true
case "--no-tac":
opts.Tac = false
case "-i": case "-i":
opts.Case = CaseIgnore opts.Case = CaseIgnore
case "+i": case "+i":

View File

@@ -219,12 +219,7 @@ Loop:
} }
} }
var matches []*Item matches := p.matchChunk(space)
if p.mode == ModeFuzzy {
matches = p.fuzzyMatch(space)
} else {
matches = p.extendedMatch(space)
}
if !p.hasInvTerm { if !p.hasInvTerm {
_cache.Add(chunk, cacheKey, matches) _cache.Add(chunk, cacheKey, matches)
@@ -232,6 +227,35 @@ Loop:
return matches 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 { func dupItem(item *Item, offsets []Offset) *Item {
sort.Sort(ByOrder(offsets)) sort.Sort(ByOrder(offsets))
return &Item{ return &Item{
@@ -243,21 +267,12 @@ func dupItem(item *Item, offsets []Offset) *Item {
rank: Rank{0, 0, item.index}} rank: Rank{0, 0, item.index}}
} }
func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { func (p *Pattern) fuzzyMatch(item *Item) (int, int) {
matches := []*Item{}
for _, item := range *chunk {
input := p.prepareInput(item) input := p.prepareInput(item)
if sidx, eidx := p.iter(algo.FuzzyMatch, input, p.text); sidx >= 0 { return p.iter(algo.FuzzyMatch, input, p.text)
matches = append(matches,
dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}}))
}
}
return matches
} }
func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { func (p *Pattern) extendedMatch(item *Item) []Offset {
matches := []*Item{}
for _, item := range *chunk {
input := p.prepareInput(item) input := p.prepareInput(item)
offsets := []Offset{} offsets := []Offset{}
for _, term := range p.terms { for _, term := range p.terms {
@@ -271,11 +286,7 @@ func (p *Pattern) extendedMatch(chunk *Chunk) []*Item {
offsets = append(offsets, Offset{0, 0}) offsets = append(offsets, Offset{0, 0})
} }
} }
if len(offsets) == len(p.terms) { return offsets
matches = append(matches, dupItem(item, offsets))
}
}
return matches
} }
func (p *Pattern) prepareInput(item *Item) *Transformed { func (p *Pattern) prepareInput(item *Item) *Transformed {

View File

@@ -98,14 +98,15 @@ func TestOrigTextAndTransformed(t *testing.T) {
tokens := Tokenize(strptr("junegunn"), nil) tokens := Tokenize(strptr("junegunn"), nil)
trans := Transform(tokens, []Range{Range{1, 1}}) 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{ chunk := Chunk{
&Item{ &Item{
text: strptr("junegunn"), text: strptr("junegunn"),
origText: strptr("junegunn.choi"), origText: strptr("junegunn.choi"),
transformed: trans}, transformed: trans},
} }
matches := fun(&chunk) pattern.mode = mode
matches := pattern.matchChunk(&chunk)
if *matches[0].text != "junegunn" || *matches[0].origText != "junegunn.choi" || if *matches[0].text != "junegunn" || *matches[0].origText != "junegunn.choi" ||
matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 ||
matches[0].transformed != trans { matches[0].transformed != trans {

View File

@@ -22,7 +22,6 @@ import (
type Terminal struct { type Terminal struct {
prompt string prompt string
reverse bool reverse bool
tac bool
cx int cx int
cy int cy int
offset int offset int
@@ -85,7 +84,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := []rune(opts.Query) input := []rune(opts.Query)
return &Terminal{ return &Terminal{
prompt: opts.Prompt, prompt: opts.Prompt,
tac: opts.Sort == 0,
reverse: opts.Reverse, reverse: opts.Reverse,
cx: len(input), cx: len(input),
cy: 0, cy: 0,
@@ -148,13 +146,6 @@ func (t *Terminal) UpdateList(merger *Merger) {
t.reqBox.Set(reqList, nil) 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() { func (t *Terminal) output() {
if t.printQuery { if t.printQuery {
fmt.Println(string(t.input)) fmt.Println(string(t.input))
@@ -162,7 +153,7 @@ func (t *Terminal) output() {
if len(t.selected) == 0 { if len(t.selected) == 0 {
cnt := t.merger.Length() cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy { 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 { } else {
sels := make([]selectedItem, 0, len(t.selected)) sels := make([]selectedItem, 0, len(t.selected))
@@ -246,7 +237,7 @@ func (t *Terminal) printList() {
for i := 0; i < maxy; i++ { for i := 0; i < maxy; i++ {
t.move(i+2, 0, true) t.move(i+2, 0, true)
if i < count { 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)
} }
} }
} }
@@ -525,9 +516,8 @@ func (t *Terminal) Loop() {
} }
} }
toggle := func() { toggle := func() {
idx := t.listIndex(t.cy) if t.cy < t.merger.Length() {
if idx < t.merger.Length() { item := t.merger.Get(t.cy)
item := t.merger.Get(idx)
if _, found := t.selected[item.text]; !found { if _, found := t.selected[item.text]; !found {
var strptr *string var strptr *string
if item.origText != nil { if item.origText != nil {
@@ -650,7 +640,7 @@ func (t *Terminal) Loop() {
} else if me.Double { } else if me.Double {
// Double-click // Double-click
if my >= 2 { 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) req(reqClose)
} }
} }

View File

@@ -2,11 +2,20 @@
# encoding: utf-8 # encoding: utf-8
require 'minitest/autorun' require 'minitest/autorun'
require 'fileutils'
class NilClass class NilClass
def include? str def include? str
false false
end end
def start_with? str
false
end
def end_with? str
false
end
end end
module Temp module Temp
@@ -15,7 +24,7 @@ module Temp
waited = 0 waited = 0
while waited < 5 while waited < 5
begin begin
data = File.read(name) data = `cat #{name}`
return data unless data.empty? return data unless data.empty?
rescue rescue
sleep 0.1 sleep 0.1
@@ -30,6 +39,20 @@ module Temp
end end
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 class Tmux
include Temp include Temp
@@ -37,18 +60,33 @@ class Tmux
attr_reader :win attr_reader :win
def initialize shell = 'bash' def initialize shell = :bash
@win = go("new-window -d -P -F '#I' 'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.#{shell}'").first @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 @lines = `tput lines`.chomp.to_i
if shell == :fish
send_keys('function fish_prompt; end; clear', :Enter)
self.until { |lines| lines.empty? }
end
end end
def closed? def closed?
!go("list-window -F '#I'").include?(win) !go("list-window -F '#I'").include?(win)
end end
def close timeout = 1 def close
send_keys 'C-c', 'C-u', 'exit', :Enter send_keys 'C-c', 'C-u', 'exit', :Enter
wait(timeout) { closed? } wait { closed? }
end end
def kill def kill
@@ -56,35 +94,68 @@ class Tmux
end end
def send_keys *args 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 ' ' args = args.map { |a| %{"#{a}"} }.join ' '
go("send-keys -t #{win} #{args}") go("send-keys -t #{target} #{args}")
end end
def capture def capture opts = {}
go("capture-pane -t #{win} \\; save-buffer #{TEMPNAME}") timeout, pane = defaults(opts).values_at(:timeout, :pane)
raise "Window not found" if $?.exitstatus != 0 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 readonce.split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
end end
def until timeout = 1 def until opts = {}
wait(timeout) { yield capture } lines = nil
wait(opts) do
yield lines = capture(opts)
end
lines
end 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 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 waited = 0
until yield until yield
waited += 0.1
sleep 0.1
if waited > timeout if waited > timeout
hl = '=' * 10 hl = '=' * 10
puts hl 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(': ') puts [idx.to_s.rjust(2), line].join(': ')
end end
puts hl puts hl
raise "timeout" raise "timeout"
end end
waited += 0.1
sleep 0.1
end end
end end
@@ -93,7 +164,7 @@ private
end end
end end
class TestGoFZF < MiniTest::Unit::TestCase class TestBase < Minitest::Test
include Temp include Temp
FIN = 'FIN' FIN = 'FIN'
@@ -104,11 +175,6 @@ class TestGoFZF < MiniTest::Unit::TestCase
def setup def setup
ENV.delete 'FZF_DEFAULT_OPTS' ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_DEFAULT_COMMAND' ENV.delete 'FZF_DEFAULT_COMMAND'
@tmux = Tmux.new
end
def teardown
@tmux.kill
end end
def fzf(*opts) def fzf(*opts)
@@ -129,10 +195,22 @@ class TestGoFZF < MiniTest::Unit::TestCase
}.compact }.compact
"fzf #{opts.join ' '}" "fzf #{opts.join ' '}"
end end
end
class TestGoFZF < TestBase
def setup
super
@tmux = Tmux.new
end
def teardown
@tmux.kill
end
def test_vanilla def test_vanilla
tmux.send_keys "seq 1 100000 | #{fzf}", :Enter tmux.send_keys "seq 1 100000 | #{fzf}", :Enter
tmux.until(10) { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ } tmux.until(timeout: 20) { |lines|
lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ }
lines = tmux.capture lines = tmux.capture
assert_equal ' 2', lines[-4] assert_equal ' 2', lines[-4]
assert_equal '> 1', lines[-3] assert_equal '> 1', lines[-3]
@@ -322,5 +400,178 @@ class TestGoFZF < MiniTest::Unit::TestCase
tmux.send_keys 'C-K', :Enter tmux.send_keys 'C-K', :Enter
assert_equal ['1919'], readonce.split($/) assert_equal ['1919'], readonce.split($/)
end 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 end

View File

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