Compare commits

..

75 Commits

Author SHA1 Message Date
Junegunn Choi
64afff6b9a 0.10.9 2015-11-03 23:03:49 +09:00
Junegunn Choi
6bddffbca4 Setup signal handlers before ncurses initialization
This prevents fzf from missing SIGWINCH during startup which
occasionally happens with fzf-tmux
2015-11-03 23:00:34 +09:00
Junegunn Choi
81a88693c1 Make --extended default
Close #400
2015-11-03 22:49:32 +09:00
Junegunn Choi
68541e66b7 [man] double-click for --bind (#374) 2015-11-03 22:40:45 +09:00
Junegunn Choi
672b593634 Update FZF_DEFAULT_COMMAND example (#310) 2015-11-03 22:25:50 +09:00
Junegunn Choi
5769d3867d [nvim] setf fzf 2015-10-31 00:18:23 +09:00
Junegunn Choi
724ffa3756 [install] Do not download binary if it's found in $PATH (#373)
/cc @xconstruct
2015-10-26 12:31:43 +09:00
Junegunn Choi
5694b5ed30 Fix #394 - --bin option is broken 2015-10-23 17:43:34 +09:00
Junegunn Choi
a1184ceb4e Fix travis CI build 2015-10-23 15:07:16 +09:00
Junegunn Choi
02203c7739 Add command-line flags to install script
Close #392

  usage: ./install [OPTIONS]

      --help               Show this message
      --bin                Download fzf binary only
      --all                Download fzf binary and update configuration files
                           to enable key bindings and fuzzy completion
      --[no-]key-bindings  Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C)
      --[no-]completion    Enable/disable fuzzy completion (bash & zsh)
      --[no-]update-rc     Whether or not to update shell configuration files
2015-10-23 15:04:32 +09:00
Junegunn Choi
4d709e0dd2 Fix #391 - Strip non-printable characters 2015-10-23 01:12:31 +09:00
Junegunn Choi
ae04f56dbd Fix --bind "double-click:execute(...)" (#374) 2015-10-13 02:36:11 +09:00
Junegunn Choi
f80ff8c917 Add bindable double-click event (#374) 2015-10-13 02:24:38 +09:00
Junegunn Choi
b4ce89bbf5 [build] Link libncursesw when building 64-bit linux binary
Close #376
2015-10-12 16:02:08 +09:00
Junegunn Choi
486b87d821 [bash-completion] Retain original completion options (#288) 2015-10-12 00:27:30 +09:00
Junegunn Choi
b3010a4624 0.10.8 2015-10-09 12:42:07 +09:00
Junegunn Choi
7d53051ec8 Merge pull request #371 from wilywampa/edit_directory
Trigger netrw autocommand when opening directory
2015-10-09 12:36:08 +09:00
Jacob Niehus
ed893c5f47 Trigger netrw autocommand when opening directory 2015-10-08 20:28:07 -07:00
Junegunn Choi
a4eb3323da Fix #370 - Panic when trying to set colors when colors are disabled 2015-10-09 12:16:47 +09:00
Junegunn Choi
1da065e50e 0.10.7 2015-10-05 23:28:24 +09:00
Junegunn Choi
86bc9d506f Fix invalid interrupt handler during execute action
Interrupt handling during execute action was not serialized and often
caused crash, failed to restore the terminal state.
2015-10-05 23:19:26 +09:00
Junegunn Choi
eee45a9578 [completion] Revamp completion API
* _fzf_complete is the helper function for custom completion
    * _fzf_complete FZF_OPTS ARGS
    * Reads the output of the source command instead of the command string
    * In zsh, you can use pipe to feed the data into the function, but
      it's not possible in bash as by doing so COMPREPLY is set from the
      subshell and thus nullified
* Change the naming convention for consistency:
    * _fzf_complete_COMMAND

e.g.

  # pass completion suggested by @d4ndo (#362)
  _fzf_complete_pass() {
    _fzf_complete '+m' "$@" < <(
      local pwdir=${PASSWORD_STORE_DIR-~/.password-store/}
      local stringsize="${#pwdir}"
      find "$pwdir" -name "*.gpg" -print |
          cut -c "$((stringsize + 1))"-  |
          sed -e 's/\(.*\)\.gpg/\1/'
    )
  }

  # Only in bash
  complete -F _fzf_complete_pass -o default -o bashdefault pass
2015-10-05 19:34:38 +09:00
Junegunn Choi
659f49a09a [fzf-tmux] Create temp files in $TMPDIR if defined 2015-10-05 13:01:09 +09:00
Junegunn Choi
8fa9e85980 [zsh-completion] Allow custom completion function
While in bash you can externally register custom completion functions
using `complete` command, it was not possible to do so in zsh without
changing completion.zsh as the name of the supported commands are
hard-coded within the code (See #362). With this commit, fzf-completion
of zsh will first look if `_fzf_COMMAND_completion` exists and calls the
function, so one can externally define completion functions for specific
commands.

This commit also tries to make the interface of (yet undocumented)
_fzf_list_completion helper function consistent across bash and zsh.

So the following code works both on bash and zsh.

    _fzf_pass_completion() {
      local pwdir=${PASSWORD_STORE_DIR-~/.password-store/}
      local stringsize="${#pwdir}"
      let "stringsize+=1"
      _fzf_list_completion '+m' "$@" << "EOF"
        find "$pwdir" -name "*.gpg" -print | cut -c "$stringsize"- | sed -e 's/\(.*\)\.gpg/\1/'
    EOF
    }

    # Only on bash
    complete -F _fzf_pass_completion -o default -o bashdefault pass

Note that the suggested convention and the interface are not yet final
and subject to change.

/cc @d4ndo
2015-10-05 01:48:45 +09:00
Junegunn Choi
92a75c9563 Use trimmed length when --nth is used with --tiebreak=length
This change improves sort ordering for aligned tabular input.
Given the following input:

    apple   juice   100
    apple   pie     200

fzf --nth=2 will now prefer the one with pie. Before this change fzf
compared "juice   " and "pie     ", both of which have the same length.
2015-10-02 18:40:20 +09:00
Junegunn Choi
7c7a30c472 Merge pull request #364 from halostatue/use-zsh-regex-module
Remove dependency on zsh/pcre module
2015-10-02 11:02:19 +09:00
Austin Ziegler
ea271cd4e2 Remove dependency on zsh/pcre module
Fixes #363.
2015-10-01 15:18:10 -04:00
Junegunn Choi
6a38d07a4c Merge pull request #361 from justinmk/swapexists
[vim] handle SwapExists
2015-09-30 16:16:18 +09:00
Justin M. Keyes
c4e5ee63bb [vim] handle SwapExists
The SwapExists dialog prevents multiple files from being opening if the
dialog occurs before all files are opened. Opening the files is more
important than showing the dialog, so choose "readonly" automatically
and continue opening files.
2015-09-30 02:48:12 -04:00
Junegunn Choi
862da2c0b1 [vim] Consistent exit status handling 2015-09-27 16:26:40 +09:00
Junegunn Choi
545370d2b3 Merge branch 'jebaum-master' 2015-09-27 15:59:04 +09:00
James Baumgarten
59220c63a6 [vim] handle exit status 1 properly (#359) 2015-09-26 16:56:52 -07:00
Junegunn Choi
86306dd45a [vim] Display proper error message when GVim launcher failed
Related: https://github.com/junegunn/fzf.vim/issues/16
2015-09-26 21:04:44 +09:00
Junegunn Choi
98d2bfa0db [install] Terminate install script when failed to update shell config
Close #354
2015-09-24 10:51:05 +09:00
Junegunn Choi
aec48f159b [neovim] Remove redraw! hack that is no longer needed 2015-09-22 16:36:48 +09:00
Junegunn Choi
ad7e433a7d Use build tags to enable static linking 2015-09-22 13:16:50 +09:00
Junegunn Choi
5a60aa5050 [vim] Display proper error message when command failed 2015-09-20 14:10:43 +09:00
Junegunn Choi
ebea470875 Build linux binary on Centos 2015-09-20 00:17:44 +09:00
Junegunn Choi
d980e00961 Revert "Revert "0.10.6""
This reverts commit 987799f8fb.
2015-09-19 22:51:12 +09:00
Junegunn Choi
987799f8fb Revert "0.10.6"
This reverts commit d2f3604c1d.
2015-09-19 22:27:09 +09:00
Junegunn Choi
d2f3604c1d 0.10.6 2015-09-19 22:18:04 +09:00
Junegunn Choi
72cc558fdc Fix travis CI build 2015-09-19 18:39:09 +09:00
Junegunn Choi
6bc3fe6e67 Build partially-static binary for linux (#350)
Instead of building a separate statically-linked binary, build
partially-static binary that only contains ncurses to avoid
compatibility issues in libc.
2015-09-19 18:33:25 +09:00
Junegunn Choi
9398878048 [fzf-tmux] Exit with the same exit status as with fzf 2015-09-18 10:28:09 +09:00
Junegunn Choi
ca19762e58 Exit status 130 when fzf is terminated by the user
Related: #345
2015-09-18 10:25:07 +09:00
Junegunn Choi
8764be07e2 [vim] Ignore exit status of 2 (#345) 2015-09-18 09:59:40 +09:00
Junegunn Choi
2022a3ad96 Replace --header-file with --header (#346)
and allow using --header and --header-lines at the same time.

Close #346.
2015-09-15 19:04:53 +09:00
Junegunn Choi
65d9d416b4 Change exit status (0: OK, 1: No match, 2: Error/Interrupted)
A la grep. Close #345
2015-09-15 13:21:51 +09:00
Junegunn Choi
fa2f9f1f21 Remove flattr badge 2015-09-14 15:49:19 +09:00
Junegunn Choi
c656cfbdce Update doc 2015-09-12 13:31:07 +09:00
Junegunn Choi
de829c0938 0.10.5 2015-09-12 12:50:32 +09:00
Junegunn Choi
64443221aa Fix #344 - Backward scan when --tiebreak=end 2015-09-12 11:37:55 +09:00
Junegunn Choi
9017e29741 Make it possible to unquote the term in extended-exact mode
Close #338
2015-09-12 11:00:30 +09:00
Junegunn Choi
0a22142d88 [fzf-tmux] Fix #343 - Escape backticks in --query 2015-09-07 18:40:39 +09:00
Junegunn Choi
ac160f98a8 [gvim] Fix #342 - Should not escape launcher part of the command 2015-09-05 21:39:12 +09:00
Junegunn Choi
62e01a2a62 [vim] Escape newline character when running fzf with :!
Fixes Helptags! command from fzf.vim
2015-09-01 01:13:35 +09:00
Junegunn Choi
5660cebaf6 [zsh-completion] Temporarily unset shwordsplit (#328) 2015-09-01 00:51:28 +09:00
Junegunn Choi
a7e588ceac Merge pull request #336 from fazibear/fix-fish-streams
Fix CTRL-T on fish to work asynchronously
2015-08-30 21:21:13 +09:00
Michał Kalbarczyk
5baf1c5536 fix fish streams 2015-08-30 14:05:24 +02:00
Junegunn Choi
9a2d9ad947 0.10.4 2015-08-29 02:36:27 +09:00
Junegunn Choi
90b0cd44ac Should not strip ANSI codes when --ansi is not set 2015-08-28 21:23:10 +09:00
Junegunn Choi
698e8008df [vim] Dynamic height specification for 'up' and 'down' options
Values for 'up' and 'down' can be written with ~ prefix. Only applies
when the source is a Vim list.

    e.g. { 'source': range(10), 'down': '~40%' }
2015-08-28 18:38:47 +09:00
Junegunn Choi
1de4cc3ba8 [install] Fall back statically-linked binary on 64-bit linux
Close #322
2015-08-27 22:50:59 +09:00
Junegunn Choi
0d66ad23c6 Fix build script 2015-08-27 22:48:42 +09:00
Junegunn Choi
7f7741099b make linux-static (#322) 2015-08-27 03:28:05 +09:00
Junegunn Choi
5a72dc6922 Fix #329 - Trim ANSI codes from output when --ansi & --with-nth are set 2015-08-26 23:58:18 +09:00
Junegunn Choi
80ed02e72e Add failing test case for #329 2015-08-26 23:35:31 +09:00
Junegunn Choi
8fb31e1b4d [vim] Escape % and # when running source command with :! 2015-08-24 01:52:16 +09:00
Junegunn Choi
148f21415a Mention fzf.vim project 2015-08-22 19:33:04 +09:00
Junegunn Choi
1c31e07d34 [install] Improve error message 2015-08-19 19:42:06 +09:00
Junegunn Choi
55d566b72f Revert "[vim] Open silently"
This reverts commit c601fc6437.
2015-08-18 12:03:08 +09:00
Junegunn Choi
60336c7423 Remove Vim examples from README.md 2015-08-16 02:47:52 +09:00
Junegunn Choi
7ae877bd3a [vim] Handle single/double quote characters in 'dir' option 2015-08-16 00:04:45 +09:00
Junegunn Choi
c601fc6437 [vim] Open silently 2015-08-15 23:53:27 +09:00
Junegunn Choi
e5fec408c4 [vim] tab split instead of tabedit 2015-08-15 23:53:11 +09:00
31 changed files with 1192 additions and 704 deletions

View File

@@ -4,7 +4,7 @@ rvm:
install: install:
- sudo apt-get update - sudo apt-get update
- sudo apt-get install -y libncurses-dev lib32ncurses5-dev - sudo apt-get install -y libncurses-dev lib32ncurses5-dev libgpm-dev
- sudo add-apt-repository -y ppa:pi-rho/dev - sudo add-apt-repository -y ppa:pi-rho/dev
- sudo apt-add-repository -y ppa:fish-shell/release-2 - sudo apt-add-repository -y ppa:fish-shell/release-2
- sudo apt-get update - sudo apt-get update

View File

@@ -1,6 +1,53 @@
CHANGELOG CHANGELOG
========= =========
0.10.9
------
- Extended-search mode is now enabled by default
- `--extended-exact` is deprecated and instead we have `--exact` for
orthogonally controlling "exactness" of search
- Fixed not to display non-printable characters
- Added `double-click` for `--bind` option
- More robust handling of SIGWINCH
0.10.8
------
- Fixed panic when trying to set colors after colors are disabled (#370)
0.10.7
------
- Fixed unserialized interrupt handling during execute action which often
caused invalid memory access and crash
- Changed `--tiebreak=length` (default) to use trimmed length when `--nth` is
used
0.10.6
------
- Replaced `--header-file` with `--header` option
- `--header` and `--header-lines` can be used together
- Changed exit status
- 0: Okay
- 1: No match
- 2: Error
- 130: Interrupted
- 64-bit linux binary is statically-linked with ncurses to avoid
compatibility issues.
0.10.5
------
- `'`-prefix to unquote the term in `--extended-exact` mode
- Backward scan when `--tiebreak=end` is set
0.10.4
------
- Fixed to remove ANSI code from output when `--with-nth` is set
0.10.3 0.10.3
------ ------

View File

@@ -1,4 +1,4 @@
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf.png" height="170" alt="fzf - a command-line fuzzy finder"> [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) <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> <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)
=== ===
fzf is a general-purpose command-line fuzzy finder. fzf is a general-purpose command-line fuzzy finder.
@@ -68,7 +68,7 @@ Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf
(recommended): (recommended):
```vim ```vim
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' } Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' }
``` ```
#### Upgrading fzf #### Upgrading fzf
@@ -110,7 +110,7 @@ vim $(fzf)
#### Extended-search mode #### Extended-search mode
With `-x` or `--extended` option, fzf will start in "extended-search mode". Since 0.10.9, fzf starts in "extended-search mode" by default.
In this mode, you can specify multiple patterns delimited by spaces, In this mode, you can specify multiple patterns delimited by spaces,
such as: `^music .mp3$ sbtrkt !rmx` such as: `^music .mp3$ sbtrkt !rmx`
@@ -124,15 +124,16 @@ such as: `^music .mp3$ sbtrkt !rmx`
| `'wild` | Items that include `wild` | exact-match (quoted) | | `'wild` | Items that include `wild` | exact-match (quoted) |
| `!'fire` | Items that do not include `fire` | inverse-exact-match | | `!'fire` | Items that do not include `fire` | inverse-exact-match |
If you don't need fuzzy matching and do not wish to "quote" every word, start If you don't prefer fuzzy matching and do not wish to "quote" every word,
fzf with `-e` or `--extended-exact` option. start fzf with `-e` or `--exact` option. Note that when `--exact` is set,
`'`-prefix "unquotes" the term.
#### Environment variables #### Environment variables
- `FZF_DEFAULT_COMMAND` - `FZF_DEFAULT_COMMAND`
- Default command to use when input is tty - Default command to use when input is tty
- `FZF_DEFAULT_OPTS` - `FZF_DEFAULT_OPTS`
- Default options. e.g. `export FZF_DEFAULT_OPTS="--extended --cycle"` - Default options. e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"`
Examples Examples
-------- --------
@@ -254,6 +255,9 @@ export FZF_COMPLETION_OPTS='+c -x'
Usage as Vim plugin Usage as Vim plugin
------------------- -------------------
This repository only enables basic integration with Vim. If you're looking for
more, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project.
(Note: To use fzf in GVim, an external terminal emulator is required.) (Note: To use fzf in GVim, an external terminal emulator is required.)
#### `:FZF[!]` #### `:FZF[!]`
@@ -286,10 +290,8 @@ customization.
#### `fzf#run([options])` #### `fzf#run([options])`
For more advanced uses, you can call `fzf#run()` function which returns the list For more advanced uses, you can use `fzf#run()` function with the following
of the selected items. options.
`fzf#run()` may take an options-dictionary:
| Option name | Type | Description | | Option name | Type | Description |
| -------------------------- | ------------- | ---------------------------------------------------------------- | | -------------------------- | ------------- | ---------------------------------------------------------------- |
@@ -305,65 +307,7 @@ of the selected items.
| `launcher` | string | External terminal emulator to start fzf with (GVim only) | | `launcher` | string | External terminal emulator to start fzf with (GVim only) |
| `launcher` | funcref | Function for generating `launcher` string (GVim only) | | `launcher` | funcref | Function for generating `launcher` string (GVim only) |
_However on Neovim `fzf#run` is asynchronous and does not return values so you Examples can be found on [the wiki
should use `sink` or `sink*` to process the output from fzf._
##### Examples
If `sink` option is not given, `fzf#run` will simply return the list.
```vim
let items = fzf#run({ 'options': '-m +c', 'dir': '~', 'source': 'ls' })
```
But if `sink` is given as a string, the command will be executed for each
selected item.
```vim
" Each selected item will be opened in a new tab
let items = fzf#run({ 'sink': 'tabe', 'options': '-m +c', 'dir': '~', 'source': 'ls' })
```
We can also use a Vim list as the source as follows:
```vim
" Choose a color scheme with fzf
nnoremap <silent> <Leader>C :call fzf#run({
\ 'source':
\ map(split(globpath(&rtp, "colors/*.vim"), "\n"),
\ "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"),
\ 'sink': 'colo',
\ 'options': '+m',
\ 'left': 20,
\ 'launcher': 'xterm -geometry 20x30 -e bash -ic %s'
\ })<CR>
```
`sink` option can be a function reference. The following example creates a
handy mapping that selects an open buffer.
```vim
" List of buffers
function! s:buflist()
redir => ls
silent ls
redir END
return split(ls, '\n')
endfunction
function! s:bufopen(e)
execute 'buffer' matchstr(a:e, '^[ 0-9]*')
endfunction
nnoremap <silent> <Leader><Enter> :call fzf#run({
\ 'source': reverse(<sid>buflist()),
\ 'sink': function('<sid>bufopen'),
\ 'options': '+m',
\ 'down': len(<sid>buflist()) + 2
\ })<CR>
```
More examples can be found on [the wiki
page](https://github.com/junegunn/fzf/wiki/Examples-(vim)). page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
Tips Tips
@@ -411,7 +355,8 @@ speed of the traversal.
```sh ```sh
export FZF_DEFAULT_COMMAND=' export FZF_DEFAULT_COMMAND='
(git ls-tree -r --name-only HEAD || (git ls-tree -r --name-only HEAD ||
find * -name ".*" -prune -o -type f -print -o -type l -print) 2> /dev/null' find . -path "*/\.*" -prune -o -type f -print -o -type l -print |
sed s/^..//) 2> /dev/null'
``` ```
#### Fish shell #### Fish shell
@@ -425,14 +370,6 @@ of fzf to a temporary file.
fzf > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result) fzf > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result)
``` ```
#### Handling UTF-8 NFD paths on OSX
Use iconv to convert NFD paths to NFC:
```sh
find . | iconv -f utf-8-mac -t utf8//ignore | fzf
```
License License
------- -------

View File

@@ -91,10 +91,10 @@ set -e
# Clean up named pipes on exit # Clean up named pipes on exit
id=$RANDOM id=$RANDOM
argsf=/tmp/fzf-args-$id argsf="${TMPDIR:-/tmp}/fzf-args-$id"
fifo1=/tmp/fzf-fifo1-$id fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
fifo2=/tmp/fzf-fifo2-$id fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
fifo3=/tmp/fzf-fifo3-$id fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
cleanup() { cleanup() {
rm -f $argsf $fifo1 $fifo2 $fifo3 rm -f $argsf $fifo1 $fifo2 $fifo3
} }
@@ -102,7 +102,7 @@ trap cleanup EXIT SIGINT SIGTERM
fail() { fail() {
>&2 echo "$1" >&2 echo "$1"
exit 1 exit 2
} }
fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
[ -x "$fzf" ] || fail "fzf executable not found" [ -x "$fzf" ] || fail "fzf executable not found"
@@ -117,7 +117,9 @@ mkfifo $fifo3
# Build arguments to fzf # Build arguments to fzf
opts="" opts=""
for arg in "${args[@]}"; do for arg in "${args[@]}"; do
opts="$opts \"${arg//\"/\\\"}\"" arg="${arg//\"/\\\"}"
arg="${arg//\`/\\\`}"
opts="$opts \"$arg\""
done done
if [ -n "$term" -o -t 0 ]; then if [ -n "$term" -o -t 0 ]; then
@@ -132,5 +134,5 @@ else
cat <&0 > $fifo1 & cat <&0 > $fifo1 &
fi fi
cat $fifo2 cat $fifo2
[ "$(cat $fifo3)" = '0' ] exit "$(cat $fifo3)"

160
install
View File

@@ -1,12 +1,60 @@
#!/usr/bin/env bash #!/usr/bin/env bash
[[ "$@" =~ --pre ]] && version=0.10.3 pre=1 || set -u
version=0.10.3 pre=0
[[ "$@" =~ --pre ]] && version=0.10.9 pre=1 ||
version=0.10.9 pre=0
auto_completion=
key_bindings=
update_config=1
help() {
cat << EOF
usage: $0 [OPTIONS]
--help Show this message
--bin Download fzf binary only
--all Download fzf binary and update configuration files
to enable key bindings and fuzzy completion
--[no-]key-bindings Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C)
--[no-]completion Enable/disable fuzzy completion (bash & zsh)
--[no-]update-rc Whether or not to update shell configuration files
EOF
}
for opt in $@; do
case $opt in
--help)
help
exit 0
;;
--all)
auto_completion=1
key_bindings=1
update_config=1
;;
--key-bindings) key_bindings=1 ;;
--no-key-bindings) key_bindings=0 ;;
--completion) auto_completion=1 ;;
--no-completion) auto_completion=0 ;;
--update-rc) update_config=1 ;;
--no-update-rc) update_config=0 ;;
--bin) ;;
*)
echo "unknown option: $opt"
help
exit 1
;;
esac
done
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base="$(pwd)"
# If stdin is a tty, we are "interactive". # If stdin is a tty, we are "interactive".
interactive=
[ -t 0 ] && interactive=yes [ -t 0 ] && interactive=yes
ask() { ask() {
@@ -16,21 +64,26 @@ ask() {
read -p "$1 ([y]/n) " $read_n -r read -p "$1 ([y]/n) " $read_n -r
echo echo
[[ ! $REPLY =~ ^[Nn]$ ]] [[ $REPLY =~ ^[Nn]$ ]]
} }
check_binary() { check_binary() {
echo -n " - Checking fzf executable ... " echo -n " - Checking fzf executable ... "
local output=$("$fzf_base"/bin/fzf --version 2>&1) local output
if [ "$version" = "$output" ]; then output=$("$fzf_base"/bin/fzf --version 2>&1)
if [ $? -ne 0 ]; then
echo "Error: $output"
binary_error="Invalid binary"
elif [ "$version" != "$output" ]; then
echo "$output != $version"
binary_error="Invalid version"
else
echo "$output" echo "$output"
binary_error="" binary_error=""
else return 0
echo "$output != $version"
rm -f "$fzf_base"/bin/fzf
binary_error="Invalid binary"
return 1
fi fi
rm -f "$fzf_base"/bin/fzf
return 1
} }
symlink() { symlink() {
@@ -50,9 +103,16 @@ download() {
if [ -x "$fzf_base"/bin/fzf ]; then if [ -x "$fzf_base"/bin/fzf ]; then
echo " - Already exists" echo " - Already exists"
check_binary && return check_binary && return
elif [ -x "$fzf_base"/bin/$1 ]; then fi
if [ -x "$fzf_base"/bin/$1 ]; then
symlink $1 && check_binary && return symlink $1 && check_binary && return
fi fi
if which_fzf="$(which fzf 2> /dev/null)"; then
echo " - Found in \$PATH"
echo " - Creating symlink: $which_fzf -> bin/fzf"
(cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
check_binary && return
fi
fi fi
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@@ -168,38 +228,42 @@ fi
[[ "$*" =~ "--bin" ]] && exit 0 [[ "$*" =~ "--bin" ]] && exit 0
# Auto-completion # Auto-completion
ask "Do you want to add auto-completion support?" if [ -z "$auto_completion" ]; then
auto_completion=$? ask "Do you want to enable fuzzy auto-completion?"
auto_completion=$?
fi
# Key-bindings # Key-bindings
ask "Do you want to add key bindings?" if [ -z "$key_bindings" ]; then
key_bindings=$? ask "Do you want to enable key bindings?"
key_bindings=$?
fi
echo echo
for shell in bash zsh; do 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/shell/completion.${shell}\" 2> /dev/null" fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null"
if [ $auto_completion -ne 0 ]; then if [ $auto_completion -eq 0 ]; then
fzf_completion="# $fzf_completion" fzf_completion="# $fzf_completion"
fi fi
fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\"" fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\""
if [ $key_bindings -ne 0 ]; then if [ $key_bindings -eq 0 ]; then
fzf_key_bindings="# $fzf_key_bindings" fzf_key_bindings="# $fzf_key_bindings"
fi fi
cat > $src << EOF cat > $src << EOF
# Setup fzf # Setup fzf
# --------- # ---------
if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then if [[ ! "\$PATH" == *$fzf_base/bin* ]]; then
export PATH="\$PATH:$fzf_base/bin" export PATH="\$PATH:$fzf_base/bin"
fi fi
# Man path # Man path
# -------- # --------
if [[ ! "\$MANPATH" =~ "$fzf_base/man" && -d "$fzf_base/man" ]]; then if [[ ! "\$MANPATH" == *$fzf_base/man* && -d "$fzf_base/man" ]]; then
export MANPATH="\$MANPATH:$fzf_base/man" export MANPATH="\$MANPATH:$fzf_base/man"
fi fi
@@ -232,40 +296,58 @@ EOF
rm -f ~/.config/fish/functions/fzf.fish && echo "OK" || echo "Failed" rm -f ~/.config/fish/functions/fzf.fish && echo "OK" || echo "Failed"
fi fi
if [ $key_bindings -eq 0 ]; then fish_binding=~/.config/fish/functions/fzf_key_bindings.fish
echo -n "Symlink ~/.config/fish/functions/fzf_key_bindings.fish ... " if [ $key_bindings -ne 0 ]; then
ln -sf $fzf_base/shell/key-bindings.fish \ echo -n "Symlink $fish_binding ... "
~/.config/fish/functions/fzf_key_bindings.fish && echo "OK" || echo "Failed" ln -sf "$fzf_base/shell/key-bindings.fish" \
"$fish_binding" && echo "OK" || echo "Failed"
else
echo -n "Removing $fish_binding ... "
rm -f "$fish_binding"
echo "OK"
fi fi
fi fi
append_line() { append_line() {
echo "Update $2:" set -e
echo " - $1"
[ -f "$2" ] || touch "$2" local skip line file pat lno
if [ $# -lt 3 ]; then skip="$1"
line=$(\grep -nF "$1" "$2" | sed 's/:.*//' | tr '\n' ' ') line="$2"
file="$3"
pat="${4:-}"
echo "Update $file:"
echo " - $line"
[ -f "$file" ] || touch "$file"
if [ $# -lt 4 ]; then
lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ')
else else
line=$(\grep -nF "$3" "$2" | sed 's/:.*//' | tr '\n' ' ') lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ')
fi fi
if [ -n "$line" ]; then if [ -n "$lno" ]; then
echo " - Already exists: line #$line" echo " - Already exists: line #$lno"
else else
echo >> "$2" if [ $skip -eq 1 ]; then
echo "$1" >> "$2" echo >> "$file"
echo " + Added" echo "$line" >> "$file"
echo " + Added"
else
echo " ~ Skipped"
fi
fi fi
echo echo
set +e
} }
echo echo
for shell in bash zsh; do for shell in bash zsh; do
append_line "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" ~/.${shell}rc "~/.fzf.${shell}" append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" ~/.${shell}rc "~/.fzf.${shell}"
done done
if [ $key_bindings -eq 0 -a $has_fish -eq 1 ]; then if [ $key_bindings -eq 1 -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 $update_config "fzf_key_bindings" "$bind_file"
fi fi
cat << EOF cat << EOF

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Aug 2015" "fzf 0.10.3" "fzf - a command-line fuzzy finder" .TH fzf 1 "Nov 2015" "fzf 0.10.9" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -36,10 +36,11 @@ fzf is a general-purpose command-line fuzzy finder.
.SS Search mode .SS Search mode
.TP .TP
.B "-x, --extended" .B "-x, --extended"
Extended-search mode Extended-search mode. Since 0.10.9, this is enabled by default. You can disable
it with \fB+x\fR or \fB--no-extended\fR.
.TP .TP
.B "-e, --extended-exact" .B "-e, --exact"
Extended-search mode (exact match) Enable exact-match
.TP .TP
.B "-i" .B "-i"
Case-insensitive match (default: smart-case match) Case-insensitive match (default: smart-case match)
@@ -179,11 +180,11 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
.RE .RE
.RS .RS
.B AVAILABLE KEYS: .B AVAILABLE KEYS: (SYNONYMS)
\fIctrl-[a-z]\fR \fIctrl-[a-z]\fR
\fIalt-[a-z]\fR \fIalt-[a-z]\fR
\fIf[1-4]\fR \fIf[1-4]\fR
\fIenter\fR (\fIreturn\fR) \fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
\fIspace\fR \fIspace\fR
\fIbspace\fR (\fIbs\fR) \fIbspace\fR (\fIbs\fR)
\fIalt-bspace\fR (\fIalt-bs\fR) \fIalt-bspace\fR (\fIalt-bs\fR)
@@ -201,13 +202,14 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fIpgdn\fR (\fIpage-down\fR) \fIpgdn\fR (\fIpage-down\fR)
\fIshift-left\fR \fIshift-left\fR
\fIshift-right\fR \fIshift-right\fR
\fIdouble-click\fR
or any single character or any single character
.RE .RE
.RS .RS
\fBACTION: DEFAULT BINDINGS: \fBACTION: DEFAULT BINDINGS:
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
\fBaccept\fR \fIctrl-m (enter)\fR \fBaccept\fR \fIenter double-click\fR
\fBbackward-char\fR \fIctrl-b left\fR \fBbackward-char\fR \fIctrl-b left\fR
\fBbackward-delete-char\fR \fIctrl-h bspace\fR \fBbackward-delete-char\fR \fIctrl-h bspace\fR
\fBbackward-kill-word\fR \fIalt-bs\fR \fBbackward-kill-word\fR \fIalt-bs\fR
@@ -285,11 +287,11 @@ When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to
Maximum number of entries in the history file (default: 1000). The file is Maximum number of entries in the history file (default: 1000). The file is
automatically truncated when the number of the lines exceeds the value. automatically truncated when the number of the lines exceeds the value.
.TP .TP
.BI "--header-file=" "FILE" .BI "--header=" "STR"
The content of the file will be printed as the sticky header. The lines in the The given string will be printed as the sticky header. The lines are displayed
file are displayed in order from top to bottom regardless of \fB--reverse\fR, in the given order from top to bottom regardless of \fB--reverse\fR option, and
and are not affected by \fB--with-nth\fR. ANSI color codes are processed even are not affected by \fB--with-nth\fR. ANSI color codes are processed even when
when \fB--ansi\fR is not set. \fB--ansi\fR is not set.
.TP .TP
.BI "--header-lines=" "N" .BI "--header-lines=" "N"
The first N lines of the input are treated as the sticky header. When The first N lines of the input are treated as the sticky header. When
@@ -369,34 +371,44 @@ of field index expressions.
.SH EXTENDED SEARCH MODE .SH EXTENDED SEARCH MODE
With \fB-x\fR or \fB--extended\fR option, fzf will start in "extended-search Unless specified otherwise, fzf will start in "extended-search mode". In this
mode". In this mode, you can specify multiple patterns delimited by spaces, mode, you can specify multiple patterns delimited by spaces, such as: \fB'wild
such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR ^music .mp3$ sbtrkt !rmx\fR
.SS Exact-match (quoted) .SS Exact-match (quoted)
A term that is prefixed by a single-quote character (') is interpreted as an A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as
"exact-match" (or "non-fuzzy") term. fzf will search for the exact occurrences an "exact-match" (or "non-fuzzy") term. fzf will search for the exact
of the string. occurrences of the string.
.SS Anchored-match .SS Anchored-match
A term can be prefixed by ^, or suffixed by $ to become an anchored-match term. A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an
Then fzf will search for the items that start with or end with the given anchored-match term. Then fzf will search for the items that start with or end
string. An anchored-match term is also an exact-match term. with the given string. An anchored-match term is also an exact-match term.
.SS Negation .SS Negation
If a term is prefixed by !, fzf will exclude the items that satisfy the term If a term is prefixed by \fB!\fR, fzf will exclude the items that satisfy the
from the result. term from the result.
.SS Extended-exact mode .SS Exact-match by default
If you don't need fuzzy matching at all and do not wish to "quote" (prefixing If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with
with ') every word, start fzf with \fB-e\fR or \fB--extended-exact\fR option \fB'\fR) every word, start fzf with \fB-e\fR or \fB--exact\fR option. Note that
(instead of \fB-x\fR or \fB--extended\fR). when \fB--exact\fR is set, \fB'\fR-prefix "unquotes" the term.
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)
.SH SEE ALSO .SH SEE ALSO
.B Project homepage:
.RS
.I https://github.com/junegunn/fzf .I https://github.com/junegunn/fzf
.RE
.br
.R ""
.br
.B Extra Vim plugin:
.RS
.I https://github.com/junegunn/fzf.vim
.RE
.SH LICENSE .SH LICENSE
MIT MIT

View File

@@ -40,9 +40,7 @@ function! s:fzf_exec()
\ input('fzf executable not found. Download binary? (y/n) ') =~? '^y' \ input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
redraw redraw
echo echo
echohl WarningMsg call s:warn('Downloading fzf binary. Please wait ...')
echo 'Downloading fzf binary. Please wait ...'
echohl None
let s:installed = 1 let s:installed = 1
call system(s:install.' --bin') call system(s:install.' --bin')
return s:fzf_exec() return s:fzf_exec()
@@ -80,7 +78,7 @@ function! s:shellesc(arg)
endfunction endfunction
function! s:escape(path) function! s:escape(path)
return escape(a:path, ' %#\') return escape(a:path, ' %#''"\')
endfunction endfunction
" Upgrade legacy options " Upgrade legacy options
@@ -98,14 +96,24 @@ function! s:upgrade(dict)
return copy return copy
endfunction endfunction
function! s:error(msg)
echohl ErrorMsg
echom a:msg
echohl None
endfunction
function! s:warn(msg)
echohl WarningMsg
echom a:msg
echohl None
endfunction
function! fzf#run(...) abort function! fzf#run(...) abort
try try
let oshell = &shell let oshell = &shell
set shell=sh set shell=sh
if has('nvim') && bufexists('term://*:FZF') if has('nvim') && bufexists('term://*:FZF')
echohl WarningMsg call s:warn('FZF is already running!')
echomsg 'FZF is already running!'
echohl None
return [] return []
endif endif
let dict = exists('a:1') ? s:upgrade(a:1) : {} let dict = exists('a:1') ? s:upgrade(a:1) : {}
@@ -164,7 +172,13 @@ function! s:fzf_tmux(dict)
let size = '' let size = ''
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 spec = a:dict[o]
if (o == 'up' || o == 'down') && spec[0] == '~'
let size = '-'.o[0].s:calc_size(&lines, spec[1:], a:dict)
else
" Legacy boolean option
let size = '-'.o[0].(spec == 1 ? '' : spec)
endif
break break
endif endif
endfor endfor
@@ -199,7 +213,7 @@ endfunction
function! s:xterm_launcher() function! s:xterm_launcher()
let fmt = 'xterm -T "[fzf]" -bg "\%s" -fg "\%s" -geometry %dx%d+%d+%d -e bash -ic %%s' let fmt = 'xterm -T "[fzf]" -bg "\%s" -fg "\%s" -geometry %dx%d+%d+%d -e bash -ic %%s'
if has('gui_macvim') if has('gui_macvim')
let fmt .= '; osascript -e "tell application \"MacVim\" to activate"' let fmt .= '&& osascript -e "tell application \"MacVim\" to activate"'
endif endif
return printf(fmt, return printf(fmt,
\ synIDattr(hlID("Normal"), "bg"), synIDattr(hlID("Normal"), "fg"), \ synIDattr(hlID("Normal"), "bg"), synIDattr(hlID("Normal"), "fg"),
@@ -208,28 +222,33 @@ endfunction
unlet! s:launcher unlet! s:launcher
let s:launcher = function('s:xterm_launcher') let s:launcher = function('s:xterm_launcher')
function! s:exit_handler(code, command, ...)
if a:code == 130
return 0
elseif a:code > 1
call s:error('Error running ' . a:command)
if !empty(a:000)
sleep
endif
return 0
endif
return 1
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 2> /dev/null silent! !clear 2> /dev/null
let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#')
if has('gui_running') if has('gui_running')
let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher))) let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher)))
let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher
let command = printf(fmt, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'") let command = printf(fmt, "'".substitute(escaped, "'", "'\"'\"'", 'g')."'")
else else
let command = a:command let command = escaped
endif endif
execute 'silent !'.command execute 'silent !'.command
redraw! redraw!
if v:shell_error return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : []
" Do not print error message on exit status 1
if v:shell_error > 1
echohl ErrorMsg
echo 'Error running ' . command
endif
return []
else
return s:callback(a:dict, a:temps)
endif
endfunction endfunction
function! s:execute_tmux(dict, command, temps) function! s:execute_tmux(dict, command, temps)
@@ -241,15 +260,24 @@ function! s:execute_tmux(dict, command, temps)
call system(command) call system(command)
redraw! redraw!
return s:callback(a:dict, a:temps) return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : []
endfunction endfunction
function! s:calc_size(max, val) function! s:calc_size(max, val, dict)
if a:val =~ '%$' if a:val =~ '%$'
return a:max * str2nr(a:val[:-2]) / 100 let size = a:max * str2nr(a:val[:-2]) / 100
else else
return min([a:max, a:val]) let size = min([a:max, str2nr(a:val)])
endif endif
let srcsz = -1
if type(get(a:dict, 'source', 0)) == type([])
let srcsz = len(a:dict.source)
endif
let opts = get(a:dict, 'options', '').$FZF_DEFAULT_OPTS
let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2
return srcsz >= 0 ? min([srcsz + margin, size]) : size
endfunction endfunction
function! s:getpos() function! s:getpos()
@@ -268,7 +296,11 @@ function! s:split(dict)
let val = get(a:dict, dir, '') let val = get(a:dict, dir, '')
if !empty(val) if !empty(val)
let [cmd, resz, max] = triple let [cmd, resz, max] = triple
let sz = s:calc_size(max, val) if (dir == 'up' || dir == 'down') && val[0] == '~'
let sz = s:calc_size(max, val[1:], a:dict)
else
let sz = s:calc_size(max, val, {})
endif
execute cmd sz.'new' execute cmd sz.'new'
execute resz sz execute resz sz
return return
@@ -289,6 +321,7 @@ function! s:execute_term(dict, command, temps)
call s:pushd(a:dict) call s:pushd(a:dict)
let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF' } let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF' }
let s:command = a:command
function! fzf.on_exit(id, code) function! fzf.on_exit(id, code)
let pos = s:getpos() let pos = s:getpos()
let inplace = pos == s:ppos " {'window': 'enew'} let inplace = pos == s:ppos " {'window': 'enew'}
@@ -302,9 +335,13 @@ function! s:execute_term(dict, command, temps)
wincmd p wincmd p
endif endif
endif endif
if !s:exit_handler(a:code, s:command, 1)
return
endif
call s:pushd(self.dict) call s:pushd(self.dict)
try try
redraw!
call s:callback(self.dict, self.temps) call s:callback(self.dict, self.temps)
if inplace && bufnr('') == self.buf if inplace && bufnr('') == self.buf
@@ -320,6 +357,7 @@ function! s:execute_term(dict, command, temps)
endfunction endfunction
call termopen(a:command, fzf) call termopen(a:command, fzf)
setf fzf
startinsert startinsert
return [] return []
endfunction endfunction
@@ -358,7 +396,7 @@ endfunction
let s:default_action = { let s:default_action = {
\ 'ctrl-m': 'e', \ 'ctrl-m': 'e',
\ 'ctrl-t': 'tabedit', \ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split', \ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' } \ 'ctrl-v': 'vsplit' }
@@ -368,14 +406,24 @@ function! s:cmd_callback(lines) abort
endif endif
let key = remove(a:lines, 0) let key = remove(a:lines, 0)
let cmd = get(s:action, key, 'e') let cmd = get(s:action, key, 'e')
if len(a:lines) > 1
augroup fzf_swap
autocmd SwapExists * let v:swapchoice='o'
\| call s:warn('fzf: E325: swap file exists: '.expand('<afile>'))
augroup END
endif
try try
let autochdir = &autochdir let autochdir = &autochdir
set noautochdir set noautochdir
for item in a:lines for item in a:lines
execute cmd s:escape(item) execute cmd s:escape(item)
if exists('#BufEnter') && isdirectory(item)
doautocmd BufEnter
endif
endfor endfor
finally finally
let &autochdir = autochdir let &autochdir = autochdir
silent! autocmd! fzf_swap
endtry endtry
endfunction endfunction
@@ -384,7 +432,7 @@ function! s:cmd(bang, ...) abort
let args = extend(['--expect='.join(keys(s:action), ',')], a:000) 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 = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g')
endif endif
if !a:bang if !a:bang
let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height)) let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height))

View File

@@ -11,8 +11,8 @@
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
_fzf_orig_completion_filter() { _fzf_orig_completion_filter() {
sed 's/.*-F *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\2=\1;/' | sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' |
sed 's/[^a-z0-9_= ;]/_/g' awk -F= '{gsub(/[^a-z0-9_= ;]/, "_", $1); print $1"="$2}'
} }
_fzf_opts_completion() { _fzf_opts_completion() {
@@ -22,7 +22,7 @@ _fzf_opts_completion() {
prev="${COMP_WORDS[COMP_CWORD-1]}" prev="${COMP_WORDS[COMP_CWORD-1]}"
opts=" opts="
-x --extended -x --extended
-e --extended-exact -e --exact
-i +i -i +i
-n --nth -n --nth
-d --delimiter -d --delimiter
@@ -49,7 +49,7 @@ _fzf_opts_completion() {
--cycle --cycle
--history --history
--history-size --history-size
--header-file --header
--header-lines --header-lines
--margin" --margin"
@@ -62,7 +62,7 @@ _fzf_opts_completion() {
COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) ) COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) )
return 0 return 0
;; ;;
--history|--header-file) --history)
COMPREPLY=() COMPREPLY=()
return 0 return 0
;; ;;
@@ -77,12 +77,12 @@ _fzf_opts_completion() {
} }
_fzf_handle_dynamic_completion() { _fzf_handle_dynamic_completion() {
local cmd orig ret orig_cmd local cmd orig_var orig ret orig_cmd
cmd="$1" cmd="$1"
shift shift
orig_cmd="$1" orig_cmd="$1"
orig_var="_fzf_orig_completion_$cmd"
orig=$(eval "echo \$_fzf_orig_completion_$cmd") orig="${!orig_var##*#}"
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
$orig "$@" $orig "$@"
elif [ -n "$_fzf_completion_loader" ]; then elif [ -n "$_fzf_completion_loader" ]; then
@@ -94,7 +94,7 @@ _fzf_handle_dynamic_completion() {
fi fi
} }
_fzf_path_completion() { __fzf_generic_path_completion() {
local cur base dir leftover matches trigger cmd fzf local cur base dir leftover matches trigger cmd fzf
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="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')
@@ -135,20 +135,29 @@ _fzf_path_completion() {
fi fi
} }
_fzf_list_completion() { _fzf_feed_fifo() (
local cur selected trigger cmd src fzf rm -f "$fifo"
mkfifo "$fifo"
cat <&0 > "$fifo" &
)
_fzf_complete() {
local fifo cur selected trigger cmd fzf
fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
read -r src
cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g')
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ ${cur} == *"$trigger" ]]; then if [[ ${cur} == *"$trigger" ]]; then
cur=${cur:0:${#cur}-${#trigger}} cur=${cur:0:${#cur}-${#trigger}}
_fzf_feed_fifo "$fifo"
tput sc tput sc
selected=$(eval "$src | $fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ') selected=$(eval "cat '$fifo' | $fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ')
selected=${selected% } selected=${selected% } # Strip trailing space not to repeat "-o nospace"
tput rc tput rc
rm -f "$fifo"
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
COMPREPLY=("$selected") COMPREPLY=("$selected")
@@ -160,25 +169,25 @@ _fzf_list_completion() {
fi fi
} }
_fzf_all_completion() { _fzf_path_completion() {
_fzf_path_completion \ __fzf_generic_path_completion \
"-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \ "-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \
"-m" "" "$@" "-m" "" "$@"
} }
_fzf_file_completion() { _fzf_file_completion() {
_fzf_path_completion \ __fzf_generic_path_completion \
"-name .git -prune -o -name .svn -prune -o -type f -print -o -type l -print" \ "-name .git -prune -o -name .svn -prune -o -type f -print -o -type l -print" \
"-m" "" "$@" "-m" "" "$@"
} }
_fzf_dir_completion() { _fzf_dir_completion() {
_fzf_path_completion \ __fzf_generic_path_completion \
"-name .git -prune -o -name .svn -prune -o -type d -print" \ "-name .git -prune -o -name .svn -prune -o -type d -print" \
"" "/" "$@" "" "/" "$@"
} }
_fzf_kill_completion() { _fzf_complete_kill() {
[ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1 [ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1
local selected fzf local selected fzf
@@ -193,28 +202,37 @@ _fzf_kill_completion() {
fi fi
} }
_fzf_telnet_completion() { _fzf_complete_telnet() {
_fzf_list_completion '+m' "$@" << "EOF" _fzf_complete '+m' "$@" < <(
\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u \grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' |
EOF awk '{if (length($2) > 0) {print $2}}' | sort -u
)
} }
_fzf_ssh_completion() { _fzf_complete_ssh() {
_fzf_list_completion '+m' "$@" << "EOF" _fzf_complete '+m' "$@" < <(
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \
EOF <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u
)
} }
_fzf_env_var_completion() { _fzf_complete_unset() {
_fzf_list_completion '-m' "$@" << "EOF" _fzf_complete '-m' "$@" < <(
declare -xp | sed 's/=.*//' | sed 's/.* //' declare -xp | sed 's/=.*//' | sed 's/.* //'
EOF )
} }
_fzf_alias_completion() { _fzf_complete_export() {
_fzf_list_completion '-m' "$@" << "EOF" _fzf_complete '-m' "$@" < <(
alias | sed 's/=.*//' | sed 's/.* //' declare -xp | sed 's/=.*//' | sed 's/.* //'
EOF )
}
_fzf_complete_unalias() {
_fzf_complete '-m' "$@" < <(
alias | sed 's/=.*//' | sed 's/.* //'
)
} }
# fzf options # fzf options
@@ -234,42 +252,58 @@ a_cmds="
x_cmds="kill ssh telnet unset unalias export" x_cmds="kill ssh telnet unset unalias export"
# Preserve existing completion # Preserve existing completion
if [ "$_fzf_completion_loaded" != '0.9.12' ]; then if [ "$_fzf_completion_loaded" != '0.10.8' ]; then
# Really wish I could use associative array but OSX comes with bash 3.2 :( # Really wish I could use associative array but OSX comes with bash 3.2 :(
eval $(complete | \grep '\-F' | \grep -v _fzf_ | eval $(complete | \grep '\-F' | \grep -v _fzf_ |
\grep -E " ($(echo $d_cmds $f_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter) \grep -E " ($(echo $d_cmds $f_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter)
export _fzf_completion_loaded=0.9.12 export _fzf_completion_loaded=0.10.8
fi fi
if type _completion_loader > /dev/null 2>&1; then if type _completion_loader > /dev/null 2>&1; then
_fzf_completion_loader=1 _fzf_completion_loader=1
fi fi
_fzf_defc() {
local cmd func opts orig_var orig
cmd="$1"
func="$2"
opts="$3"
orig_var="_fzf_orig_completion_$cmd"
orig="${!orig_var}"
if [ -n "$orig" ]; then
eval "$(printf "$orig" "$func")"
else
complete -F "$func" $opts "$cmd"
fi
}
# Directory # Directory
for cmd in $d_cmds; do for cmd in $d_cmds; do
complete -F _fzf_dir_completion -o nospace -o plusdirs $cmd _fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o plusdirs"
done done
# File # File
for cmd in $f_cmds; do for cmd in $f_cmds; do
complete -F _fzf_file_completion -o default -o bashdefault $cmd _fzf_defc "$cmd" _fzf_file_completion "-o default -o bashdefault"
done done
# Anything # Anything
for cmd in $a_cmds; do for cmd in $a_cmds; do
complete -F _fzf_all_completion -o default -o bashdefault $cmd _fzf_defc "$cmd" _fzf_path_completion "-o default -o bashdefault"
done done
unset _fzf_defc
# Kill completion # Kill completion
complete -F _fzf_kill_completion -o nospace -o default -o bashdefault kill complete -F _fzf_complete_kill -o nospace -o default -o bashdefault kill
# Host completion # Host completion
complete -F _fzf_ssh_completion -o default -o bashdefault ssh complete -F _fzf_complete_ssh -o default -o bashdefault ssh
complete -F _fzf_telnet_completion -o default -o bashdefault telnet complete -F _fzf_complete_telnet -o default -o bashdefault telnet
# Environment variables / Aliases # Environment variables / Aliases
complete -F _fzf_env_var_completion -o default -o bashdefault unset complete -F _fzf_complete_unset -o default -o bashdefault unset
complete -F _fzf_env_var_completion -o default -o bashdefault export complete -F _fzf_complete_export -o default -o bashdefault export
complete -F _fzf_alias_completion -o default -o bashdefault unalias complete -F _fzf_complete_unalias -o default -o bashdefault unalias
unset cmd d_cmds f_cmds a_cmds x_cmds unset cmd d_cmds f_cmds a_cmds x_cmds

View File

@@ -10,8 +10,9 @@
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
_fzf_path_completion() { __fzf_generic_path_completion() {
local base lbuf find_opts fzf_opts suffix tail fzf dir leftover matches nnm local base lbuf find_opts fzf_opts suffix tail fzf dir leftover matches nnm
# (Q) flag removes a quoting level: "foo\ bar" => "foo bar"
base=${(Q)1} base=${(Q)1}
lbuf=$2 lbuf=$2
find_opts=$3 find_opts=$3
@@ -47,59 +48,79 @@ _fzf_path_completion() {
[ -n "$nnm" ] && unsetopt nonomatch [ -n "$nnm" ] && unsetopt nonomatch
} }
_fzf_all_completion() { _fzf_path_completion() {
_fzf_path_completion "$1" "$2" \ __fzf_generic_path_completion "$1" "$2" \
"-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \ "-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \
"-m" "" " " "-m" "" " "
} }
_fzf_dir_completion() { _fzf_dir_completion() {
_fzf_path_completion "$1" "$2" \ __fzf_generic_path_completion "$1" "$2" \
"-name .git -prune -o -name .svn -prune -o -type d -print" \ "-name .git -prune -o -name .svn -prune -o -type d -print" \
"" "/" "" "" "/" ""
} }
_fzf_list_completion() { _fzf_feed_fifo() (
local prefix lbuf fzf_opts src fzf matches rm -f "$fifo"
prefix=$1 mkfifo "$fifo"
cat <&0 > "$fifo" &
)
_fzf_complete() {
local fifo fzf_opts lbuf fzf matches
fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
fzf_opts=$1
lbuf=$2 lbuf=$2
fzf_opts=$3
read -r src
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
matches=$(eval "$src" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$prefix") _fzf_feed_fifo "$fifo"
matches=$(cat "$fifo" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | tr '\n' ' ')
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches " LBUFFER="$lbuf$matches"
fi fi
zle redisplay zle redisplay
rm -f "$fifo"
} }
_fzf_telnet_completion() { _fzf_complete_telnet() {
_fzf_list_completion "$1" "$2" '+m' << "EOF" _fzf_complete '+m' "$@" < <(
\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u \grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' |
EOF awk '{if (length($2) > 0) {print $2}}' | sort -u
)
} }
_fzf_ssh_completion() { _fzf_complete_ssh() {
_fzf_list_completion "$1" "$2" '+m' << "EOF" _fzf_complete '+m' "$@" < <(
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \
EOF <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u
)
} }
_fzf_env_var_completion() { _fzf_complete_export() {
_fzf_list_completion "$1" "$2" '+m' << "EOF" _fzf_complete '-m' "$@" < <(
declare -xp | sed 's/=.*//' | sed 's/.* //' declare -xp | sed 's/=.*//' | sed 's/.* //'
EOF )
} }
_fzf_alias_completion() { _fzf_complete_unset() {
_fzf_list_completion "$1" "$2" '+m' << "EOF" _fzf_complete '-m' "$@" < <(
alias | sed 's/=.*//' declare -xp | sed 's/=.*//' | sed 's/.* //'
EOF )
}
_fzf_complete_unalias() {
_fzf_complete '+m' "$@" < <(
alias | sed 's/=.*//'
)
} }
fzf-completion() { fzf-completion() {
local tokens cmd prefix trigger tail fzf matches lbuf d_cmds local tokens cmd prefix trigger tail fzf matches lbuf d_cmds sws
if setopt | grep shwordsplit > /dev/null; then
sws=1
unsetopt shwordsplit
fi
# http://zsh.sourceforge.net/FAQ/zshfaq03.html # http://zsh.sourceforge.net/FAQ/zshfaq03.html
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
@@ -131,23 +152,18 @@ fzf-completion() {
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}} [ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
[ -z "${tokens[-1]}" ] && lbuf=$LBUFFER || lbuf=${LBUFFER:0:-${#tokens[-1]}} [ -z "${tokens[-1]}" ] && lbuf=$LBUFFER || lbuf=${LBUFFER:0:-${#tokens[-1]}}
if [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then if eval "type _fzf_complete_${cmd} > /dev/null"; then
_fzf_dir_completion "$prefix" $lbuf eval "prefix=\"$prefix\" _fzf_complete_${cmd} \"$lbuf\""
elif [ $cmd = telnet ]; then elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
_fzf_telnet_completion "$prefix" $lbuf _fzf_dir_completion "$prefix" "$lbuf"
elif [ $cmd = ssh ]; then
_fzf_ssh_completion "$prefix" $lbuf
elif [ $cmd = unset -o $cmd = export ]; then
_fzf_env_var_completion "$prefix" $lbuf
elif [ $cmd = unalias ]; then
_fzf_alias_completion "$prefix" $lbuf
else else
_fzf_all_completion "$prefix" $lbuf _fzf_path_completion "$prefix" "$lbuf"
fi fi
# Fall back to default completion # Fall back to default completion
else else
eval "zle ${fzf_default_completion:-expand-or-complete}" eval "zle ${fzf_default_completion:-expand-or-complete}"
fi fi
[ -n "$sws" ] && setopt shwordsplit
} }
[ -z "$fzf_default_completion" ] && [ -z "$fzf_default_completion" ] &&

View File

@@ -19,7 +19,7 @@ function fzf_key_bindings
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3-" -o -type l -print 2> /dev/null | sed 1d | cut -b3-"
eval $FZF_CTRL_T_COMMAND | eval (__fzfcmd) -m > $TMPDIR/fzf.result eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m > $TMPDIR/fzf.result"
and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape)
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result rm -f $TMPDIR/fzf.result

View File

@@ -1,6 +1,6 @@
# Key bindings # Key bindings
# ------------ # ------------
if [[ $- =~ i ]]; then if [[ $- == *i* ]]; then
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line
__fsel() { __fsel() {

View File

@@ -2,6 +2,7 @@ FROM base/archlinux:2014.07.03
MAINTAINER Junegunn Choi <junegunn.c@gmail.com> MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# apt-get # apt-get
RUN pacman-key --populate archlinux && pacman-key --refresh-keys
RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git
# Install Go 1.4 # Install Go 1.4

View File

@@ -1,8 +1,10 @@
FROM centos:centos7 FROM centos:centos6
MAINTAINER Junegunn Choi <junegunn.c@gmail.com> MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# yum # yum
RUN yum install -y git gcc make tar ncurses-devel RUN yum install -y git gcc make tar glibc-devel glibc-devel.i686 \
ncurses-devel ncurses-static ncurses-devel.i686 \
gpm-devel gpm-static libgcc.i686
# Install Go 1.4 # Install Go 1.4
RUN cd / && curl \ RUN cd / && curl \
@@ -13,6 +15,9 @@ ENV GOPATH /go
ENV GOROOT /go1.4 ENV GOROOT /go1.4
ENV PATH /go1.4/bin:$PATH ENV PATH /go1.4/bin:$PATH
# For i386 build
RUN cd $GOROOT/src && GOARCH=386 ./make.bash
# Volume # Volume
VOLUME /go VOLUME /go

View File

@@ -3,7 +3,7 @@ MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# apt-get # apt-get
RUN apt-get update && apt-get -y upgrade && \ RUN apt-get update && apt-get -y upgrade && \
apt-get install -y --force-yes git curl build-essential libncurses-dev apt-get install -y --force-yes git curl build-essential libncurses-dev libgpm-dev
# Install Go 1.4 # Install Go 1.4
RUN cd / && curl \ RUN cd / && curl \

View File

@@ -25,10 +25,9 @@ RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64
all: release all: release
release: build release: build
cd fzf && \ -cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32)
cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) && \ cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \
cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ rm -f $(RELEASE32) $(RELEASE64)
rm $(RELEASE32) $(RELEASE64)
build: test fzf/$(BINARY32) fzf/$(BINARY64) build: test fzf/$(BINARY32) fzf/$(BINARY64)
@@ -42,13 +41,13 @@ uninstall:
rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64) rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64)
clean: clean:
cd fzf && rm -f $(BINARY32) $(BINARY64) $(RELEASE32).tgz $(RELEASE64).tgz cd fzf && rm -f fzf-*
fzf/$(BINARY32): $(SOURCES) fzf/$(BINARY32): $(SOURCES)
cd fzf && GOARCH=386 CGO_ENABLED=1 go build -o $(BINARY32) cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -o $(BINARY32)
fzf/$(BINARY64): $(SOURCES) fzf/$(BINARY64): $(SOURCES)
cd fzf && go build -o $(BINARY64) cd fzf && go build -a -tags "$(TAGS)" -o $(BINARY64)
$(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) $(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR)
cp -f fzf/$(BINARY64) $(BINDIR) cp -f fzf/$(BINARY64) $(BINDIR)
@@ -57,18 +56,30 @@ $(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR)
$(BINDIR): $(BINDIR):
mkdir -p $@ mkdir -p $@
# Linux distribution to build fzf on docker-arch:
DISTRO := arch docker build -t junegunn/arch-sandbox - < Dockerfile.arch
docker: docker-ubuntu:
docker build -t junegunn/$(DISTRO)-sandbox - < Dockerfile.$(DISTRO) docker build -t junegunn/ubuntu-sandbox - < Dockerfile.ubuntu
linux: docker docker-centos:
docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \ docker build -t junegunn/centos-sandbox - < Dockerfile.centos
/bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make'
$(DISTRO): docker arch: docker-arch
docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \ docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \
sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash'
.PHONY: all build release test install uninstall clean docker linux $(DISTRO) ubuntu: docker-ubuntu
docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \
sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash'
centos: docker-centos
docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \
sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash'
linux: docker-centos
docker run -i -t -v $(GOPATH):/go junegunn/centos-sandbox \
/bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make TAGS=static'
.PHONY: all build release test install uninstall clean docker \
linux arch ubuntu centos docker-arch docker-ubuntu docker-centos

View File

@@ -15,8 +15,15 @@ import (
* In short: They try to do as little work as possible. * In short: They try to do as little work as possible.
*/ */
func runeAt(runes []rune, index int, max int, forward bool) rune {
if forward {
return runes[index]
}
return runes[max-index-1]
}
// FuzzyMatch performs fuzzy-match // FuzzyMatch performs fuzzy-match
func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
if len(pattern) == 0 { if len(pattern) == 0 {
return 0, 0 return 0, 0
} }
@@ -34,7 +41,11 @@ func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) {
sidx := -1 sidx := -1
eidx := -1 eidx := -1
for index, char := range runes { lenRunes := len(runes)
lenPattern := len(pattern)
for index := range runes {
char := runeAt(runes, index, lenRunes, forward)
// 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 { if !caseSensitive {
@@ -47,11 +58,12 @@ func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) {
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
if char == pattern[pidx] { pchar := runeAt(pattern, pidx, lenPattern, forward)
if char == pchar {
if sidx < 0 { if sidx < 0 {
sidx = index sidx = index
} }
if pidx++; pidx == len(pattern) { if pidx++; pidx == lenPattern {
eidx = index + 1 eidx = index + 1
break break
} }
@@ -61,7 +73,7 @@ func FuzzyMatch(caseSensitive bool, runes []rune, 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 := runeAt(runes, index, lenRunes, forward)
if !caseSensitive { if !caseSensitive {
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
@@ -69,14 +81,19 @@ func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) {
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
if char == pattern[pidx] {
pchar := runeAt(pattern, pidx, lenPattern, forward)
if char == pchar {
if pidx--; pidx < 0 { if pidx--; pidx < 0 {
sidx = index sidx = index
break break
} }
} }
} }
return sidx, eidx if forward {
return sidx, eidx
}
return lenRunes - eidx, lenRunes - sidx
} }
return -1, -1 return -1, -1
} }
@@ -88,20 +105,21 @@ func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, 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, runes []rune, pattern []rune) (int, int) { func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
if len(pattern) == 0 { if len(pattern) == 0 {
return 0, 0 return 0, 0
} }
numRunes := len(runes) lenRunes := len(runes)
plen := len(pattern) lenPattern := len(pattern)
if numRunes < plen {
if lenRunes < lenPattern {
return -1, -1 return -1, -1
} }
pidx := 0 pidx := 0
for index := 0; index < numRunes; index++ { for index := 0; index < lenRunes; index++ {
char := runes[index] char := runeAt(runes, index, lenRunes, forward)
if !caseSensitive { if !caseSensitive {
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
@@ -109,10 +127,14 @@ func ExactMatchNaive(caseSensitive bool, runes []rune, pattern []rune) (int, int
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
if pattern[pidx] == char { pchar := runeAt(pattern, pidx, lenPattern, forward)
if pchar == char {
pidx++ pidx++
if pidx == plen { if pidx == lenPattern {
return index - plen + 1, index + 1 if forward {
return index - lenPattern + 1, index + 1
}
return lenRunes - (index + 1), lenRunes - (index - lenPattern + 1)
} }
} else { } else {
index -= pidx index -= pidx
@@ -123,7 +145,7 @@ func ExactMatchNaive(caseSensitive bool, runes []rune, pattern []rune) (int, int
} }
// PrefixMatch performs prefix-match // PrefixMatch performs prefix-match
func PrefixMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
if len(runes) < len(pattern) { if len(runes) < len(pattern) {
return -1, -1 return -1, -1
} }
@@ -141,7 +163,7 @@ func PrefixMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) {
} }
// SuffixMatch performs suffix-match // SuffixMatch performs suffix-match
func SuffixMatch(caseSensitive bool, input []rune, pattern []rune) (int, int) { func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) (int, int) {
runes := util.TrimRight(input) runes := util.TrimRight(input)
trimmedLen := len(runes) trimmedLen := len(runes)
diff := trimmedLen - len(pattern) diff := trimmedLen - len(pattern)
@@ -162,7 +184,7 @@ func SuffixMatch(caseSensitive bool, input []rune, pattern []rune) (int, int) {
} }
// EqualMatch performs equal-match // EqualMatch performs equal-match
func EqualMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) { func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
if len(runes) != len(pattern) { if len(runes) != len(pattern) {
return -1, -1 return -1, -1
} }

View File

@@ -5,11 +5,11 @@ import (
"testing" "testing"
) )
func assertMatch(t *testing.T, fun func(bool, []rune, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) { func assertMatch(t *testing.T, fun func(bool, bool, []rune, []rune) (int, int), caseSensitive bool, forward 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, []rune(input), []rune(pattern)) s, e := fun(caseSensitive, forward, []rune(input), []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)
} }
@@ -19,33 +19,51 @@ func assertMatch(t *testing.T, fun func(bool, []rune, []rune) (int, int), caseSe
} }
func TestFuzzyMatch(t *testing.T) { func TestFuzzyMatch(t *testing.T) {
assertMatch(t, FuzzyMatch, false, "fooBarbaz", "oBZ", 2, 9) assertMatch(t, FuzzyMatch, false, true, "fooBarbaz", "oBZ", 2, 9)
assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBZ", -1, -1) assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBZ", -1, -1)
assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBz", 2, 9) assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBz", 2, 9)
assertMatch(t, FuzzyMatch, true, "fooBarbaz", "fooBarbazz", -1, -1) assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "fooBarbazz", -1, -1)
}
func TestFuzzyMatchBackward(t *testing.T) {
assertMatch(t, FuzzyMatch, false, true, "foobar fb", "fb", 0, 4)
assertMatch(t, FuzzyMatch, false, false, "foobar fb", "fb", 7, 9)
} }
func TestExactMatchNaive(t *testing.T) { func TestExactMatchNaive(t *testing.T) {
assertMatch(t, ExactMatchNaive, false, "fooBarbaz", "oBA", 2, 5) for _, dir := range []bool{true, false} {
assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "oBA", -1, -1) assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5)
assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "fooBarbazz", -1, -1) assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1)
assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1)
}
}
func TestExactMatchNaiveBackward(t *testing.T) {
assertMatch(t, FuzzyMatch, false, true, "foobar foob", "oo", 1, 3)
assertMatch(t, FuzzyMatch, false, false, "foobar foob", "oo", 8, 10)
} }
func TestPrefixMatch(t *testing.T) { func TestPrefixMatch(t *testing.T) {
assertMatch(t, PrefixMatch, false, "fooBarbaz", "Foo", 0, 3) for _, dir := range []bool{true, false} {
assertMatch(t, PrefixMatch, true, "fooBarbaz", "Foo", -1, -1) assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3)
assertMatch(t, PrefixMatch, false, "fooBarbaz", "baz", -1, -1) assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1)
assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "baz", -1, -1)
}
} }
func TestSuffixMatch(t *testing.T) { func TestSuffixMatch(t *testing.T) {
assertMatch(t, SuffixMatch, false, "fooBarbaz", "Foo", -1, -1) for _, dir := range []bool{true, false} {
assertMatch(t, SuffixMatch, false, "fooBarbaz", "baz", 6, 9) assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1)
assertMatch(t, SuffixMatch, true, "fooBarbaz", "Baz", -1, -1) assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9)
assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1)
}
} }
func TestEmptyPattern(t *testing.T) { func TestEmptyPattern(t *testing.T) {
assertMatch(t, FuzzyMatch, true, "foobar", "", 0, 0) for _, dir := range []bool{true, false} {
assertMatch(t, ExactMatchNaive, true, "foobar", "", 0, 0) assertMatch(t, FuzzyMatch, true, dir, "foobar", "", 0, 0)
assertMatch(t, PrefixMatch, true, "foobar", "", 0, 0) assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0)
assertMatch(t, SuffixMatch, true, "foobar", "", 6, 6) assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0)
assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6)
}
} }

View File

@@ -8,7 +8,7 @@ import (
const ( const (
// Current version // Current version
version = "0.10.3" version = "0.10.9"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond
@@ -47,3 +47,10 @@ const (
EvtHeader EvtHeader
EvtClose EvtClose
) )
const (
exitOk = 0
exitNoMatch = 1
exitError = 2
exitInterrupt = 130
)

View File

@@ -56,7 +56,7 @@ func Run(opts *Options) {
if opts.Version { if opts.Version {
fmt.Println(version) fmt.Println(version)
os.Exit(0) os.Exit(exitOk)
} }
// Event channel // Event channel
@@ -143,7 +143,8 @@ func Run(opts *Options) {
// Matcher // Matcher
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern( return BuildPattern(
opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes) opts.Fuzzy, opts.Extended, opts.Case, opts.Tiebreak != byEnd,
opts.Nth, opts.Delimiter, runes)
} }
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
@@ -155,12 +156,14 @@ func Run(opts *Options) {
pattern := patternBuilder([]rune(*opts.Filter)) pattern := patternBuilder([]rune(*opts.Filter))
found := false
if streamingFilter { if streamingFilter {
reader := Reader{ reader := Reader{
func(runes []byte) bool { func(runes []byte) bool {
item := chunkList.trans(runes, 0) item := chunkList.trans(runes, 0)
if item != nil && pattern.MatchItem(item) { if item != nil && pattern.MatchItem(item) {
fmt.Println(string(item.text)) fmt.Println(string(item.text))
found = true
} }
return false return false
}, eventBox, opts.ReadZero} }, eventBox, opts.ReadZero}
@@ -174,10 +177,14 @@ func Run(opts *Options) {
chunks: snapshot, chunks: snapshot,
pattern: pattern}) pattern: pattern})
for i := 0; i < merger.Length(); i++ { for i := 0; i < merger.Length(); i++ {
fmt.Println(merger.Get(i).AsString()) fmt.Println(merger.Get(i).AsString(opts.Ansi))
found = true
} }
} }
os.Exit(0) if found {
os.Exit(exitOk)
}
os.Exit(exitNoMatch)
} }
// Synchronous search // Synchronous search
@@ -231,7 +238,7 @@ func Run(opts *Options) {
} }
case EvtHeader: case EvtHeader:
terminal.UpdateHeader(value.([]string), opts.HeaderLines) terminal.UpdateHeader(value.([]string))
case EvtSearchFin: case EvtSearchFin:
switch val := value.(type) { switch val := value.(type) {
@@ -250,9 +257,12 @@ func Run(opts *Options) {
fmt.Println() 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(opts.Ansi))
} }
os.Exit(0) if count > 0 {
os.Exit(exitOk)
}
os.Exit(exitNoMatch)
} }
deferred = false deferred = false
terminal.startChan <- true terminal.startChan <- true

View File

@@ -3,14 +3,15 @@ package curses
/* /*
#include <ncurses.h> #include <ncurses.h>
#include <locale.h> #include <locale.h>
#cgo LDFLAGS: -lncurses #cgo !static LDFLAGS: -lncurses
#cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl
*/ */
import "C" import "C"
import ( import (
"fmt" "fmt"
"os" "os"
"os/signal" "strings"
"syscall" "syscall"
"time" "time"
"unicode/utf8" "unicode/utf8"
@@ -50,6 +51,7 @@ const (
Invalid Invalid
Mouse Mouse
DoubleClick
BTab BTab
BSpace BSpace
@@ -261,7 +263,7 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
_screen = C.newterm(nil, C.stderr, C.stdin) _screen = C.newterm(nil, C.stderr, C.stdin)
if _screen == nil { if _screen == nil {
fmt.Println("Invalid $TERM: " + os.Getenv("TERM")) fmt.Println("Invalid $TERM: " + os.Getenv("TERM"))
os.Exit(1) os.Exit(2)
} }
C.set_term(_screen) C.set_term(_screen)
if mouse { if mouse {
@@ -270,14 +272,6 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
C.noecho() C.noecho()
C.raw() // stty dsusp undef C.raw() // stty dsusp undef
intChan := make(chan os.Signal, 1)
signal.Notify(intChan, os.Interrupt, os.Kill)
go func() {
<-intChan
Close()
os.Exit(1)
}()
if theme != nil { if theme != nil {
C.start_color() C.start_color()
initPairs(theme, black) initPairs(theme, black)
@@ -521,7 +515,12 @@ func MoveAndClear(y int, x int) {
} }
func Print(text string) { func Print(text string) {
C.addstr(C.CString(text)) C.addstr(C.CString(strings.Map(func(r rune) rune {
if r < 32 {
return -1
}
return r
}, text)))
} }
func CPrint(pair int, bold bool, text string) { func CPrint(pair int, bold bool, text string) {

View File

@@ -6,8 +6,8 @@ import (
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/curses"
) )
// Offset holds two 32-bit integers denoting the offsets of a matched substring // Offset holds three 32-bit integers denoting the offsets of a matched substring
type Offset [2]int32 type Offset [3]int32
type colorOffset struct { type colorOffset struct {
offset [2]int32 offset [2]int32
@@ -43,10 +43,13 @@ func (item *Item) Rank(cache bool) Rank {
} }
matchlen := 0 matchlen := 0
prevEnd := 0 prevEnd := 0
lenSum := 0
minBegin := math.MaxUint16 minBegin := math.MaxUint16
for _, offset := range item.offsets { for _, offset := range item.offsets {
begin := int(offset[0]) begin := int(offset[0])
end := int(offset[1]) end := int(offset[1])
trimLen := int(offset[2])
lenSum += trimLen
if prevEnd > begin { if prevEnd > begin {
begin = prevEnd begin = prevEnd
} }
@@ -65,10 +68,7 @@ func (item *Item) Rank(cache bool) Rank {
case byLength: case byLength:
// It is guaranteed that .transformed in not null in normal execution // It is guaranteed that .transformed in not null in normal execution
if item.transformed != nil { if item.transformed != nil {
lenSum := 0 // If offsets is empty, lenSum will be 0, but we don't care
for _, token := range item.transformed {
lenSum += len(token.text)
}
tiebreak = uint16(lenSum) tiebreak = uint16(lenSum)
} else { } else {
tiebreak = uint16(len(item.text)) tiebreak = uint16(len(item.text))
@@ -94,17 +94,21 @@ func (item *Item) Rank(cache bool) Rank {
} }
// AsString returns the original string // AsString returns the original string
func (item *Item) AsString() string { func (item *Item) AsString(stripAnsi bool) string {
return *item.StringPtr() return *item.StringPtr(stripAnsi)
} }
// StringPtr returns the pointer to the original string // StringPtr returns the pointer to the original string
func (item *Item) StringPtr() *string { func (item *Item) StringPtr(stripAnsi bool) *string {
runes := item.text
if item.origText != nil { if item.origText != nil {
runes = *item.origText if stripAnsi {
trimmed, _, _ := extractColor(string(*item.origText), nil)
return &trimmed
}
orig := string(*item.origText)
return &orig
} }
str := string(runes) str := string(item.text)
return &str return &str
} }
@@ -112,7 +116,8 @@ func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset
if len(item.colors) == 0 { if len(item.colors) == 0 {
var offsets []colorOffset var offsets []colorOffset
for _, off := range item.offsets { for _, off := range item.offsets {
offsets = append(offsets, colorOffset{offset: off, color: color, bold: bold})
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold})
} }
return offsets return offsets
} }
@@ -156,7 +161,7 @@ func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset
if curr != 0 && idx > start { if curr != 0 && idx > start {
if curr == -1 { if curr == -1 {
offsets = append(offsets, colorOffset{ offsets = append(offsets, colorOffset{
offset: Offset{int32(start), int32(idx)}, color: color, bold: bold}) offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold})
} else { } else {
ansi := item.colors[curr-1] ansi := item.colors[curr-1]
fg := ansi.color.fg fg := ansi.color.fg
@@ -176,7 +181,7 @@ func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset
} }
} }
offsets = append(offsets, colorOffset{ offsets = append(offsets, colorOffset{
offset: Offset{int32(start), int32(idx)}, offset: [2]int32{int32(start), int32(idx)},
color: curses.PairFor(fg, bg), color: curses.PairFor(fg, bg),
bold: ansi.color.bold || bold}) bold: ansi.color.bold || bold})
} }

View File

@@ -1,7 +1,6 @@
package fzf package fzf
import ( import (
"io/ioutil"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
@@ -17,7 +16,8 @@ const usage = `usage: fzf [options]
Search Search
-x, --extended Extended-search mode -x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match) (enabled by default; +x or --no-extended to disable)
-e, --exact Enable Exact-match
-i Case-insensitive match (default: smart-case match) -i Case-insensitive match (default: smart-case match)
+i Case-sensitive match +i Case-sensitive match
-n, --nth=N[,..] Comma-separated list of field index expressions -n, --nth=N[,..] Comma-separated list of field index expressions
@@ -45,7 +45,7 @@ const usage = `usage: fzf [options]
--bind=KEYBINDS Custom key bindings. Refer to the man page. --bind=KEYBINDS Custom key bindings. Refer to the man page.
--history=FILE History file --history=FILE History file
--history-size=N Maximum number of history entries (default: 1000) --history-size=N Maximum number of history entries (default: 1000)
--header-file=FILE The file whose content to be printed as header --header=STR String to print as header
--header-lines=N The first N lines of the input are treated as header --header-lines=N The first N lines of the input are treated as header
Scripting Scripting
@@ -59,20 +59,10 @@ const usage = `usage: fzf [options]
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
FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m') FZF_DEFAULT_OPTS Defaults options. (e.g. '--reverse --inline-info')
` `
// Mode denotes the current search mode
type Mode int
// Search modes
const (
ModeFuzzy Mode = iota
ModeExtended
ModeExtendedExact
)
// Case denotes case-sensitivity of search // Case denotes case-sensitivity of search
type Case int type Case int
@@ -99,7 +89,8 @@ func defaultMargin() [4]string {
// Options stores the values of command-line options // Options stores the values of command-line options
type Options struct { type Options struct {
Mode Mode Fuzzy bool
Extended bool
Case Case Case Case
Nth []Range Nth []Range
WithNth []Range WithNth []Range
@@ -144,7 +135,8 @@ func defaultTheme() *curses.ColorTheme {
func defaultOptions() *Options { func defaultOptions() *Options {
return &Options{ return &Options{
Mode: ModeFuzzy, Fuzzy: true,
Extended: true,
Case: CaseSmart, Case: CaseSmart,
Nth: make([]Range, 0), Nth: make([]Range, 0),
WithNth: make([]Range, 0), WithNth: make([]Range, 0),
@@ -180,14 +172,14 @@ func defaultOptions() *Options {
Version: false} Version: false}
} }
func help(ok int) { func help(code int) {
os.Stderr.WriteString(usage) os.Stderr.WriteString(usage)
os.Exit(ok) os.Exit(code)
} }
func errorExit(msg string) { func errorExit(msg string) {
os.Stderr.WriteString(msg + "\n") os.Stderr.WriteString(msg + "\n")
os.Exit(1) os.Exit(exitError)
} }
func optString(arg string, prefixes ...string) (bool, string) { func optString(arg string, prefixes ...string) (bool, string) {
@@ -344,6 +336,8 @@ func parseKeyChords(str string, message string) map[int]string {
chord = curses.SLeft chord = curses.SLeft
case "shift-right": case "shift-right":
chord = curses.SRight chord = curses.SRight
case "double-click":
chord = curses.DoubleClick
default: default:
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chord = curses.CtrlA + int(lkey[5]) - 'a' chord = curses.CtrlA + int(lkey[5]) - 'a'
@@ -381,8 +375,11 @@ func parseTiebreak(str string) tiebreak {
} }
func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
dupe := *theme if theme != nil {
return &dupe dupe := *theme
return &dupe
}
return nil
} }
func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme { func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme {
@@ -403,7 +400,7 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme
} }
// Color is disabled // Color is disabled
if theme == nil { if theme == nil {
errorExit("colors disabled; cannot customize colors") continue
} }
pair := strings.Split(str, ":") pair := strings.Split(str, ":")
@@ -604,12 +601,8 @@ func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
return keymap return keymap
} }
func readHeaderFile(filename string) []string { func strLines(str string) []string {
content, err := ioutil.ReadFile(filename) return strings.Split(strings.TrimSuffix(str, "\n"), "\n")
if err != nil {
errorExit("failed to read header file: " + filename)
}
return strings.Split(strings.TrimSuffix(string(content), "\n"), "\n")
} }
func parseMargin(margin string) [4]string { func parseMargin(margin string) [4]string {
@@ -682,13 +675,19 @@ func parseOptions(opts *Options, allArgs []string) {
arg := allArgs[i] arg := allArgs[i]
switch arg { switch arg {
case "-h", "--help": case "-h", "--help":
help(0) help(exitOk)
case "-x", "--extended": case "-x", "--extended":
opts.Mode = ModeExtended opts.Extended = true
case "-e", "--extended-exact": case "-e", "--exact":
opts.Mode = ModeExtendedExact opts.Fuzzy = false
case "+x", "--no-extended", "+e", "--no-extended-exact": case "--extended-exact":
opts.Mode = ModeFuzzy // Note that we now don't have --no-extended-exact
opts.Fuzzy = false
opts.Extended = true
case "+x", "--no-extended":
opts.Extended = false
case "+e", "--no-exact":
opts.Fuzzy = true
case "-q", "--query": case "-q", "--query":
opts.Query = nextString(allArgs, &i, "query string required") opts.Query = nextString(allArgs, &i, "query string required")
case "-f", "--filter": case "-f", "--filter":
@@ -793,16 +792,13 @@ func parseOptions(opts *Options, allArgs []string) {
setHistory(nextString(allArgs, &i, "history file path required")) setHistory(nextString(allArgs, &i, "history file path required"))
case "--history-size": case "--history-size":
setHistoryMax(nextInt(allArgs, &i, "history max size required")) setHistoryMax(nextInt(allArgs, &i, "history max size required"))
case "--no-header-file": case "--no-header":
opts.Header = []string{} opts.Header = []string{}
case "--no-header-lines": case "--no-header-lines":
opts.HeaderLines = 0 opts.HeaderLines = 0
case "--header-file": case "--header":
opts.Header = readHeaderFile( opts.Header = strLines(nextString(allArgs, &i, "header string required"))
nextString(allArgs, &i, "header file name required"))
opts.HeaderLines = 0
case "--header-lines": case "--header-lines":
opts.Header = []string{}
opts.HeaderLines = atoi( opts.HeaderLines = atoi(
nextString(allArgs, &i, "number of header lines required")) nextString(allArgs, &i, "number of header lines required"))
case "--no-margin": case "--no-margin":
@@ -843,11 +839,9 @@ func parseOptions(opts *Options, allArgs []string) {
setHistory(value) setHistory(value)
} else if match, value := optString(arg, "--history-size="); match { } else if match, value := optString(arg, "--history-size="); match {
setHistoryMax(atoi(value)) setHistoryMax(atoi(value))
} else if match, value := optString(arg, "--header-file="); match { } else if match, value := optString(arg, "--header="); match {
opts.Header = readHeaderFile(value) opts.Header = strLines(value)
opts.HeaderLines = 0
} else if match, value := optString(arg, "--header-lines="); match { } else if match, value := optString(arg, "--header-lines="); match {
opts.Header = []string{}
opts.HeaderLines = atoi(value) opts.HeaderLines = atoi(value)
} else if match, value := optString(arg, "--margin="); match { } else if match, value := optString(arg, "--margin="); match {
opts.Margin = parseMargin(value) opts.Margin = parseMargin(value)
@@ -878,7 +872,7 @@ func parseOptions(opts *Options, allArgs []string) {
// If we're not using extended search mode, --nth option becomes irrelevant // If we're not using extended search mode, --nth option becomes irrelevant
// if it contains the whole range // if it contains the whole range
if opts.Mode == ModeFuzzy || len(opts.Nth) == 1 { if !opts.Extended || len(opts.Nth) == 1 {
for _, r := range opts.Nth { for _, r := range opts.Nth {
if r.begin == rangeEllipsis && r.end == rangeEllipsis { if r.begin == rangeEllipsis && r.end == rangeEllipsis {
opts.Nth = make([]Range, 0) opts.Nth = make([]Range, 0)

View File

@@ -100,7 +100,7 @@ func TestIrrelevantNth(t *testing.T) {
t.Errorf("nth should be empty: %s", opts.Nth) t.Errorf("nth should be empty: %s", opts.Nth)
} }
} }
for _, words := range [][]string{[]string{"--nth", "..,3"}, []string{"--nth", "3,1.."}, []string{"--nth", "..-1,1"}} { for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} {
{ {
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(opts, words)
@@ -316,3 +316,15 @@ func TestColorSpec(t *testing.T) {
t.Errorf("using default colors") t.Errorf("using default colors")
} }
} }
func TestParseNilTheme(t *testing.T) {
var theme *curses.ColorTheme
newTheme := parseTheme(theme, "prompt:12")
if newTheme != nil {
t.Errorf("color is disabled. keep it that way.")
}
newTheme = parseTheme(theme, "prompt:12,dark,prompt:13")
if newTheme.Prompt != 13 {
t.Errorf("color should now be enabled and customized")
}
}

View File

@@ -6,6 +6,7 @@ import (
"strings" "strings"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/util"
) )
// fuzzy // fuzzy
@@ -37,14 +38,16 @@ type term struct {
// Pattern represents search pattern // Pattern represents search pattern
type Pattern struct { type Pattern struct {
mode Mode fuzzy bool
extended bool
caseSensitive bool caseSensitive bool
forward bool
text []rune text []rune
terms []term terms []term
hasInvTerm bool hasInvTerm bool
delimiter Delimiter delimiter Delimiter
nth []Range nth []Range
procFun map[termType]func(bool, []rune, []rune) (int, int) procFun map[termType]func(bool, bool, []rune, []rune) (int, int)
} }
var ( var (
@@ -61,7 +64,7 @@ func init() {
func clearPatternCache() { func clearPatternCache() {
// We can uniquely identify the pattern for a given string since // We can uniquely identify the pattern for a given string since
// mode and caseMode do not change while the program is running // search mode and caseMode do not change while the program is running
_patternCache = make(map[string]*Pattern) _patternCache = make(map[string]*Pattern)
} }
@@ -70,14 +73,13 @@ func clearChunkCache() {
} }
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(mode Mode, caseMode Case, func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
nth []Range, delimiter Delimiter, runes []rune) *Pattern { nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string var asString string
switch mode { if extended {
case ModeExtended, ModeExtendedExact:
asString = strings.Trim(string(runes), " ") asString = strings.Trim(string(runes), " ")
default: } else {
asString = string(runes) asString = string(runes)
} }
@@ -89,15 +91,14 @@ func BuildPattern(mode Mode, caseMode Case,
caseSensitive, hasInvTerm := true, false caseSensitive, hasInvTerm := true, false
terms := []term{} terms := []term{}
switch mode { if extended {
case ModeExtended, ModeExtendedExact: terms = parseTerms(fuzzy, caseMode, asString)
terms = parseTerms(mode, caseMode, asString)
for _, term := range terms { for _, term := range terms {
if term.inv { if term.inv {
hasInvTerm = true hasInvTerm = true
} }
} }
default: } else {
lowerString := strings.ToLower(asString) lowerString := strings.ToLower(asString)
caseSensitive = caseMode == CaseRespect || caseSensitive = caseMode == CaseRespect ||
caseMode == CaseSmart && lowerString != asString caseMode == CaseSmart && lowerString != asString
@@ -107,14 +108,16 @@ func BuildPattern(mode Mode, caseMode Case,
} }
ptr := &Pattern{ ptr := &Pattern{
mode: mode, fuzzy: fuzzy,
extended: extended,
caseSensitive: caseSensitive, caseSensitive: caseSensitive,
forward: forward,
text: []rune(asString), text: []rune(asString),
terms: terms, terms: terms,
hasInvTerm: hasInvTerm, hasInvTerm: hasInvTerm,
nth: nth, nth: nth,
delimiter: delimiter, delimiter: delimiter,
procFun: make(map[termType]func(bool, []rune, []rune) (int, int))} procFun: make(map[termType]func(bool, bool, []rune, []rune) (int, int))}
ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termFuzzy] = algo.FuzzyMatch
ptr.procFun[termEqual] = algo.EqualMatch ptr.procFun[termEqual] = algo.EqualMatch
@@ -126,7 +129,7 @@ func BuildPattern(mode Mode, caseMode Case,
return ptr return ptr
} }
func parseTerms(mode Mode, caseMode Case, str string) []term { func parseTerms(fuzzy bool, caseMode Case, str string) []term {
tokens := _splitRegex.Split(str, -1) tokens := _splitRegex.Split(str, -1)
terms := []term{} terms := []term{}
for _, token := range tokens { for _, token := range tokens {
@@ -138,7 +141,7 @@ func parseTerms(mode Mode, caseMode Case, str string) []term {
text = lowerText text = lowerText
} }
origText := []rune(text) origText := []rune(text)
if mode == ModeExtendedExact { if !fuzzy {
typ = termExact typ = termExact
} }
@@ -148,9 +151,13 @@ func parseTerms(mode Mode, caseMode Case, str string) []term {
} }
if strings.HasPrefix(text, "'") { if strings.HasPrefix(text, "'") {
if mode == ModeExtended { // Flip exactness
if fuzzy {
typ = termExact typ = termExact
text = text[1:] text = text[1:]
} else {
typ = termFuzzy
text = text[1:]
} }
} else if strings.HasPrefix(text, "^") { } else if strings.HasPrefix(text, "^") {
if strings.HasSuffix(text, "$") { if strings.HasSuffix(text, "$") {
@@ -179,7 +186,7 @@ func parseTerms(mode Mode, caseMode Case, str string) []term {
// IsEmpty returns true if the pattern is effectively empty // IsEmpty returns true if the pattern is effectively empty
func (p *Pattern) IsEmpty() bool { func (p *Pattern) IsEmpty() bool {
if p.mode == ModeFuzzy { if !p.extended {
return len(p.text) == 0 return len(p.text) == 0
} }
return len(p.terms) == 0 return len(p.terms) == 0
@@ -192,7 +199,7 @@ func (p *Pattern) AsString() string {
// CacheKey is used to build string to be used as the key of result cache // CacheKey is used to build string to be used as the key of result cache
func (p *Pattern) CacheKey() string { func (p *Pattern) CacheKey() string {
if p.mode == ModeFuzzy { if !p.extended {
return p.AsString() return p.AsString()
} }
cacheableTerms := []string{} cacheableTerms := []string{}
@@ -244,11 +251,11 @@ Loop:
func (p *Pattern) matchChunk(chunk *Chunk) []*Item { func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
matches := []*Item{} matches := []*Item{}
if p.mode == ModeFuzzy { if !p.extended {
for _, item := range *chunk { for _, item := range *chunk {
if sidx, eidx := p.fuzzyMatch(item); sidx >= 0 { if sidx, eidx, tlen := p.basicMatch(item); sidx >= 0 {
matches = append(matches, matches = append(matches,
dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}})) dupItem(item, []Offset{Offset{int32(sidx), int32(eidx), int32(tlen)}}))
} }
} }
} else { } else {
@@ -263,8 +270,8 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
// MatchItem returns true if the Item is a match // MatchItem returns true if the Item is a match
func (p *Pattern) MatchItem(item *Item) bool { func (p *Pattern) MatchItem(item *Item) bool {
if p.mode == ModeFuzzy { if !p.extended {
sidx, _ := p.fuzzyMatch(item) sidx, _, _ := p.basicMatch(item)
return sidx >= 0 return sidx >= 0
} }
offsets := p.extendedMatch(item) offsets := p.extendedMatch(item)
@@ -283,9 +290,12 @@ func dupItem(item *Item, offsets []Offset) *Item {
rank: Rank{0, 0, item.index}} rank: Rank{0, 0, item.index}}
} }
func (p *Pattern) fuzzyMatch(item *Item) (int, int) { func (p *Pattern) basicMatch(item *Item) (int, int, int) {
input := p.prepareInput(item) input := p.prepareInput(item)
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.text) if p.fuzzy {
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text)
}
return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text)
} }
func (p *Pattern) extendedMatch(item *Item) []Offset { func (p *Pattern) extendedMatch(item *Item) []Offset {
@@ -293,13 +303,13 @@ func (p *Pattern) extendedMatch(item *Item) []Offset {
offsets := []Offset{} offsets := []Offset{}
for _, term := range p.terms { for _, term := range p.terms {
pfun := p.procFun[term.typ] pfun := p.procFun[term.typ]
if sidx, eidx := p.iter(pfun, input, term.caseSensitive, term.text); sidx >= 0 { if sidx, eidx, tlen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 {
if term.inv { if term.inv {
break break
} }
offsets = append(offsets, Offset{int32(sidx), int32(eidx)}) offsets = append(offsets, Offset{int32(sidx), int32(eidx), int32(tlen)})
} else if term.inv { } else if term.inv {
offsets = append(offsets, Offset{0, 0}) offsets = append(offsets, Offset{0, 0, 0})
} }
} }
return offsets return offsets
@@ -315,19 +325,19 @@ func (p *Pattern) prepareInput(item *Item) []Token {
tokens := Tokenize(item.text, p.delimiter) tokens := Tokenize(item.text, p.delimiter)
ret = Transform(tokens, p.nth) ret = Transform(tokens, p.nth)
} else { } else {
ret = []Token{Token{text: item.text, prefixLength: 0}} ret = []Token{Token{text: item.text, prefixLength: 0, trimLength: util.TrimLen(item.text)}}
} }
item.transformed = ret item.transformed = ret
return ret return ret
} }
func (p *Pattern) iter(pfun func(bool, []rune, []rune) (int, int), func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) (int, int),
tokens []Token, caseSensitive bool, pattern []rune) (int, int) { tokens []Token, caseSensitive bool, forward bool, pattern []rune) (int, int, int) {
for _, part := range tokens { for _, part := range tokens {
prefixLength := part.prefixLength prefixLength := part.prefixLength
if sidx, eidx := pfun(caseSensitive, part.text, pattern); sidx >= 0 { if sidx, eidx := pfun(caseSensitive, forward, part.text, pattern); sidx >= 0 {
return sidx + prefixLength, eidx + prefixLength return sidx + prefixLength, eidx + prefixLength, part.trimLength
} }
} }
return -1, -1 return -1, -1, -1 // math.MaxUint16
} }

View File

@@ -8,7 +8,7 @@ import (
) )
func TestParseTermsExtended(t *testing.T) { func TestParseTermsExtended(t *testing.T) {
terms := parseTerms(ModeExtended, CaseSmart, terms := parseTerms(true, CaseSmart,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ ^iii$") "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ ^iii$")
if len(terms) != 9 || if len(terms) != 9 ||
terms[0].typ != termFuzzy || terms[0].inv || terms[0].typ != termFuzzy || terms[0].inv ||
@@ -33,15 +33,15 @@ func TestParseTermsExtended(t *testing.T) {
} }
func TestParseTermsExtendedExact(t *testing.T) { func TestParseTermsExtendedExact(t *testing.T) {
terms := parseTerms(ModeExtendedExact, CaseSmart, terms := parseTerms(false, CaseSmart,
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
if len(terms) != 8 || if len(terms) != 8 ||
terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 || terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 ||
terms[1].typ != termExact || terms[1].inv || len(terms[1].text) != 4 || terms[1].typ != termFuzzy || terms[1].inv || len(terms[1].text) != 3 ||
terms[2].typ != termPrefix || terms[2].inv || len(terms[2].text) != 3 || terms[2].typ != termPrefix || terms[2].inv || len(terms[2].text) != 3 ||
terms[3].typ != termSuffix || terms[3].inv || len(terms[3].text) != 3 || terms[3].typ != termSuffix || terms[3].inv || len(terms[3].text) != 3 ||
terms[4].typ != termExact || !terms[4].inv || len(terms[4].text) != 3 || terms[4].typ != termExact || !terms[4].inv || len(terms[4].text) != 3 ||
terms[5].typ != termExact || !terms[5].inv || len(terms[5].text) != 4 || terms[5].typ != termFuzzy || !terms[5].inv || len(terms[5].text) != 3 ||
terms[6].typ != termPrefix || !terms[6].inv || len(terms[6].text) != 3 || terms[6].typ != termPrefix || !terms[6].inv || len(terms[6].text) != 3 ||
terms[7].typ != termSuffix || !terms[7].inv || len(terms[7].text) != 3 { terms[7].typ != termSuffix || !terms[7].inv || len(terms[7].text) != 3 {
t.Errorf("%s", terms) t.Errorf("%s", terms)
@@ -49,7 +49,7 @@ func TestParseTermsExtendedExact(t *testing.T) {
} }
func TestParseTermsEmpty(t *testing.T) { func TestParseTermsEmpty(t *testing.T) {
terms := parseTerms(ModeExtended, CaseSmart, "' $ ^ !' !^ !$") terms := parseTerms(true, CaseSmart, "' $ ^ !' !^ !$")
if len(terms) != 0 { if len(terms) != 0 {
t.Errorf("%s", terms) t.Errorf("%s", terms)
} }
@@ -58,10 +58,10 @@ func TestParseTermsEmpty(t *testing.T) {
func TestExact(t *testing.T) { func TestExact(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pattern := BuildPattern(ModeExtended, CaseSmart, pattern := BuildPattern(true, true, CaseSmart, true,
[]Range{}, Delimiter{}, []rune("'abc")) []Range{}, Delimiter{}, []rune("'abc"))
sidx, eidx := algo.ExactMatchNaive( sidx, eidx := algo.ExactMatchNaive(
pattern.caseSensitive, []rune("aabbcc abc"), pattern.terms[0].text) pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), 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)
} }
@@ -70,11 +70,11 @@ func TestExact(t *testing.T) {
func TestEqual(t *testing.T) { func TestEqual(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, Delimiter{}, []rune("^AbC$")) pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int, eidxExpected int) { match := func(str string, sidxExpected int, eidxExpected int) {
sidx, eidx := algo.EqualMatch( sidx, eidx := algo.EqualMatch(
pattern.caseSensitive, []rune(str), pattern.terms[0].text) pattern.caseSensitive, pattern.forward, []rune(str), pattern.terms[0].text)
if sidx != sidxExpected || eidx != eidxExpected { if sidx != sidxExpected || eidx != eidxExpected {
t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
} }
@@ -86,17 +86,17 @@ func TestEqual(t *testing.T) {
func TestCaseSensitivity(t *testing.T) { func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache() defer clearPatternCache()
clearPatternCache() clearPatternCache()
pat1 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, Delimiter{}, []rune("abc")) pat1 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat2 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, Delimiter{}, []rune("Abc")) pat2 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat3 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, Delimiter{}, []rune("abc")) pat3 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat4 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, Delimiter{}, []rune("Abc")) pat4 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache() clearPatternCache()
pat5 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, Delimiter{}, []rune("abc")) pat5 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache() clearPatternCache()
pat6 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, Delimiter{}, []rune("Abc")) pat6 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc"))
if string(pat1.text) != "abc" || pat1.caseSensitive != false || if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true || string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@@ -109,19 +109,19 @@ func TestCaseSensitivity(t *testing.T) {
} }
func TestOrigTextAndTransformed(t *testing.T) { func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, Delimiter{}, []rune("jg")) pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize([]rune("junegunn"), Delimiter{}) tokens := Tokenize([]rune("junegunn"), Delimiter{})
trans := Transform(tokens, []Range{Range{1, 1}}) trans := Transform(tokens, []Range{Range{1, 1}})
origRunes := []rune("junegunn.choi") origRunes := []rune("junegunn.choi")
for _, mode := range []Mode{ModeFuzzy, ModeExtended} { for _, extended := range []bool{false, true} {
chunk := Chunk{ chunk := Chunk{
&Item{ &Item{
text: []rune("junegunn"), text: []rune("junegunn"),
origText: &origRunes, origText: &origRunes,
transformed: trans}, transformed: trans},
} }
pattern.mode = mode pattern.extended = extended
matches := pattern.matchChunk(&chunk) matches := pattern.matchChunk(&chunk)
if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" || if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" ||
matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 ||

View File

@@ -42,6 +42,8 @@ type Terminal struct {
history *History history *History
cycle bool cycle bool
header []string header []string
header0 []string
ansi bool
margin [4]string margin [4]string
marginInt [4]int marginInt [4]int
count int count int
@@ -178,12 +180,19 @@ func defaultKeymap() map[int]actionType {
keymap[C.Rune] = actRune keymap[C.Rune] = actRune
keymap[C.Mouse] = actMouse keymap[C.Mouse] = actMouse
keymap[C.DoubleClick] = actAccept
return keymap return keymap
} }
// 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)
var header []string
if opts.Reverse {
header = opts.Header
} else {
header = reverseStringArray(opts.Header)
}
return &Terminal{ return &Terminal{
inlineInfo: opts.InlineInfo, inlineInfo: opts.InlineInfo,
prompt: opts.Prompt, prompt: opts.Prompt,
@@ -206,7 +215,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
margin: opts.Margin, margin: opts.Margin,
marginInt: [4]int{0, 0, 0, 0}, marginInt: [4]int{0, 0, 0, 0},
cycle: opts.Cycle, cycle: opts.Cycle,
header: opts.Header, header: header,
header0: header,
ansi: opts.Ansi,
reading: true, reading: true,
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[uint32]selectedItem), selected: make(map[uint32]selectedItem),
@@ -239,18 +250,19 @@ func (t *Terminal) UpdateCount(cnt int, final bool) {
} }
} }
// UpdateHeader updates the header func reverseStringArray(input []string) []string {
func (t *Terminal) UpdateHeader(header []string, lines int) { size := len(input)
t.mutex.Lock() reversed := make([]string, size)
t.header = make([]string, lines) for idx, str := range input {
copy(t.header, header) reversed[size-idx-1] = str
if !t.reverse {
reversed := make([]string, lines)
for idx, str := range t.header {
reversed[lines-idx-1] = str
}
t.header = reversed
} }
return reversed
}
// UpdateHeader updates the header
func (t *Terminal) UpdateHeader(header []string) {
t.mutex.Lock()
t.header = append(append([]string{}, t.header0...), header...)
t.mutex.Unlock() t.mutex.Unlock()
t.reqBox.Set(reqHeader, nil) t.reqBox.Set(reqHeader, nil)
} }
@@ -278,17 +290,19 @@ func (t *Terminal) UpdateList(merger *Merger) {
t.reqBox.Set(reqList, nil) t.reqBox.Set(reqList, nil)
} }
func (t *Terminal) output() { func (t *Terminal) output() bool {
if t.printQuery { if t.printQuery {
fmt.Println(string(t.input)) fmt.Println(string(t.input))
} }
if len(t.expect) > 0 { if len(t.expect) > 0 {
fmt.Println(t.pressed) fmt.Println(t.pressed)
} }
if len(t.selected) == 0 { found := len(t.selected) > 0
if !found {
cnt := t.merger.Length() cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy { if cnt > 0 && cnt > t.cy {
fmt.Println(t.merger.Get(t.cy).AsString()) fmt.Println(t.merger.Get(t.cy).AsString(t.ansi))
found = true
} }
} else { } else {
sels := make([]selectedItem, 0, len(t.selected)) sels := make([]selectedItem, 0, len(t.selected))
@@ -300,6 +314,7 @@ func (t *Terminal) output() {
fmt.Println(*sel.text) fmt.Println(*sel.text)
} }
} }
return found
} }
func runeWidth(r rune, prefixWidth int) int { func runeWidth(r rune, prefixWidth int) int {
@@ -431,9 +446,6 @@ func (t *Terminal) printHeader() {
max := t.maxHeight() max := t.maxHeight()
var state *ansiState var state *ansiState
for idx, lineStr := range t.header { for idx, lineStr := range t.header {
if !t.reverse {
idx = len(t.header) - idx - 1
}
line := idx + 2 line := idx + 2
if t.inlineInfo { if t.inlineInfo {
line-- line--
@@ -701,6 +713,22 @@ func executeCommand(template string, current string) {
func (t *Terminal) Loop() { func (t *Terminal) Loop() {
<-t.startChan <-t.startChan
{ // Late initialization { // Late initialization
intChan := make(chan os.Signal, 1)
signal.Notify(intChan, os.Interrupt, os.Kill)
go func() {
<-intChan
t.reqBox.Set(reqQuit, nil)
}()
resizeChan := make(chan os.Signal, 1)
signal.Notify(resizeChan, syscall.SIGWINCH)
go func() {
for {
<-resizeChan
t.reqBox.Set(reqRedraw, nil)
}
}()
t.mutex.Lock() t.mutex.Lock()
t.initFunc() t.initFunc()
t.calculateMargins() t.calculateMargins()
@@ -716,15 +744,6 @@ func (t *Terminal) Loop() {
t.reqBox.Set(reqRefresh, nil) t.reqBox.Set(reqRefresh, nil)
}() }()
resizeChan := make(chan os.Signal, 1)
signal.Notify(resizeChan, syscall.SIGWINCH)
go func() {
for {
<-resizeChan
t.reqBox.Set(reqRedraw, nil)
}
}()
// Keep the spinner spinning // Keep the spinner spinning
go func() { go func() {
for { for {
@@ -741,7 +760,7 @@ func (t *Terminal) Loop() {
} }
exit := func(code int) { exit := func(code int) {
if code == 0 && t.history != nil { if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input)) t.history.append(string(t.input))
} }
os.Exit(code) os.Exit(code)
@@ -774,11 +793,13 @@ func (t *Terminal) Loop() {
t.printAll() t.printAll()
case reqClose: case reqClose:
C.Close() C.Close()
t.output() if t.output() {
exit(0) exit(exitOk)
}
exit(exitNoMatch)
case reqQuit: case reqQuit:
C.Close() C.Close()
exit(1) exit(exitInterrupt)
} }
} }
t.placeCursor() t.placeCursor()
@@ -805,7 +826,7 @@ func (t *Terminal) Loop() {
} }
selectItem := func(item *Item) bool { selectItem := func(item *Item) bool {
if _, found := t.selected[item.index]; !found { if _, found := t.selected[item.index]; !found {
t.selected[item.index] = selectedItem{time.Now(), item.StringPtr()} t.selected[item.index] = selectedItem{time.Now(), item.StringPtr(t.ansi)}
return true return true
} }
return false return false
@@ -830,6 +851,209 @@ func (t *Terminal) Loop() {
} }
} }
var doAction func(actionType, int) bool
doAction = func(action actionType, mapkey int) bool {
switch action {
case actIgnore:
case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy)
executeCommand(t.execmap[mapkey], item.AsString(t.ansi))
}
case actInvalid:
t.mutex.Unlock()
return false
case actToggleSort:
t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock()
return false
case actBeginningOfLine:
t.cx = 0
case actBackwardChar:
if t.cx > 0 {
t.cx--
}
case actAbort:
req(reqQuit)
case actDeleteChar:
t.delChar()
case actDeleteCharEOF:
if !t.delChar() && t.cx == 0 {
req(reqQuit)
}
case actEndOfLine:
t.cx = len(t.input)
case actCancel:
if len(t.input) == 0 {
req(reqQuit)
} else {
t.yanked = t.input
t.input = []rune{}
t.cx = 0
}
case actForwardChar:
if t.cx < len(t.input) {
t.cx++
}
case actBackwardDeleteChar:
if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
t.cx--
}
case actSelectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
selectItem(item)
}
req(reqList, reqInfo)
}
case actDeselectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
delete(t.selected, item.index)
}
req(reqList, reqInfo)
}
case actToggle:
if t.multi && t.merger.Length() > 0 {
toggle()
req(reqList)
}
case actToggleAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
toggleY(i)
}
req(reqList, reqInfo)
}
case actToggleDown:
if t.multi && t.merger.Length() > 0 {
toggle()
t.vmove(-1)
req(reqList)
}
case actToggleUp:
if t.multi && t.merger.Length() > 0 {
toggle()
t.vmove(1)
req(reqList)
}
case actDown:
t.vmove(-1)
req(reqList)
case actUp:
t.vmove(1)
req(reqList)
case actAccept:
req(reqClose)
case actClearScreen:
req(reqRedraw)
case actUnixLineDiscard:
if t.cx > 0 {
t.yanked = copySlice(t.input[:t.cx])
t.input = t.input[t.cx:]
t.cx = 0
}
case actUnixWordRubout:
if t.cx > 0 {
t.rubout("\\s\\S")
}
case actBackwardKillWord:
if t.cx > 0 {
t.rubout("[^[:alnum:]][[:alnum:]]")
}
case actYank:
suffix := copySlice(t.input[t.cx:])
t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
t.cx += len(t.yanked)
case actPageUp:
t.vmove(t.maxItems() - 1)
req(reqList)
case actPageDown:
t.vmove(-(t.maxItems() - 1))
req(reqList)
case actBackwardWord:
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
case actForwardWord:
t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
case actKillWord:
ncx := t.cx +
findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
if ncx > t.cx {
t.yanked = copySlice(t.input[t.cx:ncx])
t.input = append(t.input[:t.cx], t.input[ncx:]...)
}
case actKillLine:
if t.cx < len(t.input) {
t.yanked = copySlice(t.input[t.cx:])
t.input = t.input[:t.cx]
}
case actRune:
prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
t.cx++
case actPreviousHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = []rune(t.history.previous())
t.cx = len(t.input)
}
case actNextHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = []rune(t.history.next())
t.cx = len(t.input)
}
case actMouse:
me := event.MouseEvent
mx, my := me.X, me.Y
if me.S != 0 {
// Scroll
if t.merger.Length() > 0 {
if t.multi && me.Mod {
toggle()
}
t.vmove(me.S)
req(reqList)
}
} else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] &&
my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] {
mx -= t.marginInt[3]
my -= t.marginInt[0]
mx = util.Constrain(mx-len(t.prompt), 0, len(t.input))
if !t.reverse {
my = t.maxHeight() - my - 1
}
min := 2 + len(t.header)
if t.inlineInfo {
min--
}
if me.Double {
// Double-click
if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
return doAction(t.keymap[C.DoubleClick], C.DoubleClick)
}
}
} else if me.Down {
if my == 0 && mx >= 0 {
// Prompt
t.cx = mx
} else if my >= min {
// List
if t.vset(t.offset+my-min) && t.multi && me.Mod {
toggle()
}
req(reqList)
}
}
}
}
return true
}
action := t.keymap[event.Type] action := t.keymap[event.Type]
mapkey := event.Type mapkey := event.Type
if event.Type == C.Rune { if event.Type == C.Rune {
@@ -838,204 +1062,8 @@ func (t *Terminal) Loop() {
action = act action = act
} }
} }
switch action { if !doAction(action, mapkey) {
case actIgnore:
case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy)
executeCommand(t.execmap[mapkey], item.AsString())
}
case actInvalid:
t.mutex.Unlock()
continue continue
case actToggleSort:
t.sort = !t.sort
t.eventBox.Set(EvtSearchNew, t.sort)
t.mutex.Unlock()
continue
case actBeginningOfLine:
t.cx = 0
case actBackwardChar:
if t.cx > 0 {
t.cx--
}
case actAbort:
req(reqQuit)
case actDeleteChar:
t.delChar()
case actDeleteCharEOF:
if !t.delChar() && t.cx == 0 {
req(reqQuit)
}
case actEndOfLine:
t.cx = len(t.input)
case actCancel:
if len(t.input) == 0 {
req(reqQuit)
} else {
t.yanked = t.input
t.input = []rune{}
t.cx = 0
}
case actForwardChar:
if t.cx < len(t.input) {
t.cx++
}
case actBackwardDeleteChar:
if t.cx > 0 {
t.input = append(t.input[:t.cx-1], t.input[t.cx:]...)
t.cx--
}
case actSelectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
selectItem(item)
}
req(reqList, reqInfo)
}
case actDeselectAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i)
delete(t.selected, item.index)
}
req(reqList, reqInfo)
}
case actToggle:
if t.multi && t.merger.Length() > 0 {
toggle()
req(reqList)
}
case actToggleAll:
if t.multi {
for i := 0; i < t.merger.Length(); i++ {
toggleY(i)
}
req(reqList, reqInfo)
}
case actToggleDown:
if t.multi && t.merger.Length() > 0 {
toggle()
t.vmove(-1)
req(reqList)
}
case actToggleUp:
if t.multi && t.merger.Length() > 0 {
toggle()
t.vmove(1)
req(reqList)
}
case actDown:
t.vmove(-1)
req(reqList)
case actUp:
t.vmove(1)
req(reqList)
case actAccept:
req(reqClose)
case actClearScreen:
req(reqRedraw)
case actUnixLineDiscard:
if t.cx > 0 {
t.yanked = copySlice(t.input[:t.cx])
t.input = t.input[t.cx:]
t.cx = 0
}
case actUnixWordRubout:
if t.cx > 0 {
t.rubout("\\s\\S")
}
case actBackwardKillWord:
if t.cx > 0 {
t.rubout("[^[:alnum:]][[:alnum:]]")
}
case actYank:
suffix := copySlice(t.input[t.cx:])
t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
t.cx += len(t.yanked)
case actPageUp:
t.vmove(t.maxItems() - 1)
req(reqList)
case actPageDown:
t.vmove(-(t.maxItems() - 1))
req(reqList)
case actBackwardWord:
t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1
case actForwardWord:
t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
case actKillWord:
ncx := t.cx +
findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1
if ncx > t.cx {
t.yanked = copySlice(t.input[t.cx:ncx])
t.input = append(t.input[:t.cx], t.input[ncx:]...)
}
case actKillLine:
if t.cx < len(t.input) {
t.yanked = copySlice(t.input[t.cx:])
t.input = t.input[:t.cx]
}
case actRune:
prefix := copySlice(t.input[:t.cx])
t.input = append(append(prefix, event.Char), t.input[t.cx:]...)
t.cx++
case actPreviousHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = []rune(t.history.previous())
t.cx = len(t.input)
}
case actNextHistory:
if t.history != nil {
t.history.override(string(t.input))
t.input = []rune(t.history.next())
t.cx = len(t.input)
}
case actMouse:
me := event.MouseEvent
mx, my := me.X, me.Y
if me.S != 0 {
// Scroll
if t.merger.Length() > 0 {
if t.multi && me.Mod {
toggle()
}
t.vmove(me.S)
req(reqList)
}
} else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] &&
my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] {
mx -= t.marginInt[3]
my -= t.marginInt[0]
mx = util.Constrain(mx-len(t.prompt), 0, len(t.input))
if !t.reverse {
my = t.maxHeight() - my - 1
}
min := 2 + len(t.header)
if t.inlineInfo {
min--
}
if me.Double {
// Double-click
if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
req(reqClose)
}
}
} else if me.Down {
if my == 0 && mx >= 0 {
// Prompt
t.cx = mx
} else if my >= min {
// List
if t.vset(t.offset+my-min) && t.multi && me.Mod {
toggle()
}
req(reqList)
}
}
}
} }
changed := string(previousInput) != string(t.input) changed := string(previousInput) != string(t.input)
t.mutex.Unlock() // Must be unlocked before touching reqBox t.mutex.Unlock() // Must be unlocked before touching reqBox

View File

@@ -20,6 +20,7 @@ type Range struct {
type Token struct { type Token struct {
text []rune text []rune
prefixLength int prefixLength int
trimLength int
} }
// Delimiter for tokenizing the input // Delimiter for tokenizing the input
@@ -81,7 +82,7 @@ func withPrefixLengths(tokens [][]rune, 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
ret[idx] = Token{text: token, prefixLength: prefixLength} ret[idx] = Token{token, prefixLength, util.TrimLen(token)}
prefixLength += len(token) prefixLength += len(token)
} }
return ret return ret
@@ -233,7 +234,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
} else { } else {
prefixLength = 0 prefixLength = 0
} }
transTokens[idx] = Token{part, prefixLength} transTokens[idx] = Token{part, prefixLength, util.TrimLen(part)}
} }
return transTokens return transTokens
} }

View File

@@ -44,22 +44,22 @@ func TestTokenize(t *testing.T) {
// AWK-style // AWK-style
input := " abc: def: ghi " input := " abc: def: ghi "
tokens := Tokenize([]rune(input), Delimiter{}) tokens := Tokenize([]rune(input), Delimiter{})
if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 { if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
// With delimiter // With delimiter
tokens = Tokenize([]rune(input), delimiterRegexp(":")) tokens = Tokenize([]rune(input), delimiterRegexp(":"))
if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 { if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
// With delimiter regex // With delimiter regex
tokens = Tokenize([]rune(input), delimiterRegexp("\\s+")) tokens = Tokenize([]rune(input), delimiterRegexp("\\s+"))
if string(tokens[0].text) != " " || tokens[0].prefixLength != 0 || if string(tokens[0].text) != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 ||
string(tokens[1].text) != "abc: " || tokens[1].prefixLength != 2 || string(tokens[1].text) != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 ||
string(tokens[2].text) != "def: " || tokens[2].prefixLength != 8 || string(tokens[2].text) != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 ||
string(tokens[3].text) != "ghi " || tokens[3].prefixLength != 14 { string(tokens[3].text) != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
} }

View File

@@ -75,6 +75,7 @@ func IsTty() bool {
return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 return int(C.isatty(C.int(os.Stdin.Fd()))) != 0
} }
// TrimRight returns rune array with trailing white spaces cut off
func TrimRight(runes []rune) []rune { func TrimRight(runes []rune) []rune {
var i int var i int
for i = len(runes) - 1; i >= 0; i-- { for i = len(runes) - 1; i >= 0; i-- {
@@ -86,6 +87,7 @@ func TrimRight(runes []rune) []rune {
return runes[0 : i+1] return runes[0 : i+1]
} }
// BytesToRunes converts byte array into rune array
func BytesToRunes(bytea []byte) []rune { func BytesToRunes(bytea []byte) []rune {
runes := make([]rune, 0, len(bytea)) runes := make([]rune, 0, len(bytea))
for i := 0; i < len(bytea); { for i := 0; i < len(bytea); {
@@ -100,3 +102,27 @@ func BytesToRunes(bytea []byte) []rune {
} }
return runes return runes
} }
// TrimLen returns the length of trimmed rune array
func TrimLen(runes []rune) int {
var i int
for i = len(runes) - 1; i >= 0; i-- {
char := runes[i]
if char != ' ' && char != '\t' {
break
}
}
// Completely empty
if i < 0 {
return 0
}
var j int
for j = 0; j < len(runes); j++ {
char := runes[j]
if char != ' ' && char != '\t' {
break
}
}
return i - j + 1
}

View File

@@ -20,3 +20,23 @@ func TestContrain(t *testing.T) {
t.Error("Expected", 3) t.Error("Expected", 3)
} }
} }
func TestTrimLen(t *testing.T) {
check := func(str string, exp int) {
trimmed := TrimLen([]rune(str))
if trimmed != exp {
t.Errorf("Invalid TrimLen result for '%s': %d (expected %d)",
str, trimmed, exp)
}
}
check("hello", 5)
check("hello ", 5)
check("hello ", 5)
check(" hello", 5)
check(" hello", 5)
check(" hello ", 5)
check(" hello ", 5)
check("h o", 5)
check(" h o ", 5)
check(" ", 0)
}

View File

@@ -8,7 +8,7 @@ DEFAULT_TIMEOUT = 20
base = File.expand_path('../../', __FILE__) base = File.expand_path('../../', __FILE__)
Dir.chdir base Dir.chdir base
FZF = "#{base}/bin/fzf" FZF = "FZF_DEFAULT_OPTS= FZF_DEFAULT_COMMAND= #{base}/bin/fzf"
class NilClass class NilClass
def include? str def include? str
@@ -213,7 +213,7 @@ class TestGoFZF < TestBase
end end
def test_fzf_default_command def test_fzf_default_command
tmux.send_keys "FZF_DEFAULT_COMMAND='echo hello' #{fzf}", :Enter tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND='echo hello'"), :Enter
tmux.until { |lines| lines.last =~ /^>/ } tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Enter tmux.send_keys :Enter
@@ -527,6 +527,64 @@ class TestGoFZF < TestBase
assert_equal output, `cat #{tempname} | #{FZF} -fh -n2 -d:`.split($/) assert_equal output, `cat #{tempname} | #{FZF} -fh -n2 -d:`.split($/)
end end
def test_tiebreak_length_with_nth_trim_length
input = [
"apple juice bottle 1",
"apple ui bottle 2",
"app ice bottle 3",
"app ic bottle 4",
]
writelines tempname, input
# len(1)
output = [
"app ice bottle 3",
"app ic bottle 4",
"apple juice bottle 1",
"apple ui bottle 2",
]
assert_equal output, `cat #{tempname} | #{FZF} -fa -n1`.split($/)
# len(1 ~ 2)
output = [
"apple ui bottle 2",
"app ic bottle 4",
"apple juice bottle 1",
"app ice bottle 3",
]
assert_equal output, `cat #{tempname} | #{FZF} -fai -n1..2`.split($/)
# len(1) + len(2)
output = [
"app ic bottle 4",
"app ice bottle 3",
"apple ui bottle 2",
"apple juice bottle 1",
]
assert_equal output, `cat #{tempname} | #{FZF} -x -f"a i" -n1,2`.split($/)
# len(2)
output = [
"apple ui bottle 2",
"app ic bottle 4",
"app ice bottle 3",
"apple juice bottle 1",
]
assert_equal output, `cat #{tempname} | #{FZF} -fi -n2`.split($/)
assert_equal output, `cat #{tempname} | #{FZF} -fi -n2,1..2`.split($/)
end
def test_tiebreak_end_backward_scan
input = %w[
foobar-fb
fubar
]
writelines tempname, input
assert_equal input.reverse, `cat #{tempname} | #{FZF} -f fb`.split($/)
assert_equal input, `cat #{tempname} | #{FZF} -f fb --tiebreak=end`.split($/)
end
def test_invalid_cache def test_invalid_cache
tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter
tmux.until { |lines| lines[-2].include? '2/3' } tmux.until { |lines| lines[-2].include? '2/3' }
@@ -726,8 +784,8 @@ class TestGoFZF < TestBase
assert_equal '6', readonce.chomp assert_equal '6', readonce.chomp
end end
def test_header_file def test_header
tmux.send_keys "seq 100 | #{fzf "--header-file <(head -5 #{__FILE__})"}", :Enter tmux.send_keys "seq 100 | #{fzf "--header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip) header = File.readlines(__FILE__).take(5).map(&:strip)
tmux.until do |lines| tmux.until do |lines|
lines[-2].include?('100/100') && lines[-2].include?('100/100') &&
@@ -735,8 +793,8 @@ class TestGoFZF < TestBase
end end
end end
def test_header_file_reverse def test_header_reverse
tmux.send_keys "seq 100 | #{fzf "--header-file=<(head -5 #{__FILE__}) --reverse"}", :Enter tmux.send_keys "seq 100 | #{fzf "--header=\\\"\\$(head -5 #{__FILE__})\\\" --reverse"}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip) header = File.readlines(__FILE__).take(5).map(&:strip)
tmux.until do |lines| tmux.until do |lines|
lines[1].include?('100/100') && lines[1].include?('100/100') &&
@@ -744,6 +802,26 @@ class TestGoFZF < TestBase
end end
end end
def test_header_and_header_lines
tmux.send_keys "seq 100 | #{fzf "--header-lines 10 --header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip)
tmux.until do |lines|
lines[-2].include?('90/90') &&
lines[-7...-2].map(&:strip) == header &&
lines[-17...-7].map(&:strip) == (1..10).map(&:to_s).reverse
end
end
def test_header_and_header_lines_reverse
tmux.send_keys "seq 100 | #{fzf "--reverse --header-lines 10 --header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip)
tmux.until do |lines|
lines[1].include?('90/90') &&
lines[2...7].map(&:strip) == header &&
lines[7...17].map(&:strip) == (1..10).map(&:to_s)
end
end
def test_canel def test_canel
tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter
tmux.until { |lines| lines[-2].include?('10/10') } tmux.until { |lines| lines[-2].include?('10/10') }
@@ -769,9 +847,72 @@ class TestGoFZF < TestBase
tmux.send_keys :Enter tmux.send_keys :Enter
end end
def test_with_nth
writelines tempname, ['hello world ', 'byebye']
assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1`.chomp
end
def test_with_nth_ansi
writelines tempname, ["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye']
assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi`.chomp
end
def test_with_nth_no_ansi
src = "\x1b[33mhello \x1b[34;1mworld\x1b[m "
writelines tempname, [src, 'byebye']
assert_equal src, `cat #{tempname} | #{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi`.chomp
end
def test_exit_0_exit_code
`echo foo | #{FZF} -q bar -0`
assert_equal 1, $?.exitstatus
end
def test_invalid_term def test_invalid_term
tmux.send_keys "TERM=xxx fzf", :Enter lines = `TERM=xxx #{FZF}`
tmux.until { |lines| lines.any? { |l| l.include? 'Invalid $TERM: xxx' } } assert_equal 2, $?.exitstatus
assert lines.include?('Invalid $TERM: xxx')
end
def test_invalid_option
lines = `#{FZF} --foobar 2>&1`
assert_equal 2, $?.exitstatus
assert lines.include?('unknown option: --foobar'), lines
end
def test_filter_exitstatus
# filter / streaming filter
["", "--no-sort"].each do |opts|
assert `echo foo | #{FZF} -f foo #{opts}`.include?('foo')
assert_equal 0, $?.exitstatus
assert `echo foo | #{FZF} -f bar #{opts}`.empty?
assert_equal 1, $?.exitstatus
end
end
def test_exitstatus_empty
{ '99' => '0', '999' => '1' }.each do |query, status|
tmux.send_keys "seq 100 | #{FZF} -q #{query}", :Enter
tmux.until { |lines| lines[-2] =~ %r{ [10]/100} }
tmux.send_keys :Enter
tmux.send_keys 'echo --\$?--'
tmux.until { |lines| lines.last.include? "echo --$?--" }
tmux.send_keys :Enter
tmux.until { |lines| lines.last.include? "--#{status}--" }
end
end
def test_default_extended
assert_equal '100', `seq 100 | #{FZF} -f "1 00$"`.chomp
assert_equal '', `seq 100 | #{FZF} -f "1 00$" +x`.chomp
end
def test_exact
assert_equal 4, `seq 123 | #{FZF} -f 13`.lines.length
assert_equal 2, `seq 123 | #{FZF} -f 13 -e`.lines.length
assert_equal 4, `seq 123 | #{FZF} -f 13 +e`.lines.length
end end
private private