mirror of
https://github.com/junegunn/fzf.git
synced 2025-08-01 12:42:01 -07:00
Compare commits
55 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
b86838c2b0 | ||
|
1f7d1f9b15 | ||
|
f8fdf9618a | ||
|
827a83efbc | ||
|
3e88849386 | ||
|
608c416207 | ||
|
62f6ff9d6c | ||
|
37dc273148 | ||
|
f7f01d109e | ||
|
01ee335521 | ||
|
0e0de29b87 | ||
|
babf877fd6 | ||
|
935272824e | ||
|
3a9532c8fd | ||
|
c4c92142a6 | ||
|
d4b6338102 | ||
|
8df7d962e6 | ||
|
41e916a511 | ||
|
d9c8a9a880 | ||
|
ddc7bb9064 | ||
|
1d4057c209 | ||
|
822b86942c | ||
|
1e74dbb937 | ||
|
7cef92fffe | ||
|
42e4992f06 | ||
|
a6066175c6 | ||
|
27444d6b1e | ||
|
d6a99c0391 | ||
|
f787f7e651 | ||
|
a7c9c08371 | ||
|
fccc93176b | ||
|
6439a138fe | ||
|
a9a29dff4f | ||
|
6a52f8b8dd | ||
|
a1049328d6 | ||
|
5c2b96bd00 | ||
|
c36413fdf6 | ||
|
52cf5af91c | ||
|
3a4e053af7 | ||
|
049bc9ec68 | ||
|
b461a555b8 | ||
|
0f87b2d1e1 | ||
|
0fb5b76c0d | ||
|
0c918dd87a | ||
|
05299a0fee | ||
|
b36b0a91f5 | ||
|
6081eac58a | ||
|
942ba749c7 | ||
|
f941012687 | ||
|
fed5e5d5af | ||
|
b864885753 | ||
|
64747c2324 | ||
|
34965edcda | ||
|
bd4377084d | ||
|
38a2076b89 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,6 +1,23 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.13.5
|
||||
------
|
||||
- Memory and performance optimization
|
||||
- Up to 2x performance with half the amount of memory
|
||||
|
||||
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
|
||||
------
|
||||
- Fixed race condition where preview window is not properly cleared
|
||||
|
17
README.md
17
README.md
@@ -320,10 +320,10 @@ customization.
|
||||
|
||||
[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
|
||||
options.
|
||||
For more advanced uses, you can use `fzf#run([options])` function with the
|
||||
following options.
|
||||
|
||||
| Option name | Type | Description |
|
||||
| -------------------------- | ------------- | ---------------------------------------------------------------- |
|
||||
@@ -342,6 +342,17 @@ options.
|
||||
Examples can be found on [the wiki
|
||||
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
|
||||
----
|
||||
|
||||
|
80
bin/fzf-tmux
80
bin/fzf-tmux
@@ -2,25 +2,51 @@
|
||||
# fzf-tmux: starts fzf in a tmux pane
|
||||
# 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=()
|
||||
opt=""
|
||||
skip=""
|
||||
swap=""
|
||||
close=""
|
||||
term=""
|
||||
[ -n "$LINES" ] && lines=$LINES || lines=$(tput lines)
|
||||
while [ $# -gt 0 ]; do
|
||||
[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines)
|
||||
|
||||
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"
|
||||
case "$arg" in
|
||||
shift
|
||||
[[ -z "$skip" ]] && case "$arg" in
|
||||
-)
|
||||
term=1
|
||||
;;
|
||||
--help)
|
||||
help
|
||||
;;
|
||||
--version)
|
||||
echo "fzf-tmux (with fzf $("$fzf" --version))"
|
||||
exit
|
||||
;;
|
||||
-w*|-h*|-d*|-u*|-r*|-l*)
|
||||
if [ -n "$skip" ]; then
|
||||
args+=("$1")
|
||||
shift
|
||||
continue
|
||||
fi
|
||||
if [[ "$arg" =~ ^.[lrw] ]]; then
|
||||
opt="-h"
|
||||
if [[ "$arg" =~ ^.l ]]; then
|
||||
@@ -36,35 +62,33 @@ while [ $# -gt 0 ]; do
|
||||
close="; tmux swap-pane -D"
|
||||
fi
|
||||
fi
|
||||
if [ ${#arg} -gt 2 ]; then
|
||||
if [[ ${#arg} -gt 2 ]]; then
|
||||
size="${arg:2}"
|
||||
else
|
||||
shift
|
||||
if [[ "$1" =~ ^[0-9]+%?$ ]]; then
|
||||
size="$1"
|
||||
else
|
||||
[ -n "$1" -a "$1" != "--" ] && args+=("$1")
|
||||
shift
|
||||
else
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$size" =~ %$ ]]; then
|
||||
size=${size:0:((${#size}-1))}
|
||||
if [ -n "$swap" ]; then
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -p $(( 100 - size ))"
|
||||
else
|
||||
opt="$opt -p $size"
|
||||
fi
|
||||
else
|
||||
if [ -n "$swap" ]; then
|
||||
if [[ -n "$swap" ]]; then
|
||||
if [[ "$arg" =~ ^.l ]]; then
|
||||
[ -n "$COLUMNS" ] && max=$COLUMNS || max=$(tput cols)
|
||||
[[ -n "$COLUMNS" ]] && max=$COLUMNS || max=$(tput cols)
|
||||
else
|
||||
max=$lines
|
||||
fi
|
||||
size=$(( max - size ))
|
||||
[ $size -lt 0 ] && size=0
|
||||
[[ $size -lt 0 ]] && size=0
|
||||
opt="$opt -l $size"
|
||||
else
|
||||
opt="$opt -l $size"
|
||||
@@ -75,16 +99,17 @@ while [ $# -gt 0 ]; do
|
||||
# "--" can be used to separate fzf-tmux options from fzf options to
|
||||
# avoid conflicts
|
||||
skip=1
|
||||
continue
|
||||
;;
|
||||
*)
|
||||
args+=("$1")
|
||||
args+=("$arg")
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
[[ -n "$skip" ]] && args+=("$arg")
|
||||
done
|
||||
|
||||
if ! [ -n "$TMUX" -a "$lines" -gt 15 ]; then
|
||||
fzf "${args[@]}"
|
||||
if [[ -z "$TMUX" ]] || [[ "$lines" -le 15 ]]; then
|
||||
"$fzf" "${args[@]}"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
@@ -108,7 +133,7 @@ cleanup() {
|
||||
rm -f $argsf $fifo1 $fifo2 $fifo3
|
||||
|
||||
# Remove temp window if we were zoomed
|
||||
if [ -n "$zoomed" ]; then
|
||||
if [[ -n "$zoomed" ]]; then
|
||||
tmux swap-pane -t $original_window \; \
|
||||
select-window -t $original_window \; \
|
||||
kill-window -t $tmp_window \; \
|
||||
@@ -117,16 +142,9 @@ cleanup() {
|
||||
}
|
||||
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 "
|
||||
[ -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_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
|
||||
[[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
|
||||
|
||||
mkfifo -m o+w $fifo2
|
||||
mkfifo -m o+w $fifo3
|
||||
@@ -141,7 +159,7 @@ for arg in "${args[@]}"; do
|
||||
opts="$opts \"$arg\""
|
||||
done
|
||||
|
||||
if [ -n "$term" -o -t 0 ]; then
|
||||
if [[ -n "$term" ]] || [[ -t 0 ]]; then
|
||||
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf
|
||||
tmux set-window-option synchronize-panes off \;\
|
||||
set-window-option remain-on-exit off \;\
|
||||
|
59
install
59
install
@@ -2,13 +2,14 @@
|
||||
|
||||
set -u
|
||||
|
||||
[[ "$@" =~ --pre ]] && version=0.13.2 pre=1 ||
|
||||
version=0.13.2 pre=0
|
||||
[[ "$@" =~ --pre ]] && version=0.13.5 pre=1 ||
|
||||
version=0.13.5 pre=0
|
||||
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
binary_arch=
|
||||
allow_legacy=
|
||||
|
||||
help() {
|
||||
cat << EOF
|
||||
@@ -37,6 +38,7 @@ for opt in "$@"; do
|
||||
auto_completion=1
|
||||
key_bindings=1
|
||||
update_config=1
|
||||
allow_legacy=1
|
||||
;;
|
||||
--key-bindings) key_bindings=1 ;;
|
||||
--no-key-bindings) key_bindings=0 ;;
|
||||
@@ -109,6 +111,14 @@ link_fzf_in_path() {
|
||||
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() {
|
||||
echo "Downloading bin/fzf ..."
|
||||
if [ $pre = 0 ]; then
|
||||
@@ -128,14 +138,13 @@ download() {
|
||||
fi
|
||||
|
||||
local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz
|
||||
if command -v curl > /dev/null; then
|
||||
curl -fL $url | tar -xz
|
||||
elif command -v wget > /dev/null; then
|
||||
wget -O - $url | tar -xz
|
||||
else
|
||||
binary_error="curl or wget not found"
|
||||
set -o pipefail
|
||||
if ! (try_curl $url || try_wget $url); then
|
||||
set +o pipefail
|
||||
binary_error="Failed to download with curl and wget"
|
||||
return
|
||||
fi
|
||||
set +o pipefail
|
||||
|
||||
if [ ! -f $1 ]; then
|
||||
binary_error="Failed to download ${1}"
|
||||
@@ -158,6 +167,9 @@ case "$archi" in
|
||||
esac
|
||||
|
||||
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 ..."
|
||||
|
||||
# ruby executable
|
||||
@@ -229,26 +241,25 @@ cd "$fzf_base"
|
||||
if [ -n "$binary_error" ]; then
|
||||
if [ $binary_available -eq 0 ]; then
|
||||
echo "No prebuilt binary for $archi ..."
|
||||
if command -v go > /dev/null; then
|
||||
echo -n "Building binary (go get -u github.com/junegunn/fzf/src/fzf) ... "
|
||||
if [ -z "${GOPATH-}" ]; then
|
||||
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
|
||||
mkdir -p "$GOPATH"
|
||||
fi
|
||||
if go get -u github.com/junegunn/fzf/src/fzf; then
|
||||
echo "OK"
|
||||
cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
|
||||
else
|
||||
echo "Failed to build binary ..."
|
||||
install_ruby_fzf
|
||||
fi
|
||||
else
|
||||
echo " - $binary_error !!!"
|
||||
fi
|
||||
if command -v go > /dev/null; then
|
||||
echo -n "Building binary (go get -u github.com/junegunn/fzf/src/fzf) ... "
|
||||
if [ -z "${GOPATH-}" ]; then
|
||||
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
|
||||
mkdir -p "$GOPATH"
|
||||
fi
|
||||
if go get -u github.com/junegunn/fzf/src/fzf; then
|
||||
echo "OK"
|
||||
cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
|
||||
else
|
||||
echo "go executable not found. Cannot build binary ..."
|
||||
echo "Failed to build binary ..."
|
||||
install_ruby_fzf
|
||||
fi
|
||||
else
|
||||
echo " - $binary_error !!!"
|
||||
exit 1
|
||||
echo "go executable not found. Cannot build binary ..."
|
||||
install_ruby_fzf
|
||||
fi
|
||||
fi
|
||||
|
||||
|
54
man/man1/fzf-tmux.1
Normal file
54
man/man1/fzf-tmux.1
Normal 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.5" "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
|
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf 1 "Jun 2016" "fzf 0.13.2" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Aug 2016" "fzf 0.13.5" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
|
283
plugin/fzf.vim
283
plugin/fzf.vim
@@ -21,7 +21,8 @@
|
||||
" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
" 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:install = expand('<sfile>:h:h').'/install'
|
||||
let s:installed = 0
|
||||
@@ -104,11 +105,114 @@ function! s:warn(msg)
|
||||
echohl None
|
||||
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(...)
|
||||
let args = ['', {}, 0]
|
||||
let expects = map(copy(args), 'type(v:val)')
|
||||
let tidx = 0
|
||||
for arg in copy(a:000)
|
||||
let tidx = index(expects, type(arg), tidx)
|
||||
if tidx < 0
|
||||
throw 'invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])'
|
||||
endif
|
||||
let args[tidx] = arg
|
||||
let tidx += 1
|
||||
unlet arg
|
||||
endfor
|
||||
let [name, opts, bang] = args
|
||||
|
||||
" 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(name) && 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.'/'.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
|
||||
try
|
||||
let oshell = &shell
|
||||
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!')
|
||||
return []
|
||||
endif
|
||||
@@ -122,7 +226,9 @@ try
|
||||
endtry
|
||||
|
||||
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
|
||||
|
||||
if has_key(dict, 'source')
|
||||
@@ -135,7 +241,7 @@ try
|
||||
call writefile(source, temps.input)
|
||||
let prefix = 'cat '.s:shellesc(temps.input).'|'
|
||||
else
|
||||
throw 'Invalid source type'
|
||||
throw 'invalid source type'
|
||||
endif
|
||||
else
|
||||
let prefix = ''
|
||||
@@ -147,9 +253,9 @@ try
|
||||
return s:execute_term(dict, command, temps)
|
||||
endif
|
||||
|
||||
let ret = tmux ? s:execute_tmux(dict, command, temps) : s:execute(dict, command, temps)
|
||||
call s:popd(dict, ret)
|
||||
return ret
|
||||
let lines = tmux ? s:execute_tmux(dict, command, temps) : s:execute(dict, command, temps)
|
||||
call s:callback(dict, lines)
|
||||
return lines
|
||||
finally
|
||||
let &shell = oshell
|
||||
endtry
|
||||
@@ -200,22 +306,17 @@ function! s:pushd(dict)
|
||||
return 0
|
||||
endfunction
|
||||
|
||||
function! s:popd(dict, lines)
|
||||
" 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.
|
||||
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'))
|
||||
augroup fzf_popd
|
||||
autocmd!
|
||||
autocmd WinEnter * call s:dopopd()
|
||||
augroup END
|
||||
|
||||
function! s:dopopd()
|
||||
if !exists('w:fzf_prev_dir') || exists('*haslocaldir') && !haslocaldir()
|
||||
return
|
||||
endif
|
||||
execute 'lcd' s:escape(w:fzf_prev_dir)
|
||||
unlet w:fzf_prev_dir
|
||||
endfunction
|
||||
|
||||
function! s:xterm_launcher()
|
||||
@@ -255,8 +356,9 @@ function! s:execute(dict, command, temps) abort
|
||||
let command = escaped
|
||||
endif
|
||||
execute 'silent !'.command
|
||||
let exit_status = v:shell_error
|
||||
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
|
||||
|
||||
function! s:execute_tmux(dict, command, temps) abort
|
||||
@@ -267,8 +369,9 @@ function! s:execute_tmux(dict, command, temps) abort
|
||||
endif
|
||||
|
||||
call system(command)
|
||||
let exit_status = v:shell_error
|
||||
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
|
||||
|
||||
function! s:calc_size(max, val, dict)
|
||||
@@ -285,6 +388,7 @@ function! s:calc_size(max, val, dict)
|
||||
|
||||
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, '--header') > stridx(opts, '--no-header')
|
||||
return srcsz >= 0 ? min([srcsz + margin, size]) : size
|
||||
endfunction
|
||||
|
||||
@@ -328,7 +432,7 @@ endfunction
|
||||
function! s:execute_term(dict, command, temps) abort
|
||||
let [ppos, winopts] = s:split(a:dict)
|
||||
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)
|
||||
if a:inplace && bufnr('') == self.buf
|
||||
" FIXME: Can't re-enter normal mode from terminal mode
|
||||
@@ -356,36 +460,67 @@ function! s:execute_term(dict, command, temps) abort
|
||||
execute self.ppos.win.'wincmd w'
|
||||
endif
|
||||
|
||||
if bufexists(self.buf)
|
||||
execute 'bd!' self.buf
|
||||
endif
|
||||
|
||||
if !s:exit_handler(a:code, self.command, 1)
|
||||
return
|
||||
endif
|
||||
|
||||
call s:pushd(self.dict)
|
||||
let ret = []
|
||||
try
|
||||
let ret = s:callback(self.dict, self.temps)
|
||||
call self.switch_back(s:getpos() == self.ppos)
|
||||
finally
|
||||
call s:popd(self.dict, ret)
|
||||
endtry
|
||||
let lines = s:collect(self.temps)
|
||||
call s:callback(self.dict, lines)
|
||||
call self.switch_back(s:getpos() == self.ppos)
|
||||
endfunction
|
||||
|
||||
call s:pushd(a:dict)
|
||||
call termopen(a:command, fzf)
|
||||
call s:popd(a:dict, [])
|
||||
try
|
||||
if s:present(a:dict, 'dir')
|
||||
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
|
||||
setf fzf
|
||||
startinsert
|
||||
return []
|
||||
endfunction
|
||||
|
||||
function! s:callback(dict, temps) abort
|
||||
let lines = []
|
||||
try
|
||||
if filereadable(a:temps.result)
|
||||
let lines = readfile(a:temps.result)
|
||||
function! s:collect(temps) abort
|
||||
try
|
||||
return filereadable(a:temps.result) ? readfile(a:temps.result) : []
|
||||
finally
|
||||
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')
|
||||
for line in lines
|
||||
for line in a:lines
|
||||
if type(a:dict.sink) == 2
|
||||
call a:dict.sink(line)
|
||||
else
|
||||
@@ -394,76 +529,36 @@ try
|
||||
endfor
|
||||
endif
|
||||
if has_key(a:dict, 'sink*')
|
||||
call a:dict['sink*'](lines)
|
||||
call a:dict['sink*'](a:lines)
|
||||
endif
|
||||
endif
|
||||
catch
|
||||
if stridx(v:exception, ':E325:') < 0
|
||||
echoerr v:exception
|
||||
endif
|
||||
endtry
|
||||
|
||||
for tf in values(a:temps)
|
||||
silent! call delete(tf)
|
||||
endfor
|
||||
catch
|
||||
if stridx(v:exception, ':E325:') < 0
|
||||
echoerr v:exception
|
||||
" We may have opened a new window or tab
|
||||
if popd
|
||||
let w:fzf_prev_dir = a:dict.prev_dir
|
||||
call s:dopopd()
|
||||
endif
|
||||
finally
|
||||
return lines
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
let s:default_action = {
|
||||
\ 'ctrl-m': 'e',
|
||||
\ 'ctrl-t': 'tab split',
|
||||
\ 'ctrl-x': 'split',
|
||||
\ 'ctrl-v': 'vsplit' }
|
||||
|
||||
function! s:cmd_callback(lines) abort
|
||||
if empty(a:lines)
|
||||
return
|
||||
endif
|
||||
let key = remove(a:lines, 0)
|
||||
let cmd = get(s:action, key, 'e')
|
||||
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
|
||||
let s:action = get(g:, 'fzf_action', s:default_action)
|
||||
let args = extend(['--expect='.join(keys(s:action), ',')], a:000)
|
||||
let args = copy(a:000)
|
||||
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')
|
||||
endif
|
||||
if !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))
|
||||
call fzf#run(fzf#wrap('FZF', extend({'options': join(args)}, opts), a:bang))
|
||||
endfunction
|
||||
|
||||
command! -nargs=* -complete=dir -bang FZF call s:cmd(<bang>0, <f-args>)
|
||||
|
||||
let &cpo = s:cpo_save
|
||||
unlet s:cpo_save
|
||||
|
||||
|
@@ -14,7 +14,7 @@
|
||||
if ! declare -f _fzf_compgen_path > /dev/null; then
|
||||
_fzf_compgen_path() {
|
||||
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 \) \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
@@ -22,7 +22,7 @@ fi
|
||||
|
||||
if ! declare -f _fzf_compgen_dir > /dev/null; then
|
||||
_fzf_compgen_dir() {
|
||||
\find -L "$1" \
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .svn -prune -o -type d \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
@@ -108,7 +108,7 @@ _fzf_handle_dynamic_completion() {
|
||||
elif [ -n "$_fzf_completion_loader" ]; then
|
||||
_completion_loader "$@"
|
||||
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]}"
|
||||
return $ret
|
||||
fi
|
||||
@@ -213,16 +213,16 @@ _fzf_complete_kill() {
|
||||
|
||||
_fzf_complete_telnet() {
|
||||
_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
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_ssh() {
|
||||
_fzf_complete '+m' "$@" < <(
|
||||
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \
|
||||
<(\grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
|
||||
<(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') |
|
||||
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \
|
||||
<(command grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
}
|
||||
@@ -263,8 +263,8 @@ x_cmds="kill ssh telnet unset unalias export"
|
||||
# Preserve existing completion
|
||||
if [ "$_fzf_completion_loaded" != '0.11.3' ]; then
|
||||
# Really wish I could use associative array but OSX comes with bash 3.2 :(
|
||||
eval $(complete | \grep '\-F' | \grep -v _fzf_ |
|
||||
\grep -E " ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter)
|
||||
eval $(complete | command grep '\-F' | command grep -v _fzf_ |
|
||||
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
|
||||
fi
|
||||
|
||||
|
@@ -14,7 +14,7 @@
|
||||
if ! declare -f _fzf_compgen_path > /dev/null; then
|
||||
_fzf_compgen_path() {
|
||||
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 \) \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
@@ -22,7 +22,7 @@ fi
|
||||
|
||||
if ! declare -f _fzf_compgen_dir > /dev/null; then
|
||||
_fzf_compgen_dir() {
|
||||
\find -L "$1" \
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .svn -prune -o -type d \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
@@ -58,6 +58,7 @@ __fzf_generic_path_completion() {
|
||||
LBUFFER="$lbuf$matches$tail"
|
||||
fi
|
||||
zle redisplay
|
||||
typeset -f zle-line-init >/dev/null && zle zle-line-init
|
||||
break
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
@@ -76,7 +77,7 @@ _fzf_dir_completion() {
|
||||
}
|
||||
|
||||
_fzf_feed_fifo() (
|
||||
rm -f "$1"
|
||||
command rm -f "$1"
|
||||
mkfifo "$1"
|
||||
cat <&0 > "$1" &
|
||||
)
|
||||
@@ -97,21 +98,22 @@ _fzf_complete() {
|
||||
LBUFFER="$lbuf$matches"
|
||||
fi
|
||||
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 '+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
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_ssh() {
|
||||
_fzf_complete '+m' "$@" < <(
|
||||
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \
|
||||
<(\grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
|
||||
<(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') |
|
||||
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \
|
||||
<(command grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
}
|
||||
@@ -136,7 +138,7 @@ _fzf_complete_unalias() {
|
||||
|
||||
fzf-completion() {
|
||||
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/Doc/Release/Expansion.html#Parameter-Expansion-Flags
|
||||
@@ -161,6 +163,7 @@ fzf-completion() {
|
||||
LBUFFER="$LBUFFER$matches"
|
||||
fi
|
||||
zle redisplay
|
||||
typeset -f zle-line-init >/dev/null && zle zle-line-init
|
||||
# Trigger sequence given
|
||||
elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
|
||||
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
|
||||
|
@@ -26,7 +26,7 @@ __fzf_select_tmux__() {
|
||||
height="-l $height"
|
||||
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() {
|
||||
@@ -52,7 +52,7 @@ __fzf_history__() (
|
||||
line=$(
|
||||
HISTTIMEFORMAT= history |
|
||||
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
|
||||
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
|
||||
else
|
||||
|
@@ -8,10 +8,13 @@ __fsel() {
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-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
|
||||
echo -n "${(q)item} "
|
||||
done
|
||||
local ret=$?
|
||||
echo
|
||||
return $ret
|
||||
}
|
||||
|
||||
__fzfcmd() {
|
||||
@@ -20,7 +23,10 @@ __fzfcmd() {
|
||||
|
||||
fzf-file-widget() {
|
||||
LBUFFER="${LBUFFER}$(__fsel)"
|
||||
local ret=$?
|
||||
zle redisplay
|
||||
typeset -f zle-line-init >/dev/null && zle zle-line-init
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-file-widget
|
||||
bindkey '^T' fzf-file-widget
|
||||
@@ -29,8 +35,12 @@ bindkey '^T' fzf-file-widget
|
||||
fzf-cd-widget() {
|
||||
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-"}"
|
||||
setopt localoptions pipefail 2> /dev/null
|
||||
cd "${$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS"):-.}"
|
||||
local ret=$?
|
||||
zle reset-prompt
|
||||
typeset -f zle-line-init >/dev/null && zle zle-line-init
|
||||
return $ret
|
||||
}
|
||||
zle -N 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
|
||||
fzf-history-widget() {
|
||||
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}") )
|
||||
local ret=$?
|
||||
if [ -n "$selected" ]; then
|
||||
num=$selected[1]
|
||||
if [ -n "$num" ]; then
|
||||
@@ -47,6 +58,8 @@ fzf-history-widget() {
|
||||
fi
|
||||
fi
|
||||
zle redisplay
|
||||
typeset -f zle-line-init >/dev/null && zle zle-line-init
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-history-widget
|
||||
bindkey '^R' fzf-history-widget
|
||||
|
@@ -11,18 +11,18 @@ RUN cd / && curl \
|
||||
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
|
||||
tar -xz && mv go go1.4
|
||||
|
||||
# Install Go 1.5
|
||||
# Install Go 1.7
|
||||
RUN cd / && curl \
|
||||
https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz | \
|
||||
tar -xz && mv go go1.5
|
||||
https://storage.googleapis.com/golang/go1.7.linux-amd64.tar.gz | \
|
||||
tar -xz && mv go go1.7
|
||||
|
||||
# Install RPMs for building static 32-bit binary
|
||||
RUN curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.7/os/i386/Packages/ncurses-static-5.7-4.20090207.el6.i686.rpm -o rpm && rpm -i rpm && \
|
||||
curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.7/os/i386/Packages/gpm-static-1.20.6-12.el6.i686.rpm -o rpm && rpm -i rpm
|
||||
RUN curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.8/os/i386/Packages/ncurses-static-5.7-4.20090207.el6.i686.rpm -o rpm && rpm -i rpm && \
|
||||
curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.8/os/i386/Packages/gpm-static-1.20.6-12.el6.i686.rpm -o rpm && rpm -i rpm
|
||||
|
||||
ENV GOROOT_BOOTSTRAP /go1.4
|
||||
ENV GOROOT /go1.5
|
||||
ENV PATH /go1.5/bin:$PATH
|
||||
ENV GOROOT /go1.7
|
||||
ENV PATH /go1.7/bin:$PATH
|
||||
|
||||
# For i386 build
|
||||
RUN cd $GOROOT/src && GOARCH=386 ./make.bash
|
||||
|
@@ -15,21 +15,21 @@ import (
|
||||
* 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 {
|
||||
return runes[index]
|
||||
return index
|
||||
}
|
||||
return runes[max-index-1]
|
||||
return max - index - 1
|
||||
}
|
||||
|
||||
// Result conatins the results of running a match function.
|
||||
type Result struct {
|
||||
Start int32
|
||||
End int32
|
||||
Start int
|
||||
End int
|
||||
|
||||
// Items are basically sorted by the lengths of matched substrings.
|
||||
// But we slightly adjust the score with bonus for better results.
|
||||
Bonus int32
|
||||
Bonus int
|
||||
}
|
||||
|
||||
type charClass int
|
||||
@@ -42,14 +42,14 @@ const (
|
||||
charNumber
|
||||
)
|
||||
|
||||
func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, eidx int) int32 {
|
||||
var bonus int32
|
||||
func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int) int {
|
||||
var bonus int
|
||||
pidx := 0
|
||||
lenPattern := len(pattern)
|
||||
consecutive := false
|
||||
prevClass := charNonWord
|
||||
for index := 0; index < eidx; index++ {
|
||||
char := runes[index]
|
||||
for index := util.Max(0, sidx-1); index < eidx; index++ {
|
||||
char := text.Get(index)
|
||||
var class charClass
|
||||
if unicode.IsLower(char) {
|
||||
class = charLower
|
||||
@@ -63,7 +63,7 @@ func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, e
|
||||
class = charNonWord
|
||||
}
|
||||
|
||||
var point int32
|
||||
var point int
|
||||
if prevClass == charNonWord && class != charNonWord {
|
||||
// Word boundary
|
||||
point = 2
|
||||
@@ -107,7 +107,7 @@ func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, e
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return Result{0, 0, 0}
|
||||
}
|
||||
@@ -125,11 +125,11 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
|
||||
sidx := -1
|
||||
eidx := -1
|
||||
|
||||
lenRunes := len(runes)
|
||||
lenRunes := text.Length()
|
||||
lenPattern := len(pattern)
|
||||
|
||||
for index := range runes {
|
||||
char := runeAt(runes, index, lenRunes, forward)
|
||||
for index := 0; index < lenRunes; index++ {
|
||||
char := text.Get(indexAt(index, lenRunes, forward))
|
||||
// This is considerably faster than blindly applying strings.ToLower to the
|
||||
// whole string
|
||||
if !caseSensitive {
|
||||
@@ -142,7 +142,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
}
|
||||
pchar := runeAt(pattern, pidx, lenPattern, forward)
|
||||
pchar := pattern[indexAt(pidx, lenPattern, forward)]
|
||||
if char == pchar {
|
||||
if sidx < 0 {
|
||||
sidx = index
|
||||
@@ -157,7 +157,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
|
||||
if sidx >= 0 && eidx >= 0 {
|
||||
pidx--
|
||||
for index := eidx - 1; index >= sidx; index-- {
|
||||
char := runeAt(runes, index, lenRunes, forward)
|
||||
char := text.Get(indexAt(index, lenRunes, forward))
|
||||
if !caseSensitive {
|
||||
if char >= 'A' && char <= 'Z' {
|
||||
char += 32
|
||||
@@ -166,7 +166,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 pidx--; pidx < 0 {
|
||||
sidx = index
|
||||
@@ -181,8 +181,8 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
|
||||
sidx, eidx = lenRunes-eidx, lenRunes-sidx
|
||||
}
|
||||
|
||||
return Result{int32(sidx), int32(eidx),
|
||||
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)}
|
||||
return Result{sidx, eidx,
|
||||
evaluateBonus(caseSensitive, text, pattern, sidx, eidx)}
|
||||
}
|
||||
return Result{-1, -1, 0}
|
||||
}
|
||||
@@ -194,12 +194,12 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
|
||||
//
|
||||
// We might try to implement better algorithms in the future:
|
||||
// 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 {
|
||||
return Result{0, 0, 0}
|
||||
}
|
||||
|
||||
lenRunes := len(runes)
|
||||
lenRunes := text.Length()
|
||||
lenPattern := len(pattern)
|
||||
|
||||
if lenRunes < lenPattern {
|
||||
@@ -208,7 +208,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
|
||||
|
||||
pidx := 0
|
||||
for index := 0; index < lenRunes; index++ {
|
||||
char := runeAt(runes, index, lenRunes, forward)
|
||||
char := text.Get(indexAt(index, lenRunes, forward))
|
||||
if !caseSensitive {
|
||||
if char >= 'A' && char <= 'Z' {
|
||||
char += 32
|
||||
@@ -216,7 +216,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
}
|
||||
pchar := runeAt(pattern, pidx, lenPattern, forward)
|
||||
pchar := pattern[indexAt(pidx, lenPattern, forward)]
|
||||
if pchar == char {
|
||||
pidx++
|
||||
if pidx == lenPattern {
|
||||
@@ -228,8 +228,8 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
|
||||
sidx = lenRunes - (index + 1)
|
||||
eidx = lenRunes - (index - lenPattern + 1)
|
||||
}
|
||||
return Result{int32(sidx), int32(eidx),
|
||||
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)}
|
||||
return Result{sidx, eidx,
|
||||
evaluateBonus(caseSensitive, text, pattern, sidx, eidx)}
|
||||
}
|
||||
} else {
|
||||
index -= pidx
|
||||
@@ -240,13 +240,13 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
|
||||
}
|
||||
|
||||
// PrefixMatch performs prefix-match
|
||||
func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result {
|
||||
if len(runes) < len(pattern) {
|
||||
func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
|
||||
if text.Length() < len(pattern) {
|
||||
return Result{-1, -1, 0}
|
||||
}
|
||||
|
||||
for index, r := range pattern {
|
||||
char := runes[index]
|
||||
char := text.Get(index)
|
||||
if !caseSensitive {
|
||||
char = unicode.ToLower(char)
|
||||
}
|
||||
@@ -255,21 +255,20 @@ func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
|
||||
}
|
||||
}
|
||||
lenPattern := len(pattern)
|
||||
return Result{0, int32(lenPattern),
|
||||
evaluateBonus(caseSensitive, runes, pattern, 0, lenPattern)}
|
||||
return Result{0, lenPattern,
|
||||
evaluateBonus(caseSensitive, text, pattern, 0, lenPattern)}
|
||||
}
|
||||
|
||||
// SuffixMatch performs suffix-match
|
||||
func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) Result {
|
||||
runes := util.TrimRight(input)
|
||||
trimmedLen := len(runes)
|
||||
func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result {
|
||||
trimmedLen := text.Length() - text.TrailingWhitespaces()
|
||||
diff := trimmedLen - len(pattern)
|
||||
if diff < 0 {
|
||||
return Result{-1, -1, 0}
|
||||
}
|
||||
|
||||
for index, r := range pattern {
|
||||
char := runes[index+diff]
|
||||
char := text.Get(index + diff)
|
||||
if !caseSensitive {
|
||||
char = unicode.ToLower(char)
|
||||
}
|
||||
@@ -280,22 +279,22 @@ func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune)
|
||||
lenPattern := len(pattern)
|
||||
sidx := trimmedLen - lenPattern
|
||||
eidx := trimmedLen
|
||||
return Result{int32(sidx), int32(eidx),
|
||||
evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)}
|
||||
return Result{sidx, eidx,
|
||||
evaluateBonus(caseSensitive, text, pattern, sidx, eidx)}
|
||||
}
|
||||
|
||||
// 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.
|
||||
if len(runes) != len(pattern) {
|
||||
if text.Length() != len(pattern) {
|
||||
return Result{-1, -1, 0}
|
||||
}
|
||||
runesStr := string(runes)
|
||||
runesStr := text.ToString()
|
||||
if !caseSensitive {
|
||||
runesStr = strings.ToLower(runesStr)
|
||||
}
|
||||
if runesStr == string(pattern) {
|
||||
return Result{0, int32(len(pattern)), 0}
|
||||
return Result{0, len(pattern), 0}
|
||||
}
|
||||
return Result{-1, -1, 0}
|
||||
}
|
||||
|
@@ -3,13 +3,15 @@ package algo
|
||||
import (
|
||||
"strings"
|
||||
"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 int, eidx int, bonus int) {
|
||||
if !caseSensitive {
|
||||
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 {
|
||||
t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", res.Start, sidx, input, pattern)
|
||||
}
|
||||
|
@@ -36,7 +36,7 @@ func init() {
|
||||
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]")
|
||||
}
|
||||
|
||||
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, []ansiOffset, *ansiState) {
|
||||
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
|
||||
var offsets []ansiOffset
|
||||
var output bytes.Buffer
|
||||
|
||||
@@ -49,7 +49,7 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
prev := str[idx:offset[0]]
|
||||
output.WriteString(prev)
|
||||
if proc != nil && !proc(prev, state) {
|
||||
break
|
||||
return "", nil, nil
|
||||
}
|
||||
newState := interpretCode(str[offset[0]:offset[1]], state)
|
||||
|
||||
@@ -84,7 +84,10 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
if proc != nil {
|
||||
proc(rest, state)
|
||||
}
|
||||
return output.String(), offsets, state
|
||||
if len(offsets) == 0 {
|
||||
return output.String(), nil, state
|
||||
}
|
||||
return output.String(), &offsets, state
|
||||
}
|
||||
|
||||
func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
||||
|
@@ -16,7 +16,7 @@ func TestExtractColor(t *testing.T) {
|
||||
src := "hello world"
|
||||
var state *ansiState
|
||||
clean := "\x1b[0m"
|
||||
check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) {
|
||||
check := func(assertion func(ansiOffsets *[]ansiOffset, state *ansiState)) {
|
||||
output, ansiOffsets, newState := extractColor(src, state, nil)
|
||||
state = newState
|
||||
if output != "hello world" {
|
||||
@@ -26,127 +26,127 @@ func TestExtractColor(t *testing.T) {
|
||||
assertion(ansiOffsets, state)
|
||||
}
|
||||
|
||||
check(func(offsets []ansiOffset, state *ansiState) {
|
||||
if len(offsets) > 0 {
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if offsets != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "\x1b[0mhello world"
|
||||
check(func(offsets []ansiOffset, state *ansiState) {
|
||||
if len(offsets) > 0 {
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if offsets != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "\x1b[1mhello world"
|
||||
check(func(offsets []ansiOffset, state *ansiState) {
|
||||
if len(offsets) != 1 {
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert(offsets[0], 0, 11, -1, -1, true)
|
||||
assert((*offsets)[0], 0, 11, -1, -1, true)
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "\x1b[1mhello \x1b[mworld"
|
||||
check(func(offsets []ansiOffset, state *ansiState) {
|
||||
if len(offsets) != 1 {
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert(offsets[0], 0, 6, -1, -1, true)
|
||||
assert((*offsets)[0], 0, 6, -1, -1, true)
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "\x1b[1mhello \x1b[Kworld"
|
||||
check(func(offsets []ansiOffset, state *ansiState) {
|
||||
if len(offsets) != 1 {
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert(offsets[0], 0, 11, -1, -1, true)
|
||||
assert((*offsets)[0], 0, 11, -1, -1, true)
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "hello \x1b[34;45;1mworld"
|
||||
check(func(offsets []ansiOffset, state *ansiState) {
|
||||
if len(offsets) != 1 {
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert(offsets[0], 6, 11, 4, 5, true)
|
||||
assert((*offsets)[0], 6, 11, 4, 5, true)
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld"
|
||||
check(func(offsets []ansiOffset, state *ansiState) {
|
||||
if len(offsets) != 1 {
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert(offsets[0], 6, 11, 4, 5, true)
|
||||
assert((*offsets)[0], 6, 11, 4, 5, true)
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "hello \x1b[34;45;1mwor\x1b[0mld"
|
||||
check(func(offsets []ansiOffset, state *ansiState) {
|
||||
if len(offsets) != 1 {
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
assert(offsets[0], 6, 9, 4, 5, true)
|
||||
assert((*offsets)[0], 6, 9, 4, 5, true)
|
||||
})
|
||||
|
||||
state = nil
|
||||
src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md"
|
||||
check(func(offsets []ansiOffset, state *ansiState) {
|
||||
if len(offsets) != 3 {
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 3 {
|
||||
t.Fail()
|
||||
}
|
||||
assert(offsets[0], 6, 8, 4, 233, true)
|
||||
assert(offsets[1], 8, 9, 161, 233, true)
|
||||
assert(offsets[2], 10, 11, 161, -1, false)
|
||||
assert((*offsets)[0], 6, 8, 4, 233, true)
|
||||
assert((*offsets)[1], 8, 9, 161, 233, true)
|
||||
assert((*offsets)[2], 10, 11, 161, -1, false)
|
||||
})
|
||||
|
||||
// {38,48};5;{38,48}
|
||||
state = nil
|
||||
src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md"
|
||||
check(func(offsets []ansiOffset, state *ansiState) {
|
||||
if len(offsets) != 2 {
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 2 {
|
||||
t.Fail()
|
||||
}
|
||||
assert(offsets[0], 6, 9, 38, 48, true)
|
||||
assert(offsets[1], 9, 10, 48, 38, true)
|
||||
assert((*offsets)[0], 6, 9, 38, 48, true)
|
||||
assert((*offsets)[1], 9, 10, 48, 38, true)
|
||||
})
|
||||
|
||||
src = "hello \x1b[32;1mworld"
|
||||
check(func(offsets []ansiOffset, state *ansiState) {
|
||||
if len(offsets) != 1 {
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
if state.fg != 2 || state.bg != -1 || !state.bold {
|
||||
t.Fail()
|
||||
}
|
||||
assert(offsets[0], 6, 11, 2, -1, true)
|
||||
assert((*offsets)[0], 6, 11, 2, -1, true)
|
||||
})
|
||||
|
||||
src = "hello world"
|
||||
check(func(offsets []ansiOffset, state *ansiState) {
|
||||
if len(offsets) != 1 {
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 1 {
|
||||
t.Fail()
|
||||
}
|
||||
if state.fg != 2 || state.bg != -1 || !state.bold {
|
||||
t.Fail()
|
||||
}
|
||||
assert(offsets[0], 0, 11, 2, -1, true)
|
||||
assert((*offsets)[0], 0, 11, 2, -1, true)
|
||||
})
|
||||
|
||||
src = "hello \x1b[0;38;5;200;48;5;100mworld"
|
||||
check(func(offsets []ansiOffset, state *ansiState) {
|
||||
if len(offsets) != 2 {
|
||||
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||
if len(*offsets) != 2 {
|
||||
t.Fail()
|
||||
}
|
||||
if state.fg != 200 || state.bg != 100 || state.bold {
|
||||
t.Fail()
|
||||
}
|
||||
assert(offsets[0], 0, 6, 2, -1, true)
|
||||
assert(offsets[1], 6, 11, 200, 100, false)
|
||||
assert((*offsets)[0], 0, 6, 2, -1, true)
|
||||
assert((*offsets)[1], 6, 11, 200, 100, false)
|
||||
})
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ package fzf
|
||||
import "sync"
|
||||
|
||||
// queryCache associates strings to lists of items
|
||||
type queryCache map[string][]*Item
|
||||
type queryCache map[string][]*Result
|
||||
|
||||
// ChunkCache associates Chunk and query string to lists of items
|
||||
type ChunkCache struct {
|
||||
@@ -17,7 +17,7 @@ func NewChunkCache() ChunkCache {
|
||||
}
|
||||
|
||||
// Add adds the list to the cache
|
||||
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) {
|
||||
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) {
|
||||
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
|
||||
return
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) {
|
||||
}
|
||||
|
||||
// Find is called to lookup ChunkCache
|
||||
func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Item, bool) {
|
||||
func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Result, bool) {
|
||||
if len(key) == 0 || !chunk.IsFull() {
|
||||
return nil, false
|
||||
}
|
||||
|
@@ -7,8 +7,8 @@ func TestChunkCache(t *testing.T) {
|
||||
chunk2 := make(Chunk, chunkSize)
|
||||
chunk1p := &Chunk{}
|
||||
chunk2p := &chunk2
|
||||
items1 := []*Item{&Item{}}
|
||||
items2 := []*Item{&Item{}, &Item{}}
|
||||
items1 := []*Result{&Result{}}
|
||||
items2 := []*Result{&Result{}, &Result{}}
|
||||
cache.Add(chunk1p, "foo", items1)
|
||||
cache.Add(chunk2p, "foo", items1)
|
||||
cache.Add(chunk2p, "bar", items2)
|
||||
|
@@ -3,6 +3,8 @@ package fzf
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func TestChunkList(t *testing.T) {
|
||||
@@ -10,7 +12,7 @@ func TestChunkList(t *testing.T) {
|
||||
sortCriteria = []criterion{byMatchLen, byLength}
|
||||
|
||||
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), index: int32(i * 2)}
|
||||
})
|
||||
|
||||
// Snapshot
|
||||
@@ -39,11 +41,8 @@ func TestChunkList(t *testing.T) {
|
||||
if len(*chunk1) != 2 {
|
||||
t.Error("Snapshot should contain only two items")
|
||||
}
|
||||
last := func(arr [5]int32) int32 {
|
||||
return arr[len(arr)-1]
|
||||
}
|
||||
if string((*chunk1)[0].text) != "hello" || last((*chunk1)[0].rank) != 0 ||
|
||||
string((*chunk1)[1].text) != "world" || last((*chunk1)[1].rank) != 2 {
|
||||
if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].index != 0 ||
|
||||
(*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].index != 2 {
|
||||
t.Error("Invalid data")
|
||||
}
|
||||
if chunk1.IsFull() {
|
||||
|
@@ -8,14 +8,15 @@ import (
|
||||
|
||||
const (
|
||||
// Current version
|
||||
version = "0.13.2"
|
||||
version = "0.13.5"
|
||||
|
||||
// Core
|
||||
coordinatorDelayMax time.Duration = 100 * time.Millisecond
|
||||
coordinatorDelayStep time.Duration = 10 * time.Millisecond
|
||||
|
||||
// Reader
|
||||
defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
|
||||
defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
|
||||
readerBufferSize = 64 * 1024
|
||||
|
||||
// Terminal
|
||||
initialDelay = 20 * time.Millisecond
|
||||
|
60
src/core.go
60
src/core.go
@@ -28,16 +28,11 @@ package fzf
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func initProcs() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
}
|
||||
|
||||
/*
|
||||
Reader -> EvtReadFin
|
||||
Reader -> EvtReadNew -> Matcher (restart)
|
||||
@@ -49,8 +44,6 @@ Matcher -> EvtHeader -> Terminal (update header)
|
||||
|
||||
// Run starts fzf
|
||||
func Run(opts *Options) {
|
||||
initProcs()
|
||||
|
||||
sort := opts.Sort > 0
|
||||
sortCriteria = opts.Criteria
|
||||
|
||||
@@ -63,29 +56,29 @@ func Run(opts *Options) {
|
||||
eventBox := util.NewEventBox()
|
||||
|
||||
// ANSI code processor
|
||||
ansiProcessor := func(data []byte) ([]rune, []ansiOffset) {
|
||||
return util.BytesToRunes(data), nil
|
||||
ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
return util.ToChars(data), nil
|
||||
}
|
||||
ansiProcessorRunes := func(data []rune) ([]rune, []ansiOffset) {
|
||||
return data, nil
|
||||
ansiProcessorRunes := func(data []rune) (util.Chars, *[]ansiOffset) {
|
||||
return util.RunesToChars(data), nil
|
||||
}
|
||||
if opts.Ansi {
|
||||
if opts.Theme != nil {
|
||||
var state *ansiState
|
||||
ansiProcessor = func(data []byte) ([]rune, []ansiOffset) {
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
trimmed, offsets, newState := extractColor(string(data), state, nil)
|
||||
state = newState
|
||||
return []rune(trimmed), offsets
|
||||
return util.RunesToChars([]rune(trimmed)), offsets
|
||||
}
|
||||
} else {
|
||||
// When color is disabled but ansi option is given,
|
||||
// we simply strip out ANSI codes from the input
|
||||
ansiProcessor = func(data []byte) ([]rune, []ansiOffset) {
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
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)))
|
||||
}
|
||||
}
|
||||
@@ -100,29 +93,28 @@ func Run(opts *Options) {
|
||||
eventBox.Set(EvtHeader, header)
|
||||
return nil
|
||||
}
|
||||
runes, colors := ansiProcessor(data)
|
||||
chars, colors := ansiProcessor(data)
|
||||
return &Item{
|
||||
text: runes,
|
||||
colors: colors,
|
||||
rank: buildEmptyRank(int32(index))}
|
||||
index: int32(index),
|
||||
text: chars,
|
||||
colors: colors}
|
||||
})
|
||||
} else {
|
||||
chunkList = NewChunkList(func(data []byte, index int) *Item {
|
||||
runes := util.BytesToRunes(data)
|
||||
tokens := Tokenize(runes, opts.Delimiter)
|
||||
tokens := Tokenize(util.ToChars(data), opts.Delimiter)
|
||||
trans := Transform(tokens, opts.WithNth)
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, string(joinTokens(trans)))
|
||||
eventBox.Set(EvtHeader, header)
|
||||
return nil
|
||||
}
|
||||
textRunes := joinTokens(trans)
|
||||
item := Item{
|
||||
text: joinTokens(trans),
|
||||
origText: &runes,
|
||||
colors: nil,
|
||||
rank: buildEmptyRank(int32(index))}
|
||||
index: int32(index),
|
||||
origText: &data,
|
||||
colors: nil}
|
||||
|
||||
trimmed, colors := ansiProcessorRunes(item.text)
|
||||
trimmed, colors := ansiProcessorRunes(textRunes)
|
||||
item.text = trimmed
|
||||
item.colors = colors
|
||||
return &item
|
||||
@@ -151,7 +143,7 @@ func Run(opts *Options) {
|
||||
}
|
||||
patternBuilder := func(runes []rune) *Pattern {
|
||||
return BuildPattern(
|
||||
opts.Fuzzy, opts.Extended, opts.Case, forward,
|
||||
opts.Fuzzy, opts.Extended, opts.Case, forward, opts.Filter == nil,
|
||||
opts.Nth, opts.Delimiter, runes)
|
||||
}
|
||||
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
|
||||
@@ -169,9 +161,11 @@ func Run(opts *Options) {
|
||||
reader := Reader{
|
||||
func(runes []byte) bool {
|
||||
item := chunkList.trans(runes, 0)
|
||||
if item != nil && pattern.MatchItem(item) {
|
||||
fmt.Println(string(item.text))
|
||||
found = true
|
||||
if item != nil {
|
||||
if result, _ := pattern.MatchItem(item); result != nil {
|
||||
fmt.Println(item.text.ToString())
|
||||
found = true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, eventBox, opts.ReadZero}
|
||||
@@ -185,7 +179,7 @@ func Run(opts *Options) {
|
||||
chunks: snapshot,
|
||||
pattern: pattern})
|
||||
for i := 0; i < merger.Length(); i++ {
|
||||
fmt.Println(merger.Get(i).AsString(opts.Ansi))
|
||||
fmt.Println(merger.Get(i).item.AsString(opts.Ansi))
|
||||
found = true
|
||||
}
|
||||
}
|
||||
@@ -265,7 +259,7 @@ func Run(opts *Options) {
|
||||
fmt.Println()
|
||||
}
|
||||
for i := 0; i < count; i++ {
|
||||
fmt.Println(val.Get(i).AsString(opts.Ansi))
|
||||
fmt.Println(val.Get(i).item.AsString(opts.Ansi))
|
||||
}
|
||||
if count > 0 {
|
||||
os.Exit(exitOk)
|
||||
|
282
src/item.go
282
src/item.go
@@ -1,291 +1,39 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"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
|
||||
type Offset [3]int32
|
||||
|
||||
type colorOffset struct {
|
||||
offset [2]int32
|
||||
color int
|
||||
bold bool
|
||||
}
|
||||
|
||||
// Item represents each input line
|
||||
type Item struct {
|
||||
text []rune
|
||||
origText *[]rune
|
||||
index int32
|
||||
text util.Chars
|
||||
origText *[]byte
|
||||
colors *[]ansiOffset
|
||||
transformed []Token
|
||||
offsets []Offset
|
||||
colors []ansiOffset
|
||||
rank [5]int32
|
||||
bonus int32
|
||||
}
|
||||
|
||||
// Sort criteria to use. Never changes once fzf is started.
|
||||
var sortCriteria []criterion
|
||||
|
||||
func isRankValid(rank [5]int32) bool {
|
||||
// Exclude ordinal index
|
||||
for _, r := range rank[:4] {
|
||||
if r > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func buildEmptyRank(index int32) [5]int32 {
|
||||
return [5]int32{0, 0, 0, 0, index}
|
||||
}
|
||||
|
||||
// Index returns ordinal index of the Item
|
||||
func (item *Item) Index() int32 {
|
||||
return item.rank[4]
|
||||
return item.index
|
||||
}
|
||||
|
||||
// Rank calculates rank of the Item
|
||||
func (item *Item) Rank(cache bool) [5]int32 {
|
||||
if cache && isRankValid(item.rank) {
|
||||
return item.rank
|
||||
// Colors returns ansiOffsets of the Item
|
||||
func (item *Item) Colors() []ansiOffset {
|
||||
if item.colors == nil {
|
||||
return []ansiOffset{}
|
||||
}
|
||||
matchlen := 0
|
||||
prevEnd := 0
|
||||
lenSum := 0
|
||||
minBegin := math.MaxInt32
|
||||
for _, offset := range item.offsets {
|
||||
begin := int(offset[0])
|
||||
end := int(offset[1])
|
||||
trimLen := int(offset[2])
|
||||
lenSum += trimLen
|
||||
if prevEnd > begin {
|
||||
begin = prevEnd
|
||||
}
|
||||
if end > prevEnd {
|
||||
prevEnd = end
|
||||
}
|
||||
if end > begin {
|
||||
if begin < minBegin {
|
||||
minBegin = begin
|
||||
}
|
||||
matchlen += end - begin
|
||||
}
|
||||
}
|
||||
rank := buildEmptyRank(item.Index())
|
||||
for idx, criterion := range sortCriteria {
|
||||
var val int32
|
||||
switch criterion {
|
||||
case byMatchLen:
|
||||
if matchlen == 0 {
|
||||
val = math.MaxInt32
|
||||
} else {
|
||||
// It is extremely unlikely that bonus exceeds 128
|
||||
val = 128*int32(matchlen) - item.bonus
|
||||
}
|
||||
case byLength:
|
||||
// It is guaranteed that .transformed in not null in normal execution
|
||||
if item.transformed != nil {
|
||||
// If offsets is empty, lenSum will be 0, but we don't care
|
||||
val = int32(lenSum)
|
||||
} else {
|
||||
val = int32(len(item.text))
|
||||
}
|
||||
case byBegin:
|
||||
// We can't just look at item.offsets[0][0] because it can be an inverse term
|
||||
whitePrefixLen := 0
|
||||
for idx, r := range item.text {
|
||||
whitePrefixLen = idx
|
||||
if idx == minBegin || r != ' ' && r != '\t' {
|
||||
break
|
||||
}
|
||||
}
|
||||
val = int32(minBegin - whitePrefixLen)
|
||||
case byEnd:
|
||||
if prevEnd > 0 {
|
||||
val = int32(1 + len(item.text) - prevEnd)
|
||||
} else {
|
||||
// Empty offsets due to inverse terms.
|
||||
val = 1
|
||||
}
|
||||
}
|
||||
rank[idx] = val
|
||||
}
|
||||
if cache {
|
||||
item.rank = rank
|
||||
}
|
||||
return rank
|
||||
return *item.colors
|
||||
}
|
||||
|
||||
// AsString returns the original string
|
||||
func (item *Item) AsString(stripAnsi bool) string {
|
||||
return *item.StringPtr(stripAnsi)
|
||||
}
|
||||
|
||||
// StringPtr returns the pointer to the original string
|
||||
func (item *Item) StringPtr(stripAnsi bool) *string {
|
||||
if item.origText != nil {
|
||||
if stripAnsi {
|
||||
trimmed, _, _ := extractColor(string(*item.origText), nil, nil)
|
||||
return &trimmed
|
||||
return trimmed
|
||||
}
|
||||
orig := string(*item.origText)
|
||||
return &orig
|
||||
return string(*item.origText)
|
||||
}
|
||||
str := string(item.text)
|
||||
return &str
|
||||
}
|
||||
|
||||
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {
|
||||
if len(item.colors) == 0 {
|
||||
var offsets []colorOffset
|
||||
for _, off := range item.offsets {
|
||||
|
||||
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold})
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
|
||||
// Find max column
|
||||
var maxCol int32
|
||||
for _, off := range item.offsets {
|
||||
if off[1] > maxCol {
|
||||
maxCol = off[1]
|
||||
}
|
||||
}
|
||||
for _, ansi := range item.colors {
|
||||
if ansi.offset[1] > maxCol {
|
||||
maxCol = ansi.offset[1]
|
||||
}
|
||||
}
|
||||
cols := make([]int, maxCol)
|
||||
|
||||
for colorIndex, ansi := range item.colors {
|
||||
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
|
||||
cols[i] = colorIndex + 1 // XXX
|
||||
}
|
||||
}
|
||||
|
||||
for _, off := range item.offsets {
|
||||
for i := off[0]; i < off[1]; i++ {
|
||||
cols[i] = -1
|
||||
}
|
||||
}
|
||||
|
||||
// sort.Sort(ByOrder(offsets))
|
||||
|
||||
// Merge offsets
|
||||
// ------------ ---- -- ----
|
||||
// ++++++++ ++++++++++
|
||||
// --++++++++-- --++++++++++---
|
||||
curr := 0
|
||||
start := 0
|
||||
var offsets []colorOffset
|
||||
add := func(idx int) {
|
||||
if curr != 0 && idx > start {
|
||||
if curr == -1 {
|
||||
offsets = append(offsets, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold})
|
||||
} else {
|
||||
ansi := item.colors[curr-1]
|
||||
fg := ansi.color.fg
|
||||
if fg == -1 {
|
||||
if current {
|
||||
fg = curses.CurrentFG
|
||||
} else {
|
||||
fg = curses.FG
|
||||
}
|
||||
}
|
||||
bg := ansi.color.bg
|
||||
if bg == -1 {
|
||||
if current {
|
||||
bg = curses.DarkBG
|
||||
} else {
|
||||
bg = curses.BG
|
||||
}
|
||||
}
|
||||
offsets = append(offsets, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)},
|
||||
color: curses.PairFor(fg, bg),
|
||||
bold: ansi.color.bold || bold})
|
||||
}
|
||||
}
|
||||
}
|
||||
for idx, col := range cols {
|
||||
if col != curr {
|
||||
add(idx)
|
||||
start = idx
|
||||
curr = col
|
||||
}
|
||||
}
|
||||
add(int(maxCol))
|
||||
return offsets
|
||||
}
|
||||
|
||||
// ByOrder is for sorting substring offsets
|
||||
type ByOrder []Offset
|
||||
|
||||
func (a ByOrder) Len() int {
|
||||
return len(a)
|
||||
}
|
||||
|
||||
func (a ByOrder) Swap(i, j int) {
|
||||
a[i], a[j] = a[j], a[i]
|
||||
}
|
||||
|
||||
func (a ByOrder) Less(i, j int) bool {
|
||||
ioff := a[i]
|
||||
joff := a[j]
|
||||
return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
|
||||
}
|
||||
|
||||
// ByRelevance is for sorting Items
|
||||
type ByRelevance []*Item
|
||||
|
||||
func (a ByRelevance) Len() int {
|
||||
return len(a)
|
||||
}
|
||||
|
||||
func (a ByRelevance) Swap(i, j int) {
|
||||
a[i], a[j] = a[j], a[i]
|
||||
}
|
||||
|
||||
func (a ByRelevance) Less(i, j int) bool {
|
||||
irank := a[i].Rank(true)
|
||||
jrank := a[j].Rank(true)
|
||||
|
||||
return compareRanks(irank, jrank, false)
|
||||
}
|
||||
|
||||
// ByRelevanceTac is for sorting Items
|
||||
type ByRelevanceTac []*Item
|
||||
|
||||
func (a ByRelevanceTac) Len() int {
|
||||
return len(a)
|
||||
}
|
||||
|
||||
func (a ByRelevanceTac) Swap(i, j int) {
|
||||
a[i], a[j] = a[j], a[i]
|
||||
}
|
||||
|
||||
func (a ByRelevanceTac) Less(i, j int) bool {
|
||||
irank := a[i].Rank(true)
|
||||
jrank := a[j].Rank(true)
|
||||
|
||||
return compareRanks(irank, jrank, true)
|
||||
}
|
||||
|
||||
func compareRanks(irank [5]int32, jrank [5]int32, tac bool) bool {
|
||||
for idx := 0; idx < 4; idx++ {
|
||||
left := irank[idx]
|
||||
right := jrank[idx]
|
||||
if left < right {
|
||||
return true
|
||||
} else if left > right {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return (irank[4] <= jrank[4]) != tac
|
||||
return item.text.ToString()
|
||||
}
|
||||
|
113
src/item_test.go
113
src/item_test.go
@@ -1,108 +1,23 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/curses"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func TestOffsetSort(t *testing.T) {
|
||||
offsets := []Offset{
|
||||
Offset{3, 5}, Offset{2, 7},
|
||||
Offset{1, 3}, Offset{2, 9}}
|
||||
sort.Sort(ByOrder(offsets))
|
||||
|
||||
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
|
||||
offsets[1][0] != 2 || offsets[1][1] != 7 ||
|
||||
offsets[2][0] != 2 || offsets[2][1] != 9 ||
|
||||
offsets[3][0] != 3 || offsets[3][1] != 5 {
|
||||
t.Error("Invalid order:", offsets)
|
||||
func TestStringPtr(t *testing.T) {
|
||||
orig := []byte("\x1b[34mfoo")
|
||||
text := []byte("\x1b[34mbar")
|
||||
item := Item{origText: &orig, text: util.ToChars(text)}
|
||||
if item.AsString(true) != "foo" || item.AsString(false) != string(orig) {
|
||||
t.Fail()
|
||||
}
|
||||
if item.AsString(true) != "foo" {
|
||||
t.Fail()
|
||||
}
|
||||
item.origText = nil
|
||||
if item.AsString(true) != string(text) || item.AsString(false) != string(text) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRankComparison(t *testing.T) {
|
||||
if compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{2, 0, 0, 0, 7}, false) ||
|
||||
!compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{3, 0, 0, 0, 6}, false) ||
|
||||
!compareRanks([5]int32{1, 2, 0, 0, 3}, [5]int32{1, 3, 0, 0, 2}, false) ||
|
||||
!compareRanks([5]int32{0, 0, 0, 0, 0}, [5]int32{0, 0, 0, 0, 0}, false) {
|
||||
t.Error("Invalid order")
|
||||
}
|
||||
|
||||
if compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{2, 0, 0, 0, 7}, true) ||
|
||||
!compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{3, 0, 0, 0, 6}, false) ||
|
||||
!compareRanks([5]int32{1, 2, 0, 0, 3}, [5]int32{1, 3, 0, 0, 2}, true) ||
|
||||
!compareRanks([5]int32{0, 0, 0, 0, 0}, [5]int32{0, 0, 0, 0, 0}, false) {
|
||||
t.Error("Invalid order (tac)")
|
||||
}
|
||||
}
|
||||
|
||||
// Match length, string length, index
|
||||
func TestItemRank(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byMatchLen, byLength}
|
||||
|
||||
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}}
|
||||
rank1 := item1.Rank(true)
|
||||
if rank1[0] != math.MaxInt32 || rank1[1] != 3 || rank1[4] != 1 {
|
||||
t.Error(item1.Rank(true))
|
||||
}
|
||||
// Only differ in index
|
||||
item2 := Item{text: strs[0], offsets: []Offset{}}
|
||||
|
||||
items := []*Item{&item1, &item2}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if items[0] != &item2 || items[1] != &item1 {
|
||||
t.Error(items)
|
||||
}
|
||||
|
||||
items = []*Item{&item2, &item1, &item1, &item2}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if items[0] != &item2 || items[1] != &item2 ||
|
||||
items[2] != &item1 || items[3] != &item1 {
|
||||
t.Error(items)
|
||||
}
|
||||
|
||||
// Sort by relevance
|
||||
item3 := Item{text: 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}}}
|
||||
item5 := Item{text: 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}}}
|
||||
items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if items[0] != &item6 || items[1] != &item4 ||
|
||||
items[2] != &item5 || items[3] != &item3 ||
|
||||
items[4] != &item2 || items[5] != &item1 {
|
||||
t.Error(items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorOffset(t *testing.T) {
|
||||
// ------------ 20 ---- -- ----
|
||||
// ++++++++ ++++++++++
|
||||
// --++++++++-- --++++++++++---
|
||||
item := Item{
|
||||
offsets: []Offset{Offset{5, 15}, Offset{25, 35}},
|
||||
colors: []ansiOffset{
|
||||
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}},
|
||||
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}},
|
||||
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}},
|
||||
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}
|
||||
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
|
||||
|
||||
offsets := item.colorOffsets(99, false, true)
|
||||
assert := func(idx int, b int32, e int32, c int, bold bool) {
|
||||
o := offsets[idx]
|
||||
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold {
|
||||
t.Error(o)
|
||||
}
|
||||
}
|
||||
assert(0, 0, 5, curses.ColUser, false)
|
||||
assert(1, 5, 15, 99, false)
|
||||
assert(2, 15, 20, curses.ColUser, false)
|
||||
assert(3, 22, 25, curses.ColUser+1, true)
|
||||
assert(4, 25, 35, 99, false)
|
||||
assert(5, 35, 40, curses.ColUser+2, true)
|
||||
}
|
||||
|
@@ -43,7 +43,7 @@ func NewMatcher(patternBuilder func([]rune) *Pattern,
|
||||
tac: tac,
|
||||
eventBox: eventBox,
|
||||
reqBox: util.NewEventBox(),
|
||||
partitions: runtime.NumCPU(),
|
||||
partitions: util.Min(8*runtime.NumCPU(), 32),
|
||||
mergerCache: make(map[string]*Merger)}
|
||||
}
|
||||
|
||||
@@ -106,18 +106,19 @@ func (m *Matcher) Loop() {
|
||||
}
|
||||
|
||||
func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
|
||||
perSlice := len(chunks) / m.partitions
|
||||
partitions := m.partitions
|
||||
perSlice := len(chunks) / partitions
|
||||
|
||||
// No need to parallelize
|
||||
if perSlice == 0 {
|
||||
return [][]*Chunk{chunks}
|
||||
partitions = len(chunks)
|
||||
perSlice = 1
|
||||
}
|
||||
|
||||
slices := make([][]*Chunk, m.partitions)
|
||||
for i := 0; i < m.partitions; i++ {
|
||||
slices := make([][]*Chunk, partitions)
|
||||
for i := 0; i < partitions; i++ {
|
||||
start := i * perSlice
|
||||
end := start + perSlice
|
||||
if i == m.partitions-1 {
|
||||
if i == partitions-1 {
|
||||
end = len(chunks)
|
||||
}
|
||||
slices[i] = chunks[start:end]
|
||||
@@ -127,7 +128,7 @@ func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
|
||||
|
||||
type partialResult struct {
|
||||
index int
|
||||
matches []*Item
|
||||
matches []*Result
|
||||
}
|
||||
|
||||
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
@@ -154,15 +155,21 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
waitGroup.Add(1)
|
||||
go func(idx int, chunks []*Chunk) {
|
||||
defer func() { waitGroup.Done() }()
|
||||
sliceMatches := []*Item{}
|
||||
for _, chunk := range chunks {
|
||||
count := 0
|
||||
allMatches := make([][]*Result, len(chunks))
|
||||
for idx, chunk := range chunks {
|
||||
matches := request.pattern.Match(chunk)
|
||||
sliceMatches = append(sliceMatches, matches...)
|
||||
allMatches[idx] = matches
|
||||
count += len(matches)
|
||||
if cancelled.Get() {
|
||||
return
|
||||
}
|
||||
countChan <- len(matches)
|
||||
}
|
||||
sliceMatches := make([]*Result, 0, count)
|
||||
for _, matches := range allMatches {
|
||||
sliceMatches = append(sliceMatches, matches...)
|
||||
}
|
||||
if m.sort {
|
||||
if m.tac {
|
||||
sort.Sort(ByRelevanceTac(sliceMatches))
|
||||
@@ -199,12 +206,12 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
partialResults := make([][]*Item, numSlices)
|
||||
for _, _ = range slices {
|
||||
partialResults := make([][]*Result, numSlices)
|
||||
for _ = range slices {
|
||||
partialResult := <-resultChan
|
||||
partialResults[partialResult.index] = partialResult.matches
|
||||
}
|
||||
return NewMerger(partialResults, m.sort, m.tac), false
|
||||
return NewMerger(pattern, partialResults, m.sort, m.tac), false
|
||||
}
|
||||
|
||||
// Reset is called to interrupt/signal the ongoing search
|
||||
|
@@ -2,14 +2,15 @@ package fzf
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Merger with no data
|
||||
var EmptyMerger = NewMerger([][]*Item{}, false, false)
|
||||
// EmptyMerger is a Merger with no data
|
||||
var EmptyMerger = NewMerger(nil, [][]*Result{}, false, false)
|
||||
|
||||
// Merger holds a set of locally sorted lists of items and provides the view of
|
||||
// a single, globally-sorted list
|
||||
type Merger struct {
|
||||
lists [][]*Item
|
||||
merged []*Item
|
||||
pattern *Pattern
|
||||
lists [][]*Result
|
||||
merged []*Result
|
||||
chunks *[]*Chunk
|
||||
cursors []int
|
||||
sorted bool
|
||||
@@ -22,9 +23,10 @@ type Merger struct {
|
||||
// original order
|
||||
func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
|
||||
mg := Merger{
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0}
|
||||
pattern: nil,
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0}
|
||||
|
||||
for _, chunk := range *mg.chunks {
|
||||
mg.count += len(*chunk)
|
||||
@@ -33,10 +35,11 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
|
||||
}
|
||||
|
||||
// NewMerger returns a new Merger
|
||||
func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger {
|
||||
func NewMerger(pattern *Pattern, lists [][]*Result, sorted bool, tac bool) *Merger {
|
||||
mg := Merger{
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
merged: []*Item{},
|
||||
merged: []*Result{},
|
||||
chunks: nil,
|
||||
cursors: make([]int, len(lists)),
|
||||
sorted: sorted,
|
||||
@@ -55,14 +58,14 @@ func (mg *Merger) Length() int {
|
||||
return mg.count
|
||||
}
|
||||
|
||||
// Get returns the pointer to the Item object indexed by the given integer
|
||||
func (mg *Merger) Get(idx int) *Item {
|
||||
// Get returns the pointer to the Result object indexed by the given integer
|
||||
func (mg *Merger) Get(idx int) *Result {
|
||||
if mg.chunks != nil {
|
||||
if mg.tac {
|
||||
idx = mg.count - idx - 1
|
||||
}
|
||||
chunk := (*mg.chunks)[idx/chunkSize]
|
||||
return (*chunk)[idx%chunkSize]
|
||||
return &Result{item: (*chunk)[idx%chunkSize]}
|
||||
}
|
||||
|
||||
if mg.sorted {
|
||||
@@ -86,9 +89,9 @@ func (mg *Merger) cacheable() bool {
|
||||
return mg.count < mergerCacheMax
|
||||
}
|
||||
|
||||
func (mg *Merger) mergedGet(idx int) *Item {
|
||||
func (mg *Merger) mergedGet(idx int) *Result {
|
||||
for i := len(mg.merged); i <= idx; i++ {
|
||||
minRank := buildEmptyRank(0)
|
||||
minRank := minRank()
|
||||
minIdx := -1
|
||||
for listIdx, list := range mg.lists {
|
||||
cursor := mg.cursors[listIdx]
|
||||
@@ -97,7 +100,7 @@ func (mg *Merger) mergedGet(idx int) *Item {
|
||||
continue
|
||||
}
|
||||
if cursor >= 0 {
|
||||
rank := list[cursor].Rank(false)
|
||||
rank := list[cursor].rank
|
||||
if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
|
||||
minRank = rank
|
||||
minIdx = listIdx
|
||||
|
@@ -5,6 +5,8 @@ import (
|
||||
"math/rand"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func assert(t *testing.T, cond bool, msg ...string) {
|
||||
@@ -13,18 +15,11 @@ func assert(t *testing.T, cond bool, msg ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
func randItem() *Item {
|
||||
func randResult() *Result {
|
||||
str := fmt.Sprintf("%d", rand.Uint32())
|
||||
offsets := make([]Offset, rand.Int()%3)
|
||||
for idx := range offsets {
|
||||
sidx := int32(rand.Uint32() % 20)
|
||||
eidx := sidx + int32(rand.Uint32()%20)
|
||||
offsets[idx] = Offset{sidx, eidx}
|
||||
}
|
||||
return &Item{
|
||||
text: []rune(str),
|
||||
rank: buildEmptyRank(rand.Int31()),
|
||||
offsets: offsets}
|
||||
return &Result{
|
||||
item: &Item{text: util.RunesToChars([]rune(str))},
|
||||
rank: rank{index: rand.Int31()}}
|
||||
}
|
||||
|
||||
func TestEmptyMerger(t *testing.T) {
|
||||
@@ -34,23 +29,23 @@ func TestEmptyMerger(t *testing.T) {
|
||||
assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
|
||||
}
|
||||
|
||||
func buildLists(partiallySorted bool) ([][]*Item, []*Item) {
|
||||
func buildLists(partiallySorted bool) ([][]*Result, []*Result) {
|
||||
numLists := 4
|
||||
lists := make([][]*Item, numLists)
|
||||
lists := make([][]*Result, numLists)
|
||||
cnt := 0
|
||||
for i := 0; i < numLists; i++ {
|
||||
numItems := rand.Int() % 20
|
||||
cnt += numItems
|
||||
lists[i] = make([]*Item, numItems)
|
||||
for j := 0; j < numItems; j++ {
|
||||
item := randItem()
|
||||
numResults := rand.Int() % 20
|
||||
cnt += numResults
|
||||
lists[i] = make([]*Result, numResults)
|
||||
for j := 0; j < numResults; j++ {
|
||||
item := randResult()
|
||||
lists[i][j] = item
|
||||
}
|
||||
if partiallySorted {
|
||||
sort.Sort(ByRelevance(lists[i]))
|
||||
}
|
||||
}
|
||||
items := []*Item{}
|
||||
items := []*Result{}
|
||||
for _, list := range lists {
|
||||
items = append(items, list...)
|
||||
}
|
||||
@@ -62,7 +57,7 @@ func TestMergerUnsorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Not sorted: same order
|
||||
mg := NewMerger(lists, false, false)
|
||||
mg := NewMerger(nil, lists, false, false)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
for i := 0; i < cnt; i++ {
|
||||
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
||||
@@ -74,7 +69,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Sorted sorted order
|
||||
mg := NewMerger(lists, true, false)
|
||||
mg := NewMerger(nil, lists, true, false)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
sort.Sort(ByRelevance(items))
|
||||
for i := 0; i < cnt; i++ {
|
||||
@@ -84,7 +79,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
}
|
||||
|
||||
// Inverse order
|
||||
mg2 := NewMerger(lists, true, false)
|
||||
mg2 := NewMerger(nil, lists, true, false)
|
||||
for i := cnt - 1; i >= 0; i-- {
|
||||
if items[i] != mg2.Get(i) {
|
||||
t.Error("Not sorted", items[i], mg2.Get(i))
|
||||
|
@@ -95,6 +95,7 @@ type criterion int
|
||||
|
||||
const (
|
||||
byMatchLen criterion = iota
|
||||
byBonus
|
||||
byLength
|
||||
byBegin
|
||||
byEnd
|
||||
@@ -178,7 +179,7 @@ func defaultOptions() *Options {
|
||||
Delimiter: Delimiter{},
|
||||
Sort: 1000,
|
||||
Tac: false,
|
||||
Criteria: []criterion{byMatchLen, byLength},
|
||||
Criteria: []criterion{byMatchLen, byBonus, byLength},
|
||||
Multi: false,
|
||||
Ansi: false,
|
||||
Mouse: true,
|
||||
@@ -406,7 +407,7 @@ func parseKeyChords(str string, message string) map[int]string {
|
||||
}
|
||||
|
||||
func parseTiebreak(str string) []criterion {
|
||||
criteria := []criterion{byMatchLen}
|
||||
criteria := []criterion{byMatchLen, byBonus}
|
||||
hasIndex := false
|
||||
hasLength := false
|
||||
hasBegin := false
|
||||
@@ -724,6 +725,7 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec {
|
||||
|
||||
func parsePreviewWindow(opts *previewOpts, input string) {
|
||||
layout := input
|
||||
opts.hidden = false
|
||||
if strings.HasSuffix(layout, ":hidden") {
|
||||
opts.hidden = true
|
||||
layout = strings.TrimSuffix(layout, ":hidden")
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/curses"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func TestDelimiterRegex(t *testing.T) {
|
||||
@@ -42,24 +43,24 @@ func TestDelimiterRegex(t *testing.T) {
|
||||
|
||||
func TestDelimiterRegexString(t *testing.T) {
|
||||
delim := delimiterRegexp("*")
|
||||
tokens := Tokenize([]rune("-*--*---**---"), delim)
|
||||
tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
|
||||
if delim.regex != nil ||
|
||||
string(tokens[0].text) != "-*" ||
|
||||
string(tokens[1].text) != "--*" ||
|
||||
string(tokens[2].text) != "---*" ||
|
||||
string(tokens[3].text) != "*" ||
|
||||
string(tokens[4].text) != "---" {
|
||||
tokens[0].text.ToString() != "-*" ||
|
||||
tokens[1].text.ToString() != "--*" ||
|
||||
tokens[2].text.ToString() != "---*" ||
|
||||
tokens[3].text.ToString() != "*" ||
|
||||
tokens[4].text.ToString() != "---" {
|
||||
t.Errorf("%s %s %d", delim, tokens, len(tokens))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelimiterRegexRegex(t *testing.T) {
|
||||
delim := delimiterRegexp("--\\*")
|
||||
tokens := Tokenize([]rune("-*--*---**---"), delim)
|
||||
tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
|
||||
if delim.str != nil ||
|
||||
string(tokens[0].text) != "-*--*" ||
|
||||
string(tokens[1].text) != "---*" ||
|
||||
string(tokens[2].text) != "*---" {
|
||||
tokens[0].text.ToString() != "-*--*" ||
|
||||
tokens[1].text.ToString() != "---*" ||
|
||||
tokens[2].text.ToString() != "*---" {
|
||||
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)
|
||||
}
|
||||
|
||||
func TestToggle(t *testing.T) {
|
||||
optsFor := func(words ...string) *Options {
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
postProcessOptions(opts)
|
||||
return opts
|
||||
}
|
||||
func optsFor(words ...string) *Options {
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
postProcessOptions(opts)
|
||||
return opts
|
||||
}
|
||||
|
||||
func TestToggle(t *testing.T) {
|
||||
opts := optsFor()
|
||||
if opts.ToggleSort {
|
||||
t.Error()
|
||||
@@ -375,3 +376,31 @@ func TestToggle(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
123
src/pattern.go
123
src/pattern.go
@@ -2,7 +2,6 @@ package fzf
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
@@ -49,7 +48,7 @@ type Pattern struct {
|
||||
cacheable bool
|
||||
delimiter Delimiter
|
||||
nth []Range
|
||||
procFun map[termType]func(bool, bool, []rune, []rune) algo.Result
|
||||
procFun map[termType]func(bool, bool, util.Chars, []rune) algo.Result
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -76,7 +75,7 @@ func clearChunkCache() {
|
||||
|
||||
// BuildPattern builds Pattern object from the given arguments
|
||||
func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
|
||||
nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||
cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||
|
||||
var asString string
|
||||
if extended {
|
||||
@@ -90,7 +89,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
|
||||
return cached
|
||||
}
|
||||
|
||||
caseSensitive, cacheable := true, true
|
||||
caseSensitive := true
|
||||
termSets := []termSet{}
|
||||
|
||||
if extended {
|
||||
@@ -100,7 +99,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
|
||||
for idx, term := range termSet {
|
||||
// If the query contains inverse search terms or OR operators,
|
||||
// we cannot cache the search scope
|
||||
if idx > 0 || term.inv {
|
||||
if !cacheable || idx > 0 || term.inv {
|
||||
cacheable = false
|
||||
break Loop
|
||||
}
|
||||
@@ -125,7 +124,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool,
|
||||
cacheable: cacheable,
|
||||
nth: nth,
|
||||
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[termEqual] = algo.EqualMatch
|
||||
@@ -235,9 +234,7 @@ func (p *Pattern) CacheKey() string {
|
||||
}
|
||||
|
||||
// Match returns the list of matches Items in the given Chunk
|
||||
func (p *Pattern) Match(chunk *Chunk) []*Item {
|
||||
space := chunk
|
||||
|
||||
func (p *Pattern) Match(chunk *Chunk) []*Result {
|
||||
// ChunkCache: Exact match
|
||||
cacheKey := p.CacheKey()
|
||||
if p.cacheable {
|
||||
@@ -246,7 +243,8 @@ func (p *Pattern) Match(chunk *Chunk) []*Item {
|
||||
}
|
||||
}
|
||||
|
||||
// ChunkCache: Prefix/suffix match
|
||||
// Prefix/suffix cache
|
||||
var space []*Result
|
||||
Loop:
|
||||
for idx := 1; idx < len(cacheKey); idx++ {
|
||||
// [---------| ] | [ |---------]
|
||||
@@ -256,14 +254,13 @@ Loop:
|
||||
suffix := cacheKey[idx:]
|
||||
for _, substr := range [2]*string{&prefix, &suffix} {
|
||||
if cached, found := _cache.Find(chunk, *substr); found {
|
||||
cachedChunk := Chunk(cached)
|
||||
space = &cachedChunk
|
||||
space = cached
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matches := p.matchChunk(space)
|
||||
matches := p.matchChunk(chunk, space)
|
||||
|
||||
if p.cacheable {
|
||||
_cache.Add(chunk, cacheKey, matches)
|
||||
@@ -271,20 +268,19 @@ Loop:
|
||||
return matches
|
||||
}
|
||||
|
||||
func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
|
||||
matches := []*Item{}
|
||||
if !p.extended {
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, space []*Result) []*Result {
|
||||
matches := []*Result{}
|
||||
|
||||
if space == nil {
|
||||
for _, item := range *chunk {
|
||||
offset, bonus := p.basicMatch(item)
|
||||
if sidx := offset[0]; sidx >= 0 {
|
||||
matches = append(matches,
|
||||
dupItem(item, []Offset{offset}, bonus))
|
||||
if match, _ := p.MatchItem(item); match != nil {
|
||||
matches = append(matches, match)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, item := range *chunk {
|
||||
if offsets, bonus := p.extendedMatch(item); len(offsets) == len(p.termSets) {
|
||||
matches = append(matches, dupItem(item, offsets, bonus))
|
||||
for _, result := range space {
|
||||
if match, _ := p.MatchItem(result.item); match != nil {
|
||||
matches = append(matches, match)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,29 +288,22 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
|
||||
}
|
||||
|
||||
// MatchItem returns true if the Item is a match
|
||||
func (p *Pattern) MatchItem(item *Item) bool {
|
||||
if !p.extended {
|
||||
offset, _ := p.basicMatch(item)
|
||||
sidx := offset[0]
|
||||
return sidx >= 0
|
||||
func (p *Pattern) MatchItem(item *Item) (*Result, []Offset) {
|
||||
if p.extended {
|
||||
if offsets, bonus, trimLen := p.extendedMatch(item); len(offsets) == len(p.termSets) {
|
||||
return buildResult(item, offsets, bonus, trimLen), offsets
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
offsets, _ := p.extendedMatch(item)
|
||||
return len(offsets) == len(p.termSets)
|
||||
offset, bonus, trimLen := p.basicMatch(item)
|
||||
if sidx := offset[0]; sidx >= 0 {
|
||||
offsets := []Offset{offset}
|
||||
return buildResult(item, offsets, bonus, trimLen), offsets
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func dupItem(item *Item, offsets []Offset, bonus int32) *Item {
|
||||
sort.Sort(ByOrder(offsets))
|
||||
return &Item{
|
||||
text: item.text,
|
||||
origText: item.origText,
|
||||
transformed: item.transformed,
|
||||
offsets: offsets,
|
||||
bonus: bonus,
|
||||
colors: item.colors,
|
||||
rank: buildEmptyRank(item.Index())}
|
||||
}
|
||||
|
||||
func (p *Pattern) basicMatch(item *Item) (Offset, int32) {
|
||||
func (p *Pattern) basicMatch(item *Item) (Offset, int, int) {
|
||||
input := p.prepareInput(item)
|
||||
if p.fuzzy {
|
||||
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text)
|
||||
@@ -322,33 +311,39 @@ func (p *Pattern) basicMatch(item *Item) (Offset, int32) {
|
||||
return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text)
|
||||
}
|
||||
|
||||
func (p *Pattern) extendedMatch(item *Item) ([]Offset, int32) {
|
||||
func (p *Pattern) extendedMatch(item *Item) ([]Offset, int, int) {
|
||||
input := p.prepareInput(item)
|
||||
offsets := []Offset{}
|
||||
var totalBonus int32
|
||||
var totalBonus int
|
||||
var totalTrimLen int
|
||||
for _, termSet := range p.termSets {
|
||||
var offset *Offset
|
||||
var bonus int32
|
||||
var offset Offset
|
||||
var bonus int
|
||||
var trimLen int
|
||||
matched := false
|
||||
for _, term := range termSet {
|
||||
pfun := p.procFun[term.typ]
|
||||
off, pen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text)
|
||||
off, pen, tLen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text)
|
||||
if sidx := off[0]; sidx >= 0 {
|
||||
if term.inv {
|
||||
continue
|
||||
}
|
||||
offset, bonus = &off, pen
|
||||
offset, bonus, trimLen = off, pen, tLen
|
||||
matched = true
|
||||
break
|
||||
} else if term.inv {
|
||||
offset, bonus = &Offset{0, 0, 0}, 0
|
||||
offset, bonus, trimLen = Offset{0, 0}, 0, 0
|
||||
matched = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
if offset != nil {
|
||||
offsets = append(offsets, *offset)
|
||||
if matched {
|
||||
offsets = append(offsets, offset)
|
||||
totalBonus += bonus
|
||||
totalTrimLen += trimLen
|
||||
}
|
||||
}
|
||||
return offsets, totalBonus
|
||||
return offsets, totalBonus, totalTrimLen
|
||||
}
|
||||
|
||||
func (p *Pattern) prepareInput(item *Item) []Token {
|
||||
@@ -357,26 +352,24 @@ func (p *Pattern) prepareInput(item *Item) []Token {
|
||||
}
|
||||
|
||||
var ret []Token
|
||||
if len(p.nth) > 0 {
|
||||
if len(p.nth) == 0 {
|
||||
ret = []Token{Token{text: &item.text, prefixLength: 0, trimLength: int32(item.text.TrimLength())}}
|
||||
} else {
|
||||
tokens := Tokenize(item.text, p.delimiter)
|
||||
ret = Transform(tokens, p.nth)
|
||||
} else {
|
||||
ret = []Token{Token{text: item.text, prefixLength: 0, trimLength: util.TrimLen(item.text)}}
|
||||
}
|
||||
item.transformed = ret
|
||||
return ret
|
||||
}
|
||||
|
||||
func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) algo.Result,
|
||||
tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int32) {
|
||||
func (p *Pattern) iter(pfun func(bool, bool, util.Chars, []rune) algo.Result,
|
||||
tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int, int) {
|
||||
for _, part := range tokens {
|
||||
prefixLength := int32(part.prefixLength)
|
||||
if res := pfun(caseSensitive, forward, part.text, pattern); res.Start >= 0 {
|
||||
var sidx int32 = res.Start + prefixLength
|
||||
var eidx int32 = res.End + prefixLength
|
||||
return Offset{sidx, eidx, int32(part.trimLength)}, res.Bonus
|
||||
if res := pfun(caseSensitive, forward, *part.text, pattern); res.Start >= 0 {
|
||||
sidx := int32(res.Start) + part.prefixLength
|
||||
eidx := int32(res.End) + part.prefixLength
|
||||
return Offset{sidx, eidx}, res.Bonus, int(part.trimLength)
|
||||
}
|
||||
}
|
||||
// TODO: math.MaxUint16
|
||||
return Offset{-1, -1, -1}, 0.0
|
||||
return Offset{-1, -1}, 0, -1
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func TestParseTermsExtended(t *testing.T) {
|
||||
@@ -68,10 +69,10 @@ func TestParseTermsEmpty(t *testing.T) {
|
||||
func TestExact(t *testing.T) {
|
||||
defer clearPatternCache()
|
||||
clearPatternCache()
|
||||
pattern := BuildPattern(true, true, CaseSmart, true,
|
||||
pattern := BuildPattern(true, true, CaseSmart, true, true,
|
||||
[]Range{}, Delimiter{}, []rune("'abc"))
|
||||
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 {
|
||||
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
|
||||
}
|
||||
@@ -80,11 +81,11 @@ func TestExact(t *testing.T) {
|
||||
func TestEqual(t *testing.T) {
|
||||
defer clearPatternCache()
|
||||
clearPatternCache()
|
||||
pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$"))
|
||||
pattern := BuildPattern(true, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("^AbC$"))
|
||||
|
||||
match := func(str string, sidxExpected int32, eidxExpected int32) {
|
||||
match := func(str string, sidxExpected int, eidxExpected int) {
|
||||
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 {
|
||||
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
|
||||
}
|
||||
@@ -96,17 +97,17 @@ func TestEqual(t *testing.T) {
|
||||
func TestCaseSensitivity(t *testing.T) {
|
||||
defer clearPatternCache()
|
||||
clearPatternCache()
|
||||
pat1 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
pat1 := BuildPattern(true, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
clearPatternCache()
|
||||
pat2 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
pat2 := BuildPattern(true, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
clearPatternCache()
|
||||
pat3 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
pat3 := BuildPattern(true, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
clearPatternCache()
|
||||
pat4 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
pat4 := BuildPattern(true, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
clearPatternCache()
|
||||
pat5 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
pat5 := BuildPattern(true, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
clearPatternCache()
|
||||
pat6 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
pat6 := BuildPattern(true, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
|
||||
if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
|
||||
string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
|
||||
@@ -119,31 +120,37 @@ func TestCaseSensitivity(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOrigTextAndTransformed(t *testing.T) {
|
||||
pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg"))
|
||||
tokens := Tokenize([]rune("junegunn"), Delimiter{})
|
||||
pattern := BuildPattern(true, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("jg"))
|
||||
tokens := Tokenize(util.RunesToChars([]rune("junegunn")), Delimiter{})
|
||||
trans := Transform(tokens, []Range{Range{1, 1}})
|
||||
|
||||
origRunes := []rune("junegunn.choi")
|
||||
origBytes := []byte("junegunn.choi")
|
||||
for _, extended := range []bool{false, true} {
|
||||
chunk := Chunk{
|
||||
&Item{
|
||||
text: []rune("junegunn"),
|
||||
origText: &origRunes,
|
||||
text: util.RunesToChars([]rune("junegunn")),
|
||||
origText: &origBytes,
|
||||
transformed: trans},
|
||||
}
|
||||
pattern.extended = extended
|
||||
matches := pattern.matchChunk(&chunk)
|
||||
if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" ||
|
||||
matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 ||
|
||||
!reflect.DeepEqual(matches[0].transformed, trans) {
|
||||
matches := pattern.matchChunk(&chunk, nil) // No cache
|
||||
if matches[0].item.text.ToString() != "junegunn" || string(*matches[0].item.origText) != "junegunn.choi" ||
|
||||
!reflect.DeepEqual(matches[0].item.transformed, trans) {
|
||||
t.Error("Invalid match result", matches)
|
||||
}
|
||||
|
||||
match, offsets := pattern.MatchItem(chunk[0])
|
||||
if match.item.text.ToString() != "junegunn" || string(*match.item.origText) != "junegunn.choi" ||
|
||||
offsets[0][0] != 0 || offsets[0][1] != 5 ||
|
||||
!reflect.DeepEqual(match.item.transformed, trans) {
|
||||
t.Error("Invalid match result", match)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheKey(t *testing.T) {
|
||||
test := func(extended bool, patStr string, expected string, cacheable bool) {
|
||||
pat := BuildPattern(true, extended, CaseSmart, true, []Range{}, Delimiter{}, []rune(patStr))
|
||||
pat := BuildPattern(true, extended, CaseSmart, true, true, []Range{}, Delimiter{}, []rune(patStr))
|
||||
if pat.CacheKey() != expected {
|
||||
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ func (r *Reader) feed(src io.Reader) {
|
||||
if r.delimNil {
|
||||
delim = '\000'
|
||||
}
|
||||
reader := bufio.NewReader(src)
|
||||
reader := bufio.NewReaderSize(src, readerBufferSize)
|
||||
for {
|
||||
// ReadBytes returns err != nil if and only if the returned data does not
|
||||
// end in delim.
|
||||
|
258
src/result.go
Normal file
258
src/result.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
"github.com/junegunn/fzf/src/curses"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
// Offset holds two 32-bit integers denoting the offsets of a matched substring
|
||||
type Offset [2]int32
|
||||
|
||||
type colorOffset struct {
|
||||
offset [2]int32
|
||||
color int
|
||||
bold bool
|
||||
index int32
|
||||
}
|
||||
|
||||
type rank struct {
|
||||
// byMatchLen, byBonus, ...
|
||||
points [5]uint16
|
||||
index int32
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
item *Item
|
||||
rank rank
|
||||
}
|
||||
|
||||
func buildResult(item *Item, offsets []Offset, bonus int, trimLen int) *Result {
|
||||
if len(offsets) > 1 {
|
||||
sort.Sort(ByOrder(offsets))
|
||||
}
|
||||
|
||||
result := Result{item: item, rank: rank{index: item.index}}
|
||||
|
||||
matchlen := 0
|
||||
prevEnd := 0
|
||||
minBegin := math.MaxInt32
|
||||
numChars := item.text.Length()
|
||||
for _, offset := range offsets {
|
||||
begin := int(offset[0])
|
||||
end := int(offset[1])
|
||||
if prevEnd > begin {
|
||||
begin = prevEnd
|
||||
}
|
||||
if end > prevEnd {
|
||||
prevEnd = end
|
||||
}
|
||||
if end > begin {
|
||||
if begin < minBegin {
|
||||
minBegin = begin
|
||||
}
|
||||
matchlen += end - begin
|
||||
}
|
||||
}
|
||||
|
||||
for idx, criterion := range sortCriteria {
|
||||
var val uint16
|
||||
switch criterion {
|
||||
case byMatchLen:
|
||||
if matchlen == 0 {
|
||||
val = math.MaxUint16
|
||||
} else {
|
||||
val = util.AsUint16(matchlen)
|
||||
}
|
||||
case byBonus:
|
||||
// Higher is better
|
||||
val = math.MaxUint16 - util.AsUint16(bonus)
|
||||
case byLength:
|
||||
// If offsets is empty, trimLen will be 0, but we don't care
|
||||
val = util.AsUint16(trimLen)
|
||||
case byBegin:
|
||||
// We can't just look at item.offsets[0][0] because it can be an inverse term
|
||||
whitePrefixLen := 0
|
||||
for idx := 0; idx < numChars; idx++ {
|
||||
r := item.text.Get(idx)
|
||||
whitePrefixLen = idx
|
||||
if idx == minBegin || r != ' ' && r != '\t' {
|
||||
break
|
||||
}
|
||||
}
|
||||
val = util.AsUint16(minBegin - whitePrefixLen)
|
||||
case byEnd:
|
||||
if prevEnd > 0 {
|
||||
val = util.AsUint16(1 + numChars - prevEnd)
|
||||
} else {
|
||||
// Empty offsets due to inverse terms.
|
||||
val = 1
|
||||
}
|
||||
}
|
||||
result.rank.points[idx] = val
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
// Sort criteria to use. Never changes once fzf is started.
|
||||
var sortCriteria []criterion
|
||||
|
||||
// Index returns ordinal index of the Item
|
||||
func (result *Result) Index() int32 {
|
||||
return result.item.index
|
||||
}
|
||||
|
||||
func minRank() rank {
|
||||
return rank{index: 0, points: [5]uint16{0, math.MaxUint16, 0, 0, 0}}
|
||||
}
|
||||
|
||||
func (result *Result) colorOffsets(matchOffsets []Offset, color int, bold bool, current bool) []colorOffset {
|
||||
itemColors := result.item.Colors()
|
||||
|
||||
if len(itemColors) == 0 {
|
||||
var offsets []colorOffset
|
||||
for _, off := range matchOffsets {
|
||||
|
||||
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold})
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
|
||||
// Find max column
|
||||
var maxCol int32
|
||||
for _, off := range matchOffsets {
|
||||
if off[1] > maxCol {
|
||||
maxCol = off[1]
|
||||
}
|
||||
}
|
||||
for _, ansi := range itemColors {
|
||||
if ansi.offset[1] > maxCol {
|
||||
maxCol = ansi.offset[1]
|
||||
}
|
||||
}
|
||||
cols := make([]int, maxCol)
|
||||
|
||||
for colorIndex, ansi := range itemColors {
|
||||
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
|
||||
cols[i] = colorIndex + 1 // XXX
|
||||
}
|
||||
}
|
||||
|
||||
for _, off := range matchOffsets {
|
||||
for i := off[0]; i < off[1]; i++ {
|
||||
cols[i] = -1
|
||||
}
|
||||
}
|
||||
|
||||
// sort.Sort(ByOrder(offsets))
|
||||
|
||||
// Merge offsets
|
||||
// ------------ ---- -- ----
|
||||
// ++++++++ ++++++++++
|
||||
// --++++++++-- --++++++++++---
|
||||
curr := 0
|
||||
start := 0
|
||||
var colors []colorOffset
|
||||
add := func(idx int) {
|
||||
if curr != 0 && idx > start {
|
||||
if curr == -1 {
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold})
|
||||
} else {
|
||||
ansi := itemColors[curr-1]
|
||||
fg := ansi.color.fg
|
||||
if fg == -1 {
|
||||
if current {
|
||||
fg = curses.CurrentFG
|
||||
} else {
|
||||
fg = curses.FG
|
||||
}
|
||||
}
|
||||
bg := ansi.color.bg
|
||||
if bg == -1 {
|
||||
if current {
|
||||
bg = curses.DarkBG
|
||||
} else {
|
||||
bg = curses.BG
|
||||
}
|
||||
}
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)},
|
||||
color: curses.PairFor(fg, bg),
|
||||
bold: ansi.color.bold || bold})
|
||||
}
|
||||
}
|
||||
}
|
||||
for idx, col := range cols {
|
||||
if col != curr {
|
||||
add(idx)
|
||||
start = idx
|
||||
curr = col
|
||||
}
|
||||
}
|
||||
add(int(maxCol))
|
||||
return colors
|
||||
}
|
||||
|
||||
// ByOrder is for sorting substring offsets
|
||||
type ByOrder []Offset
|
||||
|
||||
func (a ByOrder) Len() int {
|
||||
return len(a)
|
||||
}
|
||||
|
||||
func (a ByOrder) Swap(i, j int) {
|
||||
a[i], a[j] = a[j], a[i]
|
||||
}
|
||||
|
||||
func (a ByOrder) Less(i, j int) bool {
|
||||
ioff := a[i]
|
||||
joff := a[j]
|
||||
return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
|
||||
}
|
||||
|
||||
// ByRelevance is for sorting Items
|
||||
type ByRelevance []*Result
|
||||
|
||||
func (a ByRelevance) Len() int {
|
||||
return len(a)
|
||||
}
|
||||
|
||||
func (a ByRelevance) Swap(i, j int) {
|
||||
a[i], a[j] = a[j], a[i]
|
||||
}
|
||||
|
||||
func (a ByRelevance) Less(i, j int) bool {
|
||||
return compareRanks((*a[i]).rank, (*a[j]).rank, false)
|
||||
}
|
||||
|
||||
// ByRelevanceTac is for sorting Items
|
||||
type ByRelevanceTac []*Result
|
||||
|
||||
func (a ByRelevanceTac) Len() int {
|
||||
return len(a)
|
||||
}
|
||||
|
||||
func (a ByRelevanceTac) Swap(i, j int) {
|
||||
a[i], a[j] = a[j], a[i]
|
||||
}
|
||||
|
||||
func (a ByRelevanceTac) Less(i, j int) bool {
|
||||
return compareRanks((*a[i]).rank, (*a[j]).rank, true)
|
||||
}
|
||||
|
||||
func compareRanks(irank rank, jrank rank, tac bool) bool {
|
||||
for idx := 0; idx < 5; idx++ {
|
||||
left := irank.points[idx]
|
||||
right := jrank.points[idx]
|
||||
if left < right {
|
||||
return true
|
||||
} else if left > right {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return (irank.index <= jrank.index) != tac
|
||||
}
|
115
src/result_test.go
Normal file
115
src/result_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/curses"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func TestOffsetSort(t *testing.T) {
|
||||
offsets := []Offset{
|
||||
Offset{3, 5}, Offset{2, 7},
|
||||
Offset{1, 3}, Offset{2, 9}}
|
||||
sort.Sort(ByOrder(offsets))
|
||||
|
||||
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
|
||||
offsets[1][0] != 2 || offsets[1][1] != 7 ||
|
||||
offsets[2][0] != 2 || offsets[2][1] != 9 ||
|
||||
offsets[3][0] != 3 || offsets[3][1] != 5 {
|
||||
t.Error("Invalid order:", offsets)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRankComparison(t *testing.T) {
|
||||
rank := func(vals ...uint16) rank {
|
||||
return rank{
|
||||
points: [5]uint16{vals[0], 0, vals[1], vals[2], vals[3]},
|
||||
index: int32(vals[4])}
|
||||
}
|
||||
if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) ||
|
||||
!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
|
||||
!compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), false) ||
|
||||
!compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) {
|
||||
t.Error("Invalid order")
|
||||
}
|
||||
|
||||
if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), true) ||
|
||||
!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
|
||||
!compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), true) ||
|
||||
!compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) {
|
||||
t.Error("Invalid order (tac)")
|
||||
}
|
||||
}
|
||||
|
||||
// Match length, string length, index
|
||||
func TestResultRank(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byMatchLen, byBonus, byLength}
|
||||
|
||||
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
|
||||
item1 := buildResult(&Item{text: util.RunesToChars(strs[0]), index: 1}, []Offset{}, 2, 3)
|
||||
if item1.rank.points[0] != math.MaxUint16 || item1.rank.points[1] != math.MaxUint16-2 || item1.rank.points[2] != 3 || item1.item.index != 1 {
|
||||
t.Error(item1.rank)
|
||||
}
|
||||
// Only differ in index
|
||||
item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2, 3)
|
||||
|
||||
items := []*Result{item1, item2}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if items[0] != item2 || items[1] != item1 {
|
||||
t.Error(items)
|
||||
}
|
||||
|
||||
items = []*Result{item2, item1, item1, item2}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if items[0] != item2 || items[1] != item2 ||
|
||||
items[2] != item1 || items[3] != item1 {
|
||||
t.Error(items, item1, item1.item.index, item2, item2.item.index)
|
||||
}
|
||||
|
||||
// Sort by relevance
|
||||
item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 0, 0)
|
||||
item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 0, 0)
|
||||
item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 0, 0)
|
||||
item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 0, 0)
|
||||
items = []*Result{item1, item2, item3, item4, item5, item6}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if items[0] != item6 || items[1] != item4 ||
|
||||
items[2] != item5 || items[3] != item3 ||
|
||||
items[4] != item2 || items[5] != item1 {
|
||||
t.Error(items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorOffset(t *testing.T) {
|
||||
// ------------ 20 ---- -- ----
|
||||
// ++++++++ ++++++++++
|
||||
// --++++++++-- --++++++++++---
|
||||
|
||||
offsets := []Offset{Offset{5, 15}, Offset{25, 35}}
|
||||
item := Result{
|
||||
item: &Item{
|
||||
colors: &[]ansiOffset{
|
||||
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}},
|
||||
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}},
|
||||
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}},
|
||||
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}}
|
||||
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
|
||||
|
||||
colors := item.colorOffsets(offsets, 99, false, true)
|
||||
assert := func(idx int, b int32, e int32, c int, bold bool) {
|
||||
o := colors[idx]
|
||||
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold {
|
||||
t.Error(o)
|
||||
}
|
||||
}
|
||||
assert(0, 0, 5, curses.ColUser, false)
|
||||
assert(1, 5, 15, 99, false)
|
||||
assert(2, 15, 20, curses.ColUser, false)
|
||||
assert(3, 22, 25, curses.ColUser+1, true)
|
||||
assert(4, 25, 35, 99, false)
|
||||
assert(5, 35, 40, curses.ColUser+2, true)
|
||||
}
|
@@ -77,7 +77,7 @@ type Terminal struct {
|
||||
|
||||
type selectedItem struct {
|
||||
at time.Time
|
||||
text *string
|
||||
text string
|
||||
}
|
||||
|
||||
type byTimeOrder []selectedItem
|
||||
@@ -357,7 +357,7 @@ func (t *Terminal) output() bool {
|
||||
}
|
||||
} else {
|
||||
for _, sel := range t.sortSelected() {
|
||||
fmt.Println(*sel.text)
|
||||
fmt.Println(sel.text)
|
||||
}
|
||||
}
|
||||
return found
|
||||
@@ -564,12 +564,11 @@ func (t *Terminal) printHeader() {
|
||||
trimmed, colors, newState := extractColor(lineStr, state, nil)
|
||||
state = newState
|
||||
item := &Item{
|
||||
text: []rune(trimmed),
|
||||
colors: colors,
|
||||
rank: buildEmptyRank(0)}
|
||||
text: util.RunesToChars([]rune(trimmed)),
|
||||
colors: colors}
|
||||
|
||||
t.move(line, 2, true)
|
||||
t.printHighlighted(item, false, C.ColHeader, 0, false)
|
||||
t.printHighlighted(&Result{item: item}, false, C.ColHeader, 0, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,7 +589,8 @@ func (t *Terminal) printList() {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Terminal) printItem(item *Item, i int, current bool) {
|
||||
func (t *Terminal) printItem(result *Result, i int, current bool) {
|
||||
item := result.item
|
||||
_, selected := t.selected[item.Index()]
|
||||
label := " "
|
||||
if t.jumping != jumpDisabled {
|
||||
@@ -609,14 +609,14 @@ func (t *Terminal) printItem(item *Item, i int, current bool) {
|
||||
} else {
|
||||
t.window.CPrint(C.ColCurrent, true, " ")
|
||||
}
|
||||
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true)
|
||||
t.printHighlighted(result, true, C.ColCurrent, C.ColCurrentMatch, true)
|
||||
} else {
|
||||
if selected {
|
||||
t.window.CPrint(C.ColSelected, true, ">")
|
||||
} else {
|
||||
t.window.Print(" ")
|
||||
}
|
||||
t.printHighlighted(item, false, 0, C.ColMatch, false)
|
||||
t.printHighlighted(result, false, 0, C.ColMatch, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,29 +656,43 @@ func trimLeft(runes []rune, width int) ([]rune, int32) {
|
||||
return runes, trimmed
|
||||
}
|
||||
|
||||
func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) {
|
||||
var maxe int
|
||||
for _, offset := range item.offsets {
|
||||
maxe = util.Max(maxe, int(offset[1]))
|
||||
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(result *Result, bold bool, col1 int, col2 int, current bool) {
|
||||
item := result.item
|
||||
|
||||
// Overflow
|
||||
text := make([]rune, len(item.text))
|
||||
copy(text, item.text)
|
||||
offsets := item.colorOffsets(col2, bold, current)
|
||||
text := make([]rune, item.text.Length())
|
||||
copy(text, item.text.ToRunes())
|
||||
matchOffsets := []Offset{}
|
||||
if t.merger.pattern != nil {
|
||||
_, matchOffsets = t.merger.pattern.MatchItem(item)
|
||||
}
|
||||
var maxe int
|
||||
for _, offset := range matchOffsets {
|
||||
maxe = util.Max(maxe, int(offset[1]))
|
||||
}
|
||||
offsets := result.colorOffsets(matchOffsets, col2, bold, current)
|
||||
maxWidth := t.window.Width - 3
|
||||
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
|
||||
fullWidth := displayWidth(text)
|
||||
if fullWidth > maxWidth {
|
||||
if overflow(text, maxWidth) {
|
||||
if t.hscroll {
|
||||
// Stri..
|
||||
matchEndWidth := displayWidth(text[:maxe])
|
||||
if matchEndWidth <= maxWidth-2 {
|
||||
if !overflow(text[:maxe], maxWidth-2) {
|
||||
text, _ = trimRight(text, maxWidth-2)
|
||||
text = append(text, []rune("..")...)
|
||||
} else {
|
||||
// Stri..
|
||||
if matchEndWidth < fullWidth-2 {
|
||||
if overflow(text[maxe:], 2) {
|
||||
text = append(text[:maxe], []rune("..")...)
|
||||
}
|
||||
// ..ri..
|
||||
@@ -857,7 +871,7 @@ func (t *Terminal) isPreviewEnabled() bool {
|
||||
}
|
||||
|
||||
func (t *Terminal) current() string {
|
||||
return t.merger.Get(t.cy).AsString(t.ansi)
|
||||
return t.merger.Get(t.cy).item.AsString(t.ansi)
|
||||
}
|
||||
|
||||
// Loop is called to start Terminal I/O
|
||||
@@ -1028,13 +1042,13 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
selectItem := func(item *Item) bool {
|
||||
if _, found := t.selected[item.Index()]; !found {
|
||||
t.selected[item.Index()] = selectedItem{time.Now(), item.StringPtr(t.ansi)}
|
||||
t.selected[item.Index()] = selectedItem{time.Now(), item.AsString(t.ansi)}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
toggleY := func(y int) {
|
||||
item := t.merger.Get(y)
|
||||
item := t.merger.Get(y).item
|
||||
if !selectItem(item) {
|
||||
delete(t.selected, item.Index())
|
||||
}
|
||||
@@ -1059,14 +1073,14 @@ func (t *Terminal) Loop() {
|
||||
case actIgnore:
|
||||
case actExecute:
|
||||
if t.cy >= 0 && t.cy < t.merger.Length() {
|
||||
item := t.merger.Get(t.cy)
|
||||
item := t.merger.Get(t.cy).item
|
||||
t.executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi)))
|
||||
}
|
||||
case actExecuteMulti:
|
||||
if len(t.selected) > 0 {
|
||||
sels := make([]string, len(t.selected))
|
||||
for i, sel := range t.sortSelected() {
|
||||
sels[i] = quoteEntry(*sel.text)
|
||||
sels[i] = quoteEntry(sel.text)
|
||||
}
|
||||
t.executeCommand(t.execmap[mapkey], strings.Join(sels, " "))
|
||||
} else {
|
||||
@@ -1128,7 +1142,7 @@ func (t *Terminal) Loop() {
|
||||
case actSelectAll:
|
||||
if t.multi {
|
||||
for i := 0; i < t.merger.Length(); i++ {
|
||||
item := t.merger.Get(i)
|
||||
item := t.merger.Get(i).item
|
||||
selectItem(item)
|
||||
}
|
||||
req(reqList, reqInfo)
|
||||
|
102
src/tokenizer.go
102
src/tokenizer.go
@@ -18,9 +18,9 @@ type Range struct {
|
||||
|
||||
// Token contains the tokenized part of the strings and its prefix length
|
||||
type Token struct {
|
||||
text []rune
|
||||
prefixLength int
|
||||
trimLength int
|
||||
text *util.Chars
|
||||
prefixLength int32
|
||||
trimLength int32
|
||||
}
|
||||
|
||||
// Delimiter for tokenizing the input
|
||||
@@ -75,15 +75,14 @@ func ParseRange(str *string) (Range, bool) {
|
||||
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))
|
||||
|
||||
prefixLength := begin
|
||||
for idx, token := range tokens {
|
||||
// Need to define a new local variable instead of the reused token to take
|
||||
// the pointer to it
|
||||
ret[idx] = Token{token, prefixLength, util.TrimLen(token)}
|
||||
prefixLength += len(token)
|
||||
// NOTE: &tokens[idx] instead of &tokens
|
||||
ret[idx] = Token{&tokens[idx], int32(prefixLength), int32(token.TrimLength())}
|
||||
prefixLength += token.Length()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -94,59 +93,60 @@ const (
|
||||
awkWhite
|
||||
)
|
||||
|
||||
func awkTokenizer(input []rune) ([][]rune, int) {
|
||||
func awkTokenizer(input util.Chars) ([]util.Chars, int) {
|
||||
// 9, 32
|
||||
ret := [][]rune{}
|
||||
str := []rune{}
|
||||
ret := []util.Chars{}
|
||||
prefixLength := 0
|
||||
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
|
||||
switch state {
|
||||
case awkNil:
|
||||
if white {
|
||||
prefixLength++
|
||||
} else {
|
||||
state = awkBlack
|
||||
str = append(str, r)
|
||||
state, begin, end = awkBlack, idx, idx+1
|
||||
}
|
||||
case awkBlack:
|
||||
str = append(str, r)
|
||||
end = idx + 1
|
||||
if white {
|
||||
state = awkWhite
|
||||
}
|
||||
case awkWhite:
|
||||
if white {
|
||||
str = append(str, r)
|
||||
end = idx + 1
|
||||
} else {
|
||||
ret = append(ret, str)
|
||||
state = awkBlack
|
||||
str = []rune{r}
|
||||
ret = append(ret, input.Slice(begin, end))
|
||||
state, begin, end = awkBlack, idx, idx+1
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(str) > 0 {
|
||||
ret = append(ret, str)
|
||||
if begin < end {
|
||||
ret = append(ret, input.Slice(begin, end))
|
||||
}
|
||||
return ret, prefixLength
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// AWK-style (\S+\s*)
|
||||
tokens, prefixLength := awkTokenizer(runes)
|
||||
tokens, prefixLength := awkTokenizer(text)
|
||||
return withPrefixLengths(tokens, prefixLength)
|
||||
}
|
||||
|
||||
var tokens []string
|
||||
if delimiter.str != nil {
|
||||
tokens = strings.Split(string(runes), *delimiter.str)
|
||||
for i := 0; i < len(tokens)-1; i++ {
|
||||
tokens[i] = tokens[i] + *delimiter.str
|
||||
}
|
||||
} else if delimiter.regex != nil {
|
||||
str := string(runes)
|
||||
return withPrefixLengths(text.Split(*delimiter.str), 0)
|
||||
}
|
||||
|
||||
// FIXME performance
|
||||
var tokens []string
|
||||
if delimiter.regex != nil {
|
||||
str := text.ToString()
|
||||
for len(str) > 0 {
|
||||
loc := delimiter.regex.FindStringIndex(str)
|
||||
if loc == nil {
|
||||
@@ -157,9 +157,9 @@ func Tokenize(runes []rune, delimiter Delimiter) []Token {
|
||||
str = str[last:]
|
||||
}
|
||||
}
|
||||
asRunes := make([][]rune, len(tokens))
|
||||
asRunes := make([]util.Chars, len(tokens))
|
||||
for i, token := range tokens {
|
||||
asRunes[i] = []rune(token)
|
||||
asRunes[i] = util.RunesToChars([]rune(token))
|
||||
}
|
||||
return withPrefixLengths(asRunes, 0)
|
||||
}
|
||||
@@ -167,15 +167,7 @@ func Tokenize(runes []rune, delimiter Delimiter) []Token {
|
||||
func joinTokens(tokens []Token) []rune {
|
||||
ret := []rune{}
|
||||
for _, token := range tokens {
|
||||
ret = append(ret, token.text...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func joinTokensAsRunes(tokens []Token) []rune {
|
||||
ret := []rune{}
|
||||
for _, token := range tokens {
|
||||
ret = append(ret, token.text...)
|
||||
ret = append(ret, token.text.ToRunes()...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -185,19 +177,20 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
||||
transTokens := make([]Token, len(withNth))
|
||||
numTokens := len(tokens)
|
||||
for idx, r := range withNth {
|
||||
part := []rune{}
|
||||
parts := []*util.Chars{}
|
||||
minIdx := 0
|
||||
if r.begin == r.end {
|
||||
idx := r.begin
|
||||
if idx == rangeEllipsis {
|
||||
part = append(part, joinTokensAsRunes(tokens)...)
|
||||
chars := util.RunesToChars(joinTokens(tokens))
|
||||
parts = append(parts, &chars)
|
||||
} else {
|
||||
if idx < 0 {
|
||||
idx += numTokens + 1
|
||||
}
|
||||
if idx >= 1 && idx <= numTokens {
|
||||
minIdx = idx - 1
|
||||
part = append(part, tokens[idx-1].text...)
|
||||
parts = append(parts, tokens[idx-1].text)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -224,17 +217,32 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
||||
minIdx = util.Max(0, begin-1)
|
||||
for idx := begin; idx <= end; idx++ {
|
||||
if idx >= 1 && idx <= numTokens {
|
||||
part = append(part, tokens[idx-1].text...)
|
||||
parts = append(parts, tokens[idx-1].text)
|
||||
}
|
||||
}
|
||||
}
|
||||
var prefixLength int
|
||||
// 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 int32
|
||||
if minIdx < numTokens {
|
||||
prefixLength = tokens[minIdx].prefixLength
|
||||
} else {
|
||||
prefixLength = 0
|
||||
}
|
||||
transTokens[idx] = Token{part, prefixLength, util.TrimLen(part)}
|
||||
transTokens[idx] = Token{&merged, prefixLength, int32(merged.TrimLength())}
|
||||
}
|
||||
return transTokens
|
||||
}
|
||||
|
@@ -1,6 +1,10 @@
|
||||
package fzf
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func TestParseRange(t *testing.T) {
|
||||
{
|
||||
@@ -43,23 +47,23 @@ func TestParseRange(t *testing.T) {
|
||||
func TestTokenize(t *testing.T) {
|
||||
// AWK-style
|
||||
input := " abc: def: ghi "
|
||||
tokens := Tokenize([]rune(input), Delimiter{})
|
||||
if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 {
|
||||
tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
|
||||
if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 {
|
||||
t.Errorf("%s", tokens)
|
||||
}
|
||||
|
||||
// With delimiter
|
||||
tokens = Tokenize([]rune(input), delimiterRegexp(":"))
|
||||
if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 {
|
||||
tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
|
||||
if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 {
|
||||
t.Errorf("%s", tokens)
|
||||
}
|
||||
|
||||
// With delimiter regex
|
||||
tokens = Tokenize([]rune(input), delimiterRegexp("\\s+"))
|
||||
if string(tokens[0].text) != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 ||
|
||||
string(tokens[1].text) != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 ||
|
||||
string(tokens[2].text) != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 ||
|
||||
string(tokens[3].text) != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 {
|
||||
tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp("\\s+"))
|
||||
if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 ||
|
||||
tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 ||
|
||||
tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 ||
|
||||
tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 {
|
||||
t.Errorf("%s", tokens)
|
||||
}
|
||||
}
|
||||
@@ -67,7 +71,7 @@ func TestTokenize(t *testing.T) {
|
||||
func TestTransform(t *testing.T) {
|
||||
input := " abc: def: ghi: jkl"
|
||||
{
|
||||
tokens := Tokenize([]rune(input), Delimiter{})
|
||||
tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
|
||||
{
|
||||
ranges := splitNth("1,2,3")
|
||||
tx := Transform(tokens, ranges)
|
||||
@@ -80,25 +84,25 @@ func TestTransform(t *testing.T) {
|
||||
tx := Transform(tokens, ranges)
|
||||
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
|
||||
len(tx) != 4 ||
|
||||
string(tx[0].text) != "abc: def: " || tx[0].prefixLength != 2 ||
|
||||
string(tx[1].text) != "ghi: " || tx[1].prefixLength != 14 ||
|
||||
string(tx[2].text) != "def: ghi: jkl" || tx[2].prefixLength != 8 ||
|
||||
string(tx[3].text) != "abc: " || tx[3].prefixLength != 2 {
|
||||
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
|
||||
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
|
||||
tx[2].text.ToString() != "def: ghi: jkl" || tx[2].prefixLength != 8 ||
|
||||
tx[3].text.ToString() != "abc: " || tx[3].prefixLength != 2 {
|
||||
t.Errorf("%s", tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
tokens := Tokenize([]rune(input), delimiterRegexp(":"))
|
||||
tokens := Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
|
||||
{
|
||||
ranges := splitNth("1..2,3,2..,1")
|
||||
tx := Transform(tokens, ranges)
|
||||
if string(joinTokens(tx)) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
||||
len(tx) != 4 ||
|
||||
string(tx[0].text) != " abc: def:" || tx[0].prefixLength != 0 ||
|
||||
string(tx[1].text) != " ghi:" || tx[1].prefixLength != 12 ||
|
||||
string(tx[2].text) != " def: ghi: jkl" || tx[2].prefixLength != 6 ||
|
||||
string(tx[3].text) != " abc:" || tx[3].prefixLength != 0 {
|
||||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
|
||||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
|
||||
tx[2].text.ToString() != " def: ghi: jkl" || tx[2].prefixLength != 6 ||
|
||||
tx[3].text.ToString() != " abc:" || tx[3].prefixLength != 0 {
|
||||
t.Errorf("%s", tx)
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
# http://www.rubydoc.info/github/rest-client/rest-client/RestClient
|
||||
require 'rest_client'
|
||||
require 'json'
|
||||
|
||||
if ARGV.length < 3
|
||||
puts "usage: #$0 <token> <version> <files...>"
|
||||
|
156
src/util/chars.go
Normal file
156
src/util/chars.go
Normal 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
82
src/util/chars_test.go
Normal 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", "")
|
||||
}
|
@@ -4,21 +4,18 @@ package util
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Max returns the largest integer
|
||||
func Max(first int, items ...int) int {
|
||||
max := first
|
||||
for _, item := range items {
|
||||
if item > max {
|
||||
max = item
|
||||
}
|
||||
func Max(first int, second int) int {
|
||||
if first >= second {
|
||||
return first
|
||||
}
|
||||
return max
|
||||
return second
|
||||
}
|
||||
|
||||
// Min returns the smallest integer
|
||||
@@ -67,6 +64,15 @@ func Constrain(val int, min int, max int) int {
|
||||
return val
|
||||
}
|
||||
|
||||
func AsUint16(val int) uint16 {
|
||||
if val > math.MaxUint16 {
|
||||
return math.MaxUint16
|
||||
} else if val < 0 {
|
||||
return 0
|
||||
}
|
||||
return uint16(val)
|
||||
}
|
||||
|
||||
// DurWithin limits the given time.Duration with the upper and lower bounds
|
||||
func DurWithin(
|
||||
val time.Duration, min time.Duration, max time.Duration) time.Duration {
|
||||
@@ -84,58 +90,6 @@ func IsTty() bool {
|
||||
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
|
||||
func ExecCommand(command string) *exec.Cmd {
|
||||
shell := os.Getenv("SHELL")
|
||||
|
@@ -3,7 +3,7 @@ package util
|
||||
import "testing"
|
||||
|
||||
func TestMax(t *testing.T) {
|
||||
if Max(-2, 5, 1, 4, 3) != 5 {
|
||||
if Max(-2, 5) != 5 {
|
||||
t.Error("Invalid result")
|
||||
}
|
||||
}
|
||||
@@ -20,23 +20,3 @@ func TestContrain(t *testing.T) {
|
||||
t.Error("Expected", 3)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrimLen(t *testing.T) {
|
||||
check := func(str string, exp int) {
|
||||
trimmed := TrimLen([]rune(str))
|
||||
if trimmed != exp {
|
||||
t.Errorf("Invalid TrimLen result for '%s': %d (expected %d)",
|
||||
str, trimmed, exp)
|
||||
}
|
||||
}
|
||||
check("hello", 5)
|
||||
check("hello ", 5)
|
||||
check("hello ", 5)
|
||||
check(" hello", 5)
|
||||
check(" hello", 5)
|
||||
check(" hello ", 5)
|
||||
check(" hello ", 5)
|
||||
check("h o", 5)
|
||||
check(" h o ", 5)
|
||||
check(" ", 0)
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
Execute (Setup):
|
||||
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
|
||||
Save &acd
|
||||
|
||||
@@ -43,6 +44,11 @@ Execute (fzf#run with dir option and noautochdir):
|
||||
" No change in working directory
|
||||
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):
|
||||
set acd
|
||||
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
|
||||
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):
|
||||
unlet g:dir
|
||||
Restore
|
||||
|
@@ -36,6 +36,10 @@ end
|
||||
|
||||
class Shell
|
||||
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
|
||||
'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.bash'
|
||||
end
|
||||
@@ -45,6 +49,10 @@ class Shell
|
||||
FileUtils.cp File.expand_path('~/.fzf.zsh'), '/tmp/fzf-zsh/.zshrc'
|
||||
'PS1= PROMPT_COMMAND= HISTSIZE=100 ZDOTDIR=/tmp/fzf-zsh zsh'
|
||||
end
|
||||
|
||||
def fish
|
||||
'fish'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -57,11 +65,11 @@ class Tmux
|
||||
@win =
|
||||
case shell
|
||||
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
|
||||
go("new-window -d -P -F '#I' '#{Shell.zsh}'").first
|
||||
go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.zsh}'").first
|
||||
when :fish
|
||||
go("new-window -d -P -F '#I' 'fish'").first
|
||||
go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.fish}'").first
|
||||
else
|
||||
raise "Unknown shell: #{shell}"
|
||||
end
|
||||
@@ -90,6 +98,10 @@ class Tmux
|
||||
go("send-keys -t #{target} #{args}")
|
||||
end
|
||||
|
||||
def paste str
|
||||
%x[tmux setb '#{str.gsub("'", "'\\''")}' \\; pasteb -t #{win} \\; send-keys -t #{win} Enter]
|
||||
end
|
||||
|
||||
def capture pane = 0
|
||||
File.unlink TEMPNAME while File.exists? TEMPNAME
|
||||
wait do
|
||||
@@ -149,12 +161,6 @@ class TestBase < Minitest::Test
|
||||
@temp_suffix].join '-'
|
||||
end
|
||||
|
||||
def setup
|
||||
ENV.delete 'FZF_DEFAULT_OPTS'
|
||||
ENV.delete 'FZF_CTRL_T_COMMAND'
|
||||
ENV.delete 'FZF_DEFAULT_COMMAND'
|
||||
end
|
||||
|
||||
def readonce
|
||||
wait { File.exists?(tempname) }
|
||||
File.read(tempname)
|
||||
@@ -362,7 +368,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
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.send_keys :Enter
|
||||
assert_equal ['가나다'], readonce.split($/)
|
||||
@@ -1313,16 +1319,29 @@ module TestShell
|
||||
|
||||
def test_ctrl_t_unicode
|
||||
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.send_keys 'cat ', 'C-t', pane: 0
|
||||
tmux.until(1) { |lines| lines.item_count >= 1 }
|
||||
tmux.send_keys 'fzf-unicode', pane: 1
|
||||
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.send_keys :Enter, pane: 1
|
||||
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.until { |lines| lines[-1].include? 'test1test2' }
|
||||
end
|
||||
@@ -1530,12 +1549,24 @@ module CompletionTest
|
||||
|
||||
def test_file_completion_unicode
|
||||
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.send_keys 'cat fzf-unicode**', :Tab, pane: 0
|
||||
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.send_keys :Enter, pane: 1
|
||||
tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') }
|
||||
tmux.send_keys :Enter
|
||||
|
Reference in New Issue
Block a user