Compare commits

..

32 Commits

Author SHA1 Message Date
Junegunn Choi
34fe5ab143 0.26.0 2021-03-13 15:13:31 +09:00
Junegunn Choi
1b08f43f82 Advanced preview scroll offset expression to better support fixed header 2021-03-13 02:26:41 +09:00
Junegunn Choi
b24a2e2fdc Fix regression in preview window rendering 2021-03-12 21:23:16 +09:00
Junegunn Choi
4c4c6e626e Add support for preview window header
Fix #2373

  # Display top 3 lines as the fixed header
  fzf --preview 'bat --style=header,grid --color=always {}' --preview-window '~3'
2021-03-12 20:32:27 +09:00
Junegunn Choi
7310370a31 Fix truncation of colored line when --preview-window wrap is set
Fix #2346
2021-03-12 20:31:27 +09:00
Junegunn Choi
8ae94f0059 Fix premature truncation of colored line when --preview-window wrap is set
Fix #2346
2021-03-12 11:05:51 +09:00
Junegunn Choi
8fccf20892 Fix incorrect tab character handling
Fix #2372
2021-03-12 10:08:18 +09:00
Charlie Vieth
5a874ae241 Speed up ANSI code processing (#2368)
This commit speeds up the parsing/processing of ANSI escape codes by
roughly 7.5x. The speedup is mostly accomplished by replacing the regex
with dedicated parsing logic (nextAnsiEscapeSequence()) and reducing the
number of allocations in extractColor().

#### Benchmarks
```
name             old time/op    new time/op     delta
ExtractColor-16    4.89µs ± 5%     0.64µs ± 2%   -86.87%  (p=0.000 n=9+9)

name             old speed      new speed       delta
ExtractColor-16  25.6MB/s ± 5%  194.6MB/s ± 2%  +661.43%  (p=0.000 n=9+9)

name             old alloc/op   new alloc/op    delta
ExtractColor-16    1.37kB ± 0%     0.31kB ± 0%   -77.31%  (p=0.000 n=10+10)

name             old allocs/op  new allocs/op   delta
ExtractColor-16      48.0 ± 0%        4.0 ± 0%   -91.67%  (p=0.000 n=10+10)
```
2021-03-11 19:34:50 +09:00
Jannik Vieten
f4e1ed25f2 [fish] Make widgets work with --option= prefix (#2383)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2021-03-08 22:59:56 +09:00
Junegunn Choi
cbfbb49ab4 [vim] Vim 8.0 compatibility
Fix #2367
2021-03-08 12:56:06 +09:00
solarizedalias
489b16efce [fzf-tmux] Adapt to tmux latest changes (#2379) 2021-03-08 12:44:36 +09:00
Junegunn Choi
b82c1693c0 Fix deadlocks 2021-03-08 00:08:10 +09:00
Junegunn Choi
019bfc4e35 Fix yet another deadlock
EventBox.Set should not be called while holding the terminal mutex

  goroutine 1 [semacquire]:
  sync.runtime_SemacquireMutex(0xc0001923bc, 0x1000001066200, 0x1)
          /usr/local/Cellar/go/1.16/libexec/src/runtime/sema.go:71 +0x47
  sync.(*Mutex).lockSlow(0xc0001923b8)
          /usr/local/Cellar/go/1.16/libexec/src/sync/mutex.go:138 +0x105
  sync.(*Mutex).Lock(...)
          /usr/local/Cellar/go/1.16/libexec/src/sync/mutex.go:81
  github.com/junegunn/fzf/src.(*Terminal).Input(0xc000192000, 0x0, 0x0, 0x0, 0x0)
          /fzf/src/terminal.go:581 +0x145
  github.com/junegunn/fzf/src.Run.func10(0xc00010c8a0, 0xc000092050, 0xa)
          /fzf/src/core.go:245 +0x37
  github.com/junegunn/fzf/src.Run.func11(0xc00011a4e0)
          /fzf/src/core.go:295 +0x5ce
  github.com/junegunn/fzf/src/util.(*EventBox).Wait(0xc00011a4e0, 0xc000127ec8)
          /fzf/src/util/eventbox.go:34 +0x5e
  github.com/junegunn/fzf/src.Run(0xc000180000, 0x11ac014, 0x6, 0x11ac158, 0x7)
          /fzf/src/core.go:251 +0xdac
  main.main()
          /fzf/main.go:13 +0x5a

  goroutine 11 [semacquire]:
  sync.runtime_SemacquireMutex(0xc00012c31c, 0xc00010e800, 0x1)
          /usr/local/Cellar/go/1.16/libexec/src/runtime/sema.go:71 +0x47
  sync.(*Mutex).lockSlow(0xc00012c318)
          /usr/local/Cellar/go/1.16/libexec/src/sync/mutex.go:138 +0x105
  sync.(*Mutex).Lock(0xc00012c318)
          /usr/local/Cellar/go/1.16/libexec/src/sync/mutex.go:81 +0x47
  github.com/junegunn/fzf/src/util.(*EventBox).Set(0xc00011a4e0, 0x7, 0x114eb40, 0x1265460)
          /fzf/src/util/eventbox.go:40 +0x3b
  github.com/junegunn/fzf/src.(*Terminal).killPreview(0xc000192000, 0x0)
          /fzf/src/terminal.go:1831 +0xa5
  github.com/junegunn/fzf/src.(*Terminal).exit(0xc000192000, 0xc000106e58)
          /fzf/src/terminal.go:1847 +0x75
  github.com/junegunn/fzf/src.(*Terminal).Loop.func8.1(0xc00011a540)
          /fzf/src/terminal.go:2148 +0x38f
  github.com/junegunn/fzf/src/util.(*EventBox).Wait(0xc00011a540, 0xc000106f90)
          /fzf/src/util/eventbox.go:34 +0x5e
  github.com/junegunn/fzf/src.(*Terminal).Loop.func8(0xc000192000, 0xc00010a2c0)
          /fzf/src/terminal.go:2077 +0xa5
  created by github.com/junegunn/fzf/src.(*Terminal).Loop
          /fzf/src/terminal.go:2072 +0x3e8
2021-03-07 23:35:19 +09:00
Junegunn Choi
dfda5c054a [actions] Install fish using apt-get
For some reason, `test_ctrl_r` and `test_ctrl_r_abort` are not passing
on GitHub Action runner with Fish 3.2.0.
2021-03-07 22:41:27 +09:00
Junegunn Choi
f657169616 Fix deadlock on exit 2021-03-07 21:44:08 +09:00
Junegunn Choi
4c06da8b70 Fix GitHub Action build
$USER is missing
2021-03-07 18:05:39 +09:00
yoshida.shinya
9fe2393a00 Add test cases for killing input command on terminate (#2381 #2382) 2021-03-07 11:36:00 +09:00
Junegunn Choi
e2e8d94b14 Kill input command on terminate
Fix #2381
Close #2382
2021-03-07 11:30:26 +09:00
bitterfox
4f9a7f8c87 Don't exit fzf by SIGINT while executing command (#2375)
Fix #2374

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2021-02-28 21:01:03 +09:00
Junegunn Choi
bb0502ff44 Check gofmt in make test 2021-02-28 18:28:21 +09:00
Junegunn Choi
c256442245 Fix typo 2021-02-28 18:27:21 +09:00
Jonathan Müller
1137404190 [vim] Add keepjump to switch_back() function (#2363)
Otherwise, the jump list will contain a (hidden) entry for the FZF buffer if `window: enew` is used.
2021-02-25 21:58:13 +09:00
Junegunn Choi
d57c6d0284 Update build script to build macOS arm64 binary
Close #2361
2021-02-25 21:31:15 +09:00
Junegunn Choi
76bbf57b3d Add select and deselect actions
Close #2358
2021-02-25 21:23:05 +09:00
Hiroki Konishi
806a47a7cc [vim] Remove unnecessary border management in nvim floating window (#2370) 2021-02-25 14:41:23 +09:00
Junegunn Choi
29851c18aa [vim] Force redraw by exiting and re-entering terminal mode
Workaround for Neovim v0.5.0-dev

https://github.com/junegunn/fzf/issues/2352#issuecomment-782894123
2021-02-22 21:46:28 +09:00
Junegunn Choi
dea950c2c8 [vim] Call feedkeys only when the destination buffer is a terminal
Fix #2352
Fix https://github.com/junegunn/fzf.vim/issues/1216

Close #2364
2021-02-22 00:22:12 +09:00
Junegunn Choi
a367dfb22e README.md: Better example 2021-02-17 16:44:54 +09:00
odeson24
9fe1a7b373 Remove redundant assignment (#2356)
Co-authored-by: Ryan Ou <ryanou@aetherai.com>
2021-02-17 10:28:43 +09:00
Hussein Esmail
8e2d21c548 Update README.md (#2353)
Remove Linuxbrew links since Linuxbrew has been merged into Homebrew

* https://brew.sh/2019/02/02/homebrew-2.0.0/
2021-02-17 10:24:35 +09:00
Junegunn Choi
bedf1cd357 [vim] Use tnoremap only when it's available
Fix #2357
2021-02-17 10:04:38 +09:00
Junegunn Choi
13f180a70c [vim] Stay in terminal mode if fzf#run is called from sink
Fix #2352
2021-02-15 13:58:49 +09:00
29 changed files with 881 additions and 227 deletions

View File

@@ -23,16 +23,13 @@ jobs:
with:
go-version: ${{ matrix.go }}
- name: fish-actions/install-fish
uses: fish-actions/install-fish@v1.0.0
- name: Setup Ruby
uses: ruby/setup-ruby@v1.62.0
with:
ruby-version: 3.0.0
- name: Install packages
run: sudo apt-get install --yes zsh tmux
run: sudo apt-get install --yes zsh fish tmux
- name: Install Ruby gems
run: sudo gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1

View File

@@ -1,11 +0,0 @@
source = ["./dist/fzf-macos_darwin_amd64/fzf"]
bundle_id = "kr.junegunn.fzf"
apple_id {
username = "junegunn.c@gmail.com"
password = "@env:AC_PASSWORD"
}
sign {
application_identity = "Apple Development: junegunn.c@gmail.com"
}

View File

@@ -12,10 +12,25 @@ builds:
- darwin
goarch:
- amd64
- arm64
ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
hooks:
post: gon .gon.hcl
post: |-
sh -c '
cat > /tmp/fzf-gon.hcl << EOF
source = ["./dist/fzf-macos_darwin_{{ .Arch }}/fzf"]
bundle_id = "kr.junegunn.fzf"
apple_id {
username = "junegunn.c@gmail.com"
password = "@env:AC_PASSWORD"
}
sign {
application_identity = "Apple Development: junegunn.c@gmail.com"
}
EOF
gon /tmp/fzf-gon.hcl
'
- goos:
- linux

View File

@@ -1,6 +1,34 @@
CHANGELOG
=========
0.26.0
------
- Added support for fixed header in preview window
```sh
# Display top 3 lines as the fixed header
fzf --preview 'bat --style=header,grid --color=always {}' --preview-window '~3'
```
- More advanced preview offset expression to better support the fixed header
```sh
# Preview with bat, matching line in the middle of the window below
# the fixed header of the top 3 lines
#
# ~3 Top 3 lines as the fixed header
# +{2} Base scroll offset extracted from the second field
# +3 Extra offset to compensate for the 3-line header
# /2 Put in the middle of the preview area
#
git grep --line-number '' |
fzf --delimiter : \
--preview 'bat --style=full --color=always --highlight-line {2} {1}' \
--preview-window '~3:+{2}+3/2'
```
- Added `select` and `deselect` action for unconditionally selecting or
deselecting a single item in `--multi` mode. Complements `toggle` action.
- Sigificant performance improvement in ANSI code processing
- Bug fixes and improvements
- Built with Go 1.16
0.25.1
------
- Added `close` action

View File

@@ -66,12 +66,16 @@ endif
all: target/$(BINARY)
test: $(SOURCES)
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
github.com/junegunn/fzf/src \
github.com/junegunn/fzf/src/algo \
github.com/junegunn/fzf/src/tui \
github.com/junegunn/fzf/src/util
bench:
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
install: bin/fzf
build:
@@ -152,4 +156,4 @@ update:
$(GO) get -u
$(GO) mod tidy
.PHONY: all build release test install clean docker docker-test update
.PHONY: all build release test bench install clean docker docker-test update

View File

@@ -25,7 +25,7 @@ Table of Contents
<!-- vim-markdown-toc GFM -->
* [Installation](#installation)
* [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew)
* [Using Homebrew](#using-homebrew)
* [Using git](#using-git)
* [Using Linux package managers](#using-linux-package-managers)
* [Windows](#windows)
@@ -84,9 +84,9 @@ stuff.
[bin]: https://github.com/junegunn/fzf/releases
### Using Homebrew or Linuxbrew
### Using Homebrew
You can use [Homebrew](http://brew.sh/) or [Linuxbrew](http://linuxbrew.sh/)
You can use [Homebrew](http://brew.sh/) (on macOS or Linux)
to install fzf.
```sh
@@ -411,7 +411,7 @@ unalias **<TAB>
export FZF_COMPLETION_TRIGGER='~~'
# Options to fzf command
export FZF_COMPLETION_OPTS='+c -x'
export FZF_COMPLETION_OPTS='--border --info=inline'
# Use fd (https://github.com/sharkdp/fd) instead of the default find
# command for listing path candidates.

View File

@@ -204,7 +204,16 @@ if [[ "$opt" =~ "-K -E" ]]; then
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; out=\$? $close; exit \$out" >> $argsf
cat <&0 > $fifo1 &
fi
tmux popup -d "$PWD" "${tmux_args[@]}" $opt -R "bash $argsf" > /dev/null 2>&1
# tmux dropped the support for `-K`, `-R` to popup command
# TODO: We can remove this once tmux 3.2 is released
if [[ ! "$(tmux popup --help 2>&1)" =~ '-R shell-command' ]]; then
opt="${opt/-K/}"
else
opt="${opt} -R"
fi
tmux popup -d "$PWD" "${tmux_args[@]}" $opt "bash $argsf" > /dev/null 2>&1
exit $?
fi

2
go.sum
View File

@@ -17,6 +17,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90Pveol
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
@@ -32,4 +33,5 @@ golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@@ -2,7 +2,7 @@
set -u
version=0.25.1
version=0.26.0
auto_completion=
key_bindings=
update_config=2
@@ -168,7 +168,8 @@ archi=$(uname -sm)
binary_available=1
binary_error=""
case "$archi" in
Darwin\ *64) download fzf-$version-darwin_amd64.tar.gz ;;
# Darwin\ arm64) download fzf-$version-darwin_arm64.tar.gz ;; # TODO
Darwin\ x86_64) download fzf-$version-darwin_amd64.tar.gz ;;
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;

View File

@@ -1,4 +1,4 @@
$version="0.25.1"
$version="0.26.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition

View File

@@ -5,7 +5,7 @@ import (
"github.com/junegunn/fzf/src/protector"
)
var version string = "0.25"
var version string = "0.26"
var revision string = "devel"
func main() {

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf-tmux 1 "Feb 2021" "fzf 0.25.1" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "Mar 2021" "fzf 0.26.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf 1 "Feb 2021" "fzf 0.25.1" "fzf - a command-line fuzzy finder"
.TH fzf 1 "Mar 2021" "fzf 0.26.0" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -442,7 +442,7 @@ e.g.
done'\fR
.RE
.TP
.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]follow][:[no]cycle][:[no]hidden][:+SCROLL[-OFFSET]][:default]"
.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]follow][:[no]cycle][:[no]hidden][:+SCROLL[OFFSETS][/DENOM]][:~HEADER_LINES][:default]"
.RS
.B POSITION: (default: right)
@@ -480,12 +480,17 @@ e.g.
\fBrounded\fR (border with rounded edges, default), \fBsharp\fR (border with
sharp edges), or \fBnoborder\fR (no border).
* \fB+SCROLL[-OFFSET]\fR determines the initial scroll offset of the preview
window. \fBSCROLL\fR can be either a numeric integer or a single-field index
expression that refers to a numeric integer. The optional \fB-OFFSET\fR part is
for adjusting the base offset so that you can see the text above it. It should
be given as a numeric integer (\fB-INTEGER\fR), or as a denominator form
(\fB-/INTEGER\fR) for specifying a fraction of the preview window height.
* \fB[:+SCROLL[OFFSETS][/DENOM]]\fR determines the initial scroll offset of the
preview window.
- \fBSCROLL\fR can be either a numeric integer or a single-field index expression that refers to a numeric integer.
- The optional \fBOFFSETS\fR part is for adjusting the base offset. It should be given as a series of signed integers (\fB-INTEGER\fR or \fB+INTEGER\fR).
- The final \fB/DENOM\fR part is for specifying a fraction of the preview window height.
* \fB~HEADER_LINES\fR keeps the top N lines as the fixed header so that they
are always visible.
* \fBdefault\fR resets all options previously set to the default.
@@ -498,14 +503,23 @@ e.g.
# Initial scroll offset is set to the line number of each line of
# git grep output *minus* 5 lines (-5)
git grep --line-number '' |
fzf --delimiter : --preview 'nl {1}' --preview-window +{2}-5
fzf --delimiter : --preview 'nl {1}' --preview-window '+{2}-5'
# Preview with bat, matching line in the middle of the window (-/2)
# Preview with bat, matching line in the middle of the window below
# the fixed header of the top 3 lines
#
# ~3 Top 3 lines as the fixed header
# +{2} Base scroll offset extracted from the second field
# +3 Extra offset to compensate for the 3-line header
# /2 Put in the middle of the preview area
#
git grep --line-number '' |
fzf --delimiter : \\
--preview 'bat --style=numbers --color=always --highlight-line {2} {1}' \\
--preview-window +{2}-/2\fR
--preview 'bat --style=full --color=always --highlight-line {2} {1}' \\
--preview-window '~3:+{2}+3/2'
# Display top 3 lines as the fixed header
fzf --preview 'bat --style=full --color=always {}' --preview-window '~3'\fR
.RE
.SS Scripting
@@ -784,6 +798,7 @@ A key or an event can be bound to one or more of the following actions.
\fBclear-query\fR (clear query string)
\fBdelete-char\fR \fIdel\fR
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
\fBdeselect\fR
\fBdeselect-all\fR (deselect all matches)
\fBdisable-search\fR (disable search functionality)
\fBdown\fR \fIctrl-j ctrl-n down\fR
@@ -819,6 +834,7 @@ A key or an event can be bound to one or more of the following actions.
\fBrefresh-preview\fR
\fBreload(...)\fR (see below for the details)
\fBreplace-query\fR (replace query string with the current selection)
\fBselect\fR
\fBselect-all\fR (select all matches)
\fBtoggle\fR (\fIright-click\fR)
\fBtoggle-all\fR (toggle all matches)

View File

@@ -764,6 +764,13 @@ function! s:split(dict)
endtry
endfunction
nnoremap <silent> <Plug>(fzf-insert) i
nnoremap <silent> <Plug>(fzf-normal) <Nop>
if exists(':tnoremap')
tnoremap <silent> <Plug>(fzf-insert) <C-\><C-n>i
tnoremap <silent> <Plug>(fzf-normal) <C-\><C-n>
endif
function! s:execute_term(dict, command, temps) abort
let winrest = winrestcmd()
let pbuf = bufnr('')
@@ -776,7 +783,7 @@ function! s:execute_term(dict, command, temps) abort
function! fzf.switch_back(inplace)
if a:inplace && bufnr('') == self.buf
if bufexists(self.pbuf)
execute 'keepalt b' self.pbuf
execute 'keepalt keepjumps b' self.pbuf
endif
" No other listed buffer
if bufnr('') == self.buf
@@ -792,8 +799,6 @@ function! s:execute_term(dict, command, temps) abort
call self.switch_back(1)
else
if bufnr('') == self.buf
" Exit terminal mode first (see neovim#13769)
call feedkeys("\<C-\>\<C-n>", 'n')
" We use close instead of bd! since Vim does not close the split when
" there's no other listed buffer (nvim +'set nobuflisted')
close
@@ -818,6 +823,10 @@ function! s:execute_term(dict, command, temps) abort
call s:pushd(self.dict)
call s:callback(self.dict, lines)
call self.switch_back(s:getpos() == self.ppos)
if &buftype == 'terminal'
call feedkeys(&filetype == 'fzf' ? "\<Plug>(fzf-insert)" : "\<Plug>(fzf-normal)")
endif
endfunction
try
@@ -833,7 +842,10 @@ function! s:execute_term(dict, command, temps) abort
if has('nvim')
call termopen(command, fzf)
else
let term_opts = {'exit_cb': function(fzf.on_exit), 'term_kill': 'term'}
let term_opts = {'exit_cb': function(fzf.on_exit)}
if v:version >= 802
let term_opts.term_kill = 'term'
endif
if is_popup
let term_opts.hidden = 1
else
@@ -906,13 +918,9 @@ if has('nvim')
function s:create_popup(hl, opts) abort
let buf = nvim_create_buf(v:false, v:true)
let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
let border = has_key(opts, 'border') ? remove(opts, 'border') : []
let win = nvim_open_win(buf, v:true, opts)
call setwinvar(win, '&winhighlight', 'NormalFloat:'..a:hl)
call setwinvar(win, '&colorcolumn', '')
if !empty(border)
call nvim_buf_set_lines(buf, 0, -1, v:true, border)
endif
return buf
endfunction
else

View File

@@ -20,6 +20,7 @@ function fzf_key_bindings
set -l commandline (__fzf_parse_commandline)
set -l dir $commandline[1]
set -l fzf_query $commandline[2]
set -l prefix $commandline[3]
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
# $dir itself, even if hidden.
@@ -42,6 +43,7 @@ function fzf_key_bindings
commandline -t ""
end
for i in $result
commandline -it -- $prefix
commandline -it -- (string escape $i)
commandline -it -- ' '
end
@@ -74,6 +76,7 @@ function fzf_key_bindings
set -l commandline (__fzf_parse_commandline)
set -l dir $commandline[1]
set -l fzf_query $commandline[2]
set -l prefix $commandline[3]
test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND "
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
@@ -88,6 +91,7 @@ function fzf_key_bindings
# Remove last token from commandline.
commandline -t ""
commandline -it -- $prefix
end
end
@@ -116,9 +120,15 @@ function fzf_key_bindings
bind -M insert \ec fzf-cd-widget
end
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath and rest of token'
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
set -l commandline (commandline -t)
# strip -option= from token if present
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
set commandline (string replace -- "$prefix" '' $commandline)
# eval is used to do shell expansion on paths
set -l commandline (eval "printf '%s' "(commandline -t))
eval set commandline $commandline
if [ -z $commandline ]
# Default to current directory with no --query
@@ -138,6 +148,7 @@ function fzf_key_bindings
echo $dir
echo $fzf_query
echo $prefix
end
function __fzf_get_dir -d 'Find the longest existing filepath from input string'

View File

@@ -1,8 +1,6 @@
package fzf
import (
"bytes"
"regexp"
"strconv"
"strings"
"unicode/utf8"
@@ -82,73 +80,154 @@ func toAnsiString(color tui.Color, offset int) string {
return ret + ";"
}
var ansiRegex *regexp.Regexp
func init() {
/*
References:
- https://github.com/gnachman/iTerm2
- http://ascii-table.com/ansi-escape-sequences.php
- http://ascii-table.com/ansi-escape-sequences-vt-100.php
- http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
- https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
*/
// The following regular expression will include not all but most of the
// frequently used ANSI sequences
ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
func isPrint(c uint8) bool {
return '\x20' <= c && c <= '\x7e'
}
func findAnsiStart(str string) int {
idx := 0
for ; idx < len(str); idx++ {
b := str[idx]
if b == 0x1b || b == 0x0e || b == 0x0f {
return idx
func matchOperatingSystemCommand(s string) int {
// `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)`
// ^ match starting here
//
i := 5 // prefix matched in nextAnsiEscapeSequence()
for ; i < len(s) && isPrint(s[i]); i++ {
}
if i < len(s) {
if s[i] == '\x07' {
return i + 1
}
if b == 0x08 && idx > 0 {
return idx - 1
if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' {
return i + 2
}
}
return idx
return -1
}
func matchControlSequence(s string) int {
// `\x1b[\\[()][0-9;]*[a-zA-Z@]`
// ^ match starting here
//
i := 2 // prefix matched in nextAnsiEscapeSequence()
for ; i < len(s) && (isNumeric(s[i]) || s[i] == ';'); i++ {
}
if i < len(s) {
c := s[i]
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '@' {
return i + 1
}
}
return -1
}
func isCtrlSeqStart(c uint8) bool {
return c == '\\' || c == '[' || c == '(' || c == ')'
}
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
// calling FindStringIndex() on the below regex (which was originally used):
//
// "(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)"
//
func nextAnsiEscapeSequence(s string) (int, int) {
// fast check for ANSI escape sequences
i := 0
for ; i < len(s); i++ {
switch s[i] {
case '\x0e', '\x0f', '\x1b', '\x08':
// We ignore the fact that '\x08' cannot be the first char
// in the string and be an escape sequence for the sake of
// speed and simplicity.
goto Loop
}
}
return -1, -1
Loop:
for ; i < len(s); i++ {
switch s[i] {
case '\x08':
// backtrack to match: `.\x08`
if i > 0 && s[i-1] != '\n' {
if s[i-1] < utf8.RuneSelf {
return i - 1, i + 1
}
_, n := utf8.DecodeLastRuneInString(s[:i])
return i - n, i + 1
}
case '\x1b':
// match: `\x1b[\\[()][0-9;]*[a-zA-Z@]`
if i+2 < len(s) && isCtrlSeqStart(s[i+1]) {
if j := matchControlSequence(s[i:]); j != -1 {
return i, i + j
}
}
// match: `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)`
if i+5 < len(s) && s[i+1] == ']' && isNumeric(s[i+2]) &&
s[i+3] == ';' && isPrint(s[i+4]) {
if j := matchOperatingSystemCommand(s[i:]); j != -1 {
return i, i + j
}
}
// match: `\x1b.`
if i+1 < len(s) && s[i+1] != '\n' {
if s[i+1] < utf8.RuneSelf {
return i, i + 2
}
_, n := utf8.DecodeRuneInString(s[i+1:])
return i, i + n + 1
}
case '\x0e', '\x0f':
// match: `[\x0e\x0f]`
return i, i + 1
}
}
return -1, -1
}
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
var offsets []ansiOffset
var output bytes.Buffer
// We append to a stack allocated variable that we'll
// later copy and return, to save on allocations.
offsets := make([]ansiOffset, 0, 32)
if state != nil {
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
}
prevIdx := 0
runeCount := 0
var (
pstate *ansiState // lazily allocated
output strings.Builder
prevIdx int
runeCount int
)
for idx := 0; idx < len(str); {
idx += findAnsiStart(str[idx:])
if idx == len(str) {
// Make sure that we found an ANSI code
start, end := nextAnsiEscapeSequence(str[idx:])
if start == -1 {
break
}
// Make sure that we found an ANSI code
offset := ansiRegex.FindStringIndex(str[idx:])
if len(offset) < 2 {
idx++
continue
}
offset[0] += idx
offset[1] += idx
idx = offset[1]
start += idx
idx += end
// Check if we should continue
prev := str[prevIdx:offset[0]]
prev := str[prevIdx:start]
if proc != nil && !proc(prev, state) {
return "", nil, nil
}
prevIdx = idx
prevIdx = offset[1]
runeCount += utf8.RuneCountInString(prev)
output.WriteString(prev)
if len(prev) != 0 {
runeCount += utf8.RuneCountInString(prev)
// Grow the buffer size to the maximum possible length (string length
// containing ansi codes) to avoid repetitive allocation
if output.Cap() == 0 {
output.Grow(len(str))
}
output.WriteString(prev)
}
newState := interpretCode(str[offset[0]:offset[1]], state)
newState := interpretCode(str[start:idx], state)
if !newState.equals(state) {
if state != nil {
// Update last offset
@@ -157,8 +236,15 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
if newState.colored() {
// Append new offset
state = newState
offsets = append(offsets, ansiOffset{[2]int32{int32(runeCount), int32(runeCount)}, *state})
if pstate == nil {
pstate = &ansiState{}
}
*pstate = newState
state = pstate
offsets = append(offsets, ansiOffset{
[2]int32{int32(runeCount), int32(runeCount)},
newState,
})
} else {
// Discard state
state = nil
@@ -168,7 +254,6 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
var rest string
var trimmed string
if prevIdx == 0 {
// No ANSI code found
rest = str
@@ -178,51 +263,75 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
output.WriteString(rest)
trimmed = output.String()
}
if len(rest) > 0 && state != nil {
// Update last offset
runeCount += utf8.RuneCountInString(rest)
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
}
if proc != nil {
proc(rest, state)
}
if len(offsets) == 0 {
return trimmed, nil, state
if len(offsets) > 0 {
if len(rest) > 0 && state != nil {
// Update last offset
runeCount += utf8.RuneCountInString(rest)
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
}
// Return a copy of the offsets slice
a := make([]ansiOffset, len(offsets))
copy(a, offsets)
return trimmed, &a, state
}
return trimmed, &offsets, state
return trimmed, nil, state
}
func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
// State
var state *ansiState
func parseAnsiCode(s string) (int, string) {
var remaining string
if i := strings.IndexByte(s, ';'); i >= 0 {
remaining = s[i+1:]
s = s[:i]
}
if len(s) > 0 {
// Inlined version of strconv.Atoi() that only handles positive
// integers and does not allocate on error.
code := 0
for _, ch := range []byte(s) {
ch -= '0'
if ch > 9 {
return -1, remaining
}
code = code*10 + int(ch)
}
return code, remaining
}
return -1, remaining
}
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
var state ansiState
if prevState == nil {
state = &ansiState{-1, -1, 0, -1}
state = ansiState{-1, -1, 0, -1}
} else {
state = &ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
}
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
if strings.HasSuffix(ansiCode, "0K") && prevState != nil {
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
state.lbg = prevState.bg
}
return state
}
ptr := &state.fg
state256 := 0
init := func() {
if len(ansiCode) <= 3 {
state.fg = -1
state.bg = -1
state.attr = 0
state256 = 0
return state
}
ansiCode = ansiCode[2 : len(ansiCode)-1]
if len(ansiCode) == 0 {
init()
}
for _, code := range strings.Split(ansiCode, ";") {
if num, err := strconv.Atoi(code); err == nil {
state256 := 0
ptr := &state.fg
for len(ansiCode) != 0 {
var num int
if num, ansiCode = parseAnsiCode(ansiCode); num != -1 {
switch state256 {
case 0:
switch num {
@@ -253,7 +362,10 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
case 24: // tput rmul
state.attr = state.attr &^ tui.Underline
case 0:
init()
state.fg = -1
state.bg = -1
state.attr = 0
state256 = 0
default:
if num >= 30 && num <= 37 {
state.fg = tui.Color(num - 30)
@@ -289,6 +401,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
}
}
}
if state256 > 0 {
*ptr = -1
}

View File

@@ -2,12 +2,190 @@ package fzf
import (
"fmt"
"math/rand"
"regexp"
"strings"
"testing"
"unicode/utf8"
"github.com/junegunn/fzf/src/tui"
)
// The following regular expression will include not all but most of the
// frequently used ANSI sequences. This regex is used as a reference for
// testing nextAnsiEscapeSequence().
//
// References:
// - https://github.com/gnachman/iTerm2
// - http://ascii-table.com/ansi-escape-sequences.php
// - http://ascii-table.com/ansi-escape-sequences-vt-100.php
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
var ansiRegexRefence = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
func testParserReference(t testing.TB, str string) {
t.Helper()
toSlice := func(start, end int) []int {
if start == -1 {
return nil
}
return []int{start, end}
}
s := str
for i := 0; ; i++ {
got := toSlice(nextAnsiEscapeSequence(s))
exp := ansiRegexRefence.FindStringIndex(s)
equal := len(got) == len(exp)
if equal {
for i := 0; i < len(got); i++ {
if got[i] != exp[i] {
equal = false
break
}
}
}
if !equal {
var exps, gots []rune
if len(got) == 2 {
gots = []rune(s[got[0]:got[1]])
}
if len(exp) == 2 {
exps = []rune(s[exp[0]:exp[1]])
}
t.Errorf("%d: %q: got: %v (%q) want: %v (%q)", i, s, got, gots, exp, exps)
return
}
if len(exp) == 0 {
return
}
s = s[exp[1]:]
}
}
func TestNextAnsiEscapeSequence(t *testing.T) {
testStrs := []string{
"\x1b[0mhello world",
"\x1b[1mhello world",
"椙\x1b[1m椙",
"椙\x1b[1椙m椙",
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
"\x1b[1mhello \x1b[Kworld",
"hello \x1b[34;45;1mworld",
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
"hello \x1b[34;45;1mwor\x1b[0mld",
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
"hello \x1b[32;1mworld",
"hello world",
"hello \x1b[0;38;5;200;48;5;100mworld",
"\x1b椙",
"椙\x08",
"\n\x08",
"X\x08",
"",
"\x1b]4;3;rgb:aa/bb/cc\x07 ",
"\x1b]4;3;rgb:aa/bb/cc\x1b\\ ",
ansiBenchmarkString,
}
for _, s := range testStrs {
testParserReference(t, s)
}
}
func TestNextAnsiEscapeSequence_Fuzz_Modified(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("short test")
}
testStrs := []string{
"\x1b[0mhello world",
"\x1b[1mhello world",
"椙\x1b[1m椙",
"椙\x1b[1椙m椙",
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
"\x1b[1mhello \x1b[Kworld",
"hello \x1b[34;45;1mworld",
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
"hello \x1b[34;45;1mwor\x1b[0mld",
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
"hello \x1b[32;1mworld",
"hello world",
"hello \x1b[0;38;5;200;48;5;100mworld",
ansiBenchmarkString,
}
replacementBytes := [...]rune{'\x0e', '\x0f', '\x1b', '\x08'}
modifyString := func(s string, rr *rand.Rand) string {
n := rr.Intn(len(s))
b := []rune(s)
for ; n >= 0 && len(b) != 0; n-- {
i := rr.Intn(len(b))
switch x := rr.Intn(4); x {
case 0:
b = append(b[:i], b[i+1:]...)
case 1:
j := rr.Intn(len(replacementBytes) - 1)
b[i] = replacementBytes[j]
case 2:
x := rune(rr.Intn(utf8.MaxRune))
for !utf8.ValidRune(x) {
x = rune(rr.Intn(utf8.MaxRune))
}
b[i] = x
case 3:
b[i] = rune(rr.Intn(utf8.MaxRune)) // potentially invalid
default:
t.Fatalf("unsupported value: %d", x)
}
}
return string(b)
}
rr := rand.New(rand.NewSource(1))
for _, s := range testStrs {
for i := 1_000; i >= 0; i-- {
testParserReference(t, modifyString(s, rr))
}
}
}
func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("short test")
}
randomString := func(rr *rand.Rand) string {
numChars := rand.Intn(50)
codePoints := make([]rune, numChars)
for i := 0; i < len(codePoints); i++ {
var r rune
for n := 0; n < 1000; n++ {
r = rune(rr.Intn(utf8.MaxRune))
// Allow 10% of runes to be invalid
if utf8.ValidRune(r) || rr.Float64() < 0.10 {
break
}
}
codePoints[i] = r
}
return string(codePoints)
}
rr := rand.New(rand.NewSource(1))
for i := 0; i < 100_000; i++ {
testParserReference(t, randomString(rr))
}
}
func TestExtractColor(t *testing.T) {
assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) {
var attr tui.Attr
@@ -185,3 +363,64 @@ func TestAnsiCodeStringConversion(t *testing.T) {
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
}
func TestParseAnsiCode(t *testing.T) {
tests := []struct {
In, Exp string
N int
}{
{"123", "", 123},
{"1a", "", -1},
{"1a;12", "12", -1},
{"12;a", "a", 12},
{"-2", "", -1},
}
for _, x := range tests {
n, s := parseAnsiCode(x.In)
if n != x.N || s != x.Exp {
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
}
}
}
// kernel/bpf/preload/iterators/README
const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38;5;81mbpf/" +
"\x1b[0m\x1b[38;5;81mpreload/\x1b[0m\x1b[38;5;81miterators/" +
"\x1b[0m\x1b[38;5;149mMakefile\x1b[m\x1b[K\x1b[0m"
func BenchmarkNextAnsiEscapeSequence(b *testing.B) {
b.SetBytes(int64(len(ansiBenchmarkString)))
for i := 0; i < b.N; i++ {
s := ansiBenchmarkString
for {
_, o := nextAnsiEscapeSequence(s)
if o == -1 {
break
}
s = s[o:]
}
}
}
// Baseline test to compare the speed of nextAnsiEscapeSequence() to the
// previously used regex based implementation.
func BenchmarkNextAnsiEscapeSequence_Regex(b *testing.B) {
b.SetBytes(int64(len(ansiBenchmarkString)))
for i := 0; i < b.N; i++ {
s := ansiBenchmarkString
for {
a := ansiRegexRefence.FindStringIndex(s)
if len(a) == 0 {
break
}
s = s[a[1]:]
}
}
}
func BenchmarkExtractColor(b *testing.B) {
b.SetBytes(int64(len(ansiBenchmarkString)))
for i := 0; i < b.N; i++ {
extractColor(ansiBenchmarkString, nil, nil)
}
}

View File

@@ -6,8 +6,8 @@ func TestChunkCache(t *testing.T) {
cache := NewChunkCache()
chunk1p := &Chunk{}
chunk2p := &Chunk{count: chunkSize}
items1 := []Result{Result{}}
items2 := []Result{Result{}, Result{}}
items1 := []Result{{}}
items2 := []Result{{}, {}}
cache.Add(chunk1p, "foo", items1)
cache.Add(chunk2p, "foo", items1)
cache.Add(chunk2p, "bar", items2)

View File

@@ -73,6 +73,7 @@ const (
EvtSearchFin
EvtHeader
EvtReady
EvtQuit
)
const (

View File

@@ -254,7 +254,11 @@ func Run(opts *Options, version string, revision string) {
}
for evt, value := range *events {
switch evt {
case EvtQuit:
if reading {
reader.terminate()
}
os.Exit(value.(int))
case EvtReadNew, EvtReadFin:
if evt == EvtReadFin && nextCommand != nil {
restart(*nextCommand)

View File

@@ -106,7 +106,6 @@ func (mg *Merger) mergedGet(idx int) Result {
minIdx = listIdx
}
}
mg.cursors[listIdx] = cursor
}
if minIdx >= 0 {

View File

@@ -84,7 +84,7 @@ const usage = `usage: fzf [options]
[up|down|left|right][:SIZE[%]]
[:[no]wrap][:[no]cycle][:[no]follow][:[no]hidden]
[:rounded|sharp|noborder]
[:+SCROLL[-OFFSET]]
[:+SCROLL[OFFSETS][/DENOM]][:~HEADER_LINES]
[:default]
Scripting
@@ -161,15 +161,16 @@ const (
)
type previewOpts struct {
command string
position windowPosition
size sizeSpec
scroll string
hidden bool
wrap bool
cycle bool
follow bool
border tui.BorderShape
command string
position windowPosition
size sizeSpec
scroll string
hidden bool
wrap bool
cycle bool
follow bool
border tui.BorderShape
headerLines int
}
// Options stores the values of command-line options
@@ -231,7 +232,7 @@ type Options struct {
}
func defaultPreviewOpts(command string) previewOpts {
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded}
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0}
}
func defaultOptions() *Options {
@@ -839,6 +840,8 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
appendAction(actDeleteChar)
case "delete-char/eof":
appendAction(actDeleteCharEOF)
case "deselect":
appendAction(actDeselect)
case "end-of-line":
appendAction(actEndOfLine)
case "cancel":
@@ -879,6 +882,8 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
appendAction(actToggleAll)
case "toggle-search":
appendAction(actToggleSearch)
case "select":
appendAction(actSelect)
case "select-all":
appendAction(actSelectAll)
case "deselect-all":
@@ -1073,7 +1078,8 @@ func parseInfoStyle(str string) infoStyle {
func parsePreviewWindow(opts *previewOpts, input string) {
tokens := strings.Split(input, ":")
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
offsetRegex := regexp.MustCompile("^\\+([0-9]+|{-?[0-9]+})(-[0-9]+|-/[1-9][0-9]*)?$")
offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
headerRegex := regexp.MustCompile("^~(0|[1-9][0-9]*)$")
for _, token := range tokens {
switch token {
case "":
@@ -1110,10 +1116,12 @@ func parsePreviewWindow(opts *previewOpts, input string) {
case "nofollow":
opts.follow = false
default:
if sizeRegex.MatchString(token) {
if headerRegex.MatchString(token) {
opts.headerLines = atoi(token[1:])
} else if sizeRegex.MatchString(token) {
opts.size = parseSize(token, 99, "window size")
} else if offsetRegex.MatchString(token) {
opts.scroll = token[1:]
opts.scroll = token
} else {
errorExit("invalid preview window option: " + token)
}
@@ -1360,7 +1368,7 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Preview.command = ""
case "--preview-window":
parsePreviewWindow(&opts.Preview,
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:rounded|sharp|noborder][:wrap][:cycle][:hidden][:+SCROLL[-OFFSET]][:default]"))
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:rounded|sharp|noborder][:wrap][:cycle][:hidden][:+SCROLL[OFFSETS][/DENOM]][:~HEADER_LINES][:default]"))
case "--height":
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
case "--min-height":

View File

@@ -102,7 +102,7 @@ func TestIrrelevantNth(t *testing.T) {
t.Errorf("nth should be empty: %v", opts.Nth)
}
}
for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} {
for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
{
opts := defaultOptions()
parseOptions(opts, words)
@@ -389,18 +389,18 @@ func TestPreviewOpts(t *testing.T) {
opts.Preview.hidden == true &&
opts.Preview.wrap == true &&
opts.Preview.position == posLeft &&
opts.Preview.scroll == "{1}-/2" &&
opts.Preview.scroll == "+{1}-/2" &&
opts.Preview.size.percent == false &&
opts.Preview.size.size == 15) {
t.Error(opts.Preview)
}
opts = optsFor("--preview-window=up:15:wrap:hidden:+{1}-/2", "--preview-window=down", "--preview-window=cycle")
opts = optsFor("--preview-window=up:15:wrap:hidden:+{1}+3-1-2/2", "--preview-window=down", "--preview-window=cycle")
if !(opts.Preview.command == "" &&
opts.Preview.hidden == true &&
opts.Preview.wrap == true &&
opts.Preview.cycle == true &&
opts.Preview.position == posDown &&
opts.Preview.scroll == "{1}-/2" &&
opts.Preview.scroll == "+{1}+3-1-2/2" &&
opts.Preview.size.percent == false &&
opts.Preview.size.size == 15) {
t.Error(opts.Preview.size.size)

View File

@@ -337,7 +337,7 @@ func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result,
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
var input []Token
if len(p.nth) == 0 {
input = []Token{Token{text: &item.text, prefixLength: 0}}
input = []Token{{text: &item.text, prefixLength: 0}}
} else {
input = p.transformInput(item)
}
@@ -350,7 +350,7 @@ func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset,
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) {
var input []Token
if len(p.nth) == 0 {
input = []Token{Token{text: &item.text, prefixLength: 0}}
input = []Token{{text: &item.text, prefixLength: 0}}
} else {
input = p.transformInput(item)
}

View File

@@ -131,7 +131,7 @@ func TestCaseSensitivity(t *testing.T) {
func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize("junegunn", Delimiter{})
trans := Transform(tokens, []Range{Range{1, 1}})
trans := Transform(tokens, []Range{{1, 1}})
origBytes := []byte("junegunn.choi")
for _, extended := range []bool{false, true} {

View File

@@ -18,8 +18,8 @@ func withIndex(i *Item, index int) *Item {
func TestOffsetSort(t *testing.T) {
offsets := []Offset{
Offset{3, 5}, Offset{2, 7},
Offset{1, 3}, Offset{2, 9}}
{3, 5}, {2, 7},
{1, 3}, {2, 9}}
sort.Sort(ByOrder(offsets))
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
@@ -84,13 +84,13 @@ func TestResultRank(t *testing.T) {
// Sort by relevance
item3 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 3)
withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 3)
item4 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 4)
withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 4)
item5 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 5)
withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 5)
item6 := buildResult(
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 6)
withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 6)
items = []Result{item1, item2, item3, item4, item5, item6}
sort.Sort(ByRelevance(items))
if !(items[0] == item6 && items[1] == item5 &&

View File

@@ -22,8 +22,9 @@ import (
// import "github.com/pkg/profile"
var placeholder *regexp.Regexp
var numericPrefix *regexp.Regexp
var whiteSuffix *regexp.Regexp
var offsetComponentRegex *regexp.Regexp
var offsetTrimCharsRegex *regexp.Regexp
var activeTempFiles []string
const ellipsis string = ".."
@@ -31,8 +32,9 @@ const clearCode string = "\x1b[2J"
func init() {
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`)
numericPrefix = regexp.MustCompile(`^[[:punct:]]*([0-9]+)`)
whiteSuffix = regexp.MustCompile(`\s*$`)
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
activeTempFiles = []string{}
}
@@ -133,6 +135,7 @@ type Terminal struct {
count int
progress int
reading bool
running bool
failed *string
jumping jumpMode
jumpLabels string
@@ -157,6 +160,7 @@ type Terminal struct {
slab *util.Slab
theme *tui.ColorTheme
tui tui.Renderer
executing *util.AtomicBool
}
type selectedItem struct {
@@ -276,6 +280,8 @@ const (
actReload
actDisableSearch
actEnableSearch
actSelect
actDeselect
)
type placeholderFlags struct {
@@ -502,6 +508,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
ansi: opts.Ansi,
tabstop: opts.Tabstop,
reading: true,
running: true,
failed: nil,
jumping: jumpDisabled,
jumpLabels: opts.JumpLabels,
@@ -523,7 +530,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
startChan: make(chan bool, 1),
killChan: make(chan int),
tui: renderer,
initFunc: func() { renderer.Init() }}
initFunc: func() { renderer.Init() },
executing: util.NewAtomicBool(false)}
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
@@ -1140,8 +1148,8 @@ func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) {
width = util.Max(0, width)
var trimmed int32
// Assume that each rune takes at least one column on screen
if len(runes) > width {
diff := len(runes) - width
if len(runes) > width+2 {
diff := len(runes) - width - 2
trimmed = int32(diff)
runes = runes[diff:]
}
@@ -1289,18 +1297,37 @@ func (t *Terminal) renderPreviewSpinner() {
}
}
func (t *Terminal) renderPreviewText(unchanged bool) {
maxWidth := t.pwindow.Width()
lineNo := -t.previewer.offset
height := t.pwindow.Height()
func (t *Terminal) renderPreviewArea(unchanged bool) {
if unchanged {
t.pwindow.MoveAndClear(0, 0)
t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display
} else {
t.previewed.filled = false
t.pwindow.Erase()
}
height := t.pwindow.Height()
header := []string{}
body := t.previewer.lines
headerLines := t.previewOpts.headerLines
// Do not enable preview header lines if it's value is too large
if headerLines > 0 && headerLines < util.Min(len(body), height) {
header = t.previewer.lines[0:headerLines]
body = t.previewer.lines[headerLines:]
// Always redraw header
t.renderPreviewText(height, header, 0, false)
t.pwindow.MoveAndClear(t.pwindow.Y(), 0)
}
t.renderPreviewText(height, body, -t.previewer.offset+headerLines, unchanged)
if !unchanged {
t.pwindow.FinishFill()
}
}
func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) {
maxWidth := t.pwindow.Width()
var ansi *ansiState
for _, line := range t.previewer.lines {
for _, line := range lines {
var lbg tui.Color = -1
if ansi != nil {
ansi.lbg = -1
@@ -1314,8 +1341,9 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
prefixWidth := 0
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
trimmed := []rune(str)
trimmedLen := 0
if !t.previewOpts.wrap {
trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X())
trimmed, trimmedLen = t.trimRight(trimmed, maxWidth-t.pwindow.X())
}
str, width := t.processTabs(trimmed, prefixWidth)
prefixWidth += width
@@ -1325,7 +1353,8 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
} else {
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
}
return fillRet == tui.FillContinue
return trimmedLen == 0 &&
(fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine)
})
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
if fillRet == tui.FillNextLine {
@@ -1346,9 +1375,6 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
}
lineNo++
}
if !unchanged {
t.pwindow.FinishFill()
}
}
func (t *Terminal) printPreview() {
@@ -1361,7 +1387,7 @@ func (t *Terminal) printPreview() {
t.previewer.version == t.previewed.version &&
t.previewer.offset == t.previewed.offset
t.previewer.scrollable = t.previewer.offset > 0 || numLines > height
t.renderPreviewText(unchanged)
t.renderPreviewArea(unchanged)
t.renderPreviewSpinner()
t.previewed.numLines = numLines
t.previewed.version = t.previewer.version
@@ -1374,7 +1400,7 @@ func (t *Terminal) printPreviewDelayed() {
}
t.previewer.scrollable = false
t.renderPreviewText(true)
t.renderPreviewArea(true)
message := t.trimMessage("Loading ..", t.pwindow.Width())
pos := t.pwindow.Width() - len(message)
@@ -1567,43 +1593,33 @@ func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input str
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
}
// Ascii to positive integer
func atopi(s string) int {
matches := numericPrefix.FindStringSubmatch(s)
if len(matches) < 2 {
return 0
}
n, e := strconv.Atoi(matches[1])
if e != nil || n < 1 {
return 0
}
return n
}
func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
offsetExpr := t.replacePlaceholder(t.previewOpts.scroll, false, "", list)
nums := strings.Split(offsetExpr, "-")
switch len(nums) {
case 0:
return 0
case 1, 2:
base := atopi(nums[0])
if base == 0 {
offsetExpr := offsetTrimCharsRegex.ReplaceAllString(
t.replacePlaceholder(t.previewOpts.scroll, false, "", list), "")
atoi := func(s string) int {
n, e := strconv.Atoi(s)
if e != nil {
return 0
} else if len(nums) == 1 {
return base - 1
}
if nums[1][0] == '/' {
denom := atopi(nums[1][1:])
return n
}
base := -1
for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) {
if strings.HasPrefix(component, "-/") {
component = component[1:]
}
if component[0] == '/' {
denom := atoi(component[1:])
if denom == 0 {
return base
}
return base - height/denom
}
return base - atopi(nums[1]) - 1
default:
return 0
base += atoi(component)
}
return base
}
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
@@ -1711,13 +1727,17 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
t.tui.Pause(true)
t.executing.Set(true)
cmd.Run()
t.executing.Set(false)
t.tui.Resume(true, false)
t.redraw()
t.refresh()
} else {
t.tui.Pause(false)
t.executing.Set(true)
cmd.Run()
t.executing.Set(false)
t.tui.Resume(false, false)
}
cleanTemporaryFiles()
@@ -1785,11 +1805,26 @@ func (t *Terminal) selectItem(item *Item) bool {
return true
}
func (t *Terminal) selectItemChanged(item *Item) bool {
if _, found := t.selected[item.Index()]; found {
return false
}
return t.selectItem(item)
}
func (t *Terminal) deselectItem(item *Item) {
delete(t.selected, item.Index())
t.version++
}
func (t *Terminal) deselectItemChanged(item *Item) bool {
if _, found := t.selected[item.Index()]; found {
t.deselectItem(item)
return true
}
return false
}
func (t *Terminal) toggleItem(item *Item) bool {
if _, found := t.selected[item.Index()]; !found {
return t.selectItem(item)
@@ -1803,7 +1838,7 @@ func (t *Terminal) killPreview(code int) {
case t.killChan <- code:
default:
if code != exitCancel {
os.Exit(code)
t.eventBox.Set(EvtQuit, code)
}
}
}
@@ -1820,8 +1855,12 @@ func (t *Terminal) Loop() {
intChan := make(chan os.Signal, 1)
signal.Notify(intChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-intChan
t.reqBox.Set(reqQuit, nil)
for s := range intChan {
// Don't quit by SIGINT while executing because it should be for the executing command and not for fzf itself
if !(s == os.Interrupt && t.executing.Get()) {
t.reqBox.Set(reqQuit, nil)
}
}
}()
contChan := make(chan os.Signal, 1)
@@ -1898,7 +1937,7 @@ func (t *Terminal) Loop() {
cmd := util.ExecCommand(command, true)
if pwindow != nil {
height := pwindow.Height()
initialOffset = util.Max(0, t.evaluateScrollOffset(items, height))
initialOffset = util.Max(0, t.evaluateScrollOffset(items, util.Max(0, height-t.previewOpts.headerLines)))
env := os.Environ()
lines := fmt.Sprintf("LINES=%d", height)
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
@@ -1983,7 +2022,7 @@ func (t *Terminal) Loop() {
case code := <-t.killChan:
if code != exitCancel {
util.KillCommand(cmd)
os.Exit(code)
t.eventBox.Set(EvtQuit, code)
} else {
timer := time.NewTimer(previewCancelWait)
select {
@@ -2020,16 +2059,6 @@ func (t *Terminal) Loop() {
}()
}
exit := func(getCode func() int) {
t.tui.Close()
code := getCode()
if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input))
}
// prof.Stop()
t.killPreview(code)
}
refreshPreview := func(command string) {
if len(command) > 0 && t.isPreviewEnabled() {
_, list := t.buildPlusList(command, false)
@@ -2041,7 +2070,19 @@ func (t *Terminal) Loop() {
go func() {
var focusedIndex int32 = minItem.Index()
var version int64 = -1
for {
running := true
code := exitError
exit := func(getCode func() int) {
t.tui.Close()
code = getCode()
if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input))
}
running = false
t.mutex.Unlock()
}
for running {
t.reqBox.Wait(func(events *util.Events) {
defer events.Clear()
t.mutex.Lock()
@@ -2087,6 +2128,7 @@ func (t *Terminal) Loop() {
}
return exitNoMatch
})
return
case reqPreviewDisplay:
result := value.(previewResult)
if t.previewer.version != result.version {
@@ -2098,7 +2140,7 @@ func (t *Terminal) Loop() {
if t.previewer.following {
t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height()
} else if result.offset >= 0 {
t.previewer.offset = util.Constrain(result.offset, 0, len(t.previewer.lines)-1)
t.previewer.offset = util.Constrain(result.offset, t.previewOpts.headerLines, len(t.previewer.lines)-1)
}
t.printPreview()
case reqPreviewRefresh:
@@ -2111,14 +2153,18 @@ func (t *Terminal) Loop() {
t.printer(string(t.input))
return exitOk
})
return
case reqQuit:
exit(func() int { return exitInterrupt })
return
}
}
t.refresh()
t.mutex.Unlock()
})
}
// prof.Stop()
t.killPreview(code)
}()
looping := true
@@ -2167,7 +2213,7 @@ func (t *Terminal) Loop() {
if t.previewOpts.cycle {
newOffset = (newOffset + numLines) % numLines
}
newOffset = util.Constrain(newOffset, 0, numLines-1)
newOffset = util.Constrain(newOffset, t.previewOpts.headerLines, numLines-1)
if t.previewer.offset != newOffset {
t.previewer.offset = newOffset
req(reqPreviewRefresh)
@@ -2341,6 +2387,16 @@ func (t *Terminal) Loop() {
} else {
req(reqQuit)
}
case actSelect:
current := t.currentItem()
if t.multi > 0 && current != nil && t.selectItemChanged(current) {
req(reqList, reqInfo)
}
case actDeselect:
current := t.currentItem()
if t.multi > 0 && current != nil && t.deselectItemChanged(current) {
req(reqList, reqInfo)
}
case actToggle:
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
req(reqList)

View File

@@ -906,12 +906,6 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
for i, line := range allLines {
lines := wrapLine(line, w.posx, w.width, w.tabstop)
for j, wl := range lines {
if w.posx >= w.Width()-1 && wl.displayWidth == 0 {
if w.posy < w.height-1 {
w.Move(w.posy+1, 0)
}
return FillNextLine
}
w.stderrInternal(wl.text, false)
w.posx += wl.displayWidth
@@ -926,6 +920,14 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
}
}
}
if w.posx+1 >= w.Width() {
if w.posy+1 >= w.height {
return FillSuspend
}
w.Move(w.posy+1, 0)
onMove()
return FillNextLine
}
return FillContinue
}

View File

@@ -1890,6 +1890,158 @@ class TestGoFZF < TestBase
tmux.send_keys 'C-l', 'closed'
tmux.until { |lines| assert_includes lines[0], 'closed' }
end
def test_select_deselect
tmux.send_keys "seq 3 | #{FZF} --multi --bind up:deselect+up,down:select+down", :Enter
tmux.until { |lines| assert_equal 3, lines.match_count }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 1, lines.select_count }
tmux.send_keys :Up
tmux.until { |lines| assert_equal 0, lines.select_count }
tmux.send_keys :Down, :Down
tmux.until { |lines| assert_equal 2, lines.select_count }
tmux.send_keys :Tab
tmux.until { |lines| assert_equal 1, lines.select_count }
tmux.send_keys :Down, :Down
tmux.until { |lines| assert_equal 2, lines.select_count }
tmux.send_keys :Up
tmux.until { |lines| assert_equal 1, lines.select_count }
tmux.send_keys :Down
tmux.until { |lines| assert_equal 1, lines.select_count }
tmux.send_keys :Down
tmux.until { |lines| assert_equal 2, lines.select_count }
end
def test_interrupt_execute
tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-l:execute:echo executing {}; sleep 100'", :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys 'C-l'
tmux.until { |lines| assert lines.any_include?('executing 1') }
tmux.send_keys 'C-c'
tmux.until { |lines| assert_equal 100, lines.item_count }
tmux.send_keys 99
tmux.until { |lines| assert_equal 1, lines.match_count }
end
def test_kill_default_command_on_abort
script = tempname + '.sh'
writelines(script,
['#!/usr/bin/env bash',
"echo 'Started'",
'while :; do sleep 1; done'])
system("chmod +x #{script}")
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{script}"), :Enter
tmux.until { |lines| assert_equal 1, lines.item_count }
tmux.send_keys 'C-c'
tmux.send_keys 'C-l', 'closed'
tmux.until { |lines| assert_includes lines[0], 'closed' }
wait { refute system("pgrep -f #{script}") }
ensure
system("pkill -9 -f #{script}")
begin
File.unlink(script)
rescue StandardError
nil
end
end
def test_kill_default_command_on_accept
script = tempname + '.sh'
writelines(script,
['#!/usr/bin/env bash',
"echo 'Started'",
'while :; do sleep 1; done'])
system("chmod +x #{script}")
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{script}"), :Enter
tmux.until { |lines| assert_equal 1, lines.item_count }
tmux.send_keys :Enter
assert_equal 'Started', readonce.chomp
wait { refute system("pgrep -f #{script}") }
ensure
system("pkill -9 -f #{script}")
begin
File.unlink(script)
rescue StandardError
nil
end
end
def test_kill_reload_command_on_abort
script = tempname + '.sh'
writelines(script,
['#!/usr/bin/env bash',
"echo 'Started'",
'while :; do sleep 1; done'])
system("chmod +x #{script}")
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{script})'")}", :Enter
tmux.until { |lines| assert_equal 3, lines.item_count }
tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal 1, lines.item_count }
tmux.send_keys 'C-c'
tmux.send_keys 'C-l', 'closed'
tmux.until { |lines| assert_includes lines[0], 'closed' }
wait { refute system("pgrep -f #{script}") }
ensure
system("pkill -9 -f #{script}")
begin
File.unlink(script)
rescue StandardError
nil
end
end
def test_kill_reload_command_on_accept
script = tempname + '.sh'
writelines(script,
['#!/usr/bin/env bash',
"echo 'Started'",
'while :; do sleep 1; done'])
system("chmod +x #{script}")
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{script})'")}", :Enter
tmux.until { |lines| assert_equal 3, lines.item_count }
tmux.send_keys 'C-r'
tmux.until { |lines| assert_equal 1, lines.item_count }
tmux.send_keys :Enter
assert_equal 'Started', readonce.chomp
wait { refute system("pgrep -f #{script}") }
ensure
system("pkill -9 -f #{script}")
begin
File.unlink(script)
rescue StandardError
nil
end
end
def test_preview_header
tmux.send_keys "seq 100 | #{FZF} --bind ctrl-k:preview-up+preview-up,ctrl-j:preview-down+preview-down+preview-down --preview 'seq 1000' --preview-window 'top:+{1}:~3'", :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
top5 = ->(lines) { lines.drop(1).take(5).map { |s| s[/[0-9]+/] } }
tmux.until do |lines|
assert_includes lines[1], '4/1000'
assert_equal(%w[1 2 3 4 5], top5[lines])
end
tmux.send_keys '55'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_equal(%w[1 2 3 55 56], top5[lines])
end
tmux.send_keys 'C-J'
tmux.until do |lines|
assert_equal(%w[1 2 3 58 59], top5[lines])
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 19, lines.match_count
assert_equal(%w[1 2 3 5 6], top5[lines])
end
tmux.send_keys 'C-K'
tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) }
end
end
module TestShell
@@ -2060,7 +2212,7 @@ module CompletionTest
end
# ~USERNAME**<TAB>
user = ENV['USER']
user = `whoami`.chomp
tmux.send_keys 'C-u'
tmux.send_keys "cat ~#{user}**", :Tab
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }