Compare commits

..

41 Commits

Author SHA1 Message Date
Junegunn Choi
c4c92142a6 0.13.4 2016-08-14 18:10:21 +09:00
Junegunn Choi
d4b6338102 Lint 2016-08-14 17:51:34 +09:00
Junegunn Choi
8df7d962e6 Improve rendering time of long lines 2016-08-14 17:44:11 +09:00
Junegunn Choi
41e916a511 [perf] evaluateBonus can start from sidx - 1 2016-08-14 11:58:47 +09:00
Junegunn Choi
d9c8a9a880 [perf] Remove memory copy when using string delimiter 2016-08-14 04:30:55 +09:00
Junegunn Choi
ddc7bb9064 [perf] Optimize AWK-style tokenizer for --nth
Approx. 50% less memory footprint and 40% improvement in query time
2016-08-14 02:19:29 +09:00
Junegunn Choi
1d4057c209 [perf] Avoid allocating rune array for ascii string
In the best case (all ascii), this reduces the memory footprint by 60%
and the response time by 15% to 20%. In the worst case (every line has
non-ascii characters), 3 to 4% overhead is observed.
2016-08-14 00:41:30 +09:00
Junegunn Choi
822b86942c [test] Clear environment variables 2016-08-13 19:26:36 +09:00
Junegunn Choi
1e74dbb937 :hidden property of previous --preview-window should be cleared
Fix #636. Patch suggested by @edi9999.
2016-08-12 01:16:59 +09:00
Junegunn Choi
7cef92fffe [vim] Delete fzf buffer even when exit status is non-zero
Fix #183
2016-08-02 03:30:17 +09:00
Junegunn Choi
42e4992f06 [vim] Make sure to delete fzf buffer
Close junegunn/fzf.vim#173 and #630
2016-08-02 02:25:02 +09:00
Junegunn Choi
a6066175c6 Merge pull request #630 from kassio/master
Remove `name` option from `termopen`.
2016-07-30 19:05:43 +09:00
Kassio Borges
27444d6b1e Remove name option from termopen.
`termopen` no longer accepts a `name` option, instead we should suffix the
command with `;#NAME`.
2016-07-29 11:10:46 +01:00
Junegunn Choi
d6a99c0391 [vim] v:shell_error can change around redraw!
Patch suggested by Mariusz Atamańczuk
2016-07-28 01:41:11 +09:00
Junegunn Choi
f787f7e651 [vim] Add fzf#wrap helper function
Close #627
2016-07-26 02:37:12 +09:00
Junegunn Choi
a7c9c08371 [vim] Make :FZF command configurable with g:fzf_layout
To make it consistent with the other commands in fzf.vim
2016-07-21 01:47:08 +09:00
Junegunn Choi
fccc93176b 0.13.3 2016-07-16 01:06:53 +09:00
Junegunn Choi
6439a138fe [install] Build fzf if prebuilt binary doesn't work
Close #617
2016-07-16 00:36:35 +09:00
Junegunn Choi
a9a29dff4f Fix duplicate rendering of the last line in preview window 2016-07-15 23:24:14 +09:00
Junegunn Choi
6a52f8b8dd [zsh-completion] setopt localoptions noksh_arrays
Close #607
2016-07-15 01:26:29 +09:00
Junegunn Choi
a1049328d6 [vim] Adjust split size when --header option is set
Close #622
2016-07-14 13:35:18 +09:00
Junegunn Choi
5c2b96bd00 [vim] Fix error with multi-line $FZF_DEFAULT_COMMAND
Close #620
2016-07-13 13:15:14 +09:00
Junegunn Choi
c36413fdf6 [zsh] Suppress error message when pipefail is not supported
Close #615
2016-07-11 17:47:41 +09:00
Junegunn Choi
52cf5af91c [test] Fix test failure on Travis CI
No guarantee in the order in which files are listed
2016-07-10 15:44:44 +09:00
Junegunn Choi
3a4e053af7 [bash] Fall back to send-keys if named paste buffer is not supported
Related: #616
2016-07-10 15:21:28 +09:00
Junegunn Choi
049bc9ec68 [fzf-tmux] Add man page 2016-07-10 14:44:00 +09:00
Junegunn Choi
b461a555b8 [fzf-tmux] Add --version and --help flags 2016-07-10 14:41:06 +09:00
Junegunn Choi
0f87b2d1e1 [fzf-tmux] Use double brackets
For consistency and (negligible) performance improvement
2016-07-10 14:34:29 +09:00
Junegunn Choi
0fb5b76c0d [fzf-tmux] Fail fast if fzf excutable is not found 2016-07-10 14:28:58 +09:00
Junegunn Choi
0c918dd87a Merge pull request #616 from seanlaguna/master
Use tmux buffers for sending output to preserve character encoding
2016-07-10 12:29:32 +09:00
Junegunn Choi
05299a0fee [test] Use tmux buffer in unicode test cases
Related #616
2016-07-10 12:27:01 +09:00
Sean
b36b0a91f5 use tmux buffers for sending output to preserve character encoding 2016-07-09 09:47:20 -05:00
Junegunn Choi
6081eac58a [shell] Suppress alias/function expansion
Close #611
2016-07-07 01:40:14 +09:00
Junegunn Choi
942ba749c7 [vim] Restore working directory even when new window is opened
Close #612
2016-07-06 13:31:04 +09:00
Junegunn Choi
f941012687 Merge pull request #610 from eigengrau/master
[zsh] Re-initialize zle when widgets finish
2016-07-05 21:50:43 +09:00
Sebastian Reuße
fed5e5d5af [zsh] Re-initialize zle when widgets finish
zle automatically calls zle-line-init when it starts to read a new line. Many
Zsh setups use this hook to set the terminal into application mode, since this
will then allow defining keybinds based on the $terminfo variable (the escape
codes in said variable are only valid in application mode).

However, fzf resets the terminal into raw mode, rendering $terminfo values
invalid once the widget has finished. Accordingly, keyboard bindings defined
via $terminfo won’t work anymore.

This fixes the issue by calling zle-line-init when widgets finish. Care is taken
to not call this widget when it is undefined.

Fixes #279
2016-07-05 08:57:11 +02:00
Junegunn Choi
b864885753 [install] Make sure to unset pipefail 2016-07-04 13:05:26 +09:00
Junegunn Choi
64747c2324 [install] Fix error in install script
Close #608
2016-07-04 13:00:30 +09:00
Junegunn Choi
34965edcda [install] Fall back to wget if curl failed
Close #605
2016-07-04 01:41:43 +09:00
Junegunn Choi
bd4377084d Merge pull request #601 from blueyed/zsh-ret-for-fzf-file-widget
zsh: pass through exit code from fzf with fzf-file-widget
2016-06-18 23:17:10 +09:00
Daniel Hahler
38a2076b89 zsh: pass through exit code from widgets
This allows to have a custom widget like the following, which would
additionally accept the line, but only in case of entries being
selected:

    fzf-file-widget-with-accept() {
      zle fzf-file-widget
      if [[ "$?" == 0 ]] && (( $#BUFFER )); then
        zle accept-line
      fi
    }
    zle     -N   fzf-file-widget-with-accept
    bindkey '\e^T' fzf-file-widget-with-accept

With this `<C-a>t` will launch fzf, and simulate the pressing of "Enter"
afterwards.
2016-06-16 20:20:29 +02:00
35 changed files with 991 additions and 432 deletions

View File

@@ -1,6 +1,18 @@
CHANGELOG CHANGELOG
========= =========
0.13.4
------
- Performance optimization
- Memory footprint for ascii string is reduced by 60%
- 15 to 20% improvement of query performance
- Up to 45% better performance of `--nth` with non-regex delimiters
- Fixed invalid handling of `hidden` property of `--preview-window`
0.13.3
------
- Fixed duplicate rendering of the last line in preview window
0.13.2 0.13.2
------ ------
- Fixed race condition where preview window is not properly cleared - Fixed race condition where preview window is not properly cleared

View File

@@ -320,10 +320,10 @@ customization.
[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim) [fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim)
#### `fzf#run([options])` #### `fzf#run`
For more advanced uses, you can use `fzf#run()` function with the following For more advanced uses, you can use `fzf#run([options])` function with the
options. following options.
| Option name | Type | Description | | Option name | Type | Description |
| -------------------------- | ------------- | ---------------------------------------------------------------- | | -------------------------- | ------------- | ---------------------------------------------------------------- |
@@ -342,6 +342,17 @@ options.
Examples can be found on [the wiki Examples can be found on [the wiki
page](https://github.com/junegunn/fzf/wiki/Examples-(vim)). page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
#### `fzf#wrap`
`fzf#wrap(name string, [opts dict, [fullscreen boolean]])` is a helper
function that decorates the options dictionary so that it understands
`g:fzf_layout`, `g:fzf_action`, and `g:fzf_history_dir` like `:FZF`.
```vim
command! -bang MyStuff
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0))
```
Tips Tips
---- ----

View File

@@ -2,25 +2,51 @@
# fzf-tmux: starts fzf in a tmux pane # fzf-tmux: starts fzf in a tmux pane
# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] # usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
fail() {
>&2 echo "$1"
exit 2
}
fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
[[ -x "$fzf" ]] || fail 'fzf executable not found'
args=() args=()
opt="" opt=""
skip="" skip=""
swap="" swap=""
close="" close=""
term="" term=""
[ -n "$LINES" ] && lines=$LINES || lines=$(tput lines) [[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines)
while [ $# -gt 0 ]; do
help() {
>&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
Layout
-u [HEIGHT[%]] Split above (up)
-d [HEIGHT[%]] Split below (down)
-l [WIDTH[%]] Split left
-r [WIDTH[%]] Split right
(default: -d 50%)
'
exit
}
while [[ $# -gt 0 ]]; do
arg="$1" arg="$1"
case "$arg" in shift
[[ -z "$skip" ]] && case "$arg" in
-) -)
term=1 term=1
;; ;;
--help)
help
;;
--version)
echo "fzf-tmux (with fzf $("$fzf" --version))"
exit
;;
-w*|-h*|-d*|-u*|-r*|-l*) -w*|-h*|-d*|-u*|-r*|-l*)
if [ -n "$skip" ]; then
args+=("$1")
shift
continue
fi
if [[ "$arg" =~ ^.[lrw] ]]; then if [[ "$arg" =~ ^.[lrw] ]]; then
opt="-h" opt="-h"
if [[ "$arg" =~ ^.l ]]; then if [[ "$arg" =~ ^.l ]]; then
@@ -36,35 +62,33 @@ while [ $# -gt 0 ]; do
close="; tmux swap-pane -D" close="; tmux swap-pane -D"
fi fi
fi fi
if [ ${#arg} -gt 2 ]; then if [[ ${#arg} -gt 2 ]]; then
size="${arg:2}" size="${arg:2}"
else else
shift
if [[ "$1" =~ ^[0-9]+%?$ ]]; then if [[ "$1" =~ ^[0-9]+%?$ ]]; then
size="$1" size="$1"
else
[ -n "$1" -a "$1" != "--" ] && args+=("$1")
shift shift
else
continue continue
fi fi
fi fi
if [[ "$size" =~ %$ ]]; then if [[ "$size" =~ %$ ]]; then
size=${size:0:((${#size}-1))} size=${size:0:((${#size}-1))}
if [ -n "$swap" ]; then if [[ -n "$swap" ]]; then
opt="$opt -p $(( 100 - size ))" opt="$opt -p $(( 100 - size ))"
else else
opt="$opt -p $size" opt="$opt -p $size"
fi fi
else else
if [ -n "$swap" ]; then if [[ -n "$swap" ]]; then
if [[ "$arg" =~ ^.l ]]; then if [[ "$arg" =~ ^.l ]]; then
[ -n "$COLUMNS" ] && max=$COLUMNS || max=$(tput cols) [[ -n "$COLUMNS" ]] && max=$COLUMNS || max=$(tput cols)
else else
max=$lines max=$lines
fi fi
size=$(( max - size )) size=$(( max - size ))
[ $size -lt 0 ] && size=0 [[ $size -lt 0 ]] && size=0
opt="$opt -l $size" opt="$opt -l $size"
else else
opt="$opt -l $size" opt="$opt -l $size"
@@ -75,16 +99,17 @@ while [ $# -gt 0 ]; do
# "--" can be used to separate fzf-tmux options from fzf options to # "--" can be used to separate fzf-tmux options from fzf options to
# avoid conflicts # avoid conflicts
skip=1 skip=1
continue
;; ;;
*) *)
args+=("$1") args+=("$arg")
;; ;;
esac esac
shift [[ -n "$skip" ]] && args+=("$arg")
done done
if ! [ -n "$TMUX" -a "$lines" -gt 15 ]; then if [[ -z "$TMUX" ]] || [[ "$lines" -le 15 ]]; then
fzf "${args[@]}" "$fzf" "${args[@]}"
exit $? exit $?
fi fi
@@ -108,7 +133,7 @@ cleanup() {
rm -f $argsf $fifo1 $fifo2 $fifo3 rm -f $argsf $fifo1 $fifo2 $fifo3
# Remove temp window if we were zoomed # Remove temp window if we were zoomed
if [ -n "$zoomed" ]; then if [[ -n "$zoomed" ]]; then
tmux swap-pane -t $original_window \; \ tmux swap-pane -t $original_window \; \
select-window -t $original_window \; \ select-window -t $original_window \; \
kill-window -t $tmp_window \; \ kill-window -t $tmp_window \; \
@@ -117,16 +142,9 @@ cleanup() {
} }
trap cleanup EXIT SIGINT SIGTERM trap cleanup EXIT SIGINT SIGTERM
fail() {
>&2 echo "$1"
exit 2
}
fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
[ -x "$fzf" ] || fail "fzf executable not found"
envs="env TERM=$TERM " envs="env TERM=$TERM "
[ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
[ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" [[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
mkfifo -m o+w $fifo2 mkfifo -m o+w $fifo2
mkfifo -m o+w $fifo3 mkfifo -m o+w $fifo3
@@ -141,7 +159,7 @@ for arg in "${args[@]}"; do
opts="$opts \"$arg\"" opts="$opts \"$arg\""
done done
if [ -n "$term" -o -t 0 ]; then if [[ -n "$term" ]] || [[ -t 0 ]]; then
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf
tmux set-window-option synchronize-panes off \;\ tmux set-window-option synchronize-panes off \;\
set-window-option remain-on-exit off \;\ set-window-option remain-on-exit off \;\

59
install
View File

@@ -2,13 +2,14 @@
set -u set -u
[[ "$@" =~ --pre ]] && version=0.13.2 pre=1 || [[ "$@" =~ --pre ]] && version=0.13.4 pre=1 ||
version=0.13.2 pre=0 version=0.13.4 pre=0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2
binary_arch= binary_arch=
allow_legacy=
help() { help() {
cat << EOF cat << EOF
@@ -37,6 +38,7 @@ for opt in "$@"; do
auto_completion=1 auto_completion=1
key_bindings=1 key_bindings=1
update_config=1 update_config=1
allow_legacy=1
;; ;;
--key-bindings) key_bindings=1 ;; --key-bindings) key_bindings=1 ;;
--no-key-bindings) key_bindings=0 ;; --no-key-bindings) key_bindings=0 ;;
@@ -109,6 +111,14 @@ link_fzf_in_path() {
return 1 return 1
} }
try_curl() {
command -v curl > /dev/null && curl -fL $1 | tar -xz
}
try_wget() {
command -v wget > /dev/null && wget -O - $1 | tar -xz
}
download() { download() {
echo "Downloading bin/fzf ..." echo "Downloading bin/fzf ..."
if [ $pre = 0 ]; then if [ $pre = 0 ]; then
@@ -128,14 +138,13 @@ 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 command -v curl > /dev/null; then set -o pipefail
curl -fL $url | tar -xz if ! (try_curl $url || try_wget $url); then
elif command -v wget > /dev/null; then set +o pipefail
wget -O - $url | tar -xz binary_error="Failed to download with curl and wget"
else
binary_error="curl or wget not found"
return return
fi fi
set +o pipefail
if [ ! -f $1 ]; then if [ ! -f $1 ]; then
binary_error="Failed to download ${1}" binary_error="Failed to download ${1}"
@@ -158,6 +167,9 @@ case "$archi" in
esac esac
install_ruby_fzf() { install_ruby_fzf() {
if [ -z "$allow_legacy" ]; then
ask "Do you want to install legacy Ruby version instead?" && exit 1
fi
echo "Installing legacy Ruby version ..." echo "Installing legacy Ruby version ..."
# ruby executable # ruby executable
@@ -229,26 +241,25 @@ cd "$fzf_base"
if [ -n "$binary_error" ]; then if [ -n "$binary_error" ]; then
if [ $binary_available -eq 0 ]; then if [ $binary_available -eq 0 ]; then
echo "No prebuilt binary for $archi ..." echo "No prebuilt binary for $archi ..."
if command -v go > /dev/null; then else
echo -n "Building binary (go get -u github.com/junegunn/fzf/src/fzf) ... " echo " - $binary_error !!!"
if [ -z "${GOPATH-}" ]; then fi
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath" if command -v go > /dev/null; then
mkdir -p "$GOPATH" echo -n "Building binary (go get -u github.com/junegunn/fzf/src/fzf) ... "
fi if [ -z "${GOPATH-}" ]; then
if go get -u github.com/junegunn/fzf/src/fzf; then export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
echo "OK" mkdir -p "$GOPATH"
cp "$GOPATH/bin/fzf" "$fzf_base/bin/" fi
else if go get -u github.com/junegunn/fzf/src/fzf; then
echo "Failed to build binary ..." echo "OK"
install_ruby_fzf cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
fi
else else
echo "go executable not found. Cannot build binary ..." echo "Failed to build binary ..."
install_ruby_fzf install_ruby_fzf
fi fi
else else
echo " - $binary_error !!!" echo "go executable not found. Cannot build binary ..."
exit 1 install_ruby_fzf
fi fi
fi fi

54
man/man1/fzf-tmux.1 Normal file
View File

@@ -0,0 +1,54 @@
.ig
The MIT License (MIT)
Copyright (c) 2016 Junegunn Choi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf-tmux 1 "Aug 2016" "fzf 0.13.4" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane
.SH SYNOPSIS
.B fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
.SH DESCRIPTION
fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane. It is
designed to work just like fzf except that it does not take up the whole
screen. You can safely use fzf-tmux instead of fzf in your scripts as the extra
options will be silently ignored if you're not on tmux.
.SH OPTIONS
.SS Layout
(default: \fB-d 50%\fR)
.TP
.B "-u [height[%]]"
Split above (up)
.TP
.B "-d [height[%]]"
Split below (down)
.TP
.B "-l [width[%]]"
Split left
.TP
.B "-r [width[%]]"
Split right

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 "Jun 2016" "fzf 0.13.2" "fzf - a command-line fuzzy finder" .TH fzf 1 "Aug 2016" "fzf 0.13.4" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder

View File

@@ -21,7 +21,8 @@
" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION " OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
let s:default_height = '40%' let s:default_layout = { 'down': '~40%' }
let s:layout_keys = ['window', 'up', 'down', 'left', 'right']
let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf' let s:fzf_go = expand('<sfile>:h:h').'/bin/fzf'
let s:install = expand('<sfile>:h:h').'/install' let s:install = expand('<sfile>:h:h').'/install'
let s:installed = 0 let s:installed = 0
@@ -104,11 +105,106 @@ function! s:warn(msg)
echohl None echohl None
endfunction endfunction
function! s:has_any(dict, keys)
for key in a:keys
if has_key(a:dict, key)
return 1
endif
endfor
return 0
endfunction
function! s:open(cmd, target)
if stridx('edit', a:cmd) == 0 && fnamemodify(a:target, ':p') ==# expand('%:p')
return
endif
execute a:cmd s:escape(a:target)
endfunction
function! s:common_sink(action, lines) abort
if len(a:lines) < 2
return
endif
let key = remove(a:lines, 0)
let cmd = get(a:action, key, 'e')
if len(a:lines) > 1
augroup fzf_swap
autocmd SwapExists * let v:swapchoice='o'
\| call s:warn('fzf: E325: swap file exists: '.expand('<afile>'))
augroup END
endif
try
let empty = empty(expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified
let autochdir = &autochdir
set noautochdir
for item in a:lines
if empty
execute 'e' s:escape(item)
let empty = 0
else
call s:open(cmd, item)
endif
if exists('#BufEnter') && isdirectory(item)
doautocmd BufEnter
endif
endfor
finally
let &autochdir = autochdir
silent! autocmd! fzf_swap
endtry
endfunction
" name string, [opts dict, [fullscreen boolean]]
function! fzf#wrap(name, ...)
if type(a:name) != type('')
throw 'invalid name type: string expected'
endif
let opts = copy(get(a:000, 0, {}))
let bang = get(a:000, 1, 0)
" Layout: g:fzf_layout (and deprecated g:fzf_height)
if bang
for key in s:layout_keys
if has_key(opts, key)
call remove(opts, key)
endif
endfor
elseif !s:has_any(opts, s:layout_keys)
if !exists('g:fzf_layout') && exists('g:fzf_height')
let opts.down = g:fzf_height
else
let opts = extend(opts, get(g:, 'fzf_layout', s:default_layout))
endif
endif
" History: g:fzf_history_dir
let opts.options = get(opts, 'options', '')
if len(get(g:, 'fzf_history_dir', ''))
let dir = expand(g:fzf_history_dir)
if !isdirectory(dir)
call mkdir(dir, 'p')
endif
let opts.options = join(['--history', s:escape(dir.'/'.a:name), opts.options])
endif
" Action: g:fzf_action
if !s:has_any(opts, ['sink', 'sink*'])
let opts._action = get(g:, 'fzf_action', s:default_action)
let opts.options .= ' --expect='.join(keys(opts._action), ',')
function! opts.sink(lines) abort
return s:common_sink(self._action, a:lines)
endfunction
let opts['sink*'] = remove(opts, 'sink')
endif
return opts
endfunction
function! fzf#run(...) abort function! fzf#run(...) abort
try try
let oshell = &shell let oshell = &shell
set shell=sh set shell=sh
if has('nvim') && bufexists('term://*:FZF') if has('nvim') && len(filter(range(1, bufnr('$')), 'bufname(v:val) =~# ";#FZF"'))
call s:warn('FZF is already running!') call s:warn('FZF is already running!')
return [] return []
endif endif
@@ -122,7 +218,9 @@ try
endtry endtry
if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND)
let dict.source = $FZF_DEFAULT_COMMAND let temps.source = tempname()
call writefile(split($FZF_DEFAULT_COMMAND, "\n"), temps.source)
let dict.source = (empty($SHELL) ? 'sh' : $SHELL) . ' ' . s:shellesc(temps.source)
endif endif
if has_key(dict, 'source') if has_key(dict, 'source')
@@ -135,7 +233,7 @@ try
call writefile(source, temps.input) call writefile(source, temps.input)
let prefix = 'cat '.s:shellesc(temps.input).'|' let prefix = 'cat '.s:shellesc(temps.input).'|'
else else
throw 'Invalid source type' throw 'invalid source type'
endif endif
else else
let prefix = '' let prefix = ''
@@ -147,9 +245,9 @@ try
return s:execute_term(dict, command, temps) return s:execute_term(dict, command, temps)
endif endif
let ret = tmux ? s:execute_tmux(dict, command, temps) : s:execute(dict, command, temps) let lines = tmux ? s:execute_tmux(dict, command, temps) : s:execute(dict, command, temps)
call s:popd(dict, ret) call s:callback(dict, lines)
return ret return lines
finally finally
let &shell = oshell let &shell = oshell
endtry endtry
@@ -200,22 +298,17 @@ function! s:pushd(dict)
return 0 return 0
endfunction endfunction
function! s:popd(dict, lines) augroup fzf_popd
" Since anything can be done in the sink function, there is no telling that autocmd!
" the change of the working directory was made by &autochdir setting. autocmd WinEnter * call s:dopopd()
" augroup END
" We use the following heuristic to determine whether to restore CWD:
" - Always restore the current directory when &autochdir is disabled. function! s:dopopd()
" FIXME This makes it impossible to change directory from inside the sink if !exists('w:fzf_prev_dir') || exists('*haslocaldir') && !haslocaldir()
" function when &autochdir is not used. return
" - In case of an error or an interrupt, a:lines will be empty.
" And it will be an array of a single empty string when fzf was finished
" without a match. In these cases, we presume that the change of the
" directory is not expected and should be undone.
if has_key(a:dict, 'prev_dir') &&
\ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0])))
execute 'lcd' s:escape(remove(a:dict, 'prev_dir'))
endif endif
execute 'lcd' s:escape(w:fzf_prev_dir)
unlet w:fzf_prev_dir
endfunction endfunction
function! s:xterm_launcher() function! s:xterm_launcher()
@@ -255,8 +348,9 @@ function! s:execute(dict, command, temps) abort
let command = escaped let command = escaped
endif endif
execute 'silent !'.command execute 'silent !'.command
let exit_status = v:shell_error
redraw! redraw!
return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : [] return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
endfunction endfunction
function! s:execute_tmux(dict, command, temps) abort function! s:execute_tmux(dict, command, temps) abort
@@ -267,8 +361,9 @@ function! s:execute_tmux(dict, command, temps) abort
endif endif
call system(command) call system(command)
let exit_status = v:shell_error
redraw! redraw!
return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : [] return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
endfunction endfunction
function! s:calc_size(max, val, dict) function! s:calc_size(max, val, dict)
@@ -285,6 +380,7 @@ function! s:calc_size(max, val, dict)
let opts = get(a:dict, 'options', '').$FZF_DEFAULT_OPTS let opts = get(a:dict, 'options', '').$FZF_DEFAULT_OPTS
let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2 let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2
let margin += stridx(opts, '--header') > stridx(opts, '--no-header')
return srcsz >= 0 ? min([srcsz + margin, size]) : size return srcsz >= 0 ? min([srcsz + margin, size]) : size
endfunction endfunction
@@ -328,7 +424,7 @@ endfunction
function! s:execute_term(dict, command, temps) abort function! s:execute_term(dict, command, temps) abort
let [ppos, winopts] = s:split(a:dict) let [ppos, winopts] = s:split(a:dict)
let fzf = { 'buf': bufnr('%'), 'ppos': ppos, 'dict': a:dict, 'temps': a:temps, let fzf = { 'buf': bufnr('%'), 'ppos': ppos, 'dict': a:dict, 'temps': a:temps,
\ 'name': 'FZF', 'winopts': winopts, 'command': a:command } \ 'winopts': winopts, 'command': a:command }
function! fzf.switch_back(inplace) function! fzf.switch_back(inplace)
if a:inplace && bufnr('') == self.buf if a:inplace && bufnr('') == self.buf
" FIXME: Can't re-enter normal mode from terminal mode " FIXME: Can't re-enter normal mode from terminal mode
@@ -356,36 +452,67 @@ function! s:execute_term(dict, command, temps) abort
execute self.ppos.win.'wincmd w' execute self.ppos.win.'wincmd w'
endif endif
if bufexists(self.buf)
execute 'bd!' self.buf
endif
if !s:exit_handler(a:code, self.command, 1) if !s:exit_handler(a:code, self.command, 1)
return return
endif endif
call s:pushd(self.dict) call s:pushd(self.dict)
let ret = [] let lines = s:collect(self.temps)
try call s:callback(self.dict, lines)
let ret = s:callback(self.dict, self.temps) call self.switch_back(s:getpos() == self.ppos)
call self.switch_back(s:getpos() == self.ppos)
finally
call s:popd(self.dict, ret)
endtry
endfunction endfunction
call s:pushd(a:dict) try
call termopen(a:command, fzf) if s:present(a:dict, 'dir')
call s:popd(a:dict, []) execute 'lcd' s:escape(a:dict.dir)
endif
call termopen(a:command . ';#FZF', fzf)
finally
if s:present(a:dict, 'dir')
lcd -
endif
endtry
setlocal nospell bufhidden=wipe nobuflisted setlocal nospell bufhidden=wipe nobuflisted
setf fzf setf fzf
startinsert startinsert
return [] return []
endfunction endfunction
function! s:callback(dict, temps) abort function! s:collect(temps) abort
let lines = [] try
try return filereadable(a:temps.result) ? readfile(a:temps.result) : []
if filereadable(a:temps.result) finally
let lines = readfile(a:temps.result) for tf in values(a:temps)
silent! call delete(tf)
endfor
endtry
endfunction
function! s:callback(dict, lines) abort
" Since anything can be done in the sink function, there is no telling that
" the change of the working directory was made by &autochdir setting.
"
" We use the following heuristic to determine whether to restore CWD:
" - Always restore the current directory when &autochdir is disabled.
" FIXME This makes it impossible to change directory from inside the sink
" function when &autochdir is not used.
" - In case of an error or an interrupt, a:lines will be empty.
" And it will be an array of a single empty string when fzf was finished
" without a match. In these cases, we presume that the change of the
" directory is not expected and should be undone.
let popd = has_key(a:dict, 'prev_dir') &&
\ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0])))
if popd
let w:fzf_prev_dir = a:dict.prev_dir
endif
try
if has_key(a:dict, 'sink') if has_key(a:dict, 'sink')
for line in lines for line in a:lines
if type(a:dict.sink) == 2 if type(a:dict.sink) == 2
call a:dict.sink(line) call a:dict.sink(line)
else else
@@ -394,76 +521,36 @@ try
endfor endfor
endif endif
if has_key(a:dict, 'sink*') if has_key(a:dict, 'sink*')
call a:dict['sink*'](lines) call a:dict['sink*'](a:lines)
endif endif
endif catch
if stridx(v:exception, ':E325:') < 0
echoerr v:exception
endif
endtry
for tf in values(a:temps) " We may have opened a new window or tab
silent! call delete(tf) if popd
endfor let w:fzf_prev_dir = a:dict.prev_dir
catch call s:dopopd()
if stridx(v:exception, ':E325:') < 0
echoerr v:exception
endif endif
finally
return lines
endtry
endfunction endfunction
let s:default_action = { let s:default_action = {
\ 'ctrl-m': 'e',
\ 'ctrl-t': 'tab split', \ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split', \ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' } \ 'ctrl-v': 'vsplit' }
function! s:cmd_callback(lines) abort
if empty(a:lines)
return
endif
let key = remove(a:lines, 0)
let cmd = get(s:action, key, 'e')
if len(a:lines) > 1
augroup fzf_swap
autocmd SwapExists * let v:swapchoice='o'
\| call s:warn('fzf: E325: swap file exists: '.expand('<afile>'))
augroup END
endif
try
let empty = empty(expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified
let autochdir = &autochdir
set noautochdir
for item in a:lines
if empty
execute 'e' s:escape(item)
let empty = 0
else
execute cmd s:escape(item)
endif
if exists('#BufEnter') && isdirectory(item)
doautocmd BufEnter
endif
endfor
finally
let &autochdir = autochdir
silent! autocmd! fzf_swap
endtry
endfunction
function! s:cmd(bang, ...) abort function! s:cmd(bang, ...) abort
let s:action = get(g:, 'fzf_action', s:default_action) let args = copy(a:000)
let args = extend(['--expect='.join(keys(s:action), ',')], a:000)
let opts = {} let opts = {}
if len(args) > 0 && isdirectory(expand(args[-1])) if len(args) && isdirectory(expand(args[-1]))
let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g') let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g')
endif endif
if !a:bang call fzf#run(fzf#wrap('FZF', extend({'options': join(args)}, opts), a:bang))
let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height))
endif
call fzf#run(extend({'options': join(args), 'sink*': function('<sid>cmd_callback')}, opts))
endfunction endfunction
command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>) command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>)
let &cpo = s:cpo_save let &cpo = s:cpo_save
unlet s:cpo_save unlet s:cpo_save

View File

@@ -14,7 +14,7 @@
if ! declare -f _fzf_compgen_path > /dev/null; then if ! declare -f _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() { _fzf_compgen_path() {
echo "$1" echo "$1"
\find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ -name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
@@ -22,7 +22,7 @@ fi
if ! declare -f _fzf_compgen_dir > /dev/null; then if ! declare -f _fzf_compgen_dir > /dev/null; then
_fzf_compgen_dir() { _fzf_compgen_dir() {
\find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .svn -prune -o -type d \ -name .git -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
@@ -108,7 +108,7 @@ _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 | command grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)"
source "${BASH_SOURCE[0]}" source "${BASH_SOURCE[0]}"
return $ret return $ret
fi fi
@@ -213,16 +213,16 @@ _fzf_complete_kill() {
_fzf_complete_telnet() { _fzf_complete_telnet() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
awk '{if (length($2) > 0) {print $2}}' | sort -u awk '{if (length($2) > 0) {print $2}}' | sort -u
) )
} }
_fzf_complete_ssh() { _fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \ cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \
<(\grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(command grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
<(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u awk '{if (length($2) > 0) {print $2}}' | sort -u
) )
} }
@@ -263,8 +263,8 @@ x_cmds="kill ssh telnet unset unalias export"
# Preserve existing completion # Preserve existing completion
if [ "$_fzf_completion_loaded" != '0.11.3' ]; then if [ "$_fzf_completion_loaded" != '0.11.3' ]; then
# Really wish I could use associative array but OSX comes with bash 3.2 :( # Really wish I could use associative array but OSX comes with bash 3.2 :(
eval $(complete | \grep '\-F' | \grep -v _fzf_ | eval $(complete | command grep '\-F' | command grep -v _fzf_ |
\grep -E " ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter) command grep -E " ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter)
export _fzf_completion_loaded=0.11.3 export _fzf_completion_loaded=0.11.3
fi fi

View File

@@ -14,7 +14,7 @@
if ! declare -f _fzf_compgen_path > /dev/null; then if ! declare -f _fzf_compgen_path > /dev/null; then
_fzf_compgen_path() { _fzf_compgen_path() {
echo "$1" echo "$1"
\find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ -name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
@@ -22,7 +22,7 @@ fi
if ! declare -f _fzf_compgen_dir > /dev/null; then if ! declare -f _fzf_compgen_dir > /dev/null; then
_fzf_compgen_dir() { _fzf_compgen_dir() {
\find -L "$1" \ command find -L "$1" \
-name .git -prune -o -name .svn -prune -o -type d \ -name .git -prune -o -name .svn -prune -o -type d \
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
} }
@@ -58,6 +58,7 @@ __fzf_generic_path_completion() {
LBUFFER="$lbuf$matches$tail" LBUFFER="$lbuf$matches$tail"
fi fi
zle redisplay zle redisplay
typeset -f zle-line-init >/dev/null && zle zle-line-init
break break
fi fi
dir=$(dirname "$dir") dir=$(dirname "$dir")
@@ -76,7 +77,7 @@ _fzf_dir_completion() {
} }
_fzf_feed_fifo() ( _fzf_feed_fifo() (
rm -f "$1" command rm -f "$1"
mkfifo "$1" mkfifo "$1"
cat <&0 > "$1" & cat <&0 > "$1" &
) )
@@ -97,21 +98,22 @@ _fzf_complete() {
LBUFFER="$lbuf$matches" LBUFFER="$lbuf$matches"
fi fi
zle redisplay zle redisplay
rm -f "$fifo" typeset -f zle-line-init >/dev/null && zle zle-line-init
command rm -f "$fifo"
} }
_fzf_complete_telnet() { _fzf_complete_telnet() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
awk '{if (length($2) > 0) {print $2}}' | sort -u awk '{if (length($2) > 0) {print $2}}' | sort -u
) )
} }
_fzf_complete_ssh() { _fzf_complete_ssh() {
_fzf_complete '+m' "$@" < <( _fzf_complete '+m' "$@" < <(
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \ cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \
<(\grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(command grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
<(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
awk '{if (length($2) > 0) {print $2}}' | sort -u awk '{if (length($2) > 0) {print $2}}' | sort -u
) )
} }
@@ -136,7 +138,7 @@ _fzf_complete_unalias() {
fzf-completion() { fzf-completion() {
local tokens cmd prefix trigger tail fzf matches lbuf d_cmds local tokens cmd prefix trigger tail fzf matches lbuf d_cmds
setopt localoptions noshwordsplit setopt localoptions noshwordsplit noksh_arrays
# http://zsh.sourceforge.net/FAQ/zshfaq03.html # http://zsh.sourceforge.net/FAQ/zshfaq03.html
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
@@ -161,6 +163,7 @@ fzf-completion() {
LBUFFER="$LBUFFER$matches" LBUFFER="$LBUFFER$matches"
fi fi
zle redisplay zle redisplay
typeset -f zle-line-init >/dev/null && zle zle-line-init
# Trigger sequence given # Trigger sequence given
elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}) d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})

View File

@@ -26,7 +26,7 @@ __fzf_select_tmux__() {
height="-l $height" height="-l $height"
fi fi
tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'"
} }
fzf-file-widget() { fzf-file-widget() {
@@ -52,7 +52,7 @@ __fzf_history__() (
line=$( line=$(
HISTTIMEFORMAT= history | HISTTIMEFORMAT= history |
eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" |
\grep '^ *[0-9]') && command grep '^ *[0-9]') &&
if [[ $- =~ H ]]; then if [[ $- =~ H ]]; then
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
else else

View File

@@ -8,10 +8,13 @@ __fsel() {
-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-"}"
setopt localoptions pipefail 2> /dev/null
eval "$cmd | $(__fzfcmd) -m $FZF_CTRL_T_OPTS" | while read item; do eval "$cmd | $(__fzfcmd) -m $FZF_CTRL_T_OPTS" | while read item; do
echo -n "${(q)item} " echo -n "${(q)item} "
done done
local ret=$?
echo echo
return $ret
} }
__fzfcmd() { __fzfcmd() {
@@ -20,7 +23,10 @@ __fzfcmd() {
fzf-file-widget() { fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)" LBUFFER="${LBUFFER}$(__fsel)"
local ret=$?
zle redisplay zle redisplay
typeset -f zle-line-init >/dev/null && zle zle-line-init
return $ret
} }
zle -N fzf-file-widget zle -N fzf-file-widget
bindkey '^T' fzf-file-widget bindkey '^T' fzf-file-widget
@@ -29,8 +35,12 @@ bindkey '^T' fzf-file-widget
fzf-cd-widget() { fzf-cd-widget() {
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}"
setopt localoptions pipefail 2> /dev/null
cd "${$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS"):-.}" cd "${$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS"):-.}"
local ret=$?
zle reset-prompt zle reset-prompt
typeset -f zle-line-init >/dev/null && zle zle-line-init
return $ret
} }
zle -N fzf-cd-widget zle -N fzf-cd-widget
bindkey '\ec' fzf-cd-widget bindkey '\ec' fzf-cd-widget
@@ -38,8 +48,9 @@ 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
setopt localoptions noglobsubst setopt localoptions noglobsubst pipefail 2> /dev/null
selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") ) selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") )
local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$selected[1] num=$selected[1]
if [ -n "$num" ]; then if [ -n "$num" ]; then
@@ -47,6 +58,8 @@ fzf-history-widget() {
fi fi
fi fi
zle redisplay zle redisplay
typeset -f zle-line-init >/dev/null && zle zle-line-init
return $ret
} }
zle -N fzf-history-widget zle -N fzf-history-widget
bindkey '^R' fzf-history-widget bindkey '^R' fzf-history-widget

View File

@@ -15,11 +15,18 @@ import (
* In short: They try to do as little work as possible. * In short: They try to do as little work as possible.
*/ */
func runeAt(runes []rune, index int, max int, forward bool) rune { func indexAt(index int, max int, forward bool) int {
if forward { if forward {
return runes[index] return index
} }
return runes[max-index-1] return max - index - 1
}
func runeAt(text util.Chars, index int, max int, forward bool) rune {
if forward {
return text.Get(index)
}
return text.Get(max - index - 1)
} }
// Result conatins the results of running a match function. // Result conatins the results of running a match function.
@@ -42,14 +49,14 @@ const (
charNumber charNumber
) )
func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, eidx int) int32 { func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int) int32 {
var bonus int32 var bonus int32
pidx := 0 pidx := 0
lenPattern := len(pattern) lenPattern := len(pattern)
consecutive := false consecutive := false
prevClass := charNonWord prevClass := charNonWord
for index := 0; index < eidx; index++ { for index := util.Max(0, sidx-1); index < eidx; index++ {
char := runes[index] char := text.Get(index)
var class charClass var class charClass
if unicode.IsLower(char) { if unicode.IsLower(char) {
class = charLower class = charLower
@@ -107,7 +114,7 @@ func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, e
} }
// FuzzyMatch performs fuzzy-match // FuzzyMatch performs fuzzy-match
func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { func FuzzyMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
if len(pattern) == 0 { if len(pattern) == 0 {
return Result{0, 0, 0} return Result{0, 0, 0}
} }
@@ -125,11 +132,11 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
sidx := -1 sidx := -1
eidx := -1 eidx := -1
lenRunes := len(runes) lenRunes := text.Length()
lenPattern := len(pattern) lenPattern := len(pattern)
for index := range runes { for index := 0; index < lenRunes; index++ {
char := runeAt(runes, index, lenRunes, forward) char := runeAt(text, index, lenRunes, forward)
// This is considerably faster than blindly applying strings.ToLower to the // This is considerably faster than blindly applying strings.ToLower to the
// whole string // whole string
if !caseSensitive { if !caseSensitive {
@@ -142,7 +149,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
pchar := runeAt(pattern, pidx, lenPattern, forward) pchar := pattern[indexAt(pidx, lenPattern, forward)]
if char == pchar { if char == pchar {
if sidx < 0 { if sidx < 0 {
sidx = index sidx = index
@@ -157,7 +164,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
if sidx >= 0 && eidx >= 0 { if sidx >= 0 && eidx >= 0 {
pidx-- pidx--
for index := eidx - 1; index >= sidx; index-- { for index := eidx - 1; index >= sidx; index-- {
char := runeAt(runes, index, lenRunes, forward) char := runeAt(text, index, lenRunes, forward)
if !caseSensitive { if !caseSensitive {
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
@@ -166,7 +173,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
} }
} }
pchar := runeAt(pattern, pidx, lenPattern, forward) pchar := pattern[indexAt(pidx, lenPattern, forward)]
if char == pchar { if char == pchar {
if pidx--; pidx < 0 { if pidx--; pidx < 0 {
sidx = index sidx = index
@@ -182,7 +189,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
} }
return Result{int32(sidx), int32(eidx), return Result{int32(sidx), int32(eidx),
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} evaluateBonus(caseSensitive, text, pattern, sidx, eidx)}
} }
return Result{-1, -1, 0} return Result{-1, -1, 0}
} }
@@ -194,12 +201,12 @@ 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) Result { func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
if len(pattern) == 0 { if len(pattern) == 0 {
return Result{0, 0, 0} return Result{0, 0, 0}
} }
lenRunes := len(runes) lenRunes := text.Length()
lenPattern := len(pattern) lenPattern := len(pattern)
if lenRunes < lenPattern { if lenRunes < lenPattern {
@@ -208,7 +215,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
pidx := 0 pidx := 0
for index := 0; index < lenRunes; index++ { for index := 0; index < lenRunes; index++ {
char := runeAt(runes, index, lenRunes, forward) char := runeAt(text, index, lenRunes, forward)
if !caseSensitive { if !caseSensitive {
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
@@ -216,7 +223,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
char = unicode.To(unicode.LowerCase, char) char = unicode.To(unicode.LowerCase, char)
} }
} }
pchar := runeAt(pattern, pidx, lenPattern, forward) pchar := pattern[indexAt(pidx, lenPattern, forward)]
if pchar == char { if pchar == char {
pidx++ pidx++
if pidx == lenPattern { if pidx == lenPattern {
@@ -229,7 +236,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
eidx = lenRunes - (index - lenPattern + 1) eidx = lenRunes - (index - lenPattern + 1)
} }
return Result{int32(sidx), int32(eidx), return Result{int32(sidx), int32(eidx),
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} evaluateBonus(caseSensitive, text, pattern, sidx, eidx)}
} }
} else { } else {
index -= pidx index -= pidx
@@ -240,13 +247,13 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
} }
// PrefixMatch performs prefix-match // PrefixMatch performs prefix-match
func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
if len(runes) < len(pattern) { if text.Length() < len(pattern) {
return Result{-1, -1, 0} return Result{-1, -1, 0}
} }
for index, r := range pattern { for index, r := range pattern {
char := runes[index] char := text.Get(index)
if !caseSensitive { if !caseSensitive {
char = unicode.ToLower(char) char = unicode.ToLower(char)
} }
@@ -256,20 +263,19 @@ func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
} }
lenPattern := len(pattern) lenPattern := len(pattern)
return Result{0, int32(lenPattern), return Result{0, int32(lenPattern),
evaluateBonus(caseSensitive, runes, pattern, 0, lenPattern)} evaluateBonus(caseSensitive, text, pattern, 0, lenPattern)}
} }
// SuffixMatch performs suffix-match // SuffixMatch performs suffix-match
func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) Result { func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
runes := util.TrimRight(input) trimmedLen := text.Length() - text.TrailingWhitespaces()
trimmedLen := len(runes)
diff := trimmedLen - len(pattern) diff := trimmedLen - len(pattern)
if diff < 0 { if diff < 0 {
return Result{-1, -1, 0} return Result{-1, -1, 0}
} }
for index, r := range pattern { for index, r := range pattern {
char := runes[index+diff] char := text.Get(index + diff)
if !caseSensitive { if !caseSensitive {
char = unicode.ToLower(char) char = unicode.ToLower(char)
} }
@@ -281,16 +287,16 @@ func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune)
sidx := trimmedLen - lenPattern sidx := trimmedLen - lenPattern
eidx := trimmedLen eidx := trimmedLen
return Result{int32(sidx), int32(eidx), return Result{int32(sidx), int32(eidx),
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} evaluateBonus(caseSensitive, text, pattern, sidx, eidx)}
} }
// EqualMatch performs equal-match // EqualMatch performs equal-match
func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { func EqualMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
// Note: EqualMatch always return a zero bonus. // Note: EqualMatch always return a zero bonus.
if len(runes) != len(pattern) { if text.Length() != len(pattern) {
return Result{-1, -1, 0} return Result{-1, -1, 0}
} }
runesStr := string(runes) runesStr := text.ToString()
if !caseSensitive { if !caseSensitive {
runesStr = strings.ToLower(runesStr) runesStr = strings.ToLower(runesStr)
} }

View File

@@ -3,13 +3,15 @@ package algo
import ( import (
"strings" "strings"
"testing" "testing"
"github.com/junegunn/fzf/src/util"
) )
func assertMatch(t *testing.T, fun func(bool, bool, []rune, []rune) Result, caseSensitive, forward bool, input, pattern string, sidx int32, eidx int32, bonus int32) { func assertMatch(t *testing.T, fun func(bool, bool, util.Chars, []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)
} }
res := fun(caseSensitive, forward, []rune(input), []rune(pattern)) res := fun(caseSensitive, forward, util.RunesToChars([]rune(input)), []rune(pattern))
if res.Start != sidx { if res.Start != sidx {
t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", res.Start, sidx, input, pattern) t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", res.Start, sidx, input, pattern)
} }

View File

@@ -49,7 +49,7 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
prev := str[idx:offset[0]] prev := str[idx:offset[0]]
output.WriteString(prev) output.WriteString(prev)
if proc != nil && !proc(prev, state) { if proc != nil && !proc(prev, state) {
break return "", nil, nil
} }
newState := interpretCode(str[offset[0]:offset[1]], state) newState := interpretCode(str[offset[0]:offset[1]], state)

View File

@@ -3,6 +3,8 @@ package fzf
import ( import (
"fmt" "fmt"
"testing" "testing"
"github.com/junegunn/fzf/src/util"
) )
func TestChunkList(t *testing.T) { func TestChunkList(t *testing.T) {
@@ -10,7 +12,7 @@ func TestChunkList(t *testing.T) {
sortCriteria = []criterion{byMatchLen, byLength} sortCriteria = []criterion{byMatchLen, byLength}
cl := NewChunkList(func(s []byte, i int) *Item { cl := NewChunkList(func(s []byte, i int) *Item {
return &Item{text: []rune(string(s)), rank: buildEmptyRank(int32(i * 2))} return &Item{text: util.ToChars(s), rank: buildEmptyRank(int32(i * 2))}
}) })
// Snapshot // Snapshot
@@ -42,8 +44,8 @@ func TestChunkList(t *testing.T) {
last := func(arr [5]int32) int32 { last := func(arr [5]int32) int32 {
return arr[len(arr)-1] return arr[len(arr)-1]
} }
if string((*chunk1)[0].text) != "hello" || last((*chunk1)[0].rank) != 0 || if (*chunk1)[0].text.ToString() != "hello" || last((*chunk1)[0].rank) != 0 ||
string((*chunk1)[1].text) != "world" || last((*chunk1)[1].rank) != 2 { (*chunk1)[1].text.ToString() != "world" || last((*chunk1)[1].rank) != 2 {
t.Error("Invalid data") t.Error("Invalid data")
} }
if chunk1.IsFull() { if chunk1.IsFull() {

View File

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

View File

@@ -63,29 +63,29 @@ func Run(opts *Options) {
eventBox := util.NewEventBox() eventBox := util.NewEventBox()
// ANSI code processor // ANSI code processor
ansiProcessor := func(data []byte) ([]rune, []ansiOffset) { ansiProcessor := func(data []byte) (util.Chars, []ansiOffset) {
return util.BytesToRunes(data), nil return util.ToChars(data), nil
} }
ansiProcessorRunes := func(data []rune) ([]rune, []ansiOffset) { ansiProcessorRunes := func(data []rune) (util.Chars, []ansiOffset) {
return data, nil return util.RunesToChars(data), nil
} }
if opts.Ansi { if opts.Ansi {
if opts.Theme != nil { if opts.Theme != nil {
var state *ansiState var state *ansiState
ansiProcessor = func(data []byte) ([]rune, []ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, []ansiOffset) {
trimmed, offsets, newState := extractColor(string(data), state, nil) trimmed, offsets, newState := extractColor(string(data), state, nil)
state = newState state = newState
return []rune(trimmed), offsets return util.RunesToChars([]rune(trimmed)), offsets
} }
} else { } else {
// When color is disabled but ansi option is given, // When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input // we simply strip out ANSI codes from the input
ansiProcessor = func(data []byte) ([]rune, []ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, []ansiOffset) {
trimmed, _, _ := extractColor(string(data), nil, nil) trimmed, _, _ := extractColor(string(data), nil, nil)
return []rune(trimmed), nil return util.RunesToChars([]rune(trimmed)), nil
} }
} }
ansiProcessorRunes = func(data []rune) ([]rune, []ansiOffset) { ansiProcessorRunes = func(data []rune) (util.Chars, []ansiOffset) {
return ansiProcessor([]byte(string(data))) return ansiProcessor([]byte(string(data)))
} }
} }
@@ -100,29 +100,30 @@ func Run(opts *Options) {
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return nil return nil
} }
runes, colors := ansiProcessor(data) chars, colors := ansiProcessor(data)
return &Item{ return &Item{
text: runes, text: chars,
colors: colors, colors: colors,
rank: buildEmptyRank(int32(index))} rank: buildEmptyRank(int32(index))}
}) })
} else { } else {
chunkList = NewChunkList(func(data []byte, index int) *Item { chunkList = NewChunkList(func(data []byte, index int) *Item {
runes := util.BytesToRunes(data) chars := util.ToChars(data)
tokens := Tokenize(runes, opts.Delimiter) tokens := Tokenize(chars, opts.Delimiter)
trans := Transform(tokens, opts.WithNth) trans := Transform(tokens, opts.WithNth)
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, string(joinTokens(trans))) header = append(header, string(joinTokens(trans)))
eventBox.Set(EvtHeader, header) eventBox.Set(EvtHeader, header)
return nil return nil
} }
textRunes := joinTokens(trans)
item := Item{ item := Item{
text: joinTokens(trans), text: util.RunesToChars(textRunes),
origText: &runes, origText: &data,
colors: nil, colors: nil,
rank: buildEmptyRank(int32(index))} rank: buildEmptyRank(int32(index))}
trimmed, colors := ansiProcessorRunes(item.text) trimmed, colors := ansiProcessorRunes(textRunes)
item.text = trimmed item.text = trimmed
item.colors = colors item.colors = colors
return &item return &item
@@ -170,7 +171,7 @@ func Run(opts *Options) {
func(runes []byte) bool { func(runes []byte) bool {
item := chunkList.trans(runes, 0) item := chunkList.trans(runes, 0)
if item != nil && pattern.MatchItem(item) { if item != nil && pattern.MatchItem(item) {
fmt.Println(string(item.text)) fmt.Println(item.text.ToString())
found = true found = true
} }
return false return false

View File

@@ -4,6 +4,7 @@ import (
"math" "math"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util"
) )
// Offset holds three 32-bit integers denoting the offsets of a matched substring // Offset holds three 32-bit integers denoting the offsets of a matched substring
@@ -17,8 +18,8 @@ type colorOffset struct {
// Item represents each input line // Item represents each input line
type Item struct { type Item struct {
text []rune text util.Chars
origText *[]rune origText *[]byte
transformed []Token transformed []Token
offsets []Offset offsets []Offset
colors []ansiOffset colors []ansiOffset
@@ -43,6 +44,7 @@ func buildEmptyRank(index int32) [5]int32 {
return [5]int32{0, 0, 0, 0, index} return [5]int32{0, 0, 0, 0, index}
} }
// Index returns ordinal index of the Item
func (item *Item) Index() int32 { func (item *Item) Index() int32 {
return item.rank[4] return item.rank[4]
} }
@@ -91,12 +93,14 @@ func (item *Item) Rank(cache bool) [5]int32 {
// If offsets is empty, lenSum will be 0, but we don't care // If offsets is empty, lenSum will be 0, but we don't care
val = int32(lenSum) val = int32(lenSum)
} else { } else {
val = int32(len(item.text)) val = int32(item.text.Length())
} }
case byBegin: case byBegin:
// We can't just look at item.offsets[0][0] because it can be an inverse term // We can't just look at item.offsets[0][0] because it can be an inverse term
whitePrefixLen := 0 whitePrefixLen := 0
for idx, r := range item.text { numChars := item.text.Length()
for idx := 0; idx < numChars; idx++ {
r := item.text.Get(idx)
whitePrefixLen = idx whitePrefixLen = idx
if idx == minBegin || r != ' ' && r != '\t' { if idx == minBegin || r != ' ' && r != '\t' {
break break
@@ -105,7 +109,7 @@ func (item *Item) Rank(cache bool) [5]int32 {
val = int32(minBegin - whitePrefixLen) val = int32(minBegin - whitePrefixLen)
case byEnd: case byEnd:
if prevEnd > 0 { if prevEnd > 0 {
val = int32(1 + len(item.text) - prevEnd) val = int32(1 + item.text.Length() - prevEnd)
} else { } else {
// Empty offsets due to inverse terms. // Empty offsets due to inverse terms.
val = 1 val = 1
@@ -134,7 +138,7 @@ func (item *Item) StringPtr(stripAnsi bool) *string {
orig := string(*item.origText) orig := string(*item.origText)
return &orig return &orig
} }
str := string(item.text) str := item.text.ToString()
return &str return &str
} }

View File

@@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util"
) )
func TestOffsetSort(t *testing.T) { func TestOffsetSort(t *testing.T) {
@@ -44,13 +45,13 @@ func TestItemRank(t *testing.T) {
sortCriteria = []criterion{byMatchLen, byLength} sortCriteria = []criterion{byMatchLen, byLength}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := Item{text: strs[0], offsets: []Offset{}, rank: [5]int32{0, 0, 0, 0, 1}} item1 := Item{text: util.RunesToChars(strs[0]), offsets: []Offset{}, rank: [5]int32{0, 0, 0, 0, 1}}
rank1 := item1.Rank(true) rank1 := item1.Rank(true)
if rank1[0] != math.MaxInt32 || rank1[1] != 3 || rank1[4] != 1 { if rank1[0] != math.MaxInt32 || rank1[1] != 3 || rank1[4] != 1 {
t.Error(item1.Rank(true)) t.Error(item1.Rank(true))
} }
// Only differ in index // Only differ in index
item2 := Item{text: strs[0], offsets: []Offset{}} item2 := Item{text: util.RunesToChars(strs[0]), offsets: []Offset{}}
items := []*Item{&item1, &item2} items := []*Item{&item1, &item2}
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
@@ -66,10 +67,10 @@ func TestItemRank(t *testing.T) {
} }
// Sort by relevance // Sort by relevance
item3 := Item{text: strs[1], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} item3 := Item{text: util.RunesToChars(strs[1]), rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item4 := Item{text: strs[1], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} item4 := Item{text: util.RunesToChars(strs[1]), rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
item5 := Item{text: strs[2], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} item5 := Item{text: util.RunesToChars(strs[2]), rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item6 := Item{text: strs[2], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} item6 := Item{text: util.RunesToChars(strs[2]), rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6} items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6}
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
if items[0] != &item6 || items[1] != &item4 || if items[0] != &item6 || items[1] != &item4 ||

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

@@ -2,7 +2,7 @@ package fzf
import "fmt" import "fmt"
// Merger with no data // EmptyMerger is a Merger with no data
var EmptyMerger = NewMerger([][]*Item{}, false, false) var EmptyMerger = NewMerger([][]*Item{}, false, false)
// Merger holds a set of locally sorted lists of items and provides the view of // Merger holds a set of locally sorted lists of items and provides the view of

View File

@@ -5,6 +5,8 @@ import (
"math/rand" "math/rand"
"sort" "sort"
"testing" "testing"
"github.com/junegunn/fzf/src/util"
) )
func assert(t *testing.T, cond bool, msg ...string) { func assert(t *testing.T, cond bool, msg ...string) {
@@ -22,7 +24,7 @@ func randItem() *Item {
offsets[idx] = Offset{sidx, eidx} offsets[idx] = Offset{sidx, eidx}
} }
return &Item{ return &Item{
text: []rune(str), text: util.RunesToChars([]rune(str)),
rank: buildEmptyRank(rand.Int31()), rank: buildEmptyRank(rand.Int31()),
offsets: offsets} offsets: offsets}
} }

View File

@@ -724,6 +724,7 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec {
func parsePreviewWindow(opts *previewOpts, input string) { func parsePreviewWindow(opts *previewOpts, input string) {
layout := input layout := input
opts.hidden = false
if strings.HasSuffix(layout, ":hidden") { if strings.HasSuffix(layout, ":hidden") {
opts.hidden = true opts.hidden = true
layout = strings.TrimSuffix(layout, ":hidden") layout = strings.TrimSuffix(layout, ":hidden")

View File

@@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util"
) )
func TestDelimiterRegex(t *testing.T) { func TestDelimiterRegex(t *testing.T) {
@@ -42,24 +43,24 @@ func TestDelimiterRegex(t *testing.T) {
func TestDelimiterRegexString(t *testing.T) { func TestDelimiterRegexString(t *testing.T) {
delim := delimiterRegexp("*") delim := delimiterRegexp("*")
tokens := Tokenize([]rune("-*--*---**---"), delim) tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
if delim.regex != nil || if delim.regex != nil ||
string(tokens[0].text) != "-*" || tokens[0].text.ToString() != "-*" ||
string(tokens[1].text) != "--*" || tokens[1].text.ToString() != "--*" ||
string(tokens[2].text) != "---*" || tokens[2].text.ToString() != "---*" ||
string(tokens[3].text) != "*" || tokens[3].text.ToString() != "*" ||
string(tokens[4].text) != "---" { tokens[4].text.ToString() != "---" {
t.Errorf("%s %s %d", delim, tokens, len(tokens)) t.Errorf("%s %s %d", delim, tokens, len(tokens))
} }
} }
func TestDelimiterRegexRegex(t *testing.T) { func TestDelimiterRegexRegex(t *testing.T) {
delim := delimiterRegexp("--\\*") delim := delimiterRegexp("--\\*")
tokens := Tokenize([]rune("-*--*---**---"), delim) tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
if delim.str != nil || if delim.str != nil ||
string(tokens[0].text) != "-*--*" || tokens[0].text.ToString() != "-*--*" ||
string(tokens[1].text) != "---*" || tokens[1].text.ToString() != "---*" ||
string(tokens[2].text) != "*---" { tokens[2].text.ToString() != "*---" {
t.Errorf("%s %d", tokens, len(tokens)) t.Errorf("%s %d", tokens, len(tokens))
} }
} }
@@ -352,14 +353,14 @@ func TestDefaultCtrlNP(t *testing.T) {
check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlP, actAccept)
} }
func TestToggle(t *testing.T) { func optsFor(words ...string) *Options {
optsFor := func(words ...string) *Options { opts := defaultOptions()
opts := defaultOptions() parseOptions(opts, words)
parseOptions(opts, words) postProcessOptions(opts)
postProcessOptions(opts) return opts
return opts }
}
func TestToggle(t *testing.T) {
opts := optsFor() opts := optsFor()
if opts.ToggleSort { if opts.ToggleSort {
t.Error() t.Error()
@@ -375,3 +376,31 @@ func TestToggle(t *testing.T) {
t.Error() t.Error()
} }
} }
func TestPreviewOpts(t *testing.T) {
opts := optsFor()
if !(opts.Preview.command == "" &&
opts.Preview.hidden == false &&
opts.Preview.position == posRight &&
opts.Preview.size.percent == true &&
opts.Preview.size.size == 50) {
t.Error()
}
opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden")
if !(opts.Preview.command == "cat {}" &&
opts.Preview.hidden == true &&
opts.Preview.position == posLeft &&
opts.Preview.size.percent == false &&
opts.Preview.size.size == 15+2) {
t.Error(opts.Preview)
}
opts = optsFor("--preview-window=left:15:hidden", "--preview-window=down")
if !(opts.Preview.command == "" &&
opts.Preview.hidden == false &&
opts.Preview.position == posDown &&
opts.Preview.size.percent == true &&
opts.Preview.size.size == 50) {
t.Error(opts.Preview)
}
}

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) algo.Result procFun map[termType]func(bool, bool, util.Chars, []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) algo.Result)} procFun: make(map[termType]func(bool, bool, util.Chars, []rune) algo.Result)}
ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termFuzzy] = algo.FuzzyMatch
ptr.procFun[termEqual] = algo.EqualMatch ptr.procFun[termEqual] = algo.EqualMatch
@@ -361,19 +361,19 @@ func (p *Pattern) prepareInput(item *Item) []Token {
tokens := Tokenize(item.text, p.delimiter) tokens := Tokenize(item.text, p.delimiter)
ret = Transform(tokens, p.nth) ret = Transform(tokens, p.nth)
} else { } else {
ret = []Token{Token{text: item.text, prefixLength: 0, trimLength: util.TrimLen(item.text)}} ret = []Token{Token{text: item.text, prefixLength: 0, trimLength: item.text.TrimLength()}}
} }
item.transformed = ret item.transformed = ret
return ret return ret
} }
func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) algo.Result, func (p *Pattern) iter(pfun func(bool, bool, util.Chars, []rune) algo.Result,
tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int32) { tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int32) {
for _, part := range tokens { for _, part := range tokens {
prefixLength := int32(part.prefixLength) prefixLength := int32(part.prefixLength)
if res := pfun(caseSensitive, forward, part.text, pattern); res.Start >= 0 { if res := pfun(caseSensitive, forward, part.text, pattern); res.Start >= 0 {
var sidx int32 = res.Start + prefixLength sidx := res.Start + prefixLength
var eidx int32 = res.End + prefixLength eidx := res.End + prefixLength
return Offset{sidx, eidx, int32(part.trimLength)}, res.Bonus return Offset{sidx, eidx, int32(part.trimLength)}, res.Bonus
} }
} }

View File

@@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/util"
) )
func TestParseTermsExtended(t *testing.T) { func TestParseTermsExtended(t *testing.T) {
@@ -71,7 +72,7 @@ func TestExact(t *testing.T) {
pattern := BuildPattern(true, true, CaseSmart, true, pattern := BuildPattern(true, true, CaseSmart, true,
[]Range{}, Delimiter{}, []rune("'abc")) []Range{}, Delimiter{}, []rune("'abc"))
res := algo.ExactMatchNaive( res := algo.ExactMatchNaive(
pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.termSets[0][0].text) pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text)
if res.Start != 7 || res.End != 10 { if res.Start != 7 || res.End != 10 {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
} }
@@ -84,7 +85,7 @@ func TestEqual(t *testing.T) {
match := func(str string, sidxExpected int32, eidxExpected int32) { match := func(str string, sidxExpected int32, eidxExpected int32) {
res := algo.EqualMatch( res := algo.EqualMatch(
pattern.caseSensitive, pattern.forward, []rune(str), pattern.termSets[0][0].text) pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text)
if res.Start != sidxExpected || res.End != eidxExpected { if res.Start != sidxExpected || res.End != eidxExpected {
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
} }
@@ -120,20 +121,20 @@ func TestCaseSensitivity(t *testing.T) {
func TestOrigTextAndTransformed(t *testing.T) { func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg")) pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize([]rune("junegunn"), Delimiter{}) tokens := Tokenize(util.RunesToChars([]rune("junegunn")), Delimiter{})
trans := Transform(tokens, []Range{Range{1, 1}}) trans := Transform(tokens, []Range{Range{1, 1}})
origRunes := []rune("junegunn.choi") origBytes := []byte("junegunn.choi")
for _, extended := range []bool{false, true} { for _, extended := range []bool{false, true} {
chunk := Chunk{ chunk := Chunk{
&Item{ &Item{
text: []rune("junegunn"), text: util.RunesToChars([]rune("junegunn")),
origText: &origRunes, origText: &origBytes,
transformed: trans}, transformed: trans},
} }
pattern.extended = extended pattern.extended = extended
matches := pattern.matchChunk(&chunk) matches := pattern.matchChunk(&chunk)
if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" || if matches[0].text.ToString() != "junegunn" || string(*matches[0].origText) != "junegunn.choi" ||
matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 ||
!reflect.DeepEqual(matches[0].transformed, trans) { !reflect.DeepEqual(matches[0].transformed, trans) {
t.Error("Invalid match result", matches) t.Error("Invalid match result", matches)

View File

@@ -564,7 +564,7 @@ func (t *Terminal) printHeader() {
trimmed, colors, newState := extractColor(lineStr, state, nil) trimmed, colors, newState := extractColor(lineStr, state, nil)
state = newState state = newState
item := &Item{ item := &Item{
text: []rune(trimmed), text: util.RunesToChars([]rune(trimmed)),
colors: colors, colors: colors,
rank: buildEmptyRank(0)} rank: buildEmptyRank(0)}
@@ -656,6 +656,17 @@ func trimLeft(runes []rune, width int) ([]rune, int32) {
return runes, trimmed return runes, trimmed
} }
func overflow(runes []rune, max int) bool {
l := 0
for _, r := range runes {
l += runeWidth(r, l)
if l > max {
return true
}
}
return false
}
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 int var maxe int
for _, offset := range item.offsets { for _, offset := range item.offsets {
@@ -663,22 +674,20 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
} }
// Overflow // Overflow
text := make([]rune, len(item.text)) text := make([]rune, item.text.Length())
copy(text, item.text) copy(text, item.text.ToRunes())
offsets := item.colorOffsets(col2, bold, current) offsets := item.colorOffsets(col2, bold, current)
maxWidth := t.window.Width - 3 maxWidth := t.window.Width - 3
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
fullWidth := displayWidth(text) if overflow(text, maxWidth) {
if fullWidth > maxWidth {
if t.hscroll { if t.hscroll {
// Stri.. // Stri..
matchEndWidth := displayWidth(text[:maxe]) if !overflow(text[:maxe], maxWidth-2) {
if matchEndWidth <= maxWidth-2 {
text, _ = trimRight(text, maxWidth-2) text, _ = trimRight(text, maxWidth-2)
text = append(text, []rune("..")...) text = append(text, []rune("..")...)
} else { } else {
// Stri.. // Stri..
if matchEndWidth < fullWidth-2 { if overflow(text[maxe:], 2) {
text = append(text[:maxe], []rune("..")...) text = append(text[:maxe], []rune("..")...)
} }
// ..ri.. // ..ri..

View File

@@ -18,7 +18,7 @@ type Range struct {
// Token contains the tokenized part of the strings and its prefix length // Token contains the tokenized part of the strings and its prefix length
type Token struct { type Token struct {
text []rune text util.Chars
prefixLength int prefixLength int
trimLength int trimLength int
} }
@@ -75,15 +75,15 @@ func ParseRange(str *string) (Range, bool) {
return newRange(n, n), true return newRange(n, n), true
} }
func withPrefixLengths(tokens [][]rune, begin int) []Token { func withPrefixLengths(tokens []util.Chars, begin int) []Token {
ret := make([]Token, len(tokens)) ret := make([]Token, len(tokens))
prefixLength := begin prefixLength := begin
for idx, token := range tokens { for idx, token := range tokens {
// Need to define a new local variable instead of the reused token to take // Need to define a new local variable instead of the reused token to take
// the pointer to it // the pointer to it
ret[idx] = Token{token, prefixLength, util.TrimLen(token)} ret[idx] = Token{token, prefixLength, token.TrimLength()}
prefixLength += len(token) prefixLength += token.Length()
} }
return ret return ret
} }
@@ -94,59 +94,60 @@ const (
awkWhite awkWhite
) )
func awkTokenizer(input []rune) ([][]rune, int) { func awkTokenizer(input util.Chars) ([]util.Chars, int) {
// 9, 32 // 9, 32
ret := [][]rune{} ret := []util.Chars{}
str := []rune{}
prefixLength := 0 prefixLength := 0
state := awkNil state := awkNil
for _, r := range input { numChars := input.Length()
begin := 0
end := 0
for idx := 0; idx < numChars; idx++ {
r := input.Get(idx)
white := r == 9 || r == 32 white := r == 9 || r == 32
switch state { switch state {
case awkNil: case awkNil:
if white { if white {
prefixLength++ prefixLength++
} else { } else {
state = awkBlack state, begin, end = awkBlack, idx, idx+1
str = append(str, r)
} }
case awkBlack: case awkBlack:
str = append(str, r) end = idx + 1
if white { if white {
state = awkWhite state = awkWhite
} }
case awkWhite: case awkWhite:
if white { if white {
str = append(str, r) end = idx + 1
} else { } else {
ret = append(ret, str) ret = append(ret, input.Slice(begin, end))
state = awkBlack state, begin, end = awkBlack, idx, idx+1
str = []rune{r}
} }
} }
} }
if len(str) > 0 { if begin < end {
ret = append(ret, str) ret = append(ret, input.Slice(begin, end))
} }
return ret, prefixLength return ret, prefixLength
} }
// Tokenize tokenizes the given string with the delimiter // Tokenize tokenizes the given string with the delimiter
func Tokenize(runes []rune, delimiter Delimiter) []Token { func Tokenize(text util.Chars, delimiter Delimiter) []Token {
if delimiter.str == nil && delimiter.regex == nil { if delimiter.str == nil && delimiter.regex == nil {
// AWK-style (\S+\s*) // AWK-style (\S+\s*)
tokens, prefixLength := awkTokenizer(runes) tokens, prefixLength := awkTokenizer(text)
return withPrefixLengths(tokens, prefixLength) return withPrefixLengths(tokens, prefixLength)
} }
var tokens []string
if delimiter.str != nil { if delimiter.str != nil {
tokens = strings.Split(string(runes), *delimiter.str) return withPrefixLengths(text.Split(*delimiter.str), 0)
for i := 0; i < len(tokens)-1; i++ { }
tokens[i] = tokens[i] + *delimiter.str
} // FIXME performance
} else if delimiter.regex != nil { var tokens []string
str := string(runes) if delimiter.regex != nil {
str := text.ToString()
for len(str) > 0 { for len(str) > 0 {
loc := delimiter.regex.FindStringIndex(str) loc := delimiter.regex.FindStringIndex(str)
if loc == nil { if loc == nil {
@@ -157,9 +158,9 @@ func Tokenize(runes []rune, delimiter Delimiter) []Token {
str = str[last:] str = str[last:]
} }
} }
asRunes := make([][]rune, len(tokens)) asRunes := make([]util.Chars, len(tokens))
for i, token := range tokens { for i, token := range tokens {
asRunes[i] = []rune(token) asRunes[i] = util.RunesToChars([]rune(token))
} }
return withPrefixLengths(asRunes, 0) return withPrefixLengths(asRunes, 0)
} }
@@ -167,7 +168,7 @@ func Tokenize(runes []rune, delimiter Delimiter) []Token {
func joinTokens(tokens []Token) []rune { func joinTokens(tokens []Token) []rune {
ret := []rune{} ret := []rune{}
for _, token := range tokens { for _, token := range tokens {
ret = append(ret, token.text...) ret = append(ret, token.text.ToRunes()...)
} }
return ret return ret
} }
@@ -175,7 +176,7 @@ func joinTokens(tokens []Token) []rune {
func joinTokensAsRunes(tokens []Token) []rune { func joinTokensAsRunes(tokens []Token) []rune {
ret := []rune{} ret := []rune{}
for _, token := range tokens { for _, token := range tokens {
ret = append(ret, token.text...) ret = append(ret, token.text.ToRunes()...)
} }
return ret return ret
} }
@@ -185,19 +186,19 @@ func Transform(tokens []Token, withNth []Range) []Token {
transTokens := make([]Token, len(withNth)) transTokens := make([]Token, len(withNth))
numTokens := len(tokens) numTokens := len(tokens)
for idx, r := range withNth { for idx, r := range withNth {
part := []rune{} parts := []util.Chars{}
minIdx := 0 minIdx := 0
if r.begin == r.end { if r.begin == r.end {
idx := r.begin idx := r.begin
if idx == rangeEllipsis { if idx == rangeEllipsis {
part = append(part, joinTokensAsRunes(tokens)...) parts = append(parts, util.RunesToChars(joinTokensAsRunes(tokens)))
} else { } else {
if idx < 0 { if idx < 0 {
idx += numTokens + 1 idx += numTokens + 1
} }
if idx >= 1 && idx <= numTokens { if idx >= 1 && idx <= numTokens {
minIdx = idx - 1 minIdx = idx - 1
part = append(part, tokens[idx-1].text...) parts = append(parts, tokens[idx-1].text)
} }
} }
} else { } else {
@@ -224,17 +225,32 @@ func Transform(tokens []Token, withNth []Range) []Token {
minIdx = util.Max(0, begin-1) minIdx = util.Max(0, begin-1)
for idx := begin; idx <= end; idx++ { for idx := begin; idx <= end; idx++ {
if idx >= 1 && idx <= numTokens { if idx >= 1 && idx <= numTokens {
part = append(part, tokens[idx-1].text...) parts = append(parts, tokens[idx-1].text)
} }
} }
} }
// Merge multiple parts
var merged util.Chars
switch len(parts) {
case 0:
merged = util.RunesToChars([]rune{})
case 1:
merged = parts[0]
default:
runes := []rune{}
for _, part := range parts {
runes = append(runes, part.ToRunes()...)
}
merged = util.RunesToChars(runes)
}
var prefixLength int var prefixLength int
if minIdx < numTokens { if minIdx < numTokens {
prefixLength = tokens[minIdx].prefixLength prefixLength = tokens[minIdx].prefixLength
} else { } else {
prefixLength = 0 prefixLength = 0
} }
transTokens[idx] = Token{part, prefixLength, util.TrimLen(part)} transTokens[idx] = Token{merged, prefixLength, merged.TrimLength()}
} }
return transTokens return transTokens
} }

View File

@@ -1,6 +1,10 @@
package fzf package fzf
import "testing" import (
"testing"
"github.com/junegunn/fzf/src/util"
)
func TestParseRange(t *testing.T) { func TestParseRange(t *testing.T) {
{ {
@@ -43,23 +47,23 @@ func TestParseRange(t *testing.T) {
func TestTokenize(t *testing.T) { func TestTokenize(t *testing.T) {
// AWK-style // AWK-style
input := " abc: def: ghi " input := " abc: def: ghi "
tokens := Tokenize([]rune(input), Delimiter{}) tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 { if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
// With delimiter // With delimiter
tokens = Tokenize([]rune(input), delimiterRegexp(":")) tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 { if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
// With delimiter regex // With delimiter regex
tokens = Tokenize([]rune(input), delimiterRegexp("\\s+")) tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp("\\s+"))
if string(tokens[0].text) != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 || if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 ||
string(tokens[1].text) != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 || tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 ||
string(tokens[2].text) != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 || tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 ||
string(tokens[3].text) != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 { tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 {
t.Errorf("%s", tokens) t.Errorf("%s", tokens)
} }
} }
@@ -67,7 +71,7 @@ func TestTokenize(t *testing.T) {
func TestTransform(t *testing.T) { func TestTransform(t *testing.T) {
input := " abc: def: ghi: jkl" input := " abc: def: ghi: jkl"
{ {
tokens := Tokenize([]rune(input), Delimiter{}) tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
{ {
ranges := splitNth("1,2,3") ranges := splitNth("1,2,3")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
@@ -80,25 +84,25 @@ func TestTransform(t *testing.T) {
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " || if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
len(tx) != 4 || len(tx) != 4 ||
string(tx[0].text) != "abc: def: " || tx[0].prefixLength != 2 || tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
string(tx[1].text) != "ghi: " || tx[1].prefixLength != 14 || tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
string(tx[2].text) != "def: ghi: jkl" || tx[2].prefixLength != 8 || tx[2].text.ToString() != "def: ghi: jkl" || tx[2].prefixLength != 8 ||
string(tx[3].text) != "abc: " || tx[3].prefixLength != 2 { tx[3].text.ToString() != "abc: " || tx[3].prefixLength != 2 {
t.Errorf("%s", tx) t.Errorf("%s", tx)
} }
} }
} }
{ {
tokens := Tokenize([]rune(input), delimiterRegexp(":")) tokens := Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
{ {
ranges := splitNth("1..2,3,2..,1") ranges := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges) tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != " abc: def: ghi: def: ghi: jkl abc:" || if string(joinTokens(tx)) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx) != 4 || len(tx) != 4 ||
string(tx[0].text) != " abc: def:" || tx[0].prefixLength != 0 || tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
string(tx[1].text) != " ghi:" || tx[1].prefixLength != 12 || tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
string(tx[2].text) != " def: ghi: jkl" || tx[2].prefixLength != 6 || tx[2].text.ToString() != " def: ghi: jkl" || tx[2].prefixLength != 6 ||
string(tx[3].text) != " abc:" || tx[3].prefixLength != 0 { tx[3].text.ToString() != " abc:" || tx[3].prefixLength != 0 {
t.Errorf("%s", tx) t.Errorf("%s", tx)
} }
} }

156
src/util/chars.go Normal file
View File

@@ -0,0 +1,156 @@
package util
import (
"unicode/utf8"
)
type Chars struct {
runes []rune
bytes []byte
}
// ToChars converts byte array into rune array
func ToChars(bytea []byte) Chars {
var runes []rune
ascii := true
numBytes := len(bytea)
for i := 0; i < numBytes; {
if bytea[i] < utf8.RuneSelf {
if !ascii {
runes = append(runes, rune(bytea[i]))
}
i++
} else {
if ascii {
ascii = false
runes = make([]rune, i, numBytes)
for j := 0; j < i; j++ {
runes[j] = rune(bytea[j])
}
}
r, sz := utf8.DecodeRune(bytea[i:])
i += sz
runes = append(runes, r)
}
}
if ascii {
return Chars{bytes: bytea}
}
return Chars{runes: runes}
}
func RunesToChars(runes []rune) Chars {
return Chars{runes: runes}
}
func (chars *Chars) Get(i int) rune {
if chars.runes != nil {
return chars.runes[i]
}
return rune(chars.bytes[i])
}
func (chars *Chars) Length() int {
if chars.runes != nil {
return len(chars.runes)
}
return len(chars.bytes)
}
// TrimLength returns the length after trimming leading and trailing whitespaces
func (chars *Chars) TrimLength() int {
var i int
len := chars.Length()
for i = len - 1; i >= 0; i-- {
char := chars.Get(i)
if char != ' ' && char != '\t' {
break
}
}
// Completely empty
if i < 0 {
return 0
}
var j int
for j = 0; j < len; j++ {
char := chars.Get(j)
if char != ' ' && char != '\t' {
break
}
}
return i - j + 1
}
func (chars *Chars) TrailingWhitespaces() int {
whitespaces := 0
for i := chars.Length() - 1; i >= 0; i-- {
char := chars.Get(i)
if char != ' ' && char != '\t' {
break
}
whitespaces++
}
return whitespaces
}
func (chars *Chars) ToString() string {
if chars.runes != nil {
return string(chars.runes)
}
return string(chars.bytes)
}
func (chars *Chars) ToRunes() []rune {
if chars.runes != nil {
return chars.runes
}
runes := make([]rune, len(chars.bytes))
for idx, b := range chars.bytes {
runes[idx] = rune(b)
}
return runes
}
func (chars *Chars) Slice(b int, e int) Chars {
if chars.runes != nil {
return Chars{runes: chars.runes[b:e]}
}
return Chars{bytes: chars.bytes[b:e]}
}
func (chars *Chars) Split(delimiter string) []Chars {
delim := []rune(delimiter)
numChars := chars.Length()
numDelim := len(delim)
begin := 0
ret := make([]Chars, 0, 1)
for index := 0; index < numChars; {
if index+numDelim <= numChars {
match := true
for off, d := range delim {
if chars.Get(index+off) != d {
match = false
break
}
}
// Found the delimiter
if match {
incr := Max(numDelim, 1)
ret = append(ret, chars.Slice(begin, index+incr))
index += incr
begin = index
continue
}
} else {
// Impossible to find the delimiter in the remaining substring
break
}
index++
}
if begin < numChars || len(ret) == 0 {
ret = append(ret, chars.Slice(begin, numChars))
}
return ret
}

82
src/util/chars_test.go Normal file
View File

@@ -0,0 +1,82 @@
package util
import "testing"
func TestToCharsNil(t *testing.T) {
bs := Chars{bytes: []byte{}}
if bs.bytes == nil || bs.runes != nil {
t.Error()
}
rs := RunesToChars([]rune{})
if rs.bytes != nil || rs.runes == nil {
t.Error()
}
}
func TestToCharsAscii(t *testing.T) {
chars := ToChars([]byte("foobar"))
if chars.ToString() != "foobar" || chars.runes != nil {
t.Error()
}
}
func TestCharsLength(t *testing.T) {
chars := ToChars([]byte("\tabc한글 "))
if chars.Length() != 8 || chars.TrimLength() != 5 {
t.Error()
}
}
func TestCharsToString(t *testing.T) {
text := "\tabc한글 "
chars := ToChars([]byte(text))
if chars.ToString() != text {
t.Error()
}
}
func TestTrimLength(t *testing.T) {
check := func(str string, exp int) {
chars := ToChars([]byte(str))
trimmed := chars.TrimLength()
if trimmed != exp {
t.Errorf("Invalid TrimLength result for '%s': %d (expected %d)",
str, trimmed, exp)
}
}
check("hello", 5)
check("hello ", 5)
check("hello ", 5)
check(" hello", 5)
check(" hello", 5)
check(" hello ", 5)
check(" hello ", 5)
check("h o", 5)
check(" h o ", 5)
check(" ", 0)
}
func TestSplit(t *testing.T) {
check := func(str string, delim string, tokens ...string) {
input := ToChars([]byte(str))
result := input.Split(delim)
if len(result) != len(tokens) {
t.Errorf("Invalid Split result for '%s': %d tokens found (expected %d): %s",
str, len(result), len(tokens), result)
}
for idx, token := range tokens {
if result[idx].ToString() != token {
t.Errorf("Invalid Split result for '%s': %s (expected %s)",
str, result[idx].ToString(), token)
}
}
}
check("abc:def::", ":", "abc:", "def:", ":")
check("abc:def::", "-", "abc:def::")
check("abc", "", "a", "b", "c")
check("abc", "a", "a", "bc")
check("abc", "ab", "ab", "c")
check("abc", "abc", "abc")
check("abc", "abcd", "abc")
check("", "abcd", "")
}

View File

@@ -7,18 +7,14 @@ import (
"os" "os"
"os/exec" "os/exec"
"time" "time"
"unicode/utf8"
) )
// Max returns the largest integer // Max returns the largest integer
func Max(first int, items ...int) int { func Max(first int, second int) int {
max := first if first >= second {
for _, item := range items { return first
if item > max {
max = item
}
} }
return max return second
} }
// Min returns the smallest integer // Min returns the smallest integer
@@ -84,58 +80,6 @@ func IsTty() bool {
return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 return int(C.isatty(C.int(os.Stdin.Fd()))) != 0
} }
// TrimRight returns rune array with trailing white spaces cut off
func TrimRight(runes []rune) []rune {
var i int
for i = len(runes) - 1; i >= 0; i-- {
char := runes[i]
if char != ' ' && char != '\t' {
break
}
}
return runes[0 : i+1]
}
// BytesToRunes converts byte array into rune array
func BytesToRunes(bytea []byte) []rune {
runes := make([]rune, 0, len(bytea))
for i := 0; i < len(bytea); {
if bytea[i] < utf8.RuneSelf {
runes = append(runes, rune(bytea[i]))
i++
} else {
r, sz := utf8.DecodeRune(bytea[i:])
i += sz
runes = append(runes, r)
}
}
return runes
}
// TrimLen returns the length of trimmed rune array
func TrimLen(runes []rune) int {
var i int
for i = len(runes) - 1; i >= 0; i-- {
char := runes[i]
if char != ' ' && char != '\t' {
break
}
}
// Completely empty
if i < 0 {
return 0
}
var j int
for j = 0; j < len(runes); j++ {
char := runes[j]
if char != ' ' && char != '\t' {
break
}
}
return i - j + 1
}
// ExecCommand executes the given command with $SHELL // ExecCommand executes the given command with $SHELL
func ExecCommand(command string) *exec.Cmd { func ExecCommand(command string) *exec.Cmd {
shell := os.Getenv("SHELL") shell := os.Getenv("SHELL")

View File

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

View File

@@ -1,5 +1,6 @@
Execute (Setup): Execute (Setup):
let g:dir = fnamemodify(g:vader_file, ':p:h') let g:dir = fnamemodify(g:vader_file, ':p:h')
unlet! g:fzf_layout g:fzf_action g:fzf_history_dir
Log 'Test directory: ' . g:dir Log 'Test directory: ' . g:dir
Save &acd Save &acd
@@ -43,6 +44,11 @@ Execute (fzf#run with dir option and noautochdir):
" No change in working directory " No change in working directory
AssertEqual cwd, getcwd() AssertEqual cwd, getcwd()
call fzf#run({'source': ['/foobar'], 'sink': 'tabe', 'dir': '/tmp', 'options': '-1'})
AssertEqual cwd, getcwd()
tabclose
AssertEqual cwd, getcwd()
Execute (Incomplete fzf#run with dir option and autochdir): Execute (Incomplete fzf#run with dir option and autochdir):
set acd set acd
let cwd = getcwd() let cwd = getcwd()
@@ -64,6 +70,79 @@ Execute (fzf#run with dir option and autochdir when final cwd is same as dir):
" Working directory changed due to &acd " Working directory changed due to &acd
AssertEqual '/', getcwd() AssertEqual '/', getcwd()
Execute (fzf#wrap):
AssertThrows fzf#wrap({'foo': 'bar'})
let opts = fzf#wrap('foobar')
Log opts
AssertEqual '~40%', opts.down
Assert opts.options =~ '--expect='
Assert !has_key(opts, 'sink')
Assert has_key(opts, 'sink*')
let opts = fzf#wrap('foobar', {}, 0)
Log opts
AssertEqual '~40%', opts.down
let opts = fzf#wrap('foobar', {}, 1)
Log opts
Assert !has_key(opts, 'down')
let opts = fzf#wrap('foobar', {'down': '50%'})
Log opts
AssertEqual '50%', opts.down
let opts = fzf#wrap('foobar', {'down': '50%'}, 1)
Log opts
Assert !has_key(opts, 'down')
let opts = fzf#wrap('foobar', {'sink': 'e'})
Log opts
AssertEqual 'e', opts.sink
Assert !has_key(opts, 'sink*')
let opts = fzf#wrap('foobar', {'options': '--reverse'})
Log opts
Assert opts.options =~ '--expect='
Assert opts.options =~ '--reverse'
let g:fzf_layout = {'window': 'enew'}
let opts = fzf#wrap('foobar')
Log opts
AssertEqual 'enew', opts.window
let opts = fzf#wrap('foobar', {}, 1)
Log opts
Assert !has_key(opts, 'window')
let opts = fzf#wrap('foobar', {'right': '50%'})
Log opts
Assert !has_key(opts, 'window')
AssertEqual '50%', opts.right
let opts = fzf#wrap('foobar', {'right': '50%'}, 1)
Log opts
Assert !has_key(opts, 'window')
Assert !has_key(opts, 'right')
let g:fzf_action = {'a': 'tabe'}
let opts = fzf#wrap('foobar')
Log opts
Assert opts.options =~ '--expect=a'
Assert !has_key(opts, 'sink')
Assert has_key(opts, 'sink*')
let opts = fzf#wrap('foobar', {'sink': 'e'})
Log opts
AssertEqual 'e', opts.sink
Assert !has_key(opts, 'sink*')
let g:fzf_history_dir = '/tmp'
let opts = fzf#wrap('foobar', {'options': '--color light'})
Log opts
Assert opts.options =~ '--history /tmp/foobar'
Assert opts.options =~ '--color light'
Execute (Cleanup): Execute (Cleanup):
unlet g:dir unlet g:dir
Restore Restore

View File

@@ -36,6 +36,10 @@ end
class Shell class Shell
class << self class << self
def unsets
'unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS FZF_ALT_C_COMMAND FZF_ALT_C_OPTS FZF_CTRL_R_OPTS;'
end
def bash def bash
'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.bash' 'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.bash'
end end
@@ -45,6 +49,10 @@ class Shell
FileUtils.cp File.expand_path('~/.fzf.zsh'), '/tmp/fzf-zsh/.zshrc' FileUtils.cp File.expand_path('~/.fzf.zsh'), '/tmp/fzf-zsh/.zshrc'
'PS1= PROMPT_COMMAND= HISTSIZE=100 ZDOTDIR=/tmp/fzf-zsh zsh' 'PS1= PROMPT_COMMAND= HISTSIZE=100 ZDOTDIR=/tmp/fzf-zsh zsh'
end end
def fish
'fish'
end
end end
end end
@@ -57,11 +65,11 @@ class Tmux
@win = @win =
case shell case shell
when :bash when :bash
go("new-window -d -P -F '#I' '#{Shell.bash}'").first go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.bash}'").first
when :zsh when :zsh
go("new-window -d -P -F '#I' '#{Shell.zsh}'").first go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.zsh}'").first
when :fish when :fish
go("new-window -d -P -F '#I' 'fish'").first go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.fish}'").first
else else
raise "Unknown shell: #{shell}" raise "Unknown shell: #{shell}"
end end
@@ -90,6 +98,10 @@ class Tmux
go("send-keys -t #{target} #{args}") go("send-keys -t #{target} #{args}")
end end
def paste str
%x[tmux setb '#{str.gsub("'", "'\\''")}' \\; pasteb -t #{win} \\; send-keys -t #{win} Enter]
end
def capture pane = 0 def capture pane = 0
File.unlink TEMPNAME while File.exists? TEMPNAME File.unlink TEMPNAME while File.exists? TEMPNAME
wait do wait do
@@ -149,12 +161,6 @@ class TestBase < Minitest::Test
@temp_suffix].join '-' @temp_suffix].join '-'
end end
def setup
ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_CTRL_T_COMMAND'
ENV.delete 'FZF_DEFAULT_COMMAND'
end
def readonce def readonce
wait { File.exists?(tempname) } wait { File.exists?(tempname) }
File.read(tempname) File.read(tempname)
@@ -362,7 +368,7 @@ class TestGoFZF < TestBase
end end
def test_query_unicode def test_query_unicode
tmux.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter tmux.paste "(echo abc; echo 가나다) | #{fzf :query, '가다'}"
tmux.until { |lines| lines[-2].include? '1/2' } tmux.until { |lines| lines[-2].include? '1/2' }
tmux.send_keys :Enter tmux.send_keys :Enter
assert_equal ['가나다'], readonce.split($/) assert_equal ['가나다'], readonce.split($/)
@@ -1313,16 +1319,29 @@ module TestShell
def test_ctrl_t_unicode def test_ctrl_t_unicode
FileUtils.mkdir_p '/tmp/fzf-test' FileUtils.mkdir_p '/tmp/fzf-test'
tmux.send_keys 'cd /tmp/fzf-test; echo -n test1 > "fzf-unicode 테스트1"; echo -n test2 > "fzf-unicode 테스트2"', :Enter tmux.paste 'cd /tmp/fzf-test; echo -n test1 > "fzf-unicode 테스트1"; echo -n test2 > "fzf-unicode 테스트2"'
tmux.prepare tmux.prepare
tmux.send_keys 'cat ', 'C-t', pane: 0 tmux.send_keys 'cat ', 'C-t', pane: 0
tmux.until(1) { |lines| lines.item_count >= 1 } tmux.until(1) { |lines| lines.item_count >= 1 }
tmux.send_keys 'fzf-unicode', pane: 1 tmux.send_keys 'fzf-unicode', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } tmux.until(1) { |lines| lines[-2].start_with? ' 2/' }
tmux.send_keys :BTab, :BTab, pane: 1
tmux.send_keys '1', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(1)' }
tmux.send_keys :BSpace, pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' }
tmux.send_keys '2', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(2)' } tmux.until(1) { |lines| lines[-2].include? '(2)' }
tmux.send_keys :Enter, pane: 1 tmux.send_keys :Enter, pane: 1
tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') } tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') }
tmux.until { |lines| lines[-1].include?('fzf-unicode') || lines[-2].include?('fzf-unicode') }
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include? 'test1test2' } tmux.until { |lines| lines[-1].include? 'test1test2' }
end end
@@ -1530,12 +1549,24 @@ module CompletionTest
def test_file_completion_unicode def test_file_completion_unicode
FileUtils.mkdir_p '/tmp/fzf-test' FileUtils.mkdir_p '/tmp/fzf-test'
tmux.send_keys 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"', :Enter tmux.paste 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"'
tmux.prepare tmux.prepare
tmux.send_keys 'cat fzf-unicode**', :Tab, pane: 0 tmux.send_keys 'cat fzf-unicode**', :Tab, pane: 0
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } tmux.until(1) { |lines| lines[-2].start_with? ' 2/' }
tmux.send_keys :BTab, :BTab, pane: 1
tmux.send_keys '1', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(1)' }
tmux.send_keys :BSpace, pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 2/' }
tmux.send_keys '2', pane: 1
tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
tmux.send_keys :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include? '(2)' } tmux.until(1) { |lines| lines[-2].include? '(2)' }
tmux.send_keys :Enter, pane: 1 tmux.send_keys :Enter, pane: 1
tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') } tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') }
tmux.send_keys :Enter tmux.send_keys :Enter