m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-14 06:13:47 -05:00

Compare commits

...

44 Commits

Author SHA1 Message Date
Junegunn Choi
b28c14b93a 0.12.0 2016-04-16 14:45:16 +09:00
Junegunn Choi
879ead210f 0.11.2 2016-04-16 14:37:16 +09:00
Junegunn Choi
2f6d23b91e Enhanced ranking algorithm
Based on the patch by Matt Westcott (@mjwestcott).
But with a more conservative approach:
- Does not use linearly increasing penalties; It is agreed upon that we
  should prefer matching characters at the beginnings of the words, but
  it's not always clear that the relevance is inversely proportional to
  the distance from the beginning.
- The approach here is more conservative in that the bonus is never
  large enough to override the matchlen, so it can be thought of as the
  first implicit tiebreak criterion.
- One may argue the change breaks the contract of --tiebreak, but the
  judgement depends on the definition of "tie".
2016-04-16 14:33:38 +09:00
Junegunn Choi
5f63a7b587 Fix flaky test case 2016-04-15 12:57:38 +09:00
Junegunn Choi
d9ce797d88 Merge pull request #540 from WChargin/bash-vimode-delay-fix
[bash] Fix vi mode pre-launch delay
2016-04-15 09:15:12 +09:00
William Chargin
12230f8043 Fix bash-vimode normal-mode cd completion 2016-04-14 13:20:21 -04:00
William Chargin
0c8de1ca44 Fix Bash+vimode pre-launch delay
Summary:
Fix adapted from [@adamheins: fzf, vi-mode, and fixing delays][1].

  [1]: https://adamheins.com/blog/fzf-vi-mode-and-fixing-delays

The basic problem is that
fzf presses <Esc> to enter vi-movement-mode
(as opposed to insert mode)
and then presses a bunch of keys to set up the buffer.
But the <Esc> keypress is also the prefix for a bunch of other commands,
so Bash will dutifully wait an excruciating half-second
before actually executing this command.
Instead, we bind <C-x><C-a>, which is unused by default
and seems reasonably unlikely to be custom-bound,
to be another way to enter vi-movement-mode;
this binding is unambiguous, so fzf can use it without delay.

This change was made by just `:s/\\e/\\C-x\\C-a/gc`
in the relevant section,
after adding the actual binding and comment at the top.
2016-04-14 13:19:05 -04:00
Junegunn Choi
89687105f4 [install] Ask before updating shell configuration files 2016-04-14 14:51:58 +09:00
Junegunn Choi
74d1694be9 Fix #541 - Print double-click when --expect=double-click is set 2016-04-14 04:18:59 +09:00
Junegunn Choi
935e986be5 [zsh] Remove unnecessary evals 2016-04-12 21:20:03 +09:00
Junegunn Choi
e5ac2ebd7c [vim] Escape $
https://github.com/junegunn/fzf.vim/issues/114
2016-04-09 21:44:07 +09:00
Junegunn Choi
8d6e13bf94 Merge pull request #535 from mjwestcott/master
Fix algorithm tests
2016-04-01 09:05:33 +09:00
Matt Westcott
2ca704405a Fix algorithm tests 2016-04-01 00:06:09 +01:00
Junegunn Choi
802c1c2937 Clean up install script
- Do not create zsh files if zsh is not installed (@adam8157)
- Use command -v instead of which (@netei)
- Reenable --pre option

Close #531
2016-03-29 22:30:55 +09:00
Junegunn Choi
3cb5fef6b6 Merge pull request #529 from mjwestcott/master
Fix typo in README.md
2016-03-28 23:21:14 +09:00
Matt Westcott
6da2e0aa1e Fix typo in README.md 2016-03-28 13:40:28 +01:00
Junegunn Choi
24f3ec7f33 Fix FZF_CTRL_R_OPTS for zsh (#526) 2016-03-23 03:02:57 +09:00
Junegunn Choi
a57b375b41 Add $FZF_CTRL_R_OPTS for overriding the default options for CTRL-R
Close #526
2016-03-23 03:00:20 +09:00
Junegunn Choi
6cc9d53978 [fzf-tmux] Fix invalid redirection 2016-03-15 21:21:21 +09:00
Junegunn Choi
df32c05833 [fzf-tmux] Fix issues on tmux 1.8 2016-03-15 21:18:15 +09:00
Junegunn Choi
c0652adf4c [fzf-tmux] tmux 1.6 compatibility
Patch submitted by @netei. Close #524.
2016-03-15 20:35:25 +09:00
Junegunn Choi
6ea760a336 Make 32-bit linux binary (partially) static (#523) 2016-03-15 20:17:29 +09:00
Junegunn Choi
f704b94603 [neovim] Open tab before current tab
Related: https://github.com/junegunn/gv.vim/issues/19
2016-03-06 13:56:29 +09:00
Junegunn Choi
444a67cafa Fix flaky test cases 2016-03-06 12:46:28 +09:00
Junegunn Choi
f91cbd5688 Add ISSUE_TEMPLATE.md
Close #500
2016-03-06 11:24:03 +09:00
Junegunn Choi
3073ca3e5a [neovim] Take total number of tab pages into account (#520)
This fixes the problem where a new tab page is not closed when the
following configuration is used:

  let g:fzf_layout = { 'window': 'execute (tabpagenr()-1)."tabnew"' }
2016-03-04 15:23:07 +09:00
Junegunn Choi
b47ab633e2 0.11.4 2016-03-03 01:57:28 +09:00
Junegunn Choi
09a2ab39fe [bash] Fix shellcheck warnings
Close #516
2016-03-02 23:59:42 +09:00
Junegunn Choi
6cf54833f7 Fix flaky test case 2016-03-02 03:29:08 +09:00
Junegunn Choi
2ccdf21a1f Add --hscroll-off=COL option
Close #513
2016-03-02 03:14:35 +09:00
Junegunn Choi
cf8afc527e Remove .gitmodules 2016-03-02 02:04:38 +09:00
Junegunn Choi
1d6f05f974 [man] Fix invalid exit status in man page
Close #511
2016-02-26 23:36:07 +09:00
Junegunn Choi
85751966e9 Merge pull request #506 from justinmk/fixvarmismatch
[vim] s:callback: Always return list.
2016-02-23 15:39:53 +09:00
Justin M. Keyes
a7bc9d5351 s:callback: Always return list.
Fixes "E706: Variable type mismatch for: ret" when an exception is
caught.
2016-02-23 00:36:36 -05:00
Junegunn Choi
42c006d07c Update install script to try "go get ..."
Related: #470, #497
2016-02-21 22:11:28 +09:00
Junegunn Choi
1b9ca314b8 Update build script
- GOPATH is no longer required
- fzf repository does not have to be in GOPATH
- Build Linux binary with Go 1.5.3
2016-02-20 21:16:24 +09:00
Junegunn Choi
e72a360337 Minor refactoring
- Slightly more efficient processing of Options
- Do not return reference type arguments that are mutated inside the
  function
- Use util.Constrain function when appropriate
2016-02-18 01:46:18 +09:00
Junegunn Choi
45108ddd53 Merge pull request #496 from noscript/master
Go 1.3 compatibility
2016-02-16 18:48:39 +09:00
Sergey Vlasov
e3401a0645 Go 1.3 compatibility 2016-02-16 11:28:40 +02:00
Junegunn Choi
26b9100709 Minor code cleanup 2016-02-16 12:35:42 +09:00
Junegunn Choi
a568120e42 Fix #494 - _fzf_complete hangs on zsh when not using tmux pane 2016-02-16 12:32:05 +09:00
Junegunn Choi
e57182c658 Merge pull request #488 from nhooyr/man-fix-redirect
[man] Remove useless `.R` macros
2016-02-12 13:29:39 +09:00
Anmol Sethi
6354dbbbdf Removed the useless .R macros
If you do `man fzf > /dev/null`, you'll get the following output

`R' is a string (producing the registered sign), not a macro.
`R' is a string (producing the registered sign), not a macro.
`R' is a string (producing the registered sign), not a macro.
`R' is a string (producing the registered sign), not a macro.
`R' is a string (producing the registered sign), not a macro.
`R' is a string (producing the registered sign), not a macro.

Removing these `.R` macros with a newline seems to have no effect on the
page but gets rid of the error.
2016-02-11 23:04:38 -05:00
Junegunn Choi
2b3e740569 [neovim] Fix error in finally block when callback failed
e.g. Opening another buffer when `set nohidden`

https://github.com/junegunn/fzf.vim/issues/77
2016-02-12 12:33:47 +09:00
31 changed files with 646 additions and 334 deletions

29
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,29 @@
<!-- Check all that apply [x] -->
- Category
- [ ] fzf binary
- [ ] fzf-tmux script
- [ ] Key bindings
- [ ] Completion
- [ ] Vim
- [ ] Neovim
- [ ] Etc.
- OS
- [ ] Linux
- [ ] Mac OS X
- [ ] Windows
- [ ] Etc.
- Shell
- [ ] bash
- [ ] zsh
- [ ] fish
<!--
### Before submitting
- Make sure that you have the latest version of fzf
- If you use tmux, make sure $TERM is set to screen or screen-256color
- For more Vim stuff, check out https://github.com/junegunn/fzf.vim
Describe your problem or suggestion from here ...
-->

3
.gitignore vendored
View File

@@ -1,5 +1,6 @@
bin bin
src/fzf/fzf_* src/fzf/fzf-*
gopath
pkg pkg
Gemfile.lock Gemfile.lock
.DS_Store .DS_Store

0
.gitmodules vendored
View File

View File

@@ -1,6 +1,18 @@
CHANGELOG CHANGELOG
========= =========
0.12.0
------
- Enhanced ranking algorithm
- Minor bug fixes
0.11.4
------
- Added `--hscroll-off=COL` option (default: 10) (#513)
- Some fixes in Vim plugin and shell extensions
0.11.3 0.11.3
------ ------

View File

@@ -21,7 +21,7 @@ Pros
Installation Installation
------------ ------------
fzf project consists of the followings: fzf project consists of the following:
- `fzf` executable - `fzf` executable
- `fzf-tmux` script for launching fzf in a tmux pane - `fzf-tmux` script for launching fzf in a tmux pane
@@ -343,7 +343,7 @@ Tips
#### Rendering issues #### Rendering issues
If you have any rendering issues, check the followings: If you have any rendering issues, check the following:
1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it 1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it
contains `256` (e.g. `xterm-256color`) contains `256` (e.g. `xterm-256color`)

View File

@@ -140,15 +140,17 @@ done
if [ -n "$term" -o -t 0 ]; then if [ -n "$term" -o -t 0 ]; then
cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf
tmux set-window-option -q synchronize-panes off \;\ tmux set-window-option synchronize-panes off \;\
set-window-option -q remain-on-exit off \;\ set-window-option remain-on-exit off \;\
split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \
> /dev/null 2>&1
else else
mkfifo $fifo1 mkfifo $fifo1
cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf
tmux set-window-option -q synchronize-panes off \;\ tmux set-window-option synchronize-panes off \;\
set-window-option -q remain-on-exit off \;\ set-window-option remain-on-exit off \;\
split-window $opt "$envs bash $argsf" $swap split-window $opt "$envs bash $argsf" $swap \
> /dev/null 2>&1
cat <&0 > $fifo1 & cat <&0 > $fifo1 &
fi fi
cat $fifo2 cat $fifo2

121
install
View File

@@ -2,12 +2,12 @@
set -u set -u
[[ "$@" =~ --pre ]] && version=0.11.3 pre=1 || [[ "$@" =~ --pre ]] && version=0.12.0 pre=1 ||
version=0.11.3 pre=0 version=0.12.0 pre=0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=1 update_config=2
binary_arch= binary_arch=
help() { help() {
@@ -27,7 +27,7 @@ usage: $0 [OPTIONS]
EOF EOF
} }
for opt in $@; do for opt in "$@"; do
case $opt in case $opt in
--help) --help)
help help
@@ -46,7 +46,7 @@ for opt in $@; do
--no-update-rc) update_config=0 ;; --no-update-rc) update_config=0 ;;
--32) binary_arch=386 ;; --32) binary_arch=386 ;;
--64) binary_arch=amd64 ;; --64) binary_arch=amd64 ;;
--bin) ;; --bin|--pre) ;;
*) *)
echo "unknown option: $opt" echo "unknown option: $opt"
help help
@@ -55,17 +55,14 @@ for opt in $@; do
esac esac
done done
cd $(dirname $BASH_SOURCE) cd "$(dirname "${BASH_SOURCE[0]}")"
fzf_base="$(pwd)" fzf_base="$(pwd)"
# If stdin is a tty, we are "interactive".
interactive=
[ -t 0 ] && interactive=yes
ask() { ask() {
# If stdin is a tty, we are "interactive".
# non-interactive shell: wait for a linefeed # non-interactive shell: wait for a linefeed
# interactive shell: continue after a single keypress # interactive shell: continue after a single keypress
[ -n "$interactive" ] && read_n='-n 1' || read_n= read_n=$([ -t 0 ] && echo "-n 1")
read -p "$1 ([y]/n) " $read_n -r read -p "$1 ([y]/n) " $read_n -r
echo echo
@@ -102,6 +99,16 @@ symlink() {
fi fi
} }
link_fzf_in_path() {
if which_fzf="$(command -v fzf)"; then
echo " - Found in \$PATH"
echo " - Creating symlink: $which_fzf -> bin/fzf"
(cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
check_binary && return
fi
return 1
}
download() { download() {
echo "Downloading bin/fzf ..." echo "Downloading bin/fzf ..."
if [ $pre = 0 ]; then if [ $pre = 0 ]; then
@@ -112,12 +119,7 @@ download() {
if [ -x "$fzf_base"/bin/$1 ]; then if [ -x "$fzf_base"/bin/$1 ]; then
symlink $1 && check_binary && return symlink $1 && check_binary && return
fi fi
if which_fzf="$(which fzf 2> /dev/null)"; then link_fzf_in_path && return
echo " - Found in \$PATH"
echo " - Creating symlink: $which_fzf -> bin/fzf"
(cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
check_binary && return
fi
fi fi
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@@ -126,9 +128,9 @@ download() {
fi fi
local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz
if which curl > /dev/null; then if command -v curl > /dev/null; then
curl -fL $url | tar -xz curl -fL $url | tar -xz
elif which wget > /dev/null; then elif command -v wget > /dev/null; then
wget -O - $url | tar -xz wget -O - $url | tar -xz
else else
binary_error="curl or wget not found" binary_error="curl or wget not found"
@@ -155,19 +157,12 @@ case "$archi" in
*) binary_available=0 binary_error=1 ;; *) binary_available=0 binary_error=1 ;;
esac esac
cd "$fzf_base" install_ruby_fzf() {
if [ -n "$binary_error" ]; then
if [ $binary_available -eq 0 ]; then
echo "No prebuilt binary for $archi ... "
else
echo " - $binary_error !!!"
exit 1
fi
echo "Installing legacy Ruby version ..." echo "Installing legacy Ruby version ..."
# ruby executable # ruby executable
echo -n "Checking Ruby executable ... " echo -n "Checking Ruby executable ... "
ruby=`which ruby` ruby=$(command -v ruby)
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "ruby executable not found !!!" echo "ruby executable not found !!!"
exit 1 exit 1
@@ -175,7 +170,7 @@ if [ -n "$binary_error" ]; then
# System ruby is preferred # System ruby is preferred
system_ruby=/usr/bin/ruby system_ruby=/usr/bin/ruby
if [ -x $system_ruby -a $system_ruby != "$ruby" ]; then if [ -x $system_ruby ] && [ $system_ruby != "$ruby" ]; then
$system_ruby --disable-gems -rcurses -e0 2> /dev/null $system_ruby --disable-gems -rcurses -e0 2> /dev/null
[ $? -eq 0 ] && ruby=$system_ruby [ $? -eq 0 ] && ruby=$system_ruby
fi fi
@@ -228,6 +223,29 @@ if [ -n "$binary_error" ]; then
echo "$fzf_cmd \"\$@\"" >> "$fzf_base"/bin/fzf echo "$fzf_cmd \"\$@\"" >> "$fzf_base"/bin/fzf
chmod +x "$fzf_base"/bin/fzf chmod +x "$fzf_base"/bin/fzf
echo "OK" echo "OK"
}
cd "$fzf_base"
if [ -n "$binary_error" ]; then
if [ $binary_available -eq 0 ]; then
echo "No prebuilt binary for $archi ..."
if command -v go > /dev/null; then
echo -n "Building binary (go get github.com/junegunn/fzf/src/fzf) ... "
if go get github.com/junegunn/fzf/src/fzf; then
echo "OK"
link_fzf_in_path
else
echo "Failed to build binary ..."
install_ruby_fzf
fi
else
echo "go executable not found. Cannot build binary ..."
install_ruby_fzf
fi
else
echo " - $binary_error !!!"
exit 1
fi
fi fi
[[ "$*" =~ "--bin" ]] && exit 0 [[ "$*" =~ "--bin" ]] && exit 0
@@ -245,7 +263,9 @@ if [ -z "$key_bindings" ]; then
fi fi
echo echo
for shell in bash zsh; do has_zsh=$(command -v zsh > /dev/null && echo 1 || echo 0)
shells=$([ $has_zsh -eq 1 ] && echo "bash zsh" || echo "bash")
for shell in $shells; do
echo -n "Generate ~/.fzf.$shell ... " echo -n "Generate ~/.fzf.$shell ... "
src=~/.fzf.${shell} src=~/.fzf.${shell}
@@ -285,9 +305,8 @@ EOF
done done
# fish # fish
has_fish=0 has_fish=$(command -v fish > /dev/null && echo 1 || echo 0)
if [ -n "$(which fish 2> /dev/null)" ]; then if [ $has_fish -eq 1 ]; then
has_fish=1
echo -n "Update fish_user_paths ... " echo -n "Update fish_user_paths ... "
fish << EOF fish << EOF
echo \$fish_user_paths | grep $fzf_base/bin > /dev/null echo \$fish_user_paths | grep $fzf_base/bin > /dev/null
@@ -316,8 +335,8 @@ fi
append_line() { append_line() {
set -e set -e
local skip line file pat lno local update line file pat lno
skip="$1" update="$1"
line="$2" line="$2"
file="$3" file="$3"
pat="${4:-}" pat="${4:-}"
@@ -333,7 +352,7 @@ append_line() {
if [ -n "$lno" ]; then if [ -n "$lno" ]; then
echo " - Already exists: line #$lno" echo " - Already exists: line #$lno"
else else
if [ $skip -eq 1 ]; then if [ $update -eq 1 ]; then
echo >> "$file" echo >> "$file"
echo "$line" >> "$file" echo "$line" >> "$file"
echo " + Added" echo " + Added"
@@ -345,26 +364,30 @@ append_line() {
set +e set +e
} }
if [ $update_config -eq 2 ]; then
echo
ask "Do you want to update your shell configuration files?"
update_config=$?
fi
echo echo
for shell in bash zsh; do for shell in $shells; do
[ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc [ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc
append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" "$dest" "~/.fzf.${shell}" append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" "$dest" "~/.fzf.${shell}"
done done
if [ $key_bindings -eq 1 -a $has_fish -eq 1 ]; then if [ $key_bindings -eq 1 ] && [ $has_fish -eq 1 ]; then
bind_file=~/.config/fish/functions/fish_user_key_bindings.fish bind_file=~/.config/fish/functions/fish_user_key_bindings.fish
append_line $update_config "fzf_key_bindings" "$bind_file" append_line $update_config "fzf_key_bindings" "$bind_file"
fi fi
cat << EOF if [ $update_config -eq 1 ]; then
Finished. Restart your shell or reload config file. echo 'Finished. Restart your shell or reload config file.'
source ~/.bashrc # bash echo ' source ~/.bashrc # bash'
source ${ZDOTDIR:-~}/.zshrc # zsh [ $has_zsh -eq 1 ] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
EOF [ $has_fish -eq 1 ] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish'
[ $has_fish -eq 1 ] && echo " fzf_key_bindings # fish"; cat << EOF echo
echo 'Use uninstall script to remove fzf.'
Use uninstall script to remove fzf. echo
fi
For more information, see: https://github.com/junegunn/fzf echo 'For more information, see: https://github.com/junegunn/fzf'
EOF

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Feb 2016" "fzf 0.11.3" "fzf - a command-line fuzzy finder" .TH fzf 1 "Apr 2016" "fzf 0.12.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -71,7 +71,7 @@ e.g. \fBhistory | fzf --tac --no-sort\fR
.BI "--tiebreak=" "CRI[,..]" .BI "--tiebreak=" "CRI[,..]"
Comma-separated list of sort criteria to apply when the scores are tied. Comma-separated list of sort criteria to apply when the scores are tied.
.br .br
.R ""
.br .br
.BR length " Prefers item with shorter length" .BR length " Prefers item with shorter length"
.br .br
@@ -81,7 +81,7 @@ Comma-separated list of sort criteria to apply when the scores are tied.
.br .br
.BR index " Prefers item that appeared earlier in the input stream" .BR index " Prefers item that appeared earlier in the input stream"
.br .br
.R ""
.br .br
- Each criterion should appear only once in the list - Each criterion should appear only once in the list
.br .br
@@ -144,7 +144,7 @@ Reverse orientation
.BI "--margin=" MARGIN .BI "--margin=" MARGIN
Comma-separated expression for margins around the finder. Comma-separated expression for margins around the finder.
.br .br
.R ""
.br .br
.RS .RS
.BR TRBL " Same margin for top, right, bottom, and left" .BR TRBL " Same margin for top, right, bottom, and left"
@@ -155,12 +155,12 @@ Comma-separated expression for margins around the finder.
.br .br
.BR T,R,B,L " Top, right, bottom, left margin" .BR T,R,B,L " Top, right, bottom, left margin"
.br .br
.R ""
.br .br
Each part can be given in absolute number or in percentage relative to the Each part can be given in absolute number or in percentage relative to the
terminal size with \fB%\fR suffix. terminal size with \fB%\fR suffix.
.br .br
.R ""
.br .br
e.g. \fBfzf --margin 10%\fR e.g. \fBfzf --margin 10%\fR
\fBfzf --margin 1,5%\fR \fBfzf --margin 1,5%\fR
@@ -175,6 +175,11 @@ Enable cyclic scroll
.B "--no-hscroll" .B "--no-hscroll"
Disable horizontal scroll Disable horizontal scroll
.TP .TP
.BI "--hscroll-off=" "COL"
Number of screen columns to keep to the right of the highlighted substring
(default: 10). Setting it to a large value will cause the text to be positioned
on the center of the screen.
.TP
.B "--inline-info" .B "--inline-info"
Display finder info inline with the query Display finder info inline with the query
.TP .TP
@@ -364,7 +369,11 @@ Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR
.SH EXIT STATUS .SH EXIT STATUS
.BR 0 " Normal exit" .BR 0 " Normal exit"
.br .br
.BR 1 " Interrupted with \fBCTRL-C\fR or \fBESC\fR" .BR 1 " No match"
.br
.BR 2 " Error"
.br
.BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR"
.SH FIELD INDEX EXPRESSION .SH FIELD INDEX EXPRESSION
@@ -431,7 +440,7 @@ Junegunn Choi (\fIjunegunn.c@gmail.com\fR)
.I https://github.com/junegunn/fzf .I https://github.com/junegunn/fzf
.RE .RE
.br .br
.R ""
.br .br
.B Extra Vim plugin: .B Extra Vim plugin:
.RS .RS

View File

@@ -74,7 +74,7 @@ function! s:shellesc(arg)
endfunction endfunction
function! s:escape(path) function! s:escape(path)
return escape(a:path, ' %#''"\') return escape(a:path, ' $%#''"\')
endfunction endfunction
" Upgrade legacy options " Upgrade legacy options
@@ -239,7 +239,7 @@ function! s:exit_handler(code, command, ...)
return 1 return 1
endfunction endfunction
function! s:execute(dict, command, temps) function! s:execute(dict, command, temps) abort
call s:pushd(a:dict) call s:pushd(a:dict)
silent! !clear 2> /dev/null silent! !clear 2> /dev/null
let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#') let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#')
@@ -255,7 +255,7 @@ function! s:execute(dict, command, temps)
return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : [] return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : []
endfunction endfunction
function! s:execute_tmux(dict, command, temps) function! s:execute_tmux(dict, command, temps) abort
let command = a:command let command = a:command
if s:pushd(a:dict) if s:pushd(a:dict)
" -c '#{pane_current_path}' is only available on tmux 1.9 or above " -c '#{pane_current_path}' is only available on tmux 1.9 or above
@@ -285,7 +285,7 @@ function! s:calc_size(max, val, dict)
endfunction endfunction
function! s:getpos() function! s:getpos()
return {'tab': tabpagenr(), 'win': winnr(), 'cnt': winnr('$')} return {'tab': tabpagenr(), 'win': winnr(), 'cnt': winnr('$'), 'tcnt': tabpagenr('$')}
endfunction endfunction
function! s:split(dict) function! s:split(dict)
@@ -313,14 +313,14 @@ function! s:split(dict)
if s:present(a:dict, 'window') if s:present(a:dict, 'window')
execute a:dict.window execute a:dict.window
else else
tabnew execute (tabpagenr()-1).'tabnew'
endif endif
finally finally
setlocal winfixwidth winfixheight buftype=nofile bufhidden=wipe nobuflisted setlocal winfixwidth winfixheight buftype=nofile bufhidden=wipe nobuflisted
endtry endtry
endfunction endfunction
function! s:execute_term(dict, command, temps) function! s:execute_term(dict, command, temps) abort
call s:split(a:dict) call s:split(a:dict)
let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF' } let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF' }
@@ -344,6 +344,7 @@ function! s:execute_term(dict, command, temps)
endif endif
call s:pushd(self.dict) call s:pushd(self.dict)
let ret = []
try try
let ret = s:callback(self.dict, self.temps) let ret = s:callback(self.dict, self.temps)
@@ -368,11 +369,10 @@ function! s:execute_term(dict, command, temps)
return [] return []
endfunction endfunction
function! s:callback(dict, temps) function! s:callback(dict, temps) abort
let lines = []
try try
if !filereadable(a:temps.result) if filereadable(a:temps.result)
let lines = []
else
let lines = readfile(a:temps.result) let lines = readfile(a:temps.result)
if has_key(a:dict, 'sink') if has_key(a:dict, 'sink')
for line in lines for line in lines
@@ -391,12 +391,12 @@ try
for tf in values(a:temps) for tf in values(a:temps)
silent! call delete(tf) silent! call delete(tf)
endfor endfor
return lines
catch catch
if stridx(v:exception, ':E325:') < 0 if stridx(v:exception, ':E325:') < 0
echoerr v:exception echoerr v:exception
endif endif
finally
return lines
endtry endtry
endfunction endfunction

View File

@@ -75,11 +75,11 @@ _fzf_opts_completion() {
case "${prev}" in case "${prev}" in
--tiebreak) --tiebreak)
COMPREPLY=( $(compgen -W "length begin end index" -- ${cur}) ) COMPREPLY=( $(compgen -W "length begin end index" -- "$cur") )
return 0 return 0
;; ;;
--color) --color)
COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) ) COMPREPLY=( $(compgen -W "dark light 16 bw" -- "$cur") )
return 0 return 0
;; ;;
--history) --history)
@@ -88,8 +88,8 @@ _fzf_opts_completion() {
;; ;;
esac esac
if [[ ${cur} =~ ^-|\+ ]]; then if [[ "$cur" =~ ^-|\+ ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) COMPREPLY=( $(compgen -W "${opts}" -- "$cur") )
return 0 return 0
fi fi
@@ -108,32 +108,32 @@ _fzf_handle_dynamic_completion() {
elif [ -n "$_fzf_completion_loader" ]; then elif [ -n "$_fzf_completion_loader" ]; then
_completion_loader "$@" _completion_loader "$@"
ret=$? ret=$?
eval $(complete | \grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter) eval "$(complete | \grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)"
source $BASH_SOURCE source "${BASH_SOURCE[0]}"
return $ret return $ret
fi fi
} }
__fzf_generic_path_completion() { __fzf_generic_path_completion() {
local cur base dir leftover matches trigger cmd fzf local cur base dir leftover matches trigger cmd fzf
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') cmd=$(echo "${COMP_WORDS[0]}" | sed 's/[^a-z0-9_=]/_/g')
COMPREPLY=() COMPREPLY=()
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ ${cur} == *"$trigger" ]]; then if [[ "$cur" == *"$trigger" ]]; then
base=${cur:0:${#cur}-${#trigger}} base=${cur:0:${#cur}-${#trigger}}
eval base=$base eval "base=$base"
dir="$base" dir="$base"
while [ 1 ]; do while true; do
if [ -z "$dir" -o -d "$dir" ]; then if [ -z "$dir" ] || [ -d "$dir" ]; then
leftover=${base/#"$dir"} leftover=${base/#"$dir"}
leftover=${leftover/#\/} leftover=${leftover/#\/}
[ -z "$dir" ] && dir='.' [ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [ "$dir" != "/" ] && dir="${dir/%\//}"
tput sc tput sc
matches=$(eval "$1 $(printf %q "$dir")" | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do matches=$(eval "$1 $(printf %q "$dir")" | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read -r item; do
printf "%q$3 " "$item" printf "%q$3 " "$item"
done) done)
matches=${matches% } matches=${matches% }
@@ -159,13 +159,13 @@ __fzf_generic_path_completion() {
_fzf_complete() { _fzf_complete() {
local cur selected trigger cmd fzf post local cur selected trigger cmd fzf post
post="$(caller 0 | awk '{print $2}')_post" post="$(caller 0 | awk '{print $2}')_post"
type -t $post > /dev/null 2>&1 || post=cat type -t "$post" > /dev/null 2>&1 || post=cat
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') cmd=$(echo "${COMP_WORDS[0]}" | sed 's/[^a-z0-9_=]/_/g')
trigger=${FZF_COMPLETION_TRIGGER-'**'} trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}" cur="${COMP_WORDS[COMP_CWORD]}"
if [[ ${cur} == *"$trigger" ]]; then if [[ "$cur" == *"$trigger" ]]; then
cur=${cur:0:${#cur}-${#trigger}} cur=${cur:0:${#cur}-${#trigger}}
tput sc tput sc
@@ -200,7 +200,7 @@ _fzf_complete_kill() {
[ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1 [ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1
local selected fzf local selected fzf
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
tput sc tput sc
selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ') selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ')
tput rc tput rc

View File

@@ -79,8 +79,15 @@ _fzf_dir_completion() {
"" "/" "" "" "/" ""
} }
_fzf_feed_fifo() (
rm -f "$1"
mkfifo "$1"
cat <&0 > "$1" &
)
_fzf_complete() { _fzf_complete() {
local fzf_opts lbuf fzf matches post local fifo fzf_opts lbuf fzf matches post
fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
fzf_opts=$1 fzf_opts=$1
lbuf=$2 lbuf=$2
post="${funcstack[2]}_post" post="${funcstack[2]}_post"
@@ -88,11 +95,13 @@ _fzf_complete() {
[ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
matches=$(cat | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') _fzf_feed_fifo "$fifo"
matches=$(cat "$fifo" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ')
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches" LBUFFER="$lbuf$matches"
fi fi
zle redisplay zle redisplay
rm -f "$fifo"
} }
_fzf_complete_telnet() { _fzf_complete_telnet() {
@@ -136,7 +145,7 @@ fzf-completion() {
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
tokens=(${(z)LBUFFER}) tokens=(${(z)LBUFFER})
if [ ${#tokens} -lt 1 ]; then if [ ${#tokens} -lt 1 ]; then
eval "zle ${fzf_default_completion:-expand-or-complete}" zle ${fzf_default_completion:-expand-or-complete}
return return
fi fi
@@ -171,7 +180,7 @@ fzf-completion() {
fi fi
# Fall back to default completion # Fall back to default completion
else else
eval "zle ${fzf_default_completion:-expand-or-complete}" zle ${fzf_default_completion:-expand-or-complete}
fi fi
} }

View File

@@ -5,7 +5,7 @@ __fzf_select__() {
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}"
eval "$cmd" | fzf -m | while read item; do eval "$cmd" | fzf -m | while read -r item; do
printf '%q ' "$item" printf '%q ' "$item"
done done
echo echo
@@ -14,7 +14,7 @@ __fzf_select__() {
if [[ $- =~ i ]]; then if [[ $- =~ i ]]; then
__fzfcmd() { __fzfcmd() {
[ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" [ "${FZF_TMUX:-1}" != 0 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
} }
__fzf_select_tmux__() { __fzf_select_tmux__() {
@@ -30,10 +30,10 @@ __fzf_select_tmux__() {
} }
__fzf_select_tmux_auto__() { __fzf_select_tmux_auto__() {
if [ ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ]; then if [ "${FZF_TMUX:-1}" != 0 ] && [ ${LINES:-40} -gt 15 ]; then
__fzf_select_tmux__ __fzf_select_tmux__
else else
tmux send-keys -t $TMUX_PANE "$(__fzf_select__)" tmux send-keys -t "$TMUX_PANE" "$(__fzf_select__)"
fi fi
} }
@@ -49,7 +49,7 @@ __fzf_history__() (
shopt -u nocaseglob nocasematch shopt -u nocaseglob nocasematch
line=$( line=$(
HISTTIMEFORMAT= history | HISTTIMEFORMAT= history |
$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS |
\grep '^ *[0-9]') && \grep '^ *[0-9]') &&
if [[ $- =~ H ]]; then if [[ $- =~ H ]]; then
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
@@ -61,7 +61,7 @@ __fzf_history__() (
__use_tmux=0 __use_tmux=0
__use_tmux_auto=0 __use_tmux_auto=0
if [ -n "$TMUX_PANE" ]; then if [ -n "$TMUX_PANE" ]; then
[ ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1 [ "${FZF_TMUX:-1}" != 0 ] && [ ${LINES:-40} -gt 15 ] && __use_tmux=1
[ $BASH_VERSINFO -gt 3 ] && __use_tmux_auto=1 [ $BASH_VERSINFO -gt 3 ] && __use_tmux_auto=1
fi fi
@@ -85,6 +85,15 @@ if [ -z "$(set -o | \grep '^vi.*on')" ]; then
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
bind '"\ec": " \C-e\C-u$(__fzf_cd__)\e\C-e\er\C-m"' bind '"\ec": " \C-e\C-u$(__fzf_cd__)\e\C-e\er\C-m"'
else else
# We'd usually use "\e" to enter vi-movement-mode so we can do our magic,
# but this incurs a very noticeable delay of a half second or so,
# because many other commands start with "\e".
# Instead, we bind an unused key, "\C-x\C-a",
# to also enter vi-movement-mode,
# and then use that thereafter.
# (We imagine that "\C-x\C-a" is relatively unlikely to be in use.)
bind '"\C-x\C-a": vi-movement-mode'
bind '"\C-x\C-e": shell-expand-line' bind '"\C-x\C-e": shell-expand-line'
bind '"\C-x\C-r": redraw-current-line' bind '"\C-x\C-r": redraw-current-line'
bind '"\C-x^": history-expand-line' bind '"\C-x^": history-expand-line'
@@ -94,19 +103,19 @@ else
if [ $__use_tmux_auto -eq 1 ]; then if [ $__use_tmux_auto -eq 1 ]; then
bind -x '"\C-t": "__fzf_select_tmux_auto__"' bind -x '"\C-t": "__fzf_select_tmux_auto__"'
elif [ $__use_tmux -eq 1 ]; then elif [ $__use_tmux -eq 1 ]; then
bind '"\C-t": "\e$a \eddi$(__fzf_select_tmux__)\C-x\C-e\e0P$xa"' bind '"\C-t": "\C-x\C-a$a \C-x\C-addi$(__fzf_select_tmux__)\C-x\C-e\C-x\C-a0P$xa"'
else else
bind '"\C-t": "\e$a \eddi$(__fzf_select__)\C-x\C-e\e0Px$a \C-x\C-r\exa "' bind '"\C-t": "\C-x\C-a$a \C-x\C-addi$(__fzf_select__)\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "'
fi fi
bind -m vi-command '"\C-t": "i\C-t"' bind -m vi-command '"\C-t": "i\C-t"'
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": "\eddi$(__fzf_history__)\C-x\C-e\C-x^\e$a\C-x\C-r"' bind '"\C-r": "\C-x\C-addi$(__fzf_history__)\C-x\C-e\C-x^\C-x\C-a$a\C-x\C-r"'
bind -m vi-command '"\C-r": "i\C-r"' bind -m vi-command '"\C-r": "i\C-r"'
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
bind '"\ec": "\eddi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"' bind '"\ec": "\C-x\C-addi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"'
bind -m vi-command '"\ec": "i\ec"' bind -m vi-command '"\ec": "ddi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"'
fi fi
unset -v __use_tmux __use_tmux_auto unset -v __use_tmux __use_tmux_auto

View File

@@ -27,7 +27,7 @@ function fzf_key_bindings
end end
function __fzf_ctrl_r function __fzf_ctrl_r
history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r > $TMPDIR/fzf.result history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS > $TMPDIR/fzf.result
and commandline (cat $TMPDIR/fzf.result) and commandline (cat $TMPDIR/fzf.result)
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result rm -f $TMPDIR/fzf.result

View File

@@ -38,7 +38,7 @@ bindkey '\ec' fzf-cd-widget
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() { fzf-history-widget() {
local selected num local selected num
selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "${LBUFFER//$/\\$}") ) selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r ${=FZF_CTRL_R_OPTS} -q "${LBUFFER//$/\\$}") )
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$selected[1] num=$selected[1]
if [ -n "$num" ]; then if [ -n "$num" ]; then

View File

@@ -11,7 +11,6 @@ RUN cd / && curl \
tar -xz && mv go go1.4 && \ tar -xz && mv go go1.4 && \
sed -i 's@#define PTHREAD_KEYS_MAX 128@@' /go1.4/src/runtime/cgo/gcc_android_arm.c sed -i 's@#define PTHREAD_KEYS_MAX 128@@' /go1.4/src/runtime/cgo/gcc_android_arm.c
ENV GOPATH /go
ENV GOROOT /go1.4 ENV GOROOT /go1.4
ENV PATH /go1.4/bin:$PATH ENV PATH /go1.4/bin:$PATH
@@ -37,8 +36,5 @@ RUN cd / && curl \
make install && \ make install && \
mv /ndk/sysroot/usr/lib/libncursesw.a /ndk/sysroot/usr/lib/libncurses.a mv /ndk/sysroot/usr/lib/libncursesw.a /ndk/sysroot/usr/lib/libncurses.a
# Volume
VOLUME /go
# Default CMD # Default CMD
CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash CMD cd /fzf/src && /bin/bash

View File

@@ -10,7 +10,6 @@ RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4 tar -xz && mv go go1.4
ENV GOPATH /go
ENV GOROOT /go1.4 ENV GOROOT /go1.4
ENV PATH /go1.4/bin:$PATH ENV PATH /go1.4/bin:$PATH
@@ -20,9 +19,6 @@ RUN echo '[multilib]' >> /etc/pacman.conf && \
pacman-db-upgrade && yes | pacman -Sy gcc-multilib lib32-ncurses && \ pacman-db-upgrade && yes | pacman -Sy gcc-multilib lib32-ncurses && \
cd $GOROOT/src && GOARCH=386 ./make.bash cd $GOROOT/src && GOARCH=386 ./make.bash
# Volume
VOLUME /go
# Default CMD # Default CMD
CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash CMD cd /fzf/src && /bin/bash

View File

@@ -13,20 +13,20 @@ RUN cd / && curl \
# Install Go 1.5 # Install Go 1.5
RUN cd / && curl \ RUN cd / && curl \
https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz | \ https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz | \
tar -xz && mv go go1.5 tar -xz && mv go go1.5
# Install RPMs for building static 32-bit binary
RUN curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.7/os/i386/Packages/ncurses-static-5.7-4.20090207.el6.i686.rpm -o rpm && rpm -i rpm && \
curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.7/os/i386/Packages/gpm-static-1.20.6-12.el6.i686.rpm -o rpm && rpm -i rpm
ENV GOROOT_BOOTSTRAP /go1.4 ENV GOROOT_BOOTSTRAP /go1.4
ENV GOROOT /go1.5 ENV GOROOT /go1.5
ENV GOPATH /go
ENV PATH /go1.5/bin:$PATH ENV PATH /go1.5/bin:$PATH
# For i386 build # For i386 build
RUN cd $GOROOT/src && GOARCH=386 ./make.bash RUN cd $GOROOT/src && GOARCH=386 ./make.bash
# Volume
VOLUME /go
# Default CMD # Default CMD
CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash CMD cd /fzf/src && /bin/bash

View File

@@ -10,7 +10,6 @@ RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4 tar -xz && mv go go1.4
ENV GOPATH /go
ENV GOROOT /go1.4 ENV GOROOT /go1.4
ENV PATH /go1.4/bin:$PATH ENV PATH /go1.4/bin:$PATH
@@ -18,9 +17,6 @@ ENV PATH /go1.4/bin:$PATH
RUN apt-get install -y lib32ncurses5-dev && \ RUN apt-get install -y lib32ncurses5-dev && \
cd $GOROOT/src && GOARCH=386 ./make.bash cd $GOROOT/src && GOARCH=386 ./make.bash
# Volume
VOLUME /go
# Default CMD # Default CMD
CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash CMD cd /fzf/src && /bin/bash

View File

@@ -1,7 +1,3 @@
ifndef GOPATH
$(error GOPATH is undefined)
endif
ifndef GOOS ifndef GOOS
UNAME_S := $(shell uname -s) UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin) ifeq ($(UNAME_S),Darwin)
@@ -15,33 +11,44 @@ ifneq ($(shell uname -m),x86_64)
$(error "Build on $(UNAME_M) is not supported, yet.") $(error "Build on $(UNAME_M) is not supported, yet.")
endif endif
SOURCES := $(wildcard *.go */*.go) SOURCES := $(wildcard *.go */*.go)
BINDIR := ../bin ROOTDIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
BINDIR := $(shell dirname $(ROOTDIR))/bin
GOPATH := $(shell dirname $(ROOTDIR))/gopath
SRCDIR := $(GOPATH)/src/github.com/junegunn/fzf/src
DOCKEROPTS := -i -t -v $(ROOTDIR):/fzf/src
BINARY32 := fzf-$(GOOS)_386 BINARY32 := fzf-$(GOOS)_386
BINARY64 := fzf-$(GOOS)_amd64 BINARY64 := fzf-$(GOOS)_amd64
BINARYARM7 := fzf-$(GOOS)_arm7 BINARYARM7 := fzf-$(GOOS)_arm7
VERSION := $(shell awk -F= '/version =/ {print $$2}' constants.go | tr -d "\" ") VERSION := $(shell awk -F= '/version =/ {print $$2}' constants.go | tr -d "\" ")
RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 RELEASE32 := fzf-$(VERSION)-$(GOOS)_386
RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64
RELEASEARM7 = fzf-$(VERSION)-$(GOOS)_arm7 RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7
export GOPATH
all: release all: release
release: build release: test build
-cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) -cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32)
cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \
rm -f $(RELEASE32) $(RELEASE64) rm -f $(RELEASE32) $(RELEASE64)
build: test fzf/$(BINARY32) fzf/$(BINARY64) build: fzf/$(BINARY32) fzf/$(BINARY64)
android-build: $(SRCDIR):
cd fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-extldflags=-pie" -o $(BINARYARM7) mkdir -p $(shell dirname $(SRCDIR))
cd fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) && \ ln -s $(ROOTDIR) $(SRCDIR)
deps: $(SRCDIR) $(SOURCES)
cd $(SRCDIR) && go get
android-build: $(SRCDIR)
cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get
cd $(SRCDIR)/fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-extldflags=-pie" -o $(BINARYARM7)
cd $(SRCDIR)/fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) && \
rm -f $(RELEASEARM7) rm -f $(RELEASEARM7)
test: test: deps
go get
SHELL=/bin/sh go test -v ./... SHELL=/bin/sh go test -v ./...
install: $(BINDIR)/fzf install: $(BINDIR)/fzf
@@ -52,10 +59,10 @@ uninstall:
clean: clean:
cd fzf && rm -f fzf-* cd fzf && rm -f fzf-*
fzf/$(BINARY32): $(SOURCES) fzf/$(BINARY32): deps
cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -o $(BINARY32) cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -tags "$(TAGS)" -o $(BINARY32)
fzf/$(BINARY64): $(SOURCES) fzf/$(BINARY64): deps
cd fzf && go build -a -tags "$(TAGS)" -o $(BINARY64) cd fzf && go build -a -tags "$(TAGS)" -o $(BINARY64)
$(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) $(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR)
@@ -78,29 +85,29 @@ docker-android:
docker build -t junegunn/android-sandbox - < Dockerfile.android docker build -t junegunn/android-sandbox - < Dockerfile.android
arch: docker-arch arch: docker-arch
docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ docker run $(DOCKEROPTS) junegunn/$@-sandbox \
sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' sh -c 'cd /fzf/src; /bin/bash'
ubuntu: docker-ubuntu ubuntu: docker-ubuntu
docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ docker run $(DOCKEROPTS) junegunn/$@-sandbox \
sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' sh -c 'cd /fzf/src; /bin/bash'
centos: docker-centos centos: docker-centos
docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ docker run $(DOCKEROPTS) junegunn/$@-sandbox \
sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' sh -c 'cd /fzf/src; /bin/bash'
linux: docker-centos linux: docker-centos
docker run -i -t -v $(GOPATH):/go junegunn/centos-sandbox \ docker run $(DOCKEROPTS) junegunn/centos-sandbox \
/bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make TAGS=static' /bin/bash -ci 'cd /fzf/src; make TAGS=static'
ubuntu-android: docker-android ubuntu-android: docker-android
docker run -i -t -v $(GOPATH):/go junegunn/android-sandbox \ docker run $(DOCKEROPTS) junegunn/android-sandbox \
sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' sh -c 'cd /fzf/src; /bin/bash'
android: docker-android android: docker-android
docker run -i -t -v $(GOPATH):/go junegunn/android-sandbox \ docker run $(DOCKEROPTS) junegunn/android-sandbox \
/bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; GOOS=android make android-build' /bin/bash -ci 'cd /fzf/src; GOOS=android make android-build'
.PHONY: all build release test install uninstall clean docker \ .PHONY: all build deps release test install uninstall clean \
linux arch ubuntu centos docker-arch docker-ubuntu docker-centos \ linux arch ubuntu centos docker-arch docker-ubuntu docker-centos \
android-build docker-android ubuntu-android android android-build docker-android ubuntu-android android

View File

@@ -22,10 +22,30 @@ func runeAt(runes []rune, index int, max int, forward bool) rune {
return runes[max-index-1] return runes[max-index-1]
} }
// Result conatins the results of running a match function.
type Result struct {
Start int32
End int32
// Items are basically sorted by the lengths of matched substrings.
// But we slightly adjust the score with bonus for better results.
Bonus int32
}
type charClass int
const (
charNonWord charClass = iota
charLower
charUpper
charLetter
charNumber
)
// FuzzyMatch performs fuzzy-match // FuzzyMatch performs fuzzy-match
func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result {
if len(pattern) == 0 { if len(pattern) == 0 {
return 0, 0 return Result{0, 0, 0}
} }
// 0. (FIXME) How to find the shortest match? // 0. (FIXME) How to find the shortest match?
@@ -90,12 +110,76 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
} }
} }
} }
if forward {
return sidx, eidx // Calculate the bonus. This can't be done at the same time as the
// pattern scan above because 'forward' may be false.
if !forward {
sidx, eidx = lenRunes-eidx, lenRunes-sidx
} }
return lenRunes - eidx, lenRunes - sidx
var bonus int32
pidx := 0
consecutive := false
prevClass := charNonWord
for index := 0; index < eidx; index++ {
char := runes[index]
var class charClass
if unicode.IsLower(char) {
class = charLower
} else if unicode.IsUpper(char) {
class = charUpper
} else if unicode.IsLetter(char) {
class = charLetter
} else if unicode.IsNumber(char) {
class = charNumber
} else {
class = charNonWord
}
var point int32
if prevClass == charNonWord && class != charNonWord {
// Word boundary
point = 2
} else if prevClass == charLower && class == charUpper ||
prevClass != charNumber && class == charNumber {
// camelCase letter123
point = 1
}
prevClass = class
if index >= sidx {
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
} else if char > unicode.MaxASCII {
char = unicode.To(unicode.LowerCase, char)
}
}
pchar := pattern[pidx]
if pchar == char {
// Boost bonus for the first character in the pattern
if pidx == 0 {
point *= 2
}
// Bonus to consecutive matching chars
if consecutive {
point++
}
bonus += point
if pidx++; pidx == lenPattern {
break
}
consecutive = true
} else {
consecutive = false
}
}
}
return Result{int32(sidx), int32(eidx), bonus}
} }
return -1, -1 return Result{-1, -1, 0}
} }
// ExactMatchNaive is a basic string searching algorithm that handles case // ExactMatchNaive is a basic string searching algorithm that handles case
@@ -105,16 +189,17 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
// //
// We might try to implement better algorithms in the future: // We might try to implement better algorithms in the future:
// http://en.wikipedia.org/wiki/String_searching_algorithm // http://en.wikipedia.org/wiki/String_searching_algorithm
func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result {
// Note: ExactMatchNaive always return a zero bonus.
if len(pattern) == 0 { if len(pattern) == 0 {
return 0, 0 return Result{0, 0, 0}
} }
lenRunes := len(runes) lenRunes := len(runes)
lenPattern := len(pattern) lenPattern := len(pattern)
if lenRunes < lenPattern { if lenRunes < lenPattern {
return -1, -1 return Result{-1, -1, 0}
} }
pidx := 0 pidx := 0
@@ -132,22 +217,23 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
pidx++ pidx++
if pidx == lenPattern { if pidx == lenPattern {
if forward { if forward {
return index - lenPattern + 1, index + 1 return Result{int32(index - lenPattern + 1), int32(index + 1), 0}
} }
return lenRunes - (index + 1), lenRunes - (index - lenPattern + 1) return Result{int32(lenRunes - (index + 1)), int32(lenRunes - (index - lenPattern + 1)), 0}
} }
} else { } else {
index -= pidx index -= pidx
pidx = 0 pidx = 0
} }
} }
return -1, -1 return Result{-1, -1, 0}
} }
// PrefixMatch performs prefix-match // PrefixMatch performs prefix-match
func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result {
// Note: PrefixMatch always return a zero bonus.
if len(runes) < len(pattern) { if len(runes) < len(pattern) {
return -1, -1 return Result{-1, -1, 0}
} }
for index, r := range pattern { for index, r := range pattern {
@@ -156,19 +242,20 @@ func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
char = unicode.ToLower(char) char = unicode.ToLower(char)
} }
if char != r { if char != r {
return -1, -1 return Result{-1, -1, 0}
} }
} }
return 0, len(pattern) return Result{0, int32(len(pattern)), 0}
} }
// SuffixMatch performs suffix-match // SuffixMatch performs suffix-match
func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) (int, int) { func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) Result {
// Note: SuffixMatch always return a zero bonus.
runes := util.TrimRight(input) runes := util.TrimRight(input)
trimmedLen := len(runes) trimmedLen := len(runes)
diff := trimmedLen - len(pattern) diff := trimmedLen - len(pattern)
if diff < 0 { if diff < 0 {
return -1, -1 return Result{-1, -1, 0}
} }
for index, r := range pattern { for index, r := range pattern {
@@ -177,23 +264,24 @@ func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune)
char = unicode.ToLower(char) char = unicode.ToLower(char)
} }
if char != r { if char != r {
return -1, -1 return Result{-1, -1, 0}
} }
} }
return trimmedLen - len(pattern), trimmedLen return Result{int32(trimmedLen - len(pattern)), int32(trimmedLen), 0}
} }
// EqualMatch performs equal-match // EqualMatch performs equal-match
func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result {
// Note: EqualMatch always return a zero bonus.
if len(runes) != len(pattern) { if len(runes) != len(pattern) {
return -1, -1 return Result{-1, -1, 0}
} }
runesStr := string(runes) runesStr := string(runes)
if !caseSensitive { if !caseSensitive {
runesStr = strings.ToLower(runesStr) runesStr = strings.ToLower(runesStr)
} }
if runesStr == string(pattern) { if runesStr == string(pattern) {
return 0, len(pattern) return Result{0, int32(len(pattern)), 0}
} }
return -1, -1 return Result{-1, -1, 0}
} }

View File

@@ -5,65 +5,84 @@ import (
"testing" "testing"
) )
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) { func assertMatch(t *testing.T, fun func(bool, bool, []rune, []rune) Result, caseSensitive, forward bool, input, pattern string, sidx int32, eidx int32, bonus int32) {
if !caseSensitive { if !caseSensitive {
pattern = strings.ToLower(pattern) pattern = strings.ToLower(pattern)
} }
s, e := fun(caseSensitive, forward, []rune(input), []rune(pattern)) res := fun(caseSensitive, forward, []rune(input), []rune(pattern))
if s != sidx { if res.Start != sidx {
t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern) t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", res.Start, sidx, input, pattern)
} }
if e != eidx { if res.End != eidx {
t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", e, eidx, input, pattern) t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", res.End, eidx, input, pattern)
}
if res.Bonus != bonus {
t.Errorf("Invalid bonus: %d (expected: %d, %s / %s)", res.Bonus, bonus, input, pattern)
} }
} }
func TestFuzzyMatch(t *testing.T) { func TestFuzzyMatch(t *testing.T) {
assertMatch(t, FuzzyMatch, false, true, "fooBarbaz", "oBZ", 2, 9) assertMatch(t, FuzzyMatch, false, true, "fooBarbaz", "oBZ", 2, 9, 2)
assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBZ", -1, -1) assertMatch(t, FuzzyMatch, false, true, "foo bar baz", "fbb", 0, 9, 8)
assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBz", 2, 9) assertMatch(t, FuzzyMatch, false, true, "/AutomatorDocument.icns", "rdoc", 9, 13, 4)
assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "fooBarbazz", -1, -1) assertMatch(t, FuzzyMatch, false, true, "/man1/zshcompctl.1", "zshc", 6, 10, 7)
assertMatch(t, FuzzyMatch, false, true, "/.oh-my-zsh/cache", "zshc", 8, 13, 8)
assertMatch(t, FuzzyMatch, false, true, "ab0123 456", "12356", 3, 10, 3)
assertMatch(t, FuzzyMatch, false, true, "abc123 456", "12356", 3, 10, 5)
assertMatch(t, FuzzyMatch, false, true, "foo/bar/baz", "fbb", 0, 9, 8)
assertMatch(t, FuzzyMatch, false, true, "fooBarBaz", "fbb", 0, 7, 6)
assertMatch(t, FuzzyMatch, false, true, "foo barbaz", "fbb", 0, 8, 6)
assertMatch(t, FuzzyMatch, false, true, "fooBar Baz", "foob", 0, 4, 8)
assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBZ", -1, -1, 0)
assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBz", 2, 9, 2)
assertMatch(t, FuzzyMatch, true, true, "Foo Bar Baz", "fbb", -1, -1, 0)
assertMatch(t, FuzzyMatch, true, true, "Foo/Bar/Baz", "FBB", 0, 9, 8)
assertMatch(t, FuzzyMatch, true, true, "FooBarBaz", "FBB", 0, 7, 6)
assertMatch(t, FuzzyMatch, true, true, "foo BarBaz", "fBB", 0, 8, 7)
assertMatch(t, FuzzyMatch, true, true, "FooBar Baz", "FooB", 0, 4, 8)
assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "fooBarbazz", -1, -1, 0)
} }
func TestFuzzyMatchBackward(t *testing.T) { func TestFuzzyMatchBackward(t *testing.T) {
assertMatch(t, FuzzyMatch, false, true, "foobar fb", "fb", 0, 4) assertMatch(t, FuzzyMatch, false, true, "foobar fb", "fb", 0, 4, 4)
assertMatch(t, FuzzyMatch, false, false, "foobar fb", "fb", 7, 9) assertMatch(t, FuzzyMatch, false, false, "foobar fb", "fb", 7, 9, 5)
} }
func TestExactMatchNaive(t *testing.T) { func TestExactMatchNaive(t *testing.T) {
for _, dir := range []bool{true, false} { for _, dir := range []bool{true, false} {
assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5) assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5, 0)
assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1) assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1, 0)
assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1) assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1, 0)
} }
} }
func TestExactMatchNaiveBackward(t *testing.T) { func TestExactMatchNaiveBackward(t *testing.T) {
assertMatch(t, FuzzyMatch, false, true, "foobar foob", "oo", 1, 3) assertMatch(t, ExactMatchNaive, false, true, "foobar foob", "oo", 1, 3, 0)
assertMatch(t, FuzzyMatch, false, false, "foobar foob", "oo", 8, 10) assertMatch(t, ExactMatchNaive, false, false, "foobar foob", "oo", 8, 10, 0)
} }
func TestPrefixMatch(t *testing.T) { func TestPrefixMatch(t *testing.T) {
for _, dir := range []bool{true, false} { for _, dir := range []bool{true, false} {
assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3) assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, 0)
assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1) assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0)
assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "baz", -1, -1) assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "baz", -1, -1, 0)
} }
} }
func TestSuffixMatch(t *testing.T) { func TestSuffixMatch(t *testing.T) {
for _, dir := range []bool{true, false} { for _, dir := range []bool{true, false} {
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1) assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1, 0)
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9) assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9, 0)
assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1) assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1, 0)
} }
} }
func TestEmptyPattern(t *testing.T) { func TestEmptyPattern(t *testing.T) {
for _, dir := range []bool{true, false} { for _, dir := range []bool{true, false} {
assertMatch(t, FuzzyMatch, true, dir, "foobar", "", 0, 0) assertMatch(t, FuzzyMatch, true, dir, "foobar", "", 0, 0, 0)
assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0) assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0, 0)
assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0) assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0, 0)
assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6) assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6, 0)
} }
} }

View File

@@ -8,7 +8,7 @@ import (
const ( const (
// Current version // Current version
version = "0.11.3" version = "0.12.0"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond

View File

@@ -23,6 +23,7 @@ type Item struct {
offsets []Offset offsets []Offset
colors []ansiOffset colors []ansiOffset
rank [5]int32 rank [5]int32
bonus int32
} }
// Sort criteria to use. Never changes once fzf is started. // Sort criteria to use. Never changes once fzf is started.
@@ -73,15 +74,17 @@ func (item *Item) Rank(cache bool) [5]int32 {
matchlen += end - begin matchlen += end - begin
} }
} }
if matchlen == 0 {
matchlen = math.MaxInt32
}
rank := buildEmptyRank(item.Index()) rank := buildEmptyRank(item.Index())
for idx, criterion := range sortCriteria { for idx, criterion := range sortCriteria {
var val int32 var val int32
switch criterion { switch criterion {
case byMatchLen: case byMatchLen:
val = int32(matchlen) if matchlen == 0 {
val = math.MaxInt32
} else {
// It is extremely unlikely that bonus exceeds 128
val = 128*int32(matchlen) - item.bonus
}
case byLength: case byLength:
// It is guaranteed that .transformed in not null in normal execution // It is guaranteed that .transformed in not null in normal execution
if item.transformed != nil { if item.transformed != nil {

View File

@@ -200,7 +200,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
} }
partialResults := make([][]*Item, numSlices) partialResults := make([][]*Item, numSlices)
for range slices { for _, _ = range slices {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches partialResults[partialResult.index] = partialResult.matches
} }

View File

@@ -42,6 +42,8 @@ const usage = `usage: fzf [options]
--tabstop=SPACES Number of spaces for a tab character (default: 8) --tabstop=SPACES Number of spaces for a tab character (default: 8)
--cycle Enable cyclic scroll --cycle Enable cyclic scroll
--no-hscroll Disable horizontal scroll --no-hscroll Disable horizontal scroll
--hscroll-off=COL Number of screen columns to keep to the right of the
highlighted substring (default: 10)
--inline-info Display finder info inline with the query --inline-info Display finder info inline with the query
--prompt=STR Input prompt (default: '> ') --prompt=STR Input prompt (default: '> ')
--bind=KEYBINDS Custom key bindings. Refer to the man page. --bind=KEYBINDS Custom key bindings. Refer to the man page.
@@ -108,6 +110,7 @@ type Options struct {
Reverse bool Reverse bool
Cycle bool Cycle bool
Hscroll bool Hscroll bool
HscrollOff int
InlineInfo bool InlineInfo bool
Prompt string Prompt string
Query string Query string
@@ -155,6 +158,7 @@ func defaultOptions() *Options {
Reverse: false, Reverse: false,
Cycle: false, Cycle: false,
Hscroll: true, Hscroll: true,
HscrollOff: 10,
InlineInfo: false, InlineInfo: false,
Prompt: "> ", Prompt: "> ",
Query: "", Query: "",
@@ -163,7 +167,7 @@ func defaultOptions() *Options {
Filter: nil, Filter: nil,
ToggleSort: false, ToggleSort: false,
Expect: make(map[int]string), Expect: make(map[int]string),
Keymap: defaultKeymap(), Keymap: make(map[int]actionType),
Execmap: make(map[int]string), Execmap: make(map[int]string),
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
@@ -484,7 +488,7 @@ const (
escapedComma = 1 escapedComma = 1
) )
func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) { func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) {
if executeRegexp == nil { if executeRegexp == nil {
// Backreferences are not supported. // Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
@@ -592,7 +596,6 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
keymap[key] = actNextHistory keymap[key] = actNextHistory
case "toggle-sort": case "toggle-sort":
keymap[key] = actToggleSort keymap[key] = actToggleSort
toggleSort = true
default: default:
if isExecuteAction(actLower) { if isExecuteAction(actLower) {
var offset int var offset int
@@ -613,7 +616,6 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
} }
} }
} }
return keymap, execmap, toggleSort
} }
func isExecuteAction(str string) bool { func isExecuteAction(str string) bool {
@@ -635,13 +637,12 @@ func isExecuteAction(str string) bool {
return false return false
} }
func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType { func parseToggleSort(keymap map[int]actionType, str string) {
keys := parseKeyChords(str, "key name required") keys := parseKeyChords(str, "key name required")
if len(keys) != 1 { if len(keys) != 1 {
errorExit("multiple keys specified") errorExit("multiple keys specified")
} }
keymap[firstKey(keys)] = actToggleSort keymap[firstKey(keys)] = actToggleSort
return keymap
} }
func strLines(str string) []string { func strLines(str string) []string {
@@ -691,7 +692,6 @@ func parseMargin(margin string) [4]string {
} }
func parseOptions(opts *Options, allArgs []string) { func parseOptions(opts *Options, allArgs []string) {
keymap := make(map[int]actionType)
var historyMax int var historyMax int
if opts.History == nil { if opts.History == nil {
historyMax = defaultHistoryMax historyMax = defaultHistoryMax
@@ -741,8 +741,7 @@ func parseOptions(opts *Options, allArgs []string) {
case "--tiebreak": case "--tiebreak":
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind": case "--bind":
keymap, opts.Execmap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.Execmap, nextString(allArgs, &i, "bind expression required"))
parseKeymap(keymap, opts.Execmap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required"))
case "--color": case "--color":
spec := optionalNextString(allArgs, &i) spec := optionalNextString(allArgs, &i)
if len(spec) == 0 { if len(spec) == 0 {
@@ -751,8 +750,7 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Theme = parseTheme(opts.Theme, spec) opts.Theme = parseTheme(opts.Theme, spec)
} }
case "--toggle-sort": case "--toggle-sort":
keymap = checkToggleSort(keymap, nextString(allArgs, &i, "key name required")) parseToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required"))
opts.ToggleSort = true
case "-d", "--delimiter": case "-d", "--delimiter":
opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required"))
case "-n", "--nth": case "-n", "--nth":
@@ -801,6 +799,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Hscroll = true opts.Hscroll = true
case "--no-hscroll": case "--no-hscroll":
opts.Hscroll = false opts.Hscroll = false
case "--hscroll-off":
opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required")
case "--inline-info": case "--inline-info":
opts.InlineInfo = true opts.InlineInfo = true
case "--no-inline-info": case "--no-inline-info":
@@ -869,8 +869,7 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, _ := optString(arg, "-s", "--sort="); match { } else if match, _ := optString(arg, "-s", "--sort="); match {
opts.Sort = 1 // Don't care opts.Sort = 1 // Don't care
} else if match, value := optString(arg, "--toggle-sort="); match { } else if match, value := optString(arg, "--toggle-sort="); match {
keymap = checkToggleSort(keymap, value) parseToggleSort(opts.Keymap, value)
opts.ToggleSort = true
} else if match, value := optString(arg, "--expect="); match { } else if match, value := optString(arg, "--expect="); match {
opts.Expect = parseKeyChords(value, "key names required") opts.Expect = parseKeyChords(value, "key names required")
} else if match, value := optString(arg, "--tiebreak="); match { } else if match, value := optString(arg, "--tiebreak="); match {
@@ -878,8 +877,7 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--color="); match { } else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(opts.Theme, value) opts.Theme = parseTheme(opts.Theme, value)
} else if match, value := optString(arg, "--bind="); match { } else if match, value := optString(arg, "--bind="); match {
keymap, opts.Execmap, opts.ToggleSort = parseKeymap(opts.Keymap, opts.Execmap, value)
parseKeymap(keymap, opts.Execmap, opts.ToggleSort, value)
} else if match, value := optString(arg, "--history="); match { } else if match, value := optString(arg, "--history="); match {
setHistory(value) setHistory(value)
} else if match, value := optString(arg, "--history-size="); match { } else if match, value := optString(arg, "--history-size="); match {
@@ -892,6 +890,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Margin = parseMargin(value) opts.Margin = parseMargin(value)
} else if match, value := optString(arg, "--tabstop="); match { } else if match, value := optString(arg, "--tabstop="); match {
opts.Tabstop = atoi(value) opts.Tabstop = atoi(value)
} else if match, value := optString(arg, "--hscroll-off="); match {
opts.HscrollOff = atoi(value)
} else { } else {
errorExit("unknown option: " + arg) errorExit("unknown option: " + arg)
} }
@@ -902,24 +902,35 @@ func parseOptions(opts *Options, allArgs []string) {
errorExit("header lines must be a non-negative integer") errorExit("header lines must be a non-negative integer")
} }
if opts.HscrollOff < 0 {
errorExit("hscroll offset must be a non-negative integer")
}
if opts.Tabstop < 1 { if opts.Tabstop < 1 {
errorExit("tab stop must be a positive integer") errorExit("tab stop must be a positive integer")
} }
}
// Change default actions for CTRL-N / CTRL-P when --history is used func postProcessOptions(opts *Options) {
// Default actions for CTRL-N / CTRL-P when --history is set
if opts.History != nil { if opts.History != nil {
if _, prs := keymap[curses.CtrlP]; !prs { if _, prs := opts.Keymap[curses.CtrlP]; !prs {
keymap[curses.CtrlP] = actPreviousHistory opts.Keymap[curses.CtrlP] = actPreviousHistory
} }
if _, prs := keymap[curses.CtrlN]; !prs { if _, prs := opts.Keymap[curses.CtrlN]; !prs {
keymap[curses.CtrlN] = actNextHistory opts.Keymap[curses.CtrlN] = actNextHistory
} }
} }
// Override default key bindings // Extend the default key map
for key, act := range keymap { keymap := defaultKeymap()
opts.Keymap[key] = act for key, act := range opts.Keymap {
if act == actToggleSort {
opts.ToggleSort = true
}
keymap[key] = act
} }
opts.Keymap = keymap
// If we're not using extended search mode, --nth option becomes irrelevant // If we're not using extended search mode, --nth option becomes irrelevant
// if it contains the whole range // if it contains the whole range
@@ -939,9 +950,13 @@ func ParseOptions() *Options {
// Options from Env var // Options from Env var
words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS")) words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))
parseOptions(opts, words) if len(words) > 0 {
parseOptions(opts, words)
}
// Options from command-line arguments // Options from command-line arguments
parseOptions(opts, os.Args[1:]) parseOptions(opts, os.Args[1:])
postProcessOptions(opts)
return opts return opts
} }

View File

@@ -96,6 +96,7 @@ func TestIrrelevantNth(t *testing.T) {
opts := defaultOptions() opts := defaultOptions()
words := []string{"--nth", "..", "-x"} words := []string{"--nth", "..", "-x"}
parseOptions(opts, words) parseOptions(opts, words)
postProcessOptions(opts)
if len(opts.Nth) != 0 { if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %s", opts.Nth) t.Errorf("nth should be empty: %s", opts.Nth)
} }
@@ -104,6 +105,7 @@ func TestIrrelevantNth(t *testing.T) {
{ {
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(opts, words)
postProcessOptions(opts)
if len(opts.Nth) != 0 { if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %s", opts.Nth) t.Errorf("nth should be empty: %s", opts.Nth)
} }
@@ -112,6 +114,7 @@ func TestIrrelevantNth(t *testing.T) {
opts := defaultOptions() opts := defaultOptions()
words = append(words, "-x") words = append(words, "-x")
parseOptions(opts, words) parseOptions(opts, words)
postProcessOptions(opts)
if len(opts.Nth) != 2 { if len(opts.Nth) != 2 {
t.Errorf("nth should not be empty: %s", opts.Nth) t.Errorf("nth should not be empty: %s", opts.Nth)
} }
@@ -231,15 +234,11 @@ func TestBind(t *testing.T) {
keymap := defaultKeymap() keymap := defaultKeymap()
execmap := make(map[int]string) execmap := make(map[int]string)
check(actBeginningOfLine, keymap[curses.CtrlA]) check(actBeginningOfLine, keymap[curses.CtrlA])
keymap, execmap, toggleSort := parseKeymap(keymap, execmap,
parseKeymap(keymap, execmap, false, "ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+ "f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+
"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ ",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)")
",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)")
if !toggleSort {
t.Errorf("toggleSort not set")
}
check(actKillLine, keymap[curses.CtrlA]) check(actKillLine, keymap[curses.CtrlA])
check(actToggleSort, keymap[curses.CtrlB]) check(actToggleSort, keymap[curses.CtrlB])
check(actPageUp, keymap[curses.AltZ+'c']) check(actPageUp, keymap[curses.AltZ+'c'])
@@ -259,15 +258,11 @@ func TestBind(t *testing.T) {
checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X']) checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X'])
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
keymap, execmap, toggleSort = parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
parseKeymap(keymap, execmap, false, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])])
} }
keymap, execmap, toggleSort = parseKeymap(keymap, execmap, false, "f1:abort") parseKeymap(keymap, execmap, "f1:abort")
if toggleSort {
t.Errorf("toggleSort set")
}
check(actAbort, keymap[curses.F1]) check(actAbort, keymap[curses.F1])
} }
@@ -328,3 +323,53 @@ func TestParseNilTheme(t *testing.T) {
t.Errorf("color should now be enabled and customized") t.Errorf("color should now be enabled and customized")
} }
} }
func TestDefaultCtrlNP(t *testing.T) {
check := func(words []string, key int, expected actionType) {
opts := defaultOptions()
parseOptions(opts, words)
postProcessOptions(opts)
if opts.Keymap[key] != expected {
t.Error()
}
}
check([]string{}, curses.CtrlN, actDown)
check([]string{}, curses.CtrlP, actUp)
check([]string{"--bind=ctrl-n:accept"}, curses.CtrlN, actAccept)
check([]string{"--bind=ctrl-p:accept"}, curses.CtrlP, actAccept)
hist := "--history=/tmp/foo"
check([]string{hist}, curses.CtrlN, actNextHistory)
check([]string{hist}, curses.CtrlP, actPreviousHistory)
check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlN, actAccept)
check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlP, actPreviousHistory)
check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlN, actNextHistory)
check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlP, actAccept)
}
func TestToggle(t *testing.T) {
optsFor := func(words ...string) *Options {
opts := defaultOptions()
parseOptions(opts, words)
postProcessOptions(opts)
return opts
}
opts := optsFor()
if opts.ToggleSort {
t.Error()
}
opts = optsFor("--bind=a:toggle-sort")
if !opts.ToggleSort {
t.Error()
}
opts = optsFor("--bind=a:toggle-sort", "--bind=a:up")
if opts.ToggleSort {
t.Error()
}
}

View File

@@ -49,7 +49,7 @@ type Pattern struct {
cacheable bool cacheable bool
delimiter Delimiter delimiter Delimiter
nth []Range nth []Range
procFun map[termType]func(bool, bool, []rune, []rune) (int, int) procFun map[termType]func(bool, bool, []rune, []rune) algo.Result
} }
var ( var (
@@ -125,7 +125,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
cacheable: cacheable, cacheable: cacheable,
nth: nth, nth: nth,
delimiter: delimiter, delimiter: delimiter,
procFun: make(map[termType]func(bool, bool, []rune, []rune) (int, int))} procFun: make(map[termType]func(bool, bool, []rune, []rune) algo.Result)}
ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termFuzzy] = algo.FuzzyMatch
ptr.procFun[termEqual] = algo.EqualMatch ptr.procFun[termEqual] = algo.EqualMatch
@@ -275,15 +275,16 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
matches := []*Item{} matches := []*Item{}
if !p.extended { if !p.extended {
for _, item := range *chunk { for _, item := range *chunk {
if sidx, eidx, tlen := p.basicMatch(item); sidx >= 0 { offset, bonus := p.basicMatch(item)
if sidx := offset[0]; sidx >= 0 {
matches = append(matches, matches = append(matches,
dupItem(item, []Offset{Offset{int32(sidx), int32(eidx), int32(tlen)}})) dupItem(item, []Offset{offset}, bonus))
} }
} }
} else { } else {
for _, item := range *chunk { for _, item := range *chunk {
if offsets := p.extendedMatch(item); len(offsets) == len(p.termSets) { if offsets, bonus := p.extendedMatch(item); len(offsets) == len(p.termSets) {
matches = append(matches, dupItem(item, offsets)) matches = append(matches, dupItem(item, offsets, bonus))
} }
} }
} }
@@ -293,25 +294,27 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
// MatchItem returns true if the Item is a match // MatchItem returns true if the Item is a match
func (p *Pattern) MatchItem(item *Item) bool { func (p *Pattern) MatchItem(item *Item) bool {
if !p.extended { if !p.extended {
sidx, _, _ := p.basicMatch(item) offset, _ := p.basicMatch(item)
sidx := offset[0]
return sidx >= 0 return sidx >= 0
} }
offsets := p.extendedMatch(item) offsets, _ := p.extendedMatch(item)
return len(offsets) == len(p.termSets) return len(offsets) == len(p.termSets)
} }
func dupItem(item *Item, offsets []Offset) *Item { func dupItem(item *Item, offsets []Offset, bonus int32) *Item {
sort.Sort(ByOrder(offsets)) sort.Sort(ByOrder(offsets))
return &Item{ return &Item{
text: item.text, text: item.text,
origText: item.origText, origText: item.origText,
transformed: item.transformed, transformed: item.transformed,
offsets: offsets, offsets: offsets,
bonus: bonus,
colors: item.colors, colors: item.colors,
rank: buildEmptyRank(item.Index())} rank: buildEmptyRank(item.Index())}
} }
func (p *Pattern) basicMatch(item *Item) (int, int, int) { func (p *Pattern) basicMatch(item *Item) (Offset, int32) {
input := p.prepareInput(item) input := p.prepareInput(item)
if p.fuzzy { if p.fuzzy {
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text)
@@ -319,29 +322,33 @@ func (p *Pattern) basicMatch(item *Item) (int, int, int) {
return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text) return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text)
} }
func (p *Pattern) extendedMatch(item *Item) []Offset { func (p *Pattern) extendedMatch(item *Item) ([]Offset, int32) {
input := p.prepareInput(item) input := p.prepareInput(item)
offsets := []Offset{} offsets := []Offset{}
var totalBonus int32
for _, termSet := range p.termSets { for _, termSet := range p.termSets {
var offset *Offset var offset *Offset
var bonus int32
for _, term := range termSet { for _, term := range termSet {
pfun := p.procFun[term.typ] pfun := p.procFun[term.typ]
if sidx, eidx, tlen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 { off, pen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text)
if sidx := off[0]; sidx >= 0 {
if term.inv { if term.inv {
continue continue
} }
offset = &Offset{int32(sidx), int32(eidx), int32(tlen)} offset, bonus = &off, pen
break break
} else if term.inv { } else if term.inv {
offset = &Offset{0, 0, 0} offset, bonus = &Offset{0, 0, 0}, 0
continue continue
} }
} }
if offset != nil { if offset != nil {
offsets = append(offsets, *offset) offsets = append(offsets, *offset)
totalBonus += bonus
} }
} }
return offsets return offsets, totalBonus
} }
func (p *Pattern) prepareInput(item *Item) []Token { func (p *Pattern) prepareInput(item *Item) []Token {
@@ -360,13 +367,16 @@ func (p *Pattern) prepareInput(item *Item) []Token {
return ret return ret
} }
func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) (int, int), func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) algo.Result,
tokens []Token, caseSensitive bool, forward bool, pattern []rune) (int, int, int) { tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int32) {
for _, part := range tokens { for _, part := range tokens {
prefixLength := part.prefixLength prefixLength := int32(part.prefixLength)
if sidx, eidx := pfun(caseSensitive, forward, part.text, pattern); sidx >= 0 { if res := pfun(caseSensitive, forward, part.text, pattern); res.Start >= 0 {
return sidx + prefixLength, eidx + prefixLength, part.trimLength var sidx int32 = res.Start + prefixLength
var eidx int32 = res.End + prefixLength
return Offset{sidx, eidx, int32(part.trimLength)}, res.Bonus
} }
} }
return -1, -1, -1 // math.MaxUint16 // TODO: math.MaxUint16
return Offset{-1, -1, -1}, 0.0
} }

View File

@@ -70,10 +70,10 @@ func TestExact(t *testing.T) {
clearPatternCache() clearPatternCache()
pattern := BuildPattern(true, true, CaseSmart, true, pattern := BuildPattern(true, true, CaseSmart, true,
[]Range{}, Delimiter{}, []rune("'abc")) []Range{}, Delimiter{}, []rune("'abc"))
sidx, eidx := algo.ExactMatchNaive( res := algo.ExactMatchNaive(
pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.termSets[0][0].text) pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.termSets[0][0].text)
if sidx != 7 || eidx != 10 { if res.Start != 7 || res.End != 10 {
t.Errorf("%s / %d / %d", pattern.termSets, sidx, eidx) t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
} }
} }
@@ -82,11 +82,11 @@ func TestEqual(t *testing.T) {
clearPatternCache() clearPatternCache()
pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$")) pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int, eidxExpected int) { match := func(str string, sidxExpected int32, eidxExpected int32) {
sidx, eidx := algo.EqualMatch( res := algo.EqualMatch(
pattern.caseSensitive, pattern.forward, []rune(str), pattern.termSets[0][0].text) pattern.caseSensitive, pattern.forward, []rune(str), pattern.termSets[0][0].text)
if sidx != sidxExpected || eidx != eidxExpected { if res.Start != sidxExpected || res.End != eidxExpected {
t.Errorf("%s / %d / %d", pattern.termSets, sidx, eidx) t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
} }
} }
match("ABC", -1, -1) match("ABC", -1, -1)

View File

@@ -26,6 +26,7 @@ type Terminal struct {
prompt string prompt string
reverse bool reverse bool
hscroll bool hscroll bool
hscrollOff int
cx int cx int
cy int cy int
offset int offset int
@@ -210,6 +211,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
prompt: opts.Prompt, prompt: opts.Prompt,
reverse: opts.Reverse, reverse: opts.Reverse,
hscroll: opts.Hscroll, hscroll: opts.Hscroll,
hscrollOff: opts.HscrollOff,
cx: len(input), cx: len(input),
cy: 0, cy: 0,
offset: 0, offset: 0,
@@ -556,11 +558,9 @@ func trimLeft(runes []rune, width int) ([]rune, int32) {
} }
func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) { func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) {
var maxe int32 var maxe int
for _, offset := range item.offsets { for _, offset := range item.offsets {
if offset[1] > maxe { maxe = util.Max(maxe, int(offset[1]))
maxe = offset[1]
}
} }
// Overflow // Overflow
@@ -568,6 +568,7 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
copy(text, item.text) copy(text, item.text)
offsets := item.colorOffsets(col2, bold, current) offsets := item.colorOffsets(col2, bold, current)
maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3] maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3]
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
fullWidth := displayWidth(text) fullWidth := displayWidth(text)
if fullWidth > maxWidth { if fullWidth > maxWidth {
if t.hscroll { if t.hscroll {
@@ -710,7 +711,9 @@ func (t *Terminal) rubout(pattern string) {
} }
func keyMatch(key int, event C.Event) bool { func keyMatch(key int, event C.Event) bool {
return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ return event.Type == key ||
event.Type == C.Rune && int(event.Char) == key-C.AltZ ||
event.Type == C.Mouse && key == C.DoubleClick && event.MouseEvent.Double
} }
func quoteEntry(entry string) string { func quoteEntry(entry string) string {
@@ -1122,15 +1125,7 @@ func (t *Terminal) constrain() {
diffpos := t.cy - t.offset diffpos := t.cy - t.offset
t.cy = util.Constrain(t.cy, 0, count-1) t.cy = util.Constrain(t.cy, 0, count-1)
t.offset = util.Constrain(t.offset, t.cy-height+1, t.cy)
if t.cy > t.offset+(height-1) {
// Ceil
t.offset = t.cy - (height - 1)
} else if t.offset > t.cy {
// Floor
t.offset = t.cy
}
// Adjustment // Adjustment
if count-t.offset < height { if count-t.offset < height {
t.offset = util.Max(0, count-height) t.offset = util.Max(0, count-height)

View File

@@ -21,6 +21,14 @@ func Max(first int, items ...int) int {
return max return max
} }
// Min returns the smallest integer
func Min(first int, second int) int {
if first <= second {
return first
}
return second
}
// Min32 returns the smallest 32-bit integer // Min32 returns the smallest 32-bit integer
func Min32(first int32, second int32) int32 { func Min32(first int32, second int32) int32 {
if first <= second { if first <= second {

View File

@@ -207,13 +207,13 @@ class TestGoFZF < TestBase
tmux.send_keys '99', 'C-a', '1', 'C-f', '3', 'C-b', 'C-h', 'C-u', 'C-e', 'C-y', 'C-k', 'Tab', 'BTab' tmux.send_keys '99', 'C-a', '1', 'C-f', '3', 'C-b', 'C-h', 'C-u', 'C-e', 'C-y', 'C-k', 'Tab', 'BTab'
tmux.until { |lines| lines[-2] == ' 856/100000' } tmux.until { |lines| lines[-2] == ' 856/100000' }
lines = tmux.capture lines = tmux.capture
assert_equal '> 1391', lines[-4] assert_equal '> 3910', lines[-4]
assert_equal ' 391', lines[-3] assert_equal ' 391', lines[-3]
assert_equal ' 856/100000', lines[-2] assert_equal ' 856/100000', lines[-2]
assert_equal '> 391', lines[-1] assert_equal '> 391', lines[-1]
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal '1391', readonce.chomp assert_equal '3910', readonce.chomp
end end
def test_fzf_default_command def test_fzf_default_command
@@ -357,7 +357,7 @@ class TestGoFZF < TestBase
tmux.send_keys :BTab, :BTab, :BTab tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') } tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal ['5', '5', '15', '25'], readonce.split($/) assert_equal ['5', '5', '50', '51'], readonce.split($/)
end end
end end
@@ -378,7 +378,7 @@ class TestGoFZF < TestBase
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == '>' } tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 'C-K', :Enter tmux.send_keys 'C-K', :Enter
assert_equal ['1919'], readonce.split($/) assert_equal ['9090'], readonce.split($/)
end end
def test_tac def test_tac
@@ -394,6 +394,7 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys '99' tmux.send_keys '99'
tmux.until { |lines| lines[-2].include? '28/1000' }
tmux.send_keys :BTab, :BTab, :BTab tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') } tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter tmux.send_keys :Enter
@@ -413,11 +414,12 @@ class TestGoFZF < TestBase
def test_expect def test_expect
test = lambda do |key, feed, expected = key| test = lambda do |key, feed, expected = key|
tmux.send_keys "seq 1 100 | #{fzf :expect, key}", :Enter tmux.send_keys "seq 1 100 | #{fzf :expect, key}; sync", :Enter
tmux.until { |lines| lines[-2].include? '100/100' } tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys '55' tmux.send_keys '55'
tmux.until { |lines| lines[-2].include? '1/100' } tmux.until { |lines| lines[-2].include? '1/100' }
tmux.send_keys *feed tmux.send_keys *feed
tmux.prepare
assert_equal [expected, '55'], readonce.split($/) assert_equal [expected, '55'], readonce.split($/)
end end
test.call 'ctrl-t', 'C-T' test.call 'ctrl-t', 'C-T'
@@ -709,10 +711,10 @@ class TestGoFZF < TestBase
# len(1 ~ 2) # len(1 ~ 2)
output = [ output = [
"apple ui bottle 2",
"app ic bottle 4", "app ic bottle 4",
"apple juice bottle 1",
"app ice bottle 3", "app ice bottle 3",
"apple ui bottle 2",
"apple juice bottle 1",
] ]
assert_equal output, `#{FZF} -fai -n1..2 < #{tempname}`.split($/) assert_equal output, `#{FZF} -fai -n1..2 < #{tempname}`.split($/)
@@ -727,9 +729,9 @@ class TestGoFZF < TestBase
# len(2) # len(2)
output = [ output = [
"apple ui bottle 2",
"app ic bottle 4", "app ic bottle 4",
"app ice bottle 3", "app ice bottle 3",
"apple ui bottle 2",
"apple juice bottle 1", "apple juice bottle 1",
] ]
assert_equal output, `#{FZF} -fi -n2 < #{tempname}`.split($/) assert_equal output, `#{FZF} -fi -n2 < #{tempname}`.split($/)
@@ -877,15 +879,19 @@ class TestGoFZF < TestBase
def test_execute_multi def test_execute_multi
output = '/tmp/fzf-test-execute-multi' output = '/tmp/fzf-test-execute-multi'
opts = %[--multi --bind \\"alt-a:execute-multi(echo '[{}], @{}@' >> #{output})\\"] opts = %[--multi --bind \\"alt-a:execute-multi(echo '[{}], @{}@' >> #{output}; sync)\\"]
tmux.send_keys "seq 100 | #{fzf opts}", :Enter tmux.send_keys "seq 100 | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' } tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys :Escape, :a tmux.send_keys :Escape, :a
tmux.until { |lines| lines[-2].include? '/100' }
tmux.send_keys :BTab, :BTab, :BTab tmux.send_keys :BTab, :BTab, :BTab
tmux.send_keys :Escape, :a tmux.send_keys :Escape, :a
tmux.until { |lines| lines[-2].include? '/100' }
tmux.send_keys :Tab, :Tab tmux.send_keys :Tab, :Tab
tmux.send_keys :Escape, :a tmux.send_keys :Escape, :a
tmux.until { |lines| lines[-2].include? '/100' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.prepare
readonce readonce
assert_equal ['["1"], @"1"@', '["1" "2" "3"], @"1" "2" "3"@', '["1" "2" "4"], @"1" "2" "4"@'], assert_equal ['["1"], @"1"@', '["1" "2" "3"], @"1" "2" "3"@', '["1" "2" "4"], @"1" "2" "4"@'],
File.readlines(output).map(&:chomp) File.readlines(output).map(&:chomp)
@@ -897,13 +903,15 @@ class TestGoFZF < TestBase
# Custom script to use as $SHELL # Custom script to use as $SHELL
output = tempname + '.out' output = tempname + '.out'
File.unlink output rescue nil File.unlink output rescue nil
writelines tempname, ['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"] writelines tempname, ['#!/usr/bin/env bash', "echo $1 / $2 > #{output}", "sync"]
system "chmod +x #{tempname}" system "chmod +x #{tempname}"
tmux.send_keys "echo foo | SHELL=#{tempname} fzf --bind 'enter:execute:{}bar'", :Enter tmux.send_keys "echo foo | SHELL=#{tempname} fzf --bind 'enter:execute:{}bar'", :Enter
tmux.until { |lines| lines[-2].include? '1/1' } tmux.until { |lines| lines[-2].include? '1/1' }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-2].include? '1/1' }
tmux.send_keys 'C-c' tmux.send_keys 'C-c'
tmux.prepare
assert_equal ['-c / "foo"bar'], File.readlines(output).map(&:chomp) assert_equal ['-c / "foo"bar'], File.readlines(output).map(&:chomp)
ensure ensure
File.unlink output rescue nil File.unlink output rescue nil
@@ -935,12 +943,12 @@ class TestGoFZF < TestBase
lines[-2].include?('/90') && lines[-2].include?('/90') &&
lines[-3] == ' 1' && lines[-3] == ' 1' &&
lines[-4] == ' 2' && lines[-4] == ' 2' &&
lines[-13] == '> 15' lines[-13] == '> 50'
end end
tmux.send_keys :Down tmux.send_keys :Down
end end
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal '15', readonce.chomp assert_equal '50', readonce.chomp
end end
def test_header_lines_reverse def test_header_lines_reverse
@@ -950,12 +958,12 @@ class TestGoFZF < TestBase
lines[1].include?('/90') && lines[1].include?('/90') &&
lines[2] == ' 1' && lines[2] == ' 1' &&
lines[3] == ' 2' && lines[3] == ' 2' &&
lines[12] == '> 15' lines[12] == '> 50'
end end
tmux.send_keys :Up tmux.send_keys :Up
end end
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal '15', readonce.chomp assert_equal '50', readonce.chomp
end end
def test_header_lines_overflow def test_header_lines_overflow
@@ -1137,10 +1145,22 @@ class TestGoFZF < TestBase
`seq 10 | #{FZF} -f '1 | !1'`.lines.map(&:chomp) `seq 10 | #{FZF} -f '1 | !1'`.lines.map(&:chomp)
end end
def test_hscroll_off
writelines tempname, ['=' * 10000 + '0123456789']
[0, 3, 6].each do |off|
tmux.prepare
tmux.send_keys "#{FZF} --hscroll-off=#{off} -q 0 < #{tempname}", :Enter
tmux.until { |lines| lines[-3].end_with?((0..off).to_a.join + '..') }
tmux.send_keys '9'
tmux.until { |lines| lines[-3].end_with? '789' }
tmux.send_keys :Enter
end
end
private private
def writelines path, lines def writelines path, lines
File.unlink path while File.exists? path File.unlink path while File.exists? path
File.open(path, 'w') { |f| f << lines.join($/) } File.open(path, 'w') { |f| f << lines.join($/) + $/ }
end end
end end
@@ -1300,6 +1320,7 @@ module CompletionTest
lines[-2].include?('100/') && lines[-2].include?('100/') &&
lines[-3].include?('/tmp/fzf-test/.hidden-') lines[-3].include?('/tmp/fzf-test/.hidden-')
end end
tmux.send_keys :Enter
ensure ensure
['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f| ['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f|
FileUtils.rm_rf File.expand_path(f) FileUtils.rm_rf File.expand_path(f)
@@ -1355,23 +1376,42 @@ module CompletionTest
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
lines[-1] == "kill #{pid}" lines[-1] == "kill #{pid}"
end end
def test_custom_completion
tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter
tmux.prepare
tmux.send_keys 'ls /tmp/**', :Tab, pane: 0
tmux.until(1) { |lines| lines.item_count == 11 }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until(1) { |lines| lines[-2].include? '(3)' }
tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1] == "ls /tmp 1 2"
end
end
ensure ensure
Process.kill 'KILL', pid.to_i rescue nil if pid Process.kill 'KILL', pid.to_i rescue nil if pid
end end
def test_custom_completion
tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter
tmux.prepare
tmux.send_keys 'ls /tmp/**', :Tab, pane: 0
tmux.until(1) { |lines| lines.item_count == 11 }
tmux.send_keys :BTab, :BTab, :BTab
tmux.until(1) { |lines| lines[-2].include? '(3)' }
tmux.send_keys :Enter
tmux.until do |lines|
tmux.send_keys 'C-L'
lines[-1] == "ls /tmp 1 2"
end
end
def test_unset_completion
tmux.send_keys 'export FOO=BAR', :Enter
tmux.prepare
# Using tmux
tmux.send_keys 'unset FOO**', :Tab, pane: 0
tmux.until(1) { |lines| lines[-2].include? ' 1/' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == 'unset FOO' }
tmux.send_keys 'C-c'
# FZF_TMUX=0
new_shell
tmux.send_keys 'unset FOO**', :Tab
tmux.until { |lines| lines[-2].include? ' 1/' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == 'unset FOO' }
end
end end
class TestBash < TestBase class TestBash < TestBase