Compare commits

...

79 Commits

Author SHA1 Message Date
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
Junegunn Choi
8156e9894e 0.10.3 2015-08-12 02:09:46 +09:00
Junegunn Choi
cacc212f12 [install] Prerelease of 0.10.3 2015-08-11 00:21:09 +09:00
Junegunn Choi
d0f2c00f9f Fix --with-nth performance; use simpler regular expression
Related #317
2015-08-11 00:15:41 +09:00
Junegunn Choi
766427de0c Fix --with-nth performance; avoid regex if possible
Close #317
2015-08-10 18:34:20 +09:00
Junegunn Choi
a7b75c99a5 [install] Stop installer when failed to download the binary
Close #312
2015-08-08 03:53:46 +09:00
Junegunn Choi
bae10a6582 [install] Add an extra new line character
so that it doesn't corrupt file that doesn't end with a new line
character. Close #311.
2015-08-05 23:50:38 +09:00
Junegunn Choi
c4cf90a3d2 0.10.2 2015-08-03 00:21:21 +09:00
Junegunn Choi
15c49a3e08 Fix race condition 2015-08-03 00:14:34 +09:00
Junegunn Choi
ae87f6548a GoLint 2015-08-02 23:54:53 +09:00
Junegunn Choi
7833fa7396 [install] Always download binary when --pre is set 2015-08-02 15:09:57 +09:00
Junegunn Choi
9278f3acd2 [install] Add --pre option for downloading prerelease binary 2015-08-02 15:02:12 +09:00
Junegunn Choi
e83ae34a3b Update CHANGELOG - 0.10.2 2015-08-02 14:32:34 +09:00
Junegunn Choi
e13bafc1ab Performance fix - unnecessary rune convertion on --ansi
> time cat /tmp/list | fzf-0.10.1-darwin_amd64 --ansi -fqwerty > /dev/null

    real    0m4.364s
    user    0m8.231s
    sys     0m0.820s

    > time cat /tmp/list | fzf --ansi -fqwerty > /dev/null

    real    0m4.624s
    user    0m5.755s
    sys     0m0.732s
2015-08-02 14:25:57 +09:00
Junegunn Choi
0ea66329b8 Performance tuning - eager rune array conversion
> wc -l /tmp/list2
     2594098 /tmp/list2

    > time cat /tmp/list2 | fzf-0.10.1-darwin_amd64 -fqwerty > /dev/null

    real    0m5.418s
    user    0m10.990s
    sys     0m1.302s

    > time cat /tmp/list2 | fzf-head -fqwerty > /dev/null

    real    0m4.862s
    user    0m6.619s
    sys     0m0.982s
2015-08-02 14:00:18 +09:00
Junegunn Choi
634670e3ea Lint 2015-08-02 13:11:59 +09:00
Junegunn Choi
dea60b11bc Only consider the lengths of the relevant parts when --nth is set 2015-08-01 23:13:24 +09:00
Junegunn Choi
5e90f0a57b Fix default command so that it doesn't fail on dash-prefixed files
Close #310
2015-08-01 21:51:10 +09:00
Junegunn Choi
0b4542fcdf [vim] Temporarily disable &autochdir when opening files (#306) 2015-07-29 17:55:58 +09:00
Junegunn Choi
02bd2d2adf Do not proceed if $TERM is invalid
Related #305
2015-07-28 14:35:46 +09:00
Junegunn Choi
dce6fe6f2d [fzf-tmux] Ensure that the same $TERM value is used in split
Fix #305. ncurses can crash on invalid $TERM. fzf-tmux uses bash on
a new pane so we have to make sure that the $TERM is consistent with
that of the hosting shell.
2015-07-28 14:17:25 +09:00
Junegunn Choi
fcae99f09b No need to "tmux list-panes" when obviously not on tmux (#303) 2015-07-28 00:56:03 +09:00
Junegunn Choi
fb1b026d3d Always check if the pane is zoomed
Close #303
2015-07-28 00:30:17 +09:00
Junegunn Choi
9f953fc944 Do not use tmux pane if the current pane is zoomed
Close #303
2015-07-28 00:22:04 +09:00
42 changed files with 1102 additions and 581 deletions

View File

@@ -4,7 +4,7 @@ rvm:
install:
- 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 apt-add-repository -y ppa:fish-shell/release-2
- sudo apt-get update

View File

@@ -1,6 +1,60 @@
CHANGELOG
=========
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
------
- Fixed slow performance of `--with-nth` when used with `--delimiter`
- Regular expression engine of Golang as of now is very slow, so the fixed
version will treat the given delimiter pattern as a plain string instead
of a regular expression unless it contains special characters and is
a valid regular expression.
- Simpler regular expression for delimiter for better performance
0.10.2
------
### Fixes and improvements
- Improvement in perceived response time of queries
- Eager, efficient rune array conversion
- Graceful exit when failed to initialize ncurses (invalid $TERM)
- Improved ranking algorithm when `--nth` option is set
- Changed the default command not to fail when there are files whose names
start with dash
0.10.1
------

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.
@@ -124,8 +124,9 @@ such as: `^music .mp3$ sbtrkt !rmx`
| `'wild` | Items that include `wild` | exact-match (quoted) |
| `!'fire` | Items that do not include `fire` | inverse-exact-match |
If you don't need fuzzy matching and do not wish to "quote" every word, start
fzf with `-e` or `--extended-exact` option.
If you don't prefer fuzzy matching and do not wish to "quote" every word,
start fzf with `-e` or `--extended-exact` option. Note that in
`--extended-exact` mode, `'`-prefix "unquotes" the term.
#### Environment variables
@@ -254,6 +255,9 @@ export FZF_COMPLETION_OPTS='+c -x'
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.)
#### `:FZF[!]`
@@ -286,10 +290,8 @@ customization.
#### `fzf#run([options])`
For more advanced uses, you can call `fzf#run()` function which returns the list
of the selected items.
`fzf#run()` may take an options-dictionary:
For more advanced uses, you can use `fzf#run()` function with the following
options.
| Option name | Type | Description |
| -------------------------- | ------------- | ---------------------------------------------------------------- |
@@ -305,65 +307,7 @@ of the selected items.
| `launcher` | string | External terminal emulator to start fzf with (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
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
Examples can be found on [the wiki
page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
Tips
@@ -425,14 +369,6 @@ of fzf to a temporary file.
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
-------

View File

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

44
install
View File

@@ -1,6 +1,7 @@
#!/usr/bin/env bash
version=0.10.1
[[ "$@" =~ --pre ]] && version=0.10.7 pre=1 ||
version=0.10.7 pre=0
cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd)
@@ -20,16 +21,21 @@ ask() {
check_binary() {
echo -n " - Checking fzf executable ... "
local output=$("$fzf_base"/bin/fzf --version 2>&1)
if [ "$version" = "$output" ]; then
local output
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"
binary_error=""
else
echo "$output != $version"
rm -f "$fzf_base"/bin/fzf
binary_error="Invalid binary"
return 1
return 0
fi
rm -f "$fzf_base"/bin/fzf
return 1
}
symlink() {
@@ -45,11 +51,13 @@ symlink() {
download() {
echo "Downloading bin/fzf ..."
if [[ ! $1 =~ dev && -x "$fzf_base"/bin/fzf ]]; then
echo " - Already exists"
check_binary && return
elif [ -x "$fzf_base"/bin/$1 ]; then
symlink $1 && check_binary && return
if [ $pre = 0 ]; then
if [ -x "$fzf_base"/bin/fzf ]; then
echo " - Already exists"
check_binary && return
elif [ -x "$fzf_base"/bin/$1 ]; then
symlink $1 && check_binary && return
fi
fi
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
if [ $? -ne 0 ]; then
@@ -93,6 +101,7 @@ if [ -n "$binary_error" ]; then
echo "No prebuilt binary for $archi ... "
else
echo " - $binary_error !!!"
exit 1
fi
echo "Installing legacy Ruby version ..."
@@ -176,7 +185,7 @@ for shell in bash zsh; do
echo -n "Generate ~/.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
fzf_completion="# $fzf_completion"
fi
@@ -189,13 +198,13 @@ for shell in bash zsh; do
cat > $src << EOF
# Setup fzf
# ---------
if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then
if [[ ! "\$PATH" == *$fzf_base/bin* ]]; then
export PATH="\$PATH:$fzf_base/bin"
fi
# 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"
fi
@@ -236,6 +245,7 @@ EOF
fi
append_line() {
set -e
echo "Update $2:"
echo " - $1"
[ -f "$2" ] || touch "$2"
@@ -247,10 +257,12 @@ append_line() {
if [ -n "$line" ]; then
echo " - Already exists: line #$line"
else
echo >> "$2"
echo "$1" >> "$2"
echo " + Added"
fi
echo
set +e
}
echo

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
THE SOFTWARE.
..
.TH fzf 1 "July 2015" "fzf 0.10.1" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Oct 2015" "fzf 0.10.7" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -285,11 +285,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
automatically truncated when the number of the lines exceeds the value.
.TP
.BI "--header-file=" "FILE"
The content of the file will be printed as the sticky header. The lines in the
file are displayed in order from top to bottom regardless of \fB--reverse\fR,
and are not affected by \fB--with-nth\fR. ANSI color codes are processed even
when \fB--ansi\fR is not set.
.BI "--header=" "STR"
The given string will be printed as the sticky header. The lines are displayed
in the given order from top to bottom regardless of \fB--reverse\fR option, and
are not affected by \fB--with-nth\fR. ANSI color codes are processed even when
\fB--ansi\fR is not set.
.TP
.BI "--header-lines=" "N"
The first N lines of the input are treated as the sticky header. When
@@ -374,29 +374,40 @@ mode". In this mode, you can specify multiple patterns delimited by spaces,
such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR
.SS Exact-match (quoted)
A term that is prefixed by a single-quote character (') is interpreted as an
"exact-match" (or "non-fuzzy") term. fzf will search for the exact occurrences
of the string.
A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as
an "exact-match" (or "non-fuzzy") term. fzf will search for the exact
occurrences of the string.
.SS Anchored-match
A term can be prefixed by ^, or suffixed by $ to become an anchored-match term.
Then fzf will search for the items that start with or end with the given
string. An anchored-match term is also an exact-match term.
A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an
anchored-match term. Then fzf will search for the items that start with or end
with the given string. An anchored-match term is also an exact-match term.
.SS Negation
If a term is prefixed by !, fzf will exclude the items that satisfy the term
from the result.
If a term is prefixed by \fB!\fR, fzf will exclude the items that satisfy the
term from the result.
.SS Extended-exact mode
If you don't need fuzzy matching at all and do not wish to "quote" (prefixing
with ') every word, start fzf with \fB-e\fR or \fB--extended-exact\fR option
(instead of \fB-x\fR or \fB--extended\fR).
If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with
\fB'\fR) every word, start fzf with \fB-e\fR or \fB--extended-exact\fR option
(instead of \fB-x\fR or \fB--extended\fR). Note that in \fB--extended-exact\fR
mode, \fB'\fR-prefix "unquotes" the term.
.SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR)
.SH SEE ALSO
.B Project homepage:
.RS
.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
MIT

View File

@@ -40,9 +40,7 @@ function! s:fzf_exec()
\ input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
redraw
echo
echohl WarningMsg
echo 'Downloading fzf binary. Please wait ...'
echohl None
call s:warn('Downloading fzf binary. Please wait ...')
let s:installed = 1
call system(s:install.' --bin')
return s:fzf_exec()
@@ -54,13 +52,17 @@ function! s:fzf_exec()
return s:exec
endfunction
function! s:tmux_not_zoomed()
return system('tmux list-panes -F "#F"') !~# 'Z'
endfunction
function! s:tmux_enabled()
if has('gui_running')
return 0
endif
if exists('s:tmux')
return s:tmux
return s:tmux && s:tmux_not_zoomed()
endif
let s:tmux = 0
@@ -68,7 +70,7 @@ function! s:tmux_enabled()
let output = system('tmux -V')
let s:tmux = !v:shell_error && output >= 'tmux 1.7'
endif
return s:tmux
return s:tmux && s:tmux_not_zoomed()
endfunction
function! s:shellesc(arg)
@@ -76,7 +78,7 @@ function! s:shellesc(arg)
endfunction
function! s:escape(path)
return escape(a:path, ' %#\')
return escape(a:path, ' %#''"\')
endfunction
" Upgrade legacy options
@@ -94,14 +96,24 @@ function! s:upgrade(dict)
return copy
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
try
let oshell = &shell
set shell=sh
if has('nvim') && bufexists('term://*:FZF')
echohl WarningMsg
echomsg 'FZF is already running!'
echohl None
call s:warn('FZF is already running!')
return []
endif
let dict = exists('a:1') ? s:upgrade(a:1) : {}
@@ -160,7 +172,13 @@ function! s:fzf_tmux(dict)
let size = ''
for o in ['up', 'down', 'left', 'right']
if s:present(a:dict, o)
let size = '-'.o[0].(a:dict[o] == 1 ? '' : a:dict[o])
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
endif
endfor
@@ -195,7 +213,7 @@ endfunction
function! s:xterm_launcher()
let fmt = 'xterm -T "[fzf]" -bg "\%s" -fg "\%s" -geometry %dx%d+%d+%d -e bash -ic %%s'
if has('gui_macvim')
let fmt .= '; osascript -e "tell application \"MacVim\" to activate"'
let fmt .= '&& osascript -e "tell application \"MacVim\" to activate"'
endif
return printf(fmt,
\ synIDattr(hlID("Normal"), "bg"), synIDattr(hlID("Normal"), "fg"),
@@ -204,28 +222,33 @@ endfunction
unlet! s: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)
call s:pushd(a:dict)
silent! !clear 2> /dev/null
let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#')
if has('gui_running')
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 command = printf(fmt, "'".substitute(a:command, "'", "'\"'\"'", 'g')."'")
let command = printf(fmt, "'".substitute(escaped, "'", "'\"'\"'", 'g')."'")
else
let command = a:command
let command = escaped
endif
execute 'silent !'.command
redraw!
if v:shell_error
" 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
return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : []
endfunction
function! s:execute_tmux(dict, command, temps)
@@ -237,15 +260,24 @@ function! s:execute_tmux(dict, command, temps)
call system(command)
redraw!
return s:callback(a:dict, a:temps)
return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : []
endfunction
function! s:calc_size(max, val)
function! s:calc_size(max, val, dict)
if a:val =~ '%$'
return a:max * str2nr(a:val[:-2]) / 100
let size = a:max * str2nr(a:val[:-2]) / 100
else
return min([a:max, a:val])
let size = min([a:max, str2nr(a:val)])
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
function! s:getpos()
@@ -264,7 +296,11 @@ function! s:split(dict)
let val = get(a:dict, dir, '')
if !empty(val)
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 resz sz
return
@@ -285,6 +321,7 @@ function! s:execute_term(dict, command, temps)
call s:pushd(a:dict)
let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF' }
let s:command = a:command
function! fzf.on_exit(id, code)
let pos = s:getpos()
let inplace = pos == s:ppos " {'window': 'enew'}
@@ -298,9 +335,13 @@ function! s:execute_term(dict, command, temps)
wincmd p
endif
endif
if !s:exit_handler(a:code, s:command, 1)
return
endif
call s:pushd(self.dict)
try
redraw!
call s:callback(self.dict, self.temps)
if inplace && bufnr('') == self.buf
@@ -354,7 +395,7 @@ endfunction
let s:default_action = {
\ 'ctrl-m': 'e',
\ 'ctrl-t': 'tabedit',
\ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' }
@@ -364,9 +405,22 @@ function! s:cmd_callback(lines) abort
endif
let key = remove(a:lines, 0)
let cmd = get(s:action, key, 'e')
for item in a:lines
execute cmd s:escape(item)
endfor
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
let autochdir = &autochdir
set noautochdir
for item in a:lines
execute cmd s:escape(item)
endfor
finally
let &autochdir = autochdir
silent! autocmd! fzf_swap
endtry
endfunction
function! s:cmd(bang, ...) abort
@@ -374,7 +428,7 @@ function! s:cmd(bang, ...) abort
let args = extend(['--expect='.join(keys(s:action), ',')], a:000)
let opts = {}
if len(args) > 0 && isdirectory(expand(args[-1]))
let opts.dir = remove(args, -1)
let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g')
endif
if !a:bang
let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height))

View File

@@ -49,7 +49,7 @@ _fzf_opts_completion() {
--cycle
--history
--history-size
--header-file
--header
--header-lines
--margin"
@@ -62,7 +62,7 @@ _fzf_opts_completion() {
COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) )
return 0
;;
--history|--header-file)
--history)
COMPREPLY=()
return 0
;;
@@ -94,7 +94,7 @@ _fzf_handle_dynamic_completion() {
fi
}
_fzf_path_completion() {
__fzf_generic_path_completion() {
local cur base dir leftover matches trigger cmd fzf
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g')
@@ -135,20 +135,29 @@ _fzf_path_completion() {
fi
}
_fzf_list_completion() {
local cur selected trigger cmd src fzf
_fzf_feed_fifo() (
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"
read -r src
cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g')
trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ ${cur} == *"$trigger" ]]; then
cur=${cur:0:${#cur}-${#trigger}}
_fzf_feed_fifo "$fifo"
tput sc
selected=$(eval "$src | $fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ')
selected=${selected% }
selected=$(eval "cat '$fifo' | $fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
tput rc
rm -f "$fifo"
if [ -n "$selected" ]; then
COMPREPLY=("$selected")
@@ -160,25 +169,25 @@ _fzf_list_completion() {
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" \
"-m" "" "$@"
}
_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" \
"-m" "" "$@"
}
_fzf_dir_completion() {
_fzf_path_completion \
__fzf_generic_path_completion \
"-name .git -prune -o -name .svn -prune -o -type d -print" \
"" "/" "$@"
}
_fzf_kill_completion() {
_fzf_complete_kill() {
[ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1
local selected fzf
@@ -193,28 +202,37 @@ _fzf_kill_completion() {
fi
}
_fzf_telnet_completion() {
_fzf_list_completion '+m' "$@" << "EOF"
\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u
EOF
_fzf_complete_telnet() {
_fzf_complete '+m' "$@" < <(
\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' |
awk '{if (length($2) > 0) {print $2}}' | sort -u
)
}
_fzf_ssh_completion() {
_fzf_list_completion '+m' "$@" << "EOF"
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
EOF
_fzf_complete_ssh() {
_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
)
}
_fzf_env_var_completion() {
_fzf_list_completion '-m' "$@" << "EOF"
declare -xp | sed 's/=.*//' | sed 's/.* //'
EOF
_fzf_complete_unset() {
_fzf_complete '-m' "$@" < <(
declare -xp | sed 's/=.*//' | sed 's/.* //'
)
}
_fzf_alias_completion() {
_fzf_list_completion '-m' "$@" << "EOF"
alias | sed 's/=.*//' | sed 's/.* //'
EOF
_fzf_complete_export() {
_fzf_complete '-m' "$@" < <(
declare -xp | sed 's/=.*//' | sed 's/.* //'
)
}
_fzf_complete_unalias() {
_fzf_complete '-m' "$@" < <(
alias | sed 's/=.*//' | sed 's/.* //'
)
}
# fzf options
@@ -257,19 +275,19 @@ done
# Anything
for cmd in $a_cmds; do
complete -F _fzf_all_completion -o default -o bashdefault $cmd
complete -F _fzf_path_completion -o default -o bashdefault $cmd
done
# 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
complete -F _fzf_ssh_completion -o default -o bashdefault ssh
complete -F _fzf_telnet_completion -o default -o bashdefault telnet
complete -F _fzf_complete_ssh -o default -o bashdefault ssh
complete -F _fzf_complete_telnet -o default -o bashdefault telnet
# Environment variables / Aliases
complete -F _fzf_env_var_completion -o default -o bashdefault unset
complete -F _fzf_env_var_completion -o default -o bashdefault export
complete -F _fzf_alias_completion -o default -o bashdefault unalias
complete -F _fzf_complete_unset -o default -o bashdefault unset
complete -F _fzf_complete_export -o default -o bashdefault export
complete -F _fzf_complete_unalias -o default -o bashdefault unalias
unset cmd d_cmds f_cmds a_cmds x_cmds

View File

@@ -10,8 +10,9 @@
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $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
# (Q) flag removes a quoting level: "foo\ bar" => "foo bar"
base=${(Q)1}
lbuf=$2
find_opts=$3
@@ -47,59 +48,79 @@ _fzf_path_completion() {
[ -n "$nnm" ] && unsetopt nonomatch
}
_fzf_all_completion() {
_fzf_path_completion "$1" "$2" \
_fzf_path_completion() {
__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" \
"-m" "" " "
}
_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" \
"" "/" ""
}
_fzf_list_completion() {
local prefix lbuf fzf_opts src fzf matches
prefix=$1
_fzf_feed_fifo() (
rm -f "$fifo"
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
fzf_opts=$3
read -r src
[ ${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
LBUFFER="$lbuf$matches "
LBUFFER="$lbuf$matches"
fi
zle redisplay
rm -f "$fifo"
}
_fzf_telnet_completion() {
_fzf_list_completion "$1" "$2" '+m' << "EOF"
\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u
EOF
_fzf_complete_telnet() {
_fzf_complete '+m' "$@" < <(
\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' |
awk '{if (length($2) > 0) {print $2}}' | sort -u
)
}
_fzf_ssh_completion() {
_fzf_list_completion "$1" "$2" '+m' << "EOF"
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
EOF
_fzf_complete_ssh() {
_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
)
}
_fzf_env_var_completion() {
_fzf_list_completion "$1" "$2" '+m' << "EOF"
declare -xp | sed 's/=.*//' | sed 's/.* //'
EOF
_fzf_complete_export() {
_fzf_complete '-m' "$@" < <(
declare -xp | sed 's/=.*//' | sed 's/.* //'
)
}
_fzf_alias_completion() {
_fzf_list_completion "$1" "$2" '+m' << "EOF"
alias | sed 's/=.*//'
EOF
_fzf_complete_unset() {
_fzf_complete '-m' "$@" < <(
declare -xp | sed 's/=.*//' | sed 's/.* //'
)
}
_fzf_complete_unalias() {
_fzf_complete '+m' "$@" < <(
alias | sed 's/=.*//'
)
}
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/Doc/Release/Expansion.html#Parameter-Expansion-Flags
@@ -131,23 +152,18 @@ fzf-completion() {
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
[ -z "${tokens[-1]}" ] && lbuf=$LBUFFER || lbuf=${LBUFFER:0:-${#tokens[-1]}}
if [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
_fzf_dir_completion "$prefix" $lbuf
elif [ $cmd = telnet ]; then
_fzf_telnet_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
if eval "type _fzf_complete_${cmd} > /dev/null"; then
eval "prefix=\"$prefix\" _fzf_complete_${cmd} \"$lbuf\""
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
_fzf_dir_completion "$prefix" "$lbuf"
else
_fzf_all_completion "$prefix" $lbuf
_fzf_path_completion "$prefix" "$lbuf"
fi
# Fall back to default completion
else
eval "zle ${fzf_default_completion:-expand-or-complete}"
fi
[ -n "$sws" ] && setopt shwordsplit
}
[ -z "$fzf_default_completion" ] &&

View File

@@ -19,7 +19,7 @@ function fzf_key_bindings
-o -type f -print \
-o -type d -print \
-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)
commandline -f repaint
rm -f $TMPDIR/fzf.result

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
FROM centos:centos7
FROM centos:centos6
MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# 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
RUN cd / && curl \
@@ -13,6 +15,9 @@ ENV GOPATH /go
ENV GOROOT /go1.4
ENV PATH /go1.4/bin:$PATH
# For i386 build
RUN cd $GOROOT/src && GOARCH=386 ./make.bash
# Volume
VOLUME /go

View File

@@ -3,7 +3,7 @@ MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
# apt-get
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
RUN cd / && curl \

View File

@@ -25,10 +25,9 @@ RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64
all: release
release: build
cd fzf && \
cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) && \
cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \
rm $(RELEASE32) $(RELEASE64)
-cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32)
cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \
rm -f $(RELEASE32) $(RELEASE64)
build: test fzf/$(BINARY32) fzf/$(BINARY64)
@@ -42,13 +41,13 @@ uninstall:
rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64)
clean:
cd fzf && rm -f $(BINARY32) $(BINARY64) $(RELEASE32).tgz $(RELEASE64).tgz
cd fzf && rm -f fzf-*
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)
cd fzf && go build -o $(BINARY64)
cd fzf && go build -a -tags "$(TAGS)" -o $(BINARY64)
$(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR)
cp -f fzf/$(BINARY64) $(BINDIR)
@@ -57,18 +56,30 @@ $(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR)
$(BINDIR):
mkdir -p $@
# Linux distribution to build fzf on
DISTRO := arch
docker-arch:
docker build -t junegunn/arch-sandbox - < Dockerfile.arch
docker:
docker build -t junegunn/$(DISTRO)-sandbox - < Dockerfile.$(DISTRO)
docker-ubuntu:
docker build -t junegunn/ubuntu-sandbox - < Dockerfile.ubuntu
linux: docker
docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \
/bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make'
docker-centos:
docker build -t junegunn/centos-sandbox - < Dockerfile.centos
$(DISTRO): docker
docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \
arch: docker-arch
docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \
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.
*/
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
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 {
return 0, 0
}
@@ -34,7 +41,11 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
sidx := -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
// whole string
if !caseSensitive {
@@ -47,11 +58,12 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
char = unicode.To(unicode.LowerCase, char)
}
}
if char == pattern[pidx] {
pchar := runeAt(pattern, pidx, lenPattern, forward)
if char == pchar {
if sidx < 0 {
sidx = index
}
if pidx++; pidx == len(pattern) {
if pidx++; pidx == lenPattern {
eidx = index + 1
break
}
@@ -61,7 +73,7 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
if sidx >= 0 && eidx >= 0 {
pidx--
for index := eidx - 1; index >= sidx; index-- {
char := (*runes)[index]
char := runeAt(runes, index, lenRunes, forward)
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
@@ -69,14 +81,19 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
char = unicode.To(unicode.LowerCase, char)
}
}
if char == pattern[pidx] {
pchar := runeAt(pattern, pidx, lenPattern, forward)
if char == pchar {
if pidx--; pidx < 0 {
sidx = index
break
}
}
}
return sidx, eidx
if forward {
return sidx, eidx
}
return lenRunes - eidx, lenRunes - sidx
}
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:
// 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 {
return 0, 0
}
numRunes := len(*runes)
plen := len(pattern)
if numRunes < plen {
lenRunes := len(runes)
lenPattern := len(pattern)
if lenRunes < lenPattern {
return -1, -1
}
pidx := 0
for index := 0; index < numRunes; index++ {
char := (*runes)[index]
for index := 0; index < lenRunes; index++ {
char := runeAt(runes, index, lenRunes, forward)
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
@@ -109,10 +127,14 @@ func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, in
char = unicode.To(unicode.LowerCase, char)
}
}
if pattern[pidx] == char {
pchar := runeAt(pattern, pidx, lenPattern, forward)
if pchar == char {
pidx++
if pidx == plen {
return index - plen + 1, index + 1
if pidx == lenPattern {
if forward {
return index - lenPattern + 1, index + 1
}
return lenRunes - (index + 1), lenRunes - (index - lenPattern + 1)
}
} else {
index -= pidx
@@ -123,13 +145,13 @@ func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, in
}
// PrefixMatch performs prefix-match
func PrefixMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
if len(*runes) < len(pattern) {
func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
if len(runes) < len(pattern) {
return -1, -1
}
for index, r := range pattern {
char := (*runes)[index]
char := runes[index]
if !caseSensitive {
char = unicode.ToLower(char)
}
@@ -141,7 +163,7 @@ func PrefixMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
}
// 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)
trimmedLen := len(runes)
diff := trimmedLen - len(pattern)
@@ -161,11 +183,12 @@ func SuffixMatch(caseSensitive bool, input *[]rune, pattern []rune) (int, int) {
return trimmedLen - len(pattern), trimmedLen
}
func EqualMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
if len(*runes) != len(pattern) {
// EqualMatch performs equal-match
func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
if len(runes) != len(pattern) {
return -1, -1
}
runesStr := string(*runes)
runesStr := string(runes)
if !caseSensitive {
runesStr = strings.ToLower(runesStr)
}

View File

@@ -5,12 +5,11 @@ import (
"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 {
pattern = strings.ToLower(pattern)
}
runes := []rune(input)
s, e := fun(caseSensitive, &runes, []rune(pattern))
s, e := fun(caseSensitive, forward, []rune(input), []rune(pattern))
if s != sidx {
t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern)
}
@@ -20,33 +19,51 @@ func assertMatch(t *testing.T, fun func(bool, *[]rune, []rune) (int, int), caseS
}
func TestFuzzyMatch(t *testing.T) {
assertMatch(t, FuzzyMatch, false, "fooBarbaz", "oBZ", 2, 9)
assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBZ", -1, -1)
assertMatch(t, FuzzyMatch, true, "fooBarbaz", "oBz", 2, 9)
assertMatch(t, FuzzyMatch, true, "fooBarbaz", "fooBarbazz", -1, -1)
assertMatch(t, FuzzyMatch, false, true, "fooBarbaz", "oBZ", 2, 9)
assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBZ", -1, -1)
assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBz", 2, 9)
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) {
assertMatch(t, ExactMatchNaive, false, "fooBarbaz", "oBA", 2, 5)
assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "oBA", -1, -1)
assertMatch(t, ExactMatchNaive, true, "fooBarbaz", "fooBarbazz", -1, -1)
for _, dir := range []bool{true, false} {
assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5)
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) {
assertMatch(t, PrefixMatch, false, "fooBarbaz", "Foo", 0, 3)
assertMatch(t, PrefixMatch, true, "fooBarbaz", "Foo", -1, -1)
assertMatch(t, PrefixMatch, false, "fooBarbaz", "baz", -1, -1)
for _, dir := range []bool{true, false} {
assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3)
assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1)
assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "baz", -1, -1)
}
}
func TestSuffixMatch(t *testing.T) {
assertMatch(t, SuffixMatch, false, "fooBarbaz", "Foo", -1, -1)
assertMatch(t, SuffixMatch, false, "fooBarbaz", "baz", 6, 9)
assertMatch(t, SuffixMatch, true, "fooBarbaz", "Baz", -1, -1)
for _, dir := range []bool{true, false} {
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -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) {
assertMatch(t, FuzzyMatch, true, "foobar", "", 0, 0)
assertMatch(t, ExactMatchNaive, true, "foobar", "", 0, 0)
assertMatch(t, PrefixMatch, true, "foobar", "", 0, 0)
assertMatch(t, SuffixMatch, true, "foobar", "", 6, 6)
for _, dir := range []bool{true, false} {
assertMatch(t, FuzzyMatch, true, dir, "foobar", "", 0, 0)
assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0)
assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0)
assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6)
}
}

View File

@@ -36,7 +36,7 @@ func init() {
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]")
}
func extractColor(str *string, state *ansiState) (*string, []ansiOffset, *ansiState) {
func extractColor(str string, state *ansiState) (string, []ansiOffset, *ansiState) {
var offsets []ansiOffset
var output bytes.Buffer
@@ -45,9 +45,9 @@ func extractColor(str *string, state *ansiState) (*string, []ansiOffset, *ansiSt
}
idx := 0
for _, offset := range ansiRegex.FindAllStringIndex(*str, -1) {
output.WriteString((*str)[idx:offset[0]])
newState := interpretCode((*str)[offset[0]:offset[1]], state)
for _, offset := range ansiRegex.FindAllStringIndex(str, -1) {
output.WriteString(str[idx:offset[0]])
newState := interpretCode(str[offset[0]:offset[1]], state)
if !newState.equals(state) {
if state != nil {
@@ -69,7 +69,7 @@ func extractColor(str *string, state *ansiState) (*string, []ansiOffset, *ansiSt
idx = offset[1]
}
rest := (*str)[idx:]
rest := str[idx:]
if len(rest) > 0 {
output.WriteString(rest)
if state != nil {
@@ -77,8 +77,7 @@ func extractColor(str *string, state *ansiState) (*string, []ansiOffset, *ansiSt
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
}
}
outputStr := output.String()
return &outputStr, offsets, state
return output.String(), offsets, state
}
func interpretCode(ansiCode string, prevState *ansiState) *ansiState {

View File

@@ -17,9 +17,9 @@ func TestExtractColor(t *testing.T) {
var state *ansiState
clean := "\x1b[0m"
check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) {
output, ansiOffsets, newState := extractColor(&src, state)
output, ansiOffsets, newState := extractColor(src, state)
state = newState
if *output != "hello world" {
if output != "hello world" {
t.Errorf("Invalid output: {}", output)
}
fmt.Println(src, ansiOffsets, clean)

View File

@@ -7,7 +7,7 @@ type Chunk []*Item // >>> []Item
// ItemBuilder is a closure type that builds Item object from a pointer to a
// string and an integer
type ItemBuilder func(*string, int) *Item
type ItemBuilder func([]byte, int) *Item
// ChunkList is a list of Chunks
type ChunkList struct {
@@ -26,7 +26,7 @@ func NewChunkList(trans ItemBuilder) *ChunkList {
trans: trans}
}
func (c *Chunk) push(trans ItemBuilder, data *string, index int) bool {
func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool {
item := trans(data, index)
if item != nil {
*c = append(*c, item)
@@ -53,7 +53,7 @@ func CountItems(cs []*Chunk) int {
}
// Push adds the item to the list
func (cl *ChunkList) Push(data string) bool {
func (cl *ChunkList) Push(data []byte) bool {
cl.mutex.Lock()
defer cl.mutex.Unlock()
@@ -62,7 +62,7 @@ func (cl *ChunkList) Push(data string) bool {
cl.chunks = append(cl.chunks, &newChunk)
}
if cl.lastChunk().push(cl.trans, &data, cl.count) {
if cl.lastChunk().push(cl.trans, data, cl.count) {
cl.count++
return true
}

View File

@@ -6,8 +6,8 @@ import (
)
func TestChunkList(t *testing.T) {
cl := NewChunkList(func(s *string, i int) *Item {
return &Item{text: s, rank: Rank{0, 0, uint32(i * 2)}}
cl := NewChunkList(func(s []byte, i int) *Item {
return &Item{text: []rune(string(s)), rank: Rank{0, 0, uint32(i * 2)}}
})
// Snapshot
@@ -17,8 +17,8 @@ func TestChunkList(t *testing.T) {
}
// Add some data
cl.Push("hello")
cl.Push("world")
cl.Push([]byte("hello"))
cl.Push([]byte("world"))
// Previously created snapshot should remain the same
if len(snapshot) > 0 {
@@ -36,8 +36,8 @@ func TestChunkList(t *testing.T) {
if len(*chunk1) != 2 {
t.Error("Snapshot should contain only two items")
}
if *(*chunk1)[0].text != "hello" || (*chunk1)[0].rank.index != 0 ||
*(*chunk1)[1].text != "world" || (*chunk1)[1].rank.index != 2 {
if string((*chunk1)[0].text) != "hello" || (*chunk1)[0].rank.index != 0 ||
string((*chunk1)[1].text) != "world" || (*chunk1)[1].rank.index != 2 {
t.Error("Invalid data")
}
if chunk1.IsFull() {
@@ -46,7 +46,7 @@ func TestChunkList(t *testing.T) {
// Add more data
for i := 0; i < chunkSize*2; i++ {
cl.Push(fmt.Sprintf("item %d", i))
cl.Push([]byte(fmt.Sprintf("item %d", i)))
}
// Previous snapshot should remain the same
@@ -64,8 +64,8 @@ func TestChunkList(t *testing.T) {
t.Error("Unexpected number of items")
}
cl.Push("hello")
cl.Push("world")
cl.Push([]byte("hello"))
cl.Push([]byte("world"))
lastChunkCount := len(*snapshot[len(snapshot)-1])
if lastChunkCount != 2 {

View File

@@ -8,14 +8,14 @@ import (
const (
// Current version
Version = "0.10.1"
version = "0.10.7"
// Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond
coordinatorDelayStep time.Duration = 10 * time.Millisecond
// Reader
defaultCommand = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null`
defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
// Terminal
initialDelay = 100 * time.Millisecond
@@ -47,3 +47,10 @@ const (
EvtHeader
EvtClose
)
const (
exitOk = 0
exitNoMatch = 1
exitError = 2
exitInterrupt = 130
)

View File

@@ -55,70 +55,76 @@ func Run(opts *Options) {
rankTiebreak = opts.Tiebreak
if opts.Version {
fmt.Println(Version)
os.Exit(0)
fmt.Println(version)
os.Exit(exitOk)
}
// Event channel
eventBox := util.NewEventBox()
// ANSI code processor
ansiProcessor := func(data *string) (*string, []ansiOffset) {
// By default, we do nothing
ansiProcessor := func(data []byte) ([]rune, []ansiOffset) {
return util.BytesToRunes(data), nil
}
ansiProcessorRunes := func(data []rune) ([]rune, []ansiOffset) {
return data, nil
}
if opts.Ansi {
if opts.Theme != nil {
var state *ansiState
ansiProcessor = func(data *string) (*string, []ansiOffset) {
trimmed, offsets, newState := extractColor(data, state)
ansiProcessor = func(data []byte) ([]rune, []ansiOffset) {
trimmed, offsets, newState := extractColor(string(data), state)
state = newState
return trimmed, offsets
return []rune(trimmed), offsets
}
} else {
// When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input
ansiProcessor = func(data *string) (*string, []ansiOffset) {
trimmed, _, _ := extractColor(data, nil)
return trimmed, nil
ansiProcessor = func(data []byte) ([]rune, []ansiOffset) {
trimmed, _, _ := extractColor(string(data), nil)
return []rune(trimmed), nil
}
}
ansiProcessorRunes = func(data []rune) ([]rune, []ansiOffset) {
return ansiProcessor([]byte(string(data)))
}
}
// Chunk list
var chunkList *ChunkList
header := make([]string, 0, opts.HeaderLines)
if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(data *string, index int) *Item {
chunkList = NewChunkList(func(data []byte, index int) *Item {
if len(header) < opts.HeaderLines {
header = append(header, *data)
header = append(header, string(data))
eventBox.Set(EvtHeader, header)
return nil
}
data, colors := ansiProcessor(data)
runes, colors := ansiProcessor(data)
return &Item{
text: data,
text: runes,
index: uint32(index),
colors: colors,
rank: Rank{0, 0, uint32(index)}}
})
} else {
chunkList = NewChunkList(func(data *string, index int) *Item {
tokens := Tokenize(data, opts.Delimiter)
chunkList = NewChunkList(func(data []byte, index int) *Item {
runes := util.BytesToRunes(data)
tokens := Tokenize(runes, opts.Delimiter)
trans := Transform(tokens, opts.WithNth)
if len(header) < opts.HeaderLines {
header = append(header, *joinTokens(trans))
header = append(header, string(joinTokens(trans)))
eventBox.Set(EvtHeader, header)
return nil
}
item := Item{
text: joinTokens(trans),
origText: data,
origText: &runes,
index: uint32(index),
colors: nil,
rank: Rank{0, 0, uint32(index)}}
trimmed, colors := ansiProcessor(item.text)
trimmed, colors := ansiProcessorRunes(item.text)
item.text = trimmed
item.colors = colors
return &item
@@ -128,8 +134,8 @@ func Run(opts *Options) {
// Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
if !streamingFilter {
reader := Reader{func(str string) bool {
return chunkList.Push(str)
reader := Reader{func(data []byte) bool {
return chunkList.Push(data)
}, eventBox, opts.ReadZero}
go reader.ReadSource()
}
@@ -137,7 +143,8 @@ func Run(opts *Options) {
// Matcher
patternBuilder := func(runes []rune) *Pattern {
return BuildPattern(
opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes)
opts.Mode, opts.Case, opts.Tiebreak != byEnd,
opts.Nth, opts.Delimiter, runes)
}
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
@@ -149,12 +156,14 @@ func Run(opts *Options) {
pattern := patternBuilder([]rune(*opts.Filter))
found := false
if streamingFilter {
reader := Reader{
func(str string) bool {
item := chunkList.trans(&str, 0)
func(runes []byte) bool {
item := chunkList.trans(runes, 0)
if item != nil && pattern.MatchItem(item) {
fmt.Println(*item.text)
fmt.Println(string(item.text))
found = true
}
return false
}, eventBox, opts.ReadZero}
@@ -168,10 +177,14 @@ func Run(opts *Options) {
chunks: snapshot,
pattern: pattern})
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
@@ -225,7 +238,7 @@ func Run(opts *Options) {
}
case EvtHeader:
terminal.UpdateHeader(value.([]string), opts.HeaderLines)
terminal.UpdateHeader(value.([]string))
case EvtSearchFin:
switch val := value.(type) {
@@ -244,9 +257,12 @@ func Run(opts *Options) {
fmt.Println()
}
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
terminal.startChan <- true

View File

@@ -3,13 +3,14 @@ package curses
/*
#include <ncurses.h>
#include <locale.h>
#cgo LDFLAGS: -lncurses
#cgo !static LDFLAGS: -lncurses
#cgo static LDFLAGS: -l:libncurses.a -l:libtinfo.a -l:libgpm.a -ldl
*/
import "C"
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
"unicode/utf8"
@@ -258,6 +259,10 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
C.setlocale(C.LC_ALL, C.CString(""))
_screen = C.newterm(nil, C.stderr, C.stdin)
if _screen == nil {
fmt.Println("Invalid $TERM: " + os.Getenv("TERM"))
os.Exit(2)
}
C.set_term(_screen)
if mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil)
@@ -265,14 +270,6 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
C.noecho()
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 {
C.start_color()
initPairs(theme, black)

View File

@@ -7,6 +7,7 @@ import (
"strings"
)
// History struct represents input history
type History struct {
path string
lines []string
@@ -15,6 +16,7 @@ type History struct {
cursor int
}
// NewHistory returns the pointer to a new History struct
func NewHistory(path string, maxSize int) (*History, error) {
fmtError := func(e error) error {
if os.IsPermission(e) {

View File

@@ -6,8 +6,8 @@ import (
"github.com/junegunn/fzf/src/curses"
)
// Offset holds two 32-bit integers denoting the offsets of a matched substring
type Offset [2]int32
// Offset holds three 32-bit integers denoting the offsets of a matched substring
type Offset [3]int32
type colorOffset struct {
offset [2]int32
@@ -17,9 +17,9 @@ type colorOffset struct {
// Item represents each input line
type Item struct {
text *string
origText *string
transformed *[]Token
text []rune
origText *[]rune
transformed []Token
index uint32
offsets []Offset
colors []ansiOffset
@@ -37,16 +37,19 @@ type Rank struct {
var rankTiebreak tiebreak
// Rank calculates rank of the Item
func (i *Item) Rank(cache bool) Rank {
if cache && (i.rank.matchlen > 0 || i.rank.tiebreak > 0) {
return i.rank
func (item *Item) Rank(cache bool) Rank {
if cache && (item.rank.matchlen > 0 || item.rank.tiebreak > 0) {
return item.rank
}
matchlen := 0
prevEnd := 0
lenSum := 0
minBegin := math.MaxUint16
for _, offset := range i.offsets {
for _, offset := range item.offsets {
begin := int(offset[0])
end := int(offset[1])
trimLen := int(offset[2])
lenSum += trimLen
if prevEnd > begin {
begin = prevEnd
}
@@ -63,13 +66,19 @@ func (i *Item) Rank(cache bool) Rank {
var tiebreak uint16
switch rankTiebreak {
case byLength:
tiebreak = uint16(len(*i.text))
// It is guaranteed that .transformed in not null in normal execution
if item.transformed != nil {
// If offsets is empty, lenSum will be 0, but we don't care
tiebreak = uint16(lenSum)
} else {
tiebreak = uint16(len(item.text))
}
case byBegin:
// We can't just look at i.offsets[0][0] because it can be an inverse term
// We can't just look at item.offsets[0][0] because it can be an inverse term
tiebreak = uint16(minBegin)
case byEnd:
if prevEnd > 0 {
tiebreak = uint16(1 + len(*i.text) - prevEnd)
tiebreak = uint16(1 + len(item.text) - prevEnd)
} else {
// Empty offsets due to inverse terms.
tiebreak = 1
@@ -77,31 +86,38 @@ func (i *Item) Rank(cache bool) Rank {
case byIndex:
tiebreak = 1
}
rank := Rank{uint16(matchlen), tiebreak, i.index}
rank := Rank{uint16(matchlen), tiebreak, item.index}
if cache {
i.rank = rank
item.rank = rank
}
return rank
}
// AsString returns the original string
func (i *Item) AsString() string {
return *i.StringPtr()
func (item *Item) AsString(stripAnsi bool) string {
return *item.StringPtr(stripAnsi)
}
// StringPtr returns the pointer to the original string
func (i *Item) StringPtr() *string {
if i.origText != nil {
return i.origText
func (item *Item) StringPtr(stripAnsi bool) *string {
if item.origText != nil {
if stripAnsi {
trimmed, _, _ := extractColor(string(*item.origText), nil)
return &trimmed
}
orig := string(*item.origText)
return &orig
}
return i.text
str := string(item.text)
return &str
}
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {
if len(item.colors) == 0 {
var offsets []colorOffset
for _, off := range item.offsets {
offsets = append(offsets, colorOffset{offset: off, color: color, bold: bold})
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold})
}
return offsets
}
@@ -145,7 +161,7 @@ func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset
if curr != 0 && idx > start {
if curr == -1 {
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 {
ansi := item.colors[curr-1]
fg := ansi.color.fg
@@ -165,7 +181,7 @@ func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset
}
}
offsets = append(offsets, colorOffset{
offset: Offset{int32(start), int32(idx)},
offset: [2]int32{int32(start), int32(idx)},
color: curses.PairFor(fg, bg),
bold: ansi.color.bold || bold})
}

View File

@@ -39,14 +39,14 @@ func TestRankComparison(t *testing.T) {
// Match length, string length, index
func TestItemRank(t *testing.T) {
strs := []string{"foo", "foobar", "bar", "baz"}
item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := Item{text: strs[0], index: 1, offsets: []Offset{}}
rank1 := item1.Rank(true)
if rank1.matchlen != 0 || rank1.tiebreak != 3 || rank1.index != 1 {
t.Error(item1.Rank(true))
}
// Only differ in index
item2 := Item{text: &strs[0], index: 0, offsets: []Offset{}}
item2 := Item{text: strs[0], index: 0, offsets: []Offset{}}
items := []*Item{&item1, &item2}
sort.Sort(ByRelevance(items))
@@ -62,10 +62,10 @@ func TestItemRank(t *testing.T) {
}
// Sort by relevance
item3 := Item{text: &strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item4 := Item{text: &strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
item5 := Item{text: &strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item6 := Item{text: &strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
item3 := Item{text: strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item4 := Item{text: strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
item5 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item6 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6}
sort.Sort(ByRelevance(items))
if items[0] != &item2 || items[1] != &item1 ||

View File

@@ -96,7 +96,7 @@ func (m *Matcher) Loop() {
}
if !cancelled {
if merger.Cacheable() {
if merger.cacheable() {
m.mergerCache[patternString] = merger
}
merger.final = request.final

View File

@@ -82,7 +82,7 @@ func (mg *Merger) Get(idx int) *Item {
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
}
func (mg *Merger) Cacheable() bool {
func (mg *Merger) cacheable() bool {
return mg.count < mergerCacheMax
}

View File

@@ -22,7 +22,7 @@ func randItem() *Item {
offsets[idx] = Offset{sidx, eidx}
}
return &Item{
text: &str,
text: []rune(str),
index: rand.Uint32(),
offsets: offsets}
}

View File

@@ -1,8 +1,6 @@
package fzf
import (
"fmt"
"io/ioutil"
"os"
"regexp"
"strconv"
@@ -46,7 +44,7 @@ const usage = `usage: fzf [options]
--bind=KEYBINDS Custom key bindings. Refer to the man page.
--history=FILE History file
--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
Scripting
@@ -104,7 +102,7 @@ type Options struct {
Case Case
Nth []Range
WithNth []Range
Delimiter *regexp.Regexp
Delimiter Delimiter
Sort int
Tac bool
Tiebreak tiebreak
@@ -149,7 +147,7 @@ func defaultOptions() *Options {
Case: CaseSmart,
Nth: make([]Range, 0),
WithNth: make([]Range, 0),
Delimiter: nil,
Delimiter: Delimiter{},
Sort: 1000,
Tac: false,
Tiebreak: byLength,
@@ -181,14 +179,14 @@ func defaultOptions() *Options {
Version: false}
}
func help(ok int) {
func help(code int) {
os.Stderr.WriteString(usage)
os.Exit(ok)
os.Exit(code)
}
func errorExit(msg string) {
os.Stderr.WriteString(msg + "\n")
os.Exit(1)
os.Exit(exitError)
}
func optString(arg string, prefixes ...string) (bool, string) {
@@ -268,17 +266,23 @@ func splitNth(str string) []Range {
return ranges
}
func delimiterRegexp(str string) *regexp.Regexp {
rx, e := regexp.Compile(str)
if e != nil {
str = regexp.QuoteMeta(str)
func delimiterRegexp(str string) Delimiter {
// Special handling of \t
str = strings.Replace(str, "\\t", "\t", -1)
// 1. Pattern does not contain any special character
if regexp.QuoteMeta(str) == str {
return Delimiter{str: &str}
}
rx, e = regexp.Compile(fmt.Sprintf("(?:.*?%s)|(?:.+?$)", str))
rx, e := regexp.Compile(str)
// 2. Pattern is not a valid regular expression
if e != nil {
errorExit("invalid regular expression: " + e.Error())
return Delimiter{str: &str}
}
return rx
// 3. Pattern as regular expression. Slow.
return Delimiter{regex: rx}
}
func isAlphabet(char uint8) bool {
@@ -513,7 +517,7 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
case "delete-char":
keymap[key] = actDeleteChar
case "delete-char/eof":
keymap[key] = actDeleteCharEof
keymap[key] = actDeleteCharEOF
case "end-of-line":
keymap[key] = actEndOfLine
case "cancel":
@@ -599,12 +603,8 @@ func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
return keymap
}
func readHeaderFile(filename string) []string {
content, err := ioutil.ReadFile(filename)
if err != nil {
errorExit("failed to read header file: " + filename)
}
return strings.Split(strings.TrimSuffix(string(content), "\n"), "\n")
func strLines(str string) []string {
return strings.Split(strings.TrimSuffix(str, "\n"), "\n")
}
func parseMargin(margin string) [4]string {
@@ -677,7 +677,7 @@ func parseOptions(opts *Options, allArgs []string) {
arg := allArgs[i]
switch arg {
case "-h", "--help":
help(0)
help(exitOk)
case "-x", "--extended":
opts.Mode = ModeExtended
case "-e", "--extended-exact":
@@ -788,16 +788,13 @@ func parseOptions(opts *Options, allArgs []string) {
setHistory(nextString(allArgs, &i, "history file path required"))
case "--history-size":
setHistoryMax(nextInt(allArgs, &i, "history max size required"))
case "--no-header-file":
case "--no-header":
opts.Header = []string{}
case "--no-header-lines":
opts.HeaderLines = 0
case "--header-file":
opts.Header = readHeaderFile(
nextString(allArgs, &i, "header file name required"))
opts.HeaderLines = 0
case "--header":
opts.Header = strLines(nextString(allArgs, &i, "header string required"))
case "--header-lines":
opts.Header = []string{}
opts.HeaderLines = atoi(
nextString(allArgs, &i, "number of header lines required"))
case "--no-margin":
@@ -838,11 +835,9 @@ func parseOptions(opts *Options, allArgs []string) {
setHistory(value)
} else if match, value := optString(arg, "--history-size="); match {
setHistoryMax(atoi(value))
} else if match, value := optString(arg, "--header-file="); match {
opts.Header = readHeaderFile(value)
opts.HeaderLines = 0
} else if match, value := optString(arg, "--header="); match {
opts.Header = strLines(value)
} else if match, value := optString(arg, "--header-lines="); match {
opts.Header = []string{}
opts.HeaderLines = atoi(value)
} else if match, value := optString(arg, "--margin="); match {
opts.Margin = parseMargin(value)

View File

@@ -8,11 +8,59 @@ import (
)
func TestDelimiterRegex(t *testing.T) {
rx := delimiterRegexp("*")
tokens := rx.FindAllString("-*--*---**---", -1)
if tokens[0] != "-*" || tokens[1] != "--*" || tokens[2] != "---*" ||
tokens[3] != "*" || tokens[4] != "---" {
t.Errorf("%s %s %d", rx, tokens, len(tokens))
// Valid regex
delim := delimiterRegexp(".")
if delim.regex == nil || delim.str != nil {
t.Error(delim)
}
// Broken regex -> string
delim = delimiterRegexp("[0-9")
if delim.regex != nil || *delim.str != "[0-9" {
t.Error(delim)
}
// Valid regex
delim = delimiterRegexp("[0-9]")
if delim.regex.String() != "[0-9]" || delim.str != nil {
t.Error(delim)
}
// Tab character
delim = delimiterRegexp("\t")
if delim.regex != nil || *delim.str != "\t" {
t.Error(delim)
}
// Tab expression
delim = delimiterRegexp("\\t")
if delim.regex != nil || *delim.str != "\t" {
t.Error(delim)
}
// Tabs -> regex
delim = delimiterRegexp("\t+")
if delim.regex == nil || delim.str != nil {
t.Error(delim)
}
}
func TestDelimiterRegexString(t *testing.T) {
delim := delimiterRegexp("*")
tokens := Tokenize([]rune("-*--*---**---"), delim)
if delim.regex != nil ||
string(tokens[0].text) != "-*" ||
string(tokens[1].text) != "--*" ||
string(tokens[2].text) != "---*" ||
string(tokens[3].text) != "*" ||
string(tokens[4].text) != "---" {
t.Errorf("%s %s %d", delim, tokens, len(tokens))
}
}
func TestDelimiterRegexRegex(t *testing.T) {
delim := delimiterRegexp("--\\*")
tokens := Tokenize([]rune("-*--*---**---"), delim)
if delim.str != nil ||
string(tokens[0].text) != "-*--*" ||
string(tokens[1].text) != "---*" ||
string(tokens[2].text) != "*---" {
t.Errorf("%s %d", tokens, len(tokens))
}
}

View File

@@ -6,6 +6,7 @@ import (
"strings"
"github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/util"
)
// fuzzy
@@ -39,12 +40,13 @@ type term struct {
type Pattern struct {
mode Mode
caseSensitive bool
forward bool
text []rune
terms []term
hasInvTerm bool
delimiter *regexp.Regexp
delimiter Delimiter
nth []Range
procFun map[termType]func(bool, *[]rune, []rune) (int, int)
procFun map[termType]func(bool, bool, []rune, []rune) (int, int)
}
var (
@@ -70,8 +72,8 @@ func clearChunkCache() {
}
// BuildPattern builds Pattern object from the given arguments
func BuildPattern(mode Mode, caseMode Case,
nth []Range, delimiter *regexp.Regexp, runes []rune) *Pattern {
func BuildPattern(mode Mode, caseMode Case, forward bool,
nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string
switch mode {
@@ -109,12 +111,13 @@ func BuildPattern(mode Mode, caseMode Case,
ptr := &Pattern{
mode: mode,
caseSensitive: caseSensitive,
forward: forward,
text: []rune(asString),
terms: terms,
hasInvTerm: hasInvTerm,
nth: nth,
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[termEqual] = algo.EqualMatch
@@ -151,6 +154,9 @@ func parseTerms(mode Mode, caseMode Case, str string) []term {
if mode == ModeExtended {
typ = termExact
text = text[1:]
} else if mode == ModeExtendedExact {
typ = termFuzzy
text = text[1:]
}
} else if strings.HasPrefix(text, "^") {
if strings.HasSuffix(text, "$") {
@@ -246,9 +252,9 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
matches := []*Item{}
if p.mode == ModeFuzzy {
for _, item := range *chunk {
if sidx, eidx := p.fuzzyMatch(item); sidx >= 0 {
if sidx, eidx, tlen := p.fuzzyMatch(item); sidx >= 0 {
matches = append(matches,
dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}}))
dupItem(item, []Offset{Offset{int32(sidx), int32(eidx), int32(tlen)}}))
}
}
} else {
@@ -264,7 +270,7 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
// MatchItem returns true if the Item is a match
func (p *Pattern) MatchItem(item *Item) bool {
if p.mode == ModeFuzzy {
sidx, _ := p.fuzzyMatch(item)
sidx, _, _ := p.fuzzyMatch(item)
return sidx >= 0
}
offsets := p.extendedMatch(item)
@@ -283,9 +289,9 @@ func dupItem(item *Item, offsets []Offset) *Item {
rank: Rank{0, 0, item.index}}
}
func (p *Pattern) fuzzyMatch(item *Item) (int, int) {
func (p *Pattern) fuzzyMatch(item *Item) (int, int, int) {
input := p.prepareInput(item)
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.text)
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text)
}
func (p *Pattern) extendedMatch(item *Item) []Offset {
@@ -293,43 +299,41 @@ func (p *Pattern) extendedMatch(item *Item) []Offset {
offsets := []Offset{}
for _, term := range p.terms {
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 {
break
}
offsets = append(offsets, Offset{int32(sidx), int32(eidx)})
offsets = append(offsets, Offset{int32(sidx), int32(eidx), int32(tlen)})
} else if term.inv {
offsets = append(offsets, Offset{0, 0})
offsets = append(offsets, Offset{0, 0, 0})
}
}
return offsets
}
func (p *Pattern) prepareInput(item *Item) *[]Token {
func (p *Pattern) prepareInput(item *Item) []Token {
if item.transformed != nil {
return item.transformed
}
var ret *[]Token
var ret []Token
if len(p.nth) > 0 {
tokens := Tokenize(item.text, p.delimiter)
ret = Transform(tokens, p.nth)
} else {
runes := []rune(*item.text)
trans := []Token{Token{text: &runes, prefixLength: 0}}
ret = &trans
ret = []Token{Token{text: item.text, prefixLength: 0, trimLength: util.TrimLen(item.text)}}
}
item.transformed = ret
return ret
}
func (p *Pattern) iter(pfun func(bool, *[]rune, []rune) (int, int),
tokens *[]Token, caseSensitive bool, pattern []rune) (int, int) {
for _, part := range *tokens {
func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) (int, int),
tokens []Token, caseSensitive bool, forward bool, pattern []rune) (int, int, int) {
for _, part := range tokens {
prefixLength := part.prefixLength
if sidx, eidx := pfun(caseSensitive, part.text, pattern); sidx >= 0 {
return sidx + prefixLength, eidx + prefixLength
if sidx, eidx := pfun(caseSensitive, forward, part.text, pattern); sidx >= 0 {
return sidx + prefixLength, eidx + prefixLength, part.trimLength
}
}
return -1, -1
return -1, -1, -1 // math.MaxUint16
}

View File

@@ -1,6 +1,7 @@
package fzf
import (
"reflect"
"testing"
"github.com/junegunn/fzf/src/algo"
@@ -36,11 +37,11 @@ func TestParseTermsExtendedExact(t *testing.T) {
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$")
if len(terms) != 8 ||
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[3].typ != termSuffix || terms[3].inv || len(terms[3].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[7].typ != termSuffix || !terms[7].inv || len(terms[7].text) != 3 {
t.Errorf("%s", terms)
@@ -57,10 +58,10 @@ func TestParseTermsEmpty(t *testing.T) {
func TestExact(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pattern := BuildPattern(ModeExtended, CaseSmart,
[]Range{}, nil, []rune("'abc"))
runes := []rune("aabbcc abc")
sidx, eidx := algo.ExactMatchNaive(pattern.caseSensitive, &runes, pattern.terms[0].text)
pattern := BuildPattern(ModeExtended, CaseSmart, true,
[]Range{}, Delimiter{}, []rune("'abc"))
sidx, eidx := algo.ExactMatchNaive(
pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.terms[0].text)
if sidx != 7 || eidx != 10 {
t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
}
@@ -69,11 +70,11 @@ func TestExact(t *testing.T) {
func TestEqual(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("^AbC$"))
pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int, eidxExpected int) {
runes := []rune(str)
sidx, eidx := algo.EqualMatch(pattern.caseSensitive, &runes, pattern.terms[0].text)
sidx, eidx := algo.EqualMatch(
pattern.caseSensitive, pattern.forward, []rune(str), pattern.terms[0].text)
if sidx != sidxExpected || eidx != eidxExpected {
t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
}
@@ -85,17 +86,17 @@ func TestEqual(t *testing.T) {
func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pat1 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, nil, []rune("abc"))
pat1 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat2 := BuildPattern(ModeFuzzy, CaseSmart, []Range{}, nil, []rune("Abc"))
pat2 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache()
pat3 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, nil, []rune("abc"))
pat3 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat4 := BuildPattern(ModeFuzzy, CaseIgnore, []Range{}, nil, []rune("Abc"))
pat4 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache()
pat5 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, nil, []rune("abc"))
pat5 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat6 := BuildPattern(ModeFuzzy, CaseRespect, []Range{}, nil, []rune("Abc"))
pat6 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc"))
if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@@ -108,25 +109,23 @@ func TestCaseSensitivity(t *testing.T) {
}
func TestOrigTextAndTransformed(t *testing.T) {
strptr := func(str string) *string {
return &str
}
pattern := BuildPattern(ModeExtended, CaseSmart, []Range{}, nil, []rune("jg"))
tokens := Tokenize(strptr("junegunn"), nil)
pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize([]rune("junegunn"), Delimiter{})
trans := Transform(tokens, []Range{Range{1, 1}})
origRunes := []rune("junegunn.choi")
for _, mode := range []Mode{ModeFuzzy, ModeExtended} {
chunk := Chunk{
&Item{
text: strptr("junegunn"),
origText: strptr("junegunn.choi"),
text: []rune("junegunn"),
origText: &origRunes,
transformed: trans},
}
pattern.mode = mode
matches := pattern.matchChunk(&chunk)
if *matches[0].text != "junegunn" || *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].transformed != trans {
!reflect.DeepEqual(matches[0].transformed, trans) {
t.Error("Invalid match result", matches)
}
}

View File

@@ -11,7 +11,7 @@ import (
// Reader reads from command or standard input
type Reader struct {
pusher func(string) bool
pusher func([]byte) bool
eventBox *util.EventBox
delimNil bool
}
@@ -37,13 +37,14 @@ func (r *Reader) feed(src io.Reader) {
}
reader := bufio.NewReader(src)
for {
line, err := reader.ReadString(delim)
if line != "" {
// "ReadString returns err != nil if and only if the returned data does not end in delim."
// ReadBytes returns err != nil if and only if the returned data does not
// end in delim.
bytea, err := reader.ReadBytes(delim)
if len(bytea) > 0 {
if err == nil {
line = line[:len(line)-1]
bytea = bytea[:len(bytea)-1]
}
if r.pusher(line) {
if r.pusher(bytea) {
r.eventBox.Set(EvtReadNew, nil)
}
}

View File

@@ -10,7 +10,7 @@ func TestReadFromCommand(t *testing.T) {
strs := []string{}
eb := util.NewEventBox()
reader := Reader{
pusher: func(s string) bool { strs = append(strs, s); return true },
pusher: func(s []byte) bool { strs = append(strs, string(s)); return true },
eventBox: eb}
// Check EventBox

View File

@@ -42,6 +42,8 @@ type Terminal struct {
history *History
cycle bool
header []string
header0 []string
ansi bool
margin [4]string
marginInt [4]int
count int
@@ -106,7 +108,7 @@ const (
actCancel
actClearScreen
actDeleteChar
actDeleteCharEof
actDeleteCharEOF
actEndOfLine
actForwardChar
actForwardWord
@@ -141,7 +143,7 @@ func defaultKeymap() map[int]actionType {
keymap[C.CtrlG] = actAbort
keymap[C.CtrlQ] = actAbort
keymap[C.ESC] = actAbort
keymap[C.CtrlD] = actDeleteCharEof
keymap[C.CtrlD] = actDeleteCharEOF
keymap[C.CtrlE] = actEndOfLine
keymap[C.CtrlF] = actForwardChar
keymap[C.CtrlH] = actBackwardDeleteChar
@@ -184,6 +186,12 @@ func defaultKeymap() map[int]actionType {
// NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := []rune(opts.Query)
var header []string
if opts.Reverse {
header = opts.Header
} else {
header = reverseStringArray(opts.Header)
}
return &Terminal{
inlineInfo: opts.InlineInfo,
prompt: opts.Prompt,
@@ -206,7 +214,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
margin: opts.Margin,
marginInt: [4]int{0, 0, 0, 0},
cycle: opts.Cycle,
header: opts.Header,
header: header,
header0: header,
ansi: opts.Ansi,
reading: true,
merger: EmptyMerger,
selected: make(map[uint32]selectedItem),
@@ -239,18 +249,19 @@ func (t *Terminal) UpdateCount(cnt int, final bool) {
}
}
// UpdateHeader updates the header
func (t *Terminal) UpdateHeader(header []string, lines int) {
t.mutex.Lock()
t.header = make([]string, lines)
copy(t.header, header)
if !t.reverse {
reversed := make([]string, lines)
for idx, str := range t.header {
reversed[lines-idx-1] = str
}
t.header = reversed
func reverseStringArray(input []string) []string {
size := len(input)
reversed := make([]string, size)
for idx, str := range input {
reversed[size-idx-1] = str
}
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.reqBox.Set(reqHeader, nil)
}
@@ -278,17 +289,19 @@ func (t *Terminal) UpdateList(merger *Merger) {
t.reqBox.Set(reqList, nil)
}
func (t *Terminal) output() {
func (t *Terminal) output() bool {
if t.printQuery {
fmt.Println(string(t.input))
}
if len(t.expect) > 0 {
fmt.Println(t.pressed)
}
if len(t.selected) == 0 {
found := len(t.selected) > 0
if !found {
cnt := t.merger.Length()
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 {
sels := make([]selectedItem, 0, len(t.selected))
@@ -300,6 +313,7 @@ func (t *Terminal) output() {
fmt.Println(*sel.text)
}
}
return found
}
func runeWidth(r rune, prefixWidth int) int {
@@ -431,20 +445,17 @@ func (t *Terminal) printHeader() {
max := t.maxHeight()
var state *ansiState
for idx, lineStr := range t.header {
if !t.reverse {
idx = len(t.header) - idx - 1
}
line := idx + 2
if t.inlineInfo {
line -= 1
line--
}
if line >= max {
continue
}
trimmed, colors, newState := extractColor(&lineStr, state)
trimmed, colors, newState := extractColor(lineStr, state)
state = newState
item := &Item{
text: trimmed,
text: []rune(trimmed),
index: 0,
colors: colors,
rank: Rank{0, 0, 0}}
@@ -462,7 +473,7 @@ func (t *Terminal) printList() {
for i := 0; i < maxy; i++ {
line := i + 2 + len(t.header)
if t.inlineInfo {
line -= 1
line--
}
t.move(line, 0, true)
if i < count {
@@ -537,7 +548,8 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
}
// Overflow
text := []rune(*item.text)
text := make([]rune, len(item.text))
copy(text, item.text)
offsets := item.colorOffsets(col2, bold, current)
maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3]
fullWidth := displayWidth(text)
@@ -715,6 +727,13 @@ func (t *Terminal) Loop() {
t.reqBox.Set(reqRefresh, nil)
}()
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() {
@@ -740,7 +759,7 @@ func (t *Terminal) Loop() {
}
exit := func(code int) {
if code == 0 && t.history != nil {
if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input))
}
os.Exit(code)
@@ -773,11 +792,13 @@ func (t *Terminal) Loop() {
t.printAll()
case reqClose:
C.Close()
t.output()
exit(0)
if t.output() {
exit(exitOk)
}
exit(exitNoMatch)
case reqQuit:
C.Close()
exit(1)
exit(exitInterrupt)
}
}
t.placeCursor()
@@ -804,7 +825,7 @@ func (t *Terminal) Loop() {
}
selectItem := func(item *Item) bool {
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 false
@@ -842,7 +863,7 @@ func (t *Terminal) Loop() {
case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy)
executeCommand(t.execmap[mapkey], item.AsString())
executeCommand(t.execmap[mapkey], item.AsString(t.ansi))
}
case actInvalid:
t.mutex.Unlock()
@@ -862,7 +883,7 @@ func (t *Terminal) Loop() {
req(reqQuit)
case actDeleteChar:
t.delChar()
case actDeleteCharEof:
case actDeleteCharEOF:
if !t.delChar() && t.cx == 0 {
req(reqQuit)
}
@@ -1013,7 +1034,7 @@ func (t *Terminal) Loop() {
}
min := 2 + len(t.header)
if t.inlineInfo {
min -= 1
min--
}
if me.Double {
// Double-click
@@ -1099,7 +1120,7 @@ func (t *Terminal) vset(o int) bool {
func (t *Terminal) maxItems() int {
max := t.maxHeight() - 2 - len(t.header)
if t.inlineInfo {
max += 1
max++
}
return util.Max(max, 0)
}

View File

@@ -18,8 +18,15 @@ type Range struct {
// Token contains the tokenized part of the strings and its prefix length
type Token struct {
text *[]rune
text []rune
prefixLength int
trimLength int
}
// Delimiter for tokenizing the input
type Delimiter struct {
regex *regexp.Regexp
str *string
}
func newRange(begin int, end int) Range {
@@ -68,16 +75,15 @@ func ParseRange(str *string) (Range, bool) {
return newRange(n, n), true
}
func withPrefixLengths(tokens []string, begin int) []Token {
func withPrefixLengths(tokens [][]rune, begin int) []Token {
ret := make([]Token, len(tokens))
prefixLength := begin
for idx, token := range tokens {
// Need to define a new local variable instead of the reused token to take
// the pointer to it
runes := []rune(token)
ret[idx] = Token{text: &runes, prefixLength: prefixLength}
prefixLength += len([]rune(token))
ret[idx] = Token{token, prefixLength, util.TrimLen(token)}
prefixLength += len(token)
}
return ret
}
@@ -88,13 +94,13 @@ const (
awkWhite
)
func awkTokenizer(input *string) ([]string, int) {
func awkTokenizer(input []rune) ([][]rune, int) {
// 9, 32
ret := []string{}
ret := [][]rune{}
str := []rune{}
prefixLength := 0
state := awkNil
for _, r := range []rune(*input) {
for _, r := range input {
white := r == 9 || r == 32
switch state {
case awkNil:
@@ -113,47 +119,69 @@ func awkTokenizer(input *string) ([]string, int) {
if white {
str = append(str, r)
} else {
ret = append(ret, string(str))
ret = append(ret, str)
state = awkBlack
str = []rune{r}
}
}
}
if len(str) > 0 {
ret = append(ret, string(str))
ret = append(ret, str)
}
return ret, prefixLength
}
// Tokenize tokenizes the given string with the delimiter
func Tokenize(str *string, delimiter *regexp.Regexp) []Token {
if delimiter == nil {
func Tokenize(runes []rune, delimiter Delimiter) []Token {
if delimiter.str == nil && delimiter.regex == nil {
// AWK-style (\S+\s*)
tokens, prefixLength := awkTokenizer(str)
tokens, prefixLength := awkTokenizer(runes)
return withPrefixLengths(tokens, prefixLength)
}
tokens := delimiter.FindAllString(*str, -1)
return withPrefixLengths(tokens, 0)
}
func joinTokens(tokens *[]Token) *string {
ret := ""
for _, token := range *tokens {
ret += string(*token.text)
var tokens []string
if delimiter.str != nil {
tokens = strings.Split(string(runes), *delimiter.str)
for i := 0; i < len(tokens)-1; i++ {
tokens[i] = tokens[i] + *delimiter.str
}
} else if delimiter.regex != nil {
str := string(runes)
for len(str) > 0 {
loc := delimiter.regex.FindStringIndex(str)
if loc == nil {
loc = []int{0, len(str)}
}
last := util.Max(loc[1], 1)
tokens = append(tokens, str[:last])
str = str[last:]
}
}
return &ret
asRunes := make([][]rune, len(tokens))
for i, token := range tokens {
asRunes[i] = []rune(token)
}
return withPrefixLengths(asRunes, 0)
}
func joinTokensAsRunes(tokens *[]Token) *[]rune {
func joinTokens(tokens []Token) []rune {
ret := []rune{}
for _, token := range *tokens {
ret = append(ret, *token.text...)
for _, token := range tokens {
ret = append(ret, token.text...)
}
return &ret
return ret
}
func joinTokensAsRunes(tokens []Token) []rune {
ret := []rune{}
for _, token := range tokens {
ret = append(ret, token.text...)
}
return ret
}
// Transform is used to transform the input when --with-nth option is given
func Transform(tokens []Token, withNth []Range) *[]Token {
func Transform(tokens []Token, withNth []Range) []Token {
transTokens := make([]Token, len(withNth))
numTokens := len(tokens)
for idx, r := range withNth {
@@ -162,14 +190,14 @@ func Transform(tokens []Token, withNth []Range) *[]Token {
if r.begin == r.end {
idx := r.begin
if idx == rangeEllipsis {
part = append(part, *joinTokensAsRunes(&tokens)...)
part = append(part, joinTokensAsRunes(tokens)...)
} else {
if idx < 0 {
idx += numTokens + 1
}
if idx >= 1 && idx <= numTokens {
minIdx = idx - 1
part = append(part, *tokens[idx-1].text...)
part = append(part, tokens[idx-1].text...)
}
}
} else {
@@ -196,7 +224,7 @@ func Transform(tokens []Token, withNth []Range) *[]Token {
minIdx = util.Max(0, begin-1)
for idx := begin; idx <= end; idx++ {
if idx >= 1 && idx <= numTokens {
part = append(part, *tokens[idx-1].text...)
part = append(part, tokens[idx-1].text...)
}
}
}
@@ -206,7 +234,7 @@ func Transform(tokens []Token, withNth []Range) *[]Token {
} else {
prefixLength = 0
}
transTokens[idx] = Token{&part, prefixLength}
transTokens[idx] = Token{part, prefixLength, util.TrimLen(part)}
}
return &transTokens
return transTokens
}

View File

@@ -43,14 +43,23 @@ func TestParseRange(t *testing.T) {
func TestTokenize(t *testing.T) {
// AWK-style
input := " abc: def: ghi "
tokens := Tokenize(&input, nil)
if string(*tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 {
tokens := Tokenize([]rune(input), Delimiter{})
if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 {
t.Errorf("%s", tokens)
}
// With delimiter
tokens = Tokenize(&input, delimiterRegexp(":"))
if string(*tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 {
tokens = Tokenize([]rune(input), delimiterRegexp(":"))
if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 {
t.Errorf("%s", tokens)
}
// With delimiter regex
tokens = Tokenize([]rune(input), delimiterRegexp("\\s+"))
if string(tokens[0].text) != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 ||
string(tokens[1].text) != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 ||
string(tokens[2].text) != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 ||
string(tokens[3].text) != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 {
t.Errorf("%s", tokens)
}
}
@@ -58,39 +67,39 @@ func TestTokenize(t *testing.T) {
func TestTransform(t *testing.T) {
input := " abc: def: ghi: jkl"
{
tokens := Tokenize(&input, nil)
tokens := Tokenize([]rune(input), Delimiter{})
{
ranges := splitNth("1,2,3")
tx := Transform(tokens, ranges)
if *joinTokens(tx) != "abc: def: ghi: " {
t.Errorf("%s", *tx)
if string(joinTokens(tx)) != "abc: def: ghi: " {
t.Errorf("%s", tx)
}
}
{
ranges := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges)
if *joinTokens(tx) != "abc: def: ghi: def: ghi: jklabc: " ||
len(*tx) != 4 ||
string(*(*tx)[0].text) != "abc: def: " || (*tx)[0].prefixLength != 2 ||
string(*(*tx)[1].text) != "ghi: " || (*tx)[1].prefixLength != 14 ||
string(*(*tx)[2].text) != "def: ghi: jkl" || (*tx)[2].prefixLength != 8 ||
string(*(*tx)[3].text) != "abc: " || (*tx)[3].prefixLength != 2 {
t.Errorf("%s", *tx)
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
len(tx) != 4 ||
string(tx[0].text) != "abc: def: " || tx[0].prefixLength != 2 ||
string(tx[1].text) != "ghi: " || tx[1].prefixLength != 14 ||
string(tx[2].text) != "def: ghi: jkl" || tx[2].prefixLength != 8 ||
string(tx[3].text) != "abc: " || tx[3].prefixLength != 2 {
t.Errorf("%s", tx)
}
}
}
{
tokens := Tokenize(&input, delimiterRegexp(":"))
tokens := Tokenize([]rune(input), delimiterRegexp(":"))
{
ranges := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges)
if *joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(*tx) != 4 ||
string(*(*tx)[0].text) != " abc: def:" || (*tx)[0].prefixLength != 0 ||
string(*(*tx)[1].text) != " ghi:" || (*tx)[1].prefixLength != 12 ||
string(*(*tx)[2].text) != " def: ghi: jkl" || (*tx)[2].prefixLength != 6 ||
string(*(*tx)[3].text) != " abc:" || (*tx)[3].prefixLength != 0 {
t.Errorf("%s", *tx)
if string(joinTokens(tx)) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx) != 4 ||
string(tx[0].text) != " abc: def:" || tx[0].prefixLength != 0 ||
string(tx[1].text) != " ghi:" || tx[1].prefixLength != 12 ||
string(tx[2].text) != " def: ghi: jkl" || tx[2].prefixLength != 6 ||
string(tx[3].text) != " abc:" || tx[3].prefixLength != 0 {
t.Errorf("%s", tx)
}
}
}

View File

@@ -6,6 +6,7 @@ import "C"
import (
"os"
"time"
"unicode/utf8"
)
// Max returns the largest integer
@@ -19,7 +20,7 @@ func Max(first int, items ...int) int {
return max
}
// Max32 returns the smallest 32-bit integer
// Min32 returns the smallest 32-bit integer
func Min32(first int32, second int32) int32 {
if first <= second {
return first
@@ -69,22 +70,59 @@ func DurWithin(
return val
}
func Between(val int, min int, max int) bool {
return val >= min && val <= max
}
// IsTty returns true is stdin is a terminal
func IsTty() bool {
return int(C.isatty(C.int(os.Stdin.Fd()))) != 0
}
func TrimRight(runes *[]rune) []rune {
// TrimRight returns rune array with trailing white spaces cut off
func TrimRight(runes []rune) []rune {
var i int
for i = len(*runes) - 1; i >= 0; i-- {
char := (*runes)[i]
for i = len(runes) - 1; i >= 0; i-- {
char := runes[i]
if char != ' ' && char != '\t' {
break
}
}
return (*runes)[0 : i+1]
return runes[0 : i+1]
}
// BytesToRunes converts byte array into rune array
func BytesToRunes(bytea []byte) []rune {
runes := make([]rune, 0, len(bytea))
for i := 0; i < len(bytea); {
if bytea[i] < utf8.RuneSelf {
runes = append(runes, rune(bytea[i]))
i++
} else {
r, sz := utf8.DecodeRune(bytea[i:])
i += sz
runes = append(runes, r)
}
}
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)
}
}
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

@@ -501,6 +501,90 @@ class TestGoFZF < TestBase
assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/)
end
def test_tiebreak_length_with_nth
input = %w[
1:hell
123:hello
12345:he
1234567:h
]
writelines tempname, input
output = %w[
1:hell
12345:he
123:hello
1234567:h
]
assert_equal output, `cat #{tempname} | #{FZF} -fh`.split($/)
output = %w[
1234567:h
12345:he
1:hell
123:hello
]
assert_equal output, `cat #{tempname} | #{FZF} -fh -n2 -d:`.split($/)
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
tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter
tmux.until { |lines| lines[-2].include? '2/3' }
@@ -700,8 +784,8 @@ class TestGoFZF < TestBase
assert_equal '6', readonce.chomp
end
def test_header_file
tmux.send_keys "seq 100 | #{fzf "--header-file <(head -5 #{__FILE__})"}", :Enter
def test_header
tmux.send_keys "seq 100 | #{fzf "--header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip)
tmux.until do |lines|
lines[-2].include?('100/100') &&
@@ -709,8 +793,8 @@ class TestGoFZF < TestBase
end
end
def test_header_file_reverse
tmux.send_keys "seq 100 | #{fzf "--header-file=<(head -5 #{__FILE__}) --reverse"}", :Enter
def test_header_reverse
tmux.send_keys "seq 100 | #{fzf "--header=\\\"\\$(head -5 #{__FILE__})\\\" --reverse"}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip)
tmux.until do |lines|
lines[1].include?('100/100') &&
@@ -718,6 +802,26 @@ class TestGoFZF < TestBase
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
tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter
tmux.until { |lines| lines[-2].include?('10/10') }
@@ -743,6 +847,63 @@ class TestGoFZF < TestBase
tmux.send_keys :Enter
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
lines = `TERM=xxx #{FZF}`
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
private
def writelines path, lines
File.unlink path while File.exists? path