Compare commits

...

57 Commits

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

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

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

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

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

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

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

    real    0m4.862s
    user    0m6.619s
    sys     0m0.982s
2015-08-02 14:00:18 +09:00
Junegunn Choi
634670e3ea Lint 2015-08-02 13:11:59 +09:00
Junegunn Choi
dea60b11bc Only consider the lengths of the relevant parts when --nth is set 2015-08-01 23:13:24 +09:00
Junegunn Choi
5e90f0a57b Fix default command so that it doesn't fail on dash-prefixed files
Close #310
2015-08-01 21:51:10 +09:00
Junegunn Choi
0b4542fcdf [vim] Temporarily disable &autochdir when opening files (#306) 2015-07-29 17:55:58 +09:00
Junegunn Choi
02bd2d2adf Do not proceed if $TERM is invalid
Related #305
2015-07-28 14:35:46 +09:00
Junegunn Choi
dce6fe6f2d [fzf-tmux] Ensure that the same $TERM value is used in split
Fix #305. ncurses can crash on invalid $TERM. fzf-tmux uses bash on
a new pane so we have to make sure that the $TERM is consistent with
that of the hosting shell.
2015-07-28 14:17:25 +09:00
Junegunn Choi
fcae99f09b No need to "tmux list-panes" when obviously not on tmux (#303) 2015-07-28 00:56:03 +09:00
Junegunn Choi
fb1b026d3d Always check if the pane is zoomed
Close #303
2015-07-28 00:30:17 +09:00
Junegunn Choi
9f953fc944 Do not use tmux pane if the current pane is zoomed
Close #303
2015-07-28 00:22:04 +09:00
Junegunn Choi
909ea1a698 0.10.1 2015-07-27 00:09:07 +09:00
Junegunn Choi
7231acd442 Fix mouse scroll when --margin is set 2015-07-27 00:06:44 +09:00
Junegunn Choi
7814371a9a Revert "0.10.1"
This reverts commit 6166e2dd80.
2015-07-27 00:03:14 +09:00
Junegunn Choi
6166e2dd80 0.10.1 2015-07-26 23:57:26 +09:00
Junegunn Choi
ee0c8a2635 Add --margin option
Close #299
2015-07-26 23:02:04 +09:00
Junegunn Choi
2bebddefc0 Do not print the entire --help on invalid option 2015-07-26 13:39:34 +09:00
Junegunn Choi
fdbf3d3fec Replace eof action with cancel (#289) 2015-07-23 21:05:33 +09:00
Junegunn Choi
f9136cffe6 Update man page 2015-07-23 10:45:01 +09:00
Junegunn Choi
51d84b1869 [bash] Update fzf option completion 2015-07-23 00:58:20 +09:00
Junegunn Choi
13e040baee Bind CTRL-D to the new delete-char/eof action
- CTRL-D - delete-char/eof
- DEL - delete-char
2015-07-23 00:56:03 +09:00
Junegunn Choi
cc0d5539ba Add "eof" action which closes the finder only when input is empty
Close #289
2015-07-22 22:57:48 +09:00
Junegunn Choi
b53f61fc59 Remove cbreak before raw 2015-07-22 22:36:39 +09:00
Junegunn Choi
4e0e03403e Fix --header-lines unaffected by --with-nth 2015-07-22 21:24:02 +09:00
Junegunn Choi
928fccc15b Fix header not shown when the lines go beyond the screen limit 2015-07-22 21:22:59 +09:00
Junegunn Choi
bbaa3ab8bd Update CHANGELOG 2015-07-22 14:19:55 +09:00
Junegunn Choi
5e3cb3a4ea Fix ANSI processor to handle multi-line regions 2015-07-22 14:19:45 +09:00
Junegunn Choi
f71ea5f3ea Add test cases for header and fix corner cases 2015-07-22 13:45:38 +09:00
Junegunn Choi
f469c25730 Add --header-lines option 2015-07-22 03:21:20 +09:00
Junegunn Choi
18469b6954 Adjust header color for dark color scheme 2015-07-22 03:07:27 +09:00
Junegunn Choi
d01db4862b Update documentation 2015-07-22 01:12:50 +09:00
Junegunn Choi
8b2adba8d6 Redraw of header on resize 2015-07-22 00:47:14 +09:00
Junegunn Choi
d459e9abce Add --header-file option 2015-07-22 00:38:38 +09:00
Junegunn Choi
c9abe1b1ff Show more specific error message on invalid binding 2015-07-18 02:31:35 +09:00
Junegunn Choi
a0e6147bb5 Fix #292 - Allow binding of colon and comma 2015-07-16 21:14:08 +09:00
Junegunn Choi
b0f491d3c3 Fix travis CI build
- Fix test failures on new fish 2.2.0
- Make timeout-based test cases more robust
2015-07-13 19:24:22 +09:00
Junegunn Choi
392da53f53 [bash] Make CTRL-R work when histexpand is unset (#286)
Note that it still can't handle properly multi-line commands.
Thanks to @jpcirrus for the bug report and the fix.
2015-07-13 00:22:13 +09:00
Junegunn Choi
ae72b0fb70 Merge pull request #285 from evverx/possible-retry-loop
[bash-completion] Fix g++: possible retry loop
2015-07-04 11:24:08 +09:00
Evgeny Vereshchagin
a79d080ea8 Fix g++: possible retry loop
See http://unix.stackexchange.com/q/213432/120177
2015-07-04 01:20:36 +00:00
Junegunn Choi
ec85fd552d Update README - how to use ag with CTRL-T 2015-06-30 13:17:48 +09:00
Junegunn Choi
11db046fc7 [neovim] Fix #281 - Properly close window with winnr 1 2015-06-27 14:23:51 +09:00
Junegunn Choi
938151a834 [shell] Add FZF_CTRL_T_COMMAND for CTRL-T
Close #40
2015-06-26 01:02:44 +09:00
Junegunn Choi
14e3b84073 [zsh] No need to define __fsel in non-interactive shell
Since we now use fzf-tmux instead of tmux split-window
2015-06-26 00:14:36 +09:00
Junegunn Choi
56100f0fa7 [bash] Use command \find for ALT-C
ALT-C can fail with the following aliases as pointed out in #272

    alias find='noglob find'
    alias command='command '
2015-06-25 23:54:05 +09:00
Junegunn Choi
5254ee2e2a Update documentation (#277) 2015-06-22 01:35:36 +09:00
36 changed files with 1135 additions and 401 deletions

View File

@@ -1,6 +1,47 @@
CHANGELOG CHANGELOG
========= =========
0.10.3
------
- Fixed slow performance of `--with-nth` when used with `--delimiter`
- Regular expression engine of Golang as of now is very slow, so the fixed
version will treat the given delimiter pattern as a plain string instead
of a regular expression unless it contains special characters and is
a valid regular expression.
- Simpler regular expression for delimiter for better performance
0.10.2
------
### Fixes and improvements
- Improvement in perceived response time of queries
- Eager, efficient rune array conversion
- Graceful exit when failed to initialize ncurses (invalid $TERM)
- Improved ranking algorithm when `--nth` option is set
- Changed the default command not to fail when there are files whose names
start with dash
0.10.1
------
### New features
- Added `--margin` option
- Added options for sticky header
- `--header-file`
- `--header-lines`
- Added `cancel` action which clears the input or closes the finder when the
input is already empty
- e.g. `export FZF_DEFAULT_OPTS="--bind esc:cancel"`
- Added `delete-char/eof` action to differentiate `CTRL-D` and `DEL`
### Minor improvements/fixes
- Fixed to allow binding colon and comma keys
- Fixed ANSI processor to handle color regions spanning multiple lines
0.10.0 0.10.0
------ ------

View File

@@ -127,6 +127,13 @@ such as: `^music .mp3$ sbtrkt !rmx`
If you don't need fuzzy matching and do not wish to "quote" every word, start If you don't need fuzzy matching and do not wish to "quote" every word, start
fzf with `-e` or `--extended-exact` option. fzf with `-e` or `--extended-exact` option.
#### Environment variables
- `FZF_DEFAULT_COMMAND`
- Default command to use when input is tty
- `FZF_DEFAULT_OPTS`
- Default options. e.g. `export FZF_DEFAULT_OPTS="--extended --cycle"`
Examples Examples
-------- --------
@@ -140,8 +147,9 @@ Key bindings for command line
The install script will setup the following key bindings for bash, zsh, and The install script will setup the following key bindings for bash, zsh, and
fish. fish.
- `CTRL-T` - Paste the selected file path(s) into the command line - `CTRL-T` - Paste the selected files and directories onto the command line
- `CTRL-R` - Paste the selected command from history into the command line - Set `FZF_CTRL_T_COMMAND` to override the default command
- `CTRL-R` - Paste the selected command from history onto the command line
- Sort is disabled by default to respect chronological ordering - Sort is disabled by default to respect chronological ordering
- Press `CTRL-R` again to toggle sort - Press `CTRL-R` again to toggle sort
- `ALT-C` - cd into the selected directory - `ALT-C` - cd into the selected directory
@@ -390,6 +398,9 @@ export FZF_DEFAULT_COMMAND='ag -l -g ""'
# Now fzf (w/o pipe) will use ag instead of find # Now fzf (w/o pipe) will use ag instead of find
fzf fzf
# To apply the command to CTRL-T as well
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
``` ```
#### `git ls-tree` for fast traversal #### `git ls-tree` for fast traversal

View File

@@ -82,7 +82,7 @@ while [ $# -gt 0 ]; do
shift shift
done done
if [ -z "$TMUX_PANE" ]; then if [ -z "$TMUX_PANE" ] || tmux list-panes -F '#F' | grep -q Z; then
fzf "${args[@]}" fzf "${args[@]}"
exit $? exit $?
fi fi
@@ -107,7 +107,7 @@ fail() {
fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
[ -x "$fzf" ] || fail "fzf executable not found" [ -x "$fzf" ] || fail "fzf executable not found"
envs="env " envs="env TERM=$TERM "
[ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
[ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" [ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"

17
install
View File

@@ -1,6 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
version=0.10.0 [[ "$@" =~ --pre ]] && version=0.10.3 pre=1 ||
version=0.10.3 pre=0
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base=$(pwd)
@@ -45,11 +46,13 @@ symlink() {
download() { download() {
echo "Downloading bin/fzf ..." echo "Downloading bin/fzf ..."
if [[ ! $1 =~ dev && -x "$fzf_base"/bin/fzf ]]; then if [ $pre = 0 ]; then
echo " - Already exists" if [ -x "$fzf_base"/bin/fzf ]; then
check_binary && return echo " - Already exists"
elif [ -x "$fzf_base"/bin/$1 ]; then check_binary && return
symlink $1 && check_binary && return elif [ -x "$fzf_base"/bin/$1 ]; then
symlink $1 && check_binary && return
fi
fi fi
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@@ -93,6 +96,7 @@ if [ -n "$binary_error" ]; then
echo "No prebuilt binary for $archi ... " echo "No prebuilt binary for $archi ... "
else else
echo " - $binary_error !!!" echo " - $binary_error !!!"
exit 1
fi fi
echo "Installing legacy Ruby version ..." echo "Installing legacy Ruby version ..."
@@ -247,6 +251,7 @@ append_line() {
if [ -n "$line" ]; then if [ -n "$line" ]; then
echo " - Already exists: line #$line" echo " - Already exists: line #$line"
else else
echo >> "$2"
echo "$1" >> "$2" echo "$1" >> "$2"
echo " + Added" echo " + Added"
fi fi

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "June 2015" "fzf 0.10.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Aug 2015" "fzf 0.10.3" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -122,6 +122,7 @@ e.g. \fBfzf --color=bg+:24\fR
\fBpointer \fRPointer to the current line \fBpointer \fRPointer to the current line
\fBmarker \fRMulti-select marker \fBmarker \fRMulti-select marker
\fBspinner \fRStreaming input indicator \fBspinner \fRStreaming input indicator
\fBheader \fRHeader
.RE .RE
.TP .TP
.B "--black" .B "--black"
@@ -130,6 +131,31 @@ Use black background
.B "--reverse" .B "--reverse"
Reverse orientation Reverse orientation
.TP .TP
.BI "--margin=" MARGIN
Comma-separated expression for margins around the finder.
.br
.R ""
.br
.RS
.BR TRBL " Same margin for top, right, bottom, and left"
.br
.BR TB,RL " Vertical, horizontal margin"
.br
.BR T,RL,B " Top, horizontal, bottom margin"
.br
.BR T,R,B,L " Top, right, bottom, left margin"
.br
.R ""
.br
Each part can be given in absolute number or in percentage relative to the
terminal size with \fB%\fR suffix.
.br
.R ""
.br
e.g. \fBfzf --margin 10%\fR
\fBfzf --margin 1,5%\fR
.RE
.TP
.B "--cycle" .B "--cycle"
Enable cyclic scroll Enable cyclic scroll
.TP .TP
@@ -187,8 +213,10 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBbackward-kill-word\fR \fIalt-bs\fR \fBbackward-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\fR \fBbackward-word\fR \fIalt-b shift-left\fR
\fBbeginning-of-line\fR \fIctrl-a home\fR \fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR
\fBclear-screen\fR \fIctrl-l\fR \fBclear-screen\fR \fIctrl-l\fR
\fBdelete-char\fR \fIctrl-d del\fR \fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR
\fBdeselect-all\fR \fBdeselect-all\fR
\fBdown\fR \fIctrl-j ctrl-n down\fR \fBdown\fR \fIctrl-j ctrl-n down\fR
\fBend-of-line\fR \fIctrl-e end\fR \fBend-of-line\fR \fIctrl-e end\fR
@@ -256,6 +284,17 @@ When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to
.BI "--history-size=" "N" .BI "--history-size=" "N"
Maximum number of entries in the history file (default: 1000). The file is Maximum number of entries in the history file (default: 1000). The file is
automatically truncated when the number of the lines exceeds the value. automatically truncated when the number of the lines exceeds the value.
.TP
.BI "--header-file=" "FILE"
The content of the file will be printed as the sticky header. The lines in the
file are displayed in order from top to bottom regardless of \fB--reverse\fR,
and are not affected by \fB--with-nth\fR. ANSI color codes are processed even
when \fB--ansi\fR is not set.
.TP
.BI "--header-lines=" "N"
The first N lines of the input are treated as the sticky header. When
\fB--with-nth\fR is set, the lines are transformed just like the other
lines that follow.
.SS Scripting .SS Scripting
.TP .TP
.BI "-q, --query=" "STR" .BI "-q, --query=" "STR"
@@ -297,7 +336,7 @@ e.g. \fBfzf --multi | fzf --sync\fR
Default command to use when input is tty Default command to use when input is tty
.TP .TP
.B FZF_DEFAULT_OPTS .B FZF_DEFAULT_OPTS
Default options. e.g. \fB--extended --ansi\fR Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR
.SH EXIT STATUS .SH EXIT STATUS
.BR 0 " Normal exit" .BR 0 " Normal exit"

View File

@@ -54,13 +54,17 @@ function! s:fzf_exec()
return s:exec return s:exec
endfunction endfunction
function! s:tmux_not_zoomed()
return system('tmux list-panes -F "#F"') !~# 'Z'
endfunction
function! s:tmux_enabled() function! s:tmux_enabled()
if has('gui_running') if has('gui_running')
return 0 return 0
endif endif
if exists('s:tmux') if exists('s:tmux')
return s:tmux return s:tmux && s:tmux_not_zoomed()
endif endif
let s:tmux = 0 let s:tmux = 0
@@ -68,7 +72,7 @@ function! s:tmux_enabled()
let output = system('tmux -V') let output = system('tmux -V')
let s:tmux = !v:shell_error && output >= 'tmux 1.7' let s:tmux = !v:shell_error && output >= 'tmux 1.7'
endif endif
return s:tmux return s:tmux && s:tmux_not_zoomed()
endfunction endfunction
function! s:shellesc(arg) function! s:shellesc(arg)
@@ -249,7 +253,7 @@ function! s:calc_size(max, val)
endfunction endfunction
function! s:getpos() function! s:getpos()
return {'tab': tabpagenr(), 'win': winnr()} return {'tab': tabpagenr(), 'win': winnr(), 'cnt': winnr('$')}
endfunction endfunction
function! s:split(dict) function! s:split(dict)
@@ -364,9 +368,15 @@ function! s:cmd_callback(lines) abort
endif endif
let key = remove(a:lines, 0) let key = remove(a:lines, 0)
let cmd = get(s:action, key, 'e') let cmd = get(s:action, key, 'e')
for item in a:lines try
execute cmd s:escape(item) let autochdir = &autochdir
endfor set noautochdir
for item in a:lines
execute cmd s:escape(item)
endfor
finally
let &autochdir = autochdir
endtry
endfunction endfunction
function! s:cmd(bang, ...) abort function! s:cmd(bang, ...) abort

View File

@@ -48,7 +48,10 @@ _fzf_opts_completion() {
--sync --sync
--cycle --cycle
--history --history
--history-size" --history-size
--header-file
--header-lines
--margin"
case "${prev}" in case "${prev}" in
--tiebreak) --tiebreak)
@@ -59,7 +62,7 @@ _fzf_opts_completion() {
COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) ) COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) )
return 0 return 0
;; ;;
--history) --history|--header-file)
COMPREPLY=() COMPREPLY=()
return 0 return 0
;; ;;
@@ -74,9 +77,10 @@ _fzf_opts_completion() {
} }
_fzf_handle_dynamic_completion() { _fzf_handle_dynamic_completion() {
local cmd orig ret local cmd orig ret orig_cmd
cmd="$1" cmd="$1"
shift shift
orig_cmd="$1"
orig=$(eval "echo \$_fzf_orig_completion_$cmd") orig=$(eval "echo \$_fzf_orig_completion_$cmd")
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
@@ -84,7 +88,7 @@ _fzf_handle_dynamic_completion() {
elif [ -n "$_fzf_completion_loader" ]; then elif [ -n "$_fzf_completion_loader" ]; then
_completion_loader "$@" _completion_loader "$@"
ret=$? ret=$?
eval $(complete | \grep "\-F.* $cmd$" | _fzf_orig_completion_filter) eval $(complete | \grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)
source $BASH_SOURCE source $BASH_SOURCE
return $ret return $ret
fi fi

View File

@@ -1,10 +1,11 @@
# Key bindings # Key bindings
# ------------ # ------------
__fzf_select__() { __fzf_select__() {
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ local cmd="${FZF_CTRL_T_COMMAND:-"command \\find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}"
eval "$cmd" | fzf -m | while read item; do
printf '%q ' "$item" printf '%q ' "$item"
done done
echo echo
@@ -24,22 +25,28 @@ __fzf_select_tmux__() {
else else
height="-l $height" height="-l $height"
fi fi
tmux split-window $height "cd $(printf %q "$PWD");bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" tmux split-window $height "cd $(printf %q "$PWD"); FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") bash -c 'source ~/.fzf.bash; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'"
} }
__fzf_cd__() { __fzf_cd__() {
local dir local dir
dir=$(command find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ dir=$(command \find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m) && printf 'cd %q' "$dir" -o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m) && printf 'cd %q' "$dir"
} }
__fzf_history__() { __fzf_history__() (
local line local line
shopt -u nocaseglob nocasematch
line=$( line=$(
HISTTIMEFORMAT= history | HISTTIMEFORMAT= history |
$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r |
\grep '^ *[0-9]') && sed 's/ *\([0-9]*\)\** .*/!\1/' <<< "$line" \grep '^ *[0-9]') &&
} if [[ $- =~ H ]]; then
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
else
sed 's/^ *\([0-9]*\)\** *//' <<< "$line"
fi
)
__use_tmux=0 __use_tmux=0
[ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1 [ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1

View File

@@ -14,10 +14,12 @@ function fzf_key_bindings
end end
function __fzf_ctrl_t function __fzf_ctrl_t
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND "
command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3- | eval (__fzfcmd) -m > $TMPDIR/fzf.result -o -type l -print 2> /dev/null | sed 1d | cut -b3-"
eval $FZF_CTRL_T_COMMAND | eval (__fzfcmd) -m > $TMPDIR/fzf.result
and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape)
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result rm -f $TMPDIR/fzf.result

View File

@@ -1,11 +1,14 @@
# Key bindings # Key bindings
# ------------ # ------------
if [[ $- =~ i ]]; then
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line
__fsel() { __fsel() {
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ local cmd="${FZF_CTRL_T_COMMAND:-"command \\find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) -m | while read item; do -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}"
eval "$cmd" | $(__fzfcmd) -m | while read item; do
printf '%q ' "$item" printf '%q ' "$item"
done done
echo echo
@@ -15,8 +18,6 @@ __fzfcmd() {
[ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" [ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
} }
if [[ $- =~ i ]]; then
fzf-file-widget() { fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)" LBUFFER="${LBUFFER}$(__fsel)"
zle redisplay zle redisplay

View File

@@ -16,7 +16,7 @@ import (
*/ */
// FuzzyMatch performs fuzzy-match // FuzzyMatch performs fuzzy-match
func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { func FuzzyMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) {
if len(pattern) == 0 { if len(pattern) == 0 {
return 0, 0 return 0, 0
} }
@@ -34,7 +34,7 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
sidx := -1 sidx := -1
eidx := -1 eidx := -1
for index, char := range *runes { for index, char := range runes {
// This is considerably faster than blindly applying strings.ToLower to the // This is considerably faster than blindly applying strings.ToLower to the
// whole string // whole string
if !caseSensitive { if !caseSensitive {
@@ -61,7 +61,7 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
if sidx >= 0 && eidx >= 0 { if sidx >= 0 && eidx >= 0 {
pidx-- pidx--
for index := eidx - 1; index >= sidx; index-- { for index := eidx - 1; index >= sidx; index-- {
char := (*runes)[index] char := runes[index]
if !caseSensitive { if !caseSensitive {
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
@@ -88,12 +88,12 @@ func FuzzyMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
// //
// We might try to implement better algorithms in the future: // We might try to implement better algorithms in the future:
// http://en.wikipedia.org/wiki/String_searching_algorithm // http://en.wikipedia.org/wiki/String_searching_algorithm
func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { func ExactMatchNaive(caseSensitive bool, runes []rune, pattern []rune) (int, int) {
if len(pattern) == 0 { if len(pattern) == 0 {
return 0, 0 return 0, 0
} }
numRunes := len(*runes) numRunes := len(runes)
plen := len(pattern) plen := len(pattern)
if numRunes < plen { if numRunes < plen {
return -1, -1 return -1, -1
@@ -101,7 +101,7 @@ func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, in
pidx := 0 pidx := 0
for index := 0; index < numRunes; index++ { for index := 0; index < numRunes; index++ {
char := (*runes)[index] char := runes[index]
if !caseSensitive { if !caseSensitive {
if char >= 'A' && char <= 'Z' { if char >= 'A' && char <= 'Z' {
char += 32 char += 32
@@ -123,13 +123,13 @@ func ExactMatchNaive(caseSensitive bool, runes *[]rune, pattern []rune) (int, in
} }
// PrefixMatch performs prefix-match // PrefixMatch performs prefix-match
func PrefixMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { func PrefixMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) {
if len(*runes) < len(pattern) { if len(runes) < len(pattern) {
return -1, -1 return -1, -1
} }
for index, r := range pattern { for index, r := range pattern {
char := (*runes)[index] char := runes[index]
if !caseSensitive { if !caseSensitive {
char = unicode.ToLower(char) char = unicode.ToLower(char)
} }
@@ -141,7 +141,7 @@ func PrefixMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) {
} }
// SuffixMatch performs suffix-match // SuffixMatch performs suffix-match
func SuffixMatch(caseSensitive bool, input *[]rune, pattern []rune) (int, int) { func SuffixMatch(caseSensitive bool, input []rune, pattern []rune) (int, int) {
runes := util.TrimRight(input) runes := util.TrimRight(input)
trimmedLen := len(runes) trimmedLen := len(runes)
diff := trimmedLen - len(pattern) diff := trimmedLen - len(pattern)
@@ -161,11 +161,12 @@ func SuffixMatch(caseSensitive bool, input *[]rune, pattern []rune) (int, int) {
return trimmedLen - len(pattern), trimmedLen return trimmedLen - len(pattern), trimmedLen
} }
func EqualMatch(caseSensitive bool, runes *[]rune, pattern []rune) (int, int) { // EqualMatch performs equal-match
if len(*runes) != len(pattern) { func EqualMatch(caseSensitive bool, runes []rune, pattern []rune) (int, int) {
if len(runes) != len(pattern) {
return -1, -1 return -1, -1
} }
runesStr := string(*runes) runesStr := string(runes)
if !caseSensitive { if !caseSensitive {
runesStr = strings.ToLower(runesStr) runesStr = strings.ToLower(runesStr)
} }

View File

@@ -5,12 +5,11 @@ import (
"testing" "testing"
) )
func assertMatch(t *testing.T, fun func(bool, *[]rune, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) { func assertMatch(t *testing.T, fun func(bool, []rune, []rune) (int, int), caseSensitive bool, input string, pattern string, sidx int, eidx int) {
if !caseSensitive { if !caseSensitive {
pattern = strings.ToLower(pattern) pattern = strings.ToLower(pattern)
} }
runes := []rune(input) s, e := fun(caseSensitive, []rune(input), []rune(pattern))
s, e := fun(caseSensitive, &runes, []rune(pattern))
if s != sidx { if s != sidx {
t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern) t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern)
} }

View File

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

View File

@@ -14,79 +14,89 @@ func TestExtractColor(t *testing.T) {
} }
src := "hello world" src := "hello world"
var state *ansiState
clean := "\x1b[0m" clean := "\x1b[0m"
check := func(assertion func(ansiOffsets []ansiOffset)) { check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) {
output, ansiOffsets := extractColor(&src) output, ansiOffsets, newState := extractColor(src, state)
if *output != "hello world" { state = newState
if output != "hello world" {
t.Errorf("Invalid output: {}", output) t.Errorf("Invalid output: {}", output)
} }
fmt.Println(src, ansiOffsets, clean) fmt.Println(src, ansiOffsets, clean)
assertion(ansiOffsets) assertion(ansiOffsets, state)
} }
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) > 0 { if len(offsets) > 0 {
t.Fail() t.Fail()
} }
}) })
state = nil
src = "\x1b[0mhello world" src = "\x1b[0mhello world"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) > 0 { if len(offsets) > 0 {
t.Fail() t.Fail()
} }
}) })
state = nil
src = "\x1b[1mhello world" src = "\x1b[1mhello world"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() 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" src = "\x1b[1mhello \x1b[mworld"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() 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" src = "\x1b[1mhello \x1b[Kworld"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() 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" src = "hello \x1b[34;45;1mworld"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() 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" src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() 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" src = "hello \x1b[34;45;1mwor\x1b[0mld"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(offsets) != 1 {
t.Fail() 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" src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 3 { if len(offsets) != 3 {
t.Fail() t.Fail()
} }
@@ -96,12 +106,47 @@ func TestExtractColor(t *testing.T) {
}) })
// {38,48};5;{38,48} // {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" src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md"
check(func(offsets []ansiOffset) { check(func(offsets []ansiOffset, state *ansiState) {
if len(offsets) != 2 { if len(offsets) != 2 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 9, 38, 48, true) assert(offsets[0], 6, 9, 38, 48, true)
assert(offsets[1], 9, 10, 48, 38, true) assert(offsets[1], 9, 10, 48, 38, true)
}) })
src = "hello \x1b[32;1mworld"
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)
})
src = "hello world"
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)
})
src = "hello \x1b[0;38;5;200;48;5;100mworld"
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)
})
} }

View File

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

View File

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

View File

@@ -8,14 +8,14 @@ import (
const ( const (
// Current version // Current version
Version = "0.10.0" version = "0.10.3"
// Core // Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond coordinatorDelayMax time.Duration = 100 * time.Millisecond
coordinatorDelayStep time.Duration = 10 * time.Millisecond coordinatorDelayStep time.Duration = 10 * time.Millisecond
// Reader // Reader
defaultCommand = `find * -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null` defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
// Terminal // Terminal
initialDelay = 100 * time.Millisecond initialDelay = 100 * time.Millisecond
@@ -44,5 +44,6 @@ const (
EvtSearchNew EvtSearchNew
EvtSearchProgress EvtSearchProgress
EvtSearchFin EvtSearchFin
EvtHeader
EvtClose EvtClose
) )

View File

@@ -44,6 +44,7 @@ Reader -> EvtReadNew -> Matcher (restart)
Terminal -> EvtSearchNew:bool -> Matcher (restart) Terminal -> EvtSearchNew:bool -> Matcher (restart)
Matcher -> EvtSearchProgress -> Terminal (update info) Matcher -> EvtSearchProgress -> Terminal (update info)
Matcher -> EvtSearchFin -> Terminal (update list) Matcher -> EvtSearchFin -> Terminal (update list)
Matcher -> EvtHeader -> Terminal (update header)
*/ */
// Run starts fzf // Run starts fzf
@@ -54,7 +55,7 @@ func Run(opts *Options) {
rankTiebreak = opts.Tiebreak rankTiebreak = opts.Tiebreak
if opts.Version { if opts.Version {
fmt.Println(Version) fmt.Println(version)
os.Exit(0) os.Exit(0)
} }
@@ -62,48 +63,68 @@ func Run(opts *Options) {
eventBox := util.NewEventBox() eventBox := util.NewEventBox()
// ANSI code processor // ANSI code processor
ansiProcessor := func(data *string) (*string, []ansiOffset) { ansiProcessor := func(data []byte) ([]rune, []ansiOffset) {
// By default, we do nothing return util.BytesToRunes(data), nil
}
ansiProcessorRunes := func(data []rune) ([]rune, []ansiOffset) {
return data, nil return data, nil
} }
if opts.Ansi { if opts.Ansi {
if opts.Theme != nil { if opts.Theme != nil {
ansiProcessor = func(data *string) (*string, []ansiOffset) { var state *ansiState
return extractColor(data) ansiProcessor = func(data []byte) ([]rune, []ansiOffset) {
trimmed, offsets, newState := extractColor(string(data), state)
state = newState
return []rune(trimmed), offsets
} }
} else { } else {
// When color is disabled but ansi option is given, // When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input // we simply strip out ANSI codes from the input
ansiProcessor = func(data *string) (*string, []ansiOffset) { ansiProcessor = func(data []byte) ([]rune, []ansiOffset) {
trimmed, _ := extractColor(data) trimmed, _, _ := extractColor(string(data), nil)
return trimmed, nil return []rune(trimmed), nil
} }
} }
ansiProcessorRunes = func(data []rune) ([]rune, []ansiOffset) {
return ansiProcessor([]byte(string(data)))
}
} }
// Chunk list // Chunk list
var chunkList *ChunkList var chunkList *ChunkList
header := make([]string, 0, opts.HeaderLines)
if len(opts.WithNth) == 0 { if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(data *string, index int) *Item { chunkList = NewChunkList(func(data []byte, index int) *Item {
data, colors := ansiProcessor(data) if len(header) < opts.HeaderLines {
header = append(header, string(data))
eventBox.Set(EvtHeader, header)
return nil
}
runes, colors := ansiProcessor(data)
return &Item{ return &Item{
text: data, text: runes,
index: uint32(index), index: uint32(index),
colors: colors, colors: colors,
rank: Rank{0, 0, uint32(index)}} rank: Rank{0, 0, uint32(index)}}
}) })
} else { } else {
chunkList = NewChunkList(func(data *string, index int) *Item { chunkList = NewChunkList(func(data []byte, index int) *Item {
tokens := Tokenize(data, opts.Delimiter) runes := util.BytesToRunes(data)
tokens := Tokenize(runes, opts.Delimiter)
trans := Transform(tokens, opts.WithNth) trans := Transform(tokens, opts.WithNth)
if len(header) < opts.HeaderLines {
header = append(header, string(joinTokens(trans)))
eventBox.Set(EvtHeader, header)
return nil
}
item := Item{ item := Item{
text: joinTokens(trans), text: joinTokens(trans),
origText: data, origText: &runes,
index: uint32(index), index: uint32(index),
colors: nil, colors: nil,
rank: Rank{0, 0, uint32(index)}} rank: Rank{0, 0, uint32(index)}}
trimmed, colors := ansiProcessor(item.text) trimmed, colors := ansiProcessorRunes(item.text)
item.text = trimmed item.text = trimmed
item.colors = colors item.colors = colors
return &item return &item
@@ -113,7 +134,9 @@ func Run(opts *Options) {
// Reader // Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
if !streamingFilter { if !streamingFilter {
reader := Reader{func(str string) { chunkList.Push(str) }, eventBox, opts.ReadZero} reader := Reader{func(data []byte) bool {
return chunkList.Push(data)
}, eventBox, opts.ReadZero}
go reader.ReadSource() go reader.ReadSource()
} }
@@ -134,11 +157,12 @@ func Run(opts *Options) {
if streamingFilter { if streamingFilter {
reader := Reader{ reader := Reader{
func(str string) { func(runes []byte) bool {
item := chunkList.trans(&str, 0) item := chunkList.trans(runes, 0)
if pattern.MatchItem(item) { if item != nil && pattern.MatchItem(item) {
fmt.Println(*item.text) fmt.Println(string(item.text))
} }
return false
}, eventBox, opts.ReadZero} }, eventBox, opts.ReadZero}
reader.ReadSource() reader.ReadSource()
} else { } else {
@@ -206,6 +230,9 @@ func Run(opts *Options) {
terminal.UpdateProgress(val) terminal.UpdateProgress(val)
} }
case EvtHeader:
terminal.UpdateHeader(value.([]string), opts.HeaderLines)
case EvtSearchFin: case EvtSearchFin:
switch val := value.(type) { switch val := value.(type) {
case *Merger: case *Merger:

View File

@@ -8,6 +8,7 @@ package curses
import "C" import "C"
import ( import (
"fmt"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
@@ -94,6 +95,7 @@ const (
ColInfo ColInfo
ColCursor ColCursor
ColSelected ColSelected
ColHeader
ColUser ColUser
) )
@@ -114,6 +116,7 @@ type ColorTheme struct {
Info int16 Info int16
Cursor int16 Cursor int16
Selected int16 Selected int16
Header int16
} }
type Event struct { type Event struct {
@@ -164,7 +167,8 @@ func init() {
Spinner: C.COLOR_GREEN, Spinner: C.COLOR_GREEN,
Info: C.COLOR_WHITE, Info: C.COLOR_WHITE,
Cursor: C.COLOR_RED, Cursor: C.COLOR_RED,
Selected: C.COLOR_MAGENTA} Selected: C.COLOR_MAGENTA,
Header: C.COLOR_CYAN}
Dark256 = &ColorTheme{ Dark256 = &ColorTheme{
UseDefault: true, UseDefault: true,
Fg: 15, Fg: 15,
@@ -177,7 +181,8 @@ func init() {
Spinner: 148, Spinner: 148,
Info: 144, Info: 144,
Cursor: 161, Cursor: 161,
Selected: 168} Selected: 168,
Header: 109}
Light256 = &ColorTheme{ Light256 = &ColorTheme{
UseDefault: true, UseDefault: true,
Fg: 15, Fg: 15,
@@ -190,7 +195,8 @@ func init() {
Spinner: 65, Spinner: 65,
Info: 101, Info: 101,
Cursor: 161, Cursor: 161,
Selected: 168} Selected: 168,
Header: 31}
} }
func attrColored(pair int, bold bool) C.int { func attrColored(pair int, bold bool) C.int {
@@ -253,11 +259,14 @@ func Init(theme *ColorTheme, black bool, mouse bool) {
C.setlocale(C.LC_ALL, C.CString("")) C.setlocale(C.LC_ALL, C.CString(""))
_screen = C.newterm(nil, C.stderr, C.stdin) _screen = C.newterm(nil, C.stderr, C.stdin)
if _screen == nil {
fmt.Println("Invalid $TERM: " + os.Getenv("TERM"))
os.Exit(1)
}
C.set_term(_screen) C.set_term(_screen)
if mouse { if mouse {
C.mousemask(C.ALL_MOUSE_EVENTS, nil) C.mousemask(C.ALL_MOUSE_EVENTS, nil)
} }
C.cbreak()
C.noecho() C.noecho()
C.raw() // stty dsusp undef C.raw() // stty dsusp undef
@@ -308,6 +317,7 @@ func initPairs(theme *ColorTheme, black bool) {
C.init_pair(ColInfo, C.short(theme.Info), bg) C.init_pair(ColInfo, C.short(theme.Info), bg)
C.init_pair(ColCursor, C.short(theme.Cursor), darkBG) C.init_pair(ColCursor, C.short(theme.Cursor), darkBG)
C.init_pair(ColSelected, C.short(theme.Selected), darkBG) C.init_pair(ColSelected, C.short(theme.Selected), darkBG)
C.init_pair(ColHeader, C.short(theme.Header), bg)
} }
func Close() { func Close() {

View File

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

View File

@@ -17,9 +17,9 @@ type colorOffset struct {
// Item represents each input line // Item represents each input line
type Item struct { type Item struct {
text *string text []rune
origText *string origText *[]rune
transformed *[]Token transformed []Token
index uint32 index uint32
offsets []Offset offsets []Offset
colors []ansiOffset colors []ansiOffset
@@ -37,14 +37,14 @@ type Rank struct {
var rankTiebreak tiebreak var rankTiebreak tiebreak
// Rank calculates rank of the Item // Rank calculates rank of the Item
func (i *Item) Rank(cache bool) Rank { func (item *Item) Rank(cache bool) Rank {
if cache && (i.rank.matchlen > 0 || i.rank.tiebreak > 0) { if cache && (item.rank.matchlen > 0 || item.rank.tiebreak > 0) {
return i.rank return item.rank
} }
matchlen := 0 matchlen := 0
prevEnd := 0 prevEnd := 0
minBegin := math.MaxUint16 minBegin := math.MaxUint16
for _, offset := range i.offsets { for _, offset := range item.offsets {
begin := int(offset[0]) begin := int(offset[0])
end := int(offset[1]) end := int(offset[1])
if prevEnd > begin { if prevEnd > begin {
@@ -63,13 +63,22 @@ func (i *Item) Rank(cache bool) Rank {
var tiebreak uint16 var tiebreak uint16
switch rankTiebreak { switch rankTiebreak {
case byLength: case byLength:
tiebreak = uint16(len(*i.text)) // It is guaranteed that .transformed in not null in normal execution
if item.transformed != nil {
lenSum := 0
for _, token := range item.transformed {
lenSum += len(token.text)
}
tiebreak = uint16(lenSum)
} else {
tiebreak = uint16(len(item.text))
}
case byBegin: case byBegin:
// We can't just look at i.offsets[0][0] because it can be an inverse term // We can't just look at item.offsets[0][0] because it can be an inverse term
tiebreak = uint16(minBegin) tiebreak = uint16(minBegin)
case byEnd: case byEnd:
if prevEnd > 0 { if prevEnd > 0 {
tiebreak = uint16(1 + len(*i.text) - prevEnd) tiebreak = uint16(1 + len(item.text) - prevEnd)
} else { } else {
// Empty offsets due to inverse terms. // Empty offsets due to inverse terms.
tiebreak = 1 tiebreak = 1
@@ -77,24 +86,26 @@ func (i *Item) Rank(cache bool) Rank {
case byIndex: case byIndex:
tiebreak = 1 tiebreak = 1
} }
rank := Rank{uint16(matchlen), tiebreak, i.index} rank := Rank{uint16(matchlen), tiebreak, item.index}
if cache { if cache {
i.rank = rank item.rank = rank
} }
return rank return rank
} }
// AsString returns the original string // AsString returns the original string
func (i *Item) AsString() string { func (item *Item) AsString() string {
return *i.StringPtr() return *item.StringPtr()
} }
// StringPtr returns the pointer to the original string // StringPtr returns the pointer to the original string
func (i *Item) StringPtr() *string { func (item *Item) StringPtr() *string {
if i.origText != nil { runes := item.text
return i.origText if item.origText != nil {
runes = *item.origText
} }
return i.text str := string(runes)
return &str
} }
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset { func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
package fzf package fzf
import ( import (
"fmt" "io/ioutil"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
@@ -23,36 +23,39 @@ const usage = `usage: fzf [options]
-n, --nth=N[,..] Comma-separated list of field index expressions -n, --nth=N[,..] Comma-separated list of field index expressions
for limiting search scope. Each can be a non-zero for limiting search scope. Each can be a non-zero
integer or a range expression ([BEGIN]..[END]) integer or a range expression ([BEGIN]..[END])
--with-nth=N[,..] Transform item using index expressions within finder --with-nth=N[,..] Transform item using index expressions within finder
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
+s, --no-sort Do not sort the result +s, --no-sort Do not sort the result
--tac Reverse the order of the input --tac Reverse the order of the input
--tiebreak=CRI Sort criterion when the scores are tied; --tiebreak=CRITERION Sort criterion when the scores are tied;
[length|begin|end|index] (default: length) [length|begin|end|index] (default: length)
Interface Interface
-m, --multi Enable multi-select with tab/shift-tab -m, --multi Enable multi-select with tab/shift-tab
--ansi Enable processing of ANSI color codes --ansi Enable processing of ANSI color codes
--no-mouse Disable mouse --no-mouse Disable mouse
--color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors
--black Use black background --black Use black background
--reverse Reverse orientation --reverse Reverse orientation
--cycle Enable cyclic scroll --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
--no-hscroll Disable horizontal scroll --cycle Enable cyclic scroll
--inline-info Display finder info inline with the query --no-hscroll Disable horizontal scroll
--prompt=STR Input prompt (default: '> ') --inline-info Display finder info inline with the query
--bind=KEYBINDS Custom key bindings. Refer to the man page. --prompt=STR Input prompt (default: '> ')
--history=FILE History file --bind=KEYBINDS Custom key bindings. Refer to the man page.
--history-size=N Maximum number of history entries (default: 1000) --history=FILE History file
--history-size=N Maximum number of history entries (default: 1000)
--header-file=FILE The file whose content to be printed as header
--header-lines=N The first N lines of the input are treated as header
Scripting Scripting
-q, --query=STR Start the finder with the given query -q, --query=STR Start the finder with the given query
-1, --select-1 Automatically select the only match -1, --select-1 Automatically select the only match
-0, --exit-0 Exit immediately when there's no match -0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder. -f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line --print-query Print query as the first line
--expect=KEYS Comma-separated list of keys to complete fzf --expect=KEYS Comma-separated list of keys to complete fzf
--sync Synchronous search for multi-staged filtering --sync Synchronous search for multi-staged filtering
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
@@ -90,39 +93,46 @@ const (
byIndex byIndex
) )
func defaultMargin() [4]string {
return [4]string{"0", "0", "0", "0"}
}
// Options stores the values of command-line options // Options stores the values of command-line options
type Options struct { type Options struct {
Mode Mode Mode Mode
Case Case Case Case
Nth []Range Nth []Range
WithNth []Range WithNth []Range
Delimiter *regexp.Regexp Delimiter Delimiter
Sort int Sort int
Tac bool Tac bool
Tiebreak tiebreak Tiebreak tiebreak
Multi bool Multi bool
Ansi bool Ansi bool
Mouse bool Mouse bool
Theme *curses.ColorTheme Theme *curses.ColorTheme
Black bool Black bool
Reverse bool Reverse bool
Cycle bool Cycle bool
Hscroll bool Hscroll bool
InlineInfo bool InlineInfo bool
Prompt string Prompt string
Query string Query string
Select1 bool Select1 bool
Exit0 bool Exit0 bool
Filter *string Filter *string
ToggleSort bool ToggleSort bool
Expect map[int]string Expect map[int]string
Keymap map[int]actionType Keymap map[int]actionType
Execmap map[int]string Execmap map[int]string
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
Sync bool Sync bool
History *History History *History
Version bool Header []string
HeaderLines int
Margin [4]string
Version bool
} }
func defaultTheme() *curses.ColorTheme { func defaultTheme() *curses.ColorTheme {
@@ -134,37 +144,40 @@ func defaultTheme() *curses.ColorTheme {
func defaultOptions() *Options { func defaultOptions() *Options {
return &Options{ return &Options{
Mode: ModeFuzzy, Mode: ModeFuzzy,
Case: CaseSmart, Case: CaseSmart,
Nth: make([]Range, 0), Nth: make([]Range, 0),
WithNth: make([]Range, 0), WithNth: make([]Range, 0),
Delimiter: nil, Delimiter: Delimiter{},
Sort: 1000, Sort: 1000,
Tac: false, Tac: false,
Tiebreak: byLength, Tiebreak: byLength,
Multi: false, Multi: false,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
Theme: defaultTheme(), Theme: defaultTheme(),
Black: false, Black: false,
Reverse: false, Reverse: false,
Cycle: false, Cycle: false,
Hscroll: true, Hscroll: true,
InlineInfo: false, InlineInfo: false,
Prompt: "> ", Prompt: "> ",
Query: "", Query: "",
Select1: false, Select1: false,
Exit0: false, Exit0: false,
Filter: nil, Filter: nil,
ToggleSort: false, ToggleSort: false,
Expect: make(map[int]string), Expect: make(map[int]string),
Keymap: defaultKeymap(), Keymap: defaultKeymap(),
Execmap: make(map[int]string), Execmap: make(map[int]string),
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
Sync: false, Sync: false,
History: nil, History: nil,
Version: false} Header: make([]string, 0),
HeaderLines: 0,
Margin: defaultMargin(),
Version: false}
} }
func help(ok int) { func help(ok int) {
@@ -174,7 +187,7 @@ func help(ok int) {
func errorExit(msg string) { func errorExit(msg string) {
os.Stderr.WriteString(msg + "\n") os.Stderr.WriteString(msg + "\n")
help(1) os.Exit(1)
} }
func optString(arg string, prefixes ...string) (bool, string) { func optString(arg string, prefixes ...string) (bool, string) {
@@ -211,6 +224,14 @@ func atoi(str string) int {
return num return num
} }
func atof(str string) float64 {
num, err := strconv.ParseFloat(str, 64)
if err != nil {
errorExit("not a valid number: " + str)
}
return num
}
func nextInt(args []string, i *int, message string) int { func nextInt(args []string, i *int, message string) int {
if len(args) > *i+1 { if len(args) > *i+1 {
*i++ *i++
@@ -246,17 +267,23 @@ func splitNth(str string) []Range {
return ranges return ranges
} }
func delimiterRegexp(str string) *regexp.Regexp { func delimiterRegexp(str string) Delimiter {
rx, e := regexp.Compile(str) // Special handling of \t
if e != nil { str = strings.Replace(str, "\\t", "\t", -1)
str = regexp.QuoteMeta(str)
// 1. Pattern does not contain any special character
if regexp.QuoteMeta(str) == str {
return Delimiter{str: &str}
} }
rx, e = regexp.Compile(fmt.Sprintf("(?:.*?%s)|(?:.+?$)", str)) rx, e := regexp.Compile(str)
// 2. Pattern is not a valid regular expression
if e != nil { if e != nil {
errorExit("invalid regular expression: " + e.Error()) return Delimiter{str: &str}
} }
return rx
// 3. Pattern as regular expression. Slow.
return Delimiter{regex: rx}
} }
func isAlphabet(char uint8) bool { func isAlphabet(char uint8) bool {
@@ -413,6 +440,8 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme
theme.Cursor = ansi theme.Cursor = ansi
case "marker": case "marker":
theme.Selected = ansi theme.Selected = ansi
case "header":
theme.Header = ansi
default: default:
fail() fail()
} }
@@ -430,6 +459,11 @@ func firstKey(keymap map[int]string) int {
return 0 return 0
} }
const (
escapedColon = 0
escapedComma = 1
)
func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) { func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) {
if executeRegexp == nil { if executeRegexp == nil {
// Backreferences are not supported. // Backreferences are not supported.
@@ -440,26 +474,31 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
return ":execute(" + strings.Repeat(" ", len(src)-10) + ")" return ":execute(" + strings.Repeat(" ", len(src)-10) + ")"
}) })
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
idx := 0 idx := 0
for _, pairStr := range strings.Split(masked, ",") { for _, pairStr := range strings.Split(masked, ",") {
pairStr = str[idx : idx+len(pairStr)] origPairStr := str[idx : idx+len(pairStr)]
idx += len(pairStr) + 1 idx += len(pairStr) + 1
fail := func() {
errorExit("invalid key binding: " + pairStr)
}
pair := strings.SplitN(pairStr, ":", 2) pair := strings.SplitN(pairStr, ":", 2)
if len(pair) != 2 { if len(pair) < 2 {
fail() errorExit("bind action not specified: " + origPairStr)
} }
keys := parseKeyChords(pair[0], "key name required") var key int
if len(keys) != 1 { if len(pair[0]) == 1 && pair[0][0] == escapedColon {
fail() key = ':' + curses.AltZ
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
key = ',' + curses.AltZ
} else {
keys := parseKeyChords(pair[0], "key name required")
key = firstKey(keys)
} }
key := firstKey(keys)
act := strings.ToLower(pair[1]) act := origPairStr[len(pair[0])+1 : len(origPairStr)]
switch act { actLower := strings.ToLower(act)
switch actLower {
case "ignore": case "ignore":
keymap[key] = actIgnore keymap[key] = actIgnore
case "beginning-of-line": case "beginning-of-line":
@@ -478,8 +517,12 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
keymap[key] = actClearScreen keymap[key] = actClearScreen
case "delete-char": case "delete-char":
keymap[key] = actDeleteChar keymap[key] = actDeleteChar
case "delete-char/eof":
keymap[key] = actDeleteCharEOF
case "end-of-line": case "end-of-line":
keymap[key] = actEndOfLine keymap[key] = actEndOfLine
case "cancel":
keymap[key] = actCancel
case "forward-char": case "forward-char":
keymap[key] = actForwardChar keymap[key] = actForwardChar
case "forward-word": case "forward-word":
@@ -524,12 +567,12 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
keymap[key] = actToggleSort keymap[key] = actToggleSort
toggleSort = true toggleSort = true
default: default:
if isExecuteAction(act) { if isExecuteAction(actLower) {
keymap[key] = actExecute keymap[key] = actExecute
if pair[1][7] == ':' { if act[7] == ':' {
execmap[key] = pair[1][8:] execmap[key] = act[8:]
} else { } else {
execmap[key] = pair[1][8 : len(act)-1] execmap[key] = act[8 : len(act)-1]
} }
} else { } else {
errorExit("unknown action: " + act) errorExit("unknown action: " + act)
@@ -561,6 +604,56 @@ func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
return keymap return keymap
} }
func readHeaderFile(filename string) []string {
content, err := ioutil.ReadFile(filename)
if err != nil {
errorExit("failed to read header file: " + filename)
}
return strings.Split(strings.TrimSuffix(string(content), "\n"), "\n")
}
func parseMargin(margin string) [4]string {
margins := strings.Split(margin, ",")
checked := func(str string) string {
if strings.HasSuffix(str, "%") {
val := atof(str[:len(str)-1])
if val < 0 {
errorExit("margin must be non-negative")
}
if val > 100 {
errorExit("margin too large")
}
} else {
val := atoi(str)
if val < 0 {
errorExit("margin must be non-negative")
}
}
return str
}
switch len(margins) {
case 1:
m := checked(margins[0])
return [4]string{m, m, m, m}
case 2:
tb := checked(margins[0])
rl := checked(margins[1])
return [4]string{tb, rl, tb, rl}
case 3:
t := checked(margins[0])
rl := checked(margins[1])
b := checked(margins[2])
return [4]string{t, rl, b, rl}
case 4:
return [4]string{
checked(margins[0]), checked(margins[1]),
checked(margins[2]), checked(margins[3])}
default:
errorExit("invalid margin: " + margin)
}
return defaultMargin()
}
func parseOptions(opts *Options, allArgs []string) { func parseOptions(opts *Options, allArgs []string) {
keymap := make(map[int]actionType) keymap := make(map[int]actionType)
var historyMax int var historyMax int
@@ -700,6 +793,23 @@ func parseOptions(opts *Options, allArgs []string) {
setHistory(nextString(allArgs, &i, "history file path required")) setHistory(nextString(allArgs, &i, "history file path required"))
case "--history-size": case "--history-size":
setHistoryMax(nextInt(allArgs, &i, "history max size required")) setHistoryMax(nextInt(allArgs, &i, "history max size required"))
case "--no-header-file":
opts.Header = []string{}
case "--no-header-lines":
opts.HeaderLines = 0
case "--header-file":
opts.Header = readHeaderFile(
nextString(allArgs, &i, "header file name required"))
opts.HeaderLines = 0
case "--header-lines":
opts.Header = []string{}
opts.HeaderLines = atoi(
nextString(allArgs, &i, "number of header lines required"))
case "--no-margin":
opts.Margin = defaultMargin()
case "--margin":
opts.Margin = parseMargin(
nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
case "--version": case "--version":
opts.Version = true opts.Version = true
default: default:
@@ -733,12 +843,24 @@ func parseOptions(opts *Options, allArgs []string) {
setHistory(value) setHistory(value)
} else if match, value := optString(arg, "--history-size="); match { } else if match, value := optString(arg, "--history-size="); match {
setHistoryMax(atoi(value)) setHistoryMax(atoi(value))
} else if match, value := optString(arg, "--header-file="); match {
opts.Header = readHeaderFile(value)
opts.HeaderLines = 0
} else if match, value := optString(arg, "--header-lines="); match {
opts.Header = []string{}
opts.HeaderLines = atoi(value)
} else if match, value := optString(arg, "--margin="); match {
opts.Margin = parseMargin(value)
} else { } else {
errorExit("unknown option: " + arg) errorExit("unknown option: " + arg)
} }
} }
} }
if opts.HeaderLines < 0 {
errorExit("header lines must be a non-negative integer")
}
// Change default actions for CTRL-N / CTRL-P when --history is used // Change default actions for CTRL-N / CTRL-P when --history is used
if opts.History != nil { if opts.History != nil {
if _, prs := keymap[curses.CtrlP]; !prs { if _, prs := keymap[curses.CtrlP]; !prs {

View File

@@ -8,11 +8,59 @@ import (
) )
func TestDelimiterRegex(t *testing.T) { func TestDelimiterRegex(t *testing.T) {
rx := delimiterRegexp("*") // Valid regex
tokens := rx.FindAllString("-*--*---**---", -1) delim := delimiterRegexp(".")
if tokens[0] != "-*" || tokens[1] != "--*" || tokens[2] != "---*" || if delim.regex == nil || delim.str != nil {
tokens[3] != "*" || tokens[4] != "---" { t.Error(delim)
t.Errorf("%s %s %d", rx, tokens, len(tokens)) }
// Broken regex -> string
delim = delimiterRegexp("[0-9")
if delim.regex != nil || *delim.str != "[0-9" {
t.Error(delim)
}
// Valid regex
delim = delimiterRegexp("[0-9]")
if delim.regex.String() != "[0-9]" || delim.str != nil {
t.Error(delim)
}
// Tab character
delim = delimiterRegexp("\t")
if delim.regex != nil || *delim.str != "\t" {
t.Error(delim)
}
// Tab expression
delim = delimiterRegexp("\\t")
if delim.regex != nil || *delim.str != "\t" {
t.Error(delim)
}
// Tabs -> regex
delim = delimiterRegexp("\t+")
if delim.regex == nil || delim.str != nil {
t.Error(delim)
}
}
func TestDelimiterRegexString(t *testing.T) {
delim := delimiterRegexp("*")
tokens := Tokenize([]rune("-*--*---**---"), delim)
if delim.regex != nil ||
string(tokens[0].text) != "-*" ||
string(tokens[1].text) != "--*" ||
string(tokens[2].text) != "---*" ||
string(tokens[3].text) != "*" ||
string(tokens[4].text) != "---" {
t.Errorf("%s %s %d", delim, tokens, len(tokens))
}
}
func TestDelimiterRegexRegex(t *testing.T) {
delim := delimiterRegexp("--\\*")
tokens := Tokenize([]rune("-*--*---**---"), delim)
if delim.str != nil ||
string(tokens[0].text) != "-*--*" ||
string(tokens[1].text) != "---*" ||
string(tokens[2].text) != "*---" {
t.Errorf("%s %d", tokens, len(tokens))
} }
} }
@@ -188,13 +236,15 @@ func TestBind(t *testing.T) {
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+ "ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+
"f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ "f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+
",X:execute:\nfoobar,Y:execute(baz)") ",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)")
if !toggleSort { if !toggleSort {
t.Errorf("toggleSort not set") t.Errorf("toggleSort not set")
} }
check(actKillLine, keymap[curses.CtrlA]) check(actKillLine, keymap[curses.CtrlA])
check(actToggleSort, keymap[curses.CtrlB]) check(actToggleSort, keymap[curses.CtrlB])
check(actPageUp, keymap[curses.AltZ+'c']) check(actPageUp, keymap[curses.AltZ+'c'])
check(actAbort, keymap[curses.AltZ+','])
check(actAccept, keymap[curses.AltZ+':'])
check(actPageDown, keymap[curses.AltZ]) check(actPageDown, keymap[curses.AltZ])
check(actExecute, keymap[curses.F1]) check(actExecute, keymap[curses.F1])
check(actExecute, keymap[curses.F2]) check(actExecute, keymap[curses.F2])

View File

@@ -42,9 +42,9 @@ type Pattern struct {
text []rune text []rune
terms []term terms []term
hasInvTerm bool hasInvTerm bool
delimiter *regexp.Regexp delimiter Delimiter
nth []Range nth []Range
procFun map[termType]func(bool, *[]rune, []rune) (int, int) procFun map[termType]func(bool, []rune, []rune) (int, int)
} }
var ( var (
@@ -71,7 +71,7 @@ func clearChunkCache() {
// BuildPattern builds Pattern object from the given arguments // BuildPattern builds Pattern object from the given arguments
func BuildPattern(mode Mode, caseMode Case, func BuildPattern(mode Mode, caseMode Case,
nth []Range, delimiter *regexp.Regexp, runes []rune) *Pattern { nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string var asString string
switch mode { switch mode {
@@ -114,7 +114,7 @@ func BuildPattern(mode Mode, caseMode Case,
hasInvTerm: hasInvTerm, hasInvTerm: hasInvTerm,
nth: nth, nth: nth,
delimiter: delimiter, delimiter: delimiter,
procFun: make(map[termType]func(bool, *[]rune, []rune) (int, int))} procFun: make(map[termType]func(bool, []rune, []rune) (int, int))}
ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termFuzzy] = algo.FuzzyMatch
ptr.procFun[termEqual] = algo.EqualMatch ptr.procFun[termEqual] = algo.EqualMatch
@@ -305,27 +305,25 @@ func (p *Pattern) extendedMatch(item *Item) []Offset {
return offsets return offsets
} }
func (p *Pattern) prepareInput(item *Item) *[]Token { func (p *Pattern) prepareInput(item *Item) []Token {
if item.transformed != nil { if item.transformed != nil {
return item.transformed return item.transformed
} }
var ret *[]Token var ret []Token
if len(p.nth) > 0 { if len(p.nth) > 0 {
tokens := Tokenize(item.text, p.delimiter) tokens := Tokenize(item.text, p.delimiter)
ret = Transform(tokens, p.nth) ret = Transform(tokens, p.nth)
} else { } else {
runes := []rune(*item.text) ret = []Token{Token{text: item.text, prefixLength: 0}}
trans := []Token{Token{text: &runes, prefixLength: 0}}
ret = &trans
} }
item.transformed = ret item.transformed = ret
return ret return ret
} }
func (p *Pattern) iter(pfun func(bool, *[]rune, []rune) (int, int), func (p *Pattern) iter(pfun func(bool, []rune, []rune) (int, int),
tokens *[]Token, caseSensitive bool, pattern []rune) (int, int) { tokens []Token, caseSensitive bool, pattern []rune) (int, int) {
for _, part := range *tokens { for _, part := range tokens {
prefixLength := part.prefixLength prefixLength := part.prefixLength
if sidx, eidx := pfun(caseSensitive, part.text, pattern); sidx >= 0 { if sidx, eidx := pfun(caseSensitive, part.text, pattern); sidx >= 0 {
return sidx + prefixLength, eidx + prefixLength return sidx + prefixLength, eidx + prefixLength

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ import (
"os/signal" "os/signal"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
@@ -40,6 +41,9 @@ type Terminal struct {
printQuery bool printQuery bool
history *History history *History
cycle bool cycle bool
header []string
margin [4]string
marginInt [4]int
count int count int
progress int progress int
reading bool reading bool
@@ -78,6 +82,7 @@ var _runeWidths = make(map[rune]int)
const ( const (
reqPrompt util.EventType = iota reqPrompt util.EventType = iota
reqInfo reqInfo
reqHeader
reqList reqList
reqRefresh reqRefresh
reqRedraw reqRedraw
@@ -98,8 +103,10 @@ const (
actBackwardChar actBackwardChar
actBackwardDeleteChar actBackwardDeleteChar
actBackwardWord actBackwardWord
actCancel
actClearScreen actClearScreen
actDeleteChar actDeleteChar
actDeleteCharEOF
actEndOfLine actEndOfLine
actForwardChar actForwardChar
actForwardWord actForwardWord
@@ -134,7 +141,7 @@ func defaultKeymap() map[int]actionType {
keymap[C.CtrlG] = actAbort keymap[C.CtrlG] = actAbort
keymap[C.CtrlQ] = actAbort keymap[C.CtrlQ] = actAbort
keymap[C.ESC] = actAbort keymap[C.ESC] = actAbort
keymap[C.CtrlD] = actDeleteChar keymap[C.CtrlD] = actDeleteCharEOF
keymap[C.CtrlE] = actEndOfLine keymap[C.CtrlE] = actEndOfLine
keymap[C.CtrlF] = actForwardChar keymap[C.CtrlF] = actForwardChar
keymap[C.CtrlH] = actBackwardDeleteChar keymap[C.CtrlH] = actBackwardDeleteChar
@@ -165,7 +172,7 @@ func defaultKeymap() map[int]actionType {
keymap[C.Home] = actBeginningOfLine keymap[C.Home] = actBeginningOfLine
keymap[C.End] = actEndOfLine keymap[C.End] = actEndOfLine
keymap[C.Del] = actDeleteChar // FIXME Del vs. CTRL-D keymap[C.Del] = actDeleteChar
keymap[C.PgUp] = actPageUp keymap[C.PgUp] = actPageUp
keymap[C.PgDn] = actPageDown keymap[C.PgDn] = actPageDown
@@ -196,7 +203,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
pressed: "", pressed: "",
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
history: opts.History, history: opts.History,
margin: opts.Margin,
marginInt: [4]int{0, 0, 0, 0},
cycle: opts.Cycle, cycle: opts.Cycle,
header: opts.Header,
reading: true, reading: true,
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[uint32]selectedItem), selected: make(map[uint32]selectedItem),
@@ -229,6 +239,22 @@ func (t *Terminal) UpdateCount(cnt int, final bool) {
} }
} }
// UpdateHeader updates the header
func (t *Terminal) UpdateHeader(header []string, lines int) {
t.mutex.Lock()
t.header = make([]string, lines)
copy(t.header, header)
if !t.reverse {
reversed := make([]string, lines)
for idx, str := range t.header {
reversed[lines-idx-1] = str
}
t.header = reversed
}
t.mutex.Unlock()
t.reqBox.Set(reqHeader, nil)
}
// UpdateProgress updates the search progress // UpdateProgress updates the search progress
func (t *Terminal) UpdateProgress(progress float32) { func (t *Terminal) UpdateProgress(progress float32) {
t.mutex.Lock() t.mutex.Lock()
@@ -296,10 +322,50 @@ func displayWidth(runes []rune) int {
return l return l
} }
const minWidth = 16
const minHeight = 4
func (t *Terminal) calculateMargins() {
screenWidth := C.MaxX()
screenHeight := C.MaxY()
for idx, str := range t.margin {
if str == "0" {
t.marginInt[idx] = 0
} else if strings.HasSuffix(str, "%") {
num, _ := strconv.ParseFloat(str[:len(str)-1], 64)
var val float64
if idx%2 == 0 {
val = float64(screenHeight)
} else {
val = float64(screenWidth)
}
t.marginInt[idx] = int(val * num * 0.01)
} else {
num, _ := strconv.Atoi(str)
t.marginInt[idx] = num
}
}
adjust := func(idx1 int, idx2 int, max int, min int) {
if max >= min {
margin := t.marginInt[idx1] + t.marginInt[idx2]
if max-margin < min {
desired := max - min
t.marginInt[idx1] = desired * t.marginInt[idx1] / margin
t.marginInt[idx2] = desired * t.marginInt[idx2] / margin
}
}
}
adjust(1, 3, screenWidth, minWidth)
adjust(0, 2, screenHeight, minHeight)
}
func (t *Terminal) move(y int, x int, clear bool) { func (t *Terminal) move(y int, x int, clear bool) {
x += t.marginInt[3]
maxy := C.MaxY() maxy := C.MaxY()
if !t.reverse { if !t.reverse {
y = maxy - y - 1 y = maxy - y - 1 - t.marginInt[2]
} else {
y += t.marginInt[0]
} }
if clear { if clear {
@@ -354,17 +420,49 @@ func (t *Terminal) printInfo() {
C.CPrint(C.ColInfo, false, output) C.CPrint(C.ColInfo, false, output)
} }
func (t *Terminal) maxHeight() int {
return C.MaxY() - t.marginInt[0] - t.marginInt[2]
}
func (t *Terminal) printHeader() {
if len(t.header) == 0 {
return
}
max := t.maxHeight()
var state *ansiState
for idx, lineStr := range t.header {
if !t.reverse {
idx = len(t.header) - idx - 1
}
line := idx + 2
if t.inlineInfo {
line--
}
if line >= max {
continue
}
trimmed, colors, newState := extractColor(lineStr, state)
state = newState
item := &Item{
text: []rune(trimmed),
index: 0,
colors: colors,
rank: Rank{0, 0, 0}}
t.move(line, 2, true)
t.printHighlighted(item, false, C.ColHeader, 0, false)
}
}
func (t *Terminal) printList() { func (t *Terminal) printList() {
t.constrain() t.constrain()
maxy := t.maxItems() maxy := t.maxItems()
count := t.merger.Length() - t.offset count := t.merger.Length() - t.offset
for i := 0; i < maxy; i++ { for i := 0; i < maxy; i++ {
var line int line := i + 2 + len(t.header)
if t.inlineInfo { if t.inlineInfo {
line = i + 1 line--
} else {
line = i + 2
} }
t.move(line, 0, true) t.move(line, 0, true)
if i < count { if i < count {
@@ -439,9 +537,10 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
} }
// Overflow // Overflow
text := []rune(*item.text) text := make([]rune, len(item.text))
copy(text, item.text)
offsets := item.colorOffsets(col2, bold, current) offsets := item.colorOffsets(col2, bold, current)
maxWidth := C.MaxX() - 3 maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3]
fullWidth := displayWidth(text) fullWidth := displayWidth(text)
if fullWidth > maxWidth { if fullWidth > maxWidth {
if t.hscroll { if t.hscroll {
@@ -524,9 +623,11 @@ func processTabs(runes []rune, prefixWidth int) (string, int) {
} }
func (t *Terminal) printAll() { func (t *Terminal) printAll() {
t.calculateMargins()
t.printList() t.printList()
t.printPrompt() t.printPrompt()
t.printInfo() t.printInfo()
t.printHeader()
} }
func (t *Terminal) refresh() { func (t *Terminal) refresh() {
@@ -602,10 +703,12 @@ func (t *Terminal) Loop() {
{ // Late initialization { // Late initialization
t.mutex.Lock() t.mutex.Lock()
t.initFunc() t.initFunc()
t.calculateMargins()
t.printPrompt() t.printPrompt()
t.placeCursor() t.placeCursor()
C.Refresh() C.Refresh()
t.printInfo() t.printInfo()
t.printHeader()
t.mutex.Unlock() t.mutex.Unlock()
go func() { go func() {
timer := time.NewTimer(initialDelay) timer := time.NewTimer(initialDelay)
@@ -660,6 +763,8 @@ func (t *Terminal) Loop() {
t.printInfo() t.printInfo()
case reqList: case reqList:
t.printList() t.printList()
case reqHeader:
t.printHeader()
case reqRefresh: case reqRefresh:
t.suppress = false t.suppress = false
case reqRedraw: case reqRedraw:
@@ -757,11 +862,21 @@ func (t *Terminal) Loop() {
case actAbort: case actAbort:
req(reqQuit) req(reqQuit)
case actDeleteChar: case actDeleteChar:
t.delChar()
case actDeleteCharEOF:
if !t.delChar() && t.cx == 0 { if !t.delChar() && t.cx == 0 {
req(reqQuit) req(reqQuit)
} }
case actEndOfLine: case actEndOfLine:
t.cx = len(t.input) t.cx = len(t.input)
case actCancel:
if len(t.input) == 0 {
req(reqQuit)
} else {
t.yanked = t.input
t.input = []rune{}
t.cx = 0
}
case actForwardChar: case actForwardChar:
if t.cx < len(t.input) { if t.cx < len(t.input) {
t.cx++ t.cx++
@@ -879,14 +994,7 @@ func (t *Terminal) Loop() {
} }
case actMouse: case actMouse:
me := event.MouseEvent me := event.MouseEvent
mx, my := util.Constrain(me.X-len(t.prompt), 0, len(t.input)), me.Y mx, my := me.X, me.Y
if !t.reverse {
my = C.MaxY() - my - 1
}
min := 2
if t.inlineInfo {
min = 1
}
if me.S != 0 { if me.S != 0 {
// Scroll // Scroll
if t.merger.Length() > 0 { if t.merger.Length() > 0 {
@@ -896,23 +1004,36 @@ func (t *Terminal) Loop() {
t.vmove(me.S) t.vmove(me.S)
req(reqList) req(reqList)
} }
} else if me.Double { } else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] &&
// Double-click my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] {
if my >= min { mx -= t.marginInt[3]
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { my -= t.marginInt[0]
req(reqClose) mx = util.Constrain(mx-len(t.prompt), 0, len(t.input))
} if !t.reverse {
my = t.maxHeight() - my - 1
} }
} else if me.Down { min := 2 + len(t.header)
if my == 0 && mx >= 0 { if t.inlineInfo {
// Prompt min--
t.cx = mx }
} else if my >= min { if me.Double {
// List // Double-click
if t.vset(t.offset+my-min) && t.multi && me.Mod { if my >= min {
toggle() if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
req(reqClose)
}
}
} else if me.Down {
if my == 0 && mx >= 0 {
// Prompt
t.cx = mx
} else if my >= min {
// List
if t.vset(t.offset+my-min) && t.multi && me.Mod {
toggle()
}
req(reqList)
} }
req(reqList)
} }
} }
} }
@@ -948,6 +1069,7 @@ func (t *Terminal) constrain() {
t.offset = util.Max(0, count-height) t.offset = util.Max(0, count-height)
t.cy = util.Constrain(t.offset+diffpos, 0, count-1) t.cy = util.Constrain(t.offset+diffpos, 0, count-1)
} }
t.offset = util.Max(0, t.offset)
} }
func (t *Terminal) vmove(o int) { func (t *Terminal) vmove(o int) {
@@ -976,8 +1098,9 @@ func (t *Terminal) vset(o int) bool {
} }
func (t *Terminal) maxItems() int { func (t *Terminal) maxItems() int {
max := t.maxHeight() - 2 - len(t.header)
if t.inlineInfo { if t.inlineInfo {
return C.MaxY() - 1 max++
} }
return C.MaxY() - 2 return util.Max(max, 0)
} }

View File

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

View File

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

View File

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

View File

@@ -148,6 +148,7 @@ class TestBase < Minitest::Test
def setup def setup
ENV.delete 'FZF_DEFAULT_OPTS' ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_CTRL_T_COMMAND'
ENV.delete 'FZF_DEFAULT_COMMAND' ENV.delete 'FZF_DEFAULT_COMMAND'
end end
@@ -311,7 +312,9 @@ class TestGoFZF < TestBase
# However, the output must not be transformed # However, the output must not be transformed
if multi if multi
tmux.send_keys :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(2)') }
tmux.send_keys :Enter
assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.split($/) assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.split($/)
else else
tmux.send_keys '^', '3' tmux.send_keys '^', '3'
@@ -347,7 +350,9 @@ class TestGoFZF < TestBase
[:'0', :'1', [:'1', :'0']].each do |opt| [:'0', :'1', [:'1', :'0']].each do |opt|
tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter
tmux.until { |lines| lines.last =~ /^> 5/ } tmux.until { |lines| lines.last =~ /^> 5/ }
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter
assert_equal ['5', '5', '15', '25'], readonce.split($/) assert_equal ['5', '5', '15', '25'], readonce.split($/)
end end
end end
@@ -364,7 +369,9 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-1] == '>' } tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 9 tmux.send_keys 9
tmux.until { |lines| lines[-2] == ' 19/100' } tmux.until { |lines| lines[-2] == ' 19/100' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == '>' } tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 'C-K', :Enter tmux.send_keys 'C-K', :Enter
assert_equal ['1919'], readonce.split($/) assert_equal ['1919'], readonce.split($/)
@@ -373,7 +380,9 @@ class TestGoFZF < TestBase
def test_tac def test_tac
tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter
assert_equal %w[1000 999 998], readonce.split($/) assert_equal %w[1000 999 998], readonce.split($/)
end end
@@ -381,7 +390,9 @@ class TestGoFZF < TestBase
tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys '99' tmux.send_keys '99'
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter
assert_equal %w[99 999 998], readonce.split($/) assert_equal %w[99 999 998], readonce.split($/)
end end
@@ -390,7 +401,9 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys '00' tmux.send_keys '00'
tmux.until { |lines| lines[-2].include? '10/1000' } tmux.until { |lines| lines[-2].include? '10/1000' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab, :BTab
tmux.until { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter
assert_equal %w[1000 900 800], readonce.split($/) assert_equal %w[1000 900 800], readonce.split($/)
end end
@@ -488,6 +501,32 @@ class TestGoFZF < TestBase
assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/) assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/)
end end
def test_tiebreak_length_with_nth
input = %w[
1:hell
123:hello
12345:he
1234567:h
]
writelines tempname, input
output = %w[
1:hell
12345:he
123:hello
1234567:h
]
assert_equal output, `cat #{tempname} | #{FZF} -fh`.split($/)
output = %w[
1234567:h
12345:he
1:hell
123:hello
]
assert_equal output, `cat #{tempname} | #{FZF} -fh -n2 -d:`.split($/)
end
def test_invalid_cache def test_invalid_cache
tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter
tmux.until { |lines| lines[-2].include? '2/3' } tmux.until { |lines| lines[-2].include? '2/3' }
@@ -635,6 +674,106 @@ class TestGoFZF < TestBase
tmux.until { |lines| lines[-10].start_with? '>' } tmux.until { |lines| lines[-10].start_with? '>' }
end end
def test_header_lines
tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5'}", :Enter
2.times do
tmux.until do |lines|
lines[-2].include?('/90') &&
lines[-3] == ' 1' &&
lines[-4] == ' 2' &&
lines[-13] == '> 15'
end
tmux.send_keys :Down
end
tmux.send_keys :Enter
assert_equal '15', readonce.chomp
end
def test_header_lines_reverse
tmux.send_keys "seq 100 | #{fzf '--header-lines=10 -q 5 --reverse'}", :Enter
2.times do
tmux.until do |lines|
lines[1].include?('/90') &&
lines[2] == ' 1' &&
lines[3] == ' 2' &&
lines[12] == '> 15'
end
tmux.send_keys :Up
end
tmux.send_keys :Enter
assert_equal '15', readonce.chomp
end
def test_header_lines_overflow
tmux.send_keys "seq 100 | #{fzf '--header-lines=200'}", :Enter
tmux.until do |lines|
lines[-2].include?('0/0') &&
lines[-3].include?(' 1')
end
tmux.send_keys :Enter
assert_equal '', readonce.chomp
end
def test_header_lines_with_nth
tmux.send_keys "seq 100 | #{fzf "--header-lines 5 --with-nth 1,1,1,1,1"}", :Enter
tmux.until do |lines|
lines[-2].include?('95/95') &&
lines[-3] == ' 11111' &&
lines[-7] == ' 55555' &&
lines[-8] == '> 66666'
end
tmux.send_keys :Enter
assert_equal '6', readonce.chomp
end
def test_header_file
tmux.send_keys "seq 100 | #{fzf "--header-file <(head -5 #{__FILE__})"}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip)
tmux.until do |lines|
lines[-2].include?('100/100') &&
lines[-7..-3].map(&:strip) == header
end
end
def test_header_file_reverse
tmux.send_keys "seq 100 | #{fzf "--header-file=<(head -5 #{__FILE__}) --reverse"}", :Enter
header = File.readlines(__FILE__).take(5).map(&:strip)
tmux.until do |lines|
lines[1].include?('100/100') &&
lines[2..6].map(&:strip) == header
end
end
def test_canel
tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter
tmux.until { |lines| lines[-2].include?('10/10') }
tmux.send_keys '123'
tmux.until { |lines| lines[-1] == '> 3' && lines[-2].include?('1/10') }
tmux.send_keys 'C-y', 'C-y'
tmux.until { |lines| lines[-1] == '> 311' }
tmux.send_keys 2
tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 2
tmux.prepare
end
def test_margin
tmux.send_keys "yes | head -1000 | #{fzf "--margin 5,3"}", :Enter
tmux.until { |lines| lines[4] == '' && lines[5] == ' y' }
tmux.send_keys :Enter
end
def test_margin_reverse
tmux.send_keys "seq 1000 | #{fzf "--margin 7,5 --reverse"}", :Enter
tmux.until { |lines| lines[1 + 7] == ' 1000/1000' }
tmux.send_keys :Enter
end
def test_invalid_term
tmux.send_keys "TERM=xxx fzf", :Enter
tmux.until { |lines| lines.any? { |l| l.include? 'Invalid $TERM: xxx' } }
end
private private
def writelines path, lines def writelines path, lines
File.unlink path while File.exists? path File.unlink path while File.exists? path
@@ -651,12 +790,20 @@ module TestShell
@tmux.kill @tmux.kill
end end
def set_var name, val
tmux.prepare
tmux.send_keys "export #{name}='#{val}'", :Enter
tmux.prepare
end
def test_ctrl_t def test_ctrl_t
tmux.prepare tmux.prepare
tmux.send_keys 'C-t', pane: 0 tmux.send_keys 'C-t', pane: 0
lines = tmux.until(1) { |lines| lines.item_count > 1 } lines = tmux.until(1) { |lines| lines.item_count > 1 }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, :Enter, pane: 1 tmux.send_keys :BTab, :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include?('(2)') }
tmux.send_keys :Enter, pane: 1
tmux.until(0) { |lines| lines[-1].include? expected } tmux.until(0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c' tmux.send_keys 'C-c'
@@ -665,11 +812,23 @@ module TestShell
tmux.send_keys 'C-t', pane: 0 tmux.send_keys 'C-t', pane: 0
lines = tmux.until(0) { |lines| lines.item_count > 1 } lines = tmux.until(0) { |lines| lines.item_count > 1 }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, :Enter, pane: 0 tmux.send_keys :BTab, :BTab, pane: 0
tmux.until(0) { |lines| lines[-2].include?('(2)') }
tmux.send_keys :Enter, pane: 0
tmux.until(0) { |lines| lines[-1].include? expected } tmux.until(0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c', 'C-d' tmux.send_keys 'C-c', 'C-d'
end end
def test_ctrl_t_command
set_var "FZF_CTRL_T_COMMAND", "seq 100"
tmux.send_keys 'C-t', pane: 0
lines = tmux.until(1) { |lines| lines.item_count == 100 }
tmux.send_keys :BTab, :BTab, :BTab, pane: 1
tmux.until(1) { |lines| lines[-2].include?('(3)') }
tmux.send_keys :Enter, pane: 1
tmux.until(0) { |lines| lines[-1].include? '1 2 3' }
end
def test_alt_c def test_alt_c
tmux.prepare tmux.prepare
tmux.send_keys :Escape, :c, pane: 0 tmux.send_keys :Escape, :c, pane: 0
@@ -710,7 +869,9 @@ module CompletionTest
tmux.prepare tmux.prepare
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0
tmux.until(1) { |lines| lines.item_count > 0 } tmux.until(1) { |lines| lines.item_count > 0 }
tmux.send_keys :BTab, :BTab, :Enter tmux.send_keys :BTab, :BTab
tmux.until(1) { |lines| lines[-2].include?('(2)') }
tmux.send_keys :Enter
tmux.until do |lines| tmux.until do |lines|
tmux.send_keys 'C-L' tmux.send_keys 'C-L'
lines[-1].include?('/tmp/fzf-test/10') && lines[-1].include?('/tmp/fzf-test/10') &&
@@ -842,6 +1003,12 @@ class TestFish < TestBase
tmux.until { |lines| lines.empty? } tmux.until { |lines| lines.empty? }
end end
def set_var name, val
tmux.prepare
tmux.send_keys "set -g #{name} '#{val}'", :Enter
tmux.prepare
end
def setup def setup
super super
@tmux = Tmux.new :fish @tmux = Tmux.new :fish