Compare commits

...

90 Commits

Author SHA1 Message Date
Junegunn Choi
1169cc8653 0.9.10 2015-04-18 10:43:40 +09:00
Junegunn Choi
f66d94c6b0 Add --color=[dark|light|16|bw] option
- dark:  the current default for 256-color terminal
- light: color scheme for 256-color terminal with light background
- 16:    the default color scheme for 16-color terminal (`+2`)
- bw:    no colors (`+c`)
2015-04-18 02:55:17 +09:00
Junegunn Choi
2fe1e28220 Improvements in performance and memory usage
I profiled fzf and it turned out that it was spending significant amount
of time repeatedly converting character arrays into Unicode codepoints.
This commit greatly improves search performance after the initial scan
by memoizing the converted results.

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

    :FZF
    <esc>
    :FZF

The main problem being that `:file [FZF]` can be used only once.
2015-04-10 22:18:46 +02:00
Junegunn Choi
622e69ff54 [vim] Neovim compatibility (#137)
Use terminal emulator of Neovim to open fzf
2015-04-10 23:23:47 +09:00
Junegunn Choi
68503d32df [vim] Code cleanup 2015-04-04 11:55:57 +09:00
Junegunn Choi
57319f8c58 [vim] Fix #177 - :FZF with relative paths 2015-04-04 09:18:04 +09:00
Junegunn Choi
dd4d465305 Update Homebrew instruction
Close #175
2015-04-03 01:22:16 +09:00
Junegunn Choi
467a22dd36 Period. 2015-03-31 22:09:04 +09:00
Junegunn Choi
50292adacb Implement --toggle-sort option (#173) 2015-03-31 22:05:16 +09:00
Junegunn Choi
84a7499ae3 Fix #172 - Print empty line when fzf with expect finished by -1 or -0 2015-03-31 20:52:16 +09:00
Junegunn Choi
39d7177bd3 [ruby] Stub out --expect option
So that it can be used with the recent Vim plugin although extra key
bindings are not available
2015-03-31 15:40:33 +09:00
Junegunn Choi
1c65139888 Update git ls-tree example (close #168) 2015-03-30 10:15:05 +09:00
Junegunn Choi
8a4db3c004 [vim] Fix #167 - :FZF with directory 2015-03-29 11:14:16 +09:00
Junegunn Choi
cef93f700b 0.9.6 2015-03-29 04:09:45 +09:00
Junegunn Choi
0a3d3460b1 Update man page 2015-03-29 04:08:37 +09:00
Junegunn Choi
d988f3fa50 Retain ANSI background color 2015-03-29 03:12:55 +09:00
Junegunn Choi
e865144ace [vim] Implement ctrlp-compatible key bindings (#139, #96, #61) 2015-03-29 03:00:32 +09:00
Junegunn Choi
2a167aa030 Implement --expect option to support simple key bindings (#163) 2015-03-29 02:59:32 +09:00
Junegunn Choi
9cfecf7f0b Fix test failure 2015-03-28 21:37:37 +09:00
Junegunn Choi
f9d6b83f5e Drop patch number and stick with M.m.p convention 2015-03-27 12:47:42 +09:00
Junegunn Choi
ce7d4a1c53 Fix #162 - Ignore \e[K 2015-03-27 12:35:06 +09:00
Junegunn Choi
9bba6bd172 Merge pull request #158 from mrap/update-macvim-link
Updates MacVim wiki link in README
2015-03-27 09:14:02 +09:00
Michael Rapadas
4ad92e3a0b Updates MacVim wiki link in README 2015-03-26 13:28:19 -07:00
Junegunn Choi
c4bf820dc3 Update man page 2015-03-26 10:31:47 +09:00
Junegunn Choi
39f43587d0 Fix typo in man page 2015-03-26 10:28:14 +09:00
Junegunn Choi
fdaa4e9b18 Append (not prepend) bin directory to PATH
Prepending can be problematic when the user install fzf using Homebrew,
execute the install script, and later upgrade fzf with Homebrew, and do
not rerun the install script. In that case, even though the homebrew
package is upgraded, the older version will still be used.
2015-03-26 03:44:18 +09:00
Junegunn Choi
91876e98cd Avoid duplicate paths in MANPATH 2015-03-26 03:26:28 +09:00
Junegunn Choi
eb8fef0031 Add man path only when the directory exists
$fzf_base/man may not exist when installed with Homebrew.
2015-03-26 03:11:08 +09:00
Junegunn Choi
87447ddd6d Add man page (#157) 2015-03-26 03:08:39 +09:00
Junegunn Choi
9d138173be Fix #155 - Empty ANSI color code to reset color state 2015-03-23 01:24:31 +09:00
Junegunn Choi
eae53576bd Update --help message 2015-03-22 21:25:46 +09:00
Junegunn Choi
f8c49effd4 Respect "boldness" of input string 2015-03-22 17:43:28 +09:00
Junegunn Choi
618706a5f5 Fix ANSI output in the presence of multibyte characters
tree -C | fzf --ansi --tac
2015-03-22 17:22:52 +09:00
Junegunn Choi
9ffcd26d50 Update CHANGELOG - 0.9.5 2015-03-22 16:21:52 +09:00
Junegunn Choi
b431e227da Code cleanup 2015-03-22 16:05:54 +09:00
Junegunn Choi
d94dfe0876 Fix #151 - reduce initial memory footprint 2015-03-19 19:59:38 +09:00
Junegunn Choi
6130026786 Bump up the version - 0.9.5 2015-03-19 19:12:22 +09:00
Junegunn Choi
a723977b9f Fix #149 - panic on empty string filter 2015-03-19 13:06:20 +09:00
Junegunn Choi
3dddbfd8fa Fix string truncation 2015-03-19 12:14:26 +09:00
Junegunn Choi
e70a2a5817 Add support for ANSI color codes 2015-03-19 01:59:14 +09:00
Junegunn Choi
d80a41bb6d Update README
Use --depth option to avoid pulling devel branches
2015-03-18 02:19:05 +09:00
Junegunn Choi
2bebd5cdb4 Update README with fzf image 2015-03-16 02:49:09 +09:00
Junegunn Choi
7bb75b0213 Update README 2015-03-15 10:57:09 +09:00
Junegunn Choi
bc2e82efc1 [vim] Suppress error message when clear command is N/A 2015-03-13 23:04:13 +09:00
Junegunn Choi
c04e8de9b0 Make sure to start tmux pane from the current directory (#143)
- fzf-tmux
- CTRL-T of bash/zsh/fish
    - fish implementation may not work if the path contains
      double-quote characters (FIXME)
2015-03-13 22:59:23 +09:00
Junegunn Choi
4977174def [fzf-mux] Remove unnecessary env var from command 2015-03-13 22:45:28 +09:00
Junegunn Choi
5eef0acea1 Merge pull request #145 from junegunn/refactor-shell-ext
Refactor shell extensions
2015-03-13 17:44:44 +09:00
Junegunn Choi
3935aa84d8 Refactor shell extensions
- Use symlinks instead of generating the full content
- Update fish_user_paths and remove ~/.config/fish/functions/fzf.fish
- Create wrapper script for fzf when Ruby version and use it instead of
  exported function not to break fzf-tmux
2015-03-13 17:41:00 +09:00
Junegunn Choi
dd6138a655 Fix #142, #144 - Improve CTRL-R for zsh 2015-03-13 01:33:01 +09:00
Junegunn Choi
68c5bea3f8 Fix install script for platforms w/o matching Go binary (#141) 2015-03-12 10:06:15 +09:00
Junegunn Choi
0f474d541d Note on upgrade 2015-03-11 15:07:34 +09:00
Junegunn Choi
c4d59aeec4 Remove legacy test code 2015-03-11 02:16:27 +09:00
Junegunn Choi
b2c423d1ff Cleanup - no more rubygems 2015-03-11 02:12:38 +09:00
46 changed files with 2091 additions and 1533 deletions

View File

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

View File

@@ -1,6 +1,97 @@
CHANGELOG CHANGELOG
========= =========
0.9.10
------
### Improvements
- Performance optimization
- Less aggressive memoization to limit memory usage
### New features
- Added color scheme for light background: `--color=light`
0.9.9
-----
### New features
- Added `--tiebreak` option (#191)
- Added `--no-hscroll` option (#193)
- Visual indication of `--toggle-sort` (#194)
0.9.8
-----
### Bug fixes
- Fixed Unicode case handling (#186)
- Fixed to terminate on RuneError (#185)
0.9.7
-----
### New features
- Added `--toggle-sort` option (#173)
- `--toggle-sort=ctrl-r` is applied to `CTRL-R` shell extension
### Bug fixes
- Fixed to print empty line if `--expect` is set and fzf is completed by
`--select-1` or `--exit-0` (#172)
- Fixed to allow comma character as an argument to `--expect` option
0.9.6
-----
### New features
#### Added `--expect` option (#163)
If you provide a comma-separated list of keys with `--expect` option, fzf will
allow you to select the match and complete the finder when any of the keys is
pressed. Additionally, fzf will print the name of the key pressed as the first
line of the output so that your script can decide what to do next based on the
information.
```sh
fzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@
```
The updated vim plugin uses this option to implement
[ctrlp](https://github.com/kien/ctrlp.vim)-compatible key bindings.
### Bug fixes
- Fixed to ignore ANSI escape code `\e[K` (#162)
0.9.5
-----
### New features
#### Added `--ansi` option (#150)
If you give `--ansi` option to fzf, fzf will interpret ANSI color codes from
the input, display the item with the ANSI colors (true colors are not
supported), and strips the codes from the output. This option is off by
default as it entails some overhead.
### Improvements
#### Reduced initial memory footprint (#151)
By removing unnecessary copy of pointers, fzf will use significantly smaller
amount of memory when it's started. The difference is hugely noticeable when
the input is extremely large. (e.g. `locate / | fzf`)
### Bug fixes
- Fixed panic on `--no-sort --filter ''` (#149)
0.9.4 0.9.4
----- -----

103
README.md
View File

@@ -1,12 +1,22 @@
fzf - Fuzzy finder for your shell <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf.png" height="170" alt="fzf - a command-line fuzzy finder"> [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) <a href="http://flattr.com/thing/3115381/junegunnfzf-on-GitHub" target="_blank"><img src="http://api.flattr.com/button/flattr-badge-large.png" alt="Flattr this" title="Flattr this" border="0" /></a>
================================= ===
fzf is a general-purpose fuzzy finder for your shell. fzf is a general-purpose command-line fuzzy finder.
![](https://raw.github.com/junegunn/i/master/fzf.gif) ![](https://raw.github.com/junegunn/i/master/fzf.gif)
It was heavily inspired by [ctrlp.vim](https://github.com/kien/ctrlp.vim) and Pros
the likes. ----
- No dependency
- Blazingly fast
- e.g. `locate / | fzf`
- Flexible layout
- Runs in fullscreen or in horizontal/vertical split using tmux
- The most comprehensive feature set
- Try `fzf --help` and be surprised
- Batteries included
- Vim/Neovim plugin, key bindings and fuzzy auto-completion
Installation Installation
------------ ------------
@@ -18,6 +28,7 @@ fzf project consists of the followings:
- 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 only) - Fuzzy auto-completion (bash only)
- Vim/Neovim plugin
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.
@@ -30,7 +41,7 @@ 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.
```sh ```sh
git clone https://github.com/junegunn/fzf.git ~/.fzf git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install ~/.fzf/install
``` ```
@@ -50,10 +61,10 @@ curl -L https://github.com/junegunn/fzf/archive/master.tar.gz |
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.
```sh ```sh
brew install fzf brew reinstall --HEAD fzf
# Install shell extensions - this should be done whenever fzf is updated # Install shell extensions
$(brew info fzf | grep /install) /usr/local/Cellar/fzf/HEAD/install
``` ```
#### Install as Vim plugin #### Install as Vim plugin
@@ -71,6 +82,16 @@ Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' } Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' }
``` ```
#### Upgrading fzf
fzf is being actively developed and you might want to upgrade it once in a
while. Please follow the instruction below depending on the installation
method.
- git: `cd ~/.fzf && git pull && ./install`
- brew: `brew reinstall --HEAD fzf`
- vim-plug: `:PlugUpdate fzf`
Usage Usage
----- -----
@@ -132,6 +153,8 @@ fish.
- `CTRL-T` - Paste the selected file path(s) into the command line - `CTRL-T` - Paste the selected file path(s) into the command line
- `CTRL-R` - Paste the selected command from history into the command line - `CTRL-R` - Paste the selected command from history into the command line
- Sort is disabled by default to respect chronological ordering
- Press `CTRL-R` again to toggle sort
- `ALT-C` - cd into the selected directory - `ALT-C` - cd into the selected directory
If you're on a tmux session, `CTRL-T` will launch fzf in a new split-window. You If you're on a tmux session, `CTRL-T` will launch fzf in a new split-window. You
@@ -253,30 +276,20 @@ If you have set up fzf for Vim, `:FZF` command will be added.
" With options " With options
:FZF --no-sort -m /tmp :FZF --no-sort -m /tmp
" Bang version starts in fullscreen instead of using tmux pane or Neovim split
:FZF!
``` ```
Note that the environment variables `FZF_DEFAULT_COMMAND` and `FZF_DEFAULT_OPTS` Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key,
also apply here. `CTRL-T`, `CTRL-X` or `CTRL-V` to open selected files in the current window,
in new tabs, in horizontal splits, or in vertical splits respectively.
If you're on a tmux session, `:FZF` will launch fzf in a new split-window whose Note that the environment variables `FZF_DEFAULT_COMMAND` and
height can be adjusted with `g:fzf_tmux_height` (default: '40%'). However, the `FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][vim-examples] for
bang version (`:FZF!`) will always start in fullscreen. customization.
In GVim, you need an external terminal emulator to start fzf with. `xterm` [vim-examples]: https://github.com/junegunn/fzf/wiki/Examples-(vim)
command is used by default, but you can customize it with `g:fzf_launcher`.
```vim
" This is the default. %s is replaced with fzf command
let g:fzf_launcher = 'xterm -e bash -ic %s'
" Use urxvt instead
let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s'
```
If you're running MacVim on OSX, I recommend you to use iTerm2 as the launcher.
Refer to the [this wiki
page](https://github.com/junegunn/fzf/wiki/fzf-with-MacVim-and-iTerm2) to see
how to set up.
#### `fzf#run([options])` #### `fzf#run([options])`
@@ -291,11 +304,16 @@ of the selected items.
| `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 |
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
| `options` | string | Options to fzf | | `options` | string | Options to fzf |
| `dir` | string | Working directory | | `dir` | string | Working directory |
| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) | | `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) |
| `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) |
| `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) |
_However on Neovim `fzf#run` is asynchronous and does not return values so you
should use `sink` or `sink*` to process the output from fzf._
##### Examples ##### 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.
@@ -332,22 +350,22 @@ handy mapping that selects an open buffer.
```vim ```vim
" List of buffers " List of buffers
function! BufList() function! s:buflist()
redir => ls redir => ls
silent ls silent ls
redir END redir END
return split(ls, '\n') return split(ls, '\n')
endfunction endfunction
function! BufOpen(e) function! s:bufopen(e)
execute 'buffer '. matchstr(a:e, '^[ 0-9]*') execute 'buffer' matchstr(a:e, '^[ 0-9]*')
endfunction endfunction
nnoremap <silent> <Leader><Enter> :call fzf#run({ nnoremap <silent> <Leader><Enter> :call fzf#run({
\ 'source': reverse(BufList()), \ 'source': reverse(<sid>buflist()),
\ 'sink': function('BufOpen'), \ 'sink': function('<sid>bufopen'),
\ 'options': '+m', \ 'options': '+m',
\ 'down': '40%' \ 'down': len(<sid>buflist()) + 2
\ })<CR> \ })<CR>
``` ```
@@ -398,18 +416,9 @@ 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.
```sh ```sh
# Copy the original fzf function to __fzf export FZF_DEFAULT_COMMAND='
declare -f __fzf > /dev/null || (git ls-tree -r --name-only HEAD ||
eval "$(echo "__fzf() {"; declare -f fzf | \grep -v '^{' | tail -n +2)" find * -name ".*" -prune -o -type f -print -o -type l -print) 2> /dev/null'
# Use git ls-tree when possible
fzf() {
if [ -n "$(git rev-parse HEAD 2> /dev/null)" ]; then
FZF_DEFAULT_COMMAND="git ls-tree -r --name-only HEAD" __fzf "$@"
else
__fzf "$@"
fi
}
``` ```
#### Using fzf with tmux panes #### Using fzf with tmux panes

View File

@@ -1,12 +0,0 @@
require "bundler/gem_tasks"
require 'rake/testtask'
Rake::TestTask.new(:test) do |test|
test.pattern = 'test/test_go.rb'
end
Rake::TestTask.new(:testall) do |test|
test.pattern = 'test/test_*.rb'
end
task :default => :test

View File

@@ -106,14 +106,18 @@ fail() {
>&2 echo "$1" >&2 echo "$1"
exit 1 exit 1
} }
fzf=$(which fzf 2> /dev/null) || fail "fzf executable not found" fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
envs="FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" [ -x "$fzf" ] || fail "fzf executable not found"
envs=""
[ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
[ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
mkfifo $fifo2 mkfifo $fifo2
mkfifo $fifo3 mkfifo $fifo3
if [ -n "$term" -o -t 0 ]; then if [ -n "$term" -o -t 0 ]; then
tmux set-window-option -q synchronize-panes off \;\ tmux set-window-option -q synchronize-panes off \;\
split-window $opt "$envs"' sh -c "'$fzf' '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap split-window $opt "cd $(printf %q "$PWD");$envs"' sh -c "'$fzf' '"$fzf_args"' > '$fifo2'; echo \$? > '$fifo3' '"$close"'"' $swap
else else
mkfifo $fifo1 mkfifo $fifo1
tmux set-window-option -q synchronize-panes off \;\ tmux set-window-option -q synchronize-panes off \;\

View File

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

17
fzf
View File

@@ -126,6 +126,7 @@ class FZF
@reverse = false @reverse = false
@prompt = '> ' @prompt = '> '
@shr_mtx = Mutex.new @shr_mtx = Mutex.new
@expect = false
@print_query = false @print_query = false
argv = argv =
@@ -200,7 +201,15 @@ 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' when '--expect'
argv.shift
@expect = true
when /^--expect=(.*)$/
@expect = true
when '--toggle-sort', '--tiebreak', '--color'
argv.shift
when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll',
/^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/
# XXX # XXX
else else
usage 1, "illegal option: #{o}" usage 1, "illegal option: #{o}"
@@ -276,10 +285,12 @@ class FZF
if loaded if loaded
if @select1 && len == 1 if @select1 && len == 1
puts @query if @print_query puts @query if @print_query
puts if @expect
burp(empty ? matches.first : matches.first.first) burp(empty ? matches.first : matches.first.first)
exit 0 exit 0
elsif @exit0 && len == 0 elsif @exit0 && len == 0
puts @query if @print_query puts @query if @print_query
puts if @expect
exit 0 exit 0
end end
end end
@@ -623,7 +634,8 @@ class FZF
def start_reader def start_reader
stream = stream =
if @source.tty? if @source.tty?
if default_command = ENV['FZF_DEFAULT_COMMAND'] default_command = ENV['FZF_DEFAULT_COMMAND']
if default_command && !default_command.empty?
IO.popen(default_command) IO.popen(default_command)
elsif !`which find`.empty? elsif !`which find`.empty?
IO.popen("find * -path '*/\\.*' -prune -o -type f -print -o -type l -print 2> /dev/null") IO.popen("find * -path '*/\\.*' -prune -o -type f -print -o -type l -print 2> /dev/null")
@@ -1152,6 +1164,7 @@ class FZF
C.close_screen C.close_screen
q, selects = geta(:@query, :@selects) q, selects = geta(:@query, :@selects)
@stdout.puts q if @print_query @stdout.puts q if @print_query
@stdout.puts if @expect
if got if got
if selects.empty? if selects.empty?
burp got burp got

View File

@@ -1,9 +0,0 @@
#!/bin/zsh
# ____ ____
# / __/___ / __/
# / /_/_ / / /_
# / __/ / /_/ __/
# /_/ /___/_/-completion.zsh
#
# TODO

View File

@@ -1,17 +0,0 @@
# coding: utf-8
Gem::Specification.new do |spec|
spec.name = 'fzf'
spec.version = '0.8.4'
spec.authors = ['Junegunn Choi']
spec.email = ['junegunn.c@gmail.com']
spec.description = %q{Fuzzy finder for your shell}
spec.summary = %q{Fuzzy finder for your shell}
spec.homepage = 'https://github.com/junegunn/fzf'
spec.license = 'MIT'
spec.bindir = '.'
spec.files = %w[fzf.gemspec]
spec.executables = 'fzf'
spec.extensions += ['ext/mkrf_conf.rb']
end

323
install
View File

@@ -1,12 +1,19 @@
#!/usr/bin/env bash #!/usr/bin/env bash
version=0.9.4 version=0.9.10
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base=$(pwd)
# If stdin is a tty, we are "interactive".
[ -t 0 ] && interactive=yes
ask() { ask() {
read -p "$1 ([y]/n) " -n 1 -r # non-interactive shell: wait for a linefeed
# interactive shell: continue after a single keypress
[ -n "$interactive" ] && read_n='-n 1' || read_n=
read -p "$1 ([y]/n) " $read_n -r
echo echo
[[ ! $REPLY =~ ^[Nn]$ ]] [[ ! $REPLY =~ ^[Nn]$ ]]
} }
@@ -77,7 +84,7 @@ case "$archi" in
Darwin\ i*86) download fzf-$version-darwin_386 ;; Darwin\ i*86) download fzf-$version-darwin_386 ;;
Linux\ x86_64) download fzf-$version-linux_amd64 ;; Linux\ x86_64) download fzf-$version-linux_amd64 ;;
Linux\ i*86) download fzf-$version-linux_386 ;; Linux\ i*86) download fzf-$version-linux_386 ;;
*) binary_available=0 ;; *) binary_available=0 binary_error=1 ;;
esac esac
cd "$fzf_base" cd "$fzf_base"
@@ -144,8 +151,18 @@ if [ -n "$binary_error" ]; then
echo "< 1.9" echo "< 1.9"
fzf_cmd="$ruby $fzf_base/fzf" fzf_cmd="$ruby $fzf_base/fzf"
fi fi
# Create fzf script
echo -n "Creating wrapper script for fzf ... "
rm -f "$fzf_base"/bin/fzf
echo "#!/bin/sh" > "$fzf_base"/bin/fzf
echo "$fzf_cmd \"\$@\"" >> "$fzf_base"/bin/fzf
chmod +x "$fzf_base"/bin/fzf
echo "OK"
fi fi
[[ "$*" =~ "--bin" ]] && exit 0
# Auto-completion # Auto-completion
ask "Do you want to add auto-completion support?" ask "Do you want to add auto-completion support?"
auto_completion=$? auto_completion=$?
@@ -159,294 +176,62 @@ for shell in bash zsh; do
echo -n "Generate ~/.fzf.$shell ... " echo -n "Generate ~/.fzf.$shell ... "
src=~/.fzf.${shell} src=~/.fzf.${shell}
fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/fzf-completion.${shell}\"" fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/shell/completion.${shell}\""
if [ $auto_completion -ne 0 ]; then if [ $shell != bash -o $auto_completion -ne 0 ]; then
fzf_completion="# $fzf_completion" fzf_completion="# $fzf_completion"
fi fi
if [ -n "$binary_error" ]; then fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
cat > $src << EOF if [ $key_bindings -ne 0 ]; then
# Setup fzf function fzf_key_bindings="# $fzf_key_bindings"
# ------------------ fi
unalias fzf 2> /dev/null
fzf() {
$fzf_cmd "\$@"
}
export -f fzf > /dev/null
# Auto-completion
# ---------------
$fzf_completion
EOF
else
cat > $src << EOF cat > $src << EOF
# Setup fzf # Setup fzf
# --------- # ---------
unalias fzf 2> /dev/null
unset fzf 2> /dev/null
if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then
export PATH="$fzf_base/bin:\$PATH" export PATH="\$PATH:$fzf_base/bin"
fi
# Man path
# --------
if [[ ! "\$MANPATH" =~ "$fzf_base/man" && -d "$fzf_base/man" ]]; then
export MANPATH="\$MANPATH:$fzf_base/man"
fi fi
# Auto-completion # Auto-completion
# --------------- # ---------------
$fzf_completion $fzf_completion
# Key bindings
# ------------
$fzf_key_bindings
EOF EOF
fi
if [ $key_bindings -eq 0 ]; then
if [ $shell = bash ]; then
cat >> $src << "EOFZF"
# Key bindings
# ------------
__fsel() {
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do
printf '%q ' "$item"
done
echo
}
if [[ $- =~ i ]]; then
__fsel_tmux() {
local height
height=${FZF_TMUX_HEIGHT:-40%}
if [[ $height =~ %$ ]]; then
height="-p ${height%\%}"
else
height="-l $height"
fi
tmux split-window $height "bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'"
}
__fcd() {
local dir
dir=$(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
[ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1
if [ -z "$(set -o | \grep '^vi.*on')" ]; then
# Required to refresh the prompt after fzf
bind '"\er": redraw-current-line'
# CTRL-T - Paste the selected file path into the command line
if [ $__use_tmux -eq 1 ]; then
bind '"\C-t": " \C-u \C-a\C-k$(__fsel_tmux)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"'
else
bind '"\C-t": " \C-u \C-a\C-k$(__fsel)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"'
fi
# CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s --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"'
else
bind '"\C-x\C-e": shell-expand-line'
bind '"\C-x\C-r": redraw-current-line'
# CTRL-T - Paste the selected file path into the command line
# - FIXME: Selected items are attached to the end regardless of cursor position
if [ $__use_tmux -eq 1 ]; then
bind '"\C-t": "\e$a \eddi$(__fsel_tmux)\C-x\C-e\e0P$xa"'
else
bind '"\C-t": "\e$a \eddi$(__fsel)\C-x\C-e\e0Px$a \C-x\C-r\exa "'
fi
bind -m vi-command '"\C-t": "i\C-t"'
# CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"'
bind -m vi-command '"\C-r": "i\C-r"'
# ALT-C - cd into the selected directory
bind '"\ec": "\eddi$(__fcd)\C-x\C-e\C-x\C-r\C-m"'
bind -m vi-command '"\ec": "i\ec"'
fi
unset __use_tmux
fi
EOFZF
else # zsh
cat >> $src << "EOFZF"
# Key bindings
# ------------
# CTRL-T - Paste the selected file path(s) into the command line
__fsel() {
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do
printf '%q ' "$item"
done
echo
}
if [[ $- =~ i ]]; then
if [ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ]; then
fzf-file-widget() {
local height
height=${FZF_TMUX_HEIGHT:-40%}
if [[ $height =~ %$ ]]; then
height="-p ${height%\%}"
else
height="-l $height"
fi
tmux split-window $height "zsh -c 'source ~/.fzf.zsh; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'"
}
else
fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)"
zle redisplay
}
fi
zle -N fzf-file-widget
bindkey '^T' fzf-file-widget
# ALT-C - cd into the selected directory
fzf-cd-widget() {
cd "${$(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
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 --tac +m -n2..,.. | sed "s/ *[0-9*]* *//")
zle redisplay
}
zle -N fzf-history-widget
bindkey '^R' fzf-history-widget
fi
EOFZF
fi
fi
echo "OK" echo "OK"
done done
# fish # fish
has_fish=0 has_fish=0
if [ -n "$(which fish)" ]; then if [ -n "$(which fish 2> /dev/null)" ]; then
has_fish=1 has_fish=1
echo -n "Generate ~/.config/fish/functions/fzf.fish ... " echo -n "Update fish_user_paths ... "
fish << EOF
echo \$fish_user_paths | grep $fzf_base/bin > /dev/null
or set --universal fish_user_paths \$fish_user_paths $fzf_base/bin
EOF
[ $? -eq 0 ] && echo "OK" || echo "Failed"
mkdir -p ~/.config/fish/functions mkdir -p ~/.config/fish/functions
if [ -n "$binary_error" ]; then if [ -e ~/.config/fish/functions/fzf.fish ]; then
cat > ~/.config/fish/functions/fzf.fish << EOFZF echo -n "Remove unnecessary ~/.config/fish/functions/fzf.fish ... "
function fzf rm -f ~/.config/fish/functions/fzf.fish && echo "OK" || echo "Failed"
$fzf_cmd \$argv
end
EOFZF
else
cat > ~/.config/fish/functions/fzf.fish << EOFZF
function fzf
$fzf_base/bin/fzf \$argv
end
EOFZF
fi fi
echo "OK"
if [ $key_bindings -eq 0 ]; then if [ $key_bindings -eq 0 ]; then
echo -n "Generate ~/.config/fish/functions/fzf_key_bindings.fish ... " echo -n "Symlink ~/.config/fish/functions/fzf_key_bindings.fish ... "
cat > ~/.config/fish/functions/fzf_key_bindings.fish << "EOFZF" ln -sf $fzf_base/shell/key-bindings.fish \
function fzf_key_bindings ~/.config/fish/functions/fzf_key_bindings.fish && echo "OK" || echo "Failed"
# Due to a bug of fish, we cannot use command substitution,
# so we use temporary file instead
if [ -z "$TMPDIR" ]
set -g TMPDIR /tmp
end
function __fzf_list
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3-
end
function __fzf_list_dir
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) \
-prune -o -type d -print 2> /dev/null | sed 1d | cut -b3-
end
function __fzf_escape
while read item
echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' '
end
end
function __fzf_ctrl_t
if [ -n "$TMUX_PANE" -a "$FZF_TMUX" != "0" ]
tmux split-window (__fzf_tmux_height) "fish -c 'fzf_key_bindings; __fzf_ctrl_t_tmux \\$TMUX_PANE'"
else
__fzf_list | fzf -m > $TMPDIR/fzf.result
and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape)
commandline -f repaint
rm -f $TMPDIR/fzf.result
end
end
function __fzf_ctrl_t_tmux
__fzf_list | fzf -m > $TMPDIR/fzf.result
and tmux send-keys -t $argv[1] (cat $TMPDIR/fzf.result | __fzf_escape)
rm -f $TMPDIR/fzf.result
end
function __fzf_reverse
if which tac > /dev/null
tac $argv
else
tail -r $argv
end
end
function __fzf_ctrl_r
history | __fzf_reverse | fzf +s --tac +m > $TMPDIR/fzf.result
and commandline (cat $TMPDIR/fzf.result)
commandline -f repaint
rm -f $TMPDIR/fzf.result
end
function __fzf_alt_c
# Fish hangs if the command before pipe redirects (2> /dev/null)
__fzf_list_dir | fzf +m > $TMPDIR/fzf.result
[ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ]
and cd (cat $TMPDIR/fzf.result)
commandline -f repaint
rm -f $TMPDIR/fzf.result
end
function __fzf_tmux_height
if set -q FZF_TMUX_HEIGHT
set height $FZF_TMUX_HEIGHT
else
set height 40%
end
if echo $height | \grep -q -E '%$'
echo "-p "(echo $height | sed 's/%$//')
else
echo "-l $height"
end
set -e height
end
bind \ct '__fzf_ctrl_t'
bind \cr '__fzf_ctrl_r'
bind \ec '__fzf_alt_c'
end
EOFZF
echo "OK"
fi fi
fi fi
@@ -476,12 +261,6 @@ done
if [ $key_bindings -eq 0 -a $has_fish -eq 1 ]; then if [ $key_bindings -eq 0 -a $has_fish -eq 1 ]; then
bind_file=~/.config/fish/functions/fish_user_key_bindings.fish bind_file=~/.config/fish/functions/fish_user_key_bindings.fish
append_line "fzf_key_bindings" "$bind_file" append_line "fzf_key_bindings" "$bind_file"
echo ' * Due to a known bug of fish, you may have issues running fzf on fish.'
echo ' * If that happens, try the following:'
echo ' - Remove ~/.config/fish/functions/fzf.fish'
echo ' - Place fzf executable in a directory included in $PATH'
echo
fi fi
cat << EOF cat << EOF

230
man/man1/fzf.1 Normal file
View File

@@ -0,0 +1,230 @@
.ig
The MIT License (MIT)
Copyright (c) 2015 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf 1 "April 2015" "fzf 0.9.10" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
.SH SYNOPSIS
fzf [options]
.SH DESCRIPTION
fzf is a general-purpose command-line fuzzy finder.
.SH OPTIONS
.SS Search mode
.TP
.B "-x, --extended"
Extended-search mode
.TP
.B "-e, --extended-exact"
Extended-search mode (exact match)
.TP
.B "-i"
Case-insensitive match (default: smart-case match)
.TP
.B "+i"
Case-sensitive match
.TP
.BI "-n, --nth=" "N[,..]"
Comma-separated list of field index expressions for limiting search scope.
See \fBFIELD INDEX EXPRESSION\fR for details.
.TP
.BI "--with-nth=" "N[,..]"
Transform the item using the list of index expressions for search
.TP
.BI "-d, --delimiter=" "STR"
Field delimiter regex for \fI--nth\fR and \fI--with-nth\fR (default: AWK-style)
.SS Search result
.TP
.B "+s, --no-sort"
Do not sort the result
.TP
.B "--tac"
Reverse the order of the input
.RS
e.g. \fBhistory | fzf --tac --no-sort\fR
.RE
.TP
.BI "--tiebreak=" "CRI"
Sort criterion to use when the scores are tied
.br
.R ""
.br
.BR length " Prefers item with shorter length"
.br
.BR begin " Prefers item with matched substring closer to the beginning"
.br
.BR end " Prefers item with matched substring closer to the end"
.br
.BR index " Prefers item that appeared earlier in the input stream"
.br
.SS Interface
.TP
.B "-m, --multi"
Enable multi-select with tab/shift-tab
.TP
.B "--ansi"
Enable processing of ANSI color codes
.TP
.B "--no-mouse"
Disable mouse
.TP
.B "--color=COL"
Color scheme: [dark|light|16|bw]
.br
(default: dark on 256-color terminal, otherwise 16)
.br
.R ""
.br
.BR dark " Color scheme for dark 256-color terminal"
.br
.BR light " Color scheme for light 256-color terminal"
.br
.BR 16 " Color scheme for 16-color terminal"
.br
.BR bw " No colors"
.br
.TP
.B "--black"
Use black background
.TP
.B "--reverse"
Reverse orientation
.TP
.B "--no-hscroll"
Disable horizontal scroll
.TP
.BI "--prompt=" "STR"
Input prompt (default: '> ')
.SS Scripting
.TP
.BI "-q, --query=" "STR"
Start the finder with the given query
.TP
.B "-1, --select-1"
Automatically select the only match
.TP
.B "-0, --exit-0"
Exit immediately when there's no match
.TP
.BI "-f, --filter=" "STR"
Filter mode. Do not start interactive finder. When used with \fB--no-sort\fR,
fzf becomes a fuzzy-version of grep.
.TP
.B "--print-query"
Print query as the first line
.TP
.BI "--expect=" "KEY[,..]"
Comma-separated list of keys (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR,
or any single character) that can be used to complete fzf in addition to the
default enter key. When this option is set, fzf will print the name of the key
pressed as the first line of its output (or as the second line if
\fB--print-query\fR is also used). The line will be empty if fzf is completed
with the default enter key.
.RS
e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
.RE
.TP
.BI "--toggle-sort=" "KEY"
Key to toggle sort (\fIctrl-[a-z]\fR, \fIalt-[a-z]\fR, \fIf[1-4]\fR,
or any single character)
.TP
.B "--sync"
Synchronous search for multi-staged filtering. If specified, fzf will launch
ncurses finder only after the input stream is complete.
.RS
e.g. \fBfzf --multi | fzf --sync\fR
.RE
.SH ENVIRONMENT
.TP
.B FZF_DEFAULT_COMMAND
Default command to use when input is tty
.TP
.B FZF_DEFAULT_OPTS
Default options. e.g. \fB--extended --ansi\fR
.SH EXIT STATUS
.BR 0 " Normal exit"
.br
.BR 1 " Interrupted with \fBCTRL-C\fR or \fBESC\fR"
.SH FIELD INDEX EXPRESSION
A field index expression can be a non-zero integer or a range expression
([BEGIN]..[END]). \fI--nth\fR and \fI--with-nth\fR take a comma-separated list
of field index expressions.
.SS Examples
.BR 1 " The 1st field"
.br
.BR 2 " The 2nd field"
.br
.BR -1 " The last field"
.br
.BR -2 " The 2nd to last field"
.br
.BR 3..5 " From the 3rd field to the 5th field"
.br
.BR 2.. " From the 2nd field to the last field"
.br
.BR ..-3 " From the 1st field to the 3rd to the last field"
.br
.BR .. " All the fields"
.br
.SH EXTENDED SEARCH MODE
With \fI-x\fR or \fI--extended\fR option, fzf will start in "extended-search
mode". In this mode, you can specify multiple patterns delimited by spaces,
such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR
.SS Exact-match (quoted)
A term that is prefixed by a single-quote character (') is interpreted as an
"exact-match" (or "non-fuzzy") term. fzf will search for the exact occurrences
of the string.
.SS Anchored-match
A term can be prefixed by ^, or suffixed by $ to become an anchored-match term.
Then fzf will search for the items that start with or end with the given
string. An anchored-match term is also an exact-match term.
.SS Negation
If a term is prefixed by !, fzf will exclude the items that satisfy the term
from the result.
.SS Extended-exact mode
If you don't need fuzzy matching at all and do not wish to "quote" (prefixing
with ') every word, start fzf with \fI-e\fR or \fI--extended-exact\fR option
(instead of \fI-x\fR or \fI--extended\fR).
.SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR)
.SH SEE ALSO
.I https://github.com/junegunn/fzf
.SH LICENSE
MIT

View File

@@ -21,9 +21,11 @@
" 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:default_tmux_height = '40%' let s:default_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:install = expand('<sfile>:h:h').'/install'
let s:installed = 0
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:fzf_tmux = expand('<sfile>:h:h').'/bin/fzf-tmux'
@@ -34,6 +36,13 @@ function! s:fzf_exec()
if !exists('s:exec') if !exists('s:exec')
if executable(s:fzf_go) if executable(s:fzf_go)
let s:exec = s:fzf_go let s:exec = s:fzf_go
elseif !s:installed && executable(s:install)
echohl WarningMsg
echo 'Downloading fzf binary. Please wait ...'
echohl None
let s:installed = 1
call system(s:install.' --bin')
return s:fzf_exec()
else else
let path = split(system('which fzf 2> /dev/null'), '\n') let path = split(system('which fzf 2> /dev/null'), '\n')
if !v:shell_error && !empty(path) if !v:shell_error && !empty(path)
@@ -96,6 +105,12 @@ function! s:upgrade(dict)
endfunction endfunction
function! fzf#run(...) abort function! fzf#run(...) abort
if has('nvim') && bufexists('[FZF]')
echohl WarningMsg
echomsg 'FZF is already running!'
echohl None
return []
endif
let dict = exists('a:1') ? s:upgrade(a:1) : {} let 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', '')
@@ -120,14 +135,20 @@ function! fzf#run(...) abort
else else
let prefix = '' let prefix = ''
endif endif
let split = s:tmux_enabled() && s:tmux_splittable(dict) let tmux = !has('nvim') && s:tmux_enabled() && s:splittable(dict)
let command = prefix.(split ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if split try
if tmux
return s:execute_tmux(dict, command, temps) return s:execute_tmux(dict, command, temps)
elseif has('nvim')
return s:execute_term(dict, command, temps)
else else
return s:execute(dict, command, temps) return s:execute(dict, command, temps)
endif endif
finally
call s:popd(dict)
endtry
endfunction endfunction
function! s:present(dict, ...) function! s:present(dict, ...)
@@ -144,20 +165,26 @@ function! s:fzf_tmux(dict)
for o in ['up', 'down', 'left', 'right'] for o in ['up', 'down', 'left', 'right']
if s:present(a:dict, o) if s:present(a:dict, o)
let size = '-'.o[0].(a:dict[o] == 1 ? '' : a:dict[o]) let size = '-'.o[0].(a:dict[o] == 1 ? '' : a:dict[o])
break
endif endif
endfor endfor
return printf('LINES=%d COLUMNS=%d %s %s %s --', return printf('LINES=%d COLUMNS=%d %s %s %s --',
\ &lines, &columns, s:fzf_tmux, size, (has_key(a:dict, 'source') ? '' : '-')) \ &lines, &columns, s:fzf_tmux, size, (has_key(a:dict, 'source') ? '' : '-'))
endfunction endfunction
function! s:tmux_splittable(dict) function! s:splittable(dict)
return s:present(a:dict, 'up', 'down', 'left', 'right') return s:present(a:dict, 'up', 'down', 'left', 'right')
endfunction endfunction
function! s:pushd(dict) function! s:pushd(dict)
if s:present(a:dict, 'dir') if s:present(a:dict, 'dir')
let a:dict.prev_dir = getcwd() let cwd = getcwd()
if get(a:dict, 'prev_dir', '') ==# cwd
return 1
endif
let a:dict.prev_dir = cwd
execute 'chdir '.s:escape(a:dict.dir) execute 'chdir '.s:escape(a:dict.dir)
let a:dict.dir = getcwd()
return 1 return 1
endif endif
return 0 return 0
@@ -171,7 +198,7 @@ endfunction
function! s:execute(dict, command, temps) function! s:execute(dict, command, temps)
call s:pushd(a:dict) call s:pushd(a:dict)
silent !clear silent! !clear 2> /dev/null
if has('gui_running') if has('gui_running')
let launcher = get(a:dict, 'launcher', get(g:, 'fzf_launcher', s:launcher)) let launcher = get(a:dict, 'launcher', get(g:, 'fzf_launcher', s:launcher))
let command = printf(launcher, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'") let command = printf(launcher, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'")
@@ -192,14 +219,6 @@ function! s:execute(dict, command, temps)
endif endif
endfunction endfunction
function! s:env_var(name)
if exists('$'.a:name)
return a:name . "='". substitute(expand('$'.a:name), "'", "'\\\\''", 'g') . "' "
else
return ''
endif
endfunction
function! s:execute_tmux(dict, command, temps) function! s:execute_tmux(dict, command, temps)
let command = a:command let command = a:command
if s:pushd(a:dict) if s:pushd(a:dict)
@@ -211,6 +230,67 @@ function! s:execute_tmux(dict, command, temps)
return s:callback(a:dict, a:temps) return s:callback(a:dict, a:temps)
endfunction endfunction
function! s:calc_size(max, val)
if a:val =~ '%$'
return a:max * str2nr(a:val[:-2]) / 100
else
return min([a:max, a:val])
endif
endfunction
function! s:split(dict)
let directions = {
\ 'up': ['topleft', 'resize', &lines],
\ 'down': ['botright', 'resize', &lines],
\ 'left': ['vertical topleft', 'vertical resize', &columns],
\ 'right': ['vertical botright', 'vertical resize', &columns] }
let s:ptab = tabpagenr()
try
for [dir, triple] in items(directions)
let val = get(a:dict, dir, '')
if !empty(val)
let [cmd, resz, max] = triple
let sz = s:calc_size(max, val)
execute cmd sz.'new'
execute resz sz
return
endif
endfor
if s:present(a:dict, 'window')
execute a:dict.window
else
tabnew
endif
finally
setlocal winfixwidth winfixheight
endtry
endfunction
function! s:execute_term(dict, command, temps)
call s:split(a:dict)
call s:pushd(a:dict)
let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps }
function! fzf.on_exit(id, code)
let tab = tabpagenr()
execute 'bd!' self.buf
if s:ptab == tab
wincmd p
endif
call s:pushd(self.dict)
try
call s:callback(self.dict, self.temps)
finally
call s:popd(self.dict)
endtry
endfunction
call termopen(a:command, fzf)
silent file [FZF]
startinsert
return []
endfunction
function! s:callback(dict, temps) function! s:callback(dict, temps)
if !filereadable(a:temps.result) if !filereadable(a:temps.result)
let lines = [] let lines = []
@@ -221,34 +301,53 @@ function! s:callback(dict, temps)
if type(a:dict.sink) == 2 if type(a:dict.sink) == 2
call a:dict.sink(line) call a:dict.sink(line)
else else
execute a:dict.sink.' '.s:escape(line) execute a:dict.sink s:escape(line)
endif endif
endfor endfor
endif endif
if has_key(a:dict, 'sink*')
call a:dict['sink*'](lines)
endif
endif endif
for tf in values(a:temps) for tf in values(a:temps)
silent! call delete(tf) silent! call delete(tf)
endfor endfor
call s:popd(a:dict)
return lines return lines
endfunction endfunction
let s:default_action = {
\ 'ctrl-m': 'e',
\ 'ctrl-t': 'tabedit',
\ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' }
function! s:cmd_callback(lines) abort
if empty(a:lines)
return
endif
let key = remove(a:lines, 0)
let cmd = get(s:action, key, 'e')
for item in a:lines
execute cmd s:escape(item)
endfor
endfunction
function! s:cmd(bang, ...) abort function! s:cmd(bang, ...) abort
let args = copy(a:000) let s:action = get(g:, 'fzf_action', s:default_action)
let args = extend(['--expect='.join(keys(s:action), ',')], a:000)
let opts = {} let opts = {}
if len(args) > 0 && isdirectory(expand(args[-1])) if len(args) > 0 && isdirectory(expand(args[-1]))
let opts.dir = remove(args, -1) let opts.dir = remove(args, -1)
endif endif
if !a:bang if !a:bang
let opts.down = get(g:, 'fzf_tmux_height', s:default_tmux_height) let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height))
endif endif
call fzf#run(extend({ 'sink': 'e', 'options': join(args) }, opts)) call fzf#run(extend({'options': join(args), 'sink*': function('<sid>cmd_callback')}, opts))
endfunction endfunction
command! -nargs=* -complete=dir -bang FZF call s:cmd('<bang>' == '!', <f-args>) command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>)
let &cpo = s:cpo_save let &cpo = s:cpo_save
unlet s:cpo_save unlet s:cpo_save

View File

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

75
shell/key-bindings.bash Normal file
View File

@@ -0,0 +1,75 @@
# Key bindings
# ------------
__fsel() {
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do
printf '%q ' "$item"
done
echo
}
if [[ $- =~ i ]]; then
__fsel_tmux() {
local height
height=${FZF_TMUX_HEIGHT:-40%}
if [[ $height =~ %$ ]]; then
height="-p ${height%\%}"
else
height="-l $height"
fi
tmux split-window $height "cd $(printf %q "$PWD");bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fsel)\"'"
}
__fcd() {
local dir
dir=$(command find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m) && printf 'cd %q' "$dir"
}
__use_tmux=0
[ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1
if [ -z "$(set -o | \grep '^vi.*on')" ]; then
# Required to refresh the prompt after fzf
bind '"\er": redraw-current-line'
# CTRL-T - Paste the selected file path into the command line
if [ $__use_tmux -eq 1 ]; then
bind '"\C-t": " \C-u \C-a\C-k$(__fsel_tmux)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"'
else
bind '"\C-t": " \C-u \C-a\C-k$(__fsel)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"'
fi
# CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | sed \"s/ *[0-9]* *//\")\e\C-e\er"'
# ALT-C - cd into the selected directory
bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"'
else
bind '"\C-x\C-e": shell-expand-line'
bind '"\C-x\C-r": redraw-current-line'
# CTRL-T - Paste the selected file path into the command line
# - FIXME: Selected items are attached to the end regardless of cursor position
if [ $__use_tmux -eq 1 ]; then
bind '"\C-t": "\e$a \eddi$(__fsel_tmux)\C-x\C-e\e0P$xa"'
else
bind '"\C-t": "\e$a \eddi$(__fsel)\C-x\C-e\e0Px$a \C-x\C-r\exa "'
fi
bind -m vi-command '"\C-t": "i\C-t"'
# CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | 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
bind '"\ec": "\eddi$(__fcd)\C-x\C-e\C-x\C-r\C-m"'
bind -m vi-command '"\ec": "i\ec"'
fi
unset __use_tmux
fi

80
shell/key-bindings.fish Normal file
View File

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

59
shell/key-bindings.zsh Normal file
View File

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

View File

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

View File

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

148
src/ansi.go Normal file
View File

@@ -0,0 +1,148 @@
package fzf
import (
"bytes"
"regexp"
"strconv"
"strings"
"unicode/utf8"
)
type ansiOffset struct {
offset [2]int32
color ansiState
}
type ansiState struct {
fg int
bg int
bold bool
}
func (s *ansiState) colored() bool {
return s.fg != -1 || s.bg != -1 || s.bold
}
func (s *ansiState) equals(t *ansiState) bool {
if t == nil {
return !s.colored()
}
return s.fg == t.fg && s.bg == t.bg && s.bold == t.bold
}
var ansiRegex *regexp.Regexp
func init() {
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]")
}
func extractColor(str *string) (*string, []ansiOffset) {
var offsets []ansiOffset
var output bytes.Buffer
var state *ansiState
idx := 0
for _, offset := range ansiRegex.FindAllStringIndex(*str, -1) {
output.WriteString((*str)[idx:offset[0]])
newState := interpretCode((*str)[offset[0]:offset[1]], state)
if !newState.equals(state) {
if state != nil {
// Update last offset
(&offsets[len(offsets)-1]).offset[1] = int32(output.Len())
}
if newState.colored() {
// Append new offset
state = newState
newLen := int32(utf8.RuneCount(output.Bytes()))
offsets = append(offsets, ansiOffset{[2]int32{newLen, newLen}, *state})
} else {
// Discard state
state = nil
}
}
idx = offset[1]
}
rest := (*str)[idx:]
if len(rest) > 0 {
output.WriteString(rest)
if state != nil {
// Update last offset
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
}
}
outputStr := output.String()
return &outputStr, offsets
}
func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
// State
var state *ansiState
if prevState == nil {
state = &ansiState{-1, -1, false}
} else {
state = &ansiState{prevState.fg, prevState.bg, prevState.bold}
}
if ansiCode[len(ansiCode)-1] == 'K' {
return state
}
ptr := &state.fg
state256 := 0
init := func() {
state.fg = -1
state.bg = -1
state.bold = false
state256 = 0
}
ansiCode = ansiCode[2 : len(ansiCode)-1]
if len(ansiCode) == 0 {
init()
}
for _, code := range strings.Split(ansiCode, ";") {
if num, err := strconv.Atoi(code); err == nil {
switch state256 {
case 0:
switch num {
case 38:
ptr = &state.fg
state256++
case 48:
ptr = &state.bg
state256++
case 39:
state.fg = -1
case 49:
state.bg = -1
case 1:
state.bold = true
case 0:
init()
default:
if num >= 30 && num <= 37 {
state.fg = num - 30
} else if num >= 40 && num <= 47 {
state.bg = num - 40
}
}
case 1:
switch num {
case 5:
state256++
default:
state256 = 0
}
case 2:
*ptr = num
state256 = 0
}
}
}
return state
}

107
src/ansi_test.go Normal file
View File

@@ -0,0 +1,107 @@
package fzf
import (
"fmt"
"testing"
)
func TestExtractColor(t *testing.T) {
assert := func(offset ansiOffset, b int32, e int32, fg int, bg int, bold bool) {
if offset.offset[0] != b || offset.offset[1] != e ||
offset.color.fg != fg || offset.color.bg != bg || offset.color.bold != bold {
t.Error(offset, b, e, fg, bg, bold)
}
}
src := "hello world"
clean := "\x1b[0m"
check := func(assertion func(ansiOffsets []ansiOffset)) {
output, ansiOffsets := extractColor(&src)
if *output != "hello world" {
t.Errorf("Invalid output: {}", output)
}
fmt.Println(src, ansiOffsets, clean)
assertion(ansiOffsets)
}
check(func(offsets []ansiOffset) {
if len(offsets) > 0 {
t.Fail()
}
})
src = "\x1b[0mhello world"
check(func(offsets []ansiOffset) {
if len(offsets) > 0 {
t.Fail()
}
})
src = "\x1b[1mhello world"
check(func(offsets []ansiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 0, 11, -1, -1, true)
})
src = "\x1b[1mhello \x1b[mworld"
check(func(offsets []ansiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 0, 6, -1, -1, true)
})
src = "\x1b[1mhello \x1b[Kworld"
check(func(offsets []ansiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 0, 11, -1, -1, true)
})
src = "hello \x1b[34;45;1mworld"
check(func(offsets []ansiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 6, 11, 4, 5, true)
})
src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld"
check(func(offsets []ansiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 6, 11, 4, 5, true)
})
src = "hello \x1b[34;45;1mwor\x1b[0mld"
check(func(offsets []ansiOffset) {
if len(offsets) != 1 {
t.Fail()
}
assert(offsets[0], 6, 9, 4, 5, true)
})
src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md"
check(func(offsets []ansiOffset) {
if len(offsets) != 3 {
t.Fail()
}
assert(offsets[0], 6, 8, 4, 233, true)
assert(offsets[1], 8, 9, 161, 233, true)
assert(offsets[2], 10, 11, 161, -1, false)
})
// {38,48};5;{38,48}
src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md"
check(func(offsets []ansiOffset) {
if len(offsets) != 2 {
t.Fail()
}
assert(offsets[0], 6, 9, 38, 48, true)
assert(offsets[1], 9, 10, 48, 38, true)
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,9 +34,6 @@ import (
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
const coordinatorDelayMax time.Duration = 100 * time.Millisecond
const coordinatorDelayStep time.Duration = 10 * time.Millisecond
func initProcs() { func initProcs() {
runtime.GOMAXPROCS(runtime.NumCPU()) runtime.GOMAXPROCS(runtime.NumCPU())
} }
@@ -44,7 +41,7 @@ func initProcs() {
/* /*
Reader -> EvtReadFin Reader -> EvtReadFin
Reader -> EvtReadNew -> Matcher (restart) Reader -> EvtReadNew -> Matcher (restart)
Terminal -> EvtSearchNew -> Matcher (restart) Terminal -> EvtSearchNew:bool -> Matcher (restart)
Matcher -> EvtSearchProgress -> Terminal (update info) Matcher -> EvtSearchProgress -> Terminal (update info)
Matcher -> EvtSearchFin -> Terminal (update list) Matcher -> EvtSearchFin -> Terminal (update list)
*/ */
@@ -54,6 +51,8 @@ func Run(options *Options) {
initProcs() initProcs()
opts := ParseOptions() opts := ParseOptions()
sort := opts.Sort > 0
rankTiebreak = opts.Tiebreak
if opts.Version { if opts.Version {
fmt.Println(Version) fmt.Println(Version)
@@ -63,29 +62,57 @@ func Run(options *Options) {
// Event channel // Event channel
eventBox := util.NewEventBox() eventBox := util.NewEventBox()
// ANSI code processor
ansiProcessor := func(data *string) (*string, []ansiOffset) {
// By default, we do nothing
return data, nil
}
if opts.Ansi {
if opts.Theme != nil {
ansiProcessor = func(data *string) (*string, []ansiOffset) {
return extractColor(data)
}
} else {
// When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input
ansiProcessor = func(data *string) (*string, []ansiOffset) {
trimmed, _ := extractColor(data)
return trimmed, nil
}
}
}
// Chunk list // Chunk list
var chunkList *ChunkList var chunkList *ChunkList
if len(opts.WithNth) == 0 { if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(data *string, index int) *Item { chunkList = NewChunkList(func(data *string, index int) *Item {
data, colors := ansiProcessor(data)
return &Item{ return &Item{
text: data, text: data,
index: uint32(index), index: uint32(index),
colors: colors,
rank: Rank{0, 0, uint32(index)}} rank: Rank{0, 0, uint32(index)}}
}) })
} else { } else {
chunkList = NewChunkList(func(data *string, index int) *Item { chunkList = NewChunkList(func(data *string, index int) *Item {
tokens := Tokenize(data, opts.Delimiter) tokens := Tokenize(data, opts.Delimiter)
trans := Transform(tokens, opts.WithNth)
item := Item{ item := Item{
text: Transform(tokens, opts.WithNth).whole, text: joinTokens(trans),
origText: data, origText: data,
index: uint32(index), index: uint32(index),
colors: nil,
rank: Rank{0, 0, uint32(index)}} rank: Rank{0, 0, uint32(index)}}
trimmed, colors := ansiProcessor(item.text)
item.text = trimmed
item.colors = colors
return &item return &item
}) })
} }
// Reader // Reader
streamingFilter := opts.Filter != nil && opts.Sort == 0 && !opts.Tac && !opts.Sync streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
if !streamingFilter { 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()
@@ -96,7 +123,7 @@ func Run(options *Options) {
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, opts.Tac, eventBox) matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
// Filtering mode // Filtering mode
if opts.Filter != nil { if opts.Filter != nil {
@@ -163,11 +190,15 @@ func Run(options *Options) {
reading = reading && evt == EvtReadNew reading = reading && evt == EvtReadNew
snapshot, count := chunkList.Snapshot() snapshot, count := chunkList.Snapshot()
terminal.UpdateCount(count, !reading) terminal.UpdateCount(count, !reading)
matcher.Reset(snapshot, terminal.Input(), false, !reading) matcher.Reset(snapshot, terminal.Input(), false, !reading, sort)
case EvtSearchNew: case EvtSearchNew:
switch val := value.(type) {
case bool:
sort = val
}
snapshot, _ := chunkList.Snapshot() snapshot, _ := chunkList.Snapshot()
matcher.Reset(snapshot, terminal.Input(), true, !reading) matcher.Reset(snapshot, terminal.Input(), true, !reading, sort)
delay = false delay = false
case EvtSearchProgress: case EvtSearchProgress:
@@ -189,6 +220,9 @@ func Run(options *Options) {
if opts.PrintQuery { if opts.PrintQuery {
fmt.Println(opts.Query) fmt.Println(opts.Query)
} }
if len(opts.Expect) > 0 {
fmt.Println()
}
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
fmt.Println(val.Get(i).AsString()) fmt.Println(val.Get(i).AsString())
} }

View File

@@ -61,10 +61,20 @@ const (
PgUp PgUp
PgDn PgDn
AltB F1
AltF F2
AltD F3
F4
AltBS AltBS
AltA
AltB
AltC
AltD
AltE
AltF
AltZ = AltA + 'z' - 'a'
) )
// Pallete // Pallete
@@ -78,12 +88,25 @@ const (
ColInfo ColInfo
ColCursor ColCursor
ColSelected ColSelected
ColUser
) )
const ( const (
doubleClickDuration = 500 * time.Millisecond doubleClickDuration = 500 * time.Millisecond
) )
type ColorTheme struct {
darkBg C.short
prompt C.short
match C.short
current C.short
currentMatch C.short
spinner C.short
info C.short
cursor C.short
selected C.short
}
type Event struct { type Event struct {
Type int Type int
Char rune Char rune
@@ -103,14 +126,49 @@ var (
_buf []byte _buf []byte
_in *os.File _in *os.File
_color func(int, bool) C.int _color func(int, bool) C.int
_colorMap map[int]int
_prevDownTime time.Time _prevDownTime time.Time
_prevDownY int
_clickY []int _clickY []int
Default16 *ColorTheme
Dark256 *ColorTheme
Light256 *ColorTheme
DarkBG C.short
) )
func init() { func init() {
_prevDownTime = time.Unix(0, 0) _prevDownTime = time.Unix(0, 0)
_clickY = []int{} _clickY = []int{}
_colorMap = make(map[int]int)
Default16 = &ColorTheme{
darkBg: C.COLOR_BLACK,
prompt: C.COLOR_BLUE,
match: C.COLOR_GREEN,
current: C.COLOR_YELLOW,
currentMatch: C.COLOR_GREEN,
spinner: C.COLOR_GREEN,
info: C.COLOR_WHITE,
cursor: C.COLOR_RED,
selected: C.COLOR_MAGENTA}
Dark256 = &ColorTheme{
darkBg: 236,
prompt: 110,
match: 108,
current: 254,
currentMatch: 151,
spinner: 148,
info: 144,
cursor: 161,
selected: 168}
Light256 = &ColorTheme{
darkBg: 251,
prompt: 25,
match: 66,
current: 237,
currentMatch: 23,
spinner: 65,
info: 101,
cursor: 161,
selected: 168}
} }
func attrColored(pair int, bold bool) C.int { func attrColored(pair int, bold bool) C.int {
@@ -160,7 +218,7 @@ func getch(nonblock bool) int {
return int(b[0]) return int(b[0])
} }
func Init(color bool, color256 bool, black bool, mouse bool) { func Init(theme *ColorTheme, black bool, mouse bool) {
{ {
in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0) in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0)
if err != nil { if err != nil {
@@ -190,8 +248,16 @@ func Init(color bool, color256 bool, black bool, mouse bool) {
os.Exit(1) os.Exit(1)
}() }()
if color { if theme != nil {
C.start_color() C.start_color()
initPairs(theme, black)
_color = attrColored
} else {
_color = attrMono
}
}
func initPairs(theme *ColorTheme, black bool) {
var bg C.short var bg C.short
if black { if black {
bg = C.COLOR_BLACK bg = C.COLOR_BLACK
@@ -199,29 +265,16 @@ func Init(color bool, color256 bool, black bool, mouse bool) {
C.use_default_colors() C.use_default_colors()
bg = -1 bg = -1
} }
if color256 {
C.init_pair(ColPrompt, 110, bg) DarkBG = theme.darkBg
C.init_pair(ColMatch, 108, bg) C.init_pair(ColPrompt, theme.prompt, bg)
C.init_pair(ColCurrent, 254, 236) C.init_pair(ColMatch, theme.match, bg)
C.init_pair(ColCurrentMatch, 151, 236) C.init_pair(ColCurrent, theme.current, DarkBG)
C.init_pair(ColSpinner, 148, bg) C.init_pair(ColCurrentMatch, theme.currentMatch, DarkBG)
C.init_pair(ColInfo, 144, bg) C.init_pair(ColSpinner, theme.spinner, bg)
C.init_pair(ColCursor, 161, 236) C.init_pair(ColInfo, theme.info, bg)
C.init_pair(ColSelected, 168, 236) C.init_pair(ColCursor, theme.cursor, DarkBG)
} else { C.init_pair(ColSelected, theme.selected, DarkBG)
C.init_pair(ColPrompt, C.COLOR_BLUE, bg)
C.init_pair(ColMatch, C.COLOR_GREEN, bg)
C.init_pair(ColCurrent, C.COLOR_YELLOW, C.COLOR_BLACK)
C.init_pair(ColCurrentMatch, C.COLOR_GREEN, C.COLOR_BLACK)
C.init_pair(ColSpinner, C.COLOR_GREEN, bg)
C.init_pair(ColInfo, C.COLOR_WHITE, bg)
C.init_pair(ColCursor, C.COLOR_RED, C.COLOR_BLACK)
C.init_pair(ColSelected, C.COLOR_MAGENTA, C.COLOR_BLACK)
}
_color = attrColored
} else {
_color = attrMono
}
} }
func Close() { func Close() {
@@ -318,6 +371,14 @@ func escSequence(sz *int) Event {
return Event{CtrlE, 0, nil} return Event{CtrlE, 0, nil}
case 77: case 77:
return mouseSequence(sz) return mouseSequence(sz)
case 80:
return Event{F1, 0, nil}
case 81:
return Event{F2, 0, nil}
case 82:
return Event{F3, 0, nil}
case 83:
return Event{F4, 0, nil}
case 49, 50, 51, 52, 53, 54: case 49, 50, 51, 52, 53, 54:
if len(_buf) < 4 { if len(_buf) < 4 {
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
@@ -363,6 +424,9 @@ func escSequence(sz *int) Event {
} // _buf[2] } // _buf[2]
} // _buf[2] } // _buf[2]
} // _buf[1] } // _buf[1]
if _buf[1] >= 'a' && _buf[1] <= 'z' {
return Event{AltA + int(_buf[1]) - 'a', 0, nil}
}
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }
@@ -393,6 +457,9 @@ func GetChar() Event {
return Event{int(_buf[0]), 0, nil} return Event{int(_buf[0]), 0, nil}
} }
r, rsz := utf8.DecodeRune(_buf) r, rsz := utf8.DecodeRune(_buf)
if r == utf8.RuneError {
return Event{ESC, 0, nil}
}
sz = rsz sz = rsz
return Event{Rune, r, nil} return Event{Rune, r, nil}
} }
@@ -428,3 +495,15 @@ func Endwin() {
func Refresh() { func Refresh() {
C.refresh() C.refresh()
} }
func PairFor(fg int, bg int) int {
key := (fg << 8) + bg
if found, prs := _colorMap[key]; prs {
return found
}
id := len(_colorMap) + ColUser
C.init_pair(C.short(id), C.short(fg), C.short(bg))
_colorMap[key] = id
return id
}

14
src/curses/curses_test.go Normal file
View File

@@ -0,0 +1,14 @@
package curses
import (
"testing"
)
func TestPairFor(t *testing.T) {
if PairFor(30, 50) != PairFor(30, 50) {
t.Fail()
}
if PairFor(-1, 10) != PairFor(-1, 10) {
t.Fail()
}
}

View File

@@ -1,32 +1,49 @@
package fzf package fzf
import (
"math"
"github.com/junegunn/fzf/src/curses"
)
// Offset holds two 32-bit integers denoting the offsets of a matched substring // Offset holds two 32-bit integers denoting the offsets of a matched substring
type Offset [2]int32 type Offset [2]int32
type colorOffset struct {
offset [2]int32
color int
bold bool
}
// Item represents each input line // Item represents each input line
type Item struct { type Item struct {
text *string text *string
origText *string origText *string
transformed *Transformed transformed *[]Token
index uint32 index uint32
offsets []Offset offsets []Offset
colors []ansiOffset
rank Rank rank Rank
} }
// Rank is used to sort the search result // Rank is used to sort the search result
type Rank struct { type Rank struct {
matchlen uint16 matchlen uint16
strlen uint16 tiebreak uint16
index uint32 index uint32
} }
// Tiebreak criterion to use. Never changes once fzf is started.
var rankTiebreak tiebreak
// Rank calculates rank of the Item // Rank calculates rank of the Item
func (i *Item) Rank(cache bool) Rank { func (i *Item) Rank(cache bool) Rank {
if cache && (i.rank.matchlen > 0 || i.rank.strlen > 0) { if cache && (i.rank.matchlen > 0 || i.rank.tiebreak > 0) {
return i.rank return i.rank
} }
matchlen := 0 matchlen := 0
prevEnd := 0 prevEnd := 0
minBegin := math.MaxUint16
for _, offset := range i.offsets { for _, offset := range i.offsets {
begin := int(offset[0]) begin := int(offset[0])
end := int(offset[1]) end := int(offset[1])
@@ -37,10 +54,30 @@ func (i *Item) Rank(cache bool) Rank {
prevEnd = end prevEnd = end
} }
if end > begin { if end > begin {
if begin < minBegin {
minBegin = begin
}
matchlen += end - begin matchlen += end - begin
} }
} }
rank := Rank{uint16(matchlen), uint16(len(*i.text)), i.index} var tiebreak uint16
switch rankTiebreak {
case byLength:
tiebreak = uint16(len(*i.text))
case byBegin:
// We can't just look at i.offsets[0][0] because it can be an inverse term
tiebreak = uint16(minBegin)
case byEnd:
if prevEnd > 0 {
tiebreak = uint16(1 + len(*i.text) - prevEnd)
} else {
// Empty offsets due to inverse terms.
tiebreak = 1
}
case byIndex:
tiebreak = 1
}
rank := Rank{uint16(matchlen), tiebreak, i.index}
if cache { if cache {
i.rank = rank i.rank = rank
} }
@@ -55,6 +92,79 @@ func (i *Item) AsString() string {
return *i.text return *i.text
} }
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {
if len(item.colors) == 0 {
var offsets []colorOffset
for _, off := range item.offsets {
offsets = append(offsets, colorOffset{offset: off, color: color, bold: bold})
}
return offsets
}
// Find max column
var maxCol int32
for _, off := range item.offsets {
if off[1] > maxCol {
maxCol = off[1]
}
}
for _, ansi := range item.colors {
if ansi.offset[1] > maxCol {
maxCol = ansi.offset[1]
}
}
cols := make([]int, maxCol)
for colorIndex, ansi := range item.colors {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = colorIndex + 1 // XXX
}
}
for _, off := range item.offsets {
for i := off[0]; i < off[1]; i++ {
cols[i] = -1
}
}
// sort.Sort(ByOrder(offsets))
// Merge offsets
// ------------ ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
curr := 0
start := 0
var offsets []colorOffset
add := func(idx int) {
if curr != 0 && idx > start {
if curr == -1 {
offsets = append(offsets, colorOffset{
offset: Offset{int32(start), int32(idx)}, color: color, bold: bold})
} else {
ansi := item.colors[curr-1]
bg := ansi.color.bg
if current && bg == -1 {
bg = int(curses.DarkBG)
}
offsets = append(offsets, colorOffset{
offset: Offset{int32(start), int32(idx)},
color: curses.PairFor(ansi.color.fg, bg),
bold: ansi.color.bold || bold})
}
}
}
for idx, col := range cols {
if col != curr {
add(idx)
start = idx
curr = col
}
}
add(int(maxCol))
return offsets
}
// ByOrder is for sorting substring offsets // ByOrder is for sorting substring offsets
type ByOrder []Offset type ByOrder []Offset
@@ -115,9 +225,9 @@ func compareRanks(irank Rank, jrank Rank, tac bool) bool {
return false return false
} }
if irank.strlen < jrank.strlen { if irank.tiebreak < jrank.tiebreak {
return true return true
} else if irank.strlen > jrank.strlen { } else if irank.tiebreak > jrank.tiebreak {
return false return false
} }

View File

@@ -3,6 +3,8 @@ package fzf
import ( import (
"sort" "sort"
"testing" "testing"
"github.com/junegunn/fzf/src/curses"
) )
func TestOffsetSort(t *testing.T) { func TestOffsetSort(t *testing.T) {
@@ -40,7 +42,7 @@ func TestItemRank(t *testing.T) {
strs := []string{"foo", "foobar", "bar", "baz"} strs := []string{"foo", "foobar", "bar", "baz"}
item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}} item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}}
rank1 := item1.Rank(true) rank1 := item1.Rank(true)
if rank1.matchlen != 0 || rank1.strlen != 3 || rank1.index != 1 { if rank1.matchlen != 0 || rank1.tiebreak != 3 || rank1.index != 1 {
t.Error(item1.Rank(true)) t.Error(item1.Rank(true))
} }
// Only differ in index // Only differ in index
@@ -72,3 +74,31 @@ func TestItemRank(t *testing.T) {
t.Error(items) t.Error(items)
} }
} }
func TestColorOffset(t *testing.T) {
// ------------ 20 ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
item := Item{
offsets: []Offset{Offset{5, 15}, Offset{25, 35}},
colors: []ansiOffset{
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}},
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}},
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}},
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
offsets := item.colorOffsets(99, false, true)
assert := func(idx int, b int32, e int32, c int, bold bool) {
o := offsets[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold {
t.Error(o)
}
}
assert(0, 0, 5, curses.ColUser, false)
assert(1, 5, 15, 99, false)
assert(2, 15, 20, curses.ColUser, false)
assert(3, 22, 25, curses.ColUser+1, true)
assert(4, 25, 35, 99, false)
assert(5, 35, 40, curses.ColUser+2, true)
}

View File

@@ -15,6 +15,7 @@ type MatchRequest struct {
chunks []*Chunk chunks []*Chunk
pattern *Pattern pattern *Pattern
final bool final bool
sort bool
} }
// Matcher is responsible for performing search // Matcher is responsible for performing search
@@ -33,10 +34,6 @@ const (
reqReset reqReset
) )
const (
progressMinDuration = 200 * time.Millisecond
)
// NewMatcher returns a new Matcher // NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern, func NewMatcher(patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox) *Matcher { sort bool, tac bool, eventBox *util.EventBox) *Matcher {
@@ -69,6 +66,12 @@ func (m *Matcher) Loop() {
events.Clear() events.Clear()
}) })
if request.sort != m.sort {
m.sort = request.sort
m.mergerCache = make(map[string]*Merger)
clearChunkCache()
}
// Restart search // Restart search
patternString := request.pattern.AsString() patternString := request.pattern.AsString()
var merger *Merger var merger *Merger
@@ -93,7 +96,9 @@ func (m *Matcher) Loop() {
} }
if !cancelled { if !cancelled {
if merger.Cacheable() {
m.mergerCache[patternString] = merger m.mergerCache[patternString] = merger
}
merger.final = request.final merger.final = request.final
m.eventBox.Set(EvtSearchFin, merger) m.eventBox.Set(EvtSearchFin, merger)
} }
@@ -133,7 +138,10 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
return EmptyMerger, false return EmptyMerger, false
} }
pattern := request.pattern pattern := request.pattern
empty := pattern.IsEmpty() if pattern.IsEmpty() {
return PassMerger(&request.chunks, m.tac), false
}
cancelled := util.NewAtomicBool(false) cancelled := util.NewAtomicBool(false)
slices := m.sliceChunks(request.chunks) slices := m.sliceChunks(request.chunks)
@@ -148,19 +156,14 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
defer func() { waitGroup.Done() }() defer func() { waitGroup.Done() }()
sliceMatches := []*Item{} sliceMatches := []*Item{}
for _, chunk := range chunks { for _, chunk := range chunks {
var matches []*Item matches := request.pattern.Match(chunk)
if empty {
matches = *chunk
} else {
matches = request.pattern.Match(chunk)
}
sliceMatches = append(sliceMatches, matches...) sliceMatches = append(sliceMatches, matches...)
if cancelled.Get() { if cancelled.Get() {
return return
} }
countChan <- len(matches) countChan <- len(matches)
} }
if !empty && m.sort { if m.sort {
if m.tac { if m.tac {
sort.Sort(ByRelevanceTac(sliceMatches)) sort.Sort(ByRelevanceTac(sliceMatches))
} else { } else {
@@ -187,7 +190,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
break break
} }
if !empty && m.reqBox.Peek(reqReset) { if m.reqBox.Peek(reqReset) {
return nil, wait() return nil, wait()
} }
@@ -201,11 +204,11 @@ 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, m.tac), false return NewMerger(partialResults, m.sort, m.tac), false
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool) { func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool) {
pattern := m.patternBuilder(patternRunes) pattern := m.patternBuilder(patternRunes)
var event util.EventType var event util.EventType
@@ -214,5 +217,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
} else { } else {
event = reqRetry event = reqRetry
} }
m.reqBox.Set(event, MatchRequest{chunks, pattern, final}) m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort})
} }

View File

@@ -10,6 +10,7 @@ var EmptyMerger = NewMerger([][]*Item{}, false, false)
type Merger struct { type Merger struct {
lists [][]*Item lists [][]*Item
merged []*Item merged []*Item
chunks *[]*Chunk
cursors []int cursors []int
sorted bool sorted bool
tac bool tac bool
@@ -17,11 +18,26 @@ type Merger struct {
count int count int
} }
// PassMerger returns a new Merger that simply returns the items in the
// original order
func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
mg := Merger{
chunks: chunks,
tac: tac,
count: 0}
for _, chunk := range *mg.chunks {
mg.count += len(*chunk)
}
return &mg
}
// NewMerger returns a new Merger // NewMerger returns a new Merger
func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger { func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger {
mg := Merger{ mg := Merger{
lists: lists, lists: lists,
merged: []*Item{}, merged: []*Item{},
chunks: nil,
cursors: make([]int, len(lists)), cursors: make([]int, len(lists)),
sorted: sorted, sorted: sorted,
tac: tac, tac: tac,
@@ -41,12 +57,20 @@ 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 mg.chunks != nil {
if mg.tac {
idx = mg.count - idx - 1
}
chunk := (*mg.chunks)[idx/chunkSize]
return (*chunk)[idx%chunkSize]
}
if mg.sorted { if mg.sorted {
return mg.mergedGet(idx) return mg.mergedGet(idx)
} }
if mg.tac { if mg.tac {
idx = mg.Length() - idx - 1 idx = mg.count - idx - 1
} }
for _, list := range mg.lists { for _, list := range mg.lists {
numItems := len(list) numItems := len(list)
@@ -58,6 +82,10 @@ func (mg *Merger) Get(idx int) *Item {
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))
} }
func (mg *Merger) Cacheable() bool {
return mg.count < mergerCacheMax
}
func (mg *Merger) mergedGet(idx int) *Item { func (mg *Merger) mergedGet(idx int) *Item {
for i := len(mg.merged); i <= idx; i++ { for i := len(mg.merged); i <= idx; i++ {
minRank := Rank{0, 0, 0} minRank := Rank{0, 0, 0}

View File

@@ -5,6 +5,9 @@ import (
"os" "os"
"regexp" "regexp"
"strings" "strings"
"unicode/utf8"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/go-shellwords" "github.com/junegunn/go-shellwords"
) )
@@ -25,15 +28,18 @@ const usage = `usage: fzf [options]
Search result Search result
+s, --no-sort Do not sort the result +s, --no-sort Do not sort the result
--tac Reverse the order of the input --tac Reverse the order of the input
(e.g. 'history | fzf --tac --no-sort') --tiebreak=CRI Sort criterion when the scores are tied;
[length|begin|end|index] (default: length)
Interface Interface
-m, --multi Enable multi-select with tab/shift-tab -m, --multi Enable multi-select with tab/shift-tab
--ansi Enable processing of ANSI color codes
--no-mouse Disable mouse --no-mouse Disable mouse
+c, --no-color Disable colors --color=COL Color scheme; [dark|light|16|bw]
+2, --no-256 Disable 256-color (default: dark on 256-color terminal, otherwise 16)
--black Use black background --black Use black background
--reverse Reverse orientation --reverse Reverse orientation
--no-hscroll Disable horizontal scroll
--prompt=STR Input prompt (default: '> ') --prompt=STR Input prompt (default: '> ')
Scripting Scripting
@@ -42,8 +48,9 @@ const usage = `usage: fzf [options]
-0, --exit-0 Exit immediately when there's no match -0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder. -f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line --print-query Print query as the first line
--expect=KEYS Comma-separated list of keys to complete fzf
--toggle-sort=KEY Key to toggle sort
--sync Synchronous search for multi-staged filtering --sync Synchronous search for multi-staged filtering
(e.g. 'fzf --multi | fzf --sync')
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
@@ -71,6 +78,16 @@ const (
CaseRespect CaseRespect
) )
// Sort criteria
type tiebreak int
const (
byLength tiebreak = iota
byBegin
byEnd
byIndex
)
// Options stores the values of command-line options // Options stores the values of command-line options
type Options struct { type Options struct {
Mode Mode Mode Mode
@@ -80,23 +97,34 @@ type Options struct {
Delimiter *regexp.Regexp Delimiter *regexp.Regexp
Sort int Sort int
Tac bool Tac bool
Tiebreak tiebreak
Multi bool Multi bool
Ansi bool
Mouse bool Mouse bool
Color bool Theme *curses.ColorTheme
Color256 bool
Black bool Black bool
Reverse bool Reverse bool
Hscroll bool
Prompt string Prompt string
Query string Query string
Select1 bool Select1 bool
Exit0 bool Exit0 bool
Filter *string Filter *string
ToggleSort int
Expect []int
PrintQuery bool PrintQuery bool
Sync bool Sync bool
Version bool Version bool
} }
func defaultOptions() *Options { func defaultOptions() *Options {
var defaultTheme *curses.ColorTheme
if strings.Contains(os.Getenv("TERM"), "256") {
defaultTheme = curses.Dark256
} else {
defaultTheme = curses.Default16
}
return &Options{ return &Options{
Mode: ModeFuzzy, Mode: ModeFuzzy,
Case: CaseSmart, Case: CaseSmart,
@@ -105,17 +133,21 @@ func defaultOptions() *Options {
Delimiter: nil, Delimiter: nil,
Sort: 1000, Sort: 1000,
Tac: false, Tac: false,
Tiebreak: byLength,
Multi: false, Multi: false,
Ansi: false,
Mouse: true, Mouse: true,
Color: true, Theme: defaultTheme,
Color256: strings.Contains(os.Getenv("TERM"), "256"),
Black: false, Black: false,
Reverse: false, Reverse: false,
Hscroll: true,
Prompt: "> ", Prompt: "> ",
Query: "", Query: "",
Select1: false, Select1: false,
Exit0: false, Exit0: false,
Filter: nil, Filter: nil,
ToggleSort: 0,
Expect: []int{},
PrintQuery: false, PrintQuery: false,
Sync: false, Sync: false,
Version: false} Version: false}
@@ -188,6 +220,81 @@ func delimiterRegexp(str string) *regexp.Regexp {
return rx return rx
} }
func isAlphabet(char uint8) bool {
return char >= 'a' && char <= 'z'
}
func parseKeyChords(str string, message string) []int {
if len(str) == 0 {
errorExit(message)
}
tokens := strings.Split(str, ",")
if str == "," || strings.HasPrefix(str, ",,") || strings.HasSuffix(str, ",,") || strings.Index(str, ",,,") >= 0 {
tokens = append(tokens, ",")
}
var chords []int
for _, key := range tokens {
if len(key) == 0 {
continue // ignore
}
lkey := strings.ToLower(key)
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chords = append(chords, curses.CtrlA+int(lkey[5])-'a')
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
chords = append(chords, curses.AltA+int(lkey[4])-'a')
} else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' {
chords = append(chords, curses.F1+int(key[1])-'1')
} else if utf8.RuneCountInString(key) == 1 {
chords = append(chords, curses.AltZ+int([]rune(key)[0]))
} else {
errorExit("unsupported key: " + key)
}
}
return chords
}
func parseTiebreak(str string) tiebreak {
switch strings.ToLower(str) {
case "length":
return byLength
case "index":
return byIndex
case "begin":
return byBegin
case "end":
return byEnd
default:
errorExit("invalid sort criterion: " + str)
}
return byLength
}
func parseTheme(str string) *curses.ColorTheme {
switch strings.ToLower(str) {
case "dark":
return curses.Dark256
case "light":
return curses.Light256
case "16":
return curses.Default16
case "bw", "no":
return nil
default:
errorExit("invalid color scheme: " + str)
}
return nil
}
func checkToggleSort(str string) int {
keys := parseKeyChords(str, "key name required")
if len(keys) != 1 {
errorExit("multiple keys specified")
}
return keys[0]
}
func parseOptions(opts *Options, allArgs []string) { func parseOptions(opts *Options, allArgs []string) {
for i := 0; i < len(allArgs); i++ { for i := 0; i < len(allArgs); i++ {
arg := allArgs[i] arg := allArgs[i]
@@ -205,6 +312,14 @@ func parseOptions(opts *Options, allArgs []string) {
case "-f", "--filter": case "-f", "--filter":
filter := nextString(allArgs, &i, "query string required") filter := nextString(allArgs, &i, "query string required")
opts.Filter = &filter opts.Filter = &filter
case "--expect":
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
case "--tiebreak":
opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--color":
opts.Theme = parseTheme(nextString(allArgs, &i, "color scheme name required"))
case "--toggle-sort":
opts.ToggleSort = checkToggleSort(nextString(allArgs, &i, "key name required"))
case "-d", "--delimiter": case "-d", "--delimiter":
opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required"))
case "-n", "--nth": case "-n", "--nth":
@@ -227,12 +342,16 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Multi = true opts.Multi = true
case "+m", "--no-multi": case "+m", "--no-multi":
opts.Multi = false opts.Multi = false
case "--ansi":
opts.Ansi = true
case "--no-ansi":
opts.Ansi = false
case "--no-mouse": case "--no-mouse":
opts.Mouse = false opts.Mouse = false
case "+c", "--no-color": case "+c", "--no-color":
opts.Color = false opts.Theme = nil
case "+2", "--no-256": case "+2", "--no-256":
opts.Color256 = false opts.Theme = curses.Default16
case "--black": case "--black":
opts.Black = true opts.Black = true
case "--no-black": case "--no-black":
@@ -241,6 +360,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Reverse = true opts.Reverse = true
case "--no-reverse": case "--no-reverse":
opts.Reverse = false opts.Reverse = false
case "--hscroll":
opts.Hscroll = true
case "--no-hscroll":
opts.Hscroll = false
case "-1", "--select-1": case "-1", "--select-1":
opts.Select1 = true opts.Select1 = true
case "+1", "--no-select-1": case "+1", "--no-select-1":
@@ -278,6 +401,14 @@ func parseOptions(opts *Options, allArgs []string) {
opts.WithNth = splitNth(value) opts.WithNth = splitNth(value)
} else if match, _ := optString(arg, "-s|--sort="); match { } else if match, _ := optString(arg, "-s|--sort="); match {
opts.Sort = 1 // Don't care opts.Sort = 1 // Don't care
} else if match, value := optString(arg, "--toggle-sort="); match {
opts.ToggleSort = checkToggleSort(value)
} else if match, value := optString(arg, "--expect="); match {
opts.Expect = parseKeyChords(value, "key names required")
} else if match, value := optString(arg, "--tiebreak="); match {
opts.Tiebreak = parseTiebreak(value)
} else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(value)
} else { } else {
errorExit("unknown option: " + arg) errorExit("unknown option: " + arg)
} }

View File

@@ -1,6 +1,10 @@
package fzf package fzf
import "testing" import (
"testing"
"github.com/junegunn/fzf/src/curses"
)
func TestDelimiterRegex(t *testing.T) { func TestDelimiterRegex(t *testing.T) {
rx := delimiterRegexp("*") rx := delimiterRegexp("*")
@@ -65,3 +69,63 @@ func TestIrrelevantNth(t *testing.T) {
} }
} }
} }
func TestParseKeys(t *testing.T) {
keys := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "")
check := func(key int, expected int) {
if key != expected {
t.Errorf("%d != %d", key, expected)
}
}
check(len(keys), 9)
check(keys[0], curses.CtrlZ)
check(keys[1], curses.AltZ)
check(keys[2], curses.F2)
check(keys[3], curses.AltZ+'@')
check(keys[4], curses.AltA)
check(keys[5], curses.AltZ+'!')
check(keys[6], curses.CtrlA+'g'-'a')
check(keys[7], curses.AltZ+'J')
check(keys[8], curses.AltZ+'g')
}
func TestParseKeysWithComma(t *testing.T) {
check := func(key int, expected int) {
if key != expected {
t.Errorf("%d != %d", key, expected)
}
}
keys := parseKeyChords(",", "")
check(len(keys), 1)
check(keys[0], curses.AltZ+',')
keys = parseKeyChords(",,a,b", "")
check(len(keys), 3)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+',')
keys = parseKeyChords("a,b,,", "")
check(len(keys), 3)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+',')
keys = parseKeyChords("a,,,b", "")
check(len(keys), 3)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+',')
keys = parseKeyChords("a,,,b,c", "")
check(len(keys), 4)
check(keys[0], curses.AltZ+'a')
check(keys[1], curses.AltZ+'b')
check(keys[2], curses.AltZ+'c')
check(keys[3], curses.AltZ+',')
keys = parseKeyChords(",,,", "")
check(len(keys), 1)
check(keys[0], curses.AltZ+',')
}

View File

@@ -4,12 +4,11 @@ import (
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"unicode"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
) )
const uppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// fuzzy // fuzzy
// 'exact // 'exact
// ^exact-prefix // ^exact-prefix
@@ -44,7 +43,7 @@ type Pattern struct {
hasInvTerm bool hasInvTerm bool
delimiter *regexp.Regexp delimiter *regexp.Regexp
nth []Range nth []Range
procFun map[termType]func(bool, *string, []rune) (int, int) procFun map[termType]func(bool, *[]rune, []rune) (int, int)
} }
var ( var (
@@ -54,17 +53,21 @@ var (
) )
func init() { func init() {
// We can uniquely identify the pattern for a given string since
// mode and caseMode do not change while the program is running
_patternCache = make(map[string]*Pattern)
_splitRegex = regexp.MustCompile("\\s+") _splitRegex = regexp.MustCompile("\\s+")
_cache = NewChunkCache() clearPatternCache()
clearChunkCache()
} }
func clearPatternCache() { func clearPatternCache() {
// We can uniquely identify the pattern for a given string since
// mode and caseMode do not change while the program is running
_patternCache = make(map[string]*Pattern) _patternCache = make(map[string]*Pattern)
} }
func clearChunkCache() {
_cache = NewChunkCache()
}
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(mode Mode, caseMode Case, func BuildPattern(mode Mode, caseMode Case,
nth []Range, delimiter *regexp.Regexp, runes []rune) *Pattern { nth []Range, delimiter *regexp.Regexp, runes []rune) *Pattern {
@@ -87,7 +90,14 @@ func BuildPattern(mode Mode, caseMode Case,
switch caseMode { switch caseMode {
case CaseSmart: case CaseSmart:
if !strings.ContainsAny(asString, uppercaseLetters) { hasUppercase := false
for _, r := range runes {
if unicode.IsUpper(r) {
hasUppercase = true
break
}
}
if !hasUppercase {
runes, caseSensitive = []rune(strings.ToLower(asString)), false runes, caseSensitive = []rune(strings.ToLower(asString)), false
} }
case CaseIgnore: case CaseIgnore:
@@ -112,7 +122,7 @@ func BuildPattern(mode Mode, caseMode Case,
hasInvTerm: hasInvTerm, hasInvTerm: hasInvTerm,
nth: nth, nth: nth,
delimiter: delimiter, delimiter: delimiter,
procFun: make(map[termType]func(bool, *string, []rune) (int, int))} procFun: make(map[termType]func(bool, *[]rune, []rune) (int, int))}
ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termFuzzy] = algo.FuzzyMatch
ptr.procFun[termExact] = algo.ExactMatchNaive ptr.procFun[termExact] = algo.ExactMatchNaive
@@ -264,6 +274,7 @@ func dupItem(item *Item, offsets []Offset) *Item {
transformed: item.transformed, transformed: item.transformed,
index: item.index, index: item.index,
offsets: offsets, offsets: offsets,
colors: item.colors,
rank: Rank{0, 0, item.index}} rank: Rank{0, 0, item.index}}
} }
@@ -289,28 +300,27 @@ func (p *Pattern) extendedMatch(item *Item) []Offset {
return offsets return offsets
} }
func (p *Pattern) prepareInput(item *Item) *Transformed { func (p *Pattern) prepareInput(item *Item) *[]Token {
if item.transformed != nil { if item.transformed != nil {
return item.transformed return item.transformed
} }
var ret *Transformed var ret *[]Token
if len(p.nth) > 0 { if len(p.nth) > 0 {
tokens := Tokenize(item.text, p.delimiter) tokens := Tokenize(item.text, p.delimiter)
ret = Transform(tokens, p.nth) ret = Transform(tokens, p.nth)
} else { } else {
trans := Transformed{ runes := []rune(*item.text)
whole: item.text, trans := []Token{Token{text: &runes, prefixLength: 0}}
parts: []Token{Token{text: item.text, prefixLength: 0}}}
ret = &trans ret = &trans
} }
item.transformed = ret item.transformed = ret
return ret return ret
} }
func (p *Pattern) iter(pfun func(bool, *string, []rune) (int, int), func (p *Pattern) iter(pfun func(bool, *[]rune, []rune) (int, int),
inputs *Transformed, pattern []rune) (int, int) { tokens *[]Token, pattern []rune) (int, int) {
for _, part := range inputs.parts { for _, part := range *tokens {
prefixLength := part.prefixLength prefixLength := part.prefixLength
if sidx, eidx := pfun(p.caseSensitive, part.text, pattern); sidx >= 0 { if sidx, eidx := pfun(p.caseSensitive, part.text, pattern); sidx >= 0 {
return sidx + prefixLength, eidx + prefixLength return sidx + prefixLength, eidx + prefixLength

View File

@@ -58,8 +58,8 @@ func TestExact(t *testing.T) {
clearPatternCache() clearPatternCache()
pattern := BuildPattern(ModeExtended, CaseSmart, pattern := BuildPattern(ModeExtended, CaseSmart,
[]Range{}, nil, []rune("'abc")) []Range{}, nil, []rune("'abc"))
str := "aabbcc abc" runes := []rune("aabbcc abc")
sidx, eidx := algo.ExactMatchNaive(pattern.caseSensitive, &str, pattern.terms[0].text) sidx, eidx := algo.ExactMatchNaive(pattern.caseSensitive, &runes, pattern.terms[0].text)
if sidx != 7 || eidx != 10 { if sidx != 7 || eidx != 10 {
t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
} }

View File

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

View File

@@ -22,18 +22,23 @@ import (
type Terminal struct { type Terminal struct {
prompt string prompt string
reverse bool reverse bool
hscroll bool
cx int cx int
cy int cy int
offset int offset int
yanked []rune yanked []rune
input []rune input []rune
multi bool multi bool
sort bool
toggleSort int
expect []int
pressed int
printQuery bool printQuery bool
count int count int
progress int progress int
reading bool reading bool
merger *Merger merger *Merger
selected map[*string]selectedItem selected map[uint32]selectedItem
reqBox *util.EventBox reqBox *util.EventBox
eventBox *util.EventBox eventBox *util.EventBox
mutex sync.Mutex mutex sync.Mutex
@@ -47,17 +52,17 @@ type selectedItem struct {
text *string text *string
} }
type ByTimeOrder []selectedItem type byTimeOrder []selectedItem
func (a ByTimeOrder) Len() int { func (a byTimeOrder) Len() int {
return len(a) return len(a)
} }
func (a ByTimeOrder) Swap(i, j int) { func (a byTimeOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i] a[i], a[j] = a[j], a[i]
} }
func (a ByTimeOrder) Less(i, j int) bool { func (a byTimeOrder) Less(i, j int) bool {
return a[i].at.Before(a[j].at) return a[i].at.Before(a[j].at)
} }
@@ -74,33 +79,33 @@ const (
reqQuit reqQuit
) )
const (
initialDelay = 100 * time.Millisecond
spinnerDuration = 200 * time.Millisecond
)
// NewTerminal returns new Terminal object // NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := []rune(opts.Query) input := []rune(opts.Query)
return &Terminal{ return &Terminal{
prompt: opts.Prompt, prompt: opts.Prompt,
reverse: opts.Reverse, reverse: opts.Reverse,
hscroll: opts.Hscroll,
cx: len(input), cx: len(input),
cy: 0, cy: 0,
offset: 0, offset: 0,
yanked: []rune{}, yanked: []rune{},
input: input, input: input,
multi: opts.Multi, multi: opts.Multi,
sort: opts.Sort > 0,
toggleSort: opts.ToggleSort,
expect: opts.Expect,
pressed: 0,
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[*string]selectedItem), selected: make(map[uint32]selectedItem),
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
eventBox: eventBox, eventBox: eventBox,
mutex: sync.Mutex{}, mutex: sync.Mutex{},
suppress: true, suppress: true,
startChan: make(chan bool, 1), startChan: make(chan bool, 1),
initFunc: func() { initFunc: func() {
C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse) C.Init(opts.Theme, opts.Black, opts.Mouse)
}} }}
} }
@@ -150,6 +155,19 @@ func (t *Terminal) output() {
if t.printQuery { if t.printQuery {
fmt.Println(string(t.input)) fmt.Println(string(t.input))
} }
if len(t.expect) > 0 {
if t.pressed == 0 {
fmt.Println()
} else if util.Between(t.pressed, C.AltA, C.AltZ) {
fmt.Printf("alt-%c\n", t.pressed+'a'-C.AltA)
} else if util.Between(t.pressed, C.F1, C.F4) {
fmt.Printf("f%c\n", t.pressed+'1'-C.F1)
} else if util.Between(t.pressed, C.CtrlA, C.CtrlZ) {
fmt.Printf("ctrl-%c\n", t.pressed+'a'-C.CtrlA)
} else {
fmt.Printf("%c\n", t.pressed-C.AltZ)
}
}
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 {
@@ -160,7 +178,7 @@ func (t *Terminal) output() {
for _, sel := range t.selected { for _, sel := range t.selected {
sels = append(sels, sel) sels = append(sels, sel)
} }
sort.Sort(ByTimeOrder(sels)) sort.Sort(byTimeOrder(sels))
for _, sel := range sels { for _, sel := range sels {
fmt.Println(*sel.text) fmt.Println(*sel.text)
} }
@@ -220,6 +238,13 @@ func (t *Terminal) printInfo() {
t.move(1, 2, false) t.move(1, 2, false)
output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count) output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count)
if t.toggleSort > 0 {
if t.sort {
output += "/S"
} else {
output += " "
}
}
if t.multi && len(t.selected) > 0 { if t.multi && len(t.selected) > 0 {
output += fmt.Sprintf(" (%d)", len(t.selected)) output += fmt.Sprintf(" (%d)", len(t.selected))
} }
@@ -243,7 +268,7 @@ func (t *Terminal) printList() {
} }
func (t *Terminal) printItem(item *Item, current bool) { func (t *Terminal) printItem(item *Item, current bool) {
_, selected := t.selected[item.text] _, selected := t.selected[item.index]
if current { if current {
C.CPrint(C.ColCursor, true, ">") C.CPrint(C.ColCursor, true, ">")
if selected { if selected {
@@ -251,7 +276,7 @@ func (t *Terminal) printItem(item *Item, current bool) {
} else { } else {
C.CPrint(C.ColCurrent, true, " ") C.CPrint(C.ColCurrent, true, " ")
} }
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch) t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true)
} else { } else {
C.CPrint(C.ColCursor, true, " ") C.CPrint(C.ColCursor, true, " ")
if selected { if selected {
@@ -259,7 +284,7 @@ func (t *Terminal) printItem(item *Item, current bool) {
} else { } else {
C.Print(" ") C.Print(" ")
} }
t.printHighlighted(item, false, 0, C.ColMatch) t.printHighlighted(item, false, 0, C.ColMatch, false)
} }
} }
@@ -299,7 +324,7 @@ func trimLeft(runes []rune, width int) ([]rune, int32) {
return runes, trimmed return runes, trimmed
} }
func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) { func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) {
var maxe int32 var maxe int32
for _, offset := range item.offsets { for _, offset := range item.offsets {
if offset[1] > maxe { if offset[1] > maxe {
@@ -309,10 +334,11 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) {
// Overflow // Overflow
text := []rune(*item.text) text := []rune(*item.text)
offsets := item.offsets offsets := item.colorOffsets(col2, bold, current)
maxWidth := C.MaxX() - 3 maxWidth := C.MaxX() - 3
fullWidth := displayWidth(text) fullWidth := displayWidth(text)
if fullWidth > maxWidth { if fullWidth > maxWidth {
if t.hscroll {
// Stri.. // Stri..
matchEndWidth := displayWidth(text[:maxe]) matchEndWidth := displayWidth(text[:maxe])
if matchEndWidth <= maxWidth-2 { if matchEndWidth <= maxWidth-2 {
@@ -328,37 +354,49 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) {
text, diff = trimLeft(text, maxWidth-2) text, diff = trimLeft(text, maxWidth-2)
// Transform offsets // Transform offsets
offsets = make([]Offset, len(item.offsets)) for idx, offset := range offsets {
for idx, offset := range item.offsets { b, e := offset.offset[0], offset.offset[1]
b, e := offset[0], offset[1]
b += 2 - diff b += 2 - diff
e += 2 - diff e += 2 - diff
b = util.Max32(b, 2) b = util.Max32(b, 2)
if b < e { offsets[idx].offset[0] = b
offsets[idx] = Offset{b, e} offsets[idx].offset[1] = util.Max32(b, e)
}
} }
text = append([]rune(".."), text...) text = append([]rune(".."), text...)
} }
} else {
text, _ = trimRight(text, maxWidth-2)
text = append(text, []rune("..")...)
for idx, offset := range offsets {
offsets[idx].offset[0] = util.Min32(offset.offset[0], int32(maxWidth-2))
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
}
}
} }
sort.Sort(ByOrder(offsets))
var index int32 var index int32
var substr string var substr string
var prefixWidth int var prefixWidth int
maxOffset := int32(len(text))
for _, offset := range offsets { for _, offset := range offsets {
b := util.Max32(index, offset[0]) b := util.Constrain32(offset.offset[0], index, maxOffset)
e := util.Max32(index, offset[1]) e := util.Constrain32(offset.offset[1], index, maxOffset)
substr, prefixWidth = processTabs(text[index:b], prefixWidth) substr, prefixWidth = processTabs(text[index:b], prefixWidth)
C.CPrint(col1, bold, substr) C.CPrint(col1, bold, substr)
if b < e {
substr, prefixWidth = processTabs(text[b:e], prefixWidth) substr, prefixWidth = processTabs(text[b:e], prefixWidth)
C.CPrint(col2, bold, substr) C.CPrint(offset.color, offset.bold, substr)
}
index = e index = e
if index >= maxOffset {
break
} }
if index < int32(len(text)) { }
if index < maxOffset {
substr, _ = processTabs(text[index:], prefixWidth) substr, _ = processTabs(text[index:], prefixWidth)
C.CPrint(col1, bold, substr) C.CPrint(col1, bold, substr)
} }
@@ -437,6 +475,10 @@ func (t *Terminal) rubout(pattern string) {
t.input = append(t.input[:t.cx], after...) t.input = append(t.input[:t.cx], after...)
} }
func keyMatch(key int, event C.Event) bool {
return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ
}
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
func (t *Terminal) Loop() { func (t *Terminal) Loop() {
<-t.startChan <-t.startChan
@@ -518,20 +560,35 @@ func (t *Terminal) Loop() {
toggle := func() { toggle := func() {
if t.cy < t.merger.Length() { if t.cy < t.merger.Length() {
item := t.merger.Get(t.cy) item := t.merger.Get(t.cy)
if _, found := t.selected[item.text]; !found { if _, found := t.selected[item.index]; !found {
var strptr *string var strptr *string
if item.origText != nil { if item.origText != nil {
strptr = item.origText strptr = item.origText
} else { } else {
strptr = item.text strptr = item.text
} }
t.selected[item.text] = selectedItem{time.Now(), strptr} t.selected[item.index] = selectedItem{time.Now(), strptr}
} else { } else {
delete(t.selected, item.text) delete(t.selected, item.index)
} }
req(reqInfo) req(reqInfo)
} }
} }
for _, key := range t.expect {
if keyMatch(key, event) {
t.pressed = key
req(reqClose)
break
}
}
if t.toggleSort > 0 {
if keyMatch(t.toggleSort, event) {
t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock()
continue
}
}
switch event.Type { switch event.Type {
case C.Invalid: case C.Invalid:
t.mutex.Unlock() t.mutex.Unlock()
@@ -661,7 +718,7 @@ func (t *Terminal) Loop() {
t.mutex.Unlock() // Must be unlocked before touching reqBox t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed { if changed {
t.eventBox.Set(EvtSearchNew, nil) t.eventBox.Set(EvtSearchNew, t.sort)
} }
for _, event := range events { for _, event := range events {
t.reqBox.Set(event, nil) t.reqBox.Set(event, nil)

View File

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

View File

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

View File

@@ -79,6 +79,7 @@ func (b *EventBox) Unwatch(events ...EventType) {
} }
} }
// WaitFor blocks the execution until the event is received
func (b *EventBox) WaitFor(event EventType) { func (b *EventBox) WaitFor(event EventType) {
looping := true looping := true
for looping { for looping {

View File

@@ -19,6 +19,14 @@ func Max(first int, items ...int) int {
return max return max
} }
// Max32 returns the smallest 32-bit integer
func Min32(first int32, second int32) int32 {
if first <= second {
return first
}
return second
}
// Max32 returns the largest 32-bit integer // Max32 returns the largest 32-bit integer
func Max32(first int32, second int32) int32 { func Max32(first int32, second int32) int32 {
if first > second { if first > second {
@@ -27,6 +35,17 @@ func Max32(first int32, second int32) int32 {
return second return second
} }
// Constrain32 limits the given 32-bit integer with the upper and lower bounds
func Constrain32(val int32, min int32, max int32) int32 {
if val < min {
return min
}
if val > max {
return max
}
return val
}
// Constrain limits the given integer with the upper and lower bounds // Constrain limits the given integer with the upper and lower bounds
func Constrain(val int, min int, max int) int { func Constrain(val int, min int, max int) int {
if val < min { if val < min {
@@ -50,7 +69,22 @@ func DurWithin(
return val return val
} }
func Between(val int, min int, max int) bool {
return val >= min && val <= max
}
// IsTty returns true is stdin is a terminal // IsTty returns true is stdin is a terminal
func IsTty() bool { func IsTty() bool {
return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 return int(C.isatty(C.int(os.Stdin.Fd()))) != 0
} }
func TrimRight(runes *[]rune) []rune {
var i int
for i = len(*runes) - 1; i >= 0; i-- {
char := (*runes)[i]
if char != ' ' && char != '\t' {
break
}
}
return (*runes)[0 : i+1]
}

View File

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

View File

@@ -4,6 +4,10 @@
require 'minitest/autorun' require 'minitest/autorun'
require 'fileutils' require 'fileutils'
base = File.expand_path('../../', __FILE__)
Dir.chdir base
FZF = "#{base}/bin/fzf"
class NilClass class NilClass
def include? str def include? str
false false
@@ -24,7 +28,8 @@ module Temp
waited = 0 waited = 0
while waited < 5 while waited < 5
begin begin
data = `cat #{name}` system 'sync'
data = File.read(name)
return data unless data.empty? return data unless data.empty?
rescue rescue
sleep 0.1 sleep 0.1
@@ -193,7 +198,7 @@ class TestBase < Minitest::Test
nil nil
end end
}.compact }.compact
"fzf #{opts.join ' '}" "#{FZF} #{opts.join ' '}"
end end
end end
@@ -241,7 +246,7 @@ class TestGoFZF < TestBase
end end
def test_key_bindings def test_key_bindings
tmux.send_keys "fzf -q 'foo bar foo-bar'", :Enter tmux.send_keys "#{FZF} -q 'foo bar foo-bar'", :Enter
tmux.until { |lines| lines.last =~ /^>/ } tmux.until { |lines| lines.last =~ /^>/ }
# CTRL-A # CTRL-A
@@ -423,6 +428,114 @@ class TestGoFZF < TestBase
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab, :Enter
assert_equal %w[1000 900 800], readonce.split($/) assert_equal %w[1000 900 800], readonce.split($/)
end end
def test_expect
test = lambda do |key, feed, expected = key|
tmux.send_keys "seq 1 100 | #{fzf :expect, key}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys '55'
tmux.send_keys *feed
assert_equal [expected, '55'], readonce.split($/)
end
test.call 'ctrl-t', 'C-T'
test.call 'ctrl-t', 'Enter', ''
test.call 'alt-c', [:Escape, :c]
test.call 'f1', 'f1'
test.call 'f2', 'f2'
test.call 'f3', 'f3'
test.call 'f2,f4', 'f2', 'f2'
test.call 'f2,f4', 'f4', 'f4'
test.call '@', '@'
end
def test_expect_print_query
tmux.send_keys "seq 1 100 | #{fzf '--expect=alt-z', :print_query}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys '55'
tmux.send_keys :Escape, :z
assert_equal ['55', 'alt-z', '55'], readonce.split($/)
end
def test_expect_print_query_select_1
tmux.send_keys "seq 1 100 | #{fzf '-q55 -1 --expect=alt-z --print-query'}", :Enter
assert_equal ['55', '', '55'], readonce.split($/)
end
def test_toggle_sort
tmux.send_keys "seq 1 111 | #{fzf '-m +s --tac --toggle-sort=ctrl-r -q11'}", :Enter
tmux.until { |lines| lines[-3].include? '> 111' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include? '4/111 (1)' }
tmux.send_keys 'C-R'
tmux.until { |lines| lines[-3].include? '> 11' }
tmux.send_keys :Tab
tmux.until { |lines| lines[-2].include? '4/111/S (2)' }
tmux.send_keys :Enter
assert_equal ['111', '11'], readonce.split($/)
end
def test_unicode_case
tempname = TEMPNAME + Time.now.to_f.to_s
writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4]
assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/)
assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/)
rescue
File.unlink tempname
end
def test_tiebreak
tempname = TEMPNAME + Time.now.to_f.to_s
input = %w[
--foobar--------
-----foobar---
----foobar--
-------foobar-
]
writelines tempname, input
assert_equal input, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=index`.split($/)
by_length = %w[
----foobar--
-----foobar---
-------foobar-
--foobar--------
]
assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar`.split($/)
assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=length`.split($/)
by_begin = %w[
--foobar--------
----foobar--
-----foobar---
-------foobar-
]
assert_equal by_begin, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=begin`.split($/)
assert_equal by_begin, `cat #{tempname} | #{FZF} -f"!z foobar" -x --tiebreak begin`.split($/)
assert_equal %w[
-------foobar-
----foobar--
-----foobar---
--foobar--------
], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/)
assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/)
rescue
File.unlink tempname
end
private
def writelines path, lines, timeout = 10
File.open(path, 'w') do |f|
f << lines.join($/)
f.sync
end
since = Time.now
while `cat #{path}`.split($/).length != lines.length && (Time.now - since) < 10
sleep 0.1
end
end
end end
module TestShell module TestShell
@@ -499,25 +612,29 @@ class TestBash < TestBase
def test_file_completion def test_file_completion
tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}', :Enter tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}', :Enter
tmux.prepare tmux.prepare
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0
tmux.until { |lines| lines[-1].start_with? '>' } tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' }
tmux.send_keys :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :Enter
tmux.until { |lines| tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1].include?('/tmp/fzf-test/10') && lines[-1].include?('/tmp/fzf-test/10') &&
lines[-1].include?('/tmp/fzf-test/100') lines[-1].include?('/tmp/fzf-test/100')
} end
end end
def test_dir_completion def test_dir_completion
tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter
tmux.prepare tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0
tmux.until { |lines| lines[-1].start_with? '>' } tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' }
tmux.send_keys :BTab, :BTab # BTab does not work here tmux.send_keys :BTab, :BTab # BTab does not work here
tmux.send_keys 55 tmux.send_keys 55
tmux.until { |lines| lines[-2].start_with? ' 1/' } tmux.until(pane: 1) { |lines| lines[-2].start_with? ' 1/' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' } tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1] == 'cd /tmp/fzf-test/d55/'
end
tmux.send_keys :xx tmux.send_keys :xx
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
@@ -537,12 +654,15 @@ class TestBash < TestBase
lines = tmux.until { |lines| lines[-1].start_with? '[1]' } lines = tmux.until { |lines| lines[-1].start_with? '[1]' }
pid = lines[-1].split.last pid = lines[-1].split.last
tmux.prepare tmux.prepare
tmux.send_keys 'kill ', :Tab tmux.send_keys 'kill ', :Tab, pane: 0
tmux.until { |lines| lines[-1].start_with? '>' } tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' }
tmux.send_keys 'sleep12345' tmux.send_keys 'sleep12345'
tmux.until { |lines| lines[-3].include? 'sleep 12345' } tmux.until(pane: 1) { |lines| lines[-3].include? 'sleep 12345' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == "kill #{pid}" } tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1] == "kill #{pid}"
end
end end
end end

View File

@@ -1,850 +0,0 @@
#!/usr/bin/env ruby
# encoding: utf-8
require 'rubygems'
require 'curses'
require 'timeout'
require 'stringio'
require 'minitest/autorun'
require 'tempfile'
$LOAD_PATH.unshift File.expand_path('../..', __FILE__)
ENV['FZF_EXECUTABLE'] = '0'
load 'fzf'
class MockTTY
def initialize
@buffer = ''
@mutex = Mutex.new
@condv = ConditionVariable.new
end
def read_nonblock sz
@mutex.synchronize do
take sz
end
end
def take sz
if @buffer.length >= sz
ret = @buffer[0, sz]
@buffer = @buffer[sz..-1]
ret
end
end
def getc
sleep 0.1
while true
@mutex.synchronize do
if char = take(1)
return char
else
@condv.wait(@mutex)
end
end
end
end
def << str
@mutex.synchronize do
@buffer << str
@condv.broadcast
end
self
end
end
class TestRubyFZF < Minitest::Test
def setup
ENV.delete 'FZF_DEFAULT_SORT'
ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_DEFAULT_COMMAND'
end
def test_default_options
fzf = FZF.new []
assert_equal 1000, fzf.sort
assert_equal false, fzf.multi
assert_equal true, fzf.color
assert_equal nil, fzf.rxflag
assert_equal true, fzf.mouse
assert_equal nil, fzf.nth
assert_equal nil, fzf.with_nth
assert_equal true, fzf.color
assert_equal false, fzf.black
assert_equal true, fzf.ansi256
assert_equal '', fzf.query
assert_equal false, fzf.select1
assert_equal false, fzf.exit0
assert_equal nil, fzf.filter
assert_equal nil, fzf.extended
assert_equal false, fzf.reverse
assert_equal '> ', fzf.prompt
assert_equal false, fzf.print_query
end
def test_environment_variables
# Deprecated
ENV['FZF_DEFAULT_SORT'] = '20000'
fzf = FZF.new []
assert_equal 20000, fzf.sort
assert_equal nil, fzf.nth
ENV['FZF_DEFAULT_OPTS'] =
'-x -m -s 10000 -q " hello world " +c +2 --select-1 -0 ' <<
'--no-mouse -f "goodbye world" --black --with-nth=3,-3..,2 --nth=3,-1,2 --reverse --print-query'
fzf = FZF.new []
assert_equal 10000, fzf.sort
assert_equal ' hello world ',
fzf.query
assert_equal 'goodbye world',
fzf.filter
assert_equal :fuzzy, fzf.extended
assert_equal true, fzf.multi
assert_equal false, fzf.color
assert_equal false, fzf.ansi256
assert_equal true, fzf.black
assert_equal false, fzf.mouse
assert_equal true, fzf.select1
assert_equal true, fzf.exit0
assert_equal true, fzf.reverse
assert_equal true, fzf.print_query
assert_equal [2..2, -1..-1, 1..1], fzf.nth
assert_equal [2..2, -3..-1, 1..1], fzf.with_nth
end
def test_option_parser
# Long opts
fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query hello --select-1
--exit-0 --filter=howdy --extended-exact
--no-mouse --no-256 --nth=1 --with-nth=.. --reverse --prompt (hi)
--print-query]
assert_equal 2000, fzf.sort
assert_equal true, fzf.multi
assert_equal false, fzf.color
assert_equal false, fzf.ansi256
assert_equal false, fzf.black
assert_equal false, fzf.mouse
assert_equal 0, fzf.rxflag
assert_equal 'hello', fzf.query
assert_equal true, fzf.select1
assert_equal true, fzf.exit0
assert_equal 'howdy', fzf.filter
assert_equal :exact, fzf.extended
assert_equal [0..0], fzf.nth
assert_equal nil, fzf.with_nth
assert_equal true, fzf.reverse
assert_equal '(hi)', fzf.prompt
assert_equal true, fzf.print_query
# Long opts (left-to-right)
fzf = FZF.new %w[--sort=2000 --no-color --multi +i --query=hello
--filter a --filter b --no-256 --black --nth -1 --nth -2
--select-1 --exit-0 --no-select-1 --no-exit-0
--no-sort -i --color --no-multi --256
--reverse --no-reverse --prompt (hi) --prompt=(HI)
--print-query --no-print-query]
assert_equal nil, fzf.sort
assert_equal false, fzf.multi
assert_equal true, fzf.color
assert_equal true, fzf.ansi256
assert_equal true, fzf.black
assert_equal true, fzf.mouse
assert_equal 1, fzf.rxflag
assert_equal 'b', fzf.filter
assert_equal 'hello', fzf.query
assert_equal false, fzf.select1
assert_equal false, fzf.exit0
assert_equal nil, fzf.extended
assert_equal [-2..-2], fzf.nth
assert_equal false, fzf.reverse
assert_equal '(HI)', fzf.prompt
assert_equal false, fzf.print_query
# Short opts
fzf = FZF.new %w[-s2000 +c -m +i -qhello -x -fhowdy +2 -n3 -1 -0]
assert_equal 2000, fzf.sort
assert_equal true, fzf.multi
assert_equal false, fzf.color
assert_equal false, fzf.ansi256
assert_equal 0, fzf.rxflag
assert_equal 'hello', fzf.query
assert_equal 'howdy', fzf.filter
assert_equal :fuzzy, fzf.extended
assert_equal [2..2], fzf.nth
assert_equal true, fzf.select1
assert_equal true, fzf.exit0
# Left-to-right
fzf = FZF.new %w[-s 2000 +c -m +i -qhello -x -fgoodbye +2 -n3 -n4,5
-s 3000 -c +m -i -q world +x -fworld -2 --black --no-black
-1 -0 +1 +0
]
assert_equal 3000, fzf.sort
assert_equal false, fzf.multi
assert_equal true, fzf.color
assert_equal true, fzf.ansi256
assert_equal false, fzf.black
assert_equal 1, fzf.rxflag
assert_equal 'world', fzf.query
assert_equal false, fzf.select1
assert_equal false, fzf.exit0
assert_equal 'world', fzf.filter
assert_equal nil, fzf.extended
assert_equal [3..3, 4..4], fzf.nth
rescue SystemExit => e
assert false, "Exited"
end
def test_invalid_option
[
%w[--unknown],
%w[yo dawg],
%w[--nth=0],
%w[-n 0],
%w[-n 1..2..3],
%w[-n 1....],
%w[-n ....3],
%w[-n 1....3],
%w[-n 1..0],
%w[--nth ..0],
].each do |argv|
assert_raises(SystemExit) do
fzf = FZF.new argv
end
end
end
def test_width
fzf = FZF.new []
assert_equal 5, fzf.width('abcde')
assert_equal 4, fzf.width('한글')
assert_equal 5, fzf.width('한글.')
end if RUBY_VERSION >= '1.9'
def test_trim
fzf = FZF.new []
assert_equal ['사.', 6], fzf.trim('가나다라마바사.', 4, true)
assert_equal ['바사.', 5], fzf.trim('가나다라마바사.', 5, true)
assert_equal ['바사.', 5], fzf.trim('가나다라마바사.', 6, true)
assert_equal ['마바사.', 4], fzf.trim('가나다라마바사.', 7, true)
assert_equal ['가나', 6], fzf.trim('가나다라마바사.', 4, false)
assert_equal ['가나', 6], fzf.trim('가나다라마바사.', 5, false)
assert_equal ['가나a', 6], fzf.trim('가나ab라마바사.', 5, false)
assert_equal ['가나ab', 5], fzf.trim('가나ab라마바사.', 6, false)
assert_equal ['가나ab', 5], fzf.trim('가나ab라마바사.', 7, false)
end if RUBY_VERSION >= '1.9'
def test_format
fzf = FZF.new []
assert_equal [['01234..', false]], fzf.format('0123456789', 7, [])
assert_equal [['012', false], ['34', true], ['..', false]],
fzf.format('0123456789', 7, [[3, 5]])
assert_equal [['..56', false], ['789', true]],
fzf.format('0123456789', 7, [[7, 10]])
assert_equal [['..56', false], ['78', true], ['9', false]],
fzf.format('0123456789', 7, [[7, 9]])
(3..5).each do |i|
assert_equal [['..', false], ['567', true], ['89', false]],
fzf.format('0123456789', 7, [[i, 8]])
end
assert_equal [['..', false], ['345', true], ['..', false]],
fzf.format('0123456789', 7, [[3, 6]])
assert_equal [['012', false], ['34', true], ['..', false]],
fzf.format('0123456789', 7, [[3, 5]])
# Multi-region
assert_equal [["0", true], ["1", false], ["2", true], ["34..", false]],
fzf.format('0123456789', 7, [[0, 1], [2, 3]])
assert_equal [["..", false], ["5", true], ["6", false], ["78", true], ["9", false]],
fzf.format('0123456789', 7, [[3, 6], [7, 9]])
assert_equal [["..", false], ["3", true], ["4", false], ["5", true], ["..", false]],
fzf.format('0123456789', 7, [[3, 4], [5, 6]])
# Multi-region Overlap
assert_equal [["..", false], ["345", true], ["..", false]],
fzf.format('0123456789', 7, [[4, 5], [3, 6]])
end
def test_fuzzy_matcher
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE
list = %w[
juice
juiceful
juiceless
juicily
juiciness
juicy]
assert matcher.caches.empty?
assert_equal(
[["juice", [[0, 1]]],
["juiceful", [[0, 1]]],
["juiceless", [[0, 1]]],
["juicily", [[0, 1]]],
["juiciness", [[0, 1]]],
["juicy", [[0, 1]]]], matcher.match(list, 'j', '', '').sort)
assert !matcher.caches.empty?
assert_equal [list.object_id], matcher.caches.keys
assert_equal 1, matcher.caches[list.object_id].length
assert_equal 6, matcher.caches[list.object_id]['j'].length
assert_equal(
[["juicily", [[0, 5]]],
["juiciness", [[0, 5]]]], matcher.match(list, 'jii', '', '').sort)
assert_equal(
[["juicily", [[2, 5]]],
["juiciness", [[2, 5]]]], matcher.match(list, 'ii', '', '').sort)
assert_equal 3, matcher.caches[list.object_id].length
assert_equal 2, matcher.caches[list.object_id]['ii'].length
# TODO : partial_cache
end
def test_fuzzy_matcher_rxflag
assert_equal nil, FZF::FuzzyMatcher.new(nil).rxflag
assert_equal 0, FZF::FuzzyMatcher.new(0).rxflag
assert_equal 1, FZF::FuzzyMatcher.new(1).rxflag
assert_equal 1, FZF::FuzzyMatcher.new(nil).rxflag_for('abc')
assert_equal 0, FZF::FuzzyMatcher.new(nil).rxflag_for('Abc')
assert_equal 0, FZF::FuzzyMatcher.new(0).rxflag_for('abc')
assert_equal 0, FZF::FuzzyMatcher.new(0).rxflag_for('Abc')
assert_equal 1, FZF::FuzzyMatcher.new(1).rxflag_for('abc')
assert_equal 1, FZF::FuzzyMatcher.new(1).rxflag_for('Abc')
end
def test_fuzzy_matcher_case_sensitive
# Smart-case match (Uppercase found)
assert_equal [['Fruit', [[0, 5]]]],
FZF::FuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], 'Fruit', '', '').sort
# Smart-case match (Uppercase not-found)
assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
FZF::FuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], 'fruit', '', '').sort
# Case-sensitive match (-i)
assert_equal [['Fruit', [[0, 5]]]],
FZF::FuzzyMatcher.new(0).match(%w[Fruit Grapefruit], 'Fruit', '', '').sort
# Case-insensitive match (+i)
assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
FZF::FuzzyMatcher.new(Regexp::IGNORECASE).
match(%w[Fruit Grapefruit], 'Fruit', '', '').sort
end
def test_extended_fuzzy_matcher_case_sensitive
%w['Fruit Fruit$].each do |q|
# Smart-case match (Uppercase found)
assert_equal [['Fruit', [[0, 5]]]],
FZF::ExtendedFuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], q, '', '').sort
# Smart-case match (Uppercase not-found)
assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
FZF::ExtendedFuzzyMatcher.new(nil).match(%w[Fruit Grapefruit], q.downcase, '', '').sort
# Case-sensitive match (-i)
assert_equal [['Fruit', [[0, 5]]]],
FZF::ExtendedFuzzyMatcher.new(0).match(%w[Fruit Grapefruit], q, '', '').sort
# Case-insensitive match (+i)
assert_equal [["Fruit", [[0, 5]]], ["Grapefruit", [[5, 10]]]],
FZF::ExtendedFuzzyMatcher.new(Regexp::IGNORECASE).
match(%w[Fruit Grapefruit], q, '', '').sort
end
end
def test_extended_fuzzy_matcher
matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE
list = %w[
juice
juiceful
juiceless
juicily
juiciness
juicy
_juice]
match = proc { |q, prefix|
matcher.match(list, q, prefix, '').sort.map { |p| [p.first, p.last.sort] }
}
assert matcher.caches.empty?
3.times do
['y j', 'j y'].each do |pat|
(0..pat.length - 1).each do |prefix_length|
prefix = pat[0, prefix_length]
assert_equal(
[["juicily", [[0, 1], [6, 7]]],
["juicy", [[0, 1], [4, 5]]]],
match.call(pat, prefix))
end
end
# $
assert_equal [["juiceful", [[7, 8]]]], match.call('l$', '')
assert_equal [["juiceful", [[7, 8]]],
["juiceless", [[5, 6]]],
["juicily", [[5, 6]]]], match.call('l', '')
# ^
assert_equal list.length, match.call('j', '').length
assert_equal list.length - 1, match.call('^j', '').length
# ^ + $
assert_equal 0, match.call('^juici$', '').length
assert_equal 1, match.call('^juice$', '').length
assert_equal 0, match.call('^.*$', '').length
# !
assert_equal 0, match.call('!j', '').length
# ! + ^
assert_equal [["_juice", []]], match.call('!^j', '')
# ! + $
assert_equal list.length - 1, match.call('!l$', '').length
# ! + f
assert_equal [["juicy", [[4, 5]]]], match.call('y !l', '')
# '
assert_equal %w[juiceful juiceless juicily],
match.call('il', '').map { |e| e.first }
assert_equal %w[juicily],
match.call("'il", '').map { |e| e.first }
assert_equal (list - %w[juicily]).sort,
match.call("!'il", '').map { |e| e.first }.sort
end
assert !matcher.caches.empty?
end
def test_xfuzzy_matcher_prefix_cache
matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE
list = %w[
a.java
b.java
java.jive
c.java$
d.java
]
2.times do
assert_equal 5, matcher.match(list, 'java', 'java', '').length
assert_equal 3, matcher.match(list, 'java$', 'java$', '').length
assert_equal 1, matcher.match(list, 'java$$', 'java$$', '').length
assert_equal 0, matcher.match(list, '!java', '!java', '').length
assert_equal 4, matcher.match(list, '!^jav', '!^jav', '').length
assert_equal 4, matcher.match(list, '!^java', '!^java', '').length
assert_equal 2, matcher.match(list, '!^java !b !c', '!^java', '').length
end
end
def test_sort_by_rank
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE
xmatcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE
list = %w[
0____1
0_____1
01
____0_1
01_
_01_
0______1
___01___
]
assert_equal(
[["01", [[0, 2]]],
["01_", [[0, 2]]],
["_01_", [[1, 3]]],
["___01___", [[3, 5]]],
["____0_1", [[4, 7]]],
["0____1", [[0, 6]]],
["0_____1", [[0, 7]]],
["0______1", [[0, 8]]]],
FZF.sort(matcher.match(list, '01', '', '')))
assert_equal(
[["01", [[0, 1], [1, 2]]],
["01_", [[0, 1], [1, 2]]],
["_01_", [[1, 2], [2, 3]]],
["0____1", [[0, 1], [5, 6]]],
["0_____1", [[0, 1], [6, 7]]],
["____0_1", [[4, 5], [6, 7]]],
["0______1", [[0, 1], [7, 8]]],
["___01___", [[3, 4], [4, 5]]]],
FZF.sort(xmatcher.match(list, '0 1', '', '')))
assert_equal(
[["_01_", [[1, 3], [0, 4]], [4, 4, "_01_"]],
["___01___", [[3, 5], [0, 2]], [4, 8, "___01___"]],
["____0_1", [[4, 7], [0, 2]], [5, 7, "____0_1"]],
["0____1", [[0, 6], [1, 3]], [6, 6, "0____1"]],
["0_____1", [[0, 7], [1, 3]], [7, 7, "0_____1"]],
["0______1", [[0, 8], [1, 3]], [8, 8, "0______1"]]],
FZF.sort(xmatcher.match(list, '01 __', '', '')).map { |tuple|
tuple << FZF.rank(tuple)
}
)
end
def test_extended_exact_mode
exact = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :exact
fuzzy = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy
list = %w[
extended-exact-mode-not-fuzzy
extended'-fuzzy-mode
]
assert_equal 2, fuzzy.match(list, 'extended', '', '').length
assert_equal 2, fuzzy.match(list, 'mode extended', '', '').length
assert_equal 2, fuzzy.match(list, 'xtndd', '', '').length
assert_equal 2, fuzzy.match(list, "'-fuzzy", '', '').length
assert_equal 2, exact.match(list, 'extended', '', '').length
assert_equal 2, exact.match(list, 'mode extended', '', '').length
assert_equal 0, exact.match(list, 'xtndd', '', '').length
assert_equal 1, exact.match(list, "'-fuzzy", '', '').length
assert_equal 2, exact.match(list, "-fuzzy", '', '').length
end
# ^$ -> matches empty item
def test_format_empty_item
fzf = FZF.new []
item = ['', [[0, 0]]]
line, offsets = item
tokens = fzf.format line, 80, offsets
assert_equal [], tokens
end
def test_mouse_event
interval = FZF::MouseEvent::DOUBLE_CLICK_INTERVAL
me = FZF::MouseEvent.new nil
me.v = 10
assert_equal false, me.double?(10)
assert_equal false, me.double?(20)
me.v = 20
assert_equal false, me.double?(10)
assert_equal false, me.double?(20)
me.v = 20
assert_equal false, me.double?(10)
assert_equal true, me.double?(20)
sleep interval
assert_equal false, me.double?(20)
end
def test_nth_match
list = [
' first second third',
'fourth fifth sixth',
]
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE
assert_equal list, matcher.match(list, 'f', '', '').map(&:first)
assert_equal [
[list[0], [[2, 5]]],
[list[1], [[9, 17]]]], matcher.match(list, 'is', '', '')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1]
assert_equal [[list[1], [[8, 9]]]], matcher.match(list, 'f', '', '')
assert_equal [[list[0], [[8, 9]]]], matcher.match(list, 's', '', '')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2..2]
assert_equal [[list[0], [[19, 20]]]], matcher.match(list, 'r', '', '')
# Comma-separated
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [2..2, 0..0]
assert_equal [[list[0], [[19, 20]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '')
# Ordered
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0, 2..2]
assert_equal [[list[0], [[3, 4]]], [list[1], [[3, 4]]]], matcher.match(list, 'r', '', '')
regex = FZF.build_delim_regex "\t"
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0], regex
assert_equal [[list[0], [[3, 10]]]], matcher.match(list, 're', '', '')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1], regex
assert_equal [], matcher.match(list, 'r', '', '')
assert_equal [[list[1], [[9, 17]]]], matcher.match(list, 'is', '', '')
# Negative indexing
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [-1..-1], regex
assert_equal [[list[0], [[3, 6]]]], matcher.match(list, 'rt', '', '')
assert_equal [[list[0], [[2, 5]]], [list[1], [[9, 17]]]], matcher.match(list, 'is', '', '')
# Regex delimiter
regex = FZF.build_delim_regex "[ \t]+"
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [0..0], regex
assert_equal [list[1]], matcher.match(list, 'f', '', '').map(&:first)
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..1], regex
assert_equal [[list[0], [[1, 2]]], [list[1], [[8, 9]]]], matcher.match(list, 'f', '', '')
end
def test_nth_match_range
list = [
' first second third',
'fourth fifth sixth',
]
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..2]
assert_equal [[list[0], [[8, 20]]]], matcher.match(list, 'sr', '', '')
assert_equal [], matcher.match(list, 'fo', '', '')
matcher = FZF::FuzzyMatcher.new Regexp::IGNORECASE, [1..-1, 0..0]
assert_equal [[list[0], [[8, 20]]]], matcher.match(list, 'sr', '', '')
assert_equal [[list[1], [[0, 2]]]], matcher.match(list, 'fo', '', '')
matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [0..0, 1..2]
assert_equal [], matcher.match(list, '^t', '', '')
matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [0..1, 2..2]
assert_equal [[list[0], [[16, 17]]]], matcher.match(list, '^t', '', '')
matcher = FZF::ExtendedFuzzyMatcher.new Regexp::IGNORECASE, :fuzzy, [1..-1]
assert_equal [[list[0], [[8, 9]]]], matcher.match(list, '^s', '', '')
end
def stream_for str, delay = 0
StringIO.new(str).tap do |sio|
sio.instance_eval do
alias org_gets gets
def gets
org_gets.tap { |e| sleep(@delay) unless e.nil? }
end
def reopen _
end
end
sio.instance_variable_set :@delay, delay
end
end
def assert_fzf_output opts, given, expected
stream = stream_for given
output = stream_for ''
def sorted_lines line
line.split($/).sort
end
begin
tty = MockTTY.new
$stdout = output
fzf = FZF.new(opts, stream)
fzf.instance_variable_set :@tty, tty
thr = block_given? && Thread.new { yield tty }
fzf.start
thr && thr.join
rescue SystemExit => e
assert_equal 0, e.status
assert_equal sorted_lines(expected), sorted_lines(output.string)
ensure
$stdout = STDOUT
end
end
def test_filter
{
%w[--filter=ol] => 'World',
%w[--filter=ol --print-query] => "ol\nWorld",
}.each do |opts, expected|
assert_fzf_output opts, "Hello\nWorld", expected
end
end
def test_select_1
{
%w[--query=ol --select-1] => 'World',
%w[--query=ol --select-1 --print-query] => "ol\nWorld",
}.each do |opts, expected|
assert_fzf_output opts, "Hello\nWorld", expected
end
end
def test_select_1_without_query
assert_fzf_output %w[--select-1], 'Hello World', 'Hello World'
end
def test_select_1_ambiguity
begin
Timeout::timeout(0.5) do
assert_fzf_output %w[--query=o --select-1], "hello\nworld", "should not match"
end
rescue Timeout::Error
Curses.close_screen
end
end
def test_exit_0
{
%w[--query=zz --exit-0] => '',
%w[--query=zz --exit-0 --print-query] => 'zz',
}.each do |opts, expected|
assert_fzf_output opts, "Hello\nWorld", expected
end
end
def test_exit_0_without_query
assert_fzf_output %w[--exit-0], '', ''
end
def test_with_nth
source = "hello world\nbatman"
assert_fzf_output %w[-0 -1 --with-nth=2,1 -x -q ^worl],
source, 'hello world'
assert_fzf_output %w[-0 -1 --with-nth=2,1 -x -q llo$],
source, 'hello world'
assert_fzf_output %w[-0 -1 --with-nth=.. -x -q llo$],
source, ''
assert_fzf_output %w[-0 -1 --with-nth=2,2,2,..,1 -x -q worlworlworlhellworlhell],
source, 'hello world'
assert_fzf_output %w[-0 -1 --with-nth=1,1,-1,1 -x -q batbatbatbat],
source, 'batman'
end
def test_with_nth_transform
fzf = FZF.new %w[--with-nth 2..,1]
assert_equal 'my world hello', fzf.transform('hello my world')
assert_equal 'my world hello', fzf.transform('hello my world')
assert_equal 'my world hello', fzf.transform('hello my world ')
fzf = FZF.new %w[--with-nth 2,-1,2]
assert_equal 'my world my', fzf.transform('hello my world')
assert_equal 'world world world', fzf.transform('hello world')
assert_equal 'world world world', fzf.transform('hello world ')
end
def test_ranking_overlap_match_regions
list = [
'1 3 4 2',
'1 2 3 4'
]
assert_equal [
['1 2 3 4', [[0, 13], [16, 22]]],
['1 3 4 2', [[0, 24], [12, 17]]],
], FZF.sort(FZF::ExtendedFuzzyMatcher.new(nil).match(list, '12 34', '', ''))
end
def test_constrain
fzf = FZF.new []
# [#**** ]
assert_equal [false, 0, 0], fzf.constrain(0, 0, 5, 100)
# *****[**#** ... ] => [**#******* ... ]
assert_equal [true, 0, 2], fzf.constrain(5, 7, 10, 100)
# [**********]**#** => ***[*********#]**
assert_equal [true, 3, 12], fzf.constrain(0, 12, 15, 10)
# *****[**#** ] => ***[**#****]
assert_equal [true, 3, 5], fzf.constrain(5, 7, 10, 7)
# *****[**#** ] => ****[**#***]
assert_equal [true, 4, 6], fzf.constrain(5, 7, 10, 6)
# ***** [#] => ****[#]
assert_equal [true, 4, 4], fzf.constrain(10, 10, 5, 1)
# [ ] #**** => [#]****
assert_equal [true, 0, 0], fzf.constrain(-5, 0, 5, 1)
# [ ] **#** => **[#]**
assert_equal [true, 2, 2], fzf.constrain(-5, 2, 5, 1)
# [***** #] => [****# ]
assert_equal [true, 0, 4], fzf.constrain(0, 7, 5, 10)
# **[***** #] => [******# ]
assert_equal [true, 0, 6], fzf.constrain(2, 10, 7, 10)
end
def test_invalid_utf8
tmp = Tempfile.new('fzf')
tmp << 'hello ' << [0xff].pack('C*') << ' world' << $/ << [0xff].pack('C*')
tmp.close
begin
Timeout::timeout(0.5) do
FZF.new(%w[-n..,1,2.. -q^ -x], File.open(tmp.path)).start
end
rescue Timeout::Error
Curses.close_screen
end
ensure
tmp.unlink
end
def test_with_nth_mock_tty
# Manual selection with input
assert_fzf_output ["--with-nth=2,1"], "hello world", "hello world" do |tty|
tty << "world"
tty << "hell"
tty << "\r"
end
# Manual selection without input
assert_fzf_output ["--with-nth=2,1"], "hello world", "hello world" do |tty|
tty << "\r"
end
# Manual selection with input and --multi
lines = "hello world\ngoodbye world"
assert_fzf_output %w[-m --with-nth=2,1], lines, lines do |tty|
tty << "o"
tty << "\e[Z\e[Z"
tty << "\r"
end
# Manual selection without input and --multi
assert_fzf_output %w[-m --with-nth=2,1], lines, lines do |tty|
tty << "\e[Z\e[Z"
tty << "\r"
end
# ALT-D
assert_fzf_output %w[--print-query], "", "hello baby = world" do |tty|
tty << "hello world baby"
tty << alt(:b) << alt(:b) << alt(:d)
tty << ctrl(:e) << " = " << ctrl(:y)
tty << "\r"
end
# ALT-BACKSPACE
assert_fzf_output %w[--print-query], "", "hello baby = world " do |tty|
tty << "hello world baby"
tty << alt(:b) << alt(127.chr)
tty << ctrl(:e) << " = " << ctrl(:y)
tty << "\r"
end
# Word-movements
assert_fzf_output %w[--print-query], "", "ello!_orld!~ foo=?" do |tty|
tty << "hello_world==baby?"
tty << alt(:b) << ctrl(:d)
tty << alt(:b) << ctrl(:d)
tty << alt(:b) << ctrl(:d)
tty << alt(:f) << '!'
tty << alt(:f) << '!'
tty << alt(:d) << '~'
tty << " foo=bar foo=bar"
tty << ctrl(:w)
tty << alt(127.chr)
tty << "\r"
end
end
def alt chr
"\e#{chr}"
end
def ctrl char
char.to_s.ord - 'a'.ord + 1
end
end

View File

@@ -68,6 +68,7 @@ fi
if [ -d ~/.config/fish/functions ]; then if [ -d ~/.config/fish/functions ]; then
remove ~/.config/fish/functions/fzf.fish remove ~/.config/fish/functions/fzf.fish
remove ~/.config/fish/functions/fzf_key_bindings.fish
if [ "$(ls -A ~/.config/fish/functions)" ]; then if [ "$(ls -A ~/.config/fish/functions)" ]; then
echo "Can't delete non-empty directory: \"~/.config/fish/functions\"" echo "Can't delete non-empty directory: \"~/.config/fish/functions\""