From 6bc3fe6e67e1a9f6d80d47c03dc6f4f3f4453380 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 19 Sep 2015 18:33:25 +0900 Subject: [PATCH 001/658] Build partially-static binary for linux (#350) Instead of building a separate statically-linked binary, build partially-static binary that only contains ncurses to avoid compatibility issues in libc. --- src/Makefile | 33 +++++++-------------------------- src/curses/curses.go | 4 +++- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/Makefile b/src/Makefile index 3304f57..dee7e80 100644 --- a/src/Makefile +++ b/src/Makefile @@ -5,19 +5,8 @@ endif UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) GOOS := darwin - LDFLAGS := -ifdef STATIC -$(error Static linking not possible on OS X) -endif else ifeq ($(UNAME_S),Linux) GOOS := linux -ifdef STATIC - SUFFIX := -static - LDFLAGS := --ldflags '-extldflags "-static -ltinfo -lgpm"' -else - SUFFIX := - LDFLAGS := -endif endif ifneq ($(shell uname -m),x86_64) @@ -27,11 +16,11 @@ endif SOURCES := $(wildcard *.go */*.go) BINDIR := ../bin -BINARY32 := fzf-$(GOOS)_386$(SUFFIX) -BINARY64 := fzf-$(GOOS)_amd64$(SUFFIX) +BINARY32 := fzf-$(GOOS)_386 +BINARY64 := fzf-$(GOOS)_amd64 VERSION = $(shell fzf/$(BINARY64) --version) -RELEASE32 = fzf-$(VERSION)-$(GOOS)_386$(SUFFIX) -RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64$(SUFFIX) +RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 +RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 all: release @@ -40,11 +29,7 @@ release: build cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ rm -f $(RELEASE32) $(RELEASE64) -ifndef STATIC build: test fzf/$(BINARY32) fzf/$(BINARY64) -else -build: test fzf/$(BINARY64) -endif test: go get @@ -85,13 +70,9 @@ ubuntu: docker-ubuntu docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' -linux: docker-arch - docker run -i -t -v $(GOPATH):/go junegunn/arch-sandbox \ +linux: docker-ubuntu + docker run -i -t -v $(GOPATH):/go junegunn/ubuntu-sandbox \ /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make' -linux-static: docker-ubuntu - docker run -i -t -v $(GOPATH):/go junegunn/ubuntu-sandbox \ - /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make STATIC=1' - .PHONY: all build release test install uninstall clean docker \ - linux linux-static arch ubuntu docker-arch docker-ubuntu + linux arch ubuntu docker-arch docker-ubuntu diff --git a/src/curses/curses.go b/src/curses/curses.go index 59cea3b..2c4cb0d 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -3,7 +3,9 @@ package curses /* #include #include -#cgo LDFLAGS: -lncurses +#cgo darwin LDFLAGS: -lncurses +#cgo linux,386 LDFLAGS: -lncurses +#cgo linux,amd64 LDFLAGS: -l:libncurses.a -l:libtinfo.a -l:libgpm.a */ import "C" From 72cc558fdc4547f4ec919af5952efea9de28bb9e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 19 Sep 2015 18:39:09 +0900 Subject: [PATCH 002/658] Fix travis CI build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cbd5c11..597c1f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ rvm: install: - sudo apt-get update -- sudo apt-get install -y libncurses-dev lib32ncurses5-dev +- sudo apt-get install -y libncurses-dev lib32ncurses5-dev libgpm-dev - sudo add-apt-repository -y ppa:pi-rho/dev - sudo apt-add-repository -y ppa:fish-shell/release-2 - sudo apt-get update From d2f3604c1dd176b485ab82cebc956560f2e73657 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 19 Sep 2015 22:18:04 +0900 Subject: [PATCH 003/658] 0.10.6 --- CHANGELOG.md | 5 ++++- install | 11 +++-------- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f10cbca..0018b9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,10 @@ CHANGELOG - Changed exit status - 0: Okay - 1: No match - - 2: Error/Interrupted + - 2: Error + - 130: Interrupted +- 64-bit linux binary is statically-linked with ncurses to avoid + compatibility issues. 0.10.5 ------ diff --git a/install b/install index 9b9b04d..51b56ec 100755 --- a/install +++ b/install @@ -1,7 +1,7 @@ #!/usr/bin/env bash -[[ "$@" =~ --pre ]] && version=0.10.5 pre=1 || - version=0.10.5 pre=0 +[[ "$@" =~ --pre ]] && version=0.10.6 pre=1 || + version=0.10.6 pre=0 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) @@ -80,12 +80,7 @@ download() { return fi - chmod +x $1 && symlink $1 || return 1 - if [[ $1 =~ linux_amd64$ ]]; then - check_binary || download $1-static - else - check_binary - fi + chmod +x $1 && symlink $1 && check_binary } # Try to download binary executable diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 4147371..99d40ff 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Sep 2015" "fzf 0.10.5" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Sep 2015" "fzf 0.10.6" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 1cc4f81..7deb078 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.10.5" + version = "0.10.6" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 987799f8fbd2367fe99383a1ddf9ddf5d4ed253f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 19 Sep 2015 22:27:09 +0900 Subject: [PATCH 004/658] Revert "0.10.6" This reverts commit d2f3604c1dd176b485ab82cebc956560f2e73657. --- CHANGELOG.md | 5 +---- install | 11 ++++++++--- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0018b9f..f10cbca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,7 @@ CHANGELOG - Changed exit status - 0: Okay - 1: No match - - 2: Error - - 130: Interrupted -- 64-bit linux binary is statically-linked with ncurses to avoid - compatibility issues. + - 2: Error/Interrupted 0.10.5 ------ diff --git a/install b/install index 51b56ec..9b9b04d 100755 --- a/install +++ b/install @@ -1,7 +1,7 @@ #!/usr/bin/env bash -[[ "$@" =~ --pre ]] && version=0.10.6 pre=1 || - version=0.10.6 pre=0 +[[ "$@" =~ --pre ]] && version=0.10.5 pre=1 || + version=0.10.5 pre=0 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) @@ -80,7 +80,12 @@ download() { return fi - chmod +x $1 && symlink $1 && check_binary + chmod +x $1 && symlink $1 || return 1 + if [[ $1 =~ linux_amd64$ ]]; then + check_binary || download $1-static + else + check_binary + fi } # Try to download binary executable diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 99d40ff..4147371 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Sep 2015" "fzf 0.10.6" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Sep 2015" "fzf 0.10.5" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 7deb078..1cc4f81 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.10.6" + version = "0.10.5" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From d980e00961e64c12c28fe6adb209a6c0a652efd1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 19 Sep 2015 22:51:12 +0900 Subject: [PATCH 005/658] Revert "Revert "0.10.6"" This reverts commit 987799f8fbd2367fe99383a1ddf9ddf5d4ed253f. --- CHANGELOG.md | 5 ++++- install | 11 +++-------- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f10cbca..0018b9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,10 @@ CHANGELOG - Changed exit status - 0: Okay - 1: No match - - 2: Error/Interrupted + - 2: Error + - 130: Interrupted +- 64-bit linux binary is statically-linked with ncurses to avoid + compatibility issues. 0.10.5 ------ diff --git a/install b/install index 9b9b04d..51b56ec 100755 --- a/install +++ b/install @@ -1,7 +1,7 @@ #!/usr/bin/env bash -[[ "$@" =~ --pre ]] && version=0.10.5 pre=1 || - version=0.10.5 pre=0 +[[ "$@" =~ --pre ]] && version=0.10.6 pre=1 || + version=0.10.6 pre=0 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) @@ -80,12 +80,7 @@ download() { return fi - chmod +x $1 && symlink $1 || return 1 - if [[ $1 =~ linux_amd64$ ]]; then - check_binary || download $1-static - else - check_binary - fi + chmod +x $1 && symlink $1 && check_binary } # Try to download binary executable diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 4147371..99d40ff 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Sep 2015" "fzf 0.10.5" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Sep 2015" "fzf 0.10.6" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 1cc4f81..7deb078 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.10.5" + version = "0.10.6" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From ebea470875a1f31b8d3b4b937be28d30462a397b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 20 Sep 2015 00:17:44 +0900 Subject: [PATCH 006/658] Build linux binary on Centos --- src/Dockerfile.centos | 9 +++++++-- src/Makefile | 13 ++++++++++--- src/curses/curses.go | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos index c03f43a..aed4120 100644 --- a/src/Dockerfile.centos +++ b/src/Dockerfile.centos @@ -1,8 +1,10 @@ -FROM centos:centos7 +FROM centos:centos6 MAINTAINER Junegunn Choi # yum -RUN yum install -y git gcc make tar ncurses-devel +RUN yum install -y git gcc make tar glibc-devel glibc-devel.i686 \ + ncurses-devel ncurses-static ncurses-devel.i686 \ + gpm-devel gpm-static libgcc.i686 # Install Go 1.4 RUN cd / && curl \ @@ -13,6 +15,9 @@ ENV GOPATH /go ENV GOROOT /go1.4 ENV PATH /go1.4/bin:$PATH +# For i386 build +RUN cd $GOROOT/src && GOARCH=386 ./make.bash + # Volume VOLUME /go diff --git a/src/Makefile b/src/Makefile index dee7e80..8c80ee2 100644 --- a/src/Makefile +++ b/src/Makefile @@ -62,6 +62,9 @@ docker-arch: docker-ubuntu: docker build -t junegunn/ubuntu-sandbox - < Dockerfile.ubuntu +docker-centos: + docker build -t junegunn/centos-sandbox - < Dockerfile.centos + arch: docker-arch docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' @@ -70,9 +73,13 @@ ubuntu: docker-ubuntu docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' -linux: docker-ubuntu - docker run -i -t -v $(GOPATH):/go junegunn/ubuntu-sandbox \ +centos: docker-centos + docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ + sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' + +linux: docker-centos + docker run -i -t -v $(GOPATH):/go junegunn/centos-sandbox \ /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make' .PHONY: all build release test install uninstall clean docker \ - linux arch ubuntu docker-arch docker-ubuntu + linux arch ubuntu centos docker-arch docker-ubuntu docker-centos diff --git a/src/curses/curses.go b/src/curses/curses.go index 2c4cb0d..5339ec9 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -5,7 +5,7 @@ package curses #include #cgo darwin LDFLAGS: -lncurses #cgo linux,386 LDFLAGS: -lncurses -#cgo linux,amd64 LDFLAGS: -l:libncurses.a -l:libtinfo.a -l:libgpm.a +#cgo linux,amd64 LDFLAGS: -l:libncurses.a -l:libtinfo.a -l:libgpm.a -ldl */ import "C" From 5a60aa5050b1128d1498d280b1a0c15f98a78aa6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 20 Sep 2015 14:09:54 +0900 Subject: [PATCH 007/658] [vim] Display proper error message when command failed --- plugin/fzf.vim | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index d9ced8e..bfdfed3 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -40,9 +40,7 @@ function! s:fzf_exec() \ input('fzf executable not found. Download binary? (y/n) ') =~? '^y' redraw echo - echohl WarningMsg - echo 'Downloading fzf binary. Please wait ...' - echohl None + call s:warn('Downloading fzf binary. Please wait ...') let s:installed = 1 call system(s:install.' --bin') return s:fzf_exec() @@ -98,14 +96,24 @@ function! s:upgrade(dict) return copy endfunction +function! s:error(msg) + echohl ErrorMsg + echom a:msg + echohl None +endfunction + +function! s:warn(msg) + echohl WarningMsg + echom a:msg + echohl None +endfunction + function! fzf#run(...) abort try let oshell = &shell set shell=sh if has('nvim') && bufexists('term://*:FZF') - echohl WarningMsg - echomsg 'FZF is already running!' - echohl None + call s:warn('FZF is already running!') return [] endif let dict = exists('a:1') ? s:upgrade(a:1) : {} @@ -230,9 +238,7 @@ function! s:execute(dict, command, temps) if v:shell_error " Do not print error message on exit status 1 (no match) or 130 (interrupt) if v:shell_error == 2 - echohl ErrorMsg - echo 'Error running ' . command - echohl None + call s:error('Error running ' . command) endif return [] else @@ -249,6 +255,10 @@ function! s:execute_tmux(dict, command, temps) call system(command) redraw! + if v:shell_error == 2 + call s:error('Error running ' . command) + return [] + endif return s:callback(a:dict, a:temps) endfunction @@ -310,6 +320,7 @@ function! s:execute_term(dict, command, temps) call s:pushd(a:dict) let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF' } + let s:command = a:command function! fzf.on_exit(id, code) let pos = s:getpos() let inplace = pos == s:ppos " {'window': 'enew'} @@ -323,6 +334,13 @@ function! s:execute_term(dict, command, temps) wincmd p endif endif + + if a:code == 2 + call s:error('Error running ' . s:command) + sleep + return + endif + call s:pushd(self.dict) try redraw! From ad7e433a7d5e68d749fa6da4e284de1ffa3ea99c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 22 Sep 2015 13:16:10 +0900 Subject: [PATCH 008/658] Use build tags to enable static linking --- src/Makefile | 6 +++--- src/curses/curses.go | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Makefile b/src/Makefile index 8c80ee2..ff40897 100644 --- a/src/Makefile +++ b/src/Makefile @@ -44,10 +44,10 @@ clean: cd fzf && rm -f fzf-* fzf/$(BINARY32): $(SOURCES) - cd fzf && GOARCH=386 CGO_ENABLED=1 go build -o $(BINARY32) + cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -o $(BINARY32) fzf/$(BINARY64): $(SOURCES) - cd fzf && go build $(LDFLAGS) -o $(BINARY64) + cd fzf && go build -a -tags "$(TAGS)" -o $(BINARY64) $(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) cp -f fzf/$(BINARY64) $(BINDIR) @@ -79,7 +79,7 @@ centos: docker-centos linux: docker-centos docker run -i -t -v $(GOPATH):/go junegunn/centos-sandbox \ - /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make' + /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make TAGS=static' .PHONY: all build release test install uninstall clean docker \ linux arch ubuntu centos docker-arch docker-ubuntu docker-centos diff --git a/src/curses/curses.go b/src/curses/curses.go index 5339ec9..debdeaf 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -3,9 +3,8 @@ package curses /* #include #include -#cgo darwin LDFLAGS: -lncurses -#cgo linux,386 LDFLAGS: -lncurses -#cgo linux,amd64 LDFLAGS: -l:libncurses.a -l:libtinfo.a -l:libgpm.a -ldl +#cgo !static LDFLAGS: -lncurses +#cgo static LDFLAGS: -l:libncurses.a -l:libtinfo.a -l:libgpm.a -ldl */ import "C" From aec48f159b66c0ef3be643741a2cbe99b512aa26 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 22 Sep 2015 16:36:48 +0900 Subject: [PATCH 009/658] [neovim] Remove redraw! hack that is no longer needed --- plugin/fzf.vim | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index bfdfed3..117e03f 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -343,7 +343,6 @@ function! s:execute_term(dict, command, temps) call s:pushd(self.dict) try - redraw! call s:callback(self.dict, self.temps) if inplace && bufnr('') == self.buf From 98d2bfa0db3cd605f05b518338d95d4227dabb75 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 24 Sep 2015 10:51:05 +0900 Subject: [PATCH 010/658] [install] Terminate install script when failed to update shell config Close #354 --- install | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install b/install index 51b56ec..f589076 100755 --- a/install +++ b/install @@ -245,6 +245,7 @@ EOF fi append_line() { + set -e echo "Update $2:" echo " - $1" [ -f "$2" ] || touch "$2" @@ -261,6 +262,7 @@ append_line() { echo " + Added" fi echo + set +e } echo From 86306dd45a71df4c5651b7895da4b31d49183ed6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 26 Sep 2015 21:04:44 +0900 Subject: [PATCH 011/658] [vim] Display proper error message when GVim launcher failed Related: https://github.com/junegunn/fzf.vim/issues/16 --- plugin/fzf.vim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 117e03f..0cb444f 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -213,7 +213,7 @@ endfunction function! s:xterm_launcher() let fmt = 'xterm -T "[fzf]" -bg "\%s" -fg "\%s" -geometry %dx%d+%d+%d -e bash -ic %%s' if has('gui_macvim') - let fmt .= '; osascript -e "tell application \"MacVim\" to activate"' + let fmt .= '&& osascript -e "tell application \"MacVim\" to activate"' endif return printf(fmt, \ synIDattr(hlID("Normal"), "bg"), synIDattr(hlID("Normal"), "fg"), @@ -237,7 +237,7 @@ function! s:execute(dict, command, temps) redraw! if v:shell_error " Do not print error message on exit status 1 (no match) or 130 (interrupt) - if v:shell_error == 2 + if index([1, 130], v:shell_error) < 0 call s:error('Error running ' . command) endif return [] From 59220c63a659205de1d2e39066701bb12baf91c5 Mon Sep 17 00:00:00 2001 From: James Baumgarten Date: Sat, 26 Sep 2015 16:56:52 -0700 Subject: [PATCH 012/658] [vim] handle exit status 1 properly (#359) --- plugin/fzf.vim | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 0cb444f..4aefb4c 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -235,14 +235,13 @@ function! s:execute(dict, command, temps) endif execute 'silent !'.command redraw! - if v:shell_error - " Do not print error message on exit status 1 (no match) or 130 (interrupt) - if index([1, 130], v:shell_error) < 0 - call s:error('Error running ' . command) - endif + if v:shell_error == 0 || v:shell_error == 1 + return s:callback(a:dict, a:temps) + " Do not print error message on exit status 130 (interrupt) + elseif v:shell_error == 130 return [] else - return s:callback(a:dict, a:temps) + call s:error('Error running ' . command) endif endfunction From 862da2c0b1ea2d607f9e626ee3ee8508a583bbc1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 27 Sep 2015 16:26:40 +0900 Subject: [PATCH 013/658] [vim] Consistent exit status handling --- plugin/fzf.vim | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 4aefb4c..d37e53b 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -222,6 +222,19 @@ endfunction unlet! s:launcher let s:launcher = function('s:xterm_launcher') +function! s:exit_handler(code, command, ...) + if a:code == 130 + return 0 + elseif a:code > 1 + call s:error('Error running ' . a:command) + if !empty(a:000) + sleep + endif + return 0 + endif + return 1 +endfunction + function! s:execute(dict, command, temps) call s:pushd(a:dict) silent! !clear 2> /dev/null @@ -235,14 +248,7 @@ function! s:execute(dict, command, temps) endif execute 'silent !'.command redraw! - if v:shell_error == 0 || v:shell_error == 1 - return s:callback(a:dict, a:temps) - " Do not print error message on exit status 130 (interrupt) - elseif v:shell_error == 130 - return [] - else - call s:error('Error running ' . command) - endif + return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : [] endfunction function! s:execute_tmux(dict, command, temps) @@ -254,11 +260,7 @@ function! s:execute_tmux(dict, command, temps) call system(command) redraw! - if v:shell_error == 2 - call s:error('Error running ' . command) - return [] - endif - return s:callback(a:dict, a:temps) + return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : [] endfunction function! s:calc_size(max, val, dict) @@ -334,9 +336,7 @@ function! s:execute_term(dict, command, temps) endif endif - if a:code == 2 - call s:error('Error running ' . s:command) - sleep + if !s:exit_handler(a:code, s:command, 1) return endif From c4e5ee63bb4f68a80efd7f931b65c766281fbdea Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 29 Sep 2015 23:32:48 -0400 Subject: [PATCH 014/658] [vim] handle SwapExists The SwapExists dialog prevents multiple files from being opening if the dialog occurs before all files are opened. Opening the files is more important than showing the dialog, so choose "readonly" automatically and continue opening files. --- plugin/fzf.vim | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index d37e53b..168bc61 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -405,6 +405,12 @@ function! s:cmd_callback(lines) abort endif let key = remove(a:lines, 0) let cmd = get(s:action, key, 'e') + if len(a:lines) > 1 + augroup fzf_swap + autocmd SwapExists * let v:swapchoice='o' + \| call s:warn('fzf: E325: swap file exists: '.expand('')) + augroup END + endif try let autochdir = &autochdir set noautochdir @@ -413,6 +419,7 @@ function! s:cmd_callback(lines) abort endfor finally let &autochdir = autochdir + silent! autocmd! fzf_swap endtry endfunction From ea271cd4e2b0cb3f6e8a959f54401941ac6f1d87 Mon Sep 17 00:00:00 2001 From: Austin Ziegler Date: Thu, 1 Oct 2015 15:18:10 -0400 Subject: [PATCH 015/658] Remove dependency on zsh/pcre module Fixes #363. --- install | 6 +++--- shell/key-bindings.zsh | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/install b/install index f589076..e98f920 100755 --- a/install +++ b/install @@ -185,7 +185,7 @@ for shell in bash zsh; do echo -n "Generate ~/.fzf.$shell ... " src=~/.fzf.${shell} - fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null" + fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null" if [ $auto_completion -ne 0 ]; then fzf_completion="# $fzf_completion" fi @@ -198,13 +198,13 @@ for shell in bash zsh; do cat > $src << EOF # Setup fzf # --------- -if [[ ! "\$PATH" =~ "$fzf_base/bin" ]]; then +if [[ ! "\$PATH" == *$fzf_base/bin* ]]; then export PATH="\$PATH:$fzf_base/bin" fi # Man path # -------- -if [[ ! "\$MANPATH" =~ "$fzf_base/man" && -d "$fzf_base/man" ]]; then +if [[ ! "\$MANPATH" == *$fzf_base/man* && -d "$fzf_base/man" ]]; then export MANPATH="\$MANPATH:$fzf_base/man" fi diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 735aa50..27810d3 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -1,6 +1,6 @@ # Key bindings # ------------ -if [[ $- =~ i ]]; then +if [[ $- == *i* ]]; then # CTRL-T - Paste the selected file path(s) into the command line __fsel() { From 92a75c9563600a174e9ee8334853f99ed560492a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 2 Oct 2015 18:40:20 +0900 Subject: [PATCH 016/658] Use trimmed length when --nth is used with --tiebreak=length This change improves sort ordering for aligned tabular input. Given the following input: apple juice 100 apple pie 200 fzf --nth=2 will now prefer the one with pie. Before this change fzf compared "juice " and "pie ", both of which have the same length. --- src/item.go | 19 ++++++++--------- src/pattern.go | 23 +++++++++++---------- src/tokenizer.go | 5 +++-- src/tokenizer_test.go | 12 +++++------ src/util/util.go | 26 ++++++++++++++++++++++++ src/util/util_test.go | 20 ++++++++++++++++++ test/test_go.rb | 47 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 124 insertions(+), 28 deletions(-) diff --git a/src/item.go b/src/item.go index 12ca3df..f2f105a 100644 --- a/src/item.go +++ b/src/item.go @@ -6,8 +6,8 @@ import ( "github.com/junegunn/fzf/src/curses" ) -// Offset holds two 32-bit integers denoting the offsets of a matched substring -type Offset [2]int32 +// Offset holds three 32-bit integers denoting the offsets of a matched substring +type Offset [3]int32 type colorOffset struct { offset [2]int32 @@ -43,10 +43,13 @@ func (item *Item) Rank(cache bool) Rank { } matchlen := 0 prevEnd := 0 + lenSum := 0 minBegin := math.MaxUint16 for _, offset := range item.offsets { begin := int(offset[0]) end := int(offset[1]) + trimLen := int(offset[2]) + lenSum += trimLen if prevEnd > begin { begin = prevEnd } @@ -65,10 +68,7 @@ func (item *Item) Rank(cache bool) Rank { case byLength: // 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) - } + // If offsets is empty, lenSum will be 0, but we don't care tiebreak = uint16(lenSum) } else { tiebreak = uint16(len(item.text)) @@ -116,7 +116,8 @@ func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset if len(item.colors) == 0 { var offsets []colorOffset for _, off := range item.offsets { - offsets = append(offsets, colorOffset{offset: off, color: color, bold: bold}) + + offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold}) } return offsets } @@ -160,7 +161,7 @@ func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset if curr != 0 && idx > start { if curr == -1 { offsets = append(offsets, colorOffset{ - offset: Offset{int32(start), int32(idx)}, color: color, bold: bold}) + offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold}) } else { ansi := item.colors[curr-1] fg := ansi.color.fg @@ -180,7 +181,7 @@ func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset } } offsets = append(offsets, colorOffset{ - offset: Offset{int32(start), int32(idx)}, + offset: [2]int32{int32(start), int32(idx)}, color: curses.PairFor(fg, bg), bold: ansi.color.bold || bold}) } diff --git a/src/pattern.go b/src/pattern.go index 5466b86..f5dd8a7 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/junegunn/fzf/src/algo" + "github.com/junegunn/fzf/src/util" ) // fuzzy @@ -251,9 +252,9 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item { matches := []*Item{} if p.mode == ModeFuzzy { for _, item := range *chunk { - if sidx, eidx := p.fuzzyMatch(item); sidx >= 0 { + if sidx, eidx, tlen := p.fuzzyMatch(item); sidx >= 0 { matches = append(matches, - dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}})) + dupItem(item, []Offset{Offset{int32(sidx), int32(eidx), int32(tlen)}})) } } } else { @@ -269,7 +270,7 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item { // MatchItem returns true if the Item is a match func (p *Pattern) MatchItem(item *Item) bool { if p.mode == ModeFuzzy { - sidx, _ := p.fuzzyMatch(item) + sidx, _, _ := p.fuzzyMatch(item) return sidx >= 0 } offsets := p.extendedMatch(item) @@ -288,7 +289,7 @@ func dupItem(item *Item, offsets []Offset) *Item { rank: Rank{0, 0, item.index}} } -func (p *Pattern) fuzzyMatch(item *Item) (int, int) { +func (p *Pattern) fuzzyMatch(item *Item) (int, int, int) { input := p.prepareInput(item) return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) } @@ -298,13 +299,13 @@ func (p *Pattern) extendedMatch(item *Item) []Offset { offsets := []Offset{} for _, term := range p.terms { pfun := p.procFun[term.typ] - if sidx, eidx := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 { + if sidx, eidx, tlen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 { if term.inv { break } - offsets = append(offsets, Offset{int32(sidx), int32(eidx)}) + offsets = append(offsets, Offset{int32(sidx), int32(eidx), int32(tlen)}) } else if term.inv { - offsets = append(offsets, Offset{0, 0}) + offsets = append(offsets, Offset{0, 0, 0}) } } return offsets @@ -320,19 +321,19 @@ func (p *Pattern) prepareInput(item *Item) []Token { tokens := Tokenize(item.text, p.delimiter) ret = Transform(tokens, p.nth) } else { - ret = []Token{Token{text: item.text, prefixLength: 0}} + ret = []Token{Token{text: item.text, prefixLength: 0, trimLength: util.TrimLen(item.text)}} } item.transformed = ret return ret } func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) (int, int), - tokens []Token, caseSensitive bool, forward bool, pattern []rune) (int, int) { + tokens []Token, caseSensitive bool, forward bool, pattern []rune) (int, int, int) { for _, part := range tokens { prefixLength := part.prefixLength if sidx, eidx := pfun(caseSensitive, forward, part.text, pattern); sidx >= 0 { - return sidx + prefixLength, eidx + prefixLength + return sidx + prefixLength, eidx + prefixLength, part.trimLength } } - return -1, -1 + return -1, -1, -1 // math.MaxUint16 } diff --git a/src/tokenizer.go b/src/tokenizer.go index a8d0400..4b89b38 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -20,6 +20,7 @@ type Range struct { type Token struct { text []rune prefixLength int + trimLength int } // Delimiter for tokenizing the input @@ -81,7 +82,7 @@ func withPrefixLengths(tokens [][]rune, begin int) []Token { for idx, token := range tokens { // Need to define a new local variable instead of the reused token to take // the pointer to it - ret[idx] = Token{text: token, prefixLength: prefixLength} + ret[idx] = Token{token, prefixLength, util.TrimLen(token)} prefixLength += len(token) } return ret @@ -233,7 +234,7 @@ func Transform(tokens []Token, withNth []Range) []Token { } else { prefixLength = 0 } - transTokens[idx] = Token{part, prefixLength} + transTokens[idx] = Token{part, prefixLength, util.TrimLen(part)} } return transTokens } diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go index 0f95aa1..b092440 100644 --- a/src/tokenizer_test.go +++ b/src/tokenizer_test.go @@ -44,22 +44,22 @@ func TestTokenize(t *testing.T) { // AWK-style input := " abc: def: ghi " tokens := Tokenize([]rune(input), Delimiter{}) - if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 { + if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 { t.Errorf("%s", tokens) } // With delimiter tokens = Tokenize([]rune(input), delimiterRegexp(":")) - if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 { + if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 { t.Errorf("%s", tokens) } // With delimiter regex tokens = Tokenize([]rune(input), delimiterRegexp("\\s+")) - if string(tokens[0].text) != " " || tokens[0].prefixLength != 0 || - 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 { + if string(tokens[0].text) != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 || + string(tokens[1].text) != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 || + string(tokens[2].text) != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 || + string(tokens[3].text) != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 { t.Errorf("%s", tokens) } } diff --git a/src/util/util.go b/src/util/util.go index aa5f227..e7e4f31 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -75,6 +75,7 @@ func IsTty() bool { return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 } +// TrimRight returns rune array with trailing white spaces cut off func TrimRight(runes []rune) []rune { var i int for i = len(runes) - 1; i >= 0; i-- { @@ -86,6 +87,7 @@ func TrimRight(runes []rune) []rune { return runes[0 : i+1] } +// BytesToRunes converts byte array into rune array func BytesToRunes(bytea []byte) []rune { runes := make([]rune, 0, len(bytea)) for i := 0; i < len(bytea); { @@ -100,3 +102,27 @@ func BytesToRunes(bytea []byte) []rune { } return runes } + +// TrimLen returns the length of trimmed rune array +func TrimLen(runes []rune) int { + var i int + for i = len(runes) - 1; i >= 0; i-- { + char := runes[i] + if char != ' ' && char != '\t' { + break + } + } + // Completely empty + if i < 0 { + return 0 + } + + var j int + for j = 0; j < len(runes); j++ { + char := runes[j] + if char != ' ' && char != '\t' { + break + } + } + return i - j + 1 +} diff --git a/src/util/util_test.go b/src/util/util_test.go index 06cfd4f..8aeaeac 100644 --- a/src/util/util_test.go +++ b/src/util/util_test.go @@ -20,3 +20,23 @@ func TestContrain(t *testing.T) { t.Error("Expected", 3) } } + +func TestTrimLen(t *testing.T) { + check := func(str string, exp int) { + trimmed := TrimLen([]rune(str)) + if trimmed != exp { + t.Errorf("Invalid TrimLen result for '%s': %d (expected %d)", + str, trimmed, exp) + } + } + check("hello", 5) + check("hello ", 5) + check("hello ", 5) + check(" hello", 5) + check(" hello", 5) + check(" hello ", 5) + check(" hello ", 5) + check("h o", 5) + check(" h o ", 5) + check(" ", 0) +} diff --git a/test/test_go.rb b/test/test_go.rb index e76b520..77414ec 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -527,6 +527,53 @@ class TestGoFZF < TestBase assert_equal output, `cat #{tempname} | #{FZF} -fh -n2 -d:`.split($/) end + def test_tiebreak_length_with_nth_trim_length + input = [ + "apple juice bottle 1", + "apple ui bottle 2", + "app ice bottle 3", + "app ic bottle 4", + ] + writelines tempname, input + + # len(1) + output = [ + "app ice bottle 3", + "app ic bottle 4", + "apple juice bottle 1", + "apple ui bottle 2", + ] + assert_equal output, `cat #{tempname} | #{FZF} -fa -n1`.split($/) + + # len(1 ~ 2) + output = [ + "apple ui bottle 2", + "app ic bottle 4", + "apple juice bottle 1", + "app ice bottle 3", + ] + assert_equal output, `cat #{tempname} | #{FZF} -fai -n1..2`.split($/) + + # len(1) + len(2) + output = [ + "app ic bottle 4", + "app ice bottle 3", + "apple ui bottle 2", + "apple juice bottle 1", + ] + assert_equal output, `cat #{tempname} | #{FZF} -x -f"a i" -n1,2`.split($/) + + # len(2) + output = [ + "apple ui bottle 2", + "app ic bottle 4", + "app ice bottle 3", + "apple juice bottle 1", + ] + assert_equal output, `cat #{tempname} | #{FZF} -fi -n2`.split($/) + assert_equal output, `cat #{tempname} | #{FZF} -fi -n2,1..2`.split($/) + end + def test_tiebreak_end_backward_scan input = %w[ foobar-fb From 8fa9e85980b8a7f291e5f749c93f11be7972a161 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Oct 2015 01:10:00 +0900 Subject: [PATCH 017/658] [zsh-completion] Allow custom completion function While in bash you can externally register custom completion functions using `complete` command, it was not possible to do so in zsh without changing completion.zsh as the name of the supported commands are hard-coded within the code (See #362). With this commit, fzf-completion of zsh will first look if `_fzf_COMMAND_completion` exists and calls the function, so one can externally define completion functions for specific commands. This commit also tries to make the interface of (yet undocumented) _fzf_list_completion helper function consistent across bash and zsh. So the following code works both on bash and zsh. _fzf_pass_completion() { local pwdir=${PASSWORD_STORE_DIR-~/.password-store/} local stringsize="${#pwdir}" let "stringsize+=1" _fzf_list_completion '+m' "$@" << "EOF" find "$pwdir" -name "*.gpg" -print | cut -c "$stringsize"- | sed -e 's/\(.*\)\.gpg/\1/' EOF } # Only on bash complete -F _fzf_pass_completion -o default -o bashdefault pass Note that the suggested convention and the interface are not yet final and subject to change. /cc @d4ndo --- shell/completion.bash | 6 +++--- shell/completion.zsh | 45 +++++++++++++++++++++---------------------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index d7fdf26..3335a6a 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -195,7 +195,7 @@ _fzf_kill_completion() { _fzf_telnet_completion() { _fzf_list_completion '+m' "$@" << "EOF" - \grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u + \grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u EOF } @@ -207,13 +207,13 @@ EOF _fzf_env_var_completion() { _fzf_list_completion '-m' "$@" << "EOF" - declare -xp | sed 's/=.*//' | sed 's/.* //' + declare -xp | sed 's/=.*//' | sed 's/.* //' EOF } _fzf_alias_completion() { _fzf_list_completion '-m' "$@" << "EOF" - alias | sed 's/=.*//' | sed 's/.* //' + alias | sed 's/=.*//' | sed 's/.* //' EOF } diff --git a/shell/completion.zsh b/shell/completion.zsh index f45c538..9b6f467 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -60,10 +60,9 @@ _fzf_dir_completion() { } _fzf_list_completion() { - local prefix lbuf fzf_opts src fzf matches - prefix=$1 + local fzf_opts lbuf src fzf matches + fzf_opts=$1 lbuf=$2 - fzf_opts=$3 read -r src [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" @@ -75,26 +74,32 @@ _fzf_list_completion() { } _fzf_telnet_completion() { - _fzf_list_completion "$1" "$2" '+m' << "EOF" - \grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u + _fzf_list_completion '+m' "$@" << "EOF" + \grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u EOF } _fzf_ssh_completion() { - _fzf_list_completion "$1" "$2" '+m' << "EOF" + _fzf_list_completion '+m' "$@" << "EOF" cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u EOF } -_fzf_env_var_completion() { - _fzf_list_completion "$1" "$2" '+m' << "EOF" - declare -xp | sed 's/=.*//' | sed 's/.* //' +_fzf_export_completion() { + _fzf_list_completion '+m' "$@" << "EOF" + declare -xp | sed 's/=.*//' | sed 's/.* //' EOF } -_fzf_alias_completion() { - _fzf_list_completion "$1" "$2" '+m' << "EOF" - alias | sed 's/=.*//' +_fzf_unset_completion() { + _fzf_list_completion '+m' "$@" << "EOF" + declare -xp | sed 's/=.*//' | sed 's/.* //' +EOF +} + +_fzf_unalias_completion() { + _fzf_list_completion '+m' "$@" << "EOF" + alias | sed 's/=.*//' EOF } @@ -135,18 +140,12 @@ fzf-completion() { [ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}} [ -z "${tokens[-1]}" ] && lbuf=$LBUFFER || lbuf=${LBUFFER:0:-${#tokens[-1]}} - if [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then - _fzf_dir_completion "$prefix" $lbuf - elif [ $cmd = telnet ]; then - _fzf_telnet_completion "$prefix" $lbuf - elif [ $cmd = ssh ]; then - _fzf_ssh_completion "$prefix" $lbuf - elif [ $cmd = unset -o $cmd = export ]; then - _fzf_env_var_completion "$prefix" $lbuf - elif [ $cmd = unalias ]; then - _fzf_alias_completion "$prefix" $lbuf + if eval "type _fzf_${cmd}_completion > /dev/null"; then + eval "prefix=\"$prefix\" _fzf_${cmd}_completion \"$lbuf\"" + elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then + _fzf_dir_completion "$prefix" "$lbuf" else - _fzf_all_completion "$prefix" $lbuf + _fzf_all_completion "$prefix" "$lbuf" fi # Fall back to default completion else From 659f49a09a0e77efb60f4adcb092575c8aa58d87 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Oct 2015 13:01:09 +0900 Subject: [PATCH 018/658] [fzf-tmux] Create temp files in $TMPDIR if defined --- bin/fzf-tmux | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 239c878..1085b96 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -91,10 +91,10 @@ set -e # Clean up named pipes on exit id=$RANDOM -argsf=/tmp/fzf-args-$id -fifo1=/tmp/fzf-fifo1-$id -fifo2=/tmp/fzf-fifo2-$id -fifo3=/tmp/fzf-fifo3-$id +argsf="${TMPDIR:-/tmp}/fzf-args-$id" +fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id" +fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id" +fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id" cleanup() { rm -f $argsf $fifo1 $fifo2 $fifo3 } From eee45a9578042c70353d83267e67b442afa13b4c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Oct 2015 19:34:38 +0900 Subject: [PATCH 019/658] [completion] Revamp completion API * _fzf_complete is the helper function for custom completion * _fzf_complete FZF_OPTS ARGS * Reads the output of the source command instead of the command string * In zsh, you can use pipe to feed the data into the function, but it's not possible in bash as by doing so COMPREPLY is set from the subshell and thus nullified * Change the naming convention for consistency: * _fzf_complete_COMMAND e.g. # pass completion suggested by @d4ndo (#362) _fzf_complete_pass() { _fzf_complete '+m' "$@" < <( local pwdir=${PASSWORD_STORE_DIR-~/.password-store/} local stringsize="${#pwdir}" find "$pwdir" -name "*.gpg" -print | cut -c "$((stringsize + 1))"- | sed -e 's/\(.*\)\.gpg/\1/' ) } # Only in bash complete -F _fzf_complete_pass -o default -o bashdefault pass --- shell/completion.bash | 82 ++++++++++++++++++++++++++----------------- shell/completion.zsh | 70 +++++++++++++++++++++--------------- 2 files changed, 91 insertions(+), 61 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 3335a6a..44965ae 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -94,7 +94,7 @@ _fzf_handle_dynamic_completion() { fi } -_fzf_path_completion() { +__fzf_generic_path_completion() { local cur base dir leftover matches trigger cmd fzf [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') @@ -135,20 +135,29 @@ _fzf_path_completion() { fi } -_fzf_list_completion() { - local cur selected trigger cmd src fzf +_fzf_feed_fifo() ( + rm -f "$fifo" + mkfifo "$fifo" + cat <&0 > "$fifo" & +) + +_fzf_complete() { + local fifo cur selected trigger cmd fzf + fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$" [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" - read -r src + cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') trigger=${FZF_COMPLETION_TRIGGER-'**'} cur="${COMP_WORDS[COMP_CWORD]}" if [[ ${cur} == *"$trigger" ]]; then cur=${cur:0:${#cur}-${#trigger}} + _fzf_feed_fifo "$fifo" tput sc - selected=$(eval "$src | $fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ') - selected=${selected% } + selected=$(eval "cat '$fifo' | $fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ') + selected=${selected% } # Strip trailing space not to repeat "-o nospace" tput rc + rm -f "$fifo" if [ -n "$selected" ]; then COMPREPLY=("$selected") @@ -160,25 +169,25 @@ _fzf_list_completion() { fi } -_fzf_all_completion() { - _fzf_path_completion \ +_fzf_path_completion() { + __fzf_generic_path_completion \ "-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \ "-m" "" "$@" } _fzf_file_completion() { - _fzf_path_completion \ + __fzf_generic_path_completion \ "-name .git -prune -o -name .svn -prune -o -type f -print -o -type l -print" \ "-m" "" "$@" } _fzf_dir_completion() { - _fzf_path_completion \ + __fzf_generic_path_completion \ "-name .git -prune -o -name .svn -prune -o -type d -print" \ "" "/" "$@" } -_fzf_kill_completion() { +_fzf_complete_kill() { [ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1 local selected fzf @@ -193,28 +202,37 @@ _fzf_kill_completion() { fi } -_fzf_telnet_completion() { - _fzf_list_completion '+m' "$@" << "EOF" - \grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u -EOF +_fzf_complete_telnet() { + _fzf_complete '+m' "$@" < <( + \grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | + awk '{if (length($2) > 0) {print $2}}' | sort -u + ) } -_fzf_ssh_completion() { - _fzf_list_completion '+m' "$@" << "EOF" - cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u -EOF +_fzf_complete_ssh() { + _fzf_complete '+m' "$@" < <( + cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \ + <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | + awk '{if (length($2) > 0) {print $2}}' | sort -u + ) } -_fzf_env_var_completion() { - _fzf_list_completion '-m' "$@" << "EOF" +_fzf_complete_unset() { + _fzf_complete '-m' "$@" < <( declare -xp | sed 's/=.*//' | sed 's/.* //' -EOF + ) } -_fzf_alias_completion() { - _fzf_list_completion '-m' "$@" << "EOF" +_fzf_complete_export() { + _fzf_complete '-m' "$@" < <( + declare -xp | sed 's/=.*//' | sed 's/.* //' + ) +} + +_fzf_complete_unalias() { + _fzf_complete '-m' "$@" < <( alias | sed 's/=.*//' | sed 's/.* //' -EOF + ) } # fzf options @@ -257,19 +275,19 @@ done # Anything for cmd in $a_cmds; do - complete -F _fzf_all_completion -o default -o bashdefault $cmd + complete -F _fzf_path_completion -o default -o bashdefault $cmd done # Kill completion -complete -F _fzf_kill_completion -o nospace -o default -o bashdefault kill +complete -F _fzf_complete_kill -o nospace -o default -o bashdefault kill # Host completion -complete -F _fzf_ssh_completion -o default -o bashdefault ssh -complete -F _fzf_telnet_completion -o default -o bashdefault telnet +complete -F _fzf_complete_ssh -o default -o bashdefault ssh +complete -F _fzf_complete_telnet -o default -o bashdefault telnet # Environment variables / Aliases -complete -F _fzf_env_var_completion -o default -o bashdefault unset -complete -F _fzf_env_var_completion -o default -o bashdefault export -complete -F _fzf_alias_completion -o default -o bashdefault unalias +complete -F _fzf_complete_unset -o default -o bashdefault unset +complete -F _fzf_complete_export -o default -o bashdefault export +complete -F _fzf_complete_unalias -o default -o bashdefault unalias unset cmd d_cmds f_cmds a_cmds x_cmds diff --git a/shell/completion.zsh b/shell/completion.zsh index 9b6f467..d203498 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -10,8 +10,9 @@ # - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_OPTS (default: empty) -_fzf_path_completion() { +__fzf_generic_path_completion() { local base lbuf find_opts fzf_opts suffix tail fzf dir leftover matches nnm + # (Q) flag removes a quoting level: "foo\ bar" => "foo bar" base=${(Q)1} lbuf=$2 find_opts=$3 @@ -47,60 +48,71 @@ _fzf_path_completion() { [ -n "$nnm" ] && unsetopt nonomatch } -_fzf_all_completion() { - _fzf_path_completion "$1" "$2" \ +_fzf_path_completion() { + __fzf_generic_path_completion "$1" "$2" \ "-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \ "-m" "" " " } _fzf_dir_completion() { - _fzf_path_completion "$1" "$2" \ + __fzf_generic_path_completion "$1" "$2" \ "-name .git -prune -o -name .svn -prune -o -type d -print" \ "" "/" "" } -_fzf_list_completion() { - local fzf_opts lbuf src fzf matches +_fzf_feed_fifo() ( + rm -f "$fifo" + mkfifo "$fifo" + cat <&0 > "$fifo" & +) + +_fzf_complete() { + local fifo fzf_opts lbuf fzf matches + fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$" fzf_opts=$1 lbuf=$2 - read -r src [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" - matches=$(eval "$src" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$prefix") + _fzf_feed_fifo "$fifo" + matches=$(cat "$fifo" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | tr '\n' ' ') if [ -n "$matches" ]; then - LBUFFER="$lbuf$matches " + LBUFFER="$lbuf$matches" fi zle redisplay + rm -f "$fifo" } -_fzf_telnet_completion() { - _fzf_list_completion '+m' "$@" << "EOF" - \grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u -EOF +_fzf_complete_telnet() { + _fzf_complete '+m' "$@" < <( + \grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | + awk '{if (length($2) > 0) {print $2}}' | sort -u + ) } -_fzf_ssh_completion() { - _fzf_list_completion '+m' "$@" << "EOF" - cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u -EOF +_fzf_complete_ssh() { + _fzf_complete '+m' "$@" < <( + cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \ + <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | + awk '{if (length($2) > 0) {print $2}}' | sort -u + ) } -_fzf_export_completion() { - _fzf_list_completion '+m' "$@" << "EOF" +_fzf_complete_export() { + _fzf_complete '-m' "$@" < <( declare -xp | sed 's/=.*//' | sed 's/.* //' -EOF + ) } -_fzf_unset_completion() { - _fzf_list_completion '+m' "$@" << "EOF" +_fzf_complete_unset() { + _fzf_complete '-m' "$@" < <( declare -xp | sed 's/=.*//' | sed 's/.* //' -EOF + ) } -_fzf_unalias_completion() { - _fzf_list_completion '+m' "$@" << "EOF" +_fzf_complete_unalias() { + _fzf_complete '+m' "$@" < <( alias | sed 's/=.*//' -EOF + ) } fzf-completion() { @@ -140,12 +152,12 @@ fzf-completion() { [ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}} [ -z "${tokens[-1]}" ] && lbuf=$LBUFFER || lbuf=${LBUFFER:0:-${#tokens[-1]}} - if eval "type _fzf_${cmd}_completion > /dev/null"; then - eval "prefix=\"$prefix\" _fzf_${cmd}_completion \"$lbuf\"" + if eval "type _fzf_complete_${cmd} > /dev/null"; then + eval "prefix=\"$prefix\" _fzf_complete_${cmd} \"$lbuf\"" elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then _fzf_dir_completion "$prefix" "$lbuf" else - _fzf_all_completion "$prefix" "$lbuf" + _fzf_path_completion "$prefix" "$lbuf" fi # Fall back to default completion else From 86bc9d506fb29d65d6d666d18422f3337bf52f13 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Oct 2015 23:19:26 +0900 Subject: [PATCH 020/658] Fix invalid interrupt handler during execute action Interrupt handling during execute action was not serialized and often caused crash, failed to restore the terminal state. --- src/curses/curses.go | 9 --------- src/terminal.go | 7 +++++++ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index debdeaf..06493a2 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -11,7 +11,6 @@ import "C" import ( "fmt" "os" - "os/signal" "syscall" "time" "unicode/utf8" @@ -271,14 +270,6 @@ func Init(theme *ColorTheme, black bool, mouse bool) { C.noecho() C.raw() // stty dsusp undef - intChan := make(chan os.Signal, 1) - signal.Notify(intChan, os.Interrupt, os.Kill) - go func() { - <-intChan - Close() - os.Exit(2) - }() - if theme != nil { C.start_color() initPairs(theme, black) diff --git a/src/terminal.go b/src/terminal.go index 9a92ba4..ab77ecf 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -727,6 +727,13 @@ func (t *Terminal) Loop() { t.reqBox.Set(reqRefresh, nil) }() + intChan := make(chan os.Signal, 1) + signal.Notify(intChan, os.Interrupt, os.Kill) + go func() { + <-intChan + t.reqBox.Set(reqQuit, nil) + }() + resizeChan := make(chan os.Signal, 1) signal.Notify(resizeChan, syscall.SIGWINCH) go func() { From 1da065e50e2b1270bec1f62f46daf6e84e9ca991 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Oct 2015 23:28:24 +0900 Subject: [PATCH 021/658] 0.10.7 --- CHANGELOG.md | 8 ++++++++ install | 4 ++-- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0018b9f..72bea32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +0.10.7 +------ + +- Fixed unserialized interrupt handling during execute action which often + caused invalid memory access and crash +- Changed `--tiebreak=length` (default) to use trimmed length when `--nth` is + used + 0.10.6 ------ diff --git a/install b/install index e98f920..f830bc6 100755 --- a/install +++ b/install @@ -1,7 +1,7 @@ #!/usr/bin/env bash -[[ "$@" =~ --pre ]] && version=0.10.6 pre=1 || - version=0.10.6 pre=0 +[[ "$@" =~ --pre ]] && version=0.10.7 pre=1 || + version=0.10.7 pre=0 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 99d40ff..34de2d7 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Sep 2015" "fzf 0.10.6" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Oct 2015" "fzf 0.10.7" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 7deb078..969eaa9 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.10.6" + version = "0.10.7" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From a4eb3323dafcb290ed68f6a70327bfc3b5e39f29 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 9 Oct 2015 12:16:47 +0900 Subject: [PATCH 022/658] Fix #370 - Panic when trying to set colors when colors are disabled --- src/options.go | 9 ++++++--- src/options_test.go | 12 ++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/options.go b/src/options.go index 9d8aaa1..81fa377 100644 --- a/src/options.go +++ b/src/options.go @@ -380,8 +380,11 @@ func parseTiebreak(str string) tiebreak { } func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { - dupe := *theme - return &dupe + if theme != nil { + dupe := *theme + return &dupe + } + return nil } func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme { @@ -402,7 +405,7 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme } // Color is disabled if theme == nil { - errorExit("colors disabled; cannot customize colors") + continue } pair := strings.Split(str, ":") diff --git a/src/options_test.go b/src/options_test.go index 1f96f78..1e9ede4 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -316,3 +316,15 @@ func TestColorSpec(t *testing.T) { t.Errorf("using default colors") } } + +func TestParseNilTheme(t *testing.T) { + var theme *curses.ColorTheme + newTheme := parseTheme(theme, "prompt:12") + if newTheme != nil { + t.Errorf("color is disabled. keep it that way.") + } + newTheme = parseTheme(theme, "prompt:12,dark,prompt:13") + if newTheme.Prompt != 13 { + t.Errorf("color should now be enabled and customized") + } +} From ed893c5f47453f9b27b708b01416c0a49446b97e Mon Sep 17 00:00:00 2001 From: Jacob Niehus Date: Thu, 8 Oct 2015 18:32:48 -0700 Subject: [PATCH 023/658] Trigger netrw autocommand when opening directory --- plugin/fzf.vim | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 168bc61..bd8e20b 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -416,6 +416,9 @@ function! s:cmd_callback(lines) abort set noautochdir for item in a:lines execute cmd s:escape(item) + if exists('#BufEnter') && isdirectory(item) + doautocmd BufEnter + endif endfor finally let &autochdir = autochdir From b3010a46241df01a93dd844822762d9f93ca2850 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 9 Oct 2015 12:41:29 +0900 Subject: [PATCH 024/658] 0.10.8 --- CHANGELOG.md | 5 +++++ install | 4 ++-- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72bea32..ab3dd1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +0.10.8 +------ + +- Fixed panic when trying to set colors after colors are disabled (#370) + 0.10.7 ------ diff --git a/install b/install index f830bc6..6261b8f 100755 --- a/install +++ b/install @@ -1,7 +1,7 @@ #!/usr/bin/env bash -[[ "$@" =~ --pre ]] && version=0.10.7 pre=1 || - version=0.10.7 pre=0 +[[ "$@" =~ --pre ]] && version=0.10.8 pre=1 || + version=0.10.8 pre=0 cd $(dirname $BASH_SOURCE) fzf_base=$(pwd) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 34de2d7..ccb7ead 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Oct 2015" "fzf 0.10.7" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Oct 2015" "fzf 0.10.8" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 969eaa9..1c99e11 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.10.7" + version = "0.10.8" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 486b87d8216537497078fd582ef5bd64fc8c5bfb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 12 Oct 2015 00:27:30 +0900 Subject: [PATCH 025/658] [bash-completion] Retain original completion options (#288) --- shell/completion.bash | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 44965ae..0c20383 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -11,8 +11,8 @@ # - $FZF_COMPLETION_OPTS (default: empty) _fzf_orig_completion_filter() { - sed 's/.*-F *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\2=\1;/' | - sed 's/[^a-z0-9_= ;]/_/g' + sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' | + awk -F= '{gsub(/[^a-z0-9_= ;]/, "_", $1); print $1"="$2}' } _fzf_opts_completion() { @@ -77,12 +77,12 @@ _fzf_opts_completion() { } _fzf_handle_dynamic_completion() { - local cmd orig ret orig_cmd + local cmd orig_var orig ret orig_cmd cmd="$1" shift orig_cmd="$1" - - orig=$(eval "echo \$_fzf_orig_completion_$cmd") + orig_var="_fzf_orig_completion_$cmd" + orig="${!orig_var##*#}" if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then $orig "$@" elif [ -n "$_fzf_completion_loader" ]; then @@ -252,32 +252,48 @@ a_cmds=" x_cmds="kill ssh telnet unset unalias export" # Preserve existing completion -if [ "$_fzf_completion_loaded" != '0.9.12' ]; then +if [ "$_fzf_completion_loaded" != '0.10.8' ]; then # Really wish I could use associative array but OSX comes with bash 3.2 :( eval $(complete | \grep '\-F' | \grep -v _fzf_ | \grep -E " ($(echo $d_cmds $f_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter) - export _fzf_completion_loaded=0.9.12 + export _fzf_completion_loaded=0.10.8 fi if type _completion_loader > /dev/null 2>&1; then _fzf_completion_loader=1 fi +_fzf_defc() { + local cmd func opts orig_var orig + cmd="$1" + func="$2" + opts="$3" + orig_var="_fzf_orig_completion_$cmd" + orig="${!orig_var}" + if [ -n "$orig" ]; then + eval "$(printf "$orig" "$func")" + else + complete -F "$func" $opts "$cmd" + fi +} + # Directory for cmd in $d_cmds; do - complete -F _fzf_dir_completion -o nospace -o plusdirs $cmd + _fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o plusdirs" done # File for cmd in $f_cmds; do - complete -F _fzf_file_completion -o default -o bashdefault $cmd + _fzf_defc "$cmd" _fzf_file_completion "-o default -o bashdefault" done # Anything for cmd in $a_cmds; do - complete -F _fzf_path_completion -o default -o bashdefault $cmd + _fzf_defc "$cmd" _fzf_path_completion "-o default -o bashdefault" done +unset _fzf_defc + # Kill completion complete -F _fzf_complete_kill -o nospace -o default -o bashdefault kill From b4ce89bbf58da1cadfda4be2f9f8b1257317f848 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 12 Oct 2015 16:02:01 +0900 Subject: [PATCH 026/658] [build] Link libncursesw when building 64-bit linux binary Close #376 --- src/curses/curses.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index 06493a2..3ea35bf 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -4,7 +4,7 @@ package curses #include #include #cgo !static LDFLAGS: -lncurses -#cgo static LDFLAGS: -l:libncurses.a -l:libtinfo.a -l:libgpm.a -ldl +#cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl */ import "C" From f80ff8c9174f1a4bc8ed25dd5dbde8a90dbfe7e5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 13 Oct 2015 02:24:38 +0900 Subject: [PATCH 027/658] Add bindable double-click event (#374) --- src/curses/curses.go | 1 + src/options.go | 2 + src/terminal.go | 376 ++++++++++++++++++++++--------------------- 3 files changed, 195 insertions(+), 184 deletions(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index 3ea35bf..8311719 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -50,6 +50,7 @@ const ( Invalid Mouse + DoubleClick BTab BSpace diff --git a/src/options.go b/src/options.go index 81fa377..16de221 100644 --- a/src/options.go +++ b/src/options.go @@ -343,6 +343,8 @@ func parseKeyChords(str string, message string) map[int]string { chord = curses.SLeft case "shift-right": chord = curses.SRight + case "double-click": + chord = curses.DoubleClick default: if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { chord = curses.CtrlA + int(lkey[5]) - 'a' diff --git a/src/terminal.go b/src/terminal.go index ab77ecf..ca4ca21 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -180,6 +180,7 @@ func defaultKeymap() map[int]actionType { keymap[C.Rune] = actRune keymap[C.Mouse] = actMouse + keymap[C.DoubleClick] = actAccept return keymap } @@ -858,204 +859,211 @@ func (t *Terminal) Loop() { action = act } } - switch action { - case actIgnore: - case actExecute: - if t.cy >= 0 && t.cy < t.merger.Length() { - item := t.merger.Get(t.cy) - executeCommand(t.execmap[mapkey], item.AsString(t.ansi)) - } - case actInvalid: - t.mutex.Unlock() - continue - case actToggleSort: - t.sort = !t.sort - t.eventBox.Set(EvtSearchNew, t.sort) - t.mutex.Unlock() - continue - case actBeginningOfLine: - t.cx = 0 - case actBackwardChar: - if t.cx > 0 { - t.cx-- - } - case actAbort: - req(reqQuit) - case actDeleteChar: - t.delChar() - case actDeleteCharEOF: - if !t.delChar() && t.cx == 0 { - req(reqQuit) - } - case actEndOfLine: - t.cx = len(t.input) - case actCancel: - if len(t.input) == 0 { - req(reqQuit) - } else { - t.yanked = t.input - t.input = []rune{} + var doAction func(actionType) bool + doAction = func(action actionType) bool { + switch action { + case actIgnore: + case actExecute: + if t.cy >= 0 && t.cy < t.merger.Length() { + item := t.merger.Get(t.cy) + executeCommand(t.execmap[mapkey], item.AsString(t.ansi)) + } + case actInvalid: + t.mutex.Unlock() + return false + case actToggleSort: + t.sort = !t.sort + t.eventBox.Set(EvtSearchNew, t.sort) + t.mutex.Unlock() + return false + case actBeginningOfLine: t.cx = 0 - } - case actForwardChar: - if t.cx < len(t.input) { - t.cx++ - } - case actBackwardDeleteChar: - if t.cx > 0 { - t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) - t.cx-- - } - case actSelectAll: - if t.multi { - for i := 0; i < t.merger.Length(); i++ { - item := t.merger.Get(i) - selectItem(item) + case actBackwardChar: + if t.cx > 0 { + t.cx-- } - req(reqList, reqInfo) - } - case actDeselectAll: - if t.multi { - for i := 0; i < t.merger.Length(); i++ { - item := t.merger.Get(i) - delete(t.selected, item.index) + case actAbort: + req(reqQuit) + case actDeleteChar: + t.delChar() + case actDeleteCharEOF: + if !t.delChar() && t.cx == 0 { + req(reqQuit) } - req(reqList, reqInfo) - } - case actToggle: - if t.multi && t.merger.Length() > 0 { - toggle() - req(reqList) - } - case actToggleAll: - if t.multi { - for i := 0; i < t.merger.Length(); i++ { - toggleY(i) - } - req(reqList, reqInfo) - } - case actToggleDown: - if t.multi && t.merger.Length() > 0 { - toggle() - t.vmove(-1) - req(reqList) - } - case actToggleUp: - if t.multi && t.merger.Length() > 0 { - toggle() - t.vmove(1) - req(reqList) - } - case actDown: - t.vmove(-1) - req(reqList) - case actUp: - t.vmove(1) - req(reqList) - case actAccept: - req(reqClose) - case actClearScreen: - req(reqRedraw) - case actUnixLineDiscard: - if t.cx > 0 { - t.yanked = copySlice(t.input[:t.cx]) - t.input = t.input[t.cx:] - t.cx = 0 - } - case actUnixWordRubout: - if t.cx > 0 { - t.rubout("\\s\\S") - } - case actBackwardKillWord: - if t.cx > 0 { - t.rubout("[^[:alnum:]][[:alnum:]]") - } - case actYank: - suffix := copySlice(t.input[t.cx:]) - t.input = append(append(t.input[:t.cx], t.yanked...), suffix...) - t.cx += len(t.yanked) - case actPageUp: - t.vmove(t.maxItems() - 1) - req(reqList) - case actPageDown: - t.vmove(-(t.maxItems() - 1)) - req(reqList) - case actBackwardWord: - t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 - case actForwardWord: - t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 - case actKillWord: - ncx := t.cx + - findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 - if ncx > t.cx { - t.yanked = copySlice(t.input[t.cx:ncx]) - t.input = append(t.input[:t.cx], t.input[ncx:]...) - } - case actKillLine: - if t.cx < len(t.input) { - t.yanked = copySlice(t.input[t.cx:]) - t.input = t.input[:t.cx] - } - case actRune: - prefix := copySlice(t.input[:t.cx]) - t.input = append(append(prefix, event.Char), t.input[t.cx:]...) - t.cx++ - case actPreviousHistory: - if t.history != nil { - t.history.override(string(t.input)) - t.input = []rune(t.history.previous()) + case actEndOfLine: t.cx = len(t.input) - } - case actNextHistory: - if t.history != nil { - t.history.override(string(t.input)) - t.input = []rune(t.history.next()) - t.cx = len(t.input) - } - case actMouse: - me := event.MouseEvent - mx, my := me.X, me.Y - if me.S != 0 { - // Scroll - if t.merger.Length() > 0 { - if t.multi && me.Mod { - toggle() + case actCancel: + if len(t.input) == 0 { + req(reqQuit) + } else { + t.yanked = t.input + t.input = []rune{} + t.cx = 0 + } + case actForwardChar: + if t.cx < len(t.input) { + t.cx++ + } + case actBackwardDeleteChar: + if t.cx > 0 { + t.input = append(t.input[:t.cx-1], t.input[t.cx:]...) + t.cx-- + } + case actSelectAll: + if t.multi { + for i := 0; i < t.merger.Length(); i++ { + item := t.merger.Get(i) + selectItem(item) } - t.vmove(me.S) + req(reqList, reqInfo) + } + case actDeselectAll: + if t.multi { + for i := 0; i < t.merger.Length(); i++ { + item := t.merger.Get(i) + delete(t.selected, item.index) + } + req(reqList, reqInfo) + } + case actToggle: + if t.multi && t.merger.Length() > 0 { + toggle() req(reqList) } - } else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] && - my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] { - mx -= t.marginInt[3] - my -= t.marginInt[0] - mx = util.Constrain(mx-len(t.prompt), 0, len(t.input)) - if !t.reverse { - my = t.maxHeight() - my - 1 - } - min := 2 + len(t.header) - if t.inlineInfo { - min-- - } - if me.Double { - // Double-click - if my >= min { - if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { - req(reqClose) - } + case actToggleAll: + if t.multi { + for i := 0; i < t.merger.Length(); i++ { + toggleY(i) } - } 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 { + req(reqList, reqInfo) + } + case actToggleDown: + if t.multi && t.merger.Length() > 0 { + toggle() + t.vmove(-1) + req(reqList) + } + case actToggleUp: + if t.multi && t.merger.Length() > 0 { + toggle() + t.vmove(1) + req(reqList) + } + case actDown: + t.vmove(-1) + req(reqList) + case actUp: + t.vmove(1) + req(reqList) + case actAccept: + req(reqClose) + case actClearScreen: + req(reqRedraw) + case actUnixLineDiscard: + if t.cx > 0 { + t.yanked = copySlice(t.input[:t.cx]) + t.input = t.input[t.cx:] + t.cx = 0 + } + case actUnixWordRubout: + if t.cx > 0 { + t.rubout("\\s\\S") + } + case actBackwardKillWord: + if t.cx > 0 { + t.rubout("[^[:alnum:]][[:alnum:]]") + } + case actYank: + suffix := copySlice(t.input[t.cx:]) + t.input = append(append(t.input[:t.cx], t.yanked...), suffix...) + t.cx += len(t.yanked) + case actPageUp: + t.vmove(t.maxItems() - 1) + req(reqList) + case actPageDown: + t.vmove(-(t.maxItems() - 1)) + req(reqList) + case actBackwardWord: + t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 + case actForwardWord: + t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 + case actKillWord: + ncx := t.cx + + findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 + if ncx > t.cx { + t.yanked = copySlice(t.input[t.cx:ncx]) + t.input = append(t.input[:t.cx], t.input[ncx:]...) + } + case actKillLine: + if t.cx < len(t.input) { + t.yanked = copySlice(t.input[t.cx:]) + t.input = t.input[:t.cx] + } + case actRune: + prefix := copySlice(t.input[:t.cx]) + t.input = append(append(prefix, event.Char), t.input[t.cx:]...) + t.cx++ + case actPreviousHistory: + if t.history != nil { + t.history.override(string(t.input)) + t.input = []rune(t.history.previous()) + t.cx = len(t.input) + } + case actNextHistory: + if t.history != nil { + t.history.override(string(t.input)) + t.input = []rune(t.history.next()) + t.cx = len(t.input) + } + case actMouse: + me := event.MouseEvent + mx, my := me.X, me.Y + if me.S != 0 { + // Scroll + if t.merger.Length() > 0 { + if t.multi && me.Mod { toggle() } + t.vmove(me.S) req(reqList) } + } else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] && + my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] { + mx -= t.marginInt[3] + my -= t.marginInt[0] + mx = util.Constrain(mx-len(t.prompt), 0, len(t.input)) + if !t.reverse { + my = t.maxHeight() - my - 1 + } + min := 2 + len(t.header) + if t.inlineInfo { + min-- + } + if me.Double { + // Double-click + if my >= min { + if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { + return doAction(t.keymap[C.DoubleClick]) + } + } + } 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) + } + } } } + return true + } + if !doAction(action) { + continue } changed := string(previousInput) != string(t.input) t.mutex.Unlock() // Must be unlocked before touching reqBox From ae04f56dbdc9cd635f5a3d56082faf2399d91f75 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 13 Oct 2015 02:36:11 +0900 Subject: [PATCH 028/658] Fix --bind "double-click:execute(...)" (#374) --- src/terminal.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index ca4ca21..4f8341d 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -851,16 +851,8 @@ func (t *Terminal) Loop() { } } - action := t.keymap[event.Type] - mapkey := event.Type - if event.Type == C.Rune { - mapkey = int(event.Char) + int(C.AltZ) - if act, prs := t.keymap[mapkey]; prs { - action = act - } - } - var doAction func(actionType) bool - doAction = func(action actionType) bool { + var doAction func(actionType, int) bool + doAction = func(action actionType, mapkey int) bool { switch action { case actIgnore: case actExecute: @@ -1043,7 +1035,7 @@ func (t *Terminal) Loop() { // Double-click if my >= min { if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { - return doAction(t.keymap[C.DoubleClick]) + return doAction(t.keymap[C.DoubleClick], C.DoubleClick) } } } else if me.Down { @@ -1062,7 +1054,15 @@ func (t *Terminal) Loop() { } return true } - if !doAction(action) { + action := t.keymap[event.Type] + mapkey := event.Type + if event.Type == C.Rune { + mapkey = int(event.Char) + int(C.AltZ) + if act, prs := t.keymap[mapkey]; prs { + action = act + } + } + if !doAction(action, mapkey) { continue } changed := string(previousInput) != string(t.input) From 4d709e0dd2c1dc677c4ca54dc8c6f8e17875d5fc Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 23 Oct 2015 01:12:31 +0900 Subject: [PATCH 029/658] Fix #391 - Strip non-printable characters --- src/curses/curses.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index 8311719..9619925 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -11,6 +11,7 @@ import "C" import ( "fmt" "os" + "strings" "syscall" "time" "unicode/utf8" @@ -514,7 +515,12 @@ func MoveAndClear(y int, x int) { } func Print(text string) { - C.addstr(C.CString(text)) + C.addstr(C.CString(strings.Map(func(r rune) rune { + if r < 32 { + return -1 + } + return r + }, text))) } func CPrint(pair int, bold bool, text string) { From 02203c7739f4af80b24c40caf2553fcd616bbbe0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 23 Oct 2015 14:56:27 +0900 Subject: [PATCH 030/658] Add command-line flags to install script Close #392 usage: ./install [OPTIONS] --help Show this message --bin Download fzf binary only --all Download fzf binary and update configuration files to enable key bindings and fuzzy completion --[no-]key-bindings Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C) --[no-]completion Enable/disable fuzzy completion (bash & zsh) --[no-]update-rc Whether or not to update shell configuration files --- README.md | 2 +- install | 118 ++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 93 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 956dd1e..e4521c6 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf (recommended): ```vim -Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' } +Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' } ``` #### Upgrading fzf diff --git a/install b/install index 6261b8f..be2cd02 100755 --- a/install +++ b/install @@ -1,10 +1,56 @@ #!/usr/bin/env bash +set -u + [[ "$@" =~ --pre ]] && version=0.10.8 pre=1 || version=0.10.8 pre=0 +auto_completion= +key_bindings= +update_config=1 + +help() { + cat << EOF +usage: $0 [OPTIONS] + + --help Show this message + --bin Download fzf binary only + --all Download fzf binary and update configuration files + to enable key bindings and fuzzy completion + --[no-]key-bindings Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C) + --[no-]completion Enable/disable fuzzy completion (bash & zsh) + --[no-]update-rc Whether or not to update shell configuration files + +EOF +} + +for opt in $@; do + case $opt in + --help) + help + exit 0 + ;; + --all) + auto_completion=1 + key_bindings=1 + update_config=1 + ;; + --key-bindings) key_bindings=1 ;; + --no-key-bindings) key_bindings=0 ;; + --completion) auto_completion=1 ;; + --no-completion) auto_completion=0 ;; + --update-rc) update_config=1 ;; + --no-update-rc) update_config=0 ;; + *) + echo "unknown option: $opt" + help + exit 1 + ;; + esac +done + cd $(dirname $BASH_SOURCE) -fzf_base=$(pwd) +fzf_base="$(pwd)" # If stdin is a tty, we are "interactive". [ -t 0 ] && interactive=yes @@ -16,7 +62,7 @@ ask() { read -p "$1 ([y]/n) " $read_n -r echo - [[ ! $REPLY =~ ^[Nn]$ ]] + [[ $REPLY =~ ^[Nn]$ ]] } check_binary() { @@ -173,12 +219,16 @@ fi [[ "$*" =~ "--bin" ]] && exit 0 # Auto-completion -ask "Do you want to add auto-completion support?" -auto_completion=$? +if [ -z "$auto_completion" ]; then + ask "Do you want to enable fuzzy auto-completion?" + auto_completion=$? +fi # Key-bindings -ask "Do you want to add key bindings?" -key_bindings=$? +if [ -z "$key_bindings" ]; then + ask "Do you want to enable key bindings?" + key_bindings=$? +fi echo for shell in bash zsh; do @@ -186,12 +236,12 @@ for shell in bash zsh; do src=~/.fzf.${shell} fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null" - if [ $auto_completion -ne 0 ]; then + if [ $auto_completion -eq 0 ]; then fzf_completion="# $fzf_completion" fi fzf_key_bindings="source \"$fzf_base/shell/key-bindings.${shell}\"" - if [ $key_bindings -ne 0 ]; then + if [ $key_bindings -eq 0 ]; then fzf_key_bindings="# $fzf_key_bindings" fi @@ -237,29 +287,45 @@ EOF rm -f ~/.config/fish/functions/fzf.fish && echo "OK" || echo "Failed" fi - if [ $key_bindings -eq 0 ]; then - echo -n "Symlink ~/.config/fish/functions/fzf_key_bindings.fish ... " - ln -sf $fzf_base/shell/key-bindings.fish \ - ~/.config/fish/functions/fzf_key_bindings.fish && echo "OK" || echo "Failed" + fish_binding=~/.config/fish/functions/fzf_key_bindings.fish + if [ $key_bindings -ne 0 ]; then + echo -n "Symlink $fish_binding ... " + ln -sf "$fzf_base/shell/key-bindings.fish" \ + "$fish_binding" && echo "OK" || echo "Failed" + else + echo -n "Removing $fish_binding ... " + rm -f "$fish_binding" + echo "OK" fi fi append_line() { set -e - echo "Update $2:" - echo " - $1" - [ -f "$2" ] || touch "$2" - if [ $# -lt 3 ]; then - line=$(\grep -nF "$1" "$2" | sed 's/:.*//' | tr '\n' ' ') + + local skip line file pat lno + skip="$1" + line="$2" + file="$3" + pat="${4:-}" + + echo "Update $file:" + echo " - $line" + [ -f "$file" ] || touch "$file" + if [ $# -lt 4 ]; then + lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ') else - line=$(\grep -nF "$3" "$2" | sed 's/:.*//' | tr '\n' ' ') + lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ') fi - if [ -n "$line" ]; then - echo " - Already exists: line #$line" + if [ -n "$lno" ]; then + echo " - Already exists: line #$lno" else - echo >> "$2" - echo "$1" >> "$2" - echo " + Added" + if [ $skip -eq 1 ]; then + echo >> "$file" + echo "$line" >> "$file" + echo " + Added" + else + echo " ~ Skipped" + fi fi echo set +e @@ -267,12 +333,12 @@ append_line() { echo for shell in bash zsh; do - append_line "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" ~/.${shell}rc "~/.fzf.${shell}" + append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" ~/.${shell}rc "~/.fzf.${shell}" done -if [ $key_bindings -eq 0 -a $has_fish -eq 1 ]; then +if [ $key_bindings -eq 1 -a $has_fish -eq 1 ]; then bind_file=~/.config/fish/functions/fish_user_key_bindings.fish - append_line "fzf_key_bindings" "$bind_file" + append_line $update_config "fzf_key_bindings" "$bind_file" fi cat << EOF From a1184ceb4e1fcf468a7a56052eff53192092b0f4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 23 Oct 2015 15:07:16 +0900 Subject: [PATCH 031/658] Fix travis CI build --- install | 1 + 1 file changed, 1 insertion(+) diff --git a/install b/install index be2cd02..2a43c9b 100755 --- a/install +++ b/install @@ -53,6 +53,7 @@ cd $(dirname $BASH_SOURCE) fzf_base="$(pwd)" # If stdin is a tty, we are "interactive". +interactive= [ -t 0 ] && interactive=yes ask() { From 5694b5ed30454f26f5741ab43a803b9a726a511d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 23 Oct 2015 17:43:34 +0900 Subject: [PATCH 032/658] Fix #394 - --bin option is broken --- install | 1 + 1 file changed, 1 insertion(+) diff --git a/install b/install index 2a43c9b..6425713 100755 --- a/install +++ b/install @@ -41,6 +41,7 @@ for opt in $@; do --no-completion) auto_completion=0 ;; --update-rc) update_config=1 ;; --no-update-rc) update_config=0 ;; + --bin) ;; *) echo "unknown option: $opt" help From 724ffa3756adb5913ae0ff042d0708c90a80948f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 26 Oct 2015 12:31:43 +0900 Subject: [PATCH 033/658] [install] Do not download binary if it's found in $PATH (#373) /cc @xconstruct --- install | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/install b/install index 6425713..62780f4 100755 --- a/install +++ b/install @@ -103,9 +103,16 @@ download() { if [ -x "$fzf_base"/bin/fzf ]; then echo " - Already exists" check_binary && return - elif [ -x "$fzf_base"/bin/$1 ]; then + fi + if [ -x "$fzf_base"/bin/$1 ]; then symlink $1 && check_binary && return fi + if which_fzf="$(which fzf 2> /dev/null)"; then + echo " - Found in \$PATH" + echo " - Creating symlink: $which_fzf -> bin/fzf" + (cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf) + check_binary && return + fi fi mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin if [ $? -ne 0 ]; then From 5769d3867dcb6a7b79549da7d3269140c1fc3332 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 31 Oct 2015 00:18:23 +0900 Subject: [PATCH 034/658] [nvim] setf fzf --- plugin/fzf.vim | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index bd8e20b..fddab51 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -357,6 +357,7 @@ function! s:execute_term(dict, command, temps) endfunction call termopen(a:command, fzf) + setf fzf startinsert return [] endfunction From 672b59363481337eaa8bce9b104d9a2d723a170f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 3 Nov 2015 22:25:50 +0900 Subject: [PATCH 035/658] Update FZF_DEFAULT_COMMAND example (#310) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e4521c6..ff779ab 100644 --- a/README.md +++ b/README.md @@ -355,7 +355,8 @@ speed of the traversal. ```sh export FZF_DEFAULT_COMMAND=' (git ls-tree -r --name-only HEAD || - find * -name ".*" -prune -o -type f -print -o -type l -print) 2> /dev/null' + find . -path "*/\.*" -prune -o -type f -print -o -type l -print | + sed s/^..//) 2> /dev/null' ``` #### Fish shell From 68541e66b7b4735fc720d5298ffb3d99b115025b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 3 Nov 2015 22:40:45 +0900 Subject: [PATCH 036/658] [man] double-click for --bind (#374) --- man/man1/fzf.1 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index ccb7ead..62cf960 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -179,11 +179,11 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR .RE .RS -.B AVAILABLE KEYS: +.B AVAILABLE KEYS: (SYNONYMS) \fIctrl-[a-z]\fR \fIalt-[a-z]\fR \fIf[1-4]\fR - \fIenter\fR (\fIreturn\fR) + \fIenter\fR (\fIreturn\fR \fIctrl-m\fR) \fIspace\fR \fIbspace\fR (\fIbs\fR) \fIalt-bspace\fR (\fIalt-bs\fR) @@ -201,13 +201,14 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fIpgdn\fR (\fIpage-down\fR) \fIshift-left\fR \fIshift-right\fR + \fIdouble-click\fR or any single character .RE .RS \fBACTION: DEFAULT BINDINGS: \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR - \fBaccept\fR \fIctrl-m (enter)\fR + \fBaccept\fR \fIenter double-click\fR \fBbackward-char\fR \fIctrl-b left\fR \fBbackward-delete-char\fR \fIctrl-h bspace\fR \fBbackward-kill-word\fR \fIalt-bs\fR From 81a88693c12507bcc460bd1150af0f48f917670c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 3 Nov 2015 22:49:32 +0900 Subject: [PATCH 037/658] Make --extended default Close #400 --- CHANGELOG.md | 9 ++++++++ README.md | 8 +++---- man/man1/fzf.1 | 22 +++++++++---------- shell/completion.bash | 2 +- src/core.go | 2 +- src/options.go | 39 ++++++++++++++++----------------- src/options_test.go | 2 +- src/pattern.go | 50 +++++++++++++++++++++++-------------------- src/pattern_test.go | 28 ++++++++++++------------ test/test_go.rb | 15 +++++++++++-- 10 files changed, 100 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab3dd1d..9fea973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ CHANGELOG ========= +0.10.9 +------ + +- Extended-search mode is now enabled by default + - `--extended-exact` is deprecated and instead we have `--exact` for + orthogonally controlling "exactness" of search +- Fixed not to display non-printable characters +- Added `double-click` for `--bind` option + 0.10.8 ------ diff --git a/README.md b/README.md index ff779ab..9d0945e 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ vim $(fzf) #### Extended-search mode -With `-x` or `--extended` option, fzf will start in "extended-search mode". +Since 0.10.9, fzf starts in "extended-search mode" by default. In this mode, you can specify multiple patterns delimited by spaces, such as: `^music .mp3$ sbtrkt !rmx` @@ -125,15 +125,15 @@ such as: `^music .mp3$ sbtrkt !rmx` | `!'fire` | Items that do not include `fire` | inverse-exact-match | If you don't prefer fuzzy matching and do not wish to "quote" every word, -start fzf with `-e` or `--extended-exact` option. Note that in -`--extended-exact` mode, `'`-prefix "unquotes" the term. +start fzf with `-e` or `--exact` option. Note that when `--exact` is set, +`'`-prefix "unquotes" the term. #### 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"` + - Default options. e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"` Examples -------- diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 62cf960..200464a 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Oct 2015" "fzf 0.10.8" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Nov 2015" "fzf 0.10.9" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -36,10 +36,11 @@ fzf is a general-purpose command-line fuzzy finder. .SS Search mode .TP .B "-x, --extended" -Extended-search mode +Extended-search mode. Since 0.10.9, this is enabled by default. You can disable +it with \fB+x\fR or \fB--no-extended\fR. .TP -.B "-e, --extended-exact" -Extended-search mode (exact match) +.B "-e, --exact" +Enable exact-match .TP .B "-i" Case-insensitive match (default: smart-case match) @@ -370,9 +371,9 @@ of field index expressions. .SH EXTENDED SEARCH MODE -With \fB-x\fR or \fB--extended\fR option, fzf will start in "extended-search -mode". In this mode, you can specify multiple patterns delimited by spaces, -such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR +Unless specified otherwise, fzf will start in "extended-search mode". In this +mode, you can specify multiple patterns delimited by spaces, such as: \fB'wild +^music .mp3$ sbtrkt !rmx\fR .SS Exact-match (quoted) A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as @@ -388,11 +389,10 @@ with the given string. An anchored-match term is also an exact-match term. If a term is prefixed by \fB!\fR, fzf will exclude the items that satisfy the term from the result. -.SS Extended-exact mode +.SS Exact-match by default If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with -\fB'\fR) every word, start fzf with \fB-e\fR or \fB--extended-exact\fR option -(instead of \fB-x\fR or \fB--extended\fR). Note that in \fB--extended-exact\fR -mode, \fB'\fR-prefix "unquotes" the term. +\fB'\fR) every word, start fzf with \fB-e\fR or \fB--exact\fR option. Note that +when \fB--exact\fR is set, \fB'\fR-prefix "unquotes" the term. .SH AUTHOR Junegunn Choi (\fIjunegunn.c@gmail.com\fR) diff --git a/shell/completion.bash b/shell/completion.bash index 0c20383..c8a634d 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -22,7 +22,7 @@ _fzf_opts_completion() { prev="${COMP_WORDS[COMP_CWORD-1]}" opts=" -x --extended - -e --extended-exact + -e --exact -i +i -n --nth -d --delimiter diff --git a/src/core.go b/src/core.go index 35d7ced..becaed4 100644 --- a/src/core.go +++ b/src/core.go @@ -143,7 +143,7 @@ func Run(opts *Options) { // Matcher patternBuilder := func(runes []rune) *Pattern { return BuildPattern( - opts.Mode, opts.Case, opts.Tiebreak != byEnd, + opts.Fuzzy, opts.Extended, opts.Case, opts.Tiebreak != byEnd, opts.Nth, opts.Delimiter, runes) } matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) diff --git a/src/options.go b/src/options.go index 16de221..42b27f3 100644 --- a/src/options.go +++ b/src/options.go @@ -16,7 +16,8 @@ const usage = `usage: fzf [options] Search -x, --extended Extended-search mode - -e, --extended-exact Extended-search mode (exact match) + (enabled by default; +x or --no-extended to disable) + -e, --exact Enable Exact-match -i Case-insensitive match (default: smart-case match) +i Case-sensitive match -n, --nth=N[,..] Comma-separated list of field index expressions @@ -58,20 +59,10 @@ const usage = `usage: fzf [options] Environment variables FZF_DEFAULT_COMMAND Default command to use when input is tty - FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m') + FZF_DEFAULT_OPTS Defaults options. (e.g. '--reverse --inline-info') ` -// Mode denotes the current search mode -type Mode int - -// Search modes -const ( - ModeFuzzy Mode = iota - ModeExtended - ModeExtendedExact -) - // Case denotes case-sensitivity of search type Case int @@ -98,7 +89,8 @@ func defaultMargin() [4]string { // Options stores the values of command-line options type Options struct { - Mode Mode + Fuzzy bool + Extended bool Case Case Nth []Range WithNth []Range @@ -143,7 +135,8 @@ func defaultTheme() *curses.ColorTheme { func defaultOptions() *Options { return &Options{ - Mode: ModeFuzzy, + Fuzzy: true, + Extended: true, Case: CaseSmart, Nth: make([]Range, 0), WithNth: make([]Range, 0), @@ -684,11 +677,17 @@ func parseOptions(opts *Options, allArgs []string) { case "-h", "--help": help(exitOk) case "-x", "--extended": - opts.Mode = ModeExtended - case "-e", "--extended-exact": - opts.Mode = ModeExtendedExact - case "+x", "--no-extended", "+e", "--no-extended-exact": - opts.Mode = ModeFuzzy + opts.Extended = true + case "-e", "--exact": + opts.Fuzzy = false + case "--extended-exact": + // Note that we now don't have --no-extended-exact + opts.Fuzzy = false + opts.Extended = true + case "+x", "--no-extended": + opts.Extended = false + case "+e", "--no-exact": + opts.Fuzzy = true case "-q", "--query": opts.Query = nextString(allArgs, &i, "query string required") case "-f", "--filter": @@ -873,7 +872,7 @@ func parseOptions(opts *Options, allArgs []string) { // If we're not using extended search mode, --nth option becomes irrelevant // if it contains the whole range - if opts.Mode == ModeFuzzy || len(opts.Nth) == 1 { + if !opts.Extended || len(opts.Nth) == 1 { for _, r := range opts.Nth { if r.begin == rangeEllipsis && r.end == rangeEllipsis { opts.Nth = make([]Range, 0) diff --git a/src/options_test.go b/src/options_test.go index 1e9ede4..ef86abe 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -100,7 +100,7 @@ func TestIrrelevantNth(t *testing.T) { t.Errorf("nth should be empty: %s", opts.Nth) } } - for _, words := range [][]string{[]string{"--nth", "..,3"}, []string{"--nth", "3,1.."}, []string{"--nth", "..-1,1"}} { + for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} { { opts := defaultOptions() parseOptions(opts, words) diff --git a/src/pattern.go b/src/pattern.go index f5dd8a7..7c81ea0 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -38,7 +38,8 @@ type term struct { // Pattern represents search pattern type Pattern struct { - mode Mode + fuzzy bool + extended bool caseSensitive bool forward bool text []rune @@ -63,7 +64,7 @@ func init() { func clearPatternCache() { // We can uniquely identify the pattern for a given string since - // mode and caseMode do not change while the program is running + // search mode and caseMode do not change while the program is running _patternCache = make(map[string]*Pattern) } @@ -72,14 +73,13 @@ func clearChunkCache() { } // BuildPattern builds Pattern object from the given arguments -func BuildPattern(mode Mode, caseMode Case, forward bool, +func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { var asString string - switch mode { - case ModeExtended, ModeExtendedExact: + if extended { asString = strings.Trim(string(runes), " ") - default: + } else { asString = string(runes) } @@ -91,15 +91,14 @@ func BuildPattern(mode Mode, caseMode Case, forward bool, caseSensitive, hasInvTerm := true, false terms := []term{} - switch mode { - case ModeExtended, ModeExtendedExact: - terms = parseTerms(mode, caseMode, asString) + if extended { + terms = parseTerms(fuzzy, caseMode, asString) for _, term := range terms { if term.inv { hasInvTerm = true } } - default: + } else { lowerString := strings.ToLower(asString) caseSensitive = caseMode == CaseRespect || caseMode == CaseSmart && lowerString != asString @@ -109,7 +108,8 @@ func BuildPattern(mode Mode, caseMode Case, forward bool, } ptr := &Pattern{ - mode: mode, + fuzzy: fuzzy, + extended: extended, caseSensitive: caseSensitive, forward: forward, text: []rune(asString), @@ -129,7 +129,7 @@ func BuildPattern(mode Mode, caseMode Case, forward bool, return ptr } -func parseTerms(mode Mode, caseMode Case, str string) []term { +func parseTerms(fuzzy bool, caseMode Case, str string) []term { tokens := _splitRegex.Split(str, -1) terms := []term{} for _, token := range tokens { @@ -141,7 +141,7 @@ func parseTerms(mode Mode, caseMode Case, str string) []term { text = lowerText } origText := []rune(text) - if mode == ModeExtendedExact { + if !fuzzy { typ = termExact } @@ -151,10 +151,11 @@ func parseTerms(mode Mode, caseMode Case, str string) []term { } if strings.HasPrefix(text, "'") { - if mode == ModeExtended { + // Flip exactness + if fuzzy { typ = termExact text = text[1:] - } else if mode == ModeExtendedExact { + } else { typ = termFuzzy text = text[1:] } @@ -185,7 +186,7 @@ func parseTerms(mode Mode, caseMode Case, str string) []term { // IsEmpty returns true if the pattern is effectively empty func (p *Pattern) IsEmpty() bool { - if p.mode == ModeFuzzy { + if !p.extended { return len(p.text) == 0 } return len(p.terms) == 0 @@ -198,7 +199,7 @@ func (p *Pattern) AsString() string { // CacheKey is used to build string to be used as the key of result cache func (p *Pattern) CacheKey() string { - if p.mode == ModeFuzzy { + if !p.extended { return p.AsString() } cacheableTerms := []string{} @@ -250,9 +251,9 @@ Loop: func (p *Pattern) matchChunk(chunk *Chunk) []*Item { matches := []*Item{} - if p.mode == ModeFuzzy { + if !p.extended { for _, item := range *chunk { - if sidx, eidx, tlen := p.fuzzyMatch(item); sidx >= 0 { + if sidx, eidx, tlen := p.basicMatch(item); sidx >= 0 { matches = append(matches, dupItem(item, []Offset{Offset{int32(sidx), int32(eidx), int32(tlen)}})) } @@ -269,8 +270,8 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item { // MatchItem returns true if the Item is a match func (p *Pattern) MatchItem(item *Item) bool { - if p.mode == ModeFuzzy { - sidx, _, _ := p.fuzzyMatch(item) + if !p.extended { + sidx, _, _ := p.basicMatch(item) return sidx >= 0 } offsets := p.extendedMatch(item) @@ -289,9 +290,12 @@ func dupItem(item *Item, offsets []Offset) *Item { rank: Rank{0, 0, item.index}} } -func (p *Pattern) fuzzyMatch(item *Item) (int, int, int) { +func (p *Pattern) basicMatch(item *Item) (int, int, int) { input := p.prepareInput(item) - return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) + if p.fuzzy { + return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) + } + return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text) } func (p *Pattern) extendedMatch(item *Item) []Offset { diff --git a/src/pattern_test.go b/src/pattern_test.go index d508612..8b41a69 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -8,7 +8,7 @@ import ( ) func TestParseTermsExtended(t *testing.T) { - terms := parseTerms(ModeExtended, CaseSmart, + terms := parseTerms(true, CaseSmart, "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ ^iii$") if len(terms) != 9 || terms[0].typ != termFuzzy || terms[0].inv || @@ -33,7 +33,7 @@ func TestParseTermsExtended(t *testing.T) { } func TestParseTermsExtendedExact(t *testing.T) { - terms := parseTerms(ModeExtendedExact, CaseSmart, + terms := parseTerms(false, CaseSmart, "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") if len(terms) != 8 || terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 || @@ -49,7 +49,7 @@ func TestParseTermsExtendedExact(t *testing.T) { } func TestParseTermsEmpty(t *testing.T) { - terms := parseTerms(ModeExtended, CaseSmart, "' $ ^ !' !^ !$") + terms := parseTerms(true, CaseSmart, "' $ ^ !' !^ !$") if len(terms) != 0 { t.Errorf("%s", terms) } @@ -58,7 +58,7 @@ func TestParseTermsEmpty(t *testing.T) { func TestExact(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(ModeExtended, CaseSmart, true, + pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("'abc")) sidx, eidx := algo.ExactMatchNaive( pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.terms[0].text) @@ -70,7 +70,7 @@ func TestExact(t *testing.T) { func TestEqual(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$")) + pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$")) match := func(str string, sidxExpected int, eidxExpected int) { sidx, eidx := algo.EqualMatch( @@ -86,17 +86,17 @@ func TestEqual(t *testing.T) { func TestCaseSensitivity(t *testing.T) { defer clearPatternCache() clearPatternCache() - pat1 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc")) + pat1 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat2 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc")) + pat2 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat3 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc")) + pat3 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat4 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc")) + pat4 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat5 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc")) + pat5 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat6 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc")) + pat6 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc")) if string(pat1.text) != "abc" || pat1.caseSensitive != false || string(pat2.text) != "Abc" || pat2.caseSensitive != true || @@ -109,19 +109,19 @@ func TestCaseSensitivity(t *testing.T) { } func TestOrigTextAndTransformed(t *testing.T) { - pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg")) + pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg")) tokens := Tokenize([]rune("junegunn"), Delimiter{}) trans := Transform(tokens, []Range{Range{1, 1}}) origRunes := []rune("junegunn.choi") - for _, mode := range []Mode{ModeFuzzy, ModeExtended} { + for _, extended := range []bool{false, true} { chunk := Chunk{ &Item{ text: []rune("junegunn"), origText: &origRunes, transformed: trans}, } - pattern.mode = mode + pattern.extended = extended matches := pattern.matchChunk(&chunk) if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" || matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || diff --git a/test/test_go.rb b/test/test_go.rb index 77414ec..50d401c 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -8,7 +8,7 @@ DEFAULT_TIMEOUT = 20 base = File.expand_path('../../', __FILE__) Dir.chdir base -FZF = "#{base}/bin/fzf" +FZF = "FZF_DEFAULT_OPTS= FZF_DEFAULT_COMMAND= #{base}/bin/fzf" class NilClass def include? str @@ -213,7 +213,7 @@ class TestGoFZF < TestBase end def test_fzf_default_command - tmux.send_keys "FZF_DEFAULT_COMMAND='echo hello' #{fzf}", :Enter + tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND='echo hello'"), :Enter tmux.until { |lines| lines.last =~ /^>/ } tmux.send_keys :Enter @@ -904,6 +904,17 @@ class TestGoFZF < TestBase end end + def test_default_extended + assert_equal '100', `seq 100 | #{FZF} -f "1 00$"`.chomp + assert_equal '', `seq 100 | #{FZF} -f "1 00$" +x`.chomp + end + + def test_exact + assert_equal 4, `seq 123 | #{FZF} -f 13`.lines.length + assert_equal 2, `seq 123 | #{FZF} -f 13 -e`.lines.length + assert_equal 4, `seq 123 | #{FZF} -f 13 +e`.lines.length + end + private def writelines path, lines File.unlink path while File.exists? path From 6bddffbca46e3bfdc0d858c65c8e4444dfa5b4b5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 3 Nov 2015 22:59:57 +0900 Subject: [PATCH 038/658] Setup signal handlers before ncurses initialization This prevents fzf from missing SIGWINCH during startup which occasionally happens with fzf-tmux --- CHANGELOG.md | 1 + src/terminal.go | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fea973..cd9a173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG orthogonally controlling "exactness" of search - Fixed not to display non-printable characters - Added `double-click` for `--bind` option +- More robust handling of SIGWINCH 0.10.8 ------ diff --git a/src/terminal.go b/src/terminal.go index 4f8341d..ed79728 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -713,21 +713,6 @@ func executeCommand(template string, current string) { func (t *Terminal) Loop() { <-t.startChan { // Late initialization - t.mutex.Lock() - t.initFunc() - t.calculateMargins() - t.printPrompt() - t.placeCursor() - C.Refresh() - t.printInfo() - t.printHeader() - t.mutex.Unlock() - go func() { - timer := time.NewTimer(initialDelay) - <-timer.C - t.reqBox.Set(reqRefresh, nil) - }() - intChan := make(chan os.Signal, 1) signal.Notify(intChan, os.Interrupt, os.Kill) go func() { @@ -744,6 +729,21 @@ func (t *Terminal) Loop() { } }() + t.mutex.Lock() + t.initFunc() + t.calculateMargins() + t.printPrompt() + t.placeCursor() + C.Refresh() + t.printInfo() + t.printHeader() + t.mutex.Unlock() + go func() { + timer := time.NewTimer(initialDelay) + <-timer.C + t.reqBox.Set(reqRefresh, nil) + }() + // Keep the spinner spinning go func() { for { From 64afff6b9adf366e020fa0f9836154585dae8523 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 3 Nov 2015 23:03:49 +0900 Subject: [PATCH 039/658] 0.10.9 --- install | 4 ++-- src/constants.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/install b/install index 62780f4..b60fc00 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.10.8 pre=1 || - version=0.10.8 pre=0 +[[ "$@" =~ --pre ]] && version=0.10.9 pre=1 || + version=0.10.9 pre=0 auto_completion= key_bindings= diff --git a/src/constants.go b/src/constants.go index 1c99e11..3da03a4 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.10.8" + version = "0.10.9" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 14f0d2035e0268f7d702432f25505695af81d7fa Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 4 Nov 2015 03:13:22 +0900 Subject: [PATCH 040/658] Update Homebrew instructions --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9d0945e..44b5b6a 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,10 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf On OS X, you can use [Homebrew](http://brew.sh/) to install fzf. ```sh -brew reinstall --HEAD fzf +brew install fzf # Install shell extensions -/usr/local/Cellar/fzf/HEAD/install +/usr/local/opt/fzf/install ``` #### Install as Vim plugin @@ -78,7 +78,7 @@ while. Please follow the instruction below depending on the installation method. - git: `cd ~/.fzf && git pull && ./install` -- brew: `brew reinstall --HEAD fzf` +- brew: `brew update; brew reinstall fzf` - vim-plug: `:PlugUpdate fzf` Usage From aaef18295d6625d9445a5c91bf2f64f9b4464f3d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 4 Nov 2015 03:14:38 +0900 Subject: [PATCH 041/658] Update FZF_DEFAULT_COMMAND example --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 44b5b6a..337457b 100644 --- a/README.md +++ b/README.md @@ -132,8 +132,10 @@ start fzf with `-e` or `--exact` option. Note that when `--exact` is set, - `FZF_DEFAULT_COMMAND` - Default command to use when input is tty + - e.g. `export FZF_DEFAULT_COMMAND='ag -g ""'` - `FZF_DEFAULT_OPTS` - - Default options. e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"` + - Default options + - e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"` Examples -------- @@ -335,10 +337,10 @@ filtering: ```sh # Feed the output of ag into fzf -ag -l -g "" | fzf +ag -g "" | fzf # Setting ag as the default source for fzf -export FZF_DEFAULT_COMMAND='ag -l -g ""' +export FZF_DEFAULT_COMMAND='ag -g ""' # Now fzf (w/o pipe) will use ag instead of find fzf From c6328affaede9ff2ba903bc85d69ea6290df2a77 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 4 Nov 2015 03:16:36 +0900 Subject: [PATCH 042/658] Update extended-search mode section of README --- README.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 337457b..23841c6 100644 --- a/README.md +++ b/README.md @@ -108,21 +108,20 @@ vim $(fzf) - Mouse: scroll, click, double-click; shift-click and shift-scroll on multi-select mode -#### Extended-search mode +#### Search syntax -Since 0.10.9, fzf starts in "extended-search mode" by default. +Unless otherwise specified, fzf starts in "extended-search mode" where you can +type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt +!rmx` -In this mode, you can specify multiple patterns delimited by spaces, -such as: `^music .mp3$ sbtrkt !rmx` - -| Token | Description | Match type | -| -------- | -------------------------------- | -------------------- | -| `^music` | Items that start with `music` | prefix-exact-match | -| `.mp3$` | Items that end with `.mp3` | suffix-exact-match | -| `sbtrkt` | Items that match `sbtrkt` | fuzzy-match | -| `!rmx` | Items that do not match `rmx` | inverse-fuzzy-match | -| `'wild` | Items that include `wild` | exact-match (quoted) | -| `!'fire` | Items that do not include `fire` | inverse-exact-match | +| Token | Match type | Description | +| -------- | -------------------- | -------------------------------- | +| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` | +| `^music` | prefix-exact-match | Items that start with `music` | +| `.mp3$` | suffix-exact-match | Items that end with `.mp3` | +| `'wild` | exact-match (quoted) | Items that include `wild` | +| `!rmx` | inverse-fuzzy-match | Items that do not match `rmx` | +| `!'fire` | inverse-exact-match | Items that do not include `fire` | If you don't prefer fuzzy matching and do not wish to "quote" every word, start fzf with `-e` or `--exact` option. Note that when `--exact` is set, From 2c4f71d85b2ac8dab1e270f0648387b9e099c2ec Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 4 Nov 2015 21:55:35 +0900 Subject: [PATCH 043/658] [zsh] fzf-history-widget - update local declaration --- shell/key-bindings.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 27810d3..c429d91 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -36,7 +36,7 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { - local selected restore_no_bang_hist + local selected num if selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER") ); then num=$selected[1] if [ -n "$num" ]; then From c3cf3427b113514279b47349a993db51353a364a Mon Sep 17 00:00:00 2001 From: Jack Danger Canty Date: Tue, 3 Nov 2015 17:52:44 -0800 Subject: [PATCH 044/658] Not relying on exit status for CTRL-R In the case that fzf-tmux returns a user-selected result but with a non-zero exit status (which can happen if a function inside $PS1 returns non-zero) this allows CTRL-R to continue working as expected. Addresses #203 (Tranquility's comment) --- shell/key-bindings.zsh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index c429d91..eccc826 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -37,7 +37,8 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { local selected num - if selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER") ); then + selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER") ) + if [ -n "$selected" ]; then num=$selected[1] if [ -n "$num" ]; then zle vi-fetch-history -n $num From 3ef41845a9820f2abf00f5f9d507553918b39d09 Mon Sep 17 00:00:00 2001 From: Leandro Freitas Date: Fri, 6 Nov 2015 11:22:35 -0200 Subject: [PATCH 045/658] [bash-completion] Add nvim to f_cmds --- shell/completion.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/completion.bash b/shell/completion.bash index c8a634d..60251f9 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -242,7 +242,7 @@ d_cmds="cd pushd rmdir" f_cmds=" awk cat diff diff3 emacs ex file ftp g++ gcc gvim head hg java - javac ld less more mvim patch perl python ruby + javac ld less more mvim nvim patch perl python ruby sed sftp sort source tail tee uniq vi view vim wc" a_cmds=" basename bunzip2 bzip2 chmod chown curl cp dirname du From e99731ea852d1faac5390594cd980708edf826cf Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 8 Nov 2015 00:11:46 +0900 Subject: [PATCH 046/658] [shell] Add FZF_ALT_C_COMMAND for ALT-C (#408) --- shell/key-bindings.bash | 9 +++++---- shell/key-bindings.fish | 6 ++++-- shell/key-bindings.zsh | 7 ++++--- test/test_go.rb | 16 ++++++++++++++++ 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 0bfd50d..e9bea78 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -1,7 +1,7 @@ # Key bindings # ------------ __fzf_select__() { - local cmd="${FZF_CTRL_T_COMMAND:-"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 d -print \ -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" @@ -29,9 +29,10 @@ __fzf_select_tmux__() { } __fzf_cd__() { - local dir - 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" + local cmd dir + cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ + -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" + dir=$(eval "$cmd" | $(__fzfcmd) +m) && printf 'cd %q' "$dir" } __fzf_history__() ( diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index a08df3d..3d3c2a7 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -33,9 +33,11 @@ function fzf_key_bindings end function __fzf_alt_c + set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND " + command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ + -o -type d -print 2> /dev/null | sed 1d | cut -b3-" # Fish hangs if the command before pipe redirects (2> /dev/null) - command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) \ - -prune -o -type d -print 2> /dev/null | sed 1d | cut -b3- | eval (__fzfcmd) +m > $TMPDIR/fzf.result + eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m > $TMPDIR/fzf.result" [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ] and cd (cat $TMPDIR/fzf.result) commandline -f repaint diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index eccc826..29721fc 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -4,7 +4,7 @@ if [[ $- == *i* ]]; then # CTRL-T - Paste the selected file path(s) into the command line __fsel() { - local cmd="${FZF_CTRL_T_COMMAND:-"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 d -print \ -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" @@ -27,8 +27,9 @@ bindkey '^T' fzf-file-widget # ALT-C - cd into the selected directory fzf-cd-widget() { - cd "${$(command \find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \ - -o -type d -print 2> /dev/null | sed 1d | cut -b3- | $(__fzfcmd) +m):-.}" + local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ + -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" + cd "${$(eval "$cmd" | $(__fzfcmd) +m):-.}" zle reset-prompt } zle -N fzf-cd-widget diff --git a/test/test_go.rb b/test/test_go.rb index 50d401c..7143d36 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -981,6 +981,22 @@ module TestShell tmux.until { |lines| lines[-1].end_with?(expected) } end + def test_alt_c_command + set_var 'FZF_ALT_C_COMMAND', 'echo /tmp' + + tmux.prepare + tmux.send_keys 'cd /', :Enter + + tmux.prepare + tmux.send_keys :Escape, :c, pane: 0 + lines = tmux.until(1) { |lines| lines.item_count == 1 } + tmux.send_keys :Enter, pane: 1 + + tmux.prepare + tmux.send_keys :pwd, :Enter + tmux.until { |lines| lines[-1].end_with? '/tmp' } + end + def test_ctrl_r tmux.prepare tmux.send_keys 'echo 1st', :Enter; tmux.prepare From 90d32bd7562be34ee2e6f28b840def0d6017e7e1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Nov 2015 01:48:55 +0900 Subject: [PATCH 047/658] [install] Fix #414 - Respect $ZDOTDIR --- install | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/install b/install index b60fc00..b7ed264 100755 --- a/install +++ b/install @@ -342,7 +342,8 @@ append_line() { echo for shell in bash zsh; do - append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" ~/.${shell}rc "~/.fzf.${shell}" + [ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc + append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" "$dest" "~/.fzf.${shell}" done if [ $key_bindings -eq 1 -a $has_fish -eq 1 ]; then @@ -353,7 +354,7 @@ fi cat << EOF Finished. Restart your shell or reload config file. source ~/.bashrc # bash - source ~/.zshrc # zsh + source ${ZDOTDIR:-~}/.zshrc # zsh EOF [ $has_fish -eq 1 ] && echo " fzf_key_bindings # fish"; cat << EOF From f1cd0e2daf4b0ba8fbe94b334a321c9c287414ad Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Nov 2015 12:06:10 +0900 Subject: [PATCH 048/658] [zsh] Fix #404 - Escape $ in $LBUFFER --- shell/key-bindings.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 29721fc..5817ee8 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -38,7 +38,7 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { local selected num - selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER") ) + selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "${LBUFFER//$/\\$}") ) if [ -n "$selected" ]; then num=$selected[1] if [ -n "$num" ]; then From dbc854d5f470d6a995401b3e6386687e7663cddd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Nov 2015 22:01:40 +0900 Subject: [PATCH 049/658] Handle wide unicode characters in --prompt --- src/terminal.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index ed79728..eb82bd8 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -391,7 +391,7 @@ func (t *Terminal) move(y int, x int, clear bool) { } func (t *Terminal) placeCursor() { - t.move(0, len(t.prompt)+displayWidth(t.input[:t.cx]), false) + t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input[:t.cx]), false) } func (t *Terminal) printPrompt() { @@ -402,7 +402,7 @@ func (t *Terminal) printPrompt() { func (t *Terminal) printInfo() { if t.inlineInfo { - t.move(0, len(t.prompt)+displayWidth(t.input)+1, true) + t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true) if t.reading { C.CPrint(C.ColSpinner, true, " < ") } else { @@ -1023,7 +1023,7 @@ func (t *Terminal) Loop() { my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] { mx -= t.marginInt[3] my -= t.marginInt[0] - mx = util.Constrain(mx-len(t.prompt), 0, len(t.input)) + mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input)) if !t.reverse { my = t.maxHeight() - my - 1 } From a89d8995c3a4544851ae3a40b8fb1f1c16f9535e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Nov 2015 01:42:01 +0900 Subject: [PATCH 050/658] Add execute-multi action Close #413 --- man/man1/fzf.1 | 7 +++++++ src/options.go | 32 ++++++++++++++++++++++++-------- src/terminal.go | 37 ++++++++++++++++++++++++++++--------- test/test_go.rb | 18 ++++++++++++++++++ 4 files changed, 77 insertions(+), 17 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 200464a..50de48e 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -223,6 +223,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBdown\fR \fIctrl-j ctrl-n down\fR \fBend-of-line\fR \fIctrl-e end\fR \fBexecute(...)\fR (see below for the details) + \fBexecute-multi(...)\fR (see below for the details) \fBforward-char\fR \fIctrl-f right\fR \fBforward-word\fR \fIalt-f shift-right\fR \fBignore\fR @@ -276,6 +277,12 @@ This is the special form that frees you from parse errors as it does not expect the closing character. The catch is that it should be the last one in the comma-separated list. .RE + +\fBexecute-multi(...)\fR is an alternative action that executes the command +with the selected entries when multi-select is enabled (\fB--multi\fR). With +this action, \fB{}\fR is replaced with the double-quoted strings of the +selected entries separated by spaces. + .RE .TP .BI "--history=" "HISTORY_FILE" diff --git a/src/options.go b/src/options.go index 42b27f3..d1f8201 100644 --- a/src/options.go +++ b/src/options.go @@ -466,10 +466,13 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b // Backreferences are not supported. // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') executeRegexp = regexp.MustCompile( - "(?s):execute:.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") + "(?s):execute(-multi)?:.*|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") } masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { - return ":execute(" + strings.Repeat(" ", len(src)-10) + ")" + if strings.HasPrefix(src, ":execute-multi") { + return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")" + } + return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")" }) masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) @@ -565,11 +568,18 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b toggleSort = true default: if isExecuteAction(actLower) { - keymap[key] = actExecute - if act[7] == ':' { - execmap[key] = act[8:] + var offset int + if strings.HasPrefix(actLower, "execute-multi") { + keymap[key] = actExecuteMulti + offset = len("execute-multi") } else { - execmap[key] = act[8 : len(act)-1] + keymap[key] = actExecute + offset = len("execute") + } + if act[offset] == ':' { + execmap[key] = act[offset+1:] + } else { + execmap[key] = act[offset+1 : len(act)-1] } } else { errorExit("unknown action: " + act) @@ -580,10 +590,16 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b } func isExecuteAction(str string) bool { - if !strings.HasPrefix(str, "execute") || len(str) < 9 { + if !strings.HasPrefix(str, "execute") || len(str) < len("execute()") { return false } - b := str[7] + b := str[len("execute")] + if strings.HasPrefix(str, "execute-multi") { + if len(str) < len("execute-multi()") { + return false + } + b = str[len("execute-multi")] + } e := str[len(str)-1] if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' || b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") { diff --git a/src/terminal.go b/src/terminal.go index eb82bd8..764459f 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -132,6 +132,7 @@ const ( actPreviousHistory actNextHistory actExecute + actExecuteMulti ) func defaultKeymap() map[int]actionType { @@ -305,18 +306,22 @@ func (t *Terminal) output() bool { found = true } } else { - sels := make([]selectedItem, 0, len(t.selected)) - for _, sel := range t.selected { - sels = append(sels, sel) - } - sort.Sort(byTimeOrder(sels)) - for _, sel := range sels { + for _, sel := range t.sortSelected() { fmt.Println(*sel.text) } } return found } +func (t *Terminal) sortSelected() []selectedItem { + sels := make([]selectedItem, 0, len(t.selected)) + for _, sel := range t.selected { + sels = append(sels, sel) + } + sort.Sort(byTimeOrder(sels)) + return sels +} + func runeWidth(r rune, prefixWidth int) int { if r == '\t' { return 8 - prefixWidth%8 @@ -698,8 +703,12 @@ func keyMatch(key int, event C.Event) bool { return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ } -func executeCommand(template string, current string) { - command := strings.Replace(template, "{}", fmt.Sprintf("%q", current), -1) +func quoteEntry(entry string) string { + return fmt.Sprintf("%q", entry) +} + +func executeCommand(template string, replacement string) { + command := strings.Replace(template, "{}", replacement, -1) cmd := exec.Command("sh", "-c", command) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -858,7 +867,17 @@ func (t *Terminal) Loop() { case actExecute: if t.cy >= 0 && t.cy < t.merger.Length() { item := t.merger.Get(t.cy) - executeCommand(t.execmap[mapkey], item.AsString(t.ansi)) + executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi))) + } + case actExecuteMulti: + if len(t.selected) > 0 { + sels := make([]string, len(t.selected)) + for i, sel := range t.sortSelected() { + sels[i] = quoteEntry(*sel.text) + } + executeCommand(t.execmap[mapkey], strings.Join(sels, " ")) + } else { + return doAction(actExecute, mapkey) } case actInvalid: t.mutex.Unlock() diff --git a/test/test_go.rb b/test/test_go.rb index 7143d36..e3b5274 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -713,6 +713,24 @@ class TestGoFZF < TestBase File.unlink output rescue nil end + def test_execute_multi + output = '/tmp/fzf-test-execute-multi' + opts = %[--multi --bind \\"alt-a:execute-multi(echo '[{}], @{}@' >> #{output})\\"] + tmux.send_keys "seq 100 | #{fzf opts}", :Enter + tmux.until { |lines| lines[-2].include? '100/100' } + tmux.send_keys :Escape, :a + tmux.send_keys :BTab, :BTab, :BTab + tmux.send_keys :Escape, :a + tmux.send_keys :Tab, :Tab + tmux.send_keys :Escape, :a + tmux.send_keys :Enter + readonce + assert_equal ['["1"], @"1"@', '["1" "2" "3"], @"1" "2" "3"@', '["1" "2" "4"], @"1" "2" "4"@'], + File.readlines(output).map(&:chomp) + ensure + File.unlink output rescue nil + end + def test_cycle tmux.send_keys "seq 8 | #{fzf :cycle}", :Enter tmux.until { |lines| lines[-2].include? '8/8' } From e7e86b68f4e6a27cc071cf48530ad6ae2c0c37bb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Nov 2015 00:58:20 +0900 Subject: [PATCH 051/658] Add OR operator Close #412 --- README.md | 8 +++++ man/man1/fzf.1 | 7 ++++ src/pattern.go | 87 +++++++++++++++++++++++++++++---------------- src/pattern_test.go | 78 ++++++++++++++++++++++++++++------------ 4 files changed, 127 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 23841c6..6069836 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,14 @@ If you don't prefer fuzzy matching and do not wish to "quote" every word, start fzf with `-e` or `--exact` option. Note that when `--exact` is set, `'`-prefix "unquotes" the term. +A single bar character term acts as an OR operator. For example, the following +query matches entries that start with `core` and end with either `go`, `rb`, +or `py`. + +``` +^core go$ | rb$ | py$ +``` + #### Environment variables - `FZF_DEFAULT_COMMAND` diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 50de48e..275e659 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -401,6 +401,13 @@ If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with \fB'\fR) every word, start fzf with \fB-e\fR or \fB--exact\fR option. Note that when \fB--exact\fR is set, \fB'\fR-prefix "unquotes" the term. +.SS OR operator +A single bar character term acts as an OR operator. For example, the following +query matches entries that start with \fBcore\fR and end with either \fBgo\fR, +\fBrb\fR, or \fBpy\fR. + +e.g. \fB^core go$ | rb$ | py$\fR + .SH AUTHOR Junegunn Choi (\fIjunegunn.c@gmail.com\fR) diff --git a/src/pattern.go b/src/pattern.go index 7c81ea0..795fbb5 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -36,6 +36,8 @@ type term struct { origText []rune } +type termSet []term + // Pattern represents search pattern type Pattern struct { fuzzy bool @@ -43,8 +45,8 @@ type Pattern struct { caseSensitive bool forward bool text []rune - terms []term - hasInvTerm bool + termSets []termSet + cacheable bool delimiter Delimiter nth []Range procFun map[termType]func(bool, bool, []rune, []rune) (int, int) @@ -88,14 +90,20 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool, return cached } - caseSensitive, hasInvTerm := true, false - terms := []term{} + caseSensitive, cacheable := true, true + termSets := []termSet{} if extended { - terms = parseTerms(fuzzy, caseMode, asString) - for _, term := range terms { - if term.inv { - hasInvTerm = true + termSets = parseTerms(fuzzy, caseMode, asString) + Loop: + for _, termSet := range termSets { + for idx, term := range termSet { + // If the query contains inverse search terms or OR operators, + // we cannot cache the search scope + if idx > 0 || term.inv { + cacheable = false + break Loop + } } } } else { @@ -113,8 +121,8 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool, caseSensitive: caseSensitive, forward: forward, text: []rune(asString), - terms: terms, - hasInvTerm: hasInvTerm, + termSets: termSets, + cacheable: cacheable, nth: nth, delimiter: delimiter, procFun: make(map[termType]func(bool, bool, []rune, []rune) (int, int))} @@ -129,9 +137,11 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool, return ptr } -func parseTerms(fuzzy bool, caseMode Case, str string) []term { +func parseTerms(fuzzy bool, caseMode Case, str string) []termSet { tokens := _splitRegex.Split(str, -1) - terms := []term{} + sets := []termSet{} + set := termSet{} + switchSet := false for _, token := range tokens { typ, inv, text := termFuzzy, false, token lowerText := strings.ToLower(text) @@ -145,6 +155,11 @@ func parseTerms(fuzzy bool, caseMode Case, str string) []term { typ = termExact } + if text == "|" { + switchSet = false + continue + } + if strings.HasPrefix(text, "!") { inv = true text = text[1:] @@ -173,15 +188,23 @@ func parseTerms(fuzzy bool, caseMode Case, str string) []term { } if len(text) > 0 { - terms = append(terms, term{ + if switchSet { + sets = append(sets, set) + set = termSet{} + } + set = append(set, term{ typ: typ, inv: inv, text: []rune(text), caseSensitive: caseSensitive, origText: origText}) + switchSet = true } } - return terms + if len(set) > 0 { + sets = append(sets, set) + } + return sets } // IsEmpty returns true if the pattern is effectively empty @@ -189,7 +212,7 @@ func (p *Pattern) IsEmpty() bool { if !p.extended { return len(p.text) == 0 } - return len(p.terms) == 0 + return len(p.termSets) == 0 } // AsString returns the search query in string type @@ -203,11 +226,10 @@ func (p *Pattern) CacheKey() string { return p.AsString() } cacheableTerms := []string{} - for _, term := range p.terms { - if term.inv { - continue + for _, termSet := range p.termSets { + if len(termSet) == 1 && !termSet[0].inv { + cacheableTerms = append(cacheableTerms, string(termSet[0].origText)) } - cacheableTerms = append(cacheableTerms, string(term.origText)) } return strings.Join(cacheableTerms, " ") } @@ -218,7 +240,7 @@ func (p *Pattern) Match(chunk *Chunk) []*Item { // ChunkCache: Exact match cacheKey := p.CacheKey() - if !p.hasInvTerm { // Because we're excluding Inv-term from cache key + if p.cacheable { if cached, found := _cache.Find(chunk, cacheKey); found { return cached } @@ -243,7 +265,7 @@ Loop: matches := p.matchChunk(space) - if !p.hasInvTerm { + if p.cacheable { _cache.Add(chunk, cacheKey, matches) } return matches @@ -260,7 +282,7 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item { } } else { for _, item := range *chunk { - if offsets := p.extendedMatch(item); len(offsets) == len(p.terms) { + if offsets := p.extendedMatch(item); len(offsets) == len(p.termSets) { matches = append(matches, dupItem(item, offsets)) } } @@ -275,7 +297,7 @@ func (p *Pattern) MatchItem(item *Item) bool { return sidx >= 0 } offsets := p.extendedMatch(item) - return len(offsets) == len(p.terms) + return len(offsets) == len(p.termSets) } func dupItem(item *Item, offsets []Offset) *Item { @@ -301,15 +323,20 @@ func (p *Pattern) basicMatch(item *Item) (int, int, int) { func (p *Pattern) extendedMatch(item *Item) []Offset { input := p.prepareInput(item) offsets := []Offset{} - for _, term := range p.terms { - pfun := p.procFun[term.typ] - if sidx, eidx, tlen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 { - if term.inv { +Loop: + for _, termSet := range p.termSets { + for _, term := range termSet { + pfun := p.procFun[term.typ] + if sidx, eidx, tlen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 { + if term.inv { + break Loop + } + offsets = append(offsets, Offset{int32(sidx), int32(eidx), int32(tlen)}) + break + } else if term.inv { + offsets = append(offsets, Offset{0, 0, 0}) break } - offsets = append(offsets, Offset{int32(sidx), int32(eidx), int32(tlen)}) - } else if term.inv { - offsets = append(offsets, Offset{0, 0, 0}) } } return offsets diff --git a/src/pattern_test.go b/src/pattern_test.go index 8b41a69..6bf571c 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -9,20 +9,25 @@ import ( func TestParseTermsExtended(t *testing.T) { terms := parseTerms(true, CaseSmart, - "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ ^iii$") + "| aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | | zzz$ | !ZZZ |") if len(terms) != 9 || - terms[0].typ != termFuzzy || terms[0].inv || - terms[1].typ != termExact || terms[1].inv || - terms[2].typ != termPrefix || terms[2].inv || - terms[3].typ != termSuffix || terms[3].inv || - terms[4].typ != termFuzzy || !terms[4].inv || - terms[5].typ != termExact || !terms[5].inv || - terms[6].typ != termPrefix || !terms[6].inv || - terms[7].typ != termSuffix || !terms[7].inv || - terms[8].typ != termEqual || terms[8].inv { + terms[0][0].typ != termFuzzy || terms[0][0].inv || + terms[1][0].typ != termExact || terms[1][0].inv || + terms[2][0].typ != termPrefix || terms[2][0].inv || + terms[3][0].typ != termSuffix || terms[3][0].inv || + terms[4][0].typ != termFuzzy || !terms[4][0].inv || + terms[5][0].typ != termExact || !terms[5][0].inv || + terms[6][0].typ != termPrefix || !terms[6][0].inv || + terms[7][0].typ != termSuffix || !terms[7][0].inv || + terms[7][1].typ != termEqual || terms[7][1].inv || + terms[8][0].typ != termPrefix || terms[8][0].inv || + terms[8][1].typ != termExact || terms[8][1].inv || + terms[8][2].typ != termSuffix || terms[8][2].inv || + terms[8][3].typ != termFuzzy || !terms[8][3].inv { t.Errorf("%s", terms) } - for idx, term := range terms { + for idx, termSet := range terms[:8] { + term := termSet[0] if len(term.text) != 3 { t.Errorf("%s", term) } @@ -30,20 +35,25 @@ func TestParseTermsExtended(t *testing.T) { t.Errorf("%s", term) } } + for _, term := range terms[8] { + if len(term.origText) != 4 { + t.Errorf("%s", term) + } + } } func TestParseTermsExtendedExact(t *testing.T) { terms := parseTerms(false, CaseSmart, "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") if len(terms) != 8 || - terms[0].typ != termExact || terms[0].inv || len(terms[0].text) != 3 || - terms[1].typ != termFuzzy || terms[1].inv || len(terms[1].text) != 3 || - terms[2].typ != termPrefix || terms[2].inv || len(terms[2].text) != 3 || - terms[3].typ != termSuffix || terms[3].inv || len(terms[3].text) != 3 || - terms[4].typ != termExact || !terms[4].inv || len(terms[4].text) != 3 || - terms[5].typ != termFuzzy || !terms[5].inv || len(terms[5].text) != 3 || - terms[6].typ != termPrefix || !terms[6].inv || len(terms[6].text) != 3 || - terms[7].typ != termSuffix || !terms[7].inv || len(terms[7].text) != 3 { + terms[0][0].typ != termExact || terms[0][0].inv || len(terms[0][0].text) != 3 || + terms[1][0].typ != termFuzzy || terms[1][0].inv || len(terms[1][0].text) != 3 || + terms[2][0].typ != termPrefix || terms[2][0].inv || len(terms[2][0].text) != 3 || + terms[3][0].typ != termSuffix || terms[3][0].inv || len(terms[3][0].text) != 3 || + terms[4][0].typ != termExact || !terms[4][0].inv || len(terms[4][0].text) != 3 || + terms[5][0].typ != termFuzzy || !terms[5][0].inv || len(terms[5][0].text) != 3 || + terms[6][0].typ != termPrefix || !terms[6][0].inv || len(terms[6][0].text) != 3 || + terms[7][0].typ != termSuffix || !terms[7][0].inv || len(terms[7][0].text) != 3 { t.Errorf("%s", terms) } } @@ -61,9 +71,9 @@ func TestExact(t *testing.T) { pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("'abc")) sidx, eidx := algo.ExactMatchNaive( - pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.terms[0].text) + pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.termSets[0][0].text) if sidx != 7 || eidx != 10 { - t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) + t.Errorf("%s / %d / %d", pattern.termSets, sidx, eidx) } } @@ -74,9 +84,9 @@ func TestEqual(t *testing.T) { match := func(str string, sidxExpected int, eidxExpected int) { sidx, eidx := algo.EqualMatch( - pattern.caseSensitive, pattern.forward, []rune(str), pattern.terms[0].text) + pattern.caseSensitive, pattern.forward, []rune(str), pattern.termSets[0][0].text) if sidx != sidxExpected || eidx != eidxExpected { - t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx) + t.Errorf("%s / %d / %d", pattern.termSets, sidx, eidx) } } match("ABC", -1, -1) @@ -130,3 +140,25 @@ func TestOrigTextAndTransformed(t *testing.T) { } } } + +func TestCacheKey(t *testing.T) { + test := func(extended bool, patStr string, expected string, cacheable bool) { + pat := BuildPattern(true, extended, CaseSmart, true, []Range{}, Delimiter{}, []rune(patStr)) + if pat.CacheKey() != expected { + t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) + } + if pat.cacheable != cacheable { + t.Errorf("Expected: %s, actual: %s (%s)", cacheable, pat.cacheable, patStr) + } + clearPatternCache() + } + test(false, "foo !bar", "foo !bar", true) + test(false, "foo | bar !baz", "foo | bar !baz", true) + test(true, "foo bar baz", "foo bar baz", true) + test(true, "foo !bar", "foo", false) + test(true, "foo !bar baz", "foo baz", false) + test(true, "foo | bar baz", "baz", false) + test(true, "foo | bar | baz", "", false) + test(true, "foo | bar !baz", "", false) + test(true, "| | | foo", "foo", true) +} From 31278bcc6895089c97fc5d038cd1dd99053c3764 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 10 Nov 2015 01:50:41 +0900 Subject: [PATCH 052/658] Fix compatibility issues with OR operator and inverse terms --- src/item.go | 3 +++ src/item_test.go | 9 +++++---- src/pattern.go | 13 ++++++++----- test/test_go.rb | 6 ++++++ 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/item.go b/src/item.go index f2f105a..5ce25c7 100644 --- a/src/item.go +++ b/src/item.go @@ -63,6 +63,9 @@ func (item *Item) Rank(cache bool) Rank { matchlen += end - begin } } + if matchlen == 0 { + matchlen = math.MaxUint16 + } var tiebreak uint16 switch rankTiebreak { case byLength: diff --git a/src/item_test.go b/src/item_test.go index 5b9232a..50d6851 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -1,6 +1,7 @@ package fzf import ( + "math" "sort" "testing" @@ -42,7 +43,7 @@ func TestItemRank(t *testing.T) { strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} item1 := Item{text: strs[0], index: 1, offsets: []Offset{}} rank1 := item1.Rank(true) - if rank1.matchlen != 0 || rank1.tiebreak != 3 || rank1.index != 1 { + if rank1.matchlen != math.MaxUint16 || rank1.tiebreak != 3 || rank1.index != 1 { t.Error(item1.Rank(true)) } // Only differ in index @@ -68,9 +69,9 @@ func TestItemRank(t *testing.T) { 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} sort.Sort(ByRelevance(items)) - if items[0] != &item2 || items[1] != &item1 || - items[2] != &item6 || items[3] != &item4 || - items[4] != &item5 || items[5] != &item3 { + if items[0] != &item6 || items[1] != &item4 || + items[2] != &item5 || items[3] != &item3 || + items[4] != &item2 || items[5] != &item1 { t.Error(items) } } diff --git a/src/pattern.go b/src/pattern.go index 795fbb5..2abcf43 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -323,21 +323,24 @@ func (p *Pattern) basicMatch(item *Item) (int, int, int) { func (p *Pattern) extendedMatch(item *Item) []Offset { input := p.prepareInput(item) offsets := []Offset{} -Loop: for _, termSet := range p.termSets { + var offset *Offset for _, term := range termSet { pfun := p.procFun[term.typ] if sidx, eidx, tlen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 { if term.inv { - break Loop + continue } - offsets = append(offsets, Offset{int32(sidx), int32(eidx), int32(tlen)}) + offset = &Offset{int32(sidx), int32(eidx), int32(tlen)} break } else if term.inv { - offsets = append(offsets, Offset{0, 0, 0}) - break + offset = &Offset{0, 0, 0} + continue } } + if offset != nil { + offsets = append(offsets, *offset) + } } return offsets } diff --git a/test/test_go.rb b/test/test_go.rb index e3b5274..e33f047 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -933,6 +933,12 @@ class TestGoFZF < TestBase assert_equal 4, `seq 123 | #{FZF} -f 13 +e`.lines.length end + def test_or_operator + assert_equal %w[1 5 10], `seq 10 | #{FZF} -f "1 | 5"`.lines.map(&:chomp) + assert_equal %w[1 10 2 3 4 5 6 7 8 9], + `seq 10 | #{FZF} -f '1 | !1'`.lines.map(&:chomp) + end + private def writelines path, lines File.unlink path while File.exists? path From df468fc4827402b6abe857c114123aaed7e6636d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 10 Nov 2015 01:54:53 +0900 Subject: [PATCH 053/658] 0.11.0 --- CHANGELOG.md | 9 +++++++++ install | 4 ++-- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd9a173..7381354 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ CHANGELOG ========= +0.11.0 +------ + +- Added OR operator for extended-search mode +- Added `--execute-multi` action +- Fixed incorrect cursor position when unicode wide characters are used in + `--prompt` +- Fixes and improvements in shell extensions + 0.10.9 ------ diff --git a/install b/install index b7ed264..063ae4e 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.10.9 pre=1 || - version=0.10.9 pre=0 +[[ "$@" =~ --pre ]] && version=0.11.0 pre=1 || + version=0.11.0 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 275e659..72d3851 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Nov 2015" "fzf 0.10.9" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Nov 2015" "fzf 0.11.0" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 3da03a4..c5bcea4 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.10.9" + version = "0.11.0" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From d29ae1c4622f69a2e607221220fc4044ce2644e2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 12 Nov 2015 13:42:56 +0900 Subject: [PATCH 054/658] [install] Add --32 / --64 options Related: #373 --- install | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/install b/install index 063ae4e..f35b347 100755 --- a/install +++ b/install @@ -8,19 +8,22 @@ set -u auto_completion= key_bindings= update_config=1 +binary_arch= help() { cat << EOF usage: $0 [OPTIONS] --help Show this message - --bin Download fzf binary only + --bin Download fzf binary only; Do not generate ~/.fzf.{bash,zsh} --all Download fzf binary and update configuration files to enable key bindings and fuzzy completion --[no-]key-bindings Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C) --[no-]completion Enable/disable fuzzy completion (bash & zsh) --[no-]update-rc Whether or not to update shell configuration files + --32 Download 32-bit binary + --64 Download 64-bit binary EOF } @@ -41,6 +44,8 @@ for opt in $@; do --no-completion) auto_completion=0 ;; --update-rc) update_config=1 ;; --no-update-rc) update_config=0 ;; + --32) binary_arch=386 ;; + --64) binary_arch=amd64 ;; --bin) ;; *) echo "unknown option: $opt" @@ -143,10 +148,10 @@ archi=$(uname -sm) binary_available=1 binary_error="" case "$archi" in - Darwin\ x86_64) download fzf-$version-darwin_amd64 ;; - Darwin\ i*86) download fzf-$version-darwin_386 ;; - Linux\ x86_64) download fzf-$version-linux_amd64 ;; - Linux\ i*86) download fzf-$version-linux_386 ;; + Darwin\ x86_64) download fzf-$version-darwin_${binary_arch:-amd64} ;; + Darwin\ i*86) download fzf-$version-darwin_${binary_arch:-386} ;; + Linux\ x86_64) download fzf-$version-linux_${binary_arch:-amd64} ;; + Linux\ i*86) download fzf-$version-linux_${binary_arch:-386} ;; *) binary_available=0 binary_error=1 ;; esac From 57a37b5832061b5b9c1f04a96af6d36345fc8618 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 12 Nov 2015 13:47:52 +0900 Subject: [PATCH 055/658] [bash-completion] Fix #417 - Update command list --- README.md | 11 +++++++++++ shell/completion.bash | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6069836..567271c 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,17 @@ export FZF_COMPLETION_TRIGGER='~~' export FZF_COMPLETION_OPTS='+c -x' ``` +#### Supported commands + +On bash, fuzzy completion is enabled only for a predefined set of commands +(`complete | grep _fzf` to see the list). But you can enable it for other +commands as well like follows. + +```sh +# There are also _fzf_path_completion and _fzf_dir_completion +complete -F _fzf_file_completion -o default -o bashdefault doge +``` + Usage as Vim plugin ------------------- diff --git a/shell/completion.bash b/shell/completion.bash index 60251f9..f40e25e 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -241,9 +241,9 @@ complete -o default -F _fzf_opts_completion fzf d_cmds="cd pushd rmdir" f_cmds=" awk cat diff diff3 - emacs ex file ftp g++ gcc gvim head hg java + emacs emacsclient ex file ftp g++ gcc gvim head hg java javac ld less more mvim nvim patch perl python ruby - sed sftp sort source tail tee uniq vi view vim wc" + sed sftp sort source tail tee uniq vi view vim wc xdg-open" a_cmds=" basename bunzip2 bzip2 chmod chown curl cp dirname du find git grep gunzip gzip hg jar From 95c69083c7d2e2affce4cacf2467a7865425e167 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 17 Nov 2015 18:51:29 +0100 Subject: [PATCH 056/658] zsh completion: use \grep to skip any alias --- shell/completion.zsh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index d203498..a2b07ce 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -21,7 +21,7 @@ __fzf_generic_path_completion() { tail=$6 [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" - if ! setopt | grep nonomatch > /dev/null; then + if ! setopt | \grep nonomatch > /dev/null; then nnm=1 setopt nonomatch fi @@ -117,7 +117,7 @@ _fzf_complete_unalias() { fzf-completion() { local tokens cmd prefix trigger tail fzf matches lbuf d_cmds sws - if setopt | grep shwordsplit > /dev/null; then + if setopt | \grep shwordsplit > /dev/null; then sws=1 unsetopt shwordsplit fi @@ -167,7 +167,7 @@ fzf-completion() { } [ -z "$fzf_default_completion" ] && - fzf_default_completion=$(bindkey '^I' | grep -v undefined-key | awk '{print $2}') + fzf_default_completion=$(bindkey '^I' | \grep -v undefined-key | awk '{print $2}') zle -N fzf-completion bindkey '^I' fzf-completion From 701687faab00833700d3bc81d110119fb7af99da Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 18 Nov 2015 16:09:00 +0100 Subject: [PATCH 057/658] zsh: fzf-completion: use noshwordsplit local option This also fixes the completion causing a bell / flickering in case "shwordsplit" was not set, because then the function would return false. --- shell/completion.zsh | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index a2b07ce..76ca801 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -116,11 +116,8 @@ _fzf_complete_unalias() { } fzf-completion() { - local tokens cmd prefix trigger tail fzf matches lbuf d_cmds sws - if setopt | \grep shwordsplit > /dev/null; then - sws=1 - unsetopt shwordsplit - fi + local tokens cmd prefix trigger tail fzf matches lbuf d_cmds + setopt localoptions noshwordsplit # http://zsh.sourceforge.net/FAQ/zshfaq03.html # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags @@ -163,7 +160,6 @@ fzf-completion() { else eval "zle ${fzf_default_completion:-expand-or-complete}" fi - [ -n "$sws" ] && setopt shwordsplit } [ -z "$fzf_default_completion" ] && @@ -171,4 +167,3 @@ fzf-completion() { zle -N fzf-completion bindkey '^I' fzf-completion - From 1c202555048a399ed21eeada0e81f81d5a999edf Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 19 Nov 2015 09:58:07 +0900 Subject: [PATCH 058/658] Fix typos in help message Close #425. Thanks to @blueyed. --- fzf | 4 ++-- src/options.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fzf b/fzf index 17f86b5..62893af 100755 --- a/fzf +++ b/fzf @@ -370,7 +370,7 @@ class FZF +i Case-sensitive match -n, --nth=N[,..] Comma-separated list of field index expressions 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 the item using index expressions for search -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) @@ -396,7 +396,7 @@ class FZF Environment variables FZF_DEFAULT_COMMAND Default command to use when input is tty - FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000")] + $/ + $/ + FZF_DEFAULT_OPTS Default options (e.g. "-x -m --sort 10000")] + $/ + $/ exit x end diff --git a/src/options.go b/src/options.go index d1f8201..169b105 100644 --- a/src/options.go +++ b/src/options.go @@ -22,7 +22,7 @@ const usage = `usage: fzf [options] +i Case-sensitive match -n, --nth=N[,..] Comma-separated list of field index expressions 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 -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) +s, --no-sort Do not sort the result @@ -59,7 +59,7 @@ const usage = `usage: fzf [options] Environment variables FZF_DEFAULT_COMMAND Default command to use when input is tty - FZF_DEFAULT_OPTS Defaults options. (e.g. '--reverse --inline-info') + FZF_DEFAULT_OPTS Default options (e.g. '--reverse --inline-info') ` From 28ffb9638d7a06b4bfcb87ce6af28c66c2210de3 Mon Sep 17 00:00:00 2001 From: Alex Cornejo Date: Tue, 13 Oct 2015 09:40:40 -0700 Subject: [PATCH 059/658] add android build --- src/Dockerfile.android | 44 ++++++++++++++++++++++++++++++++++++++++++ src/Makefile | 33 +++++++++++++++++++++++++------ src/curses/curses.go | 8 +++++++- 3 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 src/Dockerfile.android diff --git a/src/Dockerfile.android b/src/Dockerfile.android new file mode 100644 index 0000000..540858f --- /dev/null +++ b/src/Dockerfile.android @@ -0,0 +1,44 @@ +FROM ubuntu:14.04 +MAINTAINER Junegunn Choi + +# apt-get +RUN apt-get update && apt-get -y upgrade && \ + apt-get install -y --force-yes git curl build-essential + +# Install Go 1.4 +RUN cd / && curl \ + https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ + tar -xz && mv go go1.4 && \ + sed -i 's@#define PTHREAD_KEYS_MAX 128@@' /go1.4/src/runtime/cgo/gcc_android_arm.c + +ENV GOPATH /go +ENV GOROOT /go1.4 +ENV PATH /go1.4/bin:$PATH + +RUN cd / && \ + curl -O http://dl.google.com/android/ndk/android-ndk-r10e-linux-x86_64.bin && \ + chmod 755 /android-ndk* && /android-ndk-r10e-linux-x86_64.bin && \ + mv android-ndk-r10e /android-ndk + +RUN cd /android-ndk && bash ./build/tools/make-standalone-toolchain.sh --platform=android-21 --install-dir=/ndk --arch=arm + +ENV NDK_CC /ndk/bin/arm-linux-androideabi-gcc + +RUN cd $GOROOT/src && \ + CC_FOR_TARGET=$NDK_CC GOOS=android GOARCH=arm GOARM=7 ./make.bash + +RUN cd / && curl \ + http://ftp.gnu.org/gnu/ncurses/ncurses-5.9.tar.gz | \ + tar -xz && cd /ncurses-5.9 && \ + ./configure CC=$NDK_CC CFLAGS="-fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch" LDFLAGS="-march=armv7-a -Wl,--no-warn-mismatch" --host=arm-linux --enable-overwrite --enable-const --without-cxx-binding --without-shared --without-debug --enable-widec --enable-ext-colors --enable-ext-mouse --enable-pc-files --with-pkg-config-libdir=$PKG_CONFIG_LIBDIR --without-manpages --without-ada --disable-shared --without-tests --prefix=/ndk/sysroot/usr --with-default-terminfo-dirs=/usr/share/terminfo --with-terminfo-dirs=/usr/share/terminfo ac_cv_header_locale_h=n ac_cv_func_getpwent=no ac_cv_func_getpwnam=no ac_cv_func_getpwuid=no && \ + sed -i 's@#define HAVE_LOCALE_H 1@/* #undef HAVE_LOCALE_H */@' include/ncurses_cfg.h && \ + make && \ + sed -i '0,/echo.*/{s/echo.*/exit 0/}' misc/run_tic.sh && \ + make install && \ + mv /ndk/sysroot/usr/lib/libncursesw.a /ndk/sysroot/usr/lib/libncurses.a + +# Volume +VOLUME /go + +# Default CMD +CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash diff --git a/src/Makefile b/src/Makefile index ff40897..6b564fc 100644 --- a/src/Makefile +++ b/src/Makefile @@ -2,12 +2,14 @@ ifndef GOPATH $(error GOPATH is undefined) endif +ifndef GOOS UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) GOOS := darwin else ifeq ($(UNAME_S),Linux) GOOS := linux endif +endif ifneq ($(shell uname -m),x86_64) $(error "Build on $(UNAME_M) is not supported, yet.") @@ -16,11 +18,13 @@ endif SOURCES := $(wildcard *.go */*.go) BINDIR := ../bin -BINARY32 := fzf-$(GOOS)_386 -BINARY64 := fzf-$(GOOS)_amd64 -VERSION = $(shell fzf/$(BINARY64) --version) -RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 -RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 +BINARY32 := fzf-$(GOOS)_386 +BINARY64 := fzf-$(GOOS)_amd64 +BINARYARM7 := fzf-$(GOOS)_arm7 +VERSION := $(shell awk -F= '/version =/ {print $$2}' constants.go | tr -d "\" ") +RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 +RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 +RELEASEARM7 = fzf-$(VERSION)-$(GOOS)_arm7 all: release @@ -31,6 +35,11 @@ release: build build: test fzf/$(BINARY32) fzf/$(BINARY64) +android-build: + cd fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-extldflags=-pie" -o $(BINARYARM7) + cd fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) && \ + rm -f $(RELEASEARM7) + test: go get go test -v ./... @@ -65,6 +74,9 @@ docker-ubuntu: docker-centos: docker build -t junegunn/centos-sandbox - < Dockerfile.centos +docker-android: + docker build -t junegunn/android-sandbox - < Dockerfile.android + arch: docker-arch docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' @@ -81,5 +93,14 @@ linux: docker-centos docker run -i -t -v $(GOPATH):/go junegunn/centos-sandbox \ /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make TAGS=static' +ubuntu-android: docker-android + docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ + sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' + +android: docker-android + docker run -i -t -v $(GOPATH):/go junegunn/android-sandbox \ + /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; GOOS=android make android-build' + .PHONY: all build release test install uninstall clean docker \ - linux arch ubuntu centos docker-arch docker-ubuntu docker-centos + linux arch ubuntu centos docker-arch docker-ubuntu docker-centos \ + android-build docker-android ubuntu-android android diff --git a/src/curses/curses.go b/src/curses/curses.go index 8311719..a8d33f6 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -5,6 +5,12 @@ package curses #include #cgo !static LDFLAGS: -lncurses #cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl +#cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch + +SCREEN *c_newterm () { + return newterm(NULL, stderr, stdin); +} + */ import "C" @@ -259,7 +265,7 @@ func Init(theme *ColorTheme, black bool, mouse bool) { } C.setlocale(C.LC_ALL, C.CString("")) - _screen = C.newterm(nil, C.stderr, C.stdin) + _screen = C.c_newterm() if _screen == nil { fmt.Println("Invalid $TERM: " + os.Getenv("TERM")) os.Exit(2) From 7bcf4effa54967448223b5e36a972432c1d076db Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 30 Nov 2015 17:32:16 +0900 Subject: [PATCH 060/658] Fix test failure - use absolute path --- test/test_go.rb | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index e33f047..275e120 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -6,6 +6,7 @@ require 'fileutils' DEFAULT_TIMEOUT = 20 +FILE = File.expand_path(__FILE__) base = File.expand_path('../../', __FILE__) Dir.chdir base FZF = "FZF_DEFAULT_OPTS= FZF_DEFAULT_COMMAND= #{base}/bin/fzf" @@ -803,8 +804,8 @@ class TestGoFZF < TestBase end def test_header - tmux.send_keys "seq 100 | #{fzf "--header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter - header = File.readlines(__FILE__).take(5).map(&:strip) + tmux.send_keys "seq 100 | #{fzf "--header \\\"\\$(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 @@ -812,8 +813,8 @@ class TestGoFZF < TestBase end def test_header_reverse - tmux.send_keys "seq 100 | #{fzf "--header=\\\"\\$(head -5 #{__FILE__})\\\" --reverse"}", :Enter - header = File.readlines(__FILE__).take(5).map(&:strip) + tmux.send_keys "seq 100 | #{fzf "--header=\\\"\\$(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 @@ -821,8 +822,8 @@ class TestGoFZF < TestBase end def test_header_and_header_lines - tmux.send_keys "seq 100 | #{fzf "--header-lines 10 --header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter - header = File.readlines(__FILE__).take(5).map(&:strip) + tmux.send_keys "seq 100 | #{fzf "--header-lines 10 --header \\\"\\$(head -5 #{FILE})\\\""}", :Enter + header = File.readlines(FILE).take(5).map(&:strip) tmux.until do |lines| lines[-2].include?('90/90') && lines[-7...-2].map(&:strip) == header && @@ -831,8 +832,8 @@ class TestGoFZF < TestBase end def test_header_and_header_lines_reverse - tmux.send_keys "seq 100 | #{fzf "--reverse --header-lines 10 --header \\\"\\$(head -5 #{__FILE__})\\\""}", :Enter - header = File.readlines(__FILE__).take(5).map(&:strip) + tmux.send_keys "seq 100 | #{fzf "--reverse --header-lines 10 --header \\\"\\$(head -5 #{FILE})\\\""}", :Enter + header = File.readlines(FILE).take(5).map(&:strip) tmux.until do |lines| lines[1].include?('90/90') && lines[2...7].map(&:strip) == header && From 99ea1056acb622573fbb62c5b7181f3137005b3e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 30 Nov 2015 17:35:03 +0900 Subject: [PATCH 061/658] Add --tabstop option Related: https://github.com/junegunn/fzf.vim/issues/49 --- CHANGELOG.md | 5 +++++ src/options.go | 11 +++++++++++ src/terminal.go | 4 +++- test/test_go.rb | 20 ++++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7381354..b1f4b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +0.11.1 +------ + +- Added `--tabstop=SPACES` option + 0.11.0 ------ diff --git a/src/options.go b/src/options.go index 169b105..ad05213 100644 --- a/src/options.go +++ b/src/options.go @@ -38,6 +38,7 @@ const usage = `usage: fzf [options] --black Use black background --reverse Reverse orientation --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) + --tabstop=SPACES Number of spaces for a tab character (default: 8) --cycle Enable cyclic scroll --no-hscroll Disable horizontal scroll --inline-info Display finder info inline with the query @@ -123,6 +124,7 @@ type Options struct { Header []string HeaderLines int Margin [4]string + Tabstop int Version bool } @@ -169,6 +171,7 @@ func defaultOptions() *Options { Header: make([]string, 0), HeaderLines: 0, Margin: defaultMargin(), + Tabstop: 8, Version: false} } @@ -822,6 +825,8 @@ func parseOptions(opts *Options, allArgs []string) { case "--margin": opts.Margin = parseMargin( nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) + case "--tabstop": + opts.Tabstop = nextInt(allArgs, &i, "tab stop required") case "--version": opts.Version = true default: @@ -861,6 +866,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.HeaderLines = atoi(value) } else if match, value := optString(arg, "--margin="); match { opts.Margin = parseMargin(value) + } else if match, value := optString(arg, "--tabstop="); match { + opts.Tabstop = atoi(value) } else { errorExit("unknown option: " + arg) } @@ -871,6 +878,10 @@ func parseOptions(opts *Options, allArgs []string) { errorExit("header lines must be a non-negative integer") } + if opts.Tabstop < 1 { + errorExit("tab stop must be a positive integer") + } + // Change default actions for CTRL-N / CTRL-P when --history is used if opts.History != nil { if _, prs := keymap[curses.CtrlP]; !prs { diff --git a/src/terminal.go b/src/terminal.go index 764459f..a19f41d 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -80,6 +80,7 @@ func (a byTimeOrder) Less(i, j int) bool { var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} var _runeWidths = make(map[rune]int) +var _tabStop int const ( reqPrompt util.EventType = iota @@ -194,6 +195,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { } else { header = reverseStringArray(opts.Header) } + _tabStop = opts.Tabstop return &Terminal{ inlineInfo: opts.InlineInfo, prompt: opts.Prompt, @@ -324,7 +326,7 @@ func (t *Terminal) sortSelected() []selectedItem { func runeWidth(r rune, prefixWidth int) int { if r == '\t' { - return 8 - prefixWidth%8 + return _tabStop - prefixWidth%_tabStop } else if w, found := _runeWidths[r]; found { return w } else { diff --git a/test/test_go.rb b/test/test_go.rb index 275e120..f36f98b 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -866,6 +866,26 @@ class TestGoFZF < TestBase tmux.send_keys :Enter end + def test_tabstop + writelines tempname, ["f\too\tba\tr\tbaz\tbarfooq\tux"] + { + 1 => '> f oo ba r baz barfooq ux', + 2 => '> f oo ba r baz barfooq ux', + 3 => '> f oo ba r baz barfooq ux', + 4 => '> f oo ba r baz barfooq ux', + 5 => '> f oo ba r baz barfooq ux', + 6 => '> f oo ba r baz barfooq ux', + 7 => '> f oo ba r baz barfooq ux', + 8 => '> f oo ba r baz barfooq ux', + 9 => '> f oo ba r baz barfooq ux', + }.each do |ts, exp| + tmux.prepare + tmux.send_keys %[cat #{tempname} | fzf --tabstop=#{ts}], :Enter + tmux.until { |lines| lines[-3] == exp } + tmux.send_keys :Enter + end + end + def test_with_nth writelines tempname, ['hello world ', 'byebye'] assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1`.chomp From d4e26707c7199aa8e048f94bfd8ceab5fafe42b6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 30 Nov 2015 18:41:53 +0900 Subject: [PATCH 062/658] GO15VENDOREXPERIMENT=1 (#430) --- .gitmodules | 6 ++++++ src/Dockerfile.centos | 11 +++++++++-- src/Makefile | 8 ++++---- src/vendor/github.com/junegunn/go-runewidth | 1 + src/vendor/github.com/junegunn/go-shellwords | 1 + 5 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 .gitmodules create mode 160000 src/vendor/github.com/junegunn/go-runewidth create mode 160000 src/vendor/github.com/junegunn/go-shellwords diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c9f17b4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "src/vendor/github.com/junegunn/go-shellwords"] + path = src/vendor/github.com/junegunn/go-shellwords + url = https://github.com/junegunn/go-shellwords.git +[submodule "src/vendor/github.com/junegunn/go-runewidth"] + path = src/vendor/github.com/junegunn/go-runewidth + url = https://github.com/junegunn/go-runewidth.git diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos index aed4120..5a685cd 100644 --- a/src/Dockerfile.centos +++ b/src/Dockerfile.centos @@ -11,9 +11,16 @@ RUN cd / && curl \ https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ tar -xz && mv go go1.4 +# Install Go 1.5 +RUN cd / && curl \ + https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz | \ + tar -xz && mv go go1.5 + +ENV GO15VENDOREXPERIMENT 1 +ENV GOROOT_BOOTSTRAP /go1.4 +ENV GOROOT /go1.5 ENV GOPATH /go -ENV GOROOT /go1.4 -ENV PATH /go1.4/bin:$PATH +ENV PATH /go1.5/bin:$PATH # For i386 build RUN cd $GOROOT/src && GOARCH=386 ./make.bash diff --git a/src/Makefile b/src/Makefile index 6b564fc..e125283 100644 --- a/src/Makefile +++ b/src/Makefile @@ -41,8 +41,8 @@ android-build: rm -f $(RELEASEARM7) test: - go get - go test -v ./... + GO15VENDOREXPERIMENT=1 go get + GO15VENDOREXPERIMENT=1 SHELL=/bin/sh go test -v ./... install: $(BINDIR)/fzf @@ -53,10 +53,10 @@ clean: cd fzf && rm -f fzf-* fzf/$(BINARY32): $(SOURCES) - cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -o $(BINARY32) + cd fzf && GO15VENDOREXPERIMENT=1 GOARCH=386 CGO_ENABLED=1 go build -a -o $(BINARY32) fzf/$(BINARY64): $(SOURCES) - cd fzf && go build -a -tags "$(TAGS)" -o $(BINARY64) + cd fzf && GO15VENDOREXPERIMENT=1 go build -a -tags "$(TAGS)" -o $(BINARY64) $(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) cp -f fzf/$(BINARY64) $(BINDIR) diff --git a/src/vendor/github.com/junegunn/go-runewidth b/src/vendor/github.com/junegunn/go-runewidth new file mode 160000 index 0000000..63c378b --- /dev/null +++ b/src/vendor/github.com/junegunn/go-runewidth @@ -0,0 +1 @@ +Subproject commit 63c378b851290989b19ca955468386485f118c65 diff --git a/src/vendor/github.com/junegunn/go-shellwords b/src/vendor/github.com/junegunn/go-shellwords new file mode 160000 index 0000000..35d512a --- /dev/null +++ b/src/vendor/github.com/junegunn/go-shellwords @@ -0,0 +1 @@ +Subproject commit 35d512af75e283aae4ca1fc3d44b159ed66189a4 From 248320fa55c75a69a8f479125de68f10ee2311d0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 1 Dec 2015 00:39:45 +0900 Subject: [PATCH 063/658] 0.11.1 --- install | 4 ++-- man/man1/fzf.1 | 5 ++++- src/constants.go | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/install b/install index f35b347..c98dc79 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.11.0 pre=1 || - version=0.11.0 pre=0 +[[ "$@" =~ --pre ]] && version=0.11.1 pre=1 || + version=0.11.1 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 72d3851..7fef2cc 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Nov 2015" "fzf 0.11.0" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Dec 2015" "fzf 0.11.1" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -157,6 +157,9 @@ e.g. \fBfzf --margin 10%\fR \fBfzf --margin 1,5%\fR .RE .TP +.BI "--tabstop=" SPACES +Number of spaces for a tab character (default: 8) +.TP .B "--cycle" Enable cyclic scroll .TP diff --git a/src/constants.go b/src/constants.go index c5bcea4..4a18da5 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.11.0" + version = "0.11.1" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 0b9c4e1e74da7dfca932bd97f004d539aa76537f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 4 Dec 2015 16:40:51 +0900 Subject: [PATCH 064/658] Remove submodules and disable GO15VENDOREXPERIMENT (#430) Having submodules causes vim-plug or other vim plugin managers to clone them with no real benefit to the end-users. There's currently no compelling reason for me to use submodules. --- .gitmodules | 6 ------ src/Makefile | 8 ++++---- src/vendor/github.com/junegunn/go-runewidth | 1 - src/vendor/github.com/junegunn/go-shellwords | 1 - 4 files changed, 4 insertions(+), 12 deletions(-) delete mode 160000 src/vendor/github.com/junegunn/go-runewidth delete mode 160000 src/vendor/github.com/junegunn/go-shellwords diff --git a/.gitmodules b/.gitmodules index c9f17b4..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +0,0 @@ -[submodule "src/vendor/github.com/junegunn/go-shellwords"] - path = src/vendor/github.com/junegunn/go-shellwords - url = https://github.com/junegunn/go-shellwords.git -[submodule "src/vendor/github.com/junegunn/go-runewidth"] - path = src/vendor/github.com/junegunn/go-runewidth - url = https://github.com/junegunn/go-runewidth.git diff --git a/src/Makefile b/src/Makefile index e125283..abdd6ef 100644 --- a/src/Makefile +++ b/src/Makefile @@ -41,8 +41,8 @@ android-build: rm -f $(RELEASEARM7) test: - GO15VENDOREXPERIMENT=1 go get - GO15VENDOREXPERIMENT=1 SHELL=/bin/sh go test -v ./... + go get + SHELL=/bin/sh go test -v ./... install: $(BINDIR)/fzf @@ -53,10 +53,10 @@ clean: cd fzf && rm -f fzf-* fzf/$(BINARY32): $(SOURCES) - cd fzf && GO15VENDOREXPERIMENT=1 GOARCH=386 CGO_ENABLED=1 go build -a -o $(BINARY32) + cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -o $(BINARY32) fzf/$(BINARY64): $(SOURCES) - cd fzf && GO15VENDOREXPERIMENT=1 go build -a -tags "$(TAGS)" -o $(BINARY64) + cd fzf && go build -a -tags "$(TAGS)" -o $(BINARY64) $(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) cp -f fzf/$(BINARY64) $(BINDIR) diff --git a/src/vendor/github.com/junegunn/go-runewidth b/src/vendor/github.com/junegunn/go-runewidth deleted file mode 160000 index 63c378b..0000000 --- a/src/vendor/github.com/junegunn/go-runewidth +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 63c378b851290989b19ca955468386485f118c65 diff --git a/src/vendor/github.com/junegunn/go-shellwords b/src/vendor/github.com/junegunn/go-shellwords deleted file mode 160000 index 35d512a..0000000 --- a/src/vendor/github.com/junegunn/go-shellwords +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 35d512af75e283aae4ca1fc3d44b159ed66189a4 From a1db64e7b1588bc44dccd3b3895a075977f70ce4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 4 Dec 2015 16:47:02 +0900 Subject: [PATCH 065/658] Unset GO15VENDOREXPERIMENT in linux build env (#430) --- src/Dockerfile.centos | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos index 5a685cd..416d547 100644 --- a/src/Dockerfile.centos +++ b/src/Dockerfile.centos @@ -16,7 +16,6 @@ RUN cd / && curl \ https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz | \ tar -xz && mv go go1.5 -ENV GO15VENDOREXPERIMENT 1 ENV GOROOT_BOOTSTRAP /go1.4 ENV GOROOT /go1.5 ENV GOPATH /go From d513a210c6b59106d899c8e3287e24a3ffc5956d Mon Sep 17 00:00:00 2001 From: Pokey Rule Date: Mon, 7 Dec 2015 17:45:22 -0800 Subject: [PATCH 066/658] Support fzf-tmux when zoomed --- bin/fzf-tmux | 18 +++++++++++++++++- plugin/fzf.vim | 8 ++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 1085b96..f882f8c 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -82,11 +82,19 @@ while [ $# -gt 0 ]; do shift done -if [ -z "$TMUX_PANE" ] || tmux list-panes -F '#F' | grep -q Z; then +if [ -z "$TMUX_PANE" ]; then fzf "${args[@]}" exit $? fi +# Handle zoomed tmux pane by moving it to a temp window +if tmux list-panes -F '#F' | grep -q Z; then + zoomed=1 + original_window=$(tmux display-message -p "#{window_id}") + tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - \\\\; do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'") + tmux swap-pane -t $tmp_window \; select-window -t $tmp_window +fi + set -e # Clean up named pipes on exit @@ -97,6 +105,14 @@ fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id" fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id" cleanup() { rm -f $argsf $fifo1 $fifo2 $fifo3 + + # Remove temp window if we were zoomed + if [ -n "$zoomed" ]; then + tmux swap-pane -t $original_window \; \ + select-window -t $original_window \; \ + kill-window -t $tmp_window \; \ + resize-pane -Z + fi } trap cleanup EXIT SIGINT SIGTERM diff --git a/plugin/fzf.vim b/plugin/fzf.vim index fddab51..d810693 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -52,17 +52,13 @@ function! s:fzf_exec() return s:exec endfunction -function! s:tmux_not_zoomed() - return system('tmux list-panes -F "#F"') !~# 'Z' -endfunction - function! s:tmux_enabled() if has('gui_running') return 0 endif if exists('s:tmux') - return s:tmux && s:tmux_not_zoomed() + return s:tmux endif let s:tmux = 0 @@ -70,7 +66,7 @@ function! s:tmux_enabled() let output = system('tmux -V') let s:tmux = !v:shell_error && output >= 'tmux 1.7' endif - return s:tmux && s:tmux_not_zoomed() + return s:tmux endfunction function! s:shellesc(arg) From 32010055e16ec135fe9c04647eb0edb2a8e52657 Mon Sep 17 00:00:00 2001 From: Pokey Rule Date: Tue, 8 Dec 2015 17:33:44 -0800 Subject: [PATCH 067/658] Correct fzf-tmux tmux checking bug --- bin/fzf-tmux | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index f882f8c..3be9b73 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -82,7 +82,7 @@ while [ $# -gt 0 ]; do shift done -if [ -z "$TMUX_PANE" ]; then +if [ -z "$TMUX" ]; then fzf "${args[@]}" exit $? fi From 033afde3b5d07eecce619bec06258fe15babebcb Mon Sep 17 00:00:00 2001 From: Chaoren Lin Date: Thu, 10 Dec 2015 19:14:22 -0800 Subject: [PATCH 068/658] Fix CTRL-T in tmux with non-standard configuration. - Don't assume ~/.fzf.bash exists. - Source the current script for __fzf_select__. - Forward $PATH. --- shell/key-bindings.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index e9bea78..2406850 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -25,7 +25,7 @@ __fzf_select_tmux__() { else height="-l $height" fi - 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__)\"'" + tmux split-window $height "cd $(printf %q "$PWD"); PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") bash -c 'source \"${BASH_SOURCE[0]}\"; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" } __fzf_cd__() { From 54b4b0c56fca309d1f59bae66a32547d03ced916 Mon Sep 17 00:00:00 2001 From: Chaoren Lin Date: Thu, 10 Dec 2015 19:32:19 -0800 Subject: [PATCH 069/658] Dynamically select which __fzf_select__ to use for tmux with bash 4+. Instead of choosing one at initialization, choose the correct one when it's actually called, so that the behavior is correct even after resizing. Bonus fixes for tmux with bash 4+: - No extra space when cancelling CTRL-T. - Fix cursor position problem in vi mode. --- shell/key-bindings.bash | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 2406850..36c36fc 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -28,6 +28,14 @@ __fzf_select_tmux__() { tmux split-window $height "cd $(printf %q "$PWD"); PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") bash -c 'source \"${BASH_SOURCE[0]}\"; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" } +__fzf_select_tmux_auto__() { + if [ ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ]; then + __fzf_select_tmux__ + else + tmux send-keys -t $TMUX_PANE "$(__fzf_select__)" + fi +} + __fzf_cd__() { local cmd dir cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ @@ -50,7 +58,11 @@ __fzf_history__() ( ) __use_tmux=0 -[ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1 +__use_tmux_auto=0 +if [ -n "$TMUX_PANE" ]; then + [ ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1 + [ $BASH_VERSINFO -gt 3 ] && __use_tmux_auto=1 +fi if [ -z "$(set -o | \grep '^vi.*on')" ]; then # Required to refresh the prompt after fzf @@ -58,7 +70,9 @@ if [ -z "$(set -o | \grep '^vi.*on')" ]; then bind '"\e^": history-expand-line' # CTRL-T - Paste the selected file path into the command line - if [ $__use_tmux -eq 1 ]; then + if [ $__use_tmux_auto -eq 1 ]; then + bind -x '"\C-t": "__fzf_select_tmux_auto__"' + elif [ $__use_tmux -eq 1 ]; then bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select_tmux__)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"' else bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select__)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"' @@ -76,7 +90,9 @@ else # CTRL-T - Paste the selected file path into the command line # - FIXME: Selected items are attached to the end regardless of cursor position - if [ $__use_tmux -eq 1 ]; then + if [ $__use_tmux_auto -eq 1 ]; then + bind -x '"\C-t": "__fzf_select_tmux_auto__"' + elif [ $__use_tmux -eq 1 ]; then bind '"\C-t": "\e$a \eddi$(__fzf_select_tmux__)\C-x\C-e\e0P$xa"' else bind '"\C-t": "\e$a \eddi$(__fzf_select__)\C-x\C-e\e0Px$a \C-x\C-r\exa "' @@ -92,6 +108,6 @@ else bind -m vi-command '"\ec": "i\ec"' fi -unset __use_tmux +unset -v __use_tmux __use_tmux_auto fi From b5850ebd4c70cad83cabbe678bf7627758841a12 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 18 Dec 2015 12:19:29 +0900 Subject: [PATCH 070/658] [vim] Open selected file in the current window if it's empty Close #451 --- plugin/fzf.vim | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index d810693..a391e33 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -409,10 +409,16 @@ function! s:cmd_callback(lines) abort augroup END endif try + let empty = empty(expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified let autochdir = &autochdir set noautochdir for item in a:lines - execute cmd s:escape(item) + if empty + execute 'e' s:escape(item) + let empty = 0 + else + execute cmd s:escape(item) + endif if exists('#BufEnter') && isdirectory(item) doautocmd BufEnter endif From a1b60b1d42267726e429118d2f8283ea95b3fd71 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 20 Dec 2015 01:41:18 +0900 Subject: [PATCH 071/658] Fix Travis CI build The size of pseudo-terminal in Travis CI environment can be small --- test/test_go.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index f36f98b..1e13ba3 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -881,7 +881,7 @@ class TestGoFZF < TestBase }.each do |ts, exp| tmux.prepare tmux.send_keys %[cat #{tempname} | fzf --tabstop=#{ts}], :Enter - tmux.until { |lines| lines[-3] == exp } + tmux.until { |lines| exp.start_with? lines[-3].to_s.strip.sub(/\.\.$/, '') } tmux.send_keys :Enter end end From 3b9984379cbd1a7a3ebb911744aca45da7ab3367 Mon Sep 17 00:00:00 2001 From: Kobe Lipkens Date: Fri, 25 Dec 2015 21:05:25 +0100 Subject: [PATCH 072/658] Pass FZF_DEFAULT_OPTS to non-interactive bash instance --- shell/key-bindings.bash | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 36c36fc..beaa5dd 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -25,7 +25,8 @@ __fzf_select_tmux__() { else height="-l $height" fi - tmux split-window $height "cd $(printf %q "$PWD"); PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") bash -c 'source \"${BASH_SOURCE[0]}\"; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" + + tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") bash -c 'source \"${BASH_SOURCE[0]}\"; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" } __fzf_select_tmux_auto__() { From e1dd798482490db6c43158d418973aa6e61659b7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 29 Dec 2015 00:00:20 +0900 Subject: [PATCH 073/658] [bash/zsh-completion] List hidden files as well Close #456 and #457 --- shell/completion.bash | 10 +++++----- shell/completion.zsh | 8 ++++---- test/test_go.rb | 12 ++++++++++++ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index f40e25e..191091d 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -110,9 +110,9 @@ __fzf_generic_path_completion() { if [ -z "$dir" -o -d "$dir" ]; then leftover=${base/#"$dir"} leftover=${leftover/#\/} - [ "$dir" = './' ] && dir='' + [ -z "$dir" ] && dir='.' || dir="${dir/%\//}" tput sc - matches=$(find -L "$dir"* $1 2> /dev/null | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do + matches=$(\find -L "$dir" $1 -a -not -path "$dir" -print 2> /dev/null | sed 's@^\./@@' | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do printf "%q$3 " "$item" done) matches=${matches% } @@ -171,19 +171,19 @@ _fzf_complete() { _fzf_path_completion() { __fzf_generic_path_completion \ - "-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \ + "-name .git -prune -o -name .svn -prune -o ( -type d -o -type f -o -type l )" \ "-m" "" "$@" } _fzf_file_completion() { __fzf_generic_path_completion \ - "-name .git -prune -o -name .svn -prune -o -type f -print -o -type l -print" \ + "-name .git -prune -o -name .svn -prune -o ( -type f -o -type l )" \ "-m" "" "$@" } _fzf_dir_completion() { __fzf_generic_path_completion \ - "-name .git -prune -o -name .svn -prune -o -type d -print" \ + "-name .git -prune -o -name .svn -prune -o -type d" \ "" "/" "$@" } diff --git a/shell/completion.zsh b/shell/completion.zsh index 76ca801..1b2a7dd 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -30,9 +30,9 @@ __fzf_generic_path_completion() { if [ -z "$dir" -o -d ${~dir} ]; then leftover=${base/#"$dir"} leftover=${leftover/#\/} - [ "$dir" = './' ] && dir='' + [ -z "$dir" ] && dir='.' || dir="${dir/%\//}" dir=${~dir} - matches=$(\find -L $dir* ${=find_opts} 2> /dev/null | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do + matches=$(\find -L "$dir" ${=find_opts} -a -not -path "$dir" -print 2> /dev/null | sed 's@^\./@@' | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do printf "%q$suffix " "$item" done) matches=${matches% } @@ -50,13 +50,13 @@ __fzf_generic_path_completion() { _fzf_path_completion() { __fzf_generic_path_completion "$1" "$2" \ - "-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \ + "-name .git -prune -o -name .svn -prune -o ( -type d -o -type f -o -type l )" \ "-m" "" " " } _fzf_dir_completion() { __fzf_generic_path_completion "$1" "$2" \ - "-name .git -prune -o -name .svn -prune -o -type d -print" \ + "-name .git -prune -o -name .svn -prune -o -type d" \ "" "/" "" } diff --git a/test/test_go.rb b/test/test_go.rb index 1e13ba3..c938921 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1071,6 +1071,8 @@ module CompletionTest tmux.prepare tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 tmux.until(1) { |lines| lines.item_count > 0 } + tmux.send_keys ' !d' + tmux.until(1) { |lines| lines[-2].include?(' 2/') } tmux.send_keys :BTab, :BTab tmux.until(1) { |lines| lines[-2].include?('(2)') } tmux.send_keys :Enter @@ -1111,6 +1113,16 @@ module CompletionTest tmux.send_keys 'C-L' lines[-1].end_with?('/tmp/fzf\ test/foobar') end + + # Should include hidden files + (1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" } + tmux.send_keys 'C-u' + tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab, pane: 0 + tmux.until(1) do |lines| + tmux.send_keys 'C-L' + lines[-2].include?('100/') && + lines[-3].include?('/tmp/fzf-test/.hidden-') + end ensure ['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f| FileUtils.rm_rf File.expand_path(f) From 6208fc9cfd96c4e64dd1cc03b947be6ee6a393c8 Mon Sep 17 00:00:00 2001 From: Kobe Lipkens Date: Mon, 28 Dec 2015 21:11:04 +0100 Subject: [PATCH 074/658] Fix autocompletion for absolute paths --- shell/completion.bash | 3 ++- shell/completion.zsh | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 191091d..ef3a965 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -110,7 +110,8 @@ __fzf_generic_path_completion() { if [ -z "$dir" -o -d "$dir" ]; then leftover=${base/#"$dir"} leftover=${leftover/#\/} - [ -z "$dir" ] && dir='.' || dir="${dir/%\//}" + [ -z "$dir" ] && dir='.' + [ "$dir" != "/" ] && dir="${dir/%\//}" tput sc matches=$(\find -L "$dir" $1 -a -not -path "$dir" -print 2> /dev/null | sed 's@^\./@@' | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do printf "%q$3 " "$item" diff --git a/shell/completion.zsh b/shell/completion.zsh index 1b2a7dd..6e2d932 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -30,7 +30,8 @@ __fzf_generic_path_completion() { if [ -z "$dir" -o -d ${~dir} ]; then leftover=${base/#"$dir"} leftover=${leftover/#\/} - [ -z "$dir" ] && dir='.' || dir="${dir/%\//}" + [ -z "$dir" ] && dir='.' + [ "$dir" != "/" ] && dir="${dir/%\//}" dir=${~dir} matches=$(\find -L "$dir" ${=find_opts} -a -not -path "$dir" -print 2> /dev/null | sed 's@^\./@@' | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do printf "%q$suffix " "$item" From 661d06c90aec18d9a84c60ceda943a437b93a1d9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 29 Dec 2015 13:02:16 +0900 Subject: [PATCH 075/658] Add regression test case for #458 --- test/test_go.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/test_go.rb b/test/test_go.rb index c938921..fcf4340 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1129,6 +1129,12 @@ module CompletionTest end end + def test_file_completion_root + tmux.send_keys 'ls /**', :Tab, pane: 0 + tmux.until(1) { |lines| lines.item_count > 0 } + tmux.send_keys :Enter + end + def test_dir_completion tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter tmux.prepare From aa171b45cb679d79a3d6f998b045d663e2525b65 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 5 Jan 2016 02:10:40 +0900 Subject: [PATCH 076/658] Fix ubuntu-android target of Makefile --- src/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Makefile b/src/Makefile index abdd6ef..7eb67f7 100644 --- a/src/Makefile +++ b/src/Makefile @@ -94,7 +94,7 @@ linux: docker-centos /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make TAGS=static' ubuntu-android: docker-android - docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ + docker run -i -t -v $(GOPATH):/go junegunn/android-sandbox \ sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' android: docker-android From 4230b6f3c9af93bc182bc0a15cbaba1509031d53 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 7 Jan 2016 00:32:38 +0900 Subject: [PATCH 077/658] [fzf-tmux] Fix #466 - Make fifos writable by other users --- bin/fzf-tmux | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 3be9b73..dcd9357 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -127,8 +127,8 @@ envs="env TERM=$TERM " [ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" -mkfifo $fifo2 -mkfifo $fifo3 +mkfifo -m o+w $fifo2 +mkfifo -m o+w $fifo3 # Build arguments to fzf opts="" From 2a2c0a0957b9142a9574b8c34931c615f4ab647f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 7 Jan 2016 01:42:03 +0900 Subject: [PATCH 078/658] [fzf-tmux] Turn off remain-on-exit option Related: https://github.com/junegunn/fzf.vim/issues/67 --- bin/fzf-tmux | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index dcd9357..f7371e1 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -141,11 +141,13 @@ done if [ -n "$term" -o -t 0 ]; then cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf tmux set-window-option -q synchronize-panes off \;\ + set-window-option -q remain-on-exit off \;\ split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap else mkfifo $fifo1 cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf tmux set-window-option -q synchronize-panes off \;\ + set-window-option -q remain-on-exit off \;\ split-window $opt "$envs bash $argsf" $swap cat <&0 > $fifo1 & fi From 6e08fe337cf90cd6eb4734000fe6da1d37184014 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 9 Jan 2016 12:08:25 +0900 Subject: [PATCH 079/658] [nvim] setlocal nospell on terminal buffer Close #469. `setlocal nospell` should appear before `setf fzf` to allow customization of the option. --- plugin/fzf.vim | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index a391e33..89f1b21 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -353,6 +353,7 @@ function! s:execute_term(dict, command, temps) endfunction call termopen(a:command, fzf) + setlocal nospell setf fzf startinsert return [] From b18db4733c635705b3aa052efe91472432727c58 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 11 Jan 2016 18:17:13 +0900 Subject: [PATCH 080/658] [vim] Do not restore working directory on unexpected cwd We should not restore the previous working directory if the current directory has changed somehow. This can happen when &autochdir is set. --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 89f1b21..8dc5b8a 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -201,7 +201,7 @@ function! s:pushd(dict) endfunction function! s:popd(dict) - if has_key(a:dict, 'prev_dir') + if has_key(a:dict, 'prev_dir') && getcwd() ==# a:dict.dir execute 'chdir '.s:escape(remove(a:dict, 'prev_dir')) endif endfunction From 0f281ef89410cd581b581fd6087e9edd1832d1e8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 12 Jan 2016 01:15:36 +0900 Subject: [PATCH 081/658] [vim] Try to make 'dir' option compatible with &autochdir When 'dir' option is passed to fzf#run(), the current working directory is temporarily changed to the given directory, and restored at the end. However, this behavior is not compatible with &autochdir. This commit introduces a heuristic to determine whether or not to restore the previous working directory. Related: https://github.com/junegunn/fzf.vim/issues/70 --- plugin/fzf.vim | 47 ++++++++++++++++++++++++++++------------------- test/fzf.vader | 29 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 8dc5b8a..f84edfa 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -1,4 +1,4 @@ -" Copyright (c) 2015 Junegunn Choi +" Copyright (c) 2016 Junegunn Choi " " MIT License " @@ -139,17 +139,13 @@ try let tmux = !has('nvim') && s:tmux_enabled() && s:splittable(dict) let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result - try - if tmux - return s:execute_tmux(dict, command, temps) - elseif has('nvim') - return s:execute_term(dict, command, temps) - else - return s:execute(dict, command, temps) - endif - finally - call s:popd(dict) - endtry + if has('nvim') + return s:execute_term(dict, command, temps) + endif + + let ret = tmux ? s:execute_tmux(dict, command, temps) : s:execute(dict, command, temps) + call s:popd(dict, ret) + return ret finally let &shell = oshell endtry @@ -193,16 +189,28 @@ function! s:pushd(dict) return 1 endif let a:dict.prev_dir = cwd - execute 'chdir '.s:escape(a:dict.dir) + execute 'chdir' s:escape(a:dict.dir) let a:dict.dir = getcwd() return 1 endif return 0 endfunction -function! s:popd(dict) - if has_key(a:dict, 'prev_dir') && getcwd() ==# a:dict.dir - execute 'chdir '.s:escape(remove(a:dict, 'prev_dir')) +function! s:popd(dict, lines) + " Since anything can be done in the sink function, there is no telling that + " the change of the working directory was made by &autochdir setting. + " + " We use the following heuristic to determine whether to restore CWD: + " - Always restore the current directory when &autochdir is disabled. + " FIXME This makes it impossible to change directory from inside the sink + " function when &autochdir is not used. + " - In case of an error or an interrupt, a:lines will be empty. + " And it will be an array of a single empty string when fzf was finished + " without a match. In these cases, we presume that the change of the + " directory is not expected and should be undone. + if has_key(a:dict, 'prev_dir') && + \ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0]))) + execute 'chdir' s:escape(remove(a:dict, 'prev_dir')) endif endfunction @@ -314,7 +322,6 @@ endfunction function! s:execute_term(dict, command, temps) call s:split(a:dict) - call s:pushd(a:dict) let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF' } let s:command = a:command @@ -338,7 +345,7 @@ function! s:execute_term(dict, command, temps) call s:pushd(self.dict) try - call s:callback(self.dict, self.temps) + let ret = s:callback(self.dict, self.temps) if inplace && bufnr('') == self.buf execute "normal! \" @@ -348,11 +355,13 @@ function! s:execute_term(dict, command, temps) endif endif finally - call s:popd(self.dict) + call s:popd(self.dict, ret) endtry endfunction + call s:pushd(a:dict) call termopen(a:command, fzf) + call s:popd(a:dict, []) setlocal nospell setf fzf startinsert diff --git a/test/fzf.vader b/test/fzf.vader index c27fddc..78bc6c5 100644 --- a/test/fzf.vader +++ b/test/fzf.vader @@ -1,6 +1,7 @@ Execute (Setup): let g:dir = fnamemodify(g:vader_file, ':p:h') Log 'Test directory: ' . g:dir + Save &acd Execute (fzf#run with dir option): let cwd = getcwd() @@ -35,6 +36,34 @@ Execute (fzf#run with string source): let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' })) AssertEqual ['hi'], result +Execute (fzf#run with dir option and noautochdir): + set noacd + let cwd = getcwd() + call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'}) + " No change in working directory + AssertEqual cwd, getcwd() + +Execute (Incomplete fzf#run with dir option and autochdir): + set acd + let cwd = getcwd() + call fzf#run({'source': [], 'sink': 'e', 'dir': '/tmp', 'options': '-0'}) + " No change in working directory even if &acd is set + AssertEqual cwd, getcwd() + +Execute (fzf#run with dir option and autochdir): + set acd + let cwd = getcwd() + call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'}) + " Working directory changed due to &acd + AssertEqual '/', getcwd() + +Execute (fzf#run with dir option and autochdir when final cwd is same as dir): + set acd + cd /tmp + call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/', 'options': '-1'}) + " Working directory changed due to &acd + AssertEqual '/', getcwd() + Execute (Cleanup): unlet g:dir Restore From d635b3fd3ca34143b203eacc4308ed35628ac2f8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 13 Jan 2016 02:16:26 +0900 Subject: [PATCH 082/658] Update license: 2016 --- LICENSE | 2 +- man/man1/fzf.1 | 2 +- src/LICENSE | 2 +- src/core.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LICENSE b/LICENSE index fe4c31a..ea43326 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Junegunn Choi +Copyright (c) 2016 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 7fef2cc..416482a 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -1,7 +1,7 @@ .ig The MIT License (MIT) -Copyright (c) 2015 Junegunn Choi +Copyright (c) 2016 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/LICENSE b/src/LICENSE index fe4c31a..ea43326 100644 --- a/src/LICENSE +++ b/src/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Junegunn Choi +Copyright (c) 2016 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/core.go b/src/core.go index becaed4..dcba7ec 100644 --- a/src/core.go +++ b/src/core.go @@ -3,7 +3,7 @@ Package fzf implements fzf, a command-line fuzzy finder. The MIT License (MIT) -Copyright (c) 2015 Junegunn Choi +Copyright (c) 2016 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 1d2d32c847e39818bedae5f86ca75e6b70b60444 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 13 Jan 2016 03:07:42 +0900 Subject: [PATCH 083/658] Accept comma-separated list of sort criteria --- CHANGELOG.md | 9 +++ man/man1/fzf.1 | 15 ++++- src/chunklist_test.go | 12 +++- src/core.go | 22 +++++-- src/item.go | 110 ++++++++++++++++++-------------- src/item_test.go | 29 +++++---- src/merger.go | 2 +- src/merger_test.go | 2 +- src/options.go | 65 +++++++++++++------ src/pattern.go | 2 +- src/terminal.go | 6 +- test/test_go.rb | 144 +++++++++++++++++++++++++++++++++++------- 12 files changed, 298 insertions(+), 120 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1f4b70..87f7827 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ CHANGELOG ========= +0.11.2 +------ + +- `--tiebreak` now accepts comma-separated list of sort criteria. + - Each criterion should appear only once in the list + - `index` is only allowed at the end of the list + - `index` is implicitly appended to the list when not specified + - Default is `length` (or equivalently `length,index`) + 0.11.1 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 416482a..feca972 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Dec 2015" "fzf 0.11.1" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jan 2016" "fzf 0.11.2" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -68,8 +68,8 @@ Reverse the order of the input e.g. \fBhistory | fzf --tac --no-sort\fR .RE .TP -.BI "--tiebreak=" "CRI" -Sort criterion to use when the scores are tied +.BI "--tiebreak=" "CRI[,..]" +Comma-separated list of sort criteria to apply when the scores are tied. .br .R "" .br @@ -81,6 +81,15 @@ Sort criterion to use when the scores are tied .br .BR index " Prefers item that appeared earlier in the input stream" .br +.R "" +.br +- Each criterion should appear only once in the list +.br +- \fBindex\fR is only allowed at the end of the list +.br +- \fBindex\fR is implicitly appended to the list when not specified +.br +- Default is \fBlength\fR (or equivalently \fBlength\fR,index) .SS Interface .TP .B "-m, --multi" diff --git a/src/chunklist_test.go b/src/chunklist_test.go index 26795ef..6ddd336 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -6,8 +6,11 @@ import ( ) func TestChunkList(t *testing.T) { + // FIXME global + sortCriteria = []criterion{byMatchLen, byLength, byIndex} + cl := NewChunkList(func(s []byte, i int) *Item { - return &Item{text: []rune(string(s)), rank: Rank{0, 0, uint32(i * 2)}} + return &Item{text: []rune(string(s)), rank: buildEmptyRank(int32(i * 2))} }) // Snapshot @@ -36,8 +39,11 @@ func TestChunkList(t *testing.T) { if len(*chunk1) != 2 { t.Error("Snapshot should contain only two items") } - if string((*chunk1)[0].text) != "hello" || (*chunk1)[0].rank.index != 0 || - string((*chunk1)[1].text) != "world" || (*chunk1)[1].rank.index != 2 { + last := func(arr []int32) int32 { + return arr[len(arr)-1] + } + if string((*chunk1)[0].text) != "hello" || last((*chunk1)[0].rank) != 0 || + string((*chunk1)[1].text) != "world" || last((*chunk1)[1].rank) != 2 { t.Error("Invalid data") } if chunk1.IsFull() { diff --git a/src/core.go b/src/core.go index dcba7ec..1906c50 100644 --- a/src/core.go +++ b/src/core.go @@ -52,7 +52,7 @@ func Run(opts *Options) { initProcs() sort := opts.Sort > 0 - rankTiebreak = opts.Tiebreak + sortCriteria = opts.Criteria if opts.Version { fmt.Println(version) @@ -103,9 +103,9 @@ func Run(opts *Options) { runes, colors := ansiProcessor(data) return &Item{ text: runes, - index: uint32(index), + index: int32(index), colors: colors, - rank: Rank{0, 0, uint32(index)}} + rank: buildEmptyRank(int32(index))} }) } else { chunkList = NewChunkList(func(data []byte, index int) *Item { @@ -120,9 +120,9 @@ func Run(opts *Options) { item := Item{ text: joinTokens(trans), origText: &runes, - index: uint32(index), + index: int32(index), colors: nil, - rank: Rank{0, 0, uint32(index)}} + rank: buildEmptyRank(int32(index))} trimmed, colors := ansiProcessorRunes(item.text) item.text = trimmed @@ -141,9 +141,19 @@ func Run(opts *Options) { } // Matcher + forward := true + for _, cri := range opts.Criteria[1:] { + if cri == byEnd { + forward = false + break + } + if cri == byBegin { + break + } + } patternBuilder := func(runes []rune) *Pattern { return BuildPattern( - opts.Fuzzy, opts.Extended, opts.Case, opts.Tiebreak != byEnd, + opts.Fuzzy, opts.Extended, opts.Case, forward, opts.Nth, opts.Delimiter, runes) } matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) diff --git a/src/item.go b/src/item.go index 5ce25c7..a4fa609 100644 --- a/src/item.go +++ b/src/item.go @@ -20,25 +20,35 @@ type Item struct { text []rune origText *[]rune transformed []Token - index uint32 + index int32 offsets []Offset colors []ansiOffset - rank Rank + rank []int32 } -// Rank is used to sort the search result -type Rank struct { - matchlen uint16 - tiebreak uint16 - index uint32 +// Sort criteria to use. Never changes once fzf is started. +var sortCriteria []criterion + +func isRankValid(rank []int32) bool { + // Exclude ordinal index + for i := 0; i < len(rank)-1; i++ { + if rank[i] > 0 { + return true + } + } + return false } -// Tiebreak criterion to use. Never changes once fzf is started. -var rankTiebreak tiebreak +func buildEmptyRank(index int32) []int32 { + len := len(sortCriteria) + arr := make([]int32, len) + arr[len-1] = index + return arr +} // Rank calculates rank of the Item -func (item *Item) Rank(cache bool) Rank { - if cache && (item.rank.matchlen > 0 || item.rank.tiebreak > 0) { +func (item *Item) Rank(cache bool) []int32 { + if cache && isRankValid(item.rank) { return item.rank } matchlen := 0 @@ -64,32 +74,37 @@ func (item *Item) Rank(cache bool) Rank { } } if matchlen == 0 { - matchlen = math.MaxUint16 + matchlen = math.MaxInt32 } - var tiebreak uint16 - switch rankTiebreak { - case byLength: - // It is guaranteed that .transformed in not null in normal execution - if item.transformed != nil { - // If offsets is empty, lenSum will be 0, but we don't care - tiebreak = uint16(lenSum) - } else { - tiebreak = uint16(len(item.text)) + rank := make([]int32, len(sortCriteria)) + for idx, criterion := range sortCriteria { + var val int32 + switch criterion { + case byMatchLen: + val = int32(matchlen) + case byLength: + // It is guaranteed that .transformed in not null in normal execution + if item.transformed != nil { + // If offsets is empty, lenSum will be 0, but we don't care + val = int32(lenSum) + } else { + val = int32(len(item.text)) + } + case byBegin: + // We can't just look at item.offsets[0][0] because it can be an inverse term + val = int32(minBegin) + case byEnd: + if prevEnd > 0 { + val = int32(1 + len(item.text) - prevEnd) + } else { + // Empty offsets due to inverse terms. + val = 1 + } + case byIndex: + val = item.index } - case byBegin: - // We can't just look at item.offsets[0][0] because it can be an inverse term - tiebreak = uint16(minBegin) - case byEnd: - if prevEnd > 0 { - tiebreak = uint16(1 + len(item.text) - prevEnd) - } else { - // Empty offsets due to inverse terms. - tiebreak = 1 - } - case byIndex: - tiebreak = 1 + rank[idx] = val } - rank := Rank{uint16(matchlen), tiebreak, item.index} if cache { item.rank = rank } @@ -254,18 +269,19 @@ func (a ByRelevanceTac) Less(i, j int) bool { return compareRanks(irank, jrank, true) } -func compareRanks(irank Rank, jrank Rank, tac bool) bool { - if irank.matchlen < jrank.matchlen { - return true - } else if irank.matchlen > jrank.matchlen { - return false +func compareRanks(irank []int32, jrank []int32, tac bool) bool { + lastIdx := len(irank) - 1 + for idx, left := range irank { + right := jrank[idx] + if tac && idx == lastIdx { + left = left * -1 + right = right * -1 + } + if left < right { + return true + } else if left > right { + return false + } } - - if irank.tiebreak < jrank.tiebreak { - return true - } else if irank.tiebreak > jrank.tiebreak { - return false - } - - return (irank.index <= jrank.index) != tac + return true } diff --git a/src/item_test.go b/src/item_test.go index 50d6851..f26f837 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -23,27 +23,30 @@ func TestOffsetSort(t *testing.T) { } func TestRankComparison(t *testing.T) { - if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, false) || - !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) || - !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, false) || - !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) { + if compareRanks([]int32{3, 0, 5}, []int32{2, 0, 7}, false) || + !compareRanks([]int32{3, 0, 5}, []int32{3, 0, 6}, false) || + !compareRanks([]int32{1, 2, 3}, []int32{1, 3, 2}, false) || + !compareRanks([]int32{0, 0, 0}, []int32{0, 0, 0}, false) { t.Error("Invalid order") } - if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, true) || - !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) || - !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, true) || - !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) { + if compareRanks([]int32{3, 0, 5}, []int32{2, 0, 7}, true) || + !compareRanks([]int32{3, 0, 5}, []int32{3, 0, 6}, false) || + !compareRanks([]int32{1, 2, 3}, []int32{1, 3, 2}, true) || + !compareRanks([]int32{0, 0, 0}, []int32{0, 0, 0}, false) { t.Error("Invalid order (tac)") } } // Match length, string length, index func TestItemRank(t *testing.T) { + // FIXME global + sortCriteria = []criterion{byMatchLen, byLength, byIndex} + strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} item1 := Item{text: strs[0], index: 1, offsets: []Offset{}} rank1 := item1.Rank(true) - if rank1.matchlen != math.MaxUint16 || rank1.tiebreak != 3 || rank1.index != 1 { + if rank1[0] != math.MaxInt32 || rank1[1] != 3 || rank1[2] != 1 { t.Error(item1.Rank(true)) } // Only differ in index @@ -63,10 +66,10 @@ func TestItemRank(t *testing.T) { } // Sort by relevance - 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}}} - 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}}} + item3 := Item{text: strs[1], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item4 := Item{text: strs[1], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + item5 := Item{text: strs[2], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item6 := Item{text: strs[2], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6} sort.Sort(ByRelevance(items)) if items[0] != &item6 || items[1] != &item4 || diff --git a/src/merger.go b/src/merger.go index cce8a94..26ed17b 100644 --- a/src/merger.go +++ b/src/merger.go @@ -88,7 +88,7 @@ func (mg *Merger) cacheable() bool { func (mg *Merger) mergedGet(idx int) *Item { for i := len(mg.merged); i <= idx; i++ { - minRank := Rank{0, 0, 0} + minRank := buildEmptyRank(0) minIdx := -1 for listIdx, list := range mg.lists { cursor := mg.cursors[listIdx] diff --git a/src/merger_test.go b/src/merger_test.go index b7a2993..34efc84 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -23,7 +23,7 @@ func randItem() *Item { } return &Item{ text: []rune(str), - index: rand.Uint32(), + index: rand.Int31(), offsets: offsets} } diff --git a/src/options.go b/src/options.go index ad05213..30e0016 100644 --- a/src/options.go +++ b/src/options.go @@ -27,7 +27,8 @@ const usage = `usage: fzf [options] -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) +s, --no-sort Do not sort the result --tac Reverse the order of the input - --tiebreak=CRITERION Sort criterion when the scores are tied; + --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply + when the scores are tied; [length|begin|end|index] (default: length) Interface @@ -75,10 +76,11 @@ const ( ) // Sort criteria -type tiebreak int +type criterion int const ( - byLength tiebreak = iota + byMatchLen criterion = iota + byLength byBegin byEnd byIndex @@ -98,7 +100,7 @@ type Options struct { Delimiter Delimiter Sort int Tac bool - Tiebreak tiebreak + Criteria []criterion Multi bool Ansi bool Mouse bool @@ -145,7 +147,7 @@ func defaultOptions() *Options { Delimiter: Delimiter{}, Sort: 1000, Tac: false, - Tiebreak: byLength, + Criteria: []criterion{byMatchLen, byLength, byIndex}, Multi: false, Ansi: false, Mouse: true, @@ -361,20 +363,43 @@ func parseKeyChords(str string, message string) map[int]string { return chords } -func parseTiebreak(str string) tiebreak { - switch strings.ToLower(str) { - case "length": - return byLength - case "index": - return byIndex - case "begin": - return byBegin - case "end": - return byEnd - default: - errorExit("invalid sort criterion: " + str) +func parseTiebreak(str string) []criterion { + criteria := []criterion{byMatchLen} + hasIndex := false + hasLength := false + hasBegin := false + hasEnd := false + check := func(notExpected *bool, name string) { + if *notExpected { + errorExit("duplicate sort criteria: " + name) + } + if hasIndex { + errorExit("index should be the last criterion") + } + *notExpected = true } - return byLength + for _, str := range strings.Split(strings.ToLower(str), ",") { + switch str { + case "index": + check(&hasIndex, "index") + criteria = append(criteria, byIndex) + case "length": + check(&hasLength, "length") + criteria = append(criteria, byLength) + case "begin": + check(&hasBegin, "begin") + criteria = append(criteria, byBegin) + case "end": + check(&hasEnd, "end") + criteria = append(criteria, byEnd) + default: + errorExit("invalid sort criterion: " + str) + } + } + if !hasIndex { + criteria = append(criteria, byIndex) + } + return criteria } func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { @@ -715,7 +740,7 @@ func parseOptions(opts *Options, allArgs []string) { case "--expect": opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") case "--tiebreak": - opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) + opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) case "--bind": keymap, opts.Execmap, opts.ToggleSort = parseKeymap(keymap, opts.Execmap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required")) @@ -850,7 +875,7 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, value := optString(arg, "--expect="); match { opts.Expect = parseKeyChords(value, "key names required") } else if match, value := optString(arg, "--tiebreak="); match { - opts.Tiebreak = parseTiebreak(value) + opts.Criteria = parseTiebreak(value) } else if match, value := optString(arg, "--color="); match { opts.Theme = parseTheme(opts.Theme, value) } else if match, value := optString(arg, "--bind="); match { diff --git a/src/pattern.go b/src/pattern.go index 2abcf43..4c61b87 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -309,7 +309,7 @@ func dupItem(item *Item, offsets []Offset) *Item { index: item.index, offsets: offsets, colors: item.colors, - rank: Rank{0, 0, item.index}} + rank: buildEmptyRank(item.index)} } func (p *Pattern) basicMatch(item *Item) (int, int, int) { diff --git a/src/terminal.go b/src/terminal.go index a19f41d..c9b8056 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -50,7 +50,7 @@ type Terminal struct { progress int reading bool merger *Merger - selected map[uint32]selectedItem + selected map[int32]selectedItem reqBox *util.EventBox eventBox *util.EventBox mutex sync.Mutex @@ -223,7 +223,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { ansi: opts.Ansi, reading: true, merger: EmptyMerger, - selected: make(map[uint32]selectedItem), + selected: make(map[int32]selectedItem), reqBox: util.NewEventBox(), eventBox: eventBox, mutex: sync.Mutex{}, @@ -466,7 +466,7 @@ func (t *Terminal) printHeader() { text: []rune(trimmed), index: 0, colors: colors, - rank: Rank{0, 0, 0}} + rank: buildEmptyRank(0)} t.move(line, 2, true) t.printHighlighted(item, false, C.ColHeader, 0, false) diff --git a/test/test_go.rb b/test/test_go.rb index fcf4340..85e3d83 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -459,8 +459,8 @@ class TestGoFZF < TestBase def test_unicode_case writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4] - assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/) - assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/) + assert_equal %w[СТРОКА2 Строка4], `#{FZF} -fС < #{tempname}`.split($/) + assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `#{FZF} -fс < #{tempname}`.split($/) end def test_tiebreak @@ -472,7 +472,7 @@ class TestGoFZF < TestBase ] writelines tempname, input - assert_equal input, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=index`.split($/) + assert_equal input, `#{FZF} -ffoobar --tiebreak=index < #{tempname}`.split($/) by_length = %w[ ----foobar-- @@ -480,8 +480,8 @@ class TestGoFZF < TestBase -------foobar- --foobar-------- ] - assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar`.split($/) - assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=length`.split($/) + assert_equal by_length, `#{FZF} -ffoobar < #{tempname}`.split($/) + assert_equal by_length, `#{FZF} -ffoobar --tiebreak=length < #{tempname}`.split($/) by_begin = %w[ --foobar-------- @@ -489,17 +489,117 @@ class TestGoFZF < TestBase -----foobar--- -------foobar- ] - assert_equal by_begin, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=begin`.split($/) - assert_equal by_begin, `cat #{tempname} | #{FZF} -f"!z foobar" -x --tiebreak begin`.split($/) + assert_equal by_begin, `#{FZF} -ffoobar --tiebreak=begin < #{tempname}`.split($/) + assert_equal by_begin, `#{FZF} -f"!z foobar" -x --tiebreak begin < #{tempname}`.split($/) assert_equal %w[ -------foobar- ----foobar-- -----foobar--- --foobar-------- - ], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/) + ], `#{FZF} -ffoobar --tiebreak end < #{tempname}`.split($/) - assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/) + assert_equal input, `#{FZF} -f"!z" -x --tiebreak end < #{tempname}`.split($/) + end + + # Since 0.11.2 + def test_tiebreak_list + input = %w[ + f-o-o-b-a-r + foobar---- + --foobar + ----foobar + foobar-- + --foobar-- + foobar + ] + writelines tempname, input + + assert_equal %w[ + foobar---- + --foobar + ----foobar + foobar-- + --foobar-- + foobar + f-o-o-b-a-r + ], `#{FZF} -ffb --tiebreak=index < #{tempname}`.split($/) + + by_length = %w[ + foobar + --foobar + foobar-- + foobar---- + ----foobar + --foobar-- + f-o-o-b-a-r + ] + assert_equal by_length, `#{FZF} -ffb < #{tempname}`.split($/) + assert_equal by_length, `#{FZF} -ffb --tiebreak=length < #{tempname}`.split($/) + + assert_equal %w[ + foobar + foobar-- + --foobar + foobar---- + --foobar-- + ----foobar + f-o-o-b-a-r + ], `#{FZF} -ffb --tiebreak=length,begin < #{tempname}`.split($/) + + assert_equal %w[ + foobar + --foobar + foobar-- + ----foobar + --foobar-- + foobar---- + f-o-o-b-a-r + ], `#{FZF} -ffb --tiebreak=length,end < #{tempname}`.split($/) + + assert_equal %w[ + foobar---- + foobar-- + foobar + --foobar + --foobar-- + ----foobar + f-o-o-b-a-r + ], `#{FZF} -ffb --tiebreak=begin < #{tempname}`.split($/) + + by_begin_end = %w[ + foobar + foobar-- + foobar---- + --foobar + --foobar-- + ----foobar + f-o-o-b-a-r + ] + assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=begin,length < #{tempname}`.split($/) + assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=begin,end < #{tempname}`.split($/) + + assert_equal %w[ + --foobar + ----foobar + foobar + foobar-- + --foobar-- + foobar---- + f-o-o-b-a-r + ], `#{FZF} -ffb --tiebreak=end < #{tempname}`.split($/) + + by_begin_end = %w[ + foobar + --foobar + ----foobar + foobar-- + --foobar-- + foobar---- + f-o-o-b-a-r + ] + assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=end,begin < #{tempname}`.split($/) + assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=end,length < #{tempname}`.split($/) end def test_tiebreak_length_with_nth @@ -517,7 +617,7 @@ class TestGoFZF < TestBase 123:hello 1234567:h ] - assert_equal output, `cat #{tempname} | #{FZF} -fh`.split($/) + assert_equal output, `#{FZF} -fh < #{tempname}`.split($/) output = %w[ 1234567:h @@ -525,7 +625,7 @@ class TestGoFZF < TestBase 1:hell 123:hello ] - assert_equal output, `cat #{tempname} | #{FZF} -fh -n2 -d:`.split($/) + assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.split($/) end def test_tiebreak_length_with_nth_trim_length @@ -544,7 +644,7 @@ class TestGoFZF < TestBase "apple juice bottle 1", "apple ui bottle 2", ] - assert_equal output, `cat #{tempname} | #{FZF} -fa -n1`.split($/) + assert_equal output, `#{FZF} -fa -n1 < #{tempname}`.split($/) # len(1 ~ 2) output = [ @@ -553,7 +653,7 @@ class TestGoFZF < TestBase "apple juice bottle 1", "app ice bottle 3", ] - assert_equal output, `cat #{tempname} | #{FZF} -fai -n1..2`.split($/) + assert_equal output, `#{FZF} -fai -n1..2 < #{tempname}`.split($/) # len(1) + len(2) output = [ @@ -562,7 +662,7 @@ class TestGoFZF < TestBase "apple ui bottle 2", "apple juice bottle 1", ] - assert_equal output, `cat #{tempname} | #{FZF} -x -f"a i" -n1,2`.split($/) + assert_equal output, `#{FZF} -x -f"a i" -n1,2 < #{tempname}`.split($/) # len(2) output = [ @@ -571,8 +671,8 @@ class TestGoFZF < TestBase "app ice bottle 3", "apple juice bottle 1", ] - assert_equal output, `cat #{tempname} | #{FZF} -fi -n2`.split($/) - assert_equal output, `cat #{tempname} | #{FZF} -fi -n2,1..2`.split($/) + assert_equal output, `#{FZF} -fi -n2 < #{tempname}`.split($/) + assert_equal output, `#{FZF} -fi -n2,1..2 < #{tempname}`.split($/) end def test_tiebreak_end_backward_scan @@ -582,8 +682,8 @@ class TestGoFZF < TestBase ] writelines tempname, input - assert_equal input.reverse, `cat #{tempname} | #{FZF} -f fb`.split($/) - assert_equal input, `cat #{tempname} | #{FZF} -f fb --tiebreak=end`.split($/) + assert_equal input.reverse, `#{FZF} -f fb < #{tempname}`.split($/) + assert_equal input, `#{FZF} -f fb --tiebreak=end < #{tempname}`.split($/) end def test_invalid_cache @@ -613,7 +713,7 @@ class TestGoFZF < TestBase File.open(tempname, 'w') do |f| f << data end - assert_equal data, `cat #{tempname} | #{FZF} -f .`.chomp + assert_equal data, `#{FZF} -f . < #{tempname}`.chomp end def test_read0 @@ -888,18 +988,18 @@ class TestGoFZF < TestBase def test_with_nth writelines tempname, ['hello world ', 'byebye'] - assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1`.chomp + assert_equal 'hello world ', `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp end def test_with_nth_ansi writelines tempname, ["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'] - assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi`.chomp + assert_equal 'hello world ', `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi < #{tempname}`.chomp end def test_with_nth_no_ansi src = "\x1b[33mhello \x1b[34;1mworld\x1b[m " writelines tempname, [src, 'byebye'] - assert_equal src, `cat #{tempname} | #{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi`.chomp + assert_equal src, `#{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi < #{tempname}`.chomp end def test_exit_0_exit_code From 8d3a302a1754a4e28cc1085b95e9a03981372d02 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 13 Jan 2016 21:36:44 +0900 Subject: [PATCH 084/658] Simplify Item structure This commit compensates for the performance overhead from the extended tiebreak option. --- src/chunklist_test.go | 4 ++-- src/core.go | 2 -- src/item.go | 40 +++++++++++++++++----------------------- src/item_test.go | 32 ++++++++++++++++---------------- src/merger_test.go | 2 +- src/options.go | 7 +------ src/pattern.go | 3 +-- src/terminal.go | 11 +++++------ 8 files changed, 43 insertions(+), 58 deletions(-) diff --git a/src/chunklist_test.go b/src/chunklist_test.go index 6ddd336..5f7481d 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -7,7 +7,7 @@ import ( func TestChunkList(t *testing.T) { // FIXME global - sortCriteria = []criterion{byMatchLen, byLength, byIndex} + sortCriteria = []criterion{byMatchLen, byLength} cl := NewChunkList(func(s []byte, i int) *Item { return &Item{text: []rune(string(s)), rank: buildEmptyRank(int32(i * 2))} @@ -39,7 +39,7 @@ func TestChunkList(t *testing.T) { if len(*chunk1) != 2 { t.Error("Snapshot should contain only two items") } - last := func(arr []int32) int32 { + last := func(arr [5]int32) int32 { return arr[len(arr)-1] } if string((*chunk1)[0].text) != "hello" || last((*chunk1)[0].rank) != 0 || diff --git a/src/core.go b/src/core.go index 1906c50..24af050 100644 --- a/src/core.go +++ b/src/core.go @@ -103,7 +103,6 @@ func Run(opts *Options) { runes, colors := ansiProcessor(data) return &Item{ text: runes, - index: int32(index), colors: colors, rank: buildEmptyRank(int32(index))} }) @@ -120,7 +119,6 @@ func Run(opts *Options) { item := Item{ text: joinTokens(trans), origText: &runes, - index: int32(index), colors: nil, rank: buildEmptyRank(int32(index))} diff --git a/src/item.go b/src/item.go index a4fa609..9200efb 100644 --- a/src/item.go +++ b/src/item.go @@ -20,41 +20,41 @@ type Item struct { text []rune origText *[]rune transformed []Token - index int32 offsets []Offset colors []ansiOffset - rank []int32 + rank [5]int32 } // Sort criteria to use. Never changes once fzf is started. var sortCriteria []criterion -func isRankValid(rank []int32) bool { +func isRankValid(rank [5]int32) bool { // Exclude ordinal index - for i := 0; i < len(rank)-1; i++ { - if rank[i] > 0 { + for _, r := range rank[:4] { + if r > 0 { return true } } return false } -func buildEmptyRank(index int32) []int32 { - len := len(sortCriteria) - arr := make([]int32, len) - arr[len-1] = index - return arr +func buildEmptyRank(index int32) [5]int32 { + return [5]int32{0, 0, 0, 0, index} +} + +func (item *Item) Index() int32 { + return item.rank[4] } // Rank calculates rank of the Item -func (item *Item) Rank(cache bool) []int32 { +func (item *Item) Rank(cache bool) [5]int32 { if cache && isRankValid(item.rank) { return item.rank } matchlen := 0 prevEnd := 0 lenSum := 0 - minBegin := math.MaxUint16 + minBegin := math.MaxInt32 for _, offset := range item.offsets { begin := int(offset[0]) end := int(offset[1]) @@ -76,7 +76,7 @@ func (item *Item) Rank(cache bool) []int32 { if matchlen == 0 { matchlen = math.MaxInt32 } - rank := make([]int32, len(sortCriteria)) + rank := buildEmptyRank(item.Index()) for idx, criterion := range sortCriteria { var val int32 switch criterion { @@ -100,8 +100,6 @@ func (item *Item) Rank(cache bool) []int32 { // Empty offsets due to inverse terms. val = 1 } - case byIndex: - val = item.index } rank[idx] = val } @@ -269,19 +267,15 @@ func (a ByRelevanceTac) Less(i, j int) bool { return compareRanks(irank, jrank, true) } -func compareRanks(irank []int32, jrank []int32, tac bool) bool { - lastIdx := len(irank) - 1 - for idx, left := range irank { +func compareRanks(irank [5]int32, jrank [5]int32, tac bool) bool { + for idx := 0; idx < 4; idx++ { + left := irank[idx] right := jrank[idx] - if tac && idx == lastIdx { - left = left * -1 - right = right * -1 - } if left < right { return true } else if left > right { return false } } - return true + return (irank[4] <= jrank[4]) != tac } diff --git a/src/item_test.go b/src/item_test.go index f26f837..d1c30d7 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -23,17 +23,17 @@ func TestOffsetSort(t *testing.T) { } func TestRankComparison(t *testing.T) { - if compareRanks([]int32{3, 0, 5}, []int32{2, 0, 7}, false) || - !compareRanks([]int32{3, 0, 5}, []int32{3, 0, 6}, false) || - !compareRanks([]int32{1, 2, 3}, []int32{1, 3, 2}, false) || - !compareRanks([]int32{0, 0, 0}, []int32{0, 0, 0}, false) { + if compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{2, 0, 0, 0, 7}, false) || + !compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{3, 0, 0, 0, 6}, false) || + !compareRanks([5]int32{1, 2, 0, 0, 3}, [5]int32{1, 3, 0, 0, 2}, false) || + !compareRanks([5]int32{0, 0, 0, 0, 0}, [5]int32{0, 0, 0, 0, 0}, false) { t.Error("Invalid order") } - if compareRanks([]int32{3, 0, 5}, []int32{2, 0, 7}, true) || - !compareRanks([]int32{3, 0, 5}, []int32{3, 0, 6}, false) || - !compareRanks([]int32{1, 2, 3}, []int32{1, 3, 2}, true) || - !compareRanks([]int32{0, 0, 0}, []int32{0, 0, 0}, false) { + if compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{2, 0, 0, 0, 7}, true) || + !compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{3, 0, 0, 0, 6}, false) || + !compareRanks([5]int32{1, 2, 0, 0, 3}, [5]int32{1, 3, 0, 0, 2}, true) || + !compareRanks([5]int32{0, 0, 0, 0, 0}, [5]int32{0, 0, 0, 0, 0}, false) { t.Error("Invalid order (tac)") } } @@ -41,16 +41,16 @@ func TestRankComparison(t *testing.T) { // Match length, string length, index func TestItemRank(t *testing.T) { // FIXME global - sortCriteria = []criterion{byMatchLen, byLength, byIndex} + sortCriteria = []criterion{byMatchLen, byLength} strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} - item1 := Item{text: strs[0], index: 1, offsets: []Offset{}} + item1 := Item{text: strs[0], offsets: []Offset{}, rank: [5]int32{0, 0, 0, 0, 1}} rank1 := item1.Rank(true) - if rank1[0] != math.MaxInt32 || rank1[1] != 3 || rank1[2] != 1 { + if rank1[0] != math.MaxInt32 || rank1[1] != 3 || rank1[4] != 1 { t.Error(item1.Rank(true)) } // Only differ in index - item2 := Item{text: strs[0], index: 0, offsets: []Offset{}} + item2 := Item{text: strs[0], offsets: []Offset{}} items := []*Item{&item1, &item2} sort.Sort(ByRelevance(items)) @@ -66,10 +66,10 @@ func TestItemRank(t *testing.T) { } // Sort by relevance - item3 := Item{text: strs[1], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} - item4 := Item{text: strs[1], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} - item5 := Item{text: strs[2], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} - item6 := Item{text: strs[2], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + item3 := Item{text: strs[1], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item4 := Item{text: strs[1], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + item5 := Item{text: strs[2], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item6 := Item{text: strs[2], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6} sort.Sort(ByRelevance(items)) if items[0] != &item6 || items[1] != &item4 || diff --git a/src/merger_test.go b/src/merger_test.go index 34efc84..472e204 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -23,7 +23,7 @@ func randItem() *Item { } return &Item{ text: []rune(str), - index: rand.Int31(), + rank: buildEmptyRank(rand.Int31()), offsets: offsets} } diff --git a/src/options.go b/src/options.go index 30e0016..6399343 100644 --- a/src/options.go +++ b/src/options.go @@ -83,7 +83,6 @@ const ( byLength byBegin byEnd - byIndex ) func defaultMargin() [4]string { @@ -147,7 +146,7 @@ func defaultOptions() *Options { Delimiter: Delimiter{}, Sort: 1000, Tac: false, - Criteria: []criterion{byMatchLen, byLength, byIndex}, + Criteria: []criterion{byMatchLen, byLength}, Multi: false, Ansi: false, Mouse: true, @@ -382,7 +381,6 @@ func parseTiebreak(str string) []criterion { switch str { case "index": check(&hasIndex, "index") - criteria = append(criteria, byIndex) case "length": check(&hasLength, "length") criteria = append(criteria, byLength) @@ -396,9 +394,6 @@ func parseTiebreak(str string) []criterion { errorExit("invalid sort criterion: " + str) } } - if !hasIndex { - criteria = append(criteria, byIndex) - } return criteria } diff --git a/src/pattern.go b/src/pattern.go index 4c61b87..af73b67 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -306,10 +306,9 @@ func dupItem(item *Item, offsets []Offset) *Item { text: item.text, origText: item.origText, transformed: item.transformed, - index: item.index, offsets: offsets, colors: item.colors, - rank: buildEmptyRank(item.index)} + rank: buildEmptyRank(item.Index())} } func (p *Pattern) basicMatch(item *Item) (int, int, int) { diff --git a/src/terminal.go b/src/terminal.go index c9b8056..381ae7c 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -464,7 +464,6 @@ func (t *Terminal) printHeader() { state = newState item := &Item{ text: []rune(trimmed), - index: 0, colors: colors, rank: buildEmptyRank(0)} @@ -491,7 +490,7 @@ func (t *Terminal) printList() { } func (t *Terminal) printItem(item *Item, current bool) { - _, selected := t.selected[item.index] + _, selected := t.selected[item.Index()] if current { C.CPrint(C.ColCursor, true, ">") if selected { @@ -836,8 +835,8 @@ func (t *Terminal) Loop() { } } selectItem := func(item *Item) bool { - if _, found := t.selected[item.index]; !found { - t.selected[item.index] = selectedItem{time.Now(), item.StringPtr(t.ansi)} + if _, found := t.selected[item.Index()]; !found { + t.selected[item.Index()] = selectedItem{time.Now(), item.StringPtr(t.ansi)} return true } return false @@ -845,7 +844,7 @@ func (t *Terminal) Loop() { toggleY := func(y int) { item := t.merger.Get(y) if !selectItem(item) { - delete(t.selected, item.index) + delete(t.selected, item.Index()) } } toggle := func() { @@ -934,7 +933,7 @@ func (t *Terminal) Loop() { if t.multi { for i := 0; i < t.merger.Length(); i++ { item := t.merger.Get(i) - delete(t.selected, item.index) + delete(t.selected, item.Index()) } req(reqList, reqInfo) } From edb647667e5630c77515e99a66bccec9d7519c0e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 14 Jan 2016 00:41:02 +0900 Subject: [PATCH 085/658] Change temporary file names to fix flaky tests --- test/test_go.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index 85e3d83..a049a15 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -143,8 +143,10 @@ class TestBase < Minitest::Test attr_reader :tmux def tempname + @temp_suffix ||= 0 [TEMPNAME, - caller_locations.map(&:label).find { |l| l =~ /^test_/ }].join '-' + caller_locations.map(&:label).find { |l| l =~ /^test_/ }, + @temp_suffix].join '-' end def setup @@ -158,6 +160,7 @@ class TestBase < Minitest::Test File.read(tempname) ensure File.unlink tempname while File.exists?(tempname) + @temp_suffix += 1 tmux.prepare end From 23244bb41087f4f833c33219bba7a7213710ab20 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 14 Jan 2016 01:09:16 +0900 Subject: [PATCH 086/658] [fish] Fix intermittent errors on CTRL-T This seems like a bug of fish, but sometimes when you select an item fish complains: "insertion mode switches can not be used when not in insertion mode" This only happens when using tmux pane. Injecting a dummy command somehow fixes the issue. --- shell/key-bindings.fish | 1 + 1 file changed, 1 insertion(+) diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index 3d3c2a7..40a1fee 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -20,6 +20,7 @@ function fzf_key_bindings -o -type d -print \ -o -type l -print 2> /dev/null | sed 1d | cut -b3-" eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m > $TMPDIR/fzf.result" + and sleep 0 and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) commandline -f repaint rm -f $TMPDIR/fzf.result From 45143f9541d55c5efdadc7ea8acd9474a19eb8c6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 14 Jan 2016 01:32:03 +0900 Subject: [PATCH 087/658] Ignore leading whitespaces when calculating 'begin' index --- CHANGELOG.md | 3 ++- src/item.go | 9 +++++++- test/test_go.rb | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87f7827..3d81d14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,12 @@ CHANGELOG 0.11.2 ------ -- `--tiebreak` now accepts comma-separated list of sort criteria. +- `--tiebreak` now accepts comma-separated list of sort criteria - Each criterion should appear only once in the list - `index` is only allowed at the end of the list - `index` is implicitly appended to the list when not specified - Default is `length` (or equivalently `length,index`) +- `begin` criterion will ignore leading whitespaces when calculating the index 0.11.1 ------ diff --git a/src/item.go b/src/item.go index 9200efb..43ce1a2 100644 --- a/src/item.go +++ b/src/item.go @@ -92,7 +92,14 @@ func (item *Item) Rank(cache bool) [5]int32 { } case byBegin: // We can't just look at item.offsets[0][0] because it can be an inverse term - val = int32(minBegin) + whitePrefixLen := 0 + for idx, r := range item.text { + whitePrefixLen = idx + if idx == minBegin || r != ' ' && r != '\t' { + break + } + } + val = int32(minBegin - whitePrefixLen) case byEnd: if prevEnd > 0 { val = int32(1 + len(item.text) - prevEnd) diff --git a/test/test_go.rb b/test/test_go.rb index a049a15..efbbf25 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -605,6 +605,64 @@ class TestGoFZF < TestBase assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=end,length < #{tempname}`.split($/) end + def test_tiebreak_white_prefix + writelines tempname, [ + 'f o o b a r', + ' foo bar', + ' foobar', + '----foo bar', + '----foobar', + ' foo bar', + ' foobar--', + ' foobar', + '--foo bar', + '--foobar', + 'foobar', + ] + + assert_equal [ + ' foobar', + ' foobar', + 'foobar', + ' foobar--', + '--foobar', + '----foobar', + ' foo bar', + ' foo bar', + '--foo bar', + '----foo bar', + 'f o o b a r', + ], `#{FZF} -ffb < #{tempname}`.split($/) + + assert_equal [ + ' foobar', + ' foobar--', + ' foobar', + 'foobar', + '--foobar', + '----foobar', + ' foo bar', + ' foo bar', + '--foo bar', + '----foo bar', + 'f o o b a r', + ], `#{FZF} -ffb --tiebreak=begin < #{tempname}`.split($/) + + assert_equal [ + ' foobar', + ' foobar', + 'foobar', + ' foobar--', + '--foobar', + '----foobar', + ' foo bar', + ' foo bar', + '--foo bar', + '----foo bar', + 'f o o b a r', + ], `#{FZF} -ffb --tiebreak=begin,length < #{tempname}`.split($/) + end + def test_tiebreak_length_with_nth input = %w[ 1:hell From f6c6e59a50d580595fd5117ab51b058988fc5f83 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 14 Jan 2016 02:35:43 +0900 Subject: [PATCH 088/658] Add toggle-in and toggle-out for --bind Related: #452 When `--multi` is set, tab key will bring your cursor down, and shift-tab up. But since fzf by default draws the screen in bottom-up fashion, one may feel that the opposite of the behavior is more desirable and choose to customize the key bindings as follows. export FZF_DEFAULT_OPTS="--bind tab:toggle-up,shift-tab:toggle-down" This configuration, however, becomes no longer straightforward when `--reverse` is set and fzf switches to top-down layout. To address the requirement, this commit adds `toggle-in` and `toggle-out` option which switch direction depending on `--reverse`-ness. export FZF_DEFAULT_OPTS="--bind tab:toggle-out,shift-tab:toggle-in" --- man/man1/fzf.1 | 4 +++- src/options.go | 4 ++++ src/terminal.go | 12 ++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index feca972..ba52f84 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -219,7 +219,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR .RE .RS - \fBACTION: DEFAULT BINDINGS: + \fBACTION: DEFAULT BINDINGS (NOTES): \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR \fBaccept\fR \fIenter double-click\fR \fBbackward-char\fR \fIctrl-b left\fR @@ -249,6 +249,8 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBtoggle\fR \fBtoggle-all\fR \fBtoggle-down\fR \fIctrl-i (tab)\fR + \fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle-up\fR : \fBtoggle-down\fR) + \fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle-down\fR : \fBtoggle-up\fR) \fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR) \fBtoggle-up\fR \fIbtab (shift-tab)\fR \fBunix-line-discard\fR \fIctrl-u\fR diff --git a/src/options.go b/src/options.go index 6399343..962e516 100644 --- a/src/options.go +++ b/src/options.go @@ -566,6 +566,10 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b keymap[key] = actToggleDown case "toggle-up": keymap[key] = actToggleUp + case "toggle-in": + keymap[key] = actToggleIn + case "toggle-out": + keymap[key] = actToggleOut case "toggle-all": keymap[key] = actToggleAll case "select-all": diff --git a/src/terminal.go b/src/terminal.go index 381ae7c..ab11587 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -125,6 +125,8 @@ const ( actToggleAll actToggleDown actToggleUp + actToggleIn + actToggleOut actDown actUp actPageUp @@ -949,6 +951,16 @@ func (t *Terminal) Loop() { } req(reqList, reqInfo) } + case actToggleIn: + if t.reverse { + return doAction(actToggleUp, mapkey) + } + return doAction(actToggleDown, mapkey) + case actToggleOut: + if t.reverse { + return doAction(actToggleDown, mapkey) + } + return doAction(actToggleUp, mapkey) case actToggleDown: if t.multi && t.merger.Length() > 0 { toggle() From 95970164ad0f03cd1a95aec2ad3d3cba180c0308 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 14 Jan 2016 02:54:08 +0900 Subject: [PATCH 089/658] 0.11.2 --- CHANGELOG.md | 3 +++ install | 4 ++-- src/constants.go | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d81d14..6827762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ CHANGELOG - `index` is implicitly appended to the list when not specified - Default is `length` (or equivalently `length,index`) - `begin` criterion will ignore leading whitespaces when calculating the index +- Added `toggle-in` and `toggle-out` actions + - Switch direction depending on `--reverse`-ness + - `export FZF_DEFAULT_OPTS="--bind tab:toggle-out,shift-tab:toggle-in"` 0.11.1 ------ diff --git a/install b/install index c98dc79..546e0a7 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.11.1 pre=1 || - version=0.11.1 pre=0 +[[ "$@" =~ --pre ]] && version=0.11.2 pre=1 || + version=0.11.2 pre=0 auto_completion= key_bindings= diff --git a/src/constants.go b/src/constants.go index 4a18da5..0145958 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.11.1" + version = "0.11.2" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 8695b5e319e02efbfdf70800dce1c73d40378d86 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 16 Jan 2016 18:07:50 +0900 Subject: [PATCH 090/658] Reduce the initial delay when --tac is not given fzf defers the initial rendering of the screen up to 100ms if the input stream is ongoing to prevent unnecessary redraw during the initial phase. However, 100ms delay is quite noticeable and might give the impression that fzf is not snappy enough. This commit reduces the maximum delay down to 20ms when --tac is not specified, in which case the input list quickly fills the entire screen. --- src/constants.go | 3 ++- src/terminal.go | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/constants.go b/src/constants.go index 0145958..6f6898d 100644 --- a/src/constants.go +++ b/src/constants.go @@ -18,7 +18,8 @@ const ( defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//` // Terminal - initialDelay = 100 * time.Millisecond + initialDelay = 20 * time.Millisecond + initialDelayTac = 100 * time.Millisecond spinnerDuration = 200 * time.Millisecond // Matcher diff --git a/src/terminal.go b/src/terminal.go index ab11587..e5b247d 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -22,6 +22,7 @@ import ( // Terminal represents terminal input/output type Terminal struct { + initDelay time.Duration inlineInfo bool prompt string reverse bool @@ -198,7 +199,14 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { header = reverseStringArray(opts.Header) } _tabStop = opts.Tabstop + var delay time.Duration + if opts.Tac { + delay = initialDelayTac + } else { + delay = initialDelay + } return &Terminal{ + initDelay: delay, inlineInfo: opts.InlineInfo, prompt: opts.Prompt, reverse: opts.Reverse, @@ -751,7 +759,7 @@ func (t *Terminal) Loop() { t.printHeader() t.mutex.Unlock() go func() { - timer := time.NewTimer(initialDelay) + timer := time.NewTimer(t.initDelay) <-timer.C t.reqBox.Set(reqRefresh, nil) }() From 69438a55ca482c9419e2721fb263e629c7155e27 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 16 Jan 2016 21:11:33 +0900 Subject: [PATCH 091/658] Update CHANGELOG: 0.11.2 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6827762..291d5e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ CHANGELOG - Added `toggle-in` and `toggle-out` actions - Switch direction depending on `--reverse`-ness - `export FZF_DEFAULT_OPTS="--bind tab:toggle-out,shift-tab:toggle-in"` +- Reduced the initial delay when --tac is not given 0.11.1 ------ From 68c84264af17e735102957a5263026e3f96b1cb7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 16 Jan 2016 21:13:12 +0900 Subject: [PATCH 092/658] Update CHANGELOG --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 291d5e8..73dbd0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,13 @@ CHANGELOG - Added `toggle-in` and `toggle-out` actions - Switch direction depending on `--reverse`-ness - `export FZF_DEFAULT_OPTS="--bind tab:toggle-out,shift-tab:toggle-in"` -- Reduced the initial delay when --tac is not given +- Reduced the initial delay when `--tac` is not given + - fzf defers the initial rendering of the screen up to 100ms if the input + stream is ongoing to prevent unnecessary redraw during the initial + phase. However, 100ms delay is quite noticeable and might give the + impression that fzf is not snappy enough. This commit reduces the + maximum delay down to 20ms when `--tac` is not specified, in which case + the input list quickly fills the entire screen. 0.11.1 ------ From 96176476f3990507b38f2ec45d24c12fcb7aa075 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 20 Jan 2016 01:09:58 +0900 Subject: [PATCH 093/658] Make fuzzy completion customizable with _fzf_compgen_{path,dir} Notes: - You can now override _fzf_compgen_path and _fzf_compgen_dir functions to use custom commands such as ag instead of find for listing completion candidates. - The first argument is the base path to start traversal - Removed file-only completion in bash, i.e. _fzf_file_completion. Maintaining a list of commands that only expect files, not directories, is cumbersome (there are too many) and error-prone. TBD: - Added $FZF_COMPLETION_DIR_COMMANDS to customize the list of commands which use directory-only completion. The default is "cd pushd rmdir". Not sure if it's the best approach to address the requirement, I'll leave it as an undocumented feature. Related: #406 (@thomcom), #456 (@frizinak) --- README.md | 8 ++++++ shell/completion.bash | 61 +++++++++++++++++++++++++------------------ shell/completion.zsh | 34 ++++++++++++++++++------ test/test_go.rb | 16 +++++++++++- 4 files changed, 84 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 567271c..5e3a1f0 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,14 @@ export FZF_COMPLETION_TRIGGER='~~' # Options to fzf command export FZF_COMPLETION_OPTS='+c -x' + +# Use ag instead of the default find command for listing candidates. +# - The first argument to the function is the base path to start traversal +# - Note that ag only lists files not directories +# - See the source code (completion.{bash,zsh}) for the details. +_fzf_compgen_paths() { + ag -g "" "$1" +} ``` #### Supported commands diff --git a/shell/completion.bash b/shell/completion.bash index ef3a965..9d84ce5 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -10,6 +10,26 @@ # - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_OPTS (default: empty) +# To use custom commands instead of find, override _fzf_compgen_{path,dir} +if ! declare -f _fzf_compgen_path > /dev/null; then + _fzf_compgen_path() { + echo "$1" + \find -L "$1" \ + -name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ + -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' + } +fi + +if ! declare -f _fzf_compgen_dir > /dev/null; then + _fzf_compgen_dir() { + \find -L "$1" \ + -name .git -prune -o -name .svn -prune -o -type d \ + -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' + } +fi + +########################################################### + _fzf_orig_completion_filter() { sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' | awk -F= '{gsub(/[^a-z0-9_= ;]/, "_", $1); print $1"="$2}' @@ -113,7 +133,7 @@ __fzf_generic_path_completion() { [ -z "$dir" ] && dir='.' [ "$dir" != "/" ] && dir="${dir/%\//}" tput sc - matches=$(\find -L "$dir" $1 -a -not -path "$dir" -print 2> /dev/null | sed 's@^\./@@' | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do + matches=$(eval "$1 $(printf %q "$dir")" | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do printf "%q$3 " "$item" done) matches=${matches% } @@ -171,21 +191,16 @@ _fzf_complete() { } _fzf_path_completion() { - __fzf_generic_path_completion \ - "-name .git -prune -o -name .svn -prune -o ( -type d -o -type f -o -type l )" \ - "-m" "" "$@" + __fzf_generic_path_completion _fzf_compgen_path "-m" "" "$@" } +# Deprecated. No file only completion. _fzf_file_completion() { - __fzf_generic_path_completion \ - "-name .git -prune -o -name .svn -prune -o ( -type f -o -type l )" \ - "-m" "" "$@" + _fzf_path_completion "$@" } _fzf_dir_completion() { - __fzf_generic_path_completion \ - "-name .git -prune -o -name .svn -prune -o -type d" \ - "" "/" "$@" + __fzf_generic_path_completion _fzf_compgen_dir "" "/" "$@" } _fzf_complete_kill() { @@ -239,13 +254,12 @@ _fzf_complete_unalias() { # fzf options complete -o default -F _fzf_opts_completion fzf -d_cmds="cd pushd rmdir" -f_cmds=" +d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}" +a_cmds=" awk cat diff diff3 emacs emacsclient ex file ftp g++ gcc gvim head hg java javac ld less more mvim nvim patch perl python ruby - sed sftp sort source tail tee uniq vi view vim wc xdg-open" -a_cmds=" + sed sftp sort source tail tee uniq vi view vim wc xdg-open basename bunzip2 bzip2 chmod chown curl cp dirname du find git grep gunzip gzip hg jar ln ls mv open rm rsync scp @@ -256,7 +270,7 @@ x_cmds="kill ssh telnet unset unalias export" if [ "$_fzf_completion_loaded" != '0.10.8' ]; then # Really wish I could use associative array but OSX comes with bash 3.2 :( eval $(complete | \grep '\-F' | \grep -v _fzf_ | - \grep -E " ($(echo $d_cmds $f_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter) + \grep -E " ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter) export _fzf_completion_loaded=0.10.8 fi @@ -278,21 +292,16 @@ _fzf_defc() { fi } -# Directory -for cmd in $d_cmds; do - _fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o plusdirs" -done - -# File -for cmd in $f_cmds; do - _fzf_defc "$cmd" _fzf_file_completion "-o default -o bashdefault" -done - # Anything for cmd in $a_cmds; do _fzf_defc "$cmd" _fzf_path_completion "-o default -o bashdefault" done +# Directory +for cmd in $d_cmds; do + _fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o plusdirs" +done + unset _fzf_defc # Kill completion @@ -307,4 +316,4 @@ complete -F _fzf_complete_unset -o default -o bashdefault unset complete -F _fzf_complete_export -o default -o bashdefault export complete -F _fzf_complete_unalias -o default -o bashdefault unalias -unset cmd d_cmds f_cmds a_cmds x_cmds +unset cmd d_cmds a_cmds x_cmds diff --git a/shell/completion.zsh b/shell/completion.zsh index 6e2d932..c7dee7a 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -10,12 +10,32 @@ # - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_OPTS (default: empty) +# To use custom commands instead of find, override _fzf_compgen_{path,dir} +if ! declare -f _fzf_compgen_path > /dev/null; then + _fzf_compgen_path() { + echo "$1" + \find -L "$1" \ + -name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ + -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' + } +fi + +if ! declare -f _fzf_compgen_dir > /dev/null; then + _fzf_compgen_dir() { + \find -L "$1" \ + -name .git -prune -o -name .svn -prune -o -type d \ + -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' + } +fi + +########################################################### + __fzf_generic_path_completion() { - local base lbuf find_opts fzf_opts suffix tail fzf dir leftover matches nnm + local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches nnm # (Q) flag removes a quoting level: "foo\ bar" => "foo bar" base=${(Q)1} lbuf=$2 - find_opts=$3 + compgen=$3 fzf_opts=$4 suffix=$5 tail=$6 @@ -33,7 +53,7 @@ __fzf_generic_path_completion() { [ -z "$dir" ] && dir='.' [ "$dir" != "/" ] && dir="${dir/%\//}" dir=${~dir} - matches=$(\find -L "$dir" ${=find_opts} -a -not -path "$dir" -print 2> /dev/null | sed 's@^\./@@' | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do + matches=$(eval "$compgen $(printf %q "$dir")" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do printf "%q$suffix " "$item" done) matches=${matches% } @@ -50,14 +70,12 @@ __fzf_generic_path_completion() { } _fzf_path_completion() { - __fzf_generic_path_completion "$1" "$2" \ - "-name .git -prune -o -name .svn -prune -o ( -type d -o -type f -o -type l )" \ + __fzf_generic_path_completion "$1" "$2" _fzf_compgen_path \ "-m" "" " " } _fzf_dir_completion() { - __fzf_generic_path_completion "$1" "$2" \ - "-name .git -prune -o -name .svn -prune -o -type d" \ + __fzf_generic_path_completion "$1" "$2" _fzf_compgen_dir \ "" "/" "" } @@ -145,7 +163,7 @@ fzf-completion() { zle redisplay # Trigger sequence given elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then - d_cmds=(cd pushd rmdir) + d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}) [ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}} [ -z "${tokens[-1]}" ] && lbuf=$LBUFFER || lbuf=${LBUFFER:0:-${#tokens[-1]}} diff --git a/test/test_go.rb b/test/test_go.rb index efbbf25..a8f75c6 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1269,7 +1269,7 @@ module CompletionTest tmux.send_keys 'C-u' tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab, pane: 0 tmux.until(1) { |lines| lines.item_count > 0 } - tmux.send_keys :Enter + tmux.send_keys 'C-K', :Enter tmux.until do |lines| tmux.send_keys 'C-L' lines[-1].end_with?('/tmp/fzf\ test/foobar') @@ -1339,6 +1339,20 @@ module CompletionTest tmux.send_keys 'C-L' lines[-1] == "kill #{pid}" end + + def test_custom_completion + tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter + tmux.prepare + tmux.send_keys 'ls /tmp/**', :Tab, pane: 0 + tmux.until(1) { |lines| lines.item_count == 11 } + tmux.send_keys :BTab, :BTab, :BTab + tmux.until(1) { |lines| lines[-2].include? '(3)' } + tmux.send_keys :Enter + tmux.until do |lines| + tmux.send_keys 'C-L' + lines[-1] == "ls /tmp 1 2" + end + end ensure Process.kill 'KILL', pid.to_i rescue nil if pid end From ce9c51d3997ca0fa4ac9ac77c5df94a0f82bfd80 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 20 Jan 2016 01:39:55 +0900 Subject: [PATCH 094/658] Typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e3a1f0..d8a5720 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,7 @@ export FZF_COMPLETION_OPTS='+c -x' # - The first argument to the function is the base path to start traversal # - Note that ag only lists files not directories # - See the source code (completion.{bash,zsh}) for the details. -_fzf_compgen_paths() { +_fzf_compgen_path() { ag -g "" "$1" } ``` From 82067463b8263692147d80b6fc909f1d98693d88 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 29 Jan 2016 01:28:51 +0900 Subject: [PATCH 095/658] [completion] _fzf_complete_COMMAND_post for post processing e.g. _fzf_complete_foo() { _fzf_complete "--multi --reverse --header-lines=3" "$@" < <( ls -al ) } _fzf_complete_foo_post() { awk '{print $NF}' } [ -n "$BASH" ] && complete -F _fzf_complete_foo -o default -o bashdefault foo --- shell/completion.bash | 15 ++++----------- shell/completion.zsh | 16 +++++----------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 9d84ce5..35b49a1 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -156,15 +156,10 @@ __fzf_generic_path_completion() { fi } -_fzf_feed_fifo() ( - rm -f "$fifo" - mkfifo "$fifo" - cat <&0 > "$fifo" & -) - _fzf_complete() { - local fifo cur selected trigger cmd fzf - fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$" + local cur selected trigger cmd fzf post + post="$(caller 0 | awk '{print $2}')_post" + type -t $post > /dev/null 2>&1 || post=cat [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') @@ -173,12 +168,10 @@ _fzf_complete() { if [[ ${cur} == *"$trigger" ]]; then cur=${cur:0:${#cur}-${#trigger}} - _fzf_feed_fifo "$fifo" tput sc - selected=$(eval "cat '$fifo' | $fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ') + selected=$(cat | $fzf $FZF_COMPLETION_OPTS $1 -q "$cur" | $post | tr '\n' ' ') selected=${selected% } # Strip trailing space not to repeat "-o nospace" tput rc - rm -f "$fifo" if [ -n "$selected" ]; then COMPREPLY=("$selected") diff --git a/shell/completion.zsh b/shell/completion.zsh index c7dee7a..a1e1e07 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -79,26 +79,20 @@ _fzf_dir_completion() { "" "/" "" } -_fzf_feed_fifo() ( - rm -f "$fifo" - mkfifo "$fifo" - cat <&0 > "$fifo" & -) - _fzf_complete() { - local fifo fzf_opts lbuf fzf matches - fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$" + local fzf_opts lbuf fzf matches post fzf_opts=$1 lbuf=$2 + post="${funcstack[2]}_post" + type $post > /dev/null 2>&1 || post=cat + [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" - _fzf_feed_fifo "$fifo" - matches=$(cat "$fifo" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | tr '\n' ' ') + matches=$(cat | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$lbuf$matches" fi zle redisplay - rm -f "$fifo" } _fzf_complete_telnet() { From 1893eca41a35a771d45e6074167e5488d9f2a78a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 2 Feb 2016 17:51:21 +0900 Subject: [PATCH 096/658] Handle SIGTERM gracefully (#482) --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index e5b247d..bd2ad15 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -734,7 +734,7 @@ func (t *Terminal) Loop() { <-t.startChan { // Late initialization intChan := make(chan os.Signal, 1) - signal.Notify(intChan, os.Interrupt, os.Kill) + signal.Notify(intChan, os.Interrupt, os.Kill, syscall.SIGTERM) go func() { <-intChan t.reqBox.Set(reqQuit, nil) From 30bd0b53dbf804a96db4c13d787771b3924d6634 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 3 Feb 2016 04:46:02 +0900 Subject: [PATCH 097/658] Fix #481 - Use $SHELL instead of sh in execute action Note that $SHELL only points to the default shell instead of the current shell. If you're on a non-default shell, you might want to override the value like follows. SHELL=zsh fzf --bind 'enter:execute:echo $ZSH_VERSION; sleep 1' --- src/terminal.go | 6 +++++- test/test_go.rb | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index bd2ad15..181b9c0 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -720,7 +720,11 @@ func quoteEntry(entry string) string { func executeCommand(template string, replacement string) { command := strings.Replace(template, "{}", replacement, -1) - cmd := exec.Command("sh", "-c", command) + shell := os.Getenv("SHELL") + if len(shell) == 0 { + shell = "sh" + } + cmd := exec.Command(shell, "-c", command) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/test/test_go.rb b/test/test_go.rb index a8f75c6..5ea255a 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -893,6 +893,22 @@ class TestGoFZF < TestBase File.unlink output rescue nil end + def test_execute_shell + # Custom script to use as $SHELL + output = tempname + '.out' + File.unlink output rescue nil + writelines tempname, ['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"] + system "chmod +x #{tempname}" + + tmux.send_keys "echo foo | SHELL=#{tempname} fzf --bind 'enter:execute:{}bar'", :Enter + tmux.until { |lines| lines[-2].include? '1/1' } + tmux.send_keys :Enter + tmux.send_keys 'C-c' + assert_equal ['-c / "foo"bar'], File.readlines(output).map(&:chomp) + ensure + File.unlink output rescue nil + end + def test_cycle tmux.send_keys "seq 8 | #{fzf :cycle}", :Enter tmux.until { |lines| lines[-2].include? '8/8' } From e95d82748fc8fe5a05d93b30388ba37adb7dbac8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 7 Feb 2016 01:49:29 +0900 Subject: [PATCH 098/658] Use $SHELL to start $FZF_DEFAULT_COMMAND (#481) --- src/reader.go | 3 +-- src/terminal.go | 7 +------ src/util/util.go | 10 ++++++++++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/reader.go b/src/reader.go index 3e2cf0a..85cbf8b 100644 --- a/src/reader.go +++ b/src/reader.go @@ -4,7 +4,6 @@ import ( "bufio" "io" "os" - "os/exec" "github.com/junegunn/fzf/src/util" ) @@ -59,7 +58,7 @@ func (r *Reader) readFromStdin() { } func (r *Reader) readFromCommand(cmd string) { - listCommand := exec.Command("sh", "-c", cmd) + listCommand := util.ExecCommand(cmd) out, err := listCommand.StdoutPipe() if err != nil { return diff --git a/src/terminal.go b/src/terminal.go index 181b9c0..f1ddc48 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "os" - "os/exec" "os/signal" "regexp" "sort" @@ -720,11 +719,7 @@ func quoteEntry(entry string) string { func executeCommand(template string, replacement string) { command := strings.Replace(template, "{}", replacement, -1) - shell := os.Getenv("SHELL") - if len(shell) == 0 { - shell = "sh" - } - cmd := exec.Command(shell, "-c", command) + cmd := util.ExecCommand(command) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/src/util/util.go b/src/util/util.go index e7e4f31..ab9e766 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -5,6 +5,7 @@ import "C" import ( "os" + "os/exec" "time" "unicode/utf8" ) @@ -126,3 +127,12 @@ func TrimLen(runes []rune) int { } return i - j + 1 } + +// ExecCommand executes the given command with $SHELL +func ExecCommand(command string) *exec.Cmd { + shell := os.Getenv("SHELL") + if len(shell) == 0 { + shell = "sh" + } + return exec.Command(shell, "-c", command) +} From 40d934e378ec2072c9d807f7ca1fca73bda1d13d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 7 Feb 2016 10:56:29 +0900 Subject: [PATCH 099/658] 0.11.3 --- CHANGELOG.md | 10 ++++++++++ install | 4 ++-- man/man1/fzf.1 | 2 +- shell/completion.bash | 4 ++-- src/constants.go | 2 +- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73dbd0d..278c196 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ CHANGELOG ========= +0.11.3 +------ + +- Graceful exit on SIGTERM (#482) +- `$SHELL` instead of `sh` for `execute` action and `$FZF_DEFAULT_COMMAND` (#481) +- Changes in fuzzy completion API + - [`_fzf_compgen_{path,dir}`](https://github.com/junegunn/fzf/commit/9617647) + - [`_fzf_complete_COMMAND_post`](https://github.com/junegunn/fzf/commit/8206746) + for post-processing + 0.11.2 ------ diff --git a/install b/install index 546e0a7..8177e7b 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.11.2 pre=1 || - version=0.11.2 pre=0 +[[ "$@" =~ --pre ]] && version=0.11.3 pre=1 || + version=0.11.3 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index ba52f84..61933e6 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Jan 2016" "fzf 0.11.2" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Feb 2016" "fzf 0.11.3" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/shell/completion.bash b/shell/completion.bash index 35b49a1..05b4ef3 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -260,11 +260,11 @@ a_cmds=" x_cmds="kill ssh telnet unset unalias export" # Preserve existing completion -if [ "$_fzf_completion_loaded" != '0.10.8' ]; then +if [ "$_fzf_completion_loaded" != '0.11.3' ]; then # Really wish I could use associative array but OSX comes with bash 3.2 :( eval $(complete | \grep '\-F' | \grep -v _fzf_ | \grep -E " ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter) - export _fzf_completion_loaded=0.10.8 + export _fzf_completion_loaded=0.11.3 fi if type _completion_loader > /dev/null 2>&1; then diff --git a/src/constants.go b/src/constants.go index 6f6898d..e859f3b 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.11.2" + version = "0.11.3" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 2b3e740569d45861eb9d5c6ac3029192ef863b99 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 12 Feb 2016 12:33:47 +0900 Subject: [PATCH 100/658] [neovim] Fix error in finally block when callback failed e.g. Opening another buffer when `set nohidden` https://github.com/junegunn/fzf.vim/issues/77 --- plugin/fzf.vim | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index f84edfa..80b85d2 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -344,6 +344,7 @@ function! s:execute_term(dict, command, temps) endif call s:pushd(self.dict) + let ret = [] try let ret = s:callback(self.dict, self.temps) From 6354dbbbdf7fb209df2fded23626f732a8003f72 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 11 Feb 2016 17:46:10 -0500 Subject: [PATCH 101/658] Removed the useless `.R` macros If you do `man fzf > /dev/null`, you'll get the following output `R' is a string (producing the registered sign), not a macro. `R' is a string (producing the registered sign), not a macro. `R' is a string (producing the registered sign), not a macro. `R' is a string (producing the registered sign), not a macro. `R' is a string (producing the registered sign), not a macro. `R' is a string (producing the registered sign), not a macro. Removing these `.R` macros with a newline seems to have no effect on the page but gets rid of the error. --- man/man1/fzf.1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 61933e6..17f3153 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -71,7 +71,7 @@ e.g. \fBhistory | fzf --tac --no-sort\fR .BI "--tiebreak=" "CRI[,..]" Comma-separated list of sort criteria to apply when the scores are tied. .br -.R "" + .br .BR length " Prefers item with shorter length" .br @@ -81,7 +81,7 @@ Comma-separated list of sort criteria to apply when the scores are tied. .br .BR index " Prefers item that appeared earlier in the input stream" .br -.R "" + .br - Each criterion should appear only once in the list .br @@ -144,7 +144,7 @@ Reverse orientation .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" @@ -155,12 +155,12 @@ Comma-separated expression for margins around the finder. .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 @@ -431,7 +431,7 @@ Junegunn Choi (\fIjunegunn.c@gmail.com\fR) .I https://github.com/junegunn/fzf .RE .br -.R "" + .br .B Extra Vim plugin: .RS From a568120e42a86bc37b87ae6facab91f7b35f988e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 16 Feb 2016 12:32:05 +0900 Subject: [PATCH 102/658] Fix #494 - _fzf_complete hangs on zsh when not using tmux pane --- shell/completion.zsh | 13 ++++++++++-- test/test_go.rb | 49 ++++++++++++++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index a1e1e07..8288cb2 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -79,8 +79,15 @@ _fzf_dir_completion() { "" "/" "" } +_fzf_feed_fifo() ( + rm -f "$fifo" + mkfifo "$fifo" + cat <&0 > "$fifo" & +) + _fzf_complete() { - local fzf_opts lbuf fzf matches post + local fifo fzf_opts lbuf fzf matches post + fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$" fzf_opts=$1 lbuf=$2 post="${funcstack[2]}_post" @@ -88,11 +95,13 @@ _fzf_complete() { [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" - matches=$(cat | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') + _fzf_feed_fifo "$fifo" + matches=$(cat "$fifo" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$lbuf$matches" fi zle redisplay + rm -f "$fifo" } _fzf_complete_telnet() { diff --git a/test/test_go.rb b/test/test_go.rb index 5ea255a..7a2d003 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1140,7 +1140,7 @@ class TestGoFZF < TestBase private def writelines path, lines File.unlink path while File.exists? path - File.open(path, 'w') { |f| f << lines.join($/) } + File.open(path, 'w') { |f| f << lines.join($/) + $/ } end end @@ -1355,23 +1355,42 @@ module CompletionTest tmux.send_keys 'C-L' lines[-1] == "kill #{pid}" end - - def test_custom_completion - tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter - tmux.prepare - tmux.send_keys 'ls /tmp/**', :Tab, pane: 0 - tmux.until(1) { |lines| lines.item_count == 11 } - tmux.send_keys :BTab, :BTab, :BTab - tmux.until(1) { |lines| lines[-2].include? '(3)' } - tmux.send_keys :Enter - tmux.until do |lines| - tmux.send_keys 'C-L' - lines[-1] == "ls /tmp 1 2" - end - end ensure Process.kill 'KILL', pid.to_i rescue nil if pid end + + def test_custom_completion + tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter + tmux.prepare + tmux.send_keys 'ls /tmp/**', :Tab, pane: 0 + tmux.until(1) { |lines| lines.item_count == 11 } + tmux.send_keys :BTab, :BTab, :BTab + tmux.until(1) { |lines| lines[-2].include? '(3)' } + tmux.send_keys :Enter + tmux.until do |lines| + tmux.send_keys 'C-L' + lines[-1] == "ls /tmp 1 2" + end + end + + def test_unset_completion + tmux.send_keys 'export FOO=BAR', :Enter + tmux.prepare + + # Using tmux + tmux.send_keys 'unset FOO**', :Tab, pane: 0 + tmux.until(1) { |lines| lines[-2].include? ' 1/' } + tmux.send_keys :Enter + tmux.until { |lines| lines[-1] == 'unset FOO' } + tmux.send_keys 'C-c' + + # FZF_TMUX=0 + new_shell + tmux.send_keys 'unset FOO**', :Tab + tmux.until { |lines| lines[-2].include? ' 1/' } + tmux.send_keys :Enter + tmux.until { |lines| lines[-1] == 'unset FOO' } + end end class TestBash < TestBase From 26b91007093914c97d8b91254b0f632543f469de Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 16 Feb 2016 12:35:35 +0900 Subject: [PATCH 103/658] Minor code cleanup --- shell/completion.zsh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 8288cb2..914f6b1 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -80,9 +80,9 @@ _fzf_dir_completion() { } _fzf_feed_fifo() ( - rm -f "$fifo" - mkfifo "$fifo" - cat <&0 > "$fifo" & + rm -f "$1" + mkfifo "$1" + cat <&0 > "$1" & ) _fzf_complete() { From e3401a0645d6d9025d1a777015de92d72c778b5f Mon Sep 17 00:00:00 2001 From: Sergey Vlasov Date: Tue, 16 Feb 2016 11:28:40 +0200 Subject: [PATCH 104/658] Go 1.3 compatibility --- src/matcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matcher.go b/src/matcher.go index 3ea2fbe..d320697 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -200,7 +200,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { } partialResults := make([][]*Item, numSlices) - for range slices { + for _, _ = range slices { partialResult := <-resultChan partialResults[partialResult.index] = partialResult.matches } From e72a360337548ffe6db0c97e9c79416e42521450 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 18 Feb 2016 01:46:18 +0900 Subject: [PATCH 105/658] Minor refactoring - Slightly more efficient processing of Options - Do not return reference type arguments that are mutated inside the function - Use util.Constrain function when appropriate --- src/options.go | 51 +++++++++++++++--------------- src/options_test.go | 75 ++++++++++++++++++++++++++++++++++++--------- src/terminal.go | 10 +----- 3 files changed, 88 insertions(+), 48 deletions(-) diff --git a/src/options.go b/src/options.go index 962e516..723c7fc 100644 --- a/src/options.go +++ b/src/options.go @@ -163,7 +163,7 @@ func defaultOptions() *Options { Filter: nil, ToggleSort: false, Expect: make(map[int]string), - Keymap: defaultKeymap(), + Keymap: make(map[int]actionType), Execmap: make(map[int]string), PrintQuery: false, ReadZero: false, @@ -484,7 +484,7 @@ const ( escapedComma = 1 ) -func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) { +func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) { if executeRegexp == nil { // Backreferences are not supported. // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') @@ -592,7 +592,6 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b keymap[key] = actNextHistory case "toggle-sort": keymap[key] = actToggleSort - toggleSort = true default: if isExecuteAction(actLower) { var offset int @@ -613,7 +612,6 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b } } } - return keymap, execmap, toggleSort } func isExecuteAction(str string) bool { @@ -635,13 +633,12 @@ func isExecuteAction(str string) bool { return false } -func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType { +func parseToggleSort(keymap map[int]actionType, str string) { keys := parseKeyChords(str, "key name required") if len(keys) != 1 { errorExit("multiple keys specified") } keymap[firstKey(keys)] = actToggleSort - return keymap } func strLines(str string) []string { @@ -691,7 +688,6 @@ func parseMargin(margin string) [4]string { } func parseOptions(opts *Options, allArgs []string) { - keymap := make(map[int]actionType) var historyMax int if opts.History == nil { historyMax = defaultHistoryMax @@ -741,8 +737,7 @@ func parseOptions(opts *Options, allArgs []string) { case "--tiebreak": opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) case "--bind": - keymap, opts.Execmap, opts.ToggleSort = - parseKeymap(keymap, opts.Execmap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required")) + parseKeymap(opts.Keymap, opts.Execmap, nextString(allArgs, &i, "bind expression required")) case "--color": spec := optionalNextString(allArgs, &i) if len(spec) == 0 { @@ -751,8 +746,7 @@ func parseOptions(opts *Options, allArgs []string) { opts.Theme = parseTheme(opts.Theme, spec) } case "--toggle-sort": - keymap = checkToggleSort(keymap, nextString(allArgs, &i, "key name required")) - opts.ToggleSort = true + parseToggleSort(opts.Keymap, nextString(allArgs, &i, "key name required")) case "-d", "--delimiter": opts.Delimiter = delimiterRegexp(nextString(allArgs, &i, "delimiter required")) case "-n", "--nth": @@ -869,8 +863,7 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, _ := optString(arg, "-s", "--sort="); match { opts.Sort = 1 // Don't care } else if match, value := optString(arg, "--toggle-sort="); match { - keymap = checkToggleSort(keymap, value) - opts.ToggleSort = true + parseToggleSort(opts.Keymap, value) } else if match, value := optString(arg, "--expect="); match { opts.Expect = parseKeyChords(value, "key names required") } else if match, value := optString(arg, "--tiebreak="); match { @@ -878,8 +871,7 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, value := optString(arg, "--color="); match { opts.Theme = parseTheme(opts.Theme, value) } else if match, value := optString(arg, "--bind="); match { - keymap, opts.Execmap, opts.ToggleSort = - parseKeymap(keymap, opts.Execmap, opts.ToggleSort, value) + parseKeymap(opts.Keymap, opts.Execmap, value) } else if match, value := optString(arg, "--history="); match { setHistory(value) } else if match, value := optString(arg, "--history-size="); match { @@ -905,21 +897,28 @@ func parseOptions(opts *Options, allArgs []string) { if opts.Tabstop < 1 { errorExit("tab stop must be a positive integer") } +} - // Change default actions for CTRL-N / CTRL-P when --history is used +func postProcessOptions(opts *Options) { + // Default actions for CTRL-N / CTRL-P when --history is set if opts.History != nil { - if _, prs := keymap[curses.CtrlP]; !prs { - keymap[curses.CtrlP] = actPreviousHistory + if _, prs := opts.Keymap[curses.CtrlP]; !prs { + opts.Keymap[curses.CtrlP] = actPreviousHistory } - if _, prs := keymap[curses.CtrlN]; !prs { - keymap[curses.CtrlN] = actNextHistory + if _, prs := opts.Keymap[curses.CtrlN]; !prs { + opts.Keymap[curses.CtrlN] = actNextHistory } } - // Override default key bindings - for key, act := range keymap { - opts.Keymap[key] = act + // Extend the default key map + keymap := defaultKeymap() + for key, act := range opts.Keymap { + if act == actToggleSort { + opts.ToggleSort = true + } + keymap[key] = act } + opts.Keymap = keymap // If we're not using extended search mode, --nth option becomes irrelevant // if it contains the whole range @@ -939,9 +938,13 @@ func ParseOptions() *Options { // Options from Env var words, _ := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS")) - parseOptions(opts, words) + if len(words) > 0 { + parseOptions(opts, words) + } // Options from command-line arguments parseOptions(opts, os.Args[1:]) + + postProcessOptions(opts) return opts } diff --git a/src/options_test.go b/src/options_test.go index ef86abe..81845d2 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -96,6 +96,7 @@ func TestIrrelevantNth(t *testing.T) { opts := defaultOptions() words := []string{"--nth", "..", "-x"} parseOptions(opts, words) + postProcessOptions(opts) if len(opts.Nth) != 0 { t.Errorf("nth should be empty: %s", opts.Nth) } @@ -104,6 +105,7 @@ func TestIrrelevantNth(t *testing.T) { { opts := defaultOptions() parseOptions(opts, words) + postProcessOptions(opts) if len(opts.Nth) != 0 { t.Errorf("nth should be empty: %s", opts.Nth) } @@ -112,6 +114,7 @@ func TestIrrelevantNth(t *testing.T) { opts := defaultOptions() words = append(words, "-x") parseOptions(opts, words) + postProcessOptions(opts) if len(opts.Nth) != 2 { t.Errorf("nth should not be empty: %s", opts.Nth) } @@ -231,15 +234,11 @@ func TestBind(t *testing.T) { keymap := defaultKeymap() execmap := make(map[int]string) check(actBeginningOfLine, keymap[curses.CtrlA]) - keymap, execmap, toggleSort := - parseKeymap(keymap, execmap, false, - "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 {};,"+ - "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ - ",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)") - if !toggleSort { - t.Errorf("toggleSort not set") - } + parseKeymap(keymap, execmap, + "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 {};,"+ + "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ + ",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)") check(actKillLine, keymap[curses.CtrlA]) check(actToggleSort, keymap[curses.CtrlB]) check(actPageUp, keymap[curses.AltZ+'c']) @@ -259,15 +258,11 @@ func TestBind(t *testing.T) { checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X']) for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { - keymap, execmap, toggleSort = - parseKeymap(keymap, execmap, false, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) + parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) } - keymap, execmap, toggleSort = parseKeymap(keymap, execmap, false, "f1:abort") - if toggleSort { - t.Errorf("toggleSort set") - } + parseKeymap(keymap, execmap, "f1:abort") check(actAbort, keymap[curses.F1]) } @@ -328,3 +323,53 @@ func TestParseNilTheme(t *testing.T) { t.Errorf("color should now be enabled and customized") } } + +func TestDefaultCtrlNP(t *testing.T) { + check := func(words []string, key int, expected actionType) { + opts := defaultOptions() + parseOptions(opts, words) + postProcessOptions(opts) + if opts.Keymap[key] != expected { + t.Error() + } + } + check([]string{}, curses.CtrlN, actDown) + check([]string{}, curses.CtrlP, actUp) + + check([]string{"--bind=ctrl-n:accept"}, curses.CtrlN, actAccept) + check([]string{"--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) + + hist := "--history=/tmp/foo" + check([]string{hist}, curses.CtrlN, actNextHistory) + check([]string{hist}, curses.CtrlP, actPreviousHistory) + + check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlN, actAccept) + check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlP, actPreviousHistory) + + check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlN, actNextHistory) + check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) +} + +func TestToggle(t *testing.T) { + optsFor := func(words ...string) *Options { + opts := defaultOptions() + parseOptions(opts, words) + postProcessOptions(opts) + return opts + } + + opts := optsFor() + if opts.ToggleSort { + t.Error() + } + + opts = optsFor("--bind=a:toggle-sort") + if !opts.ToggleSort { + t.Error() + } + + opts = optsFor("--bind=a:toggle-sort", "--bind=a:up") + if opts.ToggleSort { + t.Error() + } +} diff --git a/src/terminal.go b/src/terminal.go index f1ddc48..3c6f47c 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1122,15 +1122,7 @@ func (t *Terminal) constrain() { diffpos := t.cy - t.offset t.cy = util.Constrain(t.cy, 0, count-1) - - if t.cy > t.offset+(height-1) { - // Ceil - t.offset = t.cy - (height - 1) - } else if t.offset > t.cy { - // Floor - t.offset = t.cy - } - + t.offset = util.Constrain(t.offset, t.cy-height+1, t.cy) // Adjustment if count-t.offset < height { t.offset = util.Max(0, count-height) From 1b9ca314b87283b8daf2e0e16201779dadc1b5fa Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 20 Feb 2016 21:15:34 +0900 Subject: [PATCH 106/658] Update build script - GOPATH is no longer required - fzf repository does not have to be in GOPATH - Build Linux binary with Go 1.5.3 --- .gitignore | 3 +- src/Dockerfile.android | 6 +--- src/Dockerfile.arch | 6 +--- src/Dockerfile.centos | 8 ++--- src/Dockerfile.ubuntu | 6 +--- src/Makefile | 71 +++++++++++++++++++++++------------------- 6 files changed, 46 insertions(+), 54 deletions(-) diff --git a/.gitignore b/.gitignore index 0915467..d2372a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ bin -src/fzf/fzf_* +src/fzf/fzf-* +gopath pkg Gemfile.lock .DS_Store diff --git a/src/Dockerfile.android b/src/Dockerfile.android index 540858f..d6a0ba4 100644 --- a/src/Dockerfile.android +++ b/src/Dockerfile.android @@ -11,7 +11,6 @@ RUN cd / && curl \ tar -xz && mv go go1.4 && \ sed -i 's@#define PTHREAD_KEYS_MAX 128@@' /go1.4/src/runtime/cgo/gcc_android_arm.c -ENV GOPATH /go ENV GOROOT /go1.4 ENV PATH /go1.4/bin:$PATH @@ -37,8 +36,5 @@ RUN cd / && curl \ make install && \ mv /ndk/sysroot/usr/lib/libncursesw.a /ndk/sysroot/usr/lib/libncurses.a -# Volume -VOLUME /go - # Default CMD -CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash +CMD cd /fzf/src && /bin/bash diff --git a/src/Dockerfile.arch b/src/Dockerfile.arch index d014651..77df9e4 100644 --- a/src/Dockerfile.arch +++ b/src/Dockerfile.arch @@ -10,7 +10,6 @@ RUN cd / && curl \ https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ tar -xz && mv go go1.4 -ENV GOPATH /go ENV GOROOT /go1.4 ENV PATH /go1.4/bin:$PATH @@ -20,9 +19,6 @@ RUN echo '[multilib]' >> /etc/pacman.conf && \ pacman-db-upgrade && yes | pacman -Sy gcc-multilib lib32-ncurses && \ cd $GOROOT/src && GOARCH=386 ./make.bash -# Volume -VOLUME /go - # Default CMD -CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash +CMD cd /fzf/src && /bin/bash diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos index 416d547..a754491 100644 --- a/src/Dockerfile.centos +++ b/src/Dockerfile.centos @@ -13,20 +13,16 @@ RUN cd / && curl \ # Install Go 1.5 RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz | \ + https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz | \ tar -xz && mv go go1.5 ENV GOROOT_BOOTSTRAP /go1.4 ENV GOROOT /go1.5 -ENV GOPATH /go ENV PATH /go1.5/bin:$PATH # For i386 build RUN cd $GOROOT/src && GOARCH=386 ./make.bash -# Volume -VOLUME /go - # Default CMD -CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash +CMD cd /fzf/src && /bin/bash diff --git a/src/Dockerfile.ubuntu b/src/Dockerfile.ubuntu index d9de120..b0ae875 100644 --- a/src/Dockerfile.ubuntu +++ b/src/Dockerfile.ubuntu @@ -10,7 +10,6 @@ RUN cd / && curl \ https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ tar -xz && mv go go1.4 -ENV GOPATH /go ENV GOROOT /go1.4 ENV PATH /go1.4/bin:$PATH @@ -18,9 +17,6 @@ ENV PATH /go1.4/bin:$PATH RUN apt-get install -y lib32ncurses5-dev && \ cd $GOROOT/src && GOARCH=386 ./make.bash -# Volume -VOLUME /go - # Default CMD -CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash +CMD cd /fzf/src && /bin/bash diff --git a/src/Makefile b/src/Makefile index 7eb67f7..d8b7d80 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,7 +1,3 @@ -ifndef GOPATH -$(error GOPATH is undefined) -endif - ifndef GOOS UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) @@ -15,33 +11,44 @@ ifneq ($(shell uname -m),x86_64) $(error "Build on $(UNAME_M) is not supported, yet.") endif -SOURCES := $(wildcard *.go */*.go) -BINDIR := ../bin - +SOURCES := $(wildcard *.go */*.go) +ROOTDIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +BINDIR := $(shell dirname $(ROOTDIR))/bin +GOPATH := $(shell dirname $(ROOTDIR))/gopath +SRCDIR := $(GOPATH)/src/github.com/junegunn/fzf/src +DOCKEROPTS := -i -t -v $(ROOTDIR):/fzf/src BINARY32 := fzf-$(GOOS)_386 BINARY64 := fzf-$(GOOS)_amd64 BINARYARM7 := fzf-$(GOOS)_arm7 VERSION := $(shell awk -F= '/version =/ {print $$2}' constants.go | tr -d "\" ") -RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 -RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 -RELEASEARM7 = fzf-$(VERSION)-$(GOOS)_arm7 +RELEASE32 := fzf-$(VERSION)-$(GOOS)_386 +RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64 +RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7 +export GOPATH all: release -release: build +release: test build -cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ rm -f $(RELEASE32) $(RELEASE64) -build: test fzf/$(BINARY32) fzf/$(BINARY64) +build: fzf/$(BINARY32) fzf/$(BINARY64) -android-build: - cd fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-extldflags=-pie" -o $(BINARYARM7) - cd fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) && \ +$(SRCDIR): + mkdir -p $(shell dirname $(SRCDIR)) + ln -s $(ROOTDIR) $(SRCDIR) + +deps: $(SRCDIR) $(SOURCES) + cd $(SRCDIR) && go get + +android-build: $(SRCDIR) + cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get + cd $(SRCDIR)/fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-extldflags=-pie" -o $(BINARYARM7) + cd $(SRCDIR)/fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) && \ rm -f $(RELEASEARM7) -test: - go get +test: deps SHELL=/bin/sh go test -v ./... install: $(BINDIR)/fzf @@ -52,10 +59,10 @@ uninstall: clean: cd fzf && rm -f fzf-* -fzf/$(BINARY32): $(SOURCES) +fzf/$(BINARY32): deps cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -o $(BINARY32) -fzf/$(BINARY64): $(SOURCES) +fzf/$(BINARY64): deps cd fzf && go build -a -tags "$(TAGS)" -o $(BINARY64) $(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) @@ -78,29 +85,29 @@ docker-android: docker build -t junegunn/android-sandbox - < Dockerfile.android arch: docker-arch - docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ - sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' + docker run $(DOCKEROPTS) junegunn/$@-sandbox \ + sh -c 'cd /fzf/src; /bin/bash' ubuntu: docker-ubuntu - docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ - sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' + docker run $(DOCKEROPTS) junegunn/$@-sandbox \ + sh -c 'cd /fzf/src; /bin/bash' centos: docker-centos - docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \ - sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' + docker run $(DOCKEROPTS) junegunn/$@-sandbox \ + sh -c 'cd /fzf/src; /bin/bash' linux: docker-centos - docker run -i -t -v $(GOPATH):/go junegunn/centos-sandbox \ - /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; make TAGS=static' + docker run $(DOCKEROPTS) junegunn/centos-sandbox \ + /bin/bash -ci 'cd /fzf/src; make TAGS=static' ubuntu-android: docker-android - docker run -i -t -v $(GOPATH):/go junegunn/android-sandbox \ - sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' + docker run $(DOCKEROPTS) junegunn/android-sandbox \ + sh -c 'cd /fzf/src; /bin/bash' android: docker-android - docker run -i -t -v $(GOPATH):/go junegunn/android-sandbox \ - /bin/bash -ci 'cd /go/src/github.com/junegunn/fzf/src; GOOS=android make android-build' + docker run $(DOCKEROPTS) junegunn/android-sandbox \ + /bin/bash -ci 'cd /fzf/src; GOOS=android make android-build' -.PHONY: all build release test install uninstall clean docker \ +.PHONY: all build deps release test install uninstall clean \ linux arch ubuntu centos docker-arch docker-ubuntu docker-centos \ android-build docker-android ubuntu-android android From 42c006d07c7d795517aa6040f4ac789b371a3ea6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 21 Feb 2016 22:11:28 +0900 Subject: [PATCH 107/658] Update install script to try "go get ..." Related: #470, #497 --- install | 49 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/install b/install index 8177e7b..225d493 100755 --- a/install +++ b/install @@ -102,6 +102,16 @@ symlink() { fi } +link_fzf_in_path() { + if which_fzf="$(which fzf 2> /dev/null)"; then + echo " - Found in \$PATH" + echo " - Creating symlink: $which_fzf -> bin/fzf" + (cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf) + check_binary && return + fi + return 1 +} + download() { echo "Downloading bin/fzf ..." if [ $pre = 0 ]; then @@ -112,12 +122,7 @@ download() { if [ -x "$fzf_base"/bin/$1 ]; then symlink $1 && check_binary && return fi - if which_fzf="$(which fzf 2> /dev/null)"; then - echo " - Found in \$PATH" - echo " - Creating symlink: $which_fzf -> bin/fzf" - (cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf) - check_binary && return - fi + link_fzf_in_path && return fi mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin if [ $? -ne 0 ]; then @@ -155,14 +160,7 @@ case "$archi" in *) binary_available=0 binary_error=1 ;; esac -cd "$fzf_base" -if [ -n "$binary_error" ]; then - if [ $binary_available -eq 0 ]; then - echo "No prebuilt binary for $archi ... " - else - echo " - $binary_error !!!" - exit 1 - fi +install_ruby_fzf() { echo "Installing legacy Ruby version ..." # ruby executable @@ -228,6 +226,29 @@ if [ -n "$binary_error" ]; then echo "$fzf_cmd \"\$@\"" >> "$fzf_base"/bin/fzf chmod +x "$fzf_base"/bin/fzf echo "OK" +} + +cd "$fzf_base" +if [ -n "$binary_error" ]; then + if [ $binary_available -eq 0 ]; then + echo "No prebuilt binary for $archi ..." + if which go > /dev/null 2>&1; then + echo -n "Building binary (go get github.com/junegunn/fzf/src/fzf) ... " + if go get github.com/junegunn/fzf/src/fzf; then + echo "OK" + link_fzf_in_path + else + echo "Failed to build binary ..." + install_ruby_fzf + fi + else + echo "go executable not found. Cannot build binary ..." + install_ruby_fzf + fi + else + echo " - $binary_error !!!" + exit 1 + fi fi [[ "$*" =~ "--bin" ]] && exit 0 From a7bc9d53519569fc4e024f4ca3c733e2702ebe42 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Tue, 23 Feb 2016 00:36:36 -0500 Subject: [PATCH 108/658] s:callback: Always return list. Fixes "E706: Variable type mismatch for: ret" when an exception is caught. --- plugin/fzf.vim | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 80b85d2..ac508a6 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -239,7 +239,7 @@ function! s:exit_handler(code, command, ...) return 1 endfunction -function! s:execute(dict, command, temps) +function! s:execute(dict, command, temps) abort call s:pushd(a:dict) silent! !clear 2> /dev/null let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#') @@ -255,7 +255,7 @@ function! s:execute(dict, command, temps) return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : [] endfunction -function! s:execute_tmux(dict, command, temps) +function! s:execute_tmux(dict, command, temps) abort let command = a:command if s:pushd(a:dict) " -c '#{pane_current_path}' is only available on tmux 1.9 or above @@ -320,7 +320,7 @@ function! s:split(dict) endtry endfunction -function! s:execute_term(dict, command, temps) +function! s:execute_term(dict, command, temps) abort call s:split(a:dict) let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF' } @@ -369,11 +369,10 @@ function! s:execute_term(dict, command, temps) return [] endfunction -function! s:callback(dict, temps) +function! s:callback(dict, temps) abort +let lines = [] try - if !filereadable(a:temps.result) - let lines = [] - else + if filereadable(a:temps.result) let lines = readfile(a:temps.result) if has_key(a:dict, 'sink') for line in lines @@ -392,12 +391,12 @@ try for tf in values(a:temps) silent! call delete(tf) endfor - - return lines catch if stridx(v:exception, ':E325:') < 0 echoerr v:exception endif +finally + return lines endtry endfunction From 1d6f05f974fa31db54ede4fefe0d85cf50491e54 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 26 Feb 2016 23:36:07 +0900 Subject: [PATCH 109/658] [man] Fix invalid exit status in man page Close #511 --- man/man1/fzf.1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 17f3153..ef48770 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -364,7 +364,11 @@ Default options. e.g. \fBexport FZF_DEFAULT_OPTS="--extended --cycle"\fR .SH EXIT STATUS .BR 0 " Normal exit" .br -.BR 1 " Interrupted with \fBCTRL-C\fR or \fBESC\fR" +.BR 1 " No match" +.br +.BR 2 " Error" +.br +.BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR" .SH FIELD INDEX EXPRESSION From cf8afc527e27600ac9f91127c57f24a9c6727b6c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 2 Mar 2016 02:04:38 +0900 Subject: [PATCH 110/658] Remove .gitmodules --- .gitmodules | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29..0000000 From 2ccdf21a1fa0ce64123fe89e1b7931240420e8e6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 2 Mar 2016 03:06:21 +0900 Subject: [PATCH 111/658] Add --hscroll-off=COL option Close #513 --- man/man1/fzf.1 | 7 ++++++- src/options.go | 12 ++++++++++++ src/terminal.go | 9 +++++---- src/util/util.go | 8 ++++++++ test/test_go.rb | 12 ++++++++++++ 5 files changed, 43 insertions(+), 5 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index ef48770..1a99a54 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 2016" "fzf 0.11.3" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Mar 2016" "fzf 0.11.4" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -175,6 +175,11 @@ Enable cyclic scroll .B "--no-hscroll" Disable horizontal scroll .TP +.BI "--hscroll-off=" "COL" +Number of screen columns to keep to the right of the highlighted substring +(default: 10). Setting it to a large value will cause the text to be positioned +on the center of the screen. +.TP .B "--inline-info" Display finder info inline with the query .TP diff --git a/src/options.go b/src/options.go index 723c7fc..dfd9a1b 100644 --- a/src/options.go +++ b/src/options.go @@ -42,6 +42,8 @@ const usage = `usage: fzf [options] --tabstop=SPACES Number of spaces for a tab character (default: 8) --cycle Enable cyclic scroll --no-hscroll Disable horizontal scroll + --hscroll-off=COL Number of screen columns to keep to the right of the + highlighted substring (default: 10) --inline-info Display finder info inline with the query --prompt=STR Input prompt (default: '> ') --bind=KEYBINDS Custom key bindings. Refer to the man page. @@ -108,6 +110,7 @@ type Options struct { Reverse bool Cycle bool Hscroll bool + HscrollOff int InlineInfo bool Prompt string Query string @@ -155,6 +158,7 @@ func defaultOptions() *Options { Reverse: false, Cycle: false, Hscroll: true, + HscrollOff: 10, InlineInfo: false, Prompt: "> ", Query: "", @@ -795,6 +799,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.Hscroll = true case "--no-hscroll": opts.Hscroll = false + case "--hscroll-off": + opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required") case "--inline-info": opts.InlineInfo = true case "--no-inline-info": @@ -884,6 +890,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.Margin = parseMargin(value) } else if match, value := optString(arg, "--tabstop="); match { opts.Tabstop = atoi(value) + } else if match, value := optString(arg, "--hscroll-off="); match { + opts.HscrollOff = atoi(value) } else { errorExit("unknown option: " + arg) } @@ -894,6 +902,10 @@ func parseOptions(opts *Options, allArgs []string) { errorExit("header lines must be a non-negative integer") } + if opts.HscrollOff < 0 { + errorExit("hscroll offset must be a non-negative integer") + } + if opts.Tabstop < 1 { errorExit("tab stop must be a positive integer") } diff --git a/src/terminal.go b/src/terminal.go index 3c6f47c..d95cfad 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -26,6 +26,7 @@ type Terminal struct { prompt string reverse bool hscroll bool + hscrollOff int cx int cy int offset int @@ -210,6 +211,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { prompt: opts.Prompt, reverse: opts.Reverse, hscroll: opts.Hscroll, + hscrollOff: opts.HscrollOff, cx: len(input), cy: 0, offset: 0, @@ -556,11 +558,9 @@ func trimLeft(runes []rune, width int) ([]rune, int32) { } func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) { - var maxe int32 + var maxe int for _, offset := range item.offsets { - if offset[1] > maxe { - maxe = offset[1] - } + maxe = util.Max(maxe, int(offset[1])) } // Overflow @@ -568,6 +568,7 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c copy(text, item.text) offsets := item.colorOffsets(col2, bold, current) maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3] + maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) fullWidth := displayWidth(text) if fullWidth > maxWidth { if t.hscroll { diff --git a/src/util/util.go b/src/util/util.go index ab9e766..4f3d409 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -21,6 +21,14 @@ func Max(first int, items ...int) int { return max } +// Min returns the smallest integer +func Min(first int, second int) int { + if first <= second { + return first + } + return second +} + // Min32 returns the smallest 32-bit integer func Min32(first int32, second int32) int32 { if first <= second { diff --git a/test/test_go.rb b/test/test_go.rb index 7a2d003..28cb833 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1137,6 +1137,18 @@ class TestGoFZF < TestBase `seq 10 | #{FZF} -f '1 | !1'`.lines.map(&:chomp) end + def test_hscroll_off + writelines tempname, ['=' * 10000 + '0123456789'] + [0, 3, 6].each do |off| + tmux.prepare + tmux.send_keys "#{FZF} --hscroll-off=#{off} -q 0 < #{tempname}", :Enter + tmux.until { |lines| lines[-3].end_with?((0..off).to_a.join + '..') } + tmux.send_keys '9' + tmux.until { |lines| lines[-3].end_with? '789' } + tmux.send_keys :Enter + end + end + private def writelines path, lines File.unlink path while File.exists? path From 6cf54833f7d8319ec764ef0878295c8945c71fb4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 2 Mar 2016 03:23:32 +0900 Subject: [PATCH 112/658] Fix flaky test case --- test/test_go.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index 28cb833..a06ed8e 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -897,13 +897,15 @@ class TestGoFZF < TestBase # Custom script to use as $SHELL output = tempname + '.out' File.unlink output rescue nil - writelines tempname, ['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"] + writelines tempname, ['#!/usr/bin/env bash', "echo $1 / $2 > #{output}", "sync"] system "chmod +x #{tempname}" tmux.send_keys "echo foo | SHELL=#{tempname} fzf --bind 'enter:execute:{}bar'", :Enter tmux.until { |lines| lines[-2].include? '1/1' } tmux.send_keys :Enter + tmux.until { |lines| lines[-2].include? '1/1' } tmux.send_keys 'C-c' + tmux.prepare assert_equal ['-c / "foo"bar'], File.readlines(output).map(&:chomp) ensure File.unlink output rescue nil From 09a2ab39fe4c1c57d789d8b7c6dff91e47885a71 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 2 Mar 2016 23:59:42 +0900 Subject: [PATCH 113/658] [bash] Fix shellcheck warnings Close #516 --- shell/completion.bash | 36 ++++++++++++++++++------------------ shell/key-bindings.bash | 10 +++++----- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 05b4ef3..9005af7 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -75,11 +75,11 @@ _fzf_opts_completion() { case "${prev}" in --tiebreak) - COMPREPLY=( $(compgen -W "length begin end index" -- ${cur}) ) + COMPREPLY=( $(compgen -W "length begin end index" -- "$cur") ) return 0 ;; --color) - COMPREPLY=( $(compgen -W "dark light 16 bw" -- ${cur}) ) + COMPREPLY=( $(compgen -W "dark light 16 bw" -- "$cur") ) return 0 ;; --history) @@ -88,8 +88,8 @@ _fzf_opts_completion() { ;; esac - if [[ ${cur} =~ ^-|\+ ]]; then - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + if [[ "$cur" =~ ^-|\+ ]]; then + COMPREPLY=( $(compgen -W "${opts}" -- "$cur") ) return 0 fi @@ -108,32 +108,32 @@ _fzf_handle_dynamic_completion() { elif [ -n "$_fzf_completion_loader" ]; then _completion_loader "$@" ret=$? - eval $(complete | \grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter) - source $BASH_SOURCE + eval "$(complete | \grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)" + source "${BASH_SOURCE[0]}" return $ret fi } __fzf_generic_path_completion() { local cur base dir leftover matches trigger cmd fzf - [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" - cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') + [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + cmd=$(echo "${COMP_WORDS[0]}" | sed 's/[^a-z0-9_=]/_/g') COMPREPLY=() trigger=${FZF_COMPLETION_TRIGGER-'**'} cur="${COMP_WORDS[COMP_CWORD]}" - if [[ ${cur} == *"$trigger" ]]; then + if [[ "$cur" == *"$trigger" ]]; then base=${cur:0:${#cur}-${#trigger}} - eval base=$base + eval "base=$base" dir="$base" - while [ 1 ]; do - if [ -z "$dir" -o -d "$dir" ]; then + while true; do + if [ -z "$dir" ] || [ -d "$dir" ]; then leftover=${base/#"$dir"} leftover=${leftover/#\/} [ -z "$dir" ] && dir='.' [ "$dir" != "/" ] && dir="${dir/%\//}" tput sc - matches=$(eval "$1 $(printf %q "$dir")" | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do + matches=$(eval "$1 $(printf %q "$dir")" | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read -r item; do printf "%q$3 " "$item" done) matches=${matches% } @@ -159,13 +159,13 @@ __fzf_generic_path_completion() { _fzf_complete() { local cur selected trigger cmd fzf post post="$(caller 0 | awk '{print $2}')_post" - type -t $post > /dev/null 2>&1 || post=cat - [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + type -t "$post" > /dev/null 2>&1 || post=cat + [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" - cmd=$(echo ${COMP_WORDS[0]} | sed 's/[^a-z0-9_=]/_/g') + cmd=$(echo "${COMP_WORDS[0]}" | sed 's/[^a-z0-9_=]/_/g') trigger=${FZF_COMPLETION_TRIGGER-'**'} cur="${COMP_WORDS[COMP_CWORD]}" - if [[ ${cur} == *"$trigger" ]]; then + if [[ "$cur" == *"$trigger" ]]; then cur=${cur:0:${#cur}-${#trigger}} tput sc @@ -200,7 +200,7 @@ _fzf_complete_kill() { [ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1 local selected fzf - [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" tput sc selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ') tput rc diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index beaa5dd..34adc2b 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -5,7 +5,7 @@ __fzf_select__() { -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" - eval "$cmd" | fzf -m | while read item; do + eval "$cmd" | fzf -m | while read -r item; do printf '%q ' "$item" done echo @@ -14,7 +14,7 @@ __fzf_select__() { if [[ $- =~ i ]]; then __fzfcmd() { - [ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" + [ "${FZF_TMUX:-1}" != 0 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" } __fzf_select_tmux__() { @@ -30,10 +30,10 @@ __fzf_select_tmux__() { } __fzf_select_tmux_auto__() { - if [ ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ]; then + if [ "${FZF_TMUX:-1}" != 0 ] && [ ${LINES:-40} -gt 15 ]; then __fzf_select_tmux__ else - tmux send-keys -t $TMUX_PANE "$(__fzf_select__)" + tmux send-keys -t "$TMUX_PANE" "$(__fzf_select__)" fi } @@ -61,7 +61,7 @@ __fzf_history__() ( __use_tmux=0 __use_tmux_auto=0 if [ -n "$TMUX_PANE" ]; then - [ ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1 + [ "${FZF_TMUX:-1}" != 0 ] && [ ${LINES:-40} -gt 15 ] && __use_tmux=1 [ $BASH_VERSINFO -gt 3 ] && __use_tmux_auto=1 fi From b47ab633e297a64a4dacd59dea008144638d8e97 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 3 Mar 2016 01:57:28 +0900 Subject: [PATCH 114/658] 0.11.4 --- CHANGELOG.md | 6 ++++++ install | 4 ++-- src/constants.go | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 278c196..692d836 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +0.11.4 +------ + +- Added `--hscroll-off=COL` option (default: 10) (#513) +- Some fixes in Vim plugin and shell extensions + 0.11.3 ------ diff --git a/install b/install index 225d493..8eaf6f7 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.11.3 pre=1 || - version=0.11.3 pre=0 +[[ "$@" =~ --pre ]] && version=0.11.4 pre=1 || + version=0.11.4 pre=0 auto_completion= key_bindings= diff --git a/src/constants.go b/src/constants.go index e859f3b..0832420 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.11.3" + version = "0.11.4" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 3073ca3e5a486e004c6d2f48d72bef8e49c35818 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 4 Mar 2016 15:23:07 +0900 Subject: [PATCH 115/658] [neovim] Take total number of tab pages into account (#520) This fixes the problem where a new tab page is not closed when the following configuration is used: let g:fzf_layout = { 'window': 'execute (tabpagenr()-1)."tabnew"' } --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index ac508a6..159b205 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -285,7 +285,7 @@ function! s:calc_size(max, val, dict) endfunction function! s:getpos() - return {'tab': tabpagenr(), 'win': winnr(), 'cnt': winnr('$')} + return {'tab': tabpagenr(), 'win': winnr(), 'cnt': winnr('$'), 'tcnt': tabpagenr('$')} endfunction function! s:split(dict) From f91cbd5688b9722088809ed72561a32a48eab174 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 6 Mar 2016 11:23:37 +0900 Subject: [PATCH 116/658] Add ISSUE_TEMPLATE.md Close #500 --- .github/ISSUE_TEMPLATE.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..a3fa068 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,28 @@ + +- Category + - [ ] fzf binary + - [ ] Key bindings + - [ ] Completion + - [ ] Vim + - [ ] Neovim + - [ ] Etc. +- OS + - [ ] Linux + - [ ] Mac OS X + - [ ] Windows + - [ ] Etc. +- Shell + - [ ] bash + - [ ] zsh + - [ ] fish + + + From 444a67cafa569b999cc1569f2aacd27e9e537e3b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 6 Mar 2016 12:46:28 +0900 Subject: [PATCH 117/658] Fix flaky test cases --- test/test_go.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index a06ed8e..7c562a8 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -394,6 +394,7 @@ class TestGoFZF < TestBase tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter tmux.until { |lines| lines[-2].include? '1000/1000' } tmux.send_keys '99' + tmux.until { |lines| lines[-2].include? '28/1000' } tmux.send_keys :BTab, :BTab, :BTab tmux.until { |lines| lines[-2].include?('(3)') } tmux.send_keys :Enter @@ -877,15 +878,19 @@ class TestGoFZF < TestBase def test_execute_multi output = '/tmp/fzf-test-execute-multi' - opts = %[--multi --bind \\"alt-a:execute-multi(echo '[{}], @{}@' >> #{output})\\"] + opts = %[--multi --bind \\"alt-a:execute-multi(echo '[{}], @{}@' >> #{output}; sync)\\"] tmux.send_keys "seq 100 | #{fzf opts}", :Enter tmux.until { |lines| lines[-2].include? '100/100' } tmux.send_keys :Escape, :a + tmux.until { |lines| lines[-2].include? '/100' } tmux.send_keys :BTab, :BTab, :BTab tmux.send_keys :Escape, :a + tmux.until { |lines| lines[-2].include? '/100' } tmux.send_keys :Tab, :Tab tmux.send_keys :Escape, :a + tmux.until { |lines| lines[-2].include? '/100' } tmux.send_keys :Enter + tmux.prepare readonce assert_equal ['["1"], @"1"@', '["1" "2" "3"], @"1" "2" "3"@', '["1" "2" "4"], @"1" "2" "4"@'], File.readlines(output).map(&:chomp) @@ -1314,6 +1319,7 @@ module CompletionTest lines[-2].include?('100/') && lines[-3].include?('/tmp/fzf-test/.hidden-') end + tmux.send_keys :Enter ensure ['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f| FileUtils.rm_rf File.expand_path(f) From f704b94603f386c7b6458ba358816f52bbad2b36 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 6 Mar 2016 13:56:29 +0900 Subject: [PATCH 118/658] [neovim] Open tab before current tab Related: https://github.com/junegunn/gv.vim/issues/19 --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 159b205..d1c847e 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -313,7 +313,7 @@ function! s:split(dict) if s:present(a:dict, 'window') execute a:dict.window else - tabnew + execute (tabpagenr()-1).'tabnew' endif finally setlocal winfixwidth winfixheight buftype=nofile bufhidden=wipe nobuflisted From 6ea760a336edb18f7d7ee38dd7b1ed3040a7a577 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 15 Mar 2016 20:17:29 +0900 Subject: [PATCH 119/658] Make 32-bit linux binary (partially) static (#523) --- src/Dockerfile.centos | 4 ++++ src/Makefile | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos index a754491..2024482 100644 --- a/src/Dockerfile.centos +++ b/src/Dockerfile.centos @@ -16,6 +16,10 @@ RUN cd / && curl \ https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz | \ tar -xz && mv go go1.5 +# Install RPMs for building static 32-bit binary +RUN curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.7/os/i386/Packages/ncurses-static-5.7-4.20090207.el6.i686.rpm -o rpm && rpm -i rpm && \ + curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.7/os/i386/Packages/gpm-static-1.20.6-12.el6.i686.rpm -o rpm && rpm -i rpm + ENV GOROOT_BOOTSTRAP /go1.4 ENV GOROOT /go1.5 ENV PATH /go1.5/bin:$PATH diff --git a/src/Makefile b/src/Makefile index d8b7d80..cd28289 100644 --- a/src/Makefile +++ b/src/Makefile @@ -60,7 +60,7 @@ clean: cd fzf && rm -f fzf-* fzf/$(BINARY32): deps - cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -o $(BINARY32) + cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -tags "$(TAGS)" -o $(BINARY32) fzf/$(BINARY64): deps cd fzf && go build -a -tags "$(TAGS)" -o $(BINARY64) From c0652adf4c96cede1f6502d09d6073c84d41f3d5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 15 Mar 2016 20:35:25 +0900 Subject: [PATCH 120/658] [fzf-tmux] tmux 1.6 compatibility Patch submitted by @netei. Close #524. --- bin/fzf-tmux | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index f7371e1..cd4160f 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -140,14 +140,14 @@ done if [ -n "$term" -o -t 0 ]; then cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf - tmux set-window-option -q synchronize-panes off \;\ - set-window-option -q remain-on-exit off \;\ + tmux set-window-option synchronize-panes off 2> /dev/null \;\ + set-window-option remain-on-exit off 2> /dev/null \;\ split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap else mkfifo $fifo1 cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf - tmux set-window-option -q synchronize-panes off \;\ - set-window-option -q remain-on-exit off \;\ + tmux set-window-option synchronize-panes off 2> /dev/null \;\ + set-window-option remain-on-exit off 2> /dev/null \;\ split-window $opt "$envs bash $argsf" $swap cat <&0 > $fifo1 & fi From df32c058333f1499d6d500f3bd18c3eb4fce525e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 15 Mar 2016 21:18:15 +0900 Subject: [PATCH 121/658] [fzf-tmux] Fix issues on tmux 1.8 --- .github/ISSUE_TEMPLATE.md | 1 + bin/fzf-tmux | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index a3fa068..a876372 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,6 +1,7 @@ - Category - [ ] fzf binary + - [ ] fzf-tmux script - [ ] Key bindings - [ ] Completion - [ ] Vim diff --git a/bin/fzf-tmux b/bin/fzf-tmux index cd4160f..f8fffaf 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -140,15 +140,17 @@ done if [ -n "$term" -o -t 0 ]; then cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf - tmux set-window-option synchronize-panes off 2> /dev/null \;\ - set-window-option remain-on-exit off 2> /dev/null \;\ - split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap + tmux set-window-option synchronize-panes off \;\ + set-window-option remain-on-exit off \;\ + split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \ + 2>&1 > /dev/null else mkfifo $fifo1 cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf - tmux set-window-option synchronize-panes off 2> /dev/null \;\ - set-window-option remain-on-exit off 2> /dev/null \;\ - split-window $opt "$envs bash $argsf" $swap + tmux set-window-option synchronize-panes off \;\ + set-window-option remain-on-exit off \;\ + split-window $opt "$envs bash $argsf" $swap \ + 2>&1 > /dev/null cat <&0 > $fifo1 & fi cat $fifo2 From 6cc9d539783716c8e614364562cce8709be2957f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 15 Mar 2016 21:21:21 +0900 Subject: [PATCH 122/658] [fzf-tmux] Fix invalid redirection --- bin/fzf-tmux | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index f8fffaf..c1dfe53 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -143,14 +143,14 @@ if [ -n "$term" -o -t 0 ]; then tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \ - 2>&1 > /dev/null + > /dev/null 2>&1 else mkfifo $fifo1 cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ split-window $opt "$envs bash $argsf" $swap \ - 2>&1 > /dev/null + > /dev/null 2>&1 cat <&0 > $fifo1 & fi cat $fifo2 From a57b375b413e12b01e648d4fcaf91e29a53ae75d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 23 Mar 2016 03:00:20 +0900 Subject: [PATCH 123/658] Add $FZF_CTRL_R_OPTS for overriding the default options for CTRL-R Close #526 --- shell/key-bindings.bash | 2 +- shell/key-bindings.fish | 2 +- shell/key-bindings.zsh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 34adc2b..042e005 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -49,7 +49,7 @@ __fzf_history__() ( shopt -u nocaseglob nocasematch line=$( HISTTIMEFORMAT= history | - $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r | + $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS | \grep '^ *[0-9]') && if [[ $- =~ H ]]; then sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index 40a1fee..0a68b62 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -27,7 +27,7 @@ function fzf_key_bindings end function __fzf_ctrl_r - history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r > $TMPDIR/fzf.result + history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS > $TMPDIR/fzf.result and commandline (cat $TMPDIR/fzf.result) commandline -f repaint rm -f $TMPDIR/fzf.result diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 5817ee8..8d01437 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -38,7 +38,7 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { local selected num - selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "${LBUFFER//$/\\$}") ) + selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q "${LBUFFER//$/\\$}") ) if [ -n "$selected" ]; then num=$selected[1] if [ -n "$num" ]; then From 24f3ec7f33e2c7b0e66987558af1f150fcf148ca Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 23 Mar 2016 03:02:57 +0900 Subject: [PATCH 124/658] Fix FZF_CTRL_R_OPTS for zsh (#526) --- shell/key-bindings.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 8d01437..8a03585 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -38,7 +38,7 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { local selected num - selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q "${LBUFFER//$/\\$}") ) + selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r ${=FZF_CTRL_R_OPTS} -q "${LBUFFER//$/\\$}") ) if [ -n "$selected" ]; then num=$selected[1] if [ -n "$num" ]; then From 6da2e0aa1e1a220d3d9d0924bfac90b1c6775ea9 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 28 Mar 2016 13:40:28 +0100 Subject: [PATCH 125/658] Fix typo in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d8a5720..c26066c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Pros Installation ------------ -fzf project consists of the followings: +fzf project consists of the following: - `fzf` executable - `fzf-tmux` script for launching fzf in a tmux pane @@ -343,7 +343,7 @@ Tips #### Rendering issues -If you have any rendering issues, check the followings: +If you have any rendering issues, check the following: 1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it contains `256` (e.g. `xterm-256color`) From 802c1c2937e1629ebb3efa89ece8abfaaf65ea07 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 29 Mar 2016 22:28:25 +0900 Subject: [PATCH 126/658] Clean up install script - Do not create zsh files if zsh is not installed (@adam8157) - Use command -v instead of which (@netei) - Reenable --pre option Close #531 --- install | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/install b/install index 8eaf6f7..0ae39a9 100755 --- a/install +++ b/install @@ -27,7 +27,7 @@ usage: $0 [OPTIONS] EOF } -for opt in $@; do +for opt in "$@"; do case $opt in --help) help @@ -46,7 +46,7 @@ for opt in $@; do --no-update-rc) update_config=0 ;; --32) binary_arch=386 ;; --64) binary_arch=amd64 ;; - --bin) ;; + --bin|--pre) ;; *) echo "unknown option: $opt" help @@ -55,17 +55,14 @@ for opt in $@; do esac done -cd $(dirname $BASH_SOURCE) +cd "$(dirname "${BASH_SOURCE[0]}")" fzf_base="$(pwd)" -# If stdin is a tty, we are "interactive". -interactive= -[ -t 0 ] && interactive=yes - ask() { + # If stdin is a tty, we are "interactive". # non-interactive shell: wait for a linefeed # interactive shell: continue after a single keypress - [ -n "$interactive" ] && read_n='-n 1' || read_n= + read_n=$([ -t 0 ] && echo "-n 1") read -p "$1 ([y]/n) " $read_n -r echo @@ -103,7 +100,7 @@ symlink() { } link_fzf_in_path() { - if which_fzf="$(which fzf 2> /dev/null)"; then + if which_fzf="$(command -v fzf)"; then echo " - Found in \$PATH" echo " - Creating symlink: $which_fzf -> bin/fzf" (cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf) @@ -131,9 +128,9 @@ download() { fi local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz - if which curl > /dev/null; then + if command -v curl > /dev/null; then curl -fL $url | tar -xz - elif which wget > /dev/null; then + elif command -v wget > /dev/null; then wget -O - $url | tar -xz else binary_error="curl or wget not found" @@ -165,7 +162,7 @@ install_ruby_fzf() { # ruby executable echo -n "Checking Ruby executable ... " - ruby=`which ruby` + ruby=$(command -v ruby) if [ $? -ne 0 ]; then echo "ruby executable not found !!!" exit 1 @@ -173,7 +170,7 @@ install_ruby_fzf() { # System ruby is preferred system_ruby=/usr/bin/ruby - if [ -x $system_ruby -a $system_ruby != "$ruby" ]; then + if [ -x $system_ruby ] && [ $system_ruby != "$ruby" ]; then $system_ruby --disable-gems -rcurses -e0 2> /dev/null [ $? -eq 0 ] && ruby=$system_ruby fi @@ -232,7 +229,7 @@ cd "$fzf_base" if [ -n "$binary_error" ]; then if [ $binary_available -eq 0 ]; then echo "No prebuilt binary for $archi ..." - if which go > /dev/null 2>&1; then + if command -v go > /dev/null; then echo -n "Building binary (go get github.com/junegunn/fzf/src/fzf) ... " if go get github.com/junegunn/fzf/src/fzf; then echo "OK" @@ -266,7 +263,9 @@ if [ -z "$key_bindings" ]; then fi echo -for shell in bash zsh; do +has_zsh=$(command -v zsh > /dev/null && echo 1 || echo 0) +shells=$([ $has_zsh -eq 1 ] && echo "bash zsh" || echo "bash") +for shell in $shells; do echo -n "Generate ~/.fzf.$shell ... " src=~/.fzf.${shell} @@ -306,9 +305,8 @@ EOF done # fish -has_fish=0 -if [ -n "$(which fish 2> /dev/null)" ]; then - has_fish=1 +has_fish=$(command -v fish > /dev/null && echo 1 || echo 0) +if [ $has_fish -eq 1 ]; then echo -n "Update fish_user_paths ... " fish << EOF echo \$fish_user_paths | grep $fzf_base/bin > /dev/null @@ -367,12 +365,12 @@ append_line() { } echo -for shell in bash zsh; do +for shell in $shells; do [ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" "$dest" "~/.fzf.${shell}" done -if [ $key_bindings -eq 1 -a $has_fish -eq 1 ]; then +if [ $key_bindings -eq 1 ] && [ $has_fish -eq 1 ]; then bind_file=~/.config/fish/functions/fish_user_key_bindings.fish append_line $update_config "fzf_key_bindings" "$bind_file" fi @@ -380,8 +378,8 @@ fi cat << EOF Finished. Restart your shell or reload config file. source ~/.bashrc # bash - source ${ZDOTDIR:-~}/.zshrc # zsh EOF +[ $has_zsh -eq 1 ] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh" [ $has_fish -eq 1 ] && echo " fzf_key_bindings # fish"; cat << EOF Use uninstall script to remove fzf. From 2ca704405abe10f6a24994befb6a072585eadc80 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Fri, 1 Apr 2016 00:06:09 +0100 Subject: [PATCH 127/658] Fix algorithm tests --- src/algo/algo_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index 95a020b..06ec5ea 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -39,8 +39,8 @@ func TestExactMatchNaive(t *testing.T) { } func TestExactMatchNaiveBackward(t *testing.T) { - assertMatch(t, FuzzyMatch, false, true, "foobar foob", "oo", 1, 3) - assertMatch(t, FuzzyMatch, false, false, "foobar foob", "oo", 8, 10) + assertMatch(t, ExactMatchNaive, false, true, "foobar foob", "oo", 1, 3) + assertMatch(t, ExactMatchNaive, false, false, "foobar foob", "oo", 8, 10) } func TestPrefixMatch(t *testing.T) { From e5ac2ebd7c6f845d34b908455a7f12d1be40d6a4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 9 Apr 2016 21:44:07 +0900 Subject: [PATCH 128/658] [vim] Escape $ https://github.com/junegunn/fzf.vim/issues/114 --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index d1c847e..5b0c8d8 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -74,7 +74,7 @@ function! s:shellesc(arg) endfunction function! s:escape(path) - return escape(a:path, ' %#''"\') + return escape(a:path, ' $%#''"\') endfunction " Upgrade legacy options From 935e986be5fe3d6c9ca6fa578d836500a3b31525 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 12 Apr 2016 21:20:03 +0900 Subject: [PATCH 129/658] [zsh] Remove unnecessary evals --- shell/completion.zsh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 914f6b1..25c021a 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -145,7 +145,7 @@ fzf-completion() { # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags tokens=(${(z)LBUFFER}) if [ ${#tokens} -lt 1 ]; then - eval "zle ${fzf_default_completion:-expand-or-complete}" + zle ${fzf_default_completion:-expand-or-complete} return fi @@ -180,7 +180,7 @@ fzf-completion() { fi # Fall back to default completion else - eval "zle ${fzf_default_completion:-expand-or-complete}" + zle ${fzf_default_completion:-expand-or-complete} fi } From 74d1694be93d4edbeb38b34f7f531d4b0909f2c1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 14 Apr 2016 04:18:59 +0900 Subject: [PATCH 130/658] Fix #541 - Print double-click when --expect=double-click is set --- src/terminal.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index d95cfad..cd9ca93 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -711,7 +711,9 @@ func (t *Terminal) rubout(pattern string) { } func keyMatch(key int, event C.Event) bool { - return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ + return event.Type == key || + event.Type == C.Rune && int(event.Char) == key-C.AltZ || + event.Type == C.Mouse && key == C.DoubleClick && event.MouseEvent.Double } func quoteEntry(entry string) string { From 89687105f426968bc72e3e5d195ce5ebbe172082 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 14 Apr 2016 14:51:58 +0900 Subject: [PATCH 131/658] [install] Ask before updating shell configuration files --- install | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/install b/install index 0ae39a9..ae9425a 100755 --- a/install +++ b/install @@ -7,7 +7,7 @@ set -u auto_completion= key_bindings= -update_config=1 +update_config=2 binary_arch= help() { @@ -335,8 +335,8 @@ fi append_line() { set -e - local skip line file pat lno - skip="$1" + local update line file pat lno + update="$1" line="$2" file="$3" pat="${4:-}" @@ -352,7 +352,7 @@ append_line() { if [ -n "$lno" ]; then echo " - Already exists: line #$lno" else - if [ $skip -eq 1 ]; then + if [ $update -eq 1 ]; then echo >> "$file" echo "$line" >> "$file" echo " + Added" @@ -364,6 +364,11 @@ append_line() { set +e } +if [ $update_config -eq 2 ]; then + echo + ask "Do you want to update your shell configuration files?" + update_config=$? +fi echo for shell in $shells; do [ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc @@ -375,15 +380,14 @@ if [ $key_bindings -eq 1 ] && [ $has_fish -eq 1 ]; then append_line $update_config "fzf_key_bindings" "$bind_file" fi -cat << EOF -Finished. Restart your shell or reload config file. - source ~/.bashrc # bash -EOF -[ $has_zsh -eq 1 ] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh" -[ $has_fish -eq 1 ] && echo " fzf_key_bindings # fish"; cat << EOF - -Use uninstall script to remove fzf. - -For more information, see: https://github.com/junegunn/fzf -EOF +if [ $update_config -eq 1 ]; then + echo 'Finished. Restart your shell or reload config file.' + echo ' source ~/.bashrc # bash' + [ $has_zsh -eq 1 ] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh" + [ $has_fish -eq 1 ] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish' + echo + echo 'Use uninstall script to remove fzf.' + echo +fi +echo 'For more information, see: https://github.com/junegunn/fzf' From 0c8de1ca4468842429e7cb14ecb79391d896e0e5 Mon Sep 17 00:00:00 2001 From: William Chargin Date: Tue, 12 Apr 2016 17:20:30 -0400 Subject: [PATCH 132/658] Fix Bash+vimode pre-launch delay Summary: Fix adapted from [@adamheins: fzf, vi-mode, and fixing delays][1]. [1]: https://adamheins.com/blog/fzf-vi-mode-and-fixing-delays The basic problem is that fzf presses to enter vi-movement-mode (as opposed to insert mode) and then presses a bunch of keys to set up the buffer. But the keypress is also the prefix for a bunch of other commands, so Bash will dutifully wait an excruciating half-second before actually executing this command. Instead, we bind , which is unused by default and seems reasonably unlikely to be custom-bound, to be another way to enter vi-movement-mode; this binding is unambiguous, so fzf can use it without delay. This change was made by just `:s/\\e/\\C-x\\C-a/gc` in the relevant section, after adding the actual binding and comment at the top. --- shell/key-bindings.bash | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 042e005..fca570b 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -85,6 +85,15 @@ if [ -z "$(set -o | \grep '^vi.*on')" ]; then # ALT-C - cd into the selected directory bind '"\ec": " \C-e\C-u$(__fzf_cd__)\e\C-e\er\C-m"' else + # We'd usually use "\e" to enter vi-movement-mode so we can do our magic, + # but this incurs a very noticeable delay of a half second or so, + # because many other commands start with "\e". + # Instead, we bind an unused key, "\C-x\C-a", + # to also enter vi-movement-mode, + # and then use that thereafter. + # (We imagine that "\C-x\C-a" is relatively unlikely to be in use.) + bind '"\C-x\C-a": vi-movement-mode' + bind '"\C-x\C-e": shell-expand-line' bind '"\C-x\C-r": redraw-current-line' bind '"\C-x^": history-expand-line' @@ -94,19 +103,19 @@ else if [ $__use_tmux_auto -eq 1 ]; then bind -x '"\C-t": "__fzf_select_tmux_auto__"' elif [ $__use_tmux -eq 1 ]; then - bind '"\C-t": "\e$a \eddi$(__fzf_select_tmux__)\C-x\C-e\e0P$xa"' + bind '"\C-t": "\C-x\C-a$a \C-x\C-addi$(__fzf_select_tmux__)\C-x\C-e\C-x\C-a0P$xa"' else - bind '"\C-t": "\e$a \eddi$(__fzf_select__)\C-x\C-e\e0Px$a \C-x\C-r\exa "' + bind '"\C-t": "\C-x\C-a$a \C-x\C-addi$(__fzf_select__)\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "' fi bind -m vi-command '"\C-t": "i\C-t"' # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": "\eddi$(__fzf_history__)\C-x\C-e\C-x^\e$a\C-x\C-r"' + bind '"\C-r": "\C-x\C-addi$(__fzf_history__)\C-x\C-e\C-x^\C-x\C-a$a\C-x\C-r"' bind -m vi-command '"\C-r": "i\C-r"' # ALT-C - cd into the selected directory - bind '"\ec": "\eddi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"' - bind -m vi-command '"\ec": "i\ec"' + bind '"\ec": "\C-x\C-addi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"' + bind -m vi-command '"\C-ac": "i\C-ac"' fi unset -v __use_tmux __use_tmux_auto From 12230f8043b24f17ed32b87fa3ec1b714cba98ec Mon Sep 17 00:00:00 2001 From: William Chargin Date: Wed, 13 Apr 2016 18:14:41 -0400 Subject: [PATCH 133/658] Fix bash-vimode normal-mode cd completion --- shell/key-bindings.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index fca570b..47adf30 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -115,7 +115,7 @@ else # ALT-C - cd into the selected directory bind '"\ec": "\C-x\C-addi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"' - bind -m vi-command '"\C-ac": "i\C-ac"' + bind -m vi-command '"\ec": "ddi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"' fi unset -v __use_tmux __use_tmux_auto From 5f63a7b587b4ce221d1e90a559051c51fad2ff78 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 15 Apr 2016 12:57:38 +0900 Subject: [PATCH 134/658] Fix flaky test case --- test/test_go.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index 7c562a8..9d04eda 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -414,11 +414,12 @@ class TestGoFZF < TestBase def test_expect test = lambda do |key, feed, expected = key| - tmux.send_keys "seq 1 100 | #{fzf :expect, key}", :Enter + tmux.send_keys "seq 1 100 | #{fzf :expect, key}; sync", :Enter tmux.until { |lines| lines[-2].include? '100/100' } tmux.send_keys '55' tmux.until { |lines| lines[-2].include? '1/100' } tmux.send_keys *feed + tmux.prepare assert_equal [expected, '55'], readonce.split($/) end test.call 'ctrl-t', 'C-T' From 2f6d23b91e845f53e746e7cf74477a735ec88a85 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 16 Apr 2016 14:02:43 +0900 Subject: [PATCH 135/658] Enhanced ranking algorithm Based on the patch by Matt Westcott (@mjwestcott). But with a more conservative approach: - Does not use linearly increasing penalties; It is agreed upon that we should prefer matching characters at the beginnings of the words, but it's not always clear that the relevance is inversely proportional to the distance from the beginning. - The approach here is more conservative in that the bonus is never large enough to override the matchlen, so it can be thought of as the first implicit tiebreak criterion. - One may argue the change breaks the contract of --tiebreak, but the judgement depends on the definition of "tie". --- src/algo/algo.go | 136 ++++++++++++++++++++++++++++++++++-------- src/algo/algo_test.go | 73 ++++++++++++++--------- src/item.go | 11 ++-- src/pattern.go | 52 +++++++++------- src/pattern_test.go | 14 ++--- test/test_go.rb | 22 +++---- 6 files changed, 214 insertions(+), 94 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index ac7bd8b..8656c68 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -22,10 +22,30 @@ func runeAt(runes []rune, index int, max int, forward bool) rune { return runes[max-index-1] } +// Result conatins the results of running a match function. +type Result struct { + Start int32 + End int32 + + // Items are basically sorted by the lengths of matched substrings. + // But we slightly adjust the score with bonus for better results. + Bonus int32 +} + +type charClass int + +const ( + charNonWord charClass = iota + charLower + charUpper + charLetter + charNumber +) + // FuzzyMatch performs fuzzy-match -func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { +func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { if len(pattern) == 0 { - return 0, 0 + return Result{0, 0, 0} } // 0. (FIXME) How to find the shortest match? @@ -90,12 +110,76 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) } } } - if forward { - return sidx, eidx + + // Calculate the bonus. This can't be done at the same time as the + // pattern scan above because 'forward' may be false. + if !forward { + sidx, eidx = lenRunes-eidx, lenRunes-sidx } - return lenRunes - eidx, lenRunes - sidx + + var bonus int32 + pidx := 0 + consecutive := false + prevClass := charNonWord + for index := 0; index < eidx; index++ { + char := runes[index] + var class charClass + if unicode.IsLower(char) { + class = charLower + } else if unicode.IsUpper(char) { + class = charUpper + } else if unicode.IsLetter(char) { + class = charLetter + } else if unicode.IsNumber(char) { + class = charNumber + } else { + class = charNonWord + } + + var point int32 + if prevClass == charNonWord && class != charNonWord { + // Word boundary + point = 2 + } else if prevClass == charLower && class == charUpper || + prevClass != charNumber && class == charNumber { + // camelCase letter123 + point = 1 + } + prevClass = class + + if index >= sidx { + if !caseSensitive { + if char >= 'A' && char <= 'Z' { + char += 32 + } else if char > unicode.MaxASCII { + char = unicode.To(unicode.LowerCase, char) + } + } + pchar := pattern[pidx] + if pchar == char { + // Boost bonus for the first character in the pattern + if pidx == 0 { + point *= 2 + } + // Bonus to consecutive matching chars + if consecutive { + point++ + } + bonus += point + + if pidx++; pidx == lenPattern { + break + } + consecutive = true + } else { + consecutive = false + } + } + } + + return Result{int32(sidx), int32(eidx), bonus} } - return -1, -1 + return Result{-1, -1, 0} } // ExactMatchNaive is a basic string searching algorithm that handles case @@ -105,16 +189,17 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) // // We might try to implement better algorithms in the future: // http://en.wikipedia.org/wiki/String_searching_algorithm -func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { +func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { + // Note: ExactMatchNaive always return a zero bonus. if len(pattern) == 0 { - return 0, 0 + return Result{0, 0, 0} } lenRunes := len(runes) lenPattern := len(pattern) if lenRunes < lenPattern { - return -1, -1 + return Result{-1, -1, 0} } pidx := 0 @@ -132,22 +217,23 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r pidx++ if pidx == lenPattern { if forward { - return index - lenPattern + 1, index + 1 + return Result{int32(index - lenPattern + 1), int32(index + 1), 0} } - return lenRunes - (index + 1), lenRunes - (index - lenPattern + 1) + return Result{int32(lenRunes - (index + 1)), int32(lenRunes - (index - lenPattern + 1)), 0} } } else { index -= pidx pidx = 0 } } - return -1, -1 + return Result{-1, -1, 0} } // PrefixMatch performs prefix-match -func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { +func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { + // Note: PrefixMatch always return a zero bonus. if len(runes) < len(pattern) { - return -1, -1 + return Result{-1, -1, 0} } for index, r := range pattern { @@ -156,19 +242,20 @@ func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) char = unicode.ToLower(char) } if char != r { - return -1, -1 + return Result{-1, -1, 0} } } - return 0, len(pattern) + return Result{0, int32(len(pattern)), 0} } // SuffixMatch performs suffix-match -func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) (int, int) { +func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) Result { + // Note: SuffixMatch always return a zero bonus. runes := util.TrimRight(input) trimmedLen := len(runes) diff := trimmedLen - len(pattern) if diff < 0 { - return -1, -1 + return Result{-1, -1, 0} } for index, r := range pattern { @@ -177,23 +264,24 @@ func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) char = unicode.ToLower(char) } if char != r { - return -1, -1 + return Result{-1, -1, 0} } } - return trimmedLen - len(pattern), trimmedLen + return Result{int32(trimmedLen - len(pattern)), int32(trimmedLen), 0} } // EqualMatch performs equal-match -func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) { +func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { + // Note: EqualMatch always return a zero bonus. if len(runes) != len(pattern) { - return -1, -1 + return Result{-1, -1, 0} } runesStr := string(runes) if !caseSensitive { runesStr = strings.ToLower(runesStr) } if runesStr == string(pattern) { - return 0, len(pattern) + return Result{0, int32(len(pattern)), 0} } - return -1, -1 + return Result{-1, -1, 0} } diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index 06ec5ea..a124d05 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -5,65 +5,84 @@ import ( "testing" ) -func assertMatch(t *testing.T, fun func(bool, bool, []rune, []rune) (int, int), caseSensitive bool, forward bool, input string, pattern string, sidx int, eidx int) { +func assertMatch(t *testing.T, fun func(bool, bool, []rune, []rune) Result, caseSensitive, forward bool, input, pattern string, sidx int32, eidx int32, bonus int32) { if !caseSensitive { pattern = strings.ToLower(pattern) } - s, e := fun(caseSensitive, forward, []rune(input), []rune(pattern)) - if s != sidx { - t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", s, sidx, input, pattern) + res := fun(caseSensitive, forward, []rune(input), []rune(pattern)) + if res.Start != sidx { + t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", res.Start, sidx, input, pattern) } - if e != eidx { - t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", e, eidx, input, pattern) + if res.End != eidx { + t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", res.End, eidx, input, pattern) + } + if res.Bonus != bonus { + t.Errorf("Invalid bonus: %d (expected: %d, %s / %s)", res.Bonus, bonus, input, pattern) } } func TestFuzzyMatch(t *testing.T) { - assertMatch(t, FuzzyMatch, false, true, "fooBarbaz", "oBZ", 2, 9) - assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBZ", -1, -1) - assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBz", 2, 9) - assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "fooBarbazz", -1, -1) + assertMatch(t, FuzzyMatch, false, true, "fooBarbaz", "oBZ", 2, 9, 2) + assertMatch(t, FuzzyMatch, false, true, "foo bar baz", "fbb", 0, 9, 8) + assertMatch(t, FuzzyMatch, false, true, "/AutomatorDocument.icns", "rdoc", 9, 13, 4) + assertMatch(t, FuzzyMatch, false, true, "/man1/zshcompctl.1", "zshc", 6, 10, 7) + assertMatch(t, FuzzyMatch, false, true, "/.oh-my-zsh/cache", "zshc", 8, 13, 8) + assertMatch(t, FuzzyMatch, false, true, "ab0123 456", "12356", 3, 10, 3) + assertMatch(t, FuzzyMatch, false, true, "abc123 456", "12356", 3, 10, 5) + + assertMatch(t, FuzzyMatch, false, true, "foo/bar/baz", "fbb", 0, 9, 8) + assertMatch(t, FuzzyMatch, false, true, "fooBarBaz", "fbb", 0, 7, 6) + assertMatch(t, FuzzyMatch, false, true, "foo barbaz", "fbb", 0, 8, 6) + assertMatch(t, FuzzyMatch, false, true, "fooBar Baz", "foob", 0, 4, 8) + assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBZ", -1, -1, 0) + assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBz", 2, 9, 2) + assertMatch(t, FuzzyMatch, true, true, "Foo Bar Baz", "fbb", -1, -1, 0) + assertMatch(t, FuzzyMatch, true, true, "Foo/Bar/Baz", "FBB", 0, 9, 8) + assertMatch(t, FuzzyMatch, true, true, "FooBarBaz", "FBB", 0, 7, 6) + assertMatch(t, FuzzyMatch, true, true, "foo BarBaz", "fBB", 0, 8, 7) + assertMatch(t, FuzzyMatch, true, true, "FooBar Baz", "FooB", 0, 4, 8) + assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "fooBarbazz", -1, -1, 0) } func TestFuzzyMatchBackward(t *testing.T) { - assertMatch(t, FuzzyMatch, false, true, "foobar fb", "fb", 0, 4) - assertMatch(t, FuzzyMatch, false, false, "foobar fb", "fb", 7, 9) + assertMatch(t, FuzzyMatch, false, true, "foobar fb", "fb", 0, 4, 4) + assertMatch(t, FuzzyMatch, false, false, "foobar fb", "fb", 7, 9, 5) } func TestExactMatchNaive(t *testing.T) { for _, dir := range []bool{true, false} { - assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5) - assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1) - assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1) + assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5, 0) + assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1, 0) + assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1, 0) } } func TestExactMatchNaiveBackward(t *testing.T) { - assertMatch(t, ExactMatchNaive, false, true, "foobar foob", "oo", 1, 3) - assertMatch(t, ExactMatchNaive, false, false, "foobar foob", "oo", 8, 10) + assertMatch(t, ExactMatchNaive, false, true, "foobar foob", "oo", 1, 3, 0) + assertMatch(t, ExactMatchNaive, false, false, "foobar foob", "oo", 8, 10, 0) } func TestPrefixMatch(t *testing.T) { for _, dir := range []bool{true, false} { - assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3) - assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1) - assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "baz", -1, -1) + assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, 0) + assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0) + assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "baz", -1, -1, 0) } } func TestSuffixMatch(t *testing.T) { for _, dir := range []bool{true, false} { - assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1) - assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9) - assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1) + assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1, 0) + assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9, 0) + assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1, 0) } } func TestEmptyPattern(t *testing.T) { for _, dir := range []bool{true, false} { - assertMatch(t, FuzzyMatch, true, dir, "foobar", "", 0, 0) - assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0) - assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0) - assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6) + assertMatch(t, FuzzyMatch, true, dir, "foobar", "", 0, 0, 0) + assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0, 0) + assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0, 0) + assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6, 0) } } diff --git a/src/item.go b/src/item.go index 43ce1a2..66b3e4d 100644 --- a/src/item.go +++ b/src/item.go @@ -23,6 +23,7 @@ type Item struct { offsets []Offset colors []ansiOffset rank [5]int32 + bonus int32 } // Sort criteria to use. Never changes once fzf is started. @@ -73,15 +74,17 @@ func (item *Item) Rank(cache bool) [5]int32 { matchlen += end - begin } } - if matchlen == 0 { - matchlen = math.MaxInt32 - } rank := buildEmptyRank(item.Index()) for idx, criterion := range sortCriteria { var val int32 switch criterion { case byMatchLen: - val = int32(matchlen) + if matchlen == 0 { + val = math.MaxInt32 + } else { + // It is extremely unlikely that bonus exceeds 128 + val = 128*int32(matchlen) - item.bonus + } case byLength: // It is guaranteed that .transformed in not null in normal execution if item.transformed != nil { diff --git a/src/pattern.go b/src/pattern.go index af73b67..fda5cc9 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -49,7 +49,7 @@ type Pattern struct { cacheable bool delimiter Delimiter nth []Range - procFun map[termType]func(bool, bool, []rune, []rune) (int, int) + procFun map[termType]func(bool, bool, []rune, []rune) algo.Result } var ( @@ -125,7 +125,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool, cacheable: cacheable, nth: nth, delimiter: delimiter, - procFun: make(map[termType]func(bool, bool, []rune, []rune) (int, int))} + procFun: make(map[termType]func(bool, bool, []rune, []rune) algo.Result)} ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termEqual] = algo.EqualMatch @@ -275,15 +275,16 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item { matches := []*Item{} if !p.extended { for _, item := range *chunk { - if sidx, eidx, tlen := p.basicMatch(item); sidx >= 0 { + offset, bonus := p.basicMatch(item) + if sidx := offset[0]; sidx >= 0 { matches = append(matches, - dupItem(item, []Offset{Offset{int32(sidx), int32(eidx), int32(tlen)}})) + dupItem(item, []Offset{offset}, bonus)) } } } else { for _, item := range *chunk { - if offsets := p.extendedMatch(item); len(offsets) == len(p.termSets) { - matches = append(matches, dupItem(item, offsets)) + if offsets, bonus := p.extendedMatch(item); len(offsets) == len(p.termSets) { + matches = append(matches, dupItem(item, offsets, bonus)) } } } @@ -293,25 +294,27 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item { // MatchItem returns true if the Item is a match func (p *Pattern) MatchItem(item *Item) bool { if !p.extended { - sidx, _, _ := p.basicMatch(item) + offset, _ := p.basicMatch(item) + sidx := offset[0] return sidx >= 0 } - offsets := p.extendedMatch(item) + offsets, _ := p.extendedMatch(item) return len(offsets) == len(p.termSets) } -func dupItem(item *Item, offsets []Offset) *Item { +func dupItem(item *Item, offsets []Offset, bonus int32) *Item { sort.Sort(ByOrder(offsets)) return &Item{ text: item.text, origText: item.origText, transformed: item.transformed, offsets: offsets, + bonus: bonus, colors: item.colors, rank: buildEmptyRank(item.Index())} } -func (p *Pattern) basicMatch(item *Item) (int, int, int) { +func (p *Pattern) basicMatch(item *Item) (Offset, int32) { input := p.prepareInput(item) if p.fuzzy { return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) @@ -319,29 +322,33 @@ func (p *Pattern) basicMatch(item *Item) (int, int, int) { return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text) } -func (p *Pattern) extendedMatch(item *Item) []Offset { +func (p *Pattern) extendedMatch(item *Item) ([]Offset, int32) { input := p.prepareInput(item) offsets := []Offset{} + var totalBonus int32 for _, termSet := range p.termSets { var offset *Offset + var bonus int32 for _, term := range termSet { pfun := p.procFun[term.typ] - if sidx, eidx, tlen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text); sidx >= 0 { + off, pen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text) + if sidx := off[0]; sidx >= 0 { if term.inv { continue } - offset = &Offset{int32(sidx), int32(eidx), int32(tlen)} + offset, bonus = &off, pen break } else if term.inv { - offset = &Offset{0, 0, 0} + offset, bonus = &Offset{0, 0, 0}, 0 continue } } if offset != nil { offsets = append(offsets, *offset) + totalBonus += bonus } } - return offsets + return offsets, totalBonus } func (p *Pattern) prepareInput(item *Item) []Token { @@ -360,13 +367,16 @@ func (p *Pattern) prepareInput(item *Item) []Token { return ret } -func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) (int, int), - tokens []Token, caseSensitive bool, forward bool, pattern []rune) (int, int, int) { +func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) algo.Result, + tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int32) { for _, part := range tokens { - prefixLength := part.prefixLength - if sidx, eidx := pfun(caseSensitive, forward, part.text, pattern); sidx >= 0 { - return sidx + prefixLength, eidx + prefixLength, part.trimLength + prefixLength := int32(part.prefixLength) + if res := pfun(caseSensitive, forward, part.text, pattern); res.Start >= 0 { + var sidx int32 = res.Start + prefixLength + var eidx int32 = res.End + prefixLength + return Offset{sidx, eidx, int32(part.trimLength)}, res.Bonus } } - return -1, -1, -1 // math.MaxUint16 + // TODO: math.MaxUint16 + return Offset{-1, -1, -1}, 0.0 } diff --git a/src/pattern_test.go b/src/pattern_test.go index 6bf571c..2f27fda 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -70,10 +70,10 @@ func TestExact(t *testing.T) { clearPatternCache() pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("'abc")) - sidx, eidx := algo.ExactMatchNaive( + res := algo.ExactMatchNaive( pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.termSets[0][0].text) - if sidx != 7 || eidx != 10 { - t.Errorf("%s / %d / %d", pattern.termSets, sidx, eidx) + if res.Start != 7 || res.End != 10 { + t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) } } @@ -82,11 +82,11 @@ func TestEqual(t *testing.T) { clearPatternCache() pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$")) - match := func(str string, sidxExpected int, eidxExpected int) { - sidx, eidx := algo.EqualMatch( + match := func(str string, sidxExpected int32, eidxExpected int32) { + res := algo.EqualMatch( pattern.caseSensitive, pattern.forward, []rune(str), pattern.termSets[0][0].text) - if sidx != sidxExpected || eidx != eidxExpected { - t.Errorf("%s / %d / %d", pattern.termSets, sidx, eidx) + if res.Start != sidxExpected || res.End != eidxExpected { + t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) } } match("ABC", -1, -1) diff --git a/test/test_go.rb b/test/test_go.rb index 9d04eda..2c3ba2e 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -207,13 +207,13 @@ class TestGoFZF < TestBase tmux.send_keys '99', 'C-a', '1', 'C-f', '3', 'C-b', 'C-h', 'C-u', 'C-e', 'C-y', 'C-k', 'Tab', 'BTab' tmux.until { |lines| lines[-2] == ' 856/100000' } lines = tmux.capture - assert_equal '> 1391', lines[-4] + assert_equal '> 3910', lines[-4] assert_equal ' 391', lines[-3] assert_equal ' 856/100000', lines[-2] assert_equal '> 391', lines[-1] tmux.send_keys :Enter - assert_equal '1391', readonce.chomp + assert_equal '3910', readonce.chomp end def test_fzf_default_command @@ -357,7 +357,7 @@ class TestGoFZF < TestBase 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', '50', '51'], readonce.split($/) end end @@ -378,7 +378,7 @@ class TestGoFZF < TestBase tmux.send_keys :Enter tmux.until { |lines| lines[-1] == '>' } tmux.send_keys 'C-K', :Enter - assert_equal ['1919'], readonce.split($/) + assert_equal ['9090'], readonce.split($/) end def test_tac @@ -711,10 +711,10 @@ class TestGoFZF < TestBase # len(1 ~ 2) output = [ - "apple ui bottle 2", "app ic bottle 4", - "apple juice bottle 1", "app ice bottle 3", + "apple ui bottle 2", + "apple juice bottle 1", ] assert_equal output, `#{FZF} -fai -n1..2 < #{tempname}`.split($/) @@ -729,9 +729,9 @@ class TestGoFZF < TestBase # len(2) output = [ - "apple ui bottle 2", "app ic bottle 4", "app ice bottle 3", + "apple ui bottle 2", "apple juice bottle 1", ] assert_equal output, `#{FZF} -fi -n2 < #{tempname}`.split($/) @@ -943,12 +943,12 @@ class TestGoFZF < TestBase lines[-2].include?('/90') && lines[-3] == ' 1' && lines[-4] == ' 2' && - lines[-13] == '> 15' + lines[-13] == '> 50' end tmux.send_keys :Down end tmux.send_keys :Enter - assert_equal '15', readonce.chomp + assert_equal '50', readonce.chomp end def test_header_lines_reverse @@ -958,12 +958,12 @@ class TestGoFZF < TestBase lines[1].include?('/90') && lines[2] == ' 1' && lines[3] == ' 2' && - lines[12] == '> 15' + lines[12] == '> 50' end tmux.send_keys :Up end tmux.send_keys :Enter - assert_equal '15', readonce.chomp + assert_equal '50', readonce.chomp end def test_header_lines_overflow From 879ead210fbd5e0e05fe342f82197d2fba05c679 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 16 Apr 2016 14:37:16 +0900 Subject: [PATCH 136/658] 0.11.2 --- CHANGELOG.md | 6 ++++++ install | 4 ++-- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 692d836..9483f33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +0.12.0 +------ + +- Enhanced ranking algorithm +- Minor bug fixes + 0.11.4 ------ diff --git a/install b/install index ae9425a..df69f48 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.11.4 pre=1 || - version=0.11.4 pre=0 +[[ "$@" =~ --pre ]] && version=0.12.0 pre=1 || + version=0.12.0 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 1a99a54..649eef9 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Mar 2016" "fzf 0.11.4" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Apr 2016" "fzf 0.12.0" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 0832420..bb2af44 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.11.4" + version = "0.12.0" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From b28c14b93a94269f05f904c17547a2e40eebccea Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 16 Apr 2016 14:45:16 +0900 Subject: [PATCH 137/658] 0.12.0 From e867355b2a578438d08b74a1708d6ad582316a48 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 21 Apr 2016 00:33:30 +0900 Subject: [PATCH 138/658] [neovim] Restore winfixwidth and winfixheight Fix https://github.com/junegunn/fzf.vim/issues/128 --- plugin/fzf.vim | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 5b0c8d8..a01e582 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -307,7 +307,7 @@ function! s:split(dict) endif execute cmd sz.'new' execute resz sz - return + return {} endif endfor if s:present(a:dict, 'window') @@ -315,20 +315,25 @@ function! s:split(dict) else execute (tabpagenr()-1).'tabnew' endif + return { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh } finally setlocal winfixwidth winfixheight buftype=nofile bufhidden=wipe nobuflisted endtry endfunction function! s:execute_term(dict, command, temps) abort - call s:split(a:dict) + let winopts = s:split(a:dict) - let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF' } + let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF', 'winopts': winopts } let s:command = a:command function! fzf.on_exit(id, code) let pos = s:getpos() let inplace = pos == s:ppos " {'window': 'enew'} - if !inplace + if inplace + for [opt, val] in items(self.winopts) + execute 'let' opt '=' val + endfor + else if bufnr('') == self.buf " We use close instead of bd! since Vim does not close the split when " there's no other listed buffer (nvim +'set nobuflisted') From e2401350a367fbfcd52852ec3c76109fa831016a Mon Sep 17 00:00:00 2001 From: Gene Pavlovsky Date: Sat, 23 Apr 2016 03:12:15 +0300 Subject: [PATCH 139/658] Update completion.bash Fixes #548. Avoid using a subshell in _fzf_defc(). --- shell/completion.bash | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 9005af7..66fac61 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -272,14 +272,15 @@ if type _completion_loader > /dev/null 2>&1; then fi _fzf_defc() { - local cmd func opts orig_var orig + local cmd func opts orig_var orig def cmd="$1" func="$2" opts="$3" orig_var="_fzf_orig_completion_$cmd" orig="${!orig_var}" if [ -n "$orig" ]; then - eval "$(printf "$orig" "$func")" + printf -v def "$orig" "$func" + eval "$def" else complete -F "$func" $opts "$cmd" fi From 63dbf4854632b52d21757f9a7675726bdd840c16 Mon Sep 17 00:00:00 2001 From: Gene Pavlovsky Date: Sat, 23 Apr 2016 03:44:41 +0300 Subject: [PATCH 140/658] Update key-bindings.bash Faster startup. Use internal bash globbing instead of external grep binary (adapter from Gentoo's `/etc/bash/bashrc` TERM checking). Insignificant on Linux, but on Cygwin this cuts startup time by 40 ms on my Core i7 laptop. --- shell/key-bindings.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 47adf30..8ecd614 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -65,7 +65,7 @@ if [ -n "$TMUX_PANE" ]; then [ $BASH_VERSINFO -gt 3 ] && __use_tmux_auto=1 fi -if [ -z "$(set -o | \grep '^vi.*on')" ]; then +if [[ $'\n'$(set -o) != *$'\n'vi*on* ]]; then # Required to refresh the prompt after fzf bind '"\er": redraw-current-line' bind '"\e^": history-expand-line' From 654a7df9b080533977d34ad423e201f063843d5c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 23 Apr 2016 17:53:54 +0900 Subject: [PATCH 141/658] [neovim] Set bufhidden and nobuflisted after opening terminal --- plugin/fzf.vim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index a01e582..43d5059 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -317,7 +317,7 @@ function! s:split(dict) endif return { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh } finally - setlocal winfixwidth winfixheight buftype=nofile bufhidden=wipe nobuflisted + setlocal winfixwidth winfixheight endtry endfunction @@ -368,7 +368,7 @@ function! s:execute_term(dict, command, temps) abort call s:pushd(a:dict) call termopen(a:command, fzf) call s:popd(a:dict, []) - setlocal nospell + setlocal nospell bufhidden=wipe nobuflisted setf fzf startinsert return [] From 4bde8de63f187e487ff5bc40c1d7803ca882ff9c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 23 Apr 2016 19:48:06 +0900 Subject: [PATCH 142/658] Apply new ranking algorithm to exact match as well --- src/algo/algo.go | 150 +++++++++++++++++++++++------------------- src/algo/algo_test.go | 19 ++++-- 2 files changed, 95 insertions(+), 74 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 8656c68..d8e2fec 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -42,6 +42,70 @@ const ( charNumber ) +func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, eidx int) int32 { + var bonus int32 + pidx := 0 + lenPattern := len(pattern) + consecutive := false + prevClass := charNonWord + for index := 0; index < eidx; index++ { + char := runes[index] + var class charClass + if unicode.IsLower(char) { + class = charLower + } else if unicode.IsUpper(char) { + class = charUpper + } else if unicode.IsLetter(char) { + class = charLetter + } else if unicode.IsNumber(char) { + class = charNumber + } else { + class = charNonWord + } + + var point int32 + if prevClass == charNonWord && class != charNonWord { + // Word boundary + point = 2 + } else if prevClass == charLower && class == charUpper || + prevClass != charNumber && class == charNumber { + // camelCase letter123 + point = 1 + } + prevClass = class + + if index >= sidx { + if !caseSensitive { + if char >= 'A' && char <= 'Z' { + char += 32 + } else if char > unicode.MaxASCII { + char = unicode.To(unicode.LowerCase, char) + } + } + pchar := pattern[pidx] + if pchar == char { + // Boost bonus for the first character in the pattern + if pidx == 0 { + point *= 2 + } + // Bonus to consecutive matching chars + if consecutive { + point++ + } + bonus += point + + if pidx++; pidx == lenPattern { + break + } + consecutive = true + } else { + consecutive = false + } + } + } + return bonus +} + // FuzzyMatch performs fuzzy-match func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { if len(pattern) == 0 { @@ -117,67 +181,8 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) sidx, eidx = lenRunes-eidx, lenRunes-sidx } - var bonus int32 - pidx := 0 - consecutive := false - prevClass := charNonWord - for index := 0; index < eidx; index++ { - char := runes[index] - var class charClass - if unicode.IsLower(char) { - class = charLower - } else if unicode.IsUpper(char) { - class = charUpper - } else if unicode.IsLetter(char) { - class = charLetter - } else if unicode.IsNumber(char) { - class = charNumber - } else { - class = charNonWord - } - - var point int32 - if prevClass == charNonWord && class != charNonWord { - // Word boundary - point = 2 - } else if prevClass == charLower && class == charUpper || - prevClass != charNumber && class == charNumber { - // camelCase letter123 - point = 1 - } - prevClass = class - - if index >= sidx { - if !caseSensitive { - if char >= 'A' && char <= 'Z' { - char += 32 - } else if char > unicode.MaxASCII { - char = unicode.To(unicode.LowerCase, char) - } - } - pchar := pattern[pidx] - if pchar == char { - // Boost bonus for the first character in the pattern - if pidx == 0 { - point *= 2 - } - // Bonus to consecutive matching chars - if consecutive { - point++ - } - bonus += point - - if pidx++; pidx == lenPattern { - break - } - consecutive = true - } else { - consecutive = false - } - } - } - - return Result{int32(sidx), int32(eidx), bonus} + return Result{int32(sidx), int32(eidx), + evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} } return Result{-1, -1, 0} } @@ -190,7 +195,6 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) // We might try to implement better algorithms in the future: // http://en.wikipedia.org/wiki/String_searching_algorithm func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { - // Note: ExactMatchNaive always return a zero bonus. if len(pattern) == 0 { return Result{0, 0, 0} } @@ -216,10 +220,16 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r if pchar == char { pidx++ if pidx == lenPattern { + var sidx, eidx int if forward { - return Result{int32(index - lenPattern + 1), int32(index + 1), 0} + sidx = index - lenPattern + 1 + eidx = index + 1 + } else { + sidx = lenRunes - (index + 1) + eidx = lenRunes - (index - lenPattern + 1) } - return Result{int32(lenRunes - (index + 1)), int32(lenRunes - (index - lenPattern + 1)), 0} + return Result{int32(sidx), int32(eidx), + evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} } } else { index -= pidx @@ -231,7 +241,6 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r // PrefixMatch performs prefix-match func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { - // Note: PrefixMatch always return a zero bonus. if len(runes) < len(pattern) { return Result{-1, -1, 0} } @@ -245,12 +254,13 @@ func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) return Result{-1, -1, 0} } } - return Result{0, int32(len(pattern)), 0} + lenPattern := len(pattern) + return Result{0, int32(lenPattern), + evaluateBonus(caseSensitive, runes, pattern, 0, lenPattern)} } // SuffixMatch performs suffix-match func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) Result { - // Note: SuffixMatch always return a zero bonus. runes := util.TrimRight(input) trimmedLen := len(runes) diff := trimmedLen - len(pattern) @@ -267,7 +277,11 @@ func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) return Result{-1, -1, 0} } } - return Result{int32(trimmedLen - len(pattern)), int32(trimmedLen), 0} + lenPattern := len(pattern) + sidx := trimmedLen - lenPattern + eidx := trimmedLen + return Result{int32(sidx), int32(eidx), + evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} } // EqualMatch performs equal-match diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index a124d05..3c95458 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -51,29 +51,36 @@ func TestFuzzyMatchBackward(t *testing.T) { func TestExactMatchNaive(t *testing.T) { for _, dir := range []bool{true, false} { - assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5, 0) + assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5, 3) assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1, 0) assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1, 0) + + assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13, 4) + assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10, 7) + assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13, 10) } } func TestExactMatchNaiveBackward(t *testing.T) { - assertMatch(t, ExactMatchNaive, false, true, "foobar foob", "oo", 1, 3, 0) - assertMatch(t, ExactMatchNaive, false, false, "foobar foob", "oo", 8, 10, 0) + assertMatch(t, ExactMatchNaive, false, true, "foobar foob", "oo", 1, 3, 1) + assertMatch(t, ExactMatchNaive, false, false, "foobar foob", "oo", 8, 10, 1) } func TestPrefixMatch(t *testing.T) { for _, dir := range []bool{true, false} { - assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, 0) assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0) - assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "baz", -1, -1, 0) + assertMatch(t, PrefixMatch, false, dir, "fooBarBaz", "baz", -1, -1, 0) + assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, 6) + assertMatch(t, PrefixMatch, false, dir, "foOBarBaZ", "foo", 0, 3, 7) + assertMatch(t, PrefixMatch, false, dir, "f-oBarbaz", "f-o", 0, 3, 8) } } func TestSuffixMatch(t *testing.T) { for _, dir := range []bool{true, false} { assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1, 0) - assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9, 0) + assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9, 2) + assertMatch(t, SuffixMatch, false, dir, "fooBarBaZ", "baz", 6, 9, 5) assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1, 0) } } From 85ef3263fcfe578aecdac2aecd6c949fed921a7f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 24 Apr 2016 03:30:51 +0900 Subject: [PATCH 143/658] Fix incorrect cache reference in --exact mode (#547) When we prepend a single quote to our query in --exact mode, we are not supposed to limit the scope of the new search to the previous exact-match result. --- src/pattern.go | 2 +- test/test_go.rb | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pattern.go b/src/pattern.go index fda5cc9..42a341b 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -227,7 +227,7 @@ func (p *Pattern) CacheKey() string { } cacheableTerms := []string{} for _, termSet := range p.termSets { - if len(termSet) == 1 && !termSet[0].inv { + if len(termSet) == 1 && !termSet[0].inv && (p.fuzzy || termSet[0].typ == termExact) { cacheableTerms = append(cacheableTerms, string(termSet[0].origText)) } } diff --git a/test/test_go.rb b/test/test_go.rb index 2c3ba2e..2c1324b 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1157,6 +1157,16 @@ class TestGoFZF < TestBase end end + def test_partial_caching + tmux.send_keys 'seq 1000 | fzf -e', :Enter + tmux.until { |lines| lines[-2] == ' 1000/1000' } + tmux.send_keys 11 + tmux.until { |lines| lines[-2] == ' 19/1000' } + tmux.send_keys 'C-a', "'" + tmux.until { |lines| lines[-2] == ' 28/1000' } + tmux.send_keys :Enter + end + private def writelines path, lines File.unlink path while File.exists? path From 772fa42dcb4c2303969d937fe8d88829d2a9eb8e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 24 Apr 2016 04:51:35 +0900 Subject: [PATCH 144/658] [fish] Fix intermittent errors on CTRL-T Related: 23244bb --- shell/key-bindings.fish | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index 0a68b62..e29fdca 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -20,8 +20,7 @@ function fzf_key_bindings -o -type d -print \ -o -type l -print 2> /dev/null | sed 1d | cut -b3-" eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m > $TMPDIR/fzf.result" - and sleep 0 - and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) + and for i in (seq 20); commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) 2> /dev/null; and break; sleep 0.1; end commandline -f repaint rm -f $TMPDIR/fzf.result end From bb70923cd8526620f7ba4a356344b87434ac483f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 24 Apr 2016 04:52:01 +0900 Subject: [PATCH 145/658] Fix flaky test cases --- test/test_go.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 2c1324b..34cdd59 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -31,7 +31,7 @@ def wait return if yield sleep 0.05 end - throw 'timeout' + raise 'timeout' end class Shell @@ -1117,13 +1117,9 @@ class TestGoFZF < TestBase def test_exitstatus_empty { '99' => '0', '999' => '1' }.each do |query, status| - tmux.send_keys "seq 100 | #{FZF} -q #{query}", :Enter + tmux.send_keys "seq 100 | #{FZF} -q #{query}; echo --\\$?--", :Enter tmux.until { |lines| lines[-2] =~ %r{ [10]/100} } tmux.send_keys :Enter - - tmux.send_keys 'echo --\$?--' - tmux.until { |lines| lines.last.include? "echo --$?--" } - tmux.send_keys :Enter tmux.until { |lines| lines.last.include? "--#{status}--" } end end From c727ba1d992acfb175adb273a89729d3df2fd9b0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 24 Apr 2016 13:32:33 +0900 Subject: [PATCH 146/658] [fzf-tmux] Do not split pane if the height is too small --- bin/fzf-tmux | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index c1dfe53..8b04c5b 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -8,6 +8,7 @@ skip="" swap="" close="" term="" +[ -n "$LINES" ] && lines=$LINES || lines=$(tput lines) while [ $# -gt 0 ]; do arg="$1" case "$arg" in @@ -60,7 +61,7 @@ while [ $# -gt 0 ]; do if [[ "$arg" =~ ^.l ]]; then [ -n "$COLUMNS" ] && max=$COLUMNS || max=$(tput cols) else - [ -n "$LINES" ] && max=$LINES || max=$(tput lines) + max=$lines fi size=$(( max - size )) [ $size -lt 0 ] && size=0 @@ -82,7 +83,7 @@ while [ $# -gt 0 ]; do shift done -if [ -z "$TMUX" ]; then +if ! [ -n "$TMUX_PANE" -a $lines -gt 15 ]; then fzf "${args[@]}" exit $? fi From e986f20a85f229f1b53dfc96e169bb875f795c41 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 24 Apr 2016 13:55:48 +0900 Subject: [PATCH 147/658] [fish] Use consistent function names for key bindings (#546) - fzf-file-widget - fzf-history-widget - fzf-cd-widget --- shell/key-bindings.fish | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index e29fdca..a02937e 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -13,7 +13,7 @@ function fzf_key_bindings end end - function __fzf_ctrl_t + function fzf-file-widget 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 \ @@ -25,14 +25,14 @@ function fzf_key_bindings rm -f $TMPDIR/fzf.result end - function __fzf_ctrl_r + function fzf-history-widget history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS > $TMPDIR/fzf.result and commandline (cat $TMPDIR/fzf.result) commandline -f repaint rm -f $TMPDIR/fzf.result end - function __fzf_alt_c + function fzf-cd-widget set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND " command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-" @@ -58,14 +58,14 @@ function fzf_key_bindings end end - bind \ct '__fzf_ctrl_t' - bind \cr '__fzf_ctrl_r' - bind \ec '__fzf_alt_c' + bind \ct fzf-file-widget + bind \cr fzf-history-widget + bind \ec fzf-cd-widget if bind -M insert > /dev/null 2>&1 - bind -M insert \ct '__fzf_ctrl_t' - bind -M insert \cr '__fzf_ctrl_r' - bind -M insert \ec '__fzf_alt_c' + bind -M insert \ct fzf-file-widget + bind -M insert \cr fzf-history-widget + bind -M insert \ec fzf-cd-widget end end From d86cee2a69a2190b177d0e0b3d4819dd9f0e033e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 24 Apr 2016 14:04:15 +0900 Subject: [PATCH 148/658] [bash] Export fzf-file-widget function for bash 4+ (#546) e.g. Remapping fzf-file-widget to CTRL-X CTRL-T intead of CTRL-T bind -x '"\C-x\C-t": fzf-file-widget' bind '"\C-t": transpose-chars' --- shell/key-bindings.bash | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 8ecd614..5ff20bb 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -29,11 +29,13 @@ __fzf_select_tmux__() { tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") bash -c 'source \"${BASH_SOURCE[0]}\"; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" } -__fzf_select_tmux_auto__() { - if [ "${FZF_TMUX:-1}" != 0 ] && [ ${LINES:-40} -gt 15 ]; then +fzf-file-widget() { + if __fzf_use_tmux__; then __fzf_select_tmux__ else - tmux send-keys -t "$TMUX_PANE" "$(__fzf_select__)" + local selected="$(__fzf_select__)" + READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}" + READLINE_POINT=$(( READLINE_POINT + ${#selected} )) fi } @@ -58,12 +60,12 @@ __fzf_history__() ( fi ) -__use_tmux=0 -__use_tmux_auto=0 -if [ -n "$TMUX_PANE" ]; then - [ "${FZF_TMUX:-1}" != 0 ] && [ ${LINES:-40} -gt 15 ] && __use_tmux=1 - [ $BASH_VERSINFO -gt 3 ] && __use_tmux_auto=1 -fi +__fzf_use_tmux__() { + [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-1}" != 0 ] && [ ${LINES:-40} -gt 15 ] +} + +[ $BASH_VERSINFO -gt 3 ] && __use_bind_x=1 || __use_bind_x=0 +__fzf_use_tmux__ && __use_tmux=1 || __use_tmux=0 if [[ $'\n'$(set -o) != *$'\n'vi*on* ]]; then # Required to refresh the prompt after fzf @@ -71,8 +73,8 @@ if [[ $'\n'$(set -o) != *$'\n'vi*on* ]]; then bind '"\e^": history-expand-line' # CTRL-T - Paste the selected file path into the command line - if [ $__use_tmux_auto -eq 1 ]; then - bind -x '"\C-t": "__fzf_select_tmux_auto__"' + if [ $__use_bind_x -eq 1 ]; then + bind -x '"\C-t": "fzf-file-widget"' elif [ $__use_tmux -eq 1 ]; then bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select_tmux__)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"' else @@ -100,8 +102,8 @@ else # CTRL-T - Paste the selected file path into the command line # - FIXME: Selected items are attached to the end regardless of cursor position - if [ $__use_tmux_auto -eq 1 ]; then - bind -x '"\C-t": "__fzf_select_tmux_auto__"' + if [ $__use_bind_x -eq 1 ]; then + bind -x '"\C-t": "fzf-file-widget"' elif [ $__use_tmux -eq 1 ]; then bind '"\C-t": "\C-x\C-a$a \C-x\C-addi$(__fzf_select_tmux__)\C-x\C-e\C-x\C-a0P$xa"' else @@ -118,6 +120,6 @@ else bind -m vi-command '"\ec": "ddi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"' fi -unset -v __use_tmux __use_tmux_auto +unset -v __use_tmux __use_bind_x fi From 095f31b31691577641894b1942b316514ae23f3f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 24 Apr 2016 17:59:32 +0900 Subject: [PATCH 149/658] [vim] Explicitly set source to FZF_DEFAULT_COMMAND Helps when your `$SHELL` is slow. Close #552. --- plugin/fzf.vim | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 43d5059..526f245 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -121,6 +121,10 @@ try throw v:exception endtry + if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) + let dict.source = $FZF_DEFAULT_COMMAND + endif + if has_key(dict, 'source') let source = dict.source let type = type(source) From 988c9bd9be48b487bccca2d1d90d67c341c3bbf3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 25 Apr 2016 00:53:11 +0900 Subject: [PATCH 150/658] [zsh] Fix issues with unicode characters --- shell/completion.zsh | 2 +- shell/key-bindings.zsh | 2 +- test/test_go.rb | 30 ++++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 25c021a..74d1ad3 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -54,7 +54,7 @@ __fzf_generic_path_completion() { [ "$dir" != "/" ] && dir="${dir/%\//}" dir=${~dir} matches=$(eval "$compgen $(printf %q "$dir")" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do - printf "%q$suffix " "$item" + echo -n "${(q)item}$suffix " done) matches=${matches% } if [ -n "$matches" ]; then diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 8a03585..f60bbae 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -9,7 +9,7 @@ __fsel() { -o -type d -print \ -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" eval "$cmd" | $(__fzfcmd) -m | while read item; do - printf '%q ' "$item" + echo -n "${(q)item} " done echo } diff --git a/test/test_go.rb b/test/test_go.rb index 34cdd59..cf1fd77 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1218,6 +1218,22 @@ module TestShell tmux.until(0) { |lines| lines[-1].include? '1 2 3' } end + def test_ctrl_t_unicode + FileUtils.mkdir_p '/tmp/fzf-test' + tmux.send_keys 'cd /tmp/fzf-test; echo -n test1 > "fzf-unicode 테스트1"; echo -n test2 > "fzf-unicode 테스트2"', :Enter + tmux.prepare + tmux.send_keys 'cat ', 'C-t', pane: 0 + tmux.until(1) { |lines| lines.item_count >= 1 } + tmux.send_keys 'fzf-unicode', pane: 1 + tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } + tmux.send_keys :BTab, :BTab, pane: 1 + tmux.until(1) { |lines| lines[-2].include? '(2)' } + tmux.send_keys :Enter, pane: 1 + tmux.until { |lines| lines[-1].include? 'cat' } + tmux.send_keys :Enter + tmux.until { |lines| lines[-1].include? 'test1test2' } + end + def test_alt_c tmux.prepare tmux.send_keys :Escape, :c, pane: 0 @@ -1418,6 +1434,20 @@ module CompletionTest tmux.send_keys :Enter tmux.until { |lines| lines[-1] == 'unset FOO' } end + + def test_file_completion_unicode + FileUtils.mkdir_p '/tmp/fzf-test' + tmux.send_keys 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"', :Enter + tmux.prepare + tmux.send_keys 'cat fzf-unicode**', :Tab, pane: 0 + tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } + tmux.send_keys :BTab, :BTab, pane: 1 + tmux.until(1) { |lines| lines[-2].include? '(2)' } + tmux.send_keys :Enter, pane: 1 + tmux.until { |lines| lines[-1].include? 'cat' } + tmux.send_keys :Enter + tmux.until { |lines| lines[-1].include? 'test3test4' } + end end class TestBash < TestBase From 35a9aff8e1caaaa18bce82e93a54b12e73c897c2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 25 Apr 2016 01:23:52 +0900 Subject: [PATCH 151/658] 0.12.1 --- CHANGELOG.md | 7 +++++++ install | 4 ++-- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9483f33..fd0515a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +0.12.1 +------ + +- Ranking algorithm introduced in 0.12.0 is now universally applied +- Fixed invalid cache reference in exact mode +- Fixes and improvements in Vim plugin and shell extensions + 0.12.0 ------ diff --git a/install b/install index df69f48..6d2e886 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.12.0 pre=1 || - version=0.12.0 pre=0 +[[ "$@" =~ --pre ]] && version=0.12.1 pre=1 || + version=0.12.1 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 649eef9..53ac3fe 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Apr 2016" "fzf 0.12.0" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Apr 2016" "fzf 0.12.1" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index bb2af44..2e41e8b 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.12.0" + version = "0.12.1" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From d1b402a23c6a736d71b44a4ac36266fa605be652 Mon Sep 17 00:00:00 2001 From: Gene Pavlovsky Date: Sun, 24 Apr 2016 21:24:10 +0300 Subject: [PATCH 152/658] Fix missing reference to UNAME_M The `Build on $(UNAME_M) is not supported, yet` message was referencing an undefined UNAME_M. Fixed that. --- src/Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Makefile b/src/Makefile index cd28289..5d5779b 100644 --- a/src/Makefile +++ b/src/Makefile @@ -7,7 +7,8 @@ else ifeq ($(UNAME_S),Linux) endif endif -ifneq ($(shell uname -m),x86_64) +UNAME_M := $(shell uname -m) +ifneq ($(UNAME_M),x86_64) $(error "Build on $(UNAME_M) is not supported, yet.") endif From 7f64fba80fd4a888adb9b279672ede7291525d5a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 26 Apr 2016 01:45:04 +0900 Subject: [PATCH 153/658] Update Makefile to allow build on i686 (#555) --- src/Makefile | 32 +++++++++++++++++--------------- src/README.md | 2 +- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/Makefile b/src/Makefile index 5d5779b..5980386 100644 --- a/src/Makefile +++ b/src/Makefile @@ -7,11 +7,6 @@ else ifeq ($(UNAME_S),Linux) endif endif -UNAME_M := $(shell uname -m) -ifneq ($(UNAME_M),x86_64) -$(error "Build on $(UNAME_M) is not supported, yet.") -endif - SOURCES := $(wildcard *.go */*.go) ROOTDIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) BINDIR := $(shell dirname $(ROOTDIR))/bin @@ -27,15 +22,22 @@ RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64 RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7 export GOPATH -all: release +UNAME_M := $(shell uname -m) +ifeq ($(UNAME_M),x86_64) + BINARY := $(BINARY64) +else ifeq ($(UNAME_M),i686) + BINARY := $(BINARY32) +else +$(error "Build on $(UNAME_M) is not supported, yet.") +endif -release: test build +all: fzf/$(BINARY) + +release: test fzf/$(BINARY32) fzf/$(BINARY64) -cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ rm -f $(RELEASE32) $(RELEASE64) -build: fzf/$(BINARY32) fzf/$(BINARY64) - $(SRCDIR): mkdir -p $(shell dirname $(SRCDIR)) ln -s $(ROOTDIR) $(SRCDIR) @@ -55,7 +57,7 @@ test: deps install: $(BINDIR)/fzf uninstall: - rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64) + rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY) clean: cd fzf && rm -f fzf-* @@ -66,9 +68,9 @@ fzf/$(BINARY32): deps fzf/$(BINARY64): deps cd fzf && go build -a -tags "$(TAGS)" -o $(BINARY64) -$(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR) - cp -f fzf/$(BINARY64) $(BINDIR) - cd $(BINDIR) && ln -sf $(BINARY64) fzf +$(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR) + cp -f fzf/$(BINARY) $(BINDIR) + cd $(BINDIR) && ln -sf $(BINARY) fzf $(BINDIR): mkdir -p $@ @@ -99,7 +101,7 @@ centos: docker-centos linux: docker-centos docker run $(DOCKEROPTS) junegunn/centos-sandbox \ - /bin/bash -ci 'cd /fzf/src; make TAGS=static' + /bin/bash -ci 'cd /fzf/src; make TAGS=static release' ubuntu-android: docker-android docker run $(DOCKEROPTS) junegunn/android-sandbox \ @@ -109,6 +111,6 @@ android: docker-android docker run $(DOCKEROPTS) junegunn/android-sandbox \ /bin/bash -ci 'cd /fzf/src; GOOS=android make android-build' -.PHONY: all build deps release test install uninstall clean \ +.PHONY: all deps release test install uninstall clean \ linux arch ubuntu centos docker-arch docker-ubuntu docker-centos \ android-build docker-android ubuntu-android android diff --git a/src/README.md b/src/README.md index 18ccec3..181b761 100644 --- a/src/README.md +++ b/src/README.md @@ -79,7 +79,7 @@ Build ```sh # Build fzf executables and tarballs -make +make release # Install the executable to ../bin directory make install From 0231617857f812abfae5f68e2b8c7e9ce7053d59 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 28 Apr 2016 01:25:24 +0900 Subject: [PATCH 154/658] [neovim] Fix issues with `enew` and `tabnew` layouts Related: #559 --- plugin/fzf.vim | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 526f245..d6a79f3 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -298,7 +298,7 @@ function! s:split(dict) \ 'down': ['botright', 'resize', &lines], \ 'left': ['vertical topleft', 'vertical resize', &columns], \ 'right': ['vertical botright', 'vertical resize', &columns] } - let s:ppos = s:getpos() + let ppos = s:getpos() try for [dir, triple] in items(directions) let val = get(a:dict, dir, '') @@ -311,7 +311,7 @@ function! s:split(dict) endif execute cmd sz.'new' execute resz sz - return {} + return [ppos, {}] endif endfor if s:present(a:dict, 'window') @@ -319,36 +319,44 @@ function! s:split(dict) else execute (tabpagenr()-1).'tabnew' endif - return { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh } + return [ppos, { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh }] finally setlocal winfixwidth winfixheight endtry endfunction function! s:execute_term(dict, command, temps) abort - let winopts = s:split(a:dict) - - let fzf = { 'buf': bufnr('%'), 'dict': a:dict, 'temps': a:temps, 'name': 'FZF', 'winopts': winopts } - let s:command = a:command + let [ppos, winopts] = s:split(a:dict) + let fzf = { 'buf': bufnr('%'), 'ppos': ppos, 'dict': a:dict, 'temps': a:temps, + \ 'name': 'FZF', 'winopts': winopts, 'command': a:command } + function! fzf.switch_back(inplace) + if a:inplace && bufnr('') == self.buf + " FIXME: Can't re-enter normal mode from terminal mode + " execute "normal! \" + b # + " No other listed buffer + if bufnr('') == self.buf + enew + endif + endif + endfunction function! fzf.on_exit(id, code) - let pos = s:getpos() - let inplace = pos == s:ppos " {'window': 'enew'} - if inplace + if s:getpos() == self.ppos " {'window': 'enew'} for [opt, val] in items(self.winopts) execute 'let' opt '=' val endfor + call self.switch_back(1) else if bufnr('') == self.buf " We use close instead of bd! since Vim does not close the split when " there's no other listed buffer (nvim +'set nobuflisted') close endif - if pos.tab == s:ppos.tab - wincmd p - endif + execute 'tabnext' self.ppos.tab + execute self.ppos.win.'wincmd w' endif - if !s:exit_handler(a:code, s:command, 1) + if !s:exit_handler(a:code, self.command, 1) return endif @@ -356,14 +364,7 @@ function! s:execute_term(dict, command, temps) abort let ret = [] try let ret = s:callback(self.dict, self.temps) - - if inplace && bufnr('') == self.buf - execute "normal! \" - " No other listed buffer - if bufnr('') == self.buf - bd! - endif - endif + call self.switch_back(s:getpos() == self.ppos) finally call s:popd(self.dict, ret) endtry From b4c4a642eda9b34bc417b0cc988e3b158080be33 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 3 May 2016 00:07:53 +0900 Subject: [PATCH 155/658] Update README Close #560, #561 --- README.md | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c26066c..76a93ab 100644 --- a/README.md +++ b/README.md @@ -151,27 +151,6 @@ Many useful examples can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/examples). Feel free to add your own as well. -Key bindings for command line ------------------------------ - -The install script will setup the following key bindings for bash, zsh, and -fish. - -- `CTRL-T` - Paste the selected files and directories onto 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 - - Press `CTRL-R` again to toggle sort -- `ALT-C` - cd into the selected directory - -If you're on a tmux session, fzf will start in a split pane. You may disable -this tmux integration by setting `FZF_TMUX` to 0, or change the height of the -pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`). - -If you use vi mode on bash, you need to add `set -o vi` *before* `source -~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi -mode. - `fzf-tmux` script ----------------- @@ -191,6 +170,28 @@ cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse It will still work even when you're not on tmux, silently ignoring `-[udlr]` options, so you can invariably use `fzf-tmux` in your scripts. +Key bindings for command line +----------------------------- + +The install script will setup the following key bindings for bash, zsh, and +fish. + +- `CTRL-T` - Paste the selected files and directories onto 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 + - Press `CTRL-R` again to toggle sort +- `ALT-C` - cd into the selected directory + - Set `FZF_ALT_C_COMMAND` to override the default command + +If you're on a tmux session, fzf will start in a split pane. You may disable +this tmux integration by setting `FZF_TMUX` to 0, or change the height of the +pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`). + +If you use vi mode on bash, you need to add `set -o vi` *before* `source +~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi +mode. + Fuzzy completion for bash and zsh --------------------------------- From 24516bcf4d3be8a1a626b6ccdbe79a740fd5754f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 May 2016 02:03:08 +0900 Subject: [PATCH 156/658] [install] Set a temporary GOPATH --- install | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/install b/install index 6d2e886..8ca5cbe 100755 --- a/install +++ b/install @@ -231,9 +231,13 @@ if [ -n "$binary_error" ]; then echo "No prebuilt binary for $archi ..." if command -v go > /dev/null; then echo -n "Building binary (go get github.com/junegunn/fzf/src/fzf) ... " + if [ -z "${GOPATH-}" ]; then + export GOPATH="${TMPDIR:-/tmp}/fzf-gopath" + mkdir -p "$GOPATH" + fi if go get github.com/junegunn/fzf/src/fzf; then echo "OK" - link_fzf_in_path + cp "$GOPATH/bin/fzf" "$fzf_base/bin/" else echo "Failed to build binary ..." install_ruby_fzf From 88a80e3c2ca8f3112a13023b4bb35101ed85a8d5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 May 2016 01:07:06 +0900 Subject: [PATCH 157/658] Determine 256-color capability using tigetnum("colors") Close #570 --- src/curses/curses.go | 75 +++++++++++++++++++++++++++++++------------- src/options.go | 11 ++----- 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index 7c42c93..ebe6415 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -109,6 +109,8 @@ const ( const ( doubleClickDuration = 500 * time.Millisecond + colDefault = -1 + colUndefined = -2 ) type ColorTheme struct { @@ -159,6 +161,23 @@ var ( DarkBG int ) +func EmptyTheme() *ColorTheme { + return &ColorTheme{ + UseDefault: true, + Fg: colUndefined, + Bg: colUndefined, + DarkBg: colUndefined, + Prompt: colUndefined, + Match: colUndefined, + Current: colUndefined, + CurrentMatch: colUndefined, + Spinner: colUndefined, + Info: colUndefined, + Cursor: colUndefined, + Selected: colUndefined, + Header: colUndefined} +} + func init() { _prevDownTime = time.Unix(0, 0) _clickY = []int{} @@ -280,44 +299,58 @@ func Init(theme *ColorTheme, black bool, mouse bool) { if theme != nil { C.start_color() - initPairs(theme, black) + var baseTheme *ColorTheme + if C.tigetnum(C.CString("colors")) >= 256 { + baseTheme = Dark256 + } else { + baseTheme = Default16 + } + initPairs(baseTheme, theme, black) _color = attrColored } else { _color = attrMono } } -func initPairs(theme *ColorTheme, black bool) { - fg := C.short(theme.Fg) - bg := C.short(theme.Bg) +func override(a int16, b int16) C.short { + if b == colUndefined { + return C.short(a) + } + return C.short(b) +} + +func initPairs(baseTheme *ColorTheme, theme *ColorTheme, black bool) { + fg := override(baseTheme.Fg, theme.Fg) + bg := override(baseTheme.Bg, theme.Bg) if black { bg = C.COLOR_BLACK } else if theme.UseDefault { - fg = -1 - bg = -1 + fg = colDefault + bg = colDefault C.use_default_colors() } if theme.UseDefault { - FG = -1 - BG = -1 + FG = colDefault + BG = colDefault } else { FG = int(fg) BG = int(bg) - C.assume_default_colors(C.int(theme.Fg), C.int(bg)) + C.assume_default_colors(C.int(override(baseTheme.Fg, theme.Fg)), C.int(bg)) } - CurrentFG = int(theme.Current) - DarkBG = int(theme.DarkBg) - darkBG := C.short(DarkBG) - C.init_pair(ColPrompt, C.short(theme.Prompt), bg) - C.init_pair(ColMatch, C.short(theme.Match), bg) - C.init_pair(ColCurrent, C.short(theme.Current), darkBG) - C.init_pair(ColCurrentMatch, C.short(theme.CurrentMatch), darkBG) - C.init_pair(ColSpinner, C.short(theme.Spinner), bg) - C.init_pair(ColInfo, C.short(theme.Info), bg) - C.init_pair(ColCursor, C.short(theme.Cursor), darkBG) - C.init_pair(ColSelected, C.short(theme.Selected), darkBG) - C.init_pair(ColHeader, C.short(theme.Header), bg) + currentFG := override(baseTheme.Current, theme.Current) + darkBG := override(baseTheme.DarkBg, theme.DarkBg) + CurrentFG = int(currentFG) + DarkBG = int(darkBG) + C.init_pair(ColPrompt, override(baseTheme.Prompt, theme.Prompt), bg) + C.init_pair(ColMatch, override(baseTheme.Match, theme.Match), bg) + C.init_pair(ColCurrent, currentFG, darkBG) + C.init_pair(ColCurrentMatch, override(baseTheme.CurrentMatch, theme.CurrentMatch), darkBG) + C.init_pair(ColSpinner, override(baseTheme.Spinner, theme.Spinner), bg) + C.init_pair(ColInfo, override(baseTheme.Info, theme.Info), bg) + C.init_pair(ColCursor, override(baseTheme.Cursor, theme.Cursor), darkBG) + C.init_pair(ColSelected, override(baseTheme.Selected, theme.Selected), darkBG) + C.init_pair(ColHeader, override(baseTheme.Header, theme.Header), bg) } func Close() { diff --git a/src/options.go b/src/options.go index dfd9a1b..ab63308 100644 --- a/src/options.go +++ b/src/options.go @@ -132,13 +132,6 @@ type Options struct { Version bool } -func defaultTheme() *curses.ColorTheme { - if strings.Contains(os.Getenv("TERM"), "256") { - return curses.Dark256 - } - return curses.Default16 -} - func defaultOptions() *Options { return &Options{ Fuzzy: true, @@ -153,7 +146,7 @@ func defaultOptions() *Options { Multi: false, Ansi: false, Mouse: true, - Theme: defaultTheme(), + Theme: curses.EmptyTheme(), Black: false, Reverse: false, Cycle: false, @@ -745,7 +738,7 @@ func parseOptions(opts *Options, allArgs []string) { case "--color": spec := optionalNextString(allArgs, &i) if len(spec) == 0 { - opts.Theme = defaultTheme() + opts.Theme = curses.EmptyTheme() } else { opts.Theme = parseTheme(opts.Theme, spec) } From d082cccb6d86081afddad6afbafa505f10936480 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 May 2016 01:16:26 +0900 Subject: [PATCH 158/658] Fix flaky test case: test_ctrl_t_unicode The width of the pseudo-terminal on Travis CI environment can be small and cause the line to be wrapped. --- test/test_go.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index cf1fd77..c09c86f 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1229,7 +1229,7 @@ module TestShell tmux.send_keys :BTab, :BTab, pane: 1 tmux.until(1) { |lines| lines[-2].include? '(2)' } tmux.send_keys :Enter, pane: 1 - tmux.until { |lines| lines[-1].include? 'cat' } + tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') } tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'test1test2' } end From 73cb70dbb34d71ef88d9e161e6f8a6eed81eefaa Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 May 2016 01:25:17 +0900 Subject: [PATCH 159/658] Fix flaky test case: test_file_completion_unicode --- test/test_go.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index c09c86f..19759fe 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1444,7 +1444,7 @@ module CompletionTest tmux.send_keys :BTab, :BTab, pane: 1 tmux.until(1) { |lines| lines[-2].include? '(2)' } tmux.send_keys :Enter, pane: 1 - tmux.until { |lines| lines[-1].include? 'cat' } + tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') } tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'test3test4' } end From 4ab75b68dcbe93baf8ef65a3cb6230351deae46a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 May 2016 01:40:49 +0900 Subject: [PATCH 160/658] Fix flaky test case: test_execute Should wait until execute action completes --- test/test_go.rb | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 19759fe..64b4e26 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -859,16 +859,27 @@ class TestGoFZF < TestBase def test_execute output = '/tmp/fzf-test-execute' opts = %[--bind \\"alt-a:execute(echo '[{}]' >> #{output}),alt-b:execute[echo '({}), ({})' >> #{output}],C:execute:echo '({}), [{}], @{}@' >> #{output}\\"] - tmux.send_keys "seq 100 | #{fzf opts}", :Enter - tmux.until { |lines| lines[-2].include? '100/100' } - tmux.send_keys :Escape, :a, :Escape, :a + wait = lambda { |exp| tmux.until { |lines| lines[-2].include? exp } } + tmux.send_keys "seq 100 | #{fzf opts}; sync", :Enter + wait['100/100'] + tmux.send_keys :Escape, :a + wait['/100'] + tmux.send_keys :Escape, :a + wait['/100'] tmux.send_keys :Up - tmux.send_keys :Escape, :b, :Escape, :b + tmux.send_keys :Escape, :b + wait['/100'] + tmux.send_keys :Escape, :b + wait['/100'] tmux.send_keys :Up tmux.send_keys :C + wait['100/100'] tmux.send_keys 'foobar' - tmux.until { |lines| lines[-2].include? '0/100' } - tmux.send_keys :Escape, :a, :Escape, :b, :Escape, :c + wait['0/100'] + tmux.send_keys :Escape, :a + wait['/100'] + tmux.send_keys :Escape, :b + wait['/100'] tmux.send_keys :Enter readonce assert_equal ['["1"]', '["1"]', '("2"), ("2")', '("2"), ("2")', '("3"), ["3"], @"3"@'], From 66f86e18700f08daa20404d212021da412800d53 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 May 2016 22:07:33 +0900 Subject: [PATCH 161/658] [fzf-tmux] Fix #562 - Check $TMUX instead of $TMUX_PANE --- bin/fzf-tmux | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 8b04c5b..a164a62 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -83,7 +83,7 @@ while [ $# -gt 0 ]; do shift done -if ! [ -n "$TMUX_PANE" -a $lines -gt 15 ]; then +if ! [ -n "$TMUX" -a "$lines" -gt 15 ]; then fzf "${args[@]}" exit $? fi From dd4be1da380724cdb91daccab8978ed50d7ae6ab Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 13 May 2016 00:36:13 +0900 Subject: [PATCH 162/658] Allow alt-enter and alt-space for --bind (#571) --- src/curses/curses.go | 6 ++++++ src/options.go | 4 ++++ src/options_test.go | 8 +++++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index ebe6415..0ad382b 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -81,6 +81,8 @@ const ( F3 F4 + AltEnter + AltSpace AltBS AltA AltB @@ -417,6 +419,10 @@ func escSequence(sz *int) Event { } *sz = 2 switch _buf[1] { + case 13: + return Event{AltEnter, 0, nil} + case 32: + return Event{AltSpace, 0, nil} case 98: return Event{AltB, 0, nil} case 100: diff --git a/src/options.go b/src/options.go index ab63308..4d5ac2d 100644 --- a/src/options.go +++ b/src/options.go @@ -315,6 +315,10 @@ func parseKeyChords(str string, message string) map[int]string { chord = curses.AltZ + int(' ') case "bspace", "bs": chord = curses.BSpace + case "alt-enter", "alt-return": + chord = curses.AltEnter + case "alt-space": + chord = curses.AltSpace case "alt-bs", "alt-bspace": chord = curses.AltBS case "tab": diff --git a/src/options_test.go b/src/options_test.go index 81845d2..f3e62f8 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -123,14 +123,14 @@ func TestIrrelevantNth(t *testing.T) { } func TestParseKeys(t *testing.T) { - pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g", "") + pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ALT-enter,alt-SPACE", "") check := func(i int, s string) { if pairs[i] != s { t.Errorf("%s != %s", pairs[i], s) } } - if len(pairs) != 9 { - t.Error(9) + if len(pairs) != 11 { + t.Error(11) } check(curses.CtrlZ, "ctrl-z") check(curses.AltZ, "alt-z") @@ -141,6 +141,8 @@ func TestParseKeys(t *testing.T) { check(curses.CtrlA+'g'-'a', "ctrl-G") check(curses.AltZ+'J', "J") check(curses.AltZ+'g', "g") + check(curses.AltEnter, "ALT-enter") + check(curses.AltSpace, "alt-SPACE") // Synonyms pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") From 9bd8b1d25f7ea255fa104729e81e43453f439612 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 13 May 2016 00:44:27 +0900 Subject: [PATCH 163/658] Fix typo --- test/test_go.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index 64b4e26..81f77c9 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1037,7 +1037,7 @@ class TestGoFZF < TestBase end end - def test_canel + def test_cancel tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter tmux.until { |lines| lines[-2].include?('10/10') } tmux.send_keys '123' From 9078688baf9ee82c5cbe222b825a247dffe2814e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 13 May 2016 00:51:15 +0900 Subject: [PATCH 164/658] Add print-query action for --bind Close #571 --- src/options.go | 2 ++ src/terminal.go | 8 ++++++++ test/test_go.rb | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/src/options.go b/src/options.go index 4d5ac2d..ea7d717 100644 --- a/src/options.go +++ b/src/options.go @@ -531,6 +531,8 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) keymap[key] = actAbort case "accept": keymap[key] = actAccept + case "print-query": + keymap[key] = actPrintQuery case "backward-char": keymap[key] = actBackwardChar case "backward-delete-char": diff --git a/src/terminal.go b/src/terminal.go index cd9ca93..771cad7 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -91,6 +91,7 @@ const ( reqRefresh reqRedraw reqClose + reqPrintQuery reqQuit ) @@ -132,6 +133,7 @@ const ( actUp actPageUp actPageDown + actPrintQuery actToggleSort actPreviousHistory actNextHistory @@ -819,6 +821,10 @@ func (t *Terminal) Loop() { exit(exitOk) } exit(exitNoMatch) + case reqPrintQuery: + C.Close() + fmt.Println(string(t.input)) + exit(exitOk) case reqQuit: C.Close() exit(exitInterrupt) @@ -906,6 +912,8 @@ func (t *Terminal) Loop() { if t.cx > 0 { t.cx-- } + case actPrintQuery: + req(reqPrintQuery) case actAbort: req(reqQuit) case actDeleteChar: diff --git a/test/test_go.rb b/test/test_go.rb index 81f77c9..02bf08d 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -771,6 +771,13 @@ class TestGoFZF < TestBase assert_equal %w[4 5 6 9], readonce.split($/) end + def test_bind_print_query + tmux.send_keys "seq 1 1000 | #{fzf '-m --bind=ctrl-j:print-query'}", :Enter + tmux.until { |lines| lines[-2].end_with? '/1000' } + tmux.send_keys 'print-my-query', 'C-j' + assert_equal %w[print-my-query], readonce.split($/) + end + def test_long_line data = '.' * 256 * 1024 File.open(tempname, 'w') do |f| From faccc0a41036a7f41b28899fad4684e1a464cc73 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 May 2016 17:07:34 +0900 Subject: [PATCH 165/658] [fzf-tmux] Escape backslash in command-line arguments --- bin/fzf-tmux | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index a164a62..4de93e2 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -134,6 +134,7 @@ mkfifo -m o+w $fifo3 # Build arguments to fzf opts="" for arg in "${args[@]}"; do + arg="${arg//\\/\\\\}" arg="${arg//\"/\\\"}" arg="${arg//\`/\\\`}" opts="$opts \"$arg\"" From 4adebfc8560a367be61bb6c024095586ca6382c8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 17 May 2016 01:41:59 +0900 Subject: [PATCH 166/658] [install] go get -u github.com/junegunn/fzf/src/fzf --- install | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install b/install index 8ca5cbe..32cc19d 100755 --- a/install +++ b/install @@ -230,12 +230,12 @@ if [ -n "$binary_error" ]; then if [ $binary_available -eq 0 ]; then echo "No prebuilt binary for $archi ..." if command -v go > /dev/null; then - echo -n "Building binary (go get github.com/junegunn/fzf/src/fzf) ... " + echo -n "Building binary (go get -u github.com/junegunn/fzf/src/fzf) ... " if [ -z "${GOPATH-}" ]; then export GOPATH="${TMPDIR:-/tmp}/fzf-gopath" mkdir -p "$GOPATH" fi - if go get github.com/junegunn/fzf/src/fzf; then + if go get -u github.com/junegunn/fzf/src/fzf; then echo "OK" cp "$GOPATH/bin/fzf" "$fzf_base/bin/" else From 6d235bceee66bd6b7ca2de8311a3deae4793bfa8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 18 May 2016 02:06:52 +0900 Subject: [PATCH 167/658] Add jump and jump-accept actions for --bind jump and jump-accept implement EasyMotion-like movement in fzf. Suggested by @mhrebenyuk. Close #569. --- src/constants.go | 3 ++ src/options.go | 15 ++++++++++ src/terminal.go | 75 +++++++++++++++++++++++++++++++++++++++--------- test/test_go.rb | 37 ++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 13 deletions(-) diff --git a/src/constants.go b/src/constants.go index 2e41e8b..eb9262d 100644 --- a/src/constants.go +++ b/src/constants.go @@ -36,6 +36,9 @@ const ( // History defaultHistoryMax int = 1000 + + // Jump labels + defaultJumpLabels string = "qwertyuiopasdfghjklzxcvbnm1234567890QWERTYUIOPASDFGHJKLZXCVBNM" ) // fzf events diff --git a/src/options.go b/src/options.go index ea7d717..089b4c2 100644 --- a/src/options.go +++ b/src/options.go @@ -45,6 +45,7 @@ const usage = `usage: fzf [options] --hscroll-off=COL Number of screen columns to keep to the right of the highlighted substring (default: 10) --inline-info Display finder info inline with the query + --jump-labels=CHARS Label characters for jump and jump-accept --prompt=STR Input prompt (default: '> ') --bind=KEYBINDS Custom key bindings. Refer to the man page. --history=FILE History file @@ -112,6 +113,7 @@ type Options struct { Hscroll bool HscrollOff int InlineInfo bool + JumpLabels string Prompt string Query string Select1 bool @@ -153,6 +155,7 @@ func defaultOptions() *Options { Hscroll: true, HscrollOff: 10, InlineInfo: false, + JumpLabels: defaultJumpLabels, Prompt: "> ", Query: "", Select1: false, @@ -553,6 +556,10 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) keymap[key] = actForwardChar case "forward-word": keymap[key] = actForwardWord + case "jump": + keymap[key] = actJump + case "jump-accept": + keymap[key] = actJumpAccept case "kill-line": keymap[key] = actKillLine case "kill-word": @@ -804,6 +811,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.InlineInfo = true case "--no-inline-info": opts.InlineInfo = false + case "--jump-labels": + opts.JumpLabels = nextString(allArgs, &i, "label characters required") case "-1", "--select-1": opts.Select1 = true case "+1", "--no-select-1": @@ -891,6 +900,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.Tabstop = atoi(value) } else if match, value := optString(arg, "--hscroll-off="); match { opts.HscrollOff = atoi(value) + } else if match, value := optString(arg, "--jump-labels="); match { + opts.JumpLabels = value } else { errorExit("unknown option: " + arg) } @@ -908,6 +919,10 @@ func parseOptions(opts *Options, allArgs []string) { if opts.Tabstop < 1 { errorExit("tab stop must be a positive integer") } + + if len(opts.JumpLabels) == 0 { + errorExit("empty jump labels") + } } func postProcessOptions(opts *Options) { diff --git a/src/terminal.go b/src/terminal.go index 771cad7..4f611eb 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -19,6 +19,14 @@ import ( "github.com/junegunn/go-runewidth" ) +type jumpMode int + +const ( + jumpDisabled jumpMode = iota + jumpEnabled + jumpAcceptEnabled +) + // Terminal represents terminal input/output type Terminal struct { initDelay time.Duration @@ -50,6 +58,8 @@ type Terminal struct { count int progress int reading bool + jumping jumpMode + jumpLabels string merger *Merger selected map[int32]selectedItem reqBox *util.EventBox @@ -88,6 +98,7 @@ const ( reqInfo reqHeader reqList + reqJump reqRefresh reqRedraw reqClose @@ -133,6 +144,8 @@ const ( actUp actPageUp actPageDown + actJump + actJumpAccept actPrintQuery actToggleSort actPreviousHistory @@ -235,6 +248,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { header0: header, ansi: opts.Ansi, reading: true, + jumping: jumpDisabled, + jumpLabels: opts.JumpLabels, merger: EmptyMerger, selected: make(map[int32]selectedItem), reqBox: util.NewEventBox(), @@ -497,15 +512,25 @@ func (t *Terminal) printList() { } t.move(line, 0, true) if i < count { - t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset) + t.printItem(t.merger.Get(i+t.offset), i, i == t.cy-t.offset) } } } -func (t *Terminal) printItem(item *Item, current bool) { +func (t *Terminal) printItem(item *Item, i int, current bool) { _, selected := t.selected[item.Index()] + label := " " + if t.jumping != jumpDisabled { + if i < len(t.jumpLabels) { + // Striped + current = i%2 == 0 + label = t.jumpLabels[i : i+1] + } + } else if current { + label = ">" + } + C.CPrint(C.ColCursor, true, label) if current { - C.CPrint(C.ColCursor, true, ">") if selected { C.CPrint(C.ColSelected, true, ">") } else { @@ -513,7 +538,6 @@ func (t *Terminal) printItem(item *Item, current bool) { } t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true) } else { - C.CPrint(C.ColCursor, true, " ") if selected { C.CPrint(C.ColSelected, true, ">") } else { @@ -806,6 +830,11 @@ func (t *Terminal) Loop() { t.printInfo() case reqList: t.printList() + case reqJump: + if t.merger.Length() == 0 { + t.jumping = jumpDisabled + } + t.printList() case reqHeader: t.printHeader() case reqRefresh: @@ -1025,6 +1054,12 @@ func (t *Terminal) Loop() { case actPageDown: t.vmove(-(t.maxItems() - 1)) req(reqList) + case actJump: + t.jumping = jumpEnabled + req(reqJump) + case actJumpAccept: + t.jumping = jumpAcceptEnabled + req(reqJump) case actBackwardWord: t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 case actForwardWord: @@ -1104,18 +1139,32 @@ func (t *Terminal) Loop() { } return true } - action := t.keymap[event.Type] + changed := false mapkey := event.Type - if event.Type == C.Rune { - mapkey = int(event.Char) + int(C.AltZ) - if act, prs := t.keymap[mapkey]; prs { - action = act + if t.jumping == jumpDisabled { + action := t.keymap[mapkey] + if mapkey == C.Rune { + mapkey = int(event.Char) + int(C.AltZ) + if act, prs := t.keymap[mapkey]; prs { + action = act + } } + if !doAction(action, mapkey) { + continue + } + changed = string(previousInput) != string(t.input) + } else { + if mapkey == C.Rune { + if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 { + t.cy = idx + t.offset + if t.jumping == jumpAcceptEnabled { + req(reqClose) + } + } + } + t.jumping = jumpDisabled + req(reqList) } - if !doAction(action, mapkey) { - continue - } - changed := string(previousInput) != string(t.input) t.mutex.Unlock() // Must be unlocked before touching reqBox if changed { diff --git a/test/test_go.rb b/test/test_go.rb index 02bf08d..8f33693 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1181,6 +1181,43 @@ class TestGoFZF < TestBase tmux.send_keys :Enter end + def test_jump + tmux.send_keys "seq 1000 | #{fzf "--multi --jump-labels 12345 --bind 'ctrl-j:jump'"}", :Enter + tmux.until { |lines| lines[-2] == ' 1000/1000' } + tmux.send_keys 'C-j' + tmux.until { |lines| lines[-7] == '5 5' } + tmux.until { |lines| lines[-8] == ' 6' } + tmux.send_keys '5' + tmux.until { |lines| lines[-7] == '> 5' } + tmux.send_keys :Tab + tmux.until { |lines| lines[-7] == ' >5' } + tmux.send_keys 'C-j' + tmux.until { |lines| lines[-7] == '5>5' } + tmux.send_keys '2' + tmux.until { |lines| lines[-4] == '> 2' } + tmux.send_keys :Tab + tmux.until { |lines| lines[-4] == ' >2' } + tmux.send_keys 'C-j' + tmux.until { |lines| lines[-7] == '5>5' } + + # Press any key other than jump labels to cancel jump + tmux.send_keys '6' + tmux.until { |lines| lines[-3] == '> 1' } + tmux.send_keys :Tab + tmux.until { |lines| lines[-3] == '>>1' } + tmux.send_keys :Enter + assert_equal %w[5 2 1], readonce.split($/) + end + + def test_jump_accept + tmux.send_keys "seq 1000 | #{fzf "--multi --jump-labels 12345 --bind 'ctrl-j:jump-accept'"}", :Enter + tmux.until { |lines| lines[-2] == ' 1000/1000' } + tmux.send_keys 'C-j' + tmux.until { |lines| lines[-7] == '5 5' } + tmux.send_keys '3' + assert_equal '3', readonce.chomp + end + private def writelines path, lines File.unlink path while File.exists? path From 70245ad98cb3fa8d2c13b882e615317488ccbdc0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 18 May 2016 13:29:09 +0900 Subject: [PATCH 168/658] [make] Reduce the size of the binaries with `-ldflags -w` Related: #555 --- src/Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Makefile b/src/Makefile index 5980386..f7aa0b2 100644 --- a/src/Makefile +++ b/src/Makefile @@ -47,7 +47,7 @@ deps: $(SRCDIR) $(SOURCES) android-build: $(SRCDIR) cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get - cd $(SRCDIR)/fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-extldflags=-pie" -o $(BINARYARM7) + cd $(SRCDIR)/fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-w -extldflags=-pie" -o $(BINARYARM7) cd $(SRCDIR)/fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) && \ rm -f $(RELEASEARM7) @@ -63,10 +63,10 @@ clean: cd fzf && rm -f fzf-* fzf/$(BINARY32): deps - cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -tags "$(TAGS)" -o $(BINARY32) + cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -ldflags -w -tags "$(TAGS)" -o $(BINARY32) fzf/$(BINARY64): deps - cd fzf && go build -a -tags "$(TAGS)" -o $(BINARY64) + cd fzf && go build -a -ldflags -w -tags "$(TAGS)" -o $(BINARY64) $(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR) cp -f fzf/$(BINARY) $(BINDIR) From c62fc5e75c09262a384e703800fefa6850afa2e8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 18 May 2016 22:25:09 +0900 Subject: [PATCH 169/658] More named keys: F5 ~ F10, ALT-/ --- src/curses/curses.go | 33 +++++++++++++++++++++++++++++++++ src/options.go | 6 +++++- test/test_go.rb | 4 ++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index 0ad382b..a0240da 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -80,9 +80,16 @@ const ( F2 F3 F4 + F5 + F6 + F7 + F8 + F9 + F10 AltEnter AltSpace + AltSlash AltBS AltA AltB @@ -423,6 +430,8 @@ func escSequence(sz *int) Event { return Event{AltEnter, 0, nil} case 32: return Event{AltSpace, 0, nil} + case 47: + return Event{AltSlash, 0, nil} case 98: return Event{AltB, 0, nil} case 100: @@ -468,6 +477,15 @@ func escSequence(sz *int) Event { *sz = 4 switch _buf[2] { case 50: + if len(_buf) == 5 && _buf[4] == 126 { + *sz = 5 + switch _buf[3] { + case 48: + return Event{F9, 0, nil} + case 49: + return Event{F10, 0, nil} + } + } return Event{Invalid, 0, nil} // INS case 51: return Event{Del, 0, nil} @@ -481,6 +499,21 @@ func escSequence(sz *int) Event { switch _buf[3] { case 126: return Event{Home, 0, nil} + case 53, 55, 56, 57: + if len(_buf) == 5 && _buf[4] == 126 { + *sz = 5 + switch _buf[3] { + case 53: + return Event{F5, 0, nil} + case 55: + return Event{F6, 0, nil} + case 56: + return Event{F7, 0, nil} + case 57: + return Event{F8, 0, nil} + } + } + return Event{Invalid, 0, nil} case 59: if len(_buf) != 6 { return Event{Invalid, 0, nil} diff --git a/src/options.go b/src/options.go index 089b4c2..10a2228 100644 --- a/src/options.go +++ b/src/options.go @@ -322,6 +322,8 @@ func parseKeyChords(str string, message string) map[int]string { chord = curses.AltEnter case "alt-space": chord = curses.AltSpace + case "alt-/": + chord = curses.AltSlash case "alt-bs", "alt-bspace": chord = curses.AltBS case "tab": @@ -346,12 +348,14 @@ func parseKeyChords(str string, message string) map[int]string { chord = curses.SRight case "double-click": chord = curses.DoubleClick + case "f10": + chord = curses.F10 default: if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { chord = curses.CtrlA + int(lkey[5]) - 'a' } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { chord = curses.AltA + int(lkey[4]) - 'a' - } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' { + } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' { chord = curses.F1 + int(key[1]) - '1' } else if utf8.RuneCountInString(key) == 1 { chord = curses.AltZ + int([]rune(key)[0]) diff --git a/test/test_go.rb b/test/test_go.rb index 8f33693..d886c32 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -430,6 +430,10 @@ class TestGoFZF < TestBase test.call 'f3', 'f3' test.call 'f2,f4', 'f2', 'f2' test.call 'f2,f4', 'f4', 'f4' + test.call 'alt-/', [:Escape, :/] + %w[f5 f6 f7 f8 f9 f10].each do |key| + test.call 'f5,f6,f7,f8,f9,f10', key, key + end test.call '@', '@' end From e53535cc61e67ed58f281c943786ba84e1aeef9c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 18 May 2016 22:44:31 +0900 Subject: [PATCH 170/658] Update default jump labels --- src/constants.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constants.go b/src/constants.go index eb9262d..bee142f 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.12.1" + version = "0.12.2" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond @@ -38,7 +38,7 @@ const ( defaultHistoryMax int = 1000 // Jump labels - defaultJumpLabels string = "qwertyuiopasdfghjklzxcvbnm1234567890QWERTYUIOPASDFGHJKLZXCVBNM" + defaultJumpLabels string = "qwertyuiopasdfghjklzxcvbnm1234567890QWERTYUIOPASDFGHJKLZXCVBNM,<.>/?!@#$%^&*<>/?()[{]}" ) // fzf events From 13330738b8f8bd157e6c75c00172022636fc3422 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 18 May 2016 22:45:34 +0900 Subject: [PATCH 171/658] Do not match jump labels beyond the screen limit --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 4f611eb..c440292 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1155,7 +1155,7 @@ func (t *Terminal) Loop() { changed = string(previousInput) != string(t.input) } else { if mapkey == C.Rune { - if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 { + if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() { t.cy = idx + t.offset if t.jumping == jumpAcceptEnabled { req(reqClose) From f498a9b3fb9d9a78c6c9866ffc0e296c21b3d7b4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 18 May 2016 22:47:57 +0900 Subject: [PATCH 172/658] Revert version number --- src/constants.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants.go b/src/constants.go index bee142f..8be40ed 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.12.2" + version = "0.12.1" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 7ed9f8366235a70ce03cf3ab05fcfb6dbcfd9a10 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 19 May 2016 01:46:22 +0900 Subject: [PATCH 173/658] Validate jump label characters Also extend default jump labels --- src/constants.go | 2 +- src/options.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/constants.go b/src/constants.go index 8be40ed..c60c958 100644 --- a/src/constants.go +++ b/src/constants.go @@ -38,7 +38,7 @@ const ( defaultHistoryMax int = 1000 // Jump labels - defaultJumpLabels string = "qwertyuiopasdfghjklzxcvbnm1234567890QWERTYUIOPASDFGHJKLZXCVBNM,<.>/?!@#$%^&*<>/?()[{]}" + defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+" ) // fzf events diff --git a/src/options.go b/src/options.go index 10a2228..ffd57e9 100644 --- a/src/options.go +++ b/src/options.go @@ -724,6 +724,7 @@ func parseOptions(opts *Options, allArgs []string) { opts.History.maxSize = historyMax } } + validateJumpLabels := false for i := 0; i < len(allArgs); i++ { arg := allArgs[i] switch arg { @@ -817,6 +818,7 @@ func parseOptions(opts *Options, allArgs []string) { opts.InlineInfo = false case "--jump-labels": opts.JumpLabels = nextString(allArgs, &i, "label characters required") + validateJumpLabels = true case "-1", "--select-1": opts.Select1 = true case "+1", "--no-select-1": @@ -927,6 +929,14 @@ func parseOptions(opts *Options, allArgs []string) { if len(opts.JumpLabels) == 0 { errorExit("empty jump labels") } + + if validateJumpLabels { + for _, r := range opts.JumpLabels { + if r < 32 || r > 126 { + errorExit("non-ascii jump labels are not allowed") + } + } + } } func postProcessOptions(opts *Options) { From 2f364c62f442af24ed332dc6fe018e676b8dd28c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 19 May 2016 01:47:43 +0900 Subject: [PATCH 174/658] 0.12.2 --- CHANGELOG.md | 14 ++++++++++++++ install | 4 ++-- man/man1/fzf.1 | 13 +++++++++++-- src/constants.go | 2 +- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd0515a..4047be3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ CHANGELOG ========= +0.12.2 +------ + +- 256-color capability detection does not require `256` in `$TERM` +- Added `print-query` action +- More named keys for binding; F1 ~ F10, + ALT-/, ALT-space, and ALT-enter +- Added `jump` and `jump-accept` actions that implement [EasyMotion][em]-like + movement + ![][jump] + +[em]: https://github.com/easymotion/vim-easymotion +[jump]: https://cloud.githubusercontent.com/assets/700826/15367574/b3999dc4-1d64-11e6-85da-28ceeb1a9bc2.png + 0.12.1 ------ diff --git a/install b/install index 32cc19d..76352a3 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.12.1 pre=1 || - version=0.12.1 pre=0 +[[ "$@" =~ --pre ]] && version=0.12.2 pre=1 || + version=0.12.2 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 53ac3fe..970b17e 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Apr 2016" "fzf 0.12.1" "fzf - a command-line fuzzy finder" +.TH fzf 1 "May 2016" "fzf 0.12.2" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -183,6 +183,9 @@ on the center of the screen. .B "--inline-info" Display finder info inline with the query .TP +.BI "--jump-labels=" "CHARS" +Label characters for \fBjump\fR and \fBjump-accept\fR +.TP .BI "--prompt=" "STR" Input prompt (default: '> ') .TP @@ -200,11 +203,14 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR .B AVAILABLE KEYS: (SYNONYMS) \fIctrl-[a-z]\fR \fIalt-[a-z]\fR - \fIf[1-4]\fR + \fIf[1-10]\fR \fIenter\fR (\fIreturn\fR \fIctrl-m\fR) \fIspace\fR \fIbspace\fR (\fIbs\fR) + \fIalt-enter\fR + \fIalt-space\fR \fIalt-bspace\fR (\fIalt-bs\fR) + \fIalt-/\fR \fItab\fR \fIbtab\fR (\fIshift-tab\fR) \fIesc\fR @@ -244,12 +250,15 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBforward-char\fR \fIctrl-f right\fR \fBforward-word\fR \fIalt-f shift-right\fR \fBignore\fR + \fBjump\fR (EasyMotion-like 2-keystroke movement) + \fBjump-accept\fR (jump and accept) \fBkill-line\fR \fBkill-word\fR \fIalt-d\fR \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) \fBpage-down\fR \fIpgdn\fR \fBpage-up\fR \fIpgup\fR \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR) + \fBprint-query\fR (print query and exit) \fBselect-all\fR \fBtoggle\fR \fBtoggle-all\fR diff --git a/src/constants.go b/src/constants.go index c60c958..2848c82 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.12.1" + version = "0.12.2" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From bdb94fba7db3d0b59954b474439c28fb1f3fa8d8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 26 May 2016 00:52:06 +0900 Subject: [PATCH 175/658] [zsh] Fix #579 - Locally unset globsubst --- shell/completion.zsh | 8 ++------ shell/key-bindings.zsh | 1 + 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 74d1ad3..0094fa7 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -31,7 +31,7 @@ fi ########################################################### __fzf_generic_path_completion() { - local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches nnm + local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches # (Q) flag removes a quoting level: "foo\ bar" => "foo bar" base=${(Q)1} lbuf=$2 @@ -41,10 +41,7 @@ __fzf_generic_path_completion() { tail=$6 [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" - if ! setopt | \grep nonomatch > /dev/null; then - nnm=1 - setopt nonomatch - fi + setopt localoptions nonomatch dir="$base" while [ 1 ]; do if [ -z "$dir" -o -d ${~dir} ]; then @@ -66,7 +63,6 @@ __fzf_generic_path_completion() { dir=$(dirname "$dir") dir=${dir%/}/ done - [ -n "$nnm" ] && unsetopt nonomatch } _fzf_path_completion() { diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index f60bbae..a173b12 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -38,6 +38,7 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { local selected num + setopt localoptions noglobsubst selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r ${=FZF_CTRL_R_OPTS} -q "${LBUFFER//$/\\$}") ) if [ -n "$selected" ]; then num=$selected[1] From e0b29e437be458066fca4dab39b282dfc11466f6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 29 May 2016 02:11:50 +0900 Subject: [PATCH 176/658] [bash] Use backticks to avoid delay with blink-matching-paren Close #580 --- shell/key-bindings.bash | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 5ff20bb..41e707e 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -67,7 +67,7 @@ __fzf_use_tmux__() { [ $BASH_VERSINFO -gt 3 ] && __use_bind_x=1 || __use_bind_x=0 __fzf_use_tmux__ && __use_tmux=1 || __use_tmux=0 -if [[ $'\n'$(set -o) != *$'\n'vi*on* ]]; then +if [[ ! -o vi ]]; then # Required to refresh the prompt after fzf bind '"\er": redraw-current-line' bind '"\e^": history-expand-line' @@ -82,10 +82,10 @@ if [[ $'\n'$(set -o) != *$'\n'vi*on* ]]; then fi # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": " \C-e\C-u$(__fzf_history__)\e\C-e\e^\er"' + bind '"\C-r": " \C-e\C-u`__fzf_history__`\e\C-e\e^\er"' # ALT-C - cd into the selected directory - bind '"\ec": " \C-e\C-u$(__fzf_cd__)\e\C-e\er\C-m"' + bind '"\ec": " \C-e\C-u`__fzf_cd__`\e\C-e\er\C-m"' else # We'd usually use "\e" to enter vi-movement-mode so we can do our magic, # but this incurs a very noticeable delay of a half second or so, From f074709fc96291f9a7d41d1fda9d9193c92b57e0 Mon Sep 17 00:00:00 2001 From: Andrey Kashin Date: Tue, 31 May 2016 22:17:07 +0300 Subject: [PATCH 177/658] Change tmux pane title for fzf splits --- bin/fzf-tmux | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 4de93e2..67d1de3 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -144,14 +144,14 @@ if [ -n "$term" -o -t 0 ]; then cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ - split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \ + split-window $opt "printf '\033]2;fzf\033\\'; cd $(printf %q "$PWD");$envs bash $argsf" $swap \ > /dev/null 2>&1 else mkfifo $fifo1 cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ - split-window $opt "$envs bash $argsf" $swap \ + split-window $opt "printf '\033]2;fzf\033\\'; $envs bash $argsf" $swap \ > /dev/null 2>&1 cat <&0 > $fifo1 & fi From b8a3ba16a256e6c83abc709828e65368163e3b9e Mon Sep 17 00:00:00 2001 From: Jim Howell Date: Wed, 1 Jun 2016 18:06:51 -0500 Subject: [PATCH 178/658] [bash/zsh] Take SSH completion hints from known_hosts Signed-off-by: Junegunn Choi --- shell/completion.bash | 1 + shell/completion.zsh | 1 + 2 files changed, 2 insertions(+) diff --git a/shell/completion.bash b/shell/completion.bash index 66fac61..a0040ce 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -221,6 +221,7 @@ _fzf_complete_telnet() { _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \ + <(\grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u ) diff --git a/shell/completion.zsh b/shell/completion.zsh index 0094fa7..dca77cd 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -110,6 +110,7 @@ _fzf_complete_telnet() { _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \ + <(\grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u ) From 923feb69ab80c63c49de114c6bcc5f8c48ef16d4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 2 Jun 2016 22:01:26 +0900 Subject: [PATCH 179/658] [zsh] Fix indentation --- shell/completion.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index dca77cd..cbec114 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -110,7 +110,7 @@ _fzf_complete_telnet() { _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \ - <(\grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ + <(\grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u ) From 412c2116556204d64d1f40afeb995cae1c5bfdcb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 2 Jun 2016 22:24:47 +0900 Subject: [PATCH 180/658] [vim] Use lcd instead of chdir https://github.com/junegunn/fzf.vim/issues/147 --- plugin/fzf.vim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index d6a79f3..16220bc 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -193,7 +193,7 @@ function! s:pushd(dict) return 1 endif let a:dict.prev_dir = cwd - execute 'chdir' s:escape(a:dict.dir) + execute 'lcd' s:escape(a:dict.dir) let a:dict.dir = getcwd() return 1 endif @@ -214,7 +214,7 @@ function! s:popd(dict, lines) " directory is not expected and should be undone. if has_key(a:dict, 'prev_dir') && \ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0]))) - execute 'chdir' s:escape(remove(a:dict, 'prev_dir')) + execute 'lcd' s:escape(remove(a:dict, 'prev_dir')) endif endfunction From cfc0b18eaa2914717d143cdb110af784283a1462 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 3 Jun 2016 12:02:21 +0900 Subject: [PATCH 181/658] Revert "Change tmux pane title for fzf splits" This reverts commit f074709fc96291f9a7d41d1fda9d9193c92b57e0. Close #586. /cc @akashin --- bin/fzf-tmux | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 67d1de3..4de93e2 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -144,14 +144,14 @@ if [ -n "$term" -o -t 0 ]; then cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ - split-window $opt "printf '\033]2;fzf\033\\'; cd $(printf %q "$PWD");$envs bash $argsf" $swap \ + split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \ > /dev/null 2>&1 else mkfifo $fifo1 cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ - split-window $opt "printf '\033]2;fzf\033\\'; $envs bash $argsf" $swap \ + split-window $opt "$envs bash $argsf" $swap \ > /dev/null 2>&1 cat <&0 > $fifo1 & fi From 1c86aaf342120700c169683dd538a6d15b184369 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 3 Jun 2016 12:09:31 +0900 Subject: [PATCH 182/658] [vim/fzf-tmux] Handle fzf project directory with spaces Close #583 --- bin/fzf-tmux | 4 ++-- plugin/fzf.vim | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 4de93e2..932b3c0 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -141,14 +141,14 @@ for arg in "${args[@]}"; do done if [ -n "$term" -o -t 0 ]; then - cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf + cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \ > /dev/null 2>&1 else mkfifo $fifo1 - cat <<< "$fzf $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf + cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ split-window $opt "$envs bash $argsf" $swap \ diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 16220bc..9361181 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -49,7 +49,7 @@ function! s:fzf_exec() throw 'fzf executable not found' endif endif - return s:exec + return s:shellesc(s:exec) endfunction function! s:tmux_enabled() @@ -179,7 +179,7 @@ function! s:fzf_tmux(dict) endif endfor return printf('LINES=%d COLUMNS=%d %s %s %s --', - \ &lines, &columns, s:fzf_tmux, size, (has_key(a:dict, 'source') ? '' : '-')) + \ &lines, &columns, s:shellesc(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-')) endfunction function! s:splittable(dict) From 56fb2f00b3d3b6eed606d58411b5e6495ec99afc Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 8 Jun 2016 00:54:21 +0900 Subject: [PATCH 183/658] Use single-quoted strings in execute action Close #590 --- CHANGELOG.md | 5 +++++ src/terminal.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4047be3..558da3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +0.12.3 +------ +- `{}` in execute action is replaced to the single-quoted string of the + current line + 0.12.2 ------ diff --git a/src/terminal.go b/src/terminal.go index c440292..b1455e8 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -743,7 +743,7 @@ func keyMatch(key int, event C.Event) bool { } func quoteEntry(entry string) string { - return fmt.Sprintf("%q", entry) + return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" } func executeCommand(template string, replacement string) { From 6a431cbf49799327d763681758943be9e313b034 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 8 Jun 2016 01:27:22 +0900 Subject: [PATCH 184/658] [fzf-tmux] Escape $ in arguments e.g. fzf-tmux -q '$PATH' Related: #343 --- bin/fzf-tmux | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 932b3c0..4ab899a 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -137,6 +137,7 @@ for arg in "${args[@]}"; do arg="${arg//\\/\\\\}" arg="${arg//\"/\\\"}" arg="${arg//\`/\\\`}" + arg="${arg//$/\\$}" opts="$opts \"$arg\"" done From 95b34de339822d6e81898c7e52f94a7165be39bc Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 8 Jun 2016 01:30:26 +0900 Subject: [PATCH 185/658] [bash/zsh] Fix $FZF_CTRL_R_OPTS with option values with spaces --- shell/key-bindings.bash | 2 +- shell/key-bindings.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 41e707e..b87deca 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -51,7 +51,7 @@ __fzf_history__() ( shopt -u nocaseglob nocasematch line=$( HISTTIMEFORMAT= history | - $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS | + eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" | \grep '^ *[0-9]') && if [[ $- =~ H ]]; then sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index a173b12..f77e7ec 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -39,7 +39,7 @@ bindkey '\ec' fzf-cd-widget fzf-history-widget() { local selected num setopt localoptions noglobsubst - selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r ${=FZF_CTRL_R_OPTS} -q "${LBUFFER//$/\\$}") ) + selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") ) if [ -n "$selected" ]; then num=$selected[1] if [ -n "$num" ]; then From 3a644b16a42d26134d986e58675e74c99de8c659 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 8 Jun 2016 02:04:40 +0900 Subject: [PATCH 186/658] Update test_execute (#590) --- test/test_go.rb | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index d886c32..ef7aa3a 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -869,31 +869,34 @@ class TestGoFZF < TestBase def test_execute output = '/tmp/fzf-test-execute' - opts = %[--bind \\"alt-a:execute(echo '[{}]' >> #{output}),alt-b:execute[echo '({}), ({})' >> #{output}],C:execute:echo '({}), [{}], @{}@' >> #{output}\\"] + opts = %[--bind \\"alt-a:execute(echo [{}] >> #{output}),alt-b:execute[echo /{}{}/ >> #{output}],C:execute:echo /{}{}{}/ >> #{output}\\"] wait = lambda { |exp| tmux.until { |lines| lines[-2].include? exp } } - tmux.send_keys "seq 100 | #{fzf opts}; sync", :Enter - wait['100/100'] + writelines tempname, %w[foo'bar foo"bar foo$bar] + tmux.send_keys "cat #{tempname} | #{fzf opts}; sync", :Enter + wait['3/3'] tmux.send_keys :Escape, :a - wait['/100'] + wait['/3'] tmux.send_keys :Escape, :a - wait['/100'] + wait['/3'] tmux.send_keys :Up tmux.send_keys :Escape, :b - wait['/100'] + wait['/3'] tmux.send_keys :Escape, :b - wait['/100'] + wait['/3'] tmux.send_keys :Up tmux.send_keys :C - wait['100/100'] - tmux.send_keys 'foobar' - wait['0/100'] + wait['3/3'] + tmux.send_keys 'barfoo' + wait['0/3'] tmux.send_keys :Escape, :a - wait['/100'] + wait['/3'] tmux.send_keys :Escape, :b - wait['/100'] + wait['/3'] tmux.send_keys :Enter readonce - assert_equal ['["1"]', '["1"]', '("2"), ("2")', '("2"), ("2")', '("3"), ["3"], @"3"@'], + assert_equal %w[[foo'bar] [foo'bar] + /foo"barfoo"bar/ /foo"barfoo"bar/ + /foo$barfoo$barfoo$bar/], File.readlines(output).map(&:chomp) ensure File.unlink output rescue nil From 68b60c6d19d84cf4ffba890c7491c7a0e5892999 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 8 Jun 2016 02:15:12 +0900 Subject: [PATCH 187/658] Update test_execute_multi (#590) --- test/test_go.rb | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index ef7aa3a..3888abf 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -904,21 +904,24 @@ class TestGoFZF < TestBase def test_execute_multi output = '/tmp/fzf-test-execute-multi' - opts = %[--multi --bind \\"alt-a:execute-multi(echo '[{}], @{}@' >> #{output}; sync)\\"] - tmux.send_keys "seq 100 | #{fzf opts}", :Enter - tmux.until { |lines| lines[-2].include? '100/100' } + opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{} >> #{output}; sync)\\"] + writelines tempname, %w[foo'bar foo"bar foo$bar foobar] + tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter + tmux.until { |lines| lines[-2].include? '4/4' } tmux.send_keys :Escape, :a - tmux.until { |lines| lines[-2].include? '/100' } + tmux.until { |lines| lines[-2].include? '/4' } tmux.send_keys :BTab, :BTab, :BTab tmux.send_keys :Escape, :a - tmux.until { |lines| lines[-2].include? '/100' } + tmux.until { |lines| lines[-2].include? '/4' } tmux.send_keys :Tab, :Tab tmux.send_keys :Escape, :a - tmux.until { |lines| lines[-2].include? '/100' } + tmux.until { |lines| lines[-2].include? '/4' } tmux.send_keys :Enter tmux.prepare readonce - assert_equal ['["1"], @"1"@', '["1" "2" "3"], @"1" "2" "3"@', '["1" "2" "4"], @"1" "2" "4"@'], + assert_equal [%[foo'bar/foo'bar], + %[foo'bar foo"bar foo$bar/foo'bar foo"bar foo$bar], + %[foo'bar foo"bar foobar/foo'bar foo"bar foobar]], File.readlines(output).map(&:chomp) ensure File.unlink output rescue nil From cf9c957c665b6ca4f03a796826ca6d1a1f7e850a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 8 Jun 2016 02:16:07 +0900 Subject: [PATCH 188/658] Update test_execute_shell (#590) --- test/test_go.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index 3888abf..bb35bbf 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -940,7 +940,7 @@ class TestGoFZF < TestBase tmux.until { |lines| lines[-2].include? '1/1' } tmux.send_keys 'C-c' tmux.prepare - assert_equal ['-c / "foo"bar'], File.readlines(output).map(&:chomp) + assert_equal ["-c / 'foo'bar"], File.readlines(output).map(&:chomp) ensure File.unlink output rescue nil end From fe5db5aadc6d3a7e1db3945d196294b0a77c816b Mon Sep 17 00:00:00 2001 From: Edgar Hipp Date: Fri, 10 Jun 2016 09:05:05 +0200 Subject: [PATCH 189/658] Add fzf_prefer_tmux option --- plugin/fzf.vim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 9361181..ba5fb25 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -140,10 +140,10 @@ try else let prefix = '' endif - let tmux = !has('nvim') && s:tmux_enabled() && s:splittable(dict) + let tmux = (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict) let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result - if has('nvim') + if has('nvim') && !tmux return s:execute_term(dict, command, temps) endif From b8737b724bf5e7df15bb18fd1a33e57b4e4fe1ab Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 11 Jun 2016 12:14:34 +0900 Subject: [PATCH 190/658] Ignore controls chars for bracketed paste mode Close #594 --- src/curses/curses.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/curses/curses.go b/src/curses/curses.go index a0240da..ca10fc7 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -486,6 +486,11 @@ func escSequence(sz *int) Event { return Event{F10, 0, nil} } } + // Bracketed paste mode \e[200~ / \e[201 + if _buf[3] == 48 && (_buf[4] == 48 || _buf[4] == 49) && _buf[5] == 126 { + *sz = 6 + return Event{Invalid, 0, nil} + } return Event{Invalid, 0, nil} // INS case 51: return Event{Del, 0, nil} From 2bbc12063c475cb7c78c7633e2ffad41aa49d233 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 11 Jun 2016 19:59:12 +0900 Subject: [PATCH 191/658] Add --preview and --preview-window Close #587 --- src/curses/curses.go | 100 ++++++++++++--- src/options.go | 172 +++++++++++++++++++------ src/terminal.go | 295 +++++++++++++++++++++++++++++++++---------- test/test_go.rb | 22 ++++ 4 files changed, 467 insertions(+), 122 deletions(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index ca10fc7..7a9ccd4 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -113,7 +113,8 @@ const ( ColCursor ColSelected ColHeader - ColUser + ColBorder + ColUser // Should be the last entry ) const ( @@ -136,6 +137,7 @@ type ColorTheme struct { Cursor int16 Selected int16 Header int16 + Border int16 } type Event struct { @@ -170,6 +172,31 @@ var ( DarkBG int ) +type Window struct { + win *C.WINDOW + Top int + Left int + Width int + Height int +} + +func NewWindow(top int, left int, width int, height int, border bool) *Window { + win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left)) + if border { + attr := _color(ColBorder, false) + C.wattron(win, attr) + C.box(win, 0, 0) + C.wattroff(win, attr) + } + return &Window{ + win: win, + Top: top, + Left: left, + Width: width, + Height: height, + } +} + func EmptyTheme() *ColorTheme { return &ColorTheme{ UseDefault: true, @@ -184,7 +211,8 @@ func EmptyTheme() *ColorTheme { Info: colUndefined, Cursor: colUndefined, Selected: colUndefined, - Header: colUndefined} + Header: colUndefined, + Border: colUndefined} } func init() { @@ -204,7 +232,8 @@ func init() { Info: C.COLOR_WHITE, Cursor: C.COLOR_RED, Selected: C.COLOR_MAGENTA, - Header: C.COLOR_CYAN} + Header: C.COLOR_CYAN, + Border: C.COLOR_BLACK} Dark256 = &ColorTheme{ UseDefault: true, Fg: 15, @@ -218,7 +247,8 @@ func init() { Info: 144, Cursor: 161, Selected: 168, - Header: 109} + Header: 109, + Border: 59} Light256 = &ColorTheme{ UseDefault: true, Fg: 15, @@ -232,7 +262,8 @@ func init() { Info: 101, Cursor: 161, Selected: 168, - Header: 31} + Header: 31, + Border: 145} } func attrColored(pair int, bold bool) C.int { @@ -360,6 +391,7 @@ func initPairs(baseTheme *ColorTheme, theme *ColorTheme, black bool) { C.init_pair(ColCursor, override(baseTheme.Cursor, theme.Cursor), darkBG) C.init_pair(ColSelected, override(baseTheme.Selected, theme.Selected), darkBG) C.init_pair(ColHeader, override(baseTheme.Header, theme.Header), bg) + C.init_pair(ColBorder, override(baseTheme.Border, theme.Border), bg) } func Close() { @@ -415,7 +447,9 @@ func mouseSequence(sz *int) Event { 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl mod := _buf[3] >= 100 s := 1 - int(_buf[3]%2)*2 - return Event{Mouse, 0, &MouseEvent{0, 0, s, false, false, mod}} + x := int(_buf[4] - 33) + y := int(_buf[5] - 33) + return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}} } return Event{Invalid, 0, nil} } @@ -588,17 +622,25 @@ func GetChar() Event { return Event{Rune, r, nil} } -func Move(y int, x int) { - C.move(C.int(y), C.int(x)) +func (w *Window) Close() { + C.delwin(w.win) } -func MoveAndClear(y int, x int) { - Move(y, x) - C.clrtoeol() +func (w *Window) Enclose(y int, x int) bool { + return bool(C.wenclose(w.win, C.int(y), C.int(x))) } -func Print(text string) { - C.addstr(C.CString(strings.Map(func(r rune) rune { +func (w *Window) Move(y int, x int) { + C.wmove(w.win, C.int(y), C.int(x)) +} + +func (w *Window) MoveAndClear(y int, x int) { + w.Move(y, x) + C.wclrtoeol(w.win) +} + +func (w *Window) Print(text string) { + C.waddstr(w.win, C.CString(strings.Map(func(r rune) rune { if r < 32 { return -1 } @@ -606,11 +648,11 @@ func Print(text string) { }, text))) } -func CPrint(pair int, bold bool, text string) { +func (w *Window) CPrint(pair int, bold bool, text string) { attr := _color(pair, bold) - C.attron(attr) - Print(text) - C.attroff(attr) + C.wattron(w.win, attr) + w.Print(text) + C.wattroff(w.win, attr) } func Clear() { @@ -625,6 +667,30 @@ func Refresh() { C.refresh() } +func (w *Window) Erase() { + C.werase(w.win) +} + +func (w *Window) Fill(str string) bool { + return C.waddstr(w.win, C.CString(str)) == C.OK +} + +func (w *Window) CFill(str string, fg int, bg int, bold bool) bool { + attr := _color(PairFor(fg, bg), bold) + C.wattron(w.win, attr) + ret := w.Fill(str) + C.wattroff(w.win, attr) + return ret +} + +func (w *Window) Refresh() { + C.wnoutrefresh(w.win) +} + +func DoUpdate() { + C.doupdate() +} + func PairFor(fg int, bg int) int { key := (fg << 8) + bg if found, prs := _colorMap[key]; prs { diff --git a/src/options.go b/src/options.go index ffd57e9..2a72427 100644 --- a/src/options.go +++ b/src/options.go @@ -1,6 +1,7 @@ package fzf import ( + "fmt" "os" "regexp" "strconv" @@ -23,36 +24,47 @@ const usage = `usage: fzf [options] -n, --nth=N[,..] Comma-separated list of field index expressions for limiting search scope. Each can be a non-zero integer or a range expression ([BEGIN]..[END]). - --with-nth=N[,..] Transform item using index expressions within finder - -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) + --with-nth=N[,..] Transform the presentation of each line using + field index expressions + -d, --delimiter=STR Field delimiter regex (default: AWK-style) +s, --no-sort Do not sort the result --tac Reverse the order of the input --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply - when the scores are tied; - [length|begin|end|index] (default: length) + when the scores are tied [length|begin|end|index] + (default: length) Interface -m, --multi Enable multi-select with tab/shift-tab - --ansi Enable processing of ANSI color codes --no-mouse Disable mouse - --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors - --black Use black background - --reverse Reverse orientation - --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) - --tabstop=SPACES Number of spaces for a tab character (default: 8) + --bind=KEYBINDS Custom key bindings. Refer to the man page. --cycle Enable cyclic scroll --no-hscroll Disable horizontal scroll --hscroll-off=COL Number of screen columns to keep to the right of the highlighted substring (default: 10) - --inline-info Display finder info inline with the query --jump-labels=CHARS Label characters for jump and jump-accept + + Layout + --reverse Reverse orientation + --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) + --inline-info Display finder info inline with the query --prompt=STR Input prompt (default: '> ') - --bind=KEYBINDS Custom key bindings. Refer to the man page. - --history=FILE History file - --history-size=N Maximum number of history entries (default: 1000) --header=STR String to print as header --header-lines=N The first N lines of the input are treated as header + Display + --ansi Enable processing of ANSI color codes + --tabstop=SPACES Number of spaces for a tab character (default: 8) + --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors + + History + --history=FILE History file + --history-size=N Maximum number of history entries (default: 1000) + + Preview + --preview=COMMAND Command to preview highlighted line ({}) + --preview-window=OPT Preview window layout (default: right:50%) + [up|down|left|right][:SIZE[%]][:hidden] + Scripting -q, --query=STR Start the finder with the given query -1, --select-1 Automatically select the only match @@ -88,8 +100,29 @@ const ( byEnd ) -func defaultMargin() [4]string { - return [4]string{"0", "0", "0", "0"} +type sizeSpec struct { + size float64 + percent bool +} + +func defaultMargin() [4]sizeSpec { + return [4]sizeSpec{} +} + +type windowPosition int + +const ( + posUp windowPosition = iota + posDown + posLeft + posRight +) + +type previewOpts struct { + command string + position windowPosition + size sizeSpec + hidden bool } // Options stores the values of command-line options @@ -123,13 +156,14 @@ type Options struct { Expect map[int]string Keymap map[int]actionType Execmap map[int]string + Preview previewOpts PrintQuery bool ReadZero bool Sync bool History *History Header []string HeaderLines int - Margin [4]string + Margin [4]sizeSpec Tabstop int Version bool } @@ -165,6 +199,7 @@ func defaultOptions() *Options { Expect: make(map[int]string), Keymap: make(map[int]actionType), Execmap: make(map[int]string), + Preview: previewOpts{"", posRight, sizeSpec{50, true}, false}, PrintQuery: false, ReadZero: false, Sync: false, @@ -458,6 +493,8 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme theme.Match = ansi case "hl+": theme.CurrentMatch = ansi + case "border": + theme.Border = ansi case "prompt": theme.Prompt = ansi case "spinner": @@ -604,6 +641,8 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) keymap[key] = actPreviousHistory case "next-history": keymap[key] = actNextHistory + case "toggle-preview": + keymap[key] = actTogglePreview case "toggle-sort": keymap[key] = actToggleSort default: @@ -659,40 +698,86 @@ func strLines(str string) []string { return strings.Split(strings.TrimSuffix(str, "\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") - } +func parseSize(str string, maxPercent float64, label string) sizeSpec { + var val float64 + percent := strings.HasSuffix(str, "%") + if percent { + val = atof(str[:len(str)-1]) + if val < 0 { + errorExit(label + " must be non-negative") } - return str + if val > maxPercent { + errorExit(fmt.Sprintf("%s too large (max: %d%%)", label, int(maxPercent))) + } + } else { + if strings.Contains(str, ".") { + errorExit(label + " (without %) must be a non-negative integer") + } + + val = float64(atoi(str)) + if val < 0 { + errorExit(label + " must be non-negative") + } + } + return sizeSpec{val, percent} +} + +func parsePreviewWindow(opts *previewOpts, input string) { + layout := input + if strings.HasSuffix(layout, ":hidden") { + opts.hidden = true + layout = strings.TrimSuffix(layout, ":hidden") + } + + tokens := strings.Split(layout, ":") + if len(tokens) == 0 || len(tokens) > 2 { + errorExit("invalid window layout: " + input) + } + + if len(tokens) > 1 { + opts.size = parseSize(tokens[1], 99, "window size") + } else { + opts.size = sizeSpec{50, true} + } + if !opts.size.percent && opts.size.size > 0 { + // Adjust size for border + opts.size.size += 2 + } + + switch tokens[0] { + case "up": + opts.position = posUp + case "down": + opts.position = posDown + case "left": + opts.position = posLeft + case "right": + opts.position = posRight + default: + errorExit("invalid window position: " + input) + } +} + +func parseMargin(margin string) [4]sizeSpec { + margins := strings.Split(margin, ",") + checked := func(str string) sizeSpec { + return parseSize(str, 49, "margin") } switch len(margins) { case 1: m := checked(margins[0]) - return [4]string{m, m, m, m} + return [4]sizeSpec{m, m, m, m} case 2: tb := checked(margins[0]) rl := checked(margins[1]) - return [4]string{tb, rl, tb, rl} + return [4]sizeSpec{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} + return [4]sizeSpec{t, rl, b, rl} case 4: - return [4]string{ + return [4]sizeSpec{ checked(margins[0]), checked(margins[1]), checked(margins[2]), checked(margins[3])} default: @@ -858,6 +943,13 @@ func parseOptions(opts *Options, allArgs []string) { case "--header-lines": opts.HeaderLines = atoi( nextString(allArgs, &i, "number of header lines required")) + case "--preview": + opts.Preview.command = nextString(allArgs, &i, "preview command required") + case "--no-preview": + opts.Preview.command = "" + case "--preview-window": + parsePreviewWindow(&opts.Preview, + nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]]")) case "--no-margin": opts.Margin = defaultMargin() case "--margin": @@ -900,6 +992,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Header = strLines(value) } else if match, value := optString(arg, "--header-lines="); match { opts.HeaderLines = atoi(value) + } else if match, value := optString(arg, "--preview="); match { + opts.Preview.command = value + } else if match, value := optString(arg, "--preview-window="); match { + parsePreviewWindow(&opts.Preview, value) } else if match, value := optString(arg, "--margin="); match { opts.Margin = parseMargin(value) } else if match, value := optString(arg, "--tabstop="); match { diff --git a/src/terminal.go b/src/terminal.go index b1455e8..ce10ee7 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -7,7 +7,6 @@ import ( "os/signal" "regexp" "sort" - "strconv" "strings" "sync" "syscall" @@ -53,8 +52,10 @@ type Terminal struct { header []string header0 []string ansi bool - margin [4]string - marginInt [4]int + margin [4]sizeSpec + window *C.Window + bwindow *C.Window + pwindow *C.Window count int progress int reading bool @@ -63,6 +64,10 @@ type Terminal struct { merger *Merger selected map[int32]selectedItem reqBox *util.EventBox + preview previewOpts + previewing bool + previewTxt string + previewBox *util.EventBox eventBox *util.EventBox mutex sync.Mutex initFunc func() @@ -103,6 +108,8 @@ const ( reqRedraw reqClose reqPrintQuery + reqPreviewEnqueue + reqPreviewDisplay reqQuit ) @@ -148,6 +155,7 @@ const ( actJumpAccept actPrintQuery actToggleSort + actTogglePreview actPreviousHistory actNextHistory actExecute @@ -220,6 +228,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { } else { delay = initialDelay } + var previewBox *util.EventBox + if len(opts.Preview.command) > 0 { + previewBox = util.NewEventBox() + } return &Terminal{ initDelay: delay, inlineInfo: opts.InlineInfo, @@ -242,7 +254,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { printQuery: opts.PrintQuery, history: opts.History, margin: opts.Margin, - marginInt: [4]int{0, 0, 0, 0}, cycle: opts.Cycle, header: header, header0: header, @@ -253,6 +264,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { merger: EmptyMerger, selected: make(map[int32]selectedItem), reqBox: util.NewEventBox(), + preview: opts.Preview, + previewing: previewBox != nil && !opts.Preview.hidden, + previewTxt: "", + previewBox: previewBox, eventBox: eventBox, mutex: sync.Mutex{}, suppress: true, @@ -332,7 +347,7 @@ func (t *Terminal) output() bool { if !found { cnt := t.merger.Length() if cnt > 0 && cnt > t.cy { - fmt.Println(t.merger.Get(t.cy).AsString(t.ansi)) + fmt.Println(t.current()) found = true } } else { @@ -372,56 +387,113 @@ func displayWidth(runes []rune) int { return l } -const minWidth = 16 -const minHeight = 4 +const ( + minWidth = 16 + minHeight = 4 +) -func (t *Terminal) calculateMargins() { +func calculateSize(base int, size sizeSpec, margin int, minSize int) int { + max := base - margin + if size.percent { + return util.Constrain(int(float64(base)*0.01*size.size), minSize, max) + } + return util.Constrain(int(size.size), minSize, max) +} + +func (t *Terminal) resizeWindows() { 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 + marginInt := [4]int{} + for idx, sizeSpec := range t.margin { + if sizeSpec.percent { + var max float64 if idx%2 == 0 { - val = float64(screenHeight) + max = float64(screenHeight) } else { - val = float64(screenWidth) + max = float64(screenWidth) } - t.marginInt[idx] = int(val * num * 0.01) + marginInt[idx] = int(max * sizeSpec.size * 0.01) } else { - num, _ := strconv.Atoi(str) - t.marginInt[idx] = num + marginInt[idx] = int(sizeSpec.size) } } adjust := func(idx1 int, idx2 int, max int, min int) { if max >= min { - margin := t.marginInt[idx1] + t.marginInt[idx2] + margin := marginInt[idx1] + 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 + marginInt[idx1] = desired * marginInt[idx1] / margin + marginInt[idx2] = desired * marginInt[idx2] / margin } } } - adjust(1, 3, screenWidth, minWidth) - adjust(0, 2, screenHeight, minHeight) + minAreaWidth := minWidth + minAreaHeight := minHeight + if t.isPreviewEnabled() { + switch t.preview.position { + case posUp, posDown: + minAreaHeight *= 2 + case posLeft, posRight: + minAreaWidth *= 2 + } + } + adjust(1, 3, screenWidth, minAreaWidth) + adjust(0, 2, screenHeight, minAreaHeight) + if t.window != nil { + t.window.Close() + } + if t.bwindow != nil { + t.bwindow.Close() + t.pwindow.Close() + } + + width := screenWidth - marginInt[1] - marginInt[3] + height := screenHeight - marginInt[0] - marginInt[2] + if t.isPreviewEnabled() { + createPreviewWindow := func(y int, x int, w int, h int) { + t.bwindow = C.NewWindow(y, x, w, h, true) + t.pwindow = C.NewWindow(y+1, x+2, w-4, h-2, false) + } + switch t.preview.position { + case posUp: + pheight := calculateSize(height, t.preview.size, minHeight, 3) + t.window = C.NewWindow( + marginInt[0]+pheight, marginInt[3], width, height-pheight, false) + createPreviewWindow(marginInt[0], marginInt[3], width, pheight) + case posDown: + pheight := calculateSize(height, t.preview.size, minHeight, 3) + t.window = C.NewWindow( + marginInt[0], marginInt[3], width, height-pheight, false) + createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) + case posLeft: + pwidth := calculateSize(width, t.preview.size, minWidth, 5) + t.window = C.NewWindow( + marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false) + createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) + case posRight: + pwidth := calculateSize(width, t.preview.size, minWidth, 5) + t.window = C.NewWindow( + marginInt[0], marginInt[3], width-pwidth, height, false) + createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) + } + } else { + t.window = C.NewWindow( + marginInt[0], + marginInt[3], + width, + height, false) + } } func (t *Terminal) move(y int, x int, clear bool) { - x += t.marginInt[3] - maxy := C.MaxY() if !t.reverse { - y = maxy - y - 1 - t.marginInt[2] - } else { - y += t.marginInt[0] + y = t.window.Height - y - 1 } if clear { - C.MoveAndClear(y, x) + t.window.MoveAndClear(y, x) } else { - C.Move(y, x) + t.window.Move(y, x) } } @@ -431,24 +503,24 @@ func (t *Terminal) placeCursor() { func (t *Terminal) printPrompt() { t.move(0, 0, true) - C.CPrint(C.ColPrompt, true, t.prompt) - C.CPrint(C.ColNormal, true, string(t.input)) + t.window.CPrint(C.ColPrompt, true, t.prompt) + t.window.CPrint(C.ColNormal, true, string(t.input)) } func (t *Terminal) printInfo() { if t.inlineInfo { t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true) if t.reading { - C.CPrint(C.ColSpinner, true, " < ") + t.window.CPrint(C.ColSpinner, true, " < ") } else { - C.CPrint(C.ColPrompt, true, " < ") + t.window.CPrint(C.ColPrompt, true, " < ") } } else { t.move(1, 0, true) if t.reading { duration := int64(spinnerDuration) idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration - C.CPrint(C.ColSpinner, true, _spinner[idx]) + t.window.CPrint(C.ColSpinner, true, _spinner[idx]) } t.move(1, 2, false) } @@ -467,18 +539,14 @@ func (t *Terminal) printInfo() { if t.progress > 0 && t.progress < 100 { output += fmt.Sprintf(" (%d%%)", t.progress) } - C.CPrint(C.ColInfo, false, output) -} - -func (t *Terminal) maxHeight() int { - return C.MaxY() - t.marginInt[0] - t.marginInt[2] + t.window.CPrint(C.ColInfo, false, output) } func (t *Terminal) printHeader() { if len(t.header) == 0 { return } - max := t.maxHeight() + max := t.window.Height var state *ansiState for idx, lineStr := range t.header { line := idx + 2 @@ -529,19 +597,19 @@ func (t *Terminal) printItem(item *Item, i int, current bool) { } else if current { label = ">" } - C.CPrint(C.ColCursor, true, label) + t.window.CPrint(C.ColCursor, true, label) if current { if selected { - C.CPrint(C.ColSelected, true, ">") + t.window.CPrint(C.ColSelected, true, ">") } else { - C.CPrint(C.ColCurrent, true, " ") + t.window.CPrint(C.ColCurrent, true, " ") } t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true) } else { if selected { - C.CPrint(C.ColSelected, true, ">") + t.window.CPrint(C.ColSelected, true, ">") } else { - C.Print(" ") + t.window.Print(" ") } t.printHighlighted(item, false, 0, C.ColMatch, false) } @@ -593,7 +661,7 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c text := make([]rune, len(item.text)) copy(text, item.text) offsets := item.colorOffsets(col2, bold, current) - maxWidth := C.MaxX() - 3 - t.marginInt[1] - t.marginInt[3] + maxWidth := t.window.Width - 3 maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) fullWidth := displayWidth(text) if fullWidth > maxWidth { @@ -643,11 +711,11 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c e := util.Constrain32(offset.offset[1], index, maxOffset) substr, prefixWidth = processTabs(text[index:b], prefixWidth) - C.CPrint(col1, bold, substr) + t.window.CPrint(col1, bold, substr) if b < e { substr, prefixWidth = processTabs(text[b:e], prefixWidth) - C.CPrint(offset.color, offset.bold, substr) + t.window.CPrint(offset.color, offset.bold, substr) } index = e @@ -657,7 +725,29 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c } if index < maxOffset { substr, _ = processTabs(text[index:], prefixWidth) - C.CPrint(col1, bold, substr) + t.window.CPrint(col1, bold, substr) + } +} + +func (t *Terminal) printPreview() { + trimmed, ansiOffsets, _ := extractColor(t.previewTxt, nil) + var index int32 + t.pwindow.Erase() + for _, o := range ansiOffsets { + b := o.offset[0] + e := o.offset[1] + if b > index { + if !t.pwindow.Fill(trimmed[index:b]) { + return + } + } + if !t.pwindow.CFill(trimmed[b:e], o.color.fg, o.color.bg, o.color.bold) { + return + } + index = e + } + if int(index) < len(trimmed) { + t.pwindow.Fill(trimmed[index:]) } } @@ -677,16 +767,24 @@ func processTabs(runes []rune, prefixWidth int) (string, int) { } func (t *Terminal) printAll() { - t.calculateMargins() + t.resizeWindows() t.printList() t.printPrompt() t.printInfo() t.printHeader() + if t.isPreviewEnabled() { + t.printPreview() + } } func (t *Terminal) refresh() { if !t.suppress { - C.Refresh() + if t.isPreviewEnabled() { + t.bwindow.Refresh() + t.pwindow.Refresh() + } + t.window.Refresh() + C.DoUpdate() } } @@ -746,7 +844,7 @@ func quoteEntry(entry string) string { return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" } -func executeCommand(template string, replacement string) { +func (t *Terminal) executeCommand(template string, replacement string) { command := strings.Replace(template, "{}", replacement, -1) cmd := util.ExecCommand(command) cmd.Stdin = os.Stdin @@ -754,7 +852,19 @@ func executeCommand(template string, replacement string) { cmd.Stderr = os.Stderr C.Endwin() cmd.Run() - C.Refresh() + t.refresh() +} + +func (t *Terminal) hasPreviewWindow() bool { + return t.previewBox != nil +} + +func (t *Terminal) isPreviewEnabled() bool { + return t.previewBox != nil && t.previewing +} + +func (t *Terminal) current() string { + return t.merger.Get(t.cy).AsString(t.ansi) } // Loop is called to start Terminal I/O @@ -779,10 +889,10 @@ func (t *Terminal) Loop() { t.mutex.Lock() t.initFunc() - t.calculateMargins() + t.resizeWindows() t.printPrompt() t.placeCursor() - C.Refresh() + t.refresh() t.printInfo() t.printHeader() t.mutex.Unlock() @@ -807,6 +917,29 @@ func (t *Terminal) Loop() { }() } + if t.hasPreviewWindow() { + go func() { + for { + focused := "" + t.previewBox.Wait(func(events *util.Events) { + for req, value := range *events { + switch req { + case reqPreviewEnqueue: + focused = value.(string) + } + } + events.Clear() + }) + if len(focused) > 0 { + command := strings.Replace(t.preview.command, "{}", quoteEntry(focused), -1) + cmd := util.ExecCommand(command) + out, _ := cmd.CombinedOutput() + t.reqBox.Set(reqPreviewDisplay, string(out)) + } + } + }() + } + exit := func(code int) { if code <= exitNoMatch && t.history != nil { t.history.append(string(t.input)) @@ -815,11 +948,12 @@ func (t *Terminal) Loop() { } go func() { + focused := "" for { t.reqBox.Wait(func(events *util.Events) { defer events.Clear() t.mutex.Lock() - for req := range *events { + for req, value := range *events { switch req { case reqPrompt: t.printPrompt() @@ -830,6 +964,21 @@ func (t *Terminal) Loop() { t.printInfo() case reqList: t.printList() + cnt := t.merger.Length() + if cnt > 0 && cnt > t.cy { + currentFocus := t.current() + if currentFocus != focused { + focused = currentFocus + if t.isPreviewEnabled() { + t.previewBox.Set(reqPreviewEnqueue, focused) + } + } + } else { + if focused != "" && t.isPreviewEnabled() { + t.pwindow.Erase() + } + focused = "" + } case reqJump: if t.merger.Length() == 0 { t.jumping = jumpDisabled @@ -850,6 +999,9 @@ func (t *Terminal) Loop() { exit(exitOk) } exit(exitNoMatch) + case reqPreviewDisplay: + t.previewTxt = value.(string) + t.printPreview() case reqPrintQuery: C.Close() fmt.Println(string(t.input)) @@ -915,7 +1067,7 @@ func (t *Terminal) Loop() { case actExecute: if t.cy >= 0 && t.cy < t.merger.Length() { item := t.merger.Get(t.cy) - executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi))) + t.executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi))) } case actExecuteMulti: if len(t.selected) > 0 { @@ -923,13 +1075,23 @@ func (t *Terminal) Loop() { for i, sel := range t.sortSelected() { sels[i] = quoteEntry(*sel.text) } - executeCommand(t.execmap[mapkey], strings.Join(sels, " ")) + t.executeCommand(t.execmap[mapkey], strings.Join(sels, " ")) } else { return doAction(actExecute, mapkey) } case actInvalid: t.mutex.Unlock() return false + case actTogglePreview: + if t.hasPreviewWindow() { + t.previewing = !t.previewing + t.resizeWindows() + cnt := t.merger.Length() + if t.previewing && cnt > 0 && cnt > t.cy { + t.previewBox.Set(reqPreviewEnqueue, t.current()) + } + req(reqList, reqInfo) + } case actToggleSort: t.sort = !t.sort t.eventBox.Set(EvtSearchNew, t.sort) @@ -1097,20 +1259,19 @@ func (t *Terminal) Loop() { mx, my := me.X, me.Y if me.S != 0 { // Scroll - if t.merger.Length() > 0 { + if t.window.Enclose(my, mx) && t.merger.Length() > 0 { if t.multi && me.Mod { toggle() } t.vmove(me.S) req(reqList) } - } else if mx >= t.marginInt[3] && mx < C.MaxX()-t.marginInt[1] && - my >= t.marginInt[0] && my < C.MaxY()-t.marginInt[2] { - mx -= t.marginInt[3] - my -= t.marginInt[0] + } else if t.window.Enclose(my, mx) { + mx -= t.window.Left + my -= t.window.Top mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input)) if !t.reverse { - my = t.maxHeight() - my - 1 + my = t.window.Height - my - 1 } min := 2 + len(t.header) if t.inlineInfo { @@ -1217,7 +1378,7 @@ func (t *Terminal) vset(o int) bool { } func (t *Terminal) maxItems() int { - max := t.maxHeight() - 2 - len(t.header) + max := t.window.Height - 2 - len(t.header) if t.inlineInfo { max++ } diff --git a/test/test_go.rb b/test/test_go.rb index bb35bbf..a6d6719 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1228,6 +1228,28 @@ class TestGoFZF < TestBase assert_equal '3', readonce.chomp end + def test_preview + tmux.send_keys %[seq 1000 | #{FZF} --preview 'echo {{}-{}}' --bind ?:toggle-preview], :Enter + tmux.until { |lines| lines[1].include?(' {1-1}') } + tmux.send_keys '555' + tmux.until { |lines| lines[1].include?(' {555-555}') } + tmux.send_keys '?' + tmux.until { |lines| !lines[1].include?(' {555-555}') } + tmux.send_keys '?' + tmux.until { |lines| lines[1].include?(' {555-555}') } + end + + def test_preview_hidden + tmux.send_keys %[seq 1000 | #{FZF} --preview 'echo {{}-{}}' --preview-window down:1:hidden --bind ?:toggle-preview], :Enter + tmux.until { |lines| lines[-1] == '>' } + tmux.send_keys '?' + tmux.until { |lines| lines[-2].include?(' {1-1}') } + tmux.send_keys '555' + tmux.until { |lines| lines[-2].include?(' {555-555}') } + tmux.send_keys '?' + tmux.until { |lines| lines[-1] == '> 555' } + end + private def writelines path, lines File.unlink path while File.exists? path From 3e1d6a7bcf2a47a494d602389fda415d8fad8e03 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 12 Jun 2016 02:15:11 +0900 Subject: [PATCH 192/658] 0.13.0 --- CHANGELOG.md | 10 +- install | 4 +- man/man1/fzf.1 | 421 +++++++++++++++++++++++++---------------------- src/constants.go | 2 +- 4 files changed, 235 insertions(+), 202 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 558da3e..9844d71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ CHANGELOG ========= -0.12.3 +0.13.0 ------ -- `{}` in execute action is replaced to the single-quoted string of the - current line +- Added preview feature + - `--preview CMD` + - `--preview-window POS[:SIZE][:hidden]` +- `{}` in execute action is now replaced to the single-quoted (instead of + double-quoted) string of the current line +- Fixed to ignore control characters for bracketed paste mode 0.12.2 ------ diff --git a/install b/install index 76352a3..cb09f1d 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.12.2 pre=1 || - version=0.12.2 pre=0 +[[ "$@" =~ --pre ]] && version=0.13.0 pre=1 || + version=0.13.0 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 970b17e..d8bb925 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "May 2016" "fzf 0.12.2" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jun 2016" "fzf 0.13.0" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -50,10 +50,10 @@ Case-sensitive match .TP .BI "-n, --nth=" "N[,..]" Comma-separated list of field index expressions for limiting search scope. -See \fBFIELD INDEX EXPRESSION\fR for details. +See \fBFIELD INDEX EXPRESSION\fR for the details. .TP .BI "--with-nth=" "N[,..]" -Transform each item using index expressions within finder +Transform the presentation of each line using field index expressions .TP .BI "-d, --delimiter=" "STR" Field delimiter regex for \fB--nth\fR and \fB--with-nth\fR (default: AWK-style) @@ -64,6 +64,7 @@ Do not sort the result .TP .B "--tac" Reverse the order of the input + .RS e.g. \fBhistory | fzf --tac --no-sort\fR .RE @@ -73,13 +74,13 @@ Comma-separated list of sort criteria to apply when the scores are tied. .br .br -.BR length " Prefers item with shorter length" +.BR length " Prefers line with shorter length" .br -.BR begin " Prefers item with matched substring closer to the beginning" +.BR begin " Prefers line with matched substring closer to the beginning" .br -.BR end " Prefers item with matched substring closer to the end" +.BR end " Prefers line with matched substring closer to the end" .br -.BR index " Prefers item that appeared earlier in the input stream" +.BR index " Prefers line that appeared earlier in the input stream" .br .br @@ -90,16 +91,86 @@ Comma-separated list of sort criteria to apply when the scores are tied. - \fBindex\fR is implicitly appended to the list when not specified .br - Default is \fBlength\fR (or equivalently \fBlength\fR,index) +.br +- If \fBend\fR is found in the list, fzf will scan each line backwards .SS Interface .TP .B "-m, --multi" Enable multi-select with tab/shift-tab .TP +.B "--no-mouse" +Disable mouse +.TP +.BI "--bind=" "KEYBINDS" +Comma-separated list of custom key bindings. See \fBKEY BINDINGS\fR for the +details. +.TP +.B "--cycle" +Enable cyclic scroll +.TP +.B "--no-hscroll" +Disable horizontal scroll +.TP +.BI "--hscroll-off=" "COL" +Number of screen columns to keep to the right of the highlighted substring +(default: 10). Setting it to a large value will cause the text to be positioned +on the center of the screen. +.TP +.BI "--jump-labels=" "CHARS" +Label characters for \fBjump\fR and \fBjump-accept\fR +.SS Layout +.TP +.B "--reverse" +Reverse orientation +.TP +.BI "--margin=" MARGIN +Comma-separated expression for margins around the finder. +.br + +.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 + +.br +Each part can be given in absolute number or in percentage relative to the +terminal size with \fB%\fR suffix. +.br + +.br +e.g. \fBfzf --margin 10%\fR + \fBfzf --margin 1,5%\fR +.RE +.TP +.B "--inline-info" +Display finder info inline with the query +.TP +.BI "--prompt=" "STR" +Input prompt (default: '> ') +.TP +.BI "--header=" "STR" +The given string will be printed as the sticky header. The lines are displayed +in the given order from top to bottom regardless of \fB--reverse\fR option, 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 Display +.TP .B "--ansi" Enable processing of ANSI color codes .TP -.B "--no-mouse" -Disable mouse +.BI "--tabstop=" SPACES +Number of spaces for a tab character (default: 8) .TP .BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]" Color configuration. The name of the base color scheme is followed by custom @@ -137,181 +208,7 @@ e.g. \fBfzf --color=bg+:24\fR .TP .B "--black" Use black background -.TP -.B "--reverse" -Reverse orientation -.TP -.BI "--margin=" MARGIN -Comma-separated expression for margins around the finder. -.br - -.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 - -.br -Each part can be given in absolute number or in percentage relative to the -terminal size with \fB%\fR suffix. -.br - -.br -e.g. \fBfzf --margin 10%\fR - \fBfzf --margin 1,5%\fR -.RE -.TP -.BI "--tabstop=" SPACES -Number of spaces for a tab character (default: 8) -.TP -.B "--cycle" -Enable cyclic scroll -.TP -.B "--no-hscroll" -Disable horizontal scroll -.TP -.BI "--hscroll-off=" "COL" -Number of screen columns to keep to the right of the highlighted substring -(default: 10). Setting it to a large value will cause the text to be positioned -on the center of the screen. -.TP -.B "--inline-info" -Display finder info inline with the query -.TP -.BI "--jump-labels=" "CHARS" -Label characters for \fBjump\fR and \fBjump-accept\fR -.TP -.BI "--prompt=" "STR" -Input prompt (default: '> ') -.TP -.BI "--toggle-sort=" "KEY" -Key to toggle sort. For the list of the allowed key names, see \fB--bind\fR. -.TP -.BI "--bind=" "KEYBINDS" -Comma-separated list of custom key bindings. Each key binding expression -follows the following format: \fBKEY:ACTION\fR -.RS -e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR -.RE - -.RS -.B AVAILABLE KEYS: (SYNONYMS) - \fIctrl-[a-z]\fR - \fIalt-[a-z]\fR - \fIf[1-10]\fR - \fIenter\fR (\fIreturn\fR \fIctrl-m\fR) - \fIspace\fR - \fIbspace\fR (\fIbs\fR) - \fIalt-enter\fR - \fIalt-space\fR - \fIalt-bspace\fR (\fIalt-bs\fR) - \fIalt-/\fR - \fItab\fR - \fIbtab\fR (\fIshift-tab\fR) - \fIesc\fR - \fIdel\fR - \fIup\fR - \fIdown\fR - \fIleft\fR - \fIright\fR - \fIhome\fR - \fIend\fR - \fIpgup\fR (\fIpage-up\fR) - \fIpgdn\fR (\fIpage-down\fR) - \fIshift-left\fR - \fIshift-right\fR - \fIdouble-click\fR - or any single character -.RE - -.RS - \fBACTION: DEFAULT BINDINGS (NOTES): - \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR - \fBaccept\fR \fIenter double-click\fR - \fBbackward-char\fR \fIctrl-b left\fR - \fBbackward-delete-char\fR \fIctrl-h bspace\fR - \fBbackward-kill-word\fR \fIalt-bs\fR - \fBbackward-word\fR \fIalt-b shift-left\fR - \fBbeginning-of-line\fR \fIctrl-a home\fR - \fBcancel\fR - \fBclear-screen\fR \fIctrl-l\fR - \fBdelete-char\fR \fIdel\fR - \fBdelete-char/eof\fR \fIctrl-d\fR - \fBdeselect-all\fR - \fBdown\fR \fIctrl-j ctrl-n down\fR - \fBend-of-line\fR \fIctrl-e end\fR - \fBexecute(...)\fR (see below for the details) - \fBexecute-multi(...)\fR (see below for the details) - \fBforward-char\fR \fIctrl-f right\fR - \fBforward-word\fR \fIalt-f shift-right\fR - \fBignore\fR - \fBjump\fR (EasyMotion-like 2-keystroke movement) - \fBjump-accept\fR (jump and accept) - \fBkill-line\fR - \fBkill-word\fR \fIalt-d\fR - \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) - \fBpage-down\fR \fIpgdn\fR - \fBpage-up\fR \fIpgup\fR - \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR) - \fBprint-query\fR (print query and exit) - \fBselect-all\fR - \fBtoggle\fR - \fBtoggle-all\fR - \fBtoggle-down\fR \fIctrl-i (tab)\fR - \fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle-up\fR : \fBtoggle-down\fR) - \fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle-down\fR : \fBtoggle-up\fR) - \fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR) - \fBtoggle-up\fR \fIbtab (shift-tab)\fR - \fBunix-line-discard\fR \fIctrl-u\fR - \fBunix-word-rubout\fR \fIctrl-w\fR - \fBup\fR \fIctrl-k ctrl-p up\fR - \fByank\fR \fIctrl-y\fR -.RE - -.RS -With \fBexecute(...)\fR action, you can execute arbitrary commands without -leaving fzf. For example, you can turn fzf into a simple file browser by -binding \fBenter\fR key to \fBless\fR command like follows. - -.RS -\fBfzf --bind "enter:execute(less {})"\fR -.RE - -\fB{}\fR is the placeholder for the double-quoted string of the current line. -If the command contains parentheses, you can use any of the following -alternative notations to avoid parse errors. - - \fBexecute[...]\fR - \fBexecute~...~\fR - \fBexecute!...!\fR - \fBexecute@...@\fR - \fBexecute#...#\fR - \fBexecute$...$\fR - \fBexecute%...%\fR - \fBexecute^...^\fR - \fBexecute&...&\fR - \fBexecute*...*\fR - \fBexecute;...;\fR - \fBexecute/.../\fR - \fBexecute|...|\fR - \fBexecute:...\fR -.RS -This is the special form that frees you from parse errors as it does not expect -the closing character. The catch is that it should be the last one in the -comma-separated list. -.RE - -\fBexecute-multi(...)\fR is an alternative action that executes the command -with the selected entries when multi-select is enabled (\fB--multi\fR). With -this action, \fB{}\fR is replaced with the double-quoted strings of the -selected entries separated by spaces. - -.RE +.SS History .TP .BI "--history=" "HISTORY_FILE" Load search history from the specified file and update the file on completion. @@ -321,17 +218,34 @@ When enabled, \fBCTRL-N\fR and \fBCTRL-P\fR are automatically remapped to .BI "--history-size=" "N" Maximum number of entries in the history file (default: 1000). The file is automatically truncated when the number of the lines exceeds the value. +.SS Preview .TP -.BI "--header=" "STR" -The given string will be printed as the sticky header. The lines are displayed -in the given order from top to bottom regardless of \fB--reverse\fR option, and -are not affected by \fB--with-nth\fR. ANSI color codes are processed even when -\fB--ansi\fR is not set. +.BI "--preview=" "COMMAND" +Execute the given command for the current line and display the result on the +preview window. \fB{}\fR is the placeholder for the quoted string of the +current line. + +.RS +e.g. \fBfzf --preview="head -$LINES {}"\fR +.RE .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. +.BI "--preview-window=" "[POSITION][:SIZE[%]][:hidden]" +Determine the layout of the preview window. If the argument ends with +\fB:hidden\fR, the preview window will be hidden by default until +\fBtoggle-preview\fR action is triggered. + +.RS +.B POSITION: (default: right) + \fBup + \fBdown + \fBleft + \fBright +.RE + +.RS +e.g. \fBfzf --preview="head {}" --preview-window=up:30%\fR + \fBfzf --preview="file {}" --preview-window=down:1\fR +.RE .SS Scripting .TP .BI "-q, --query=" "STR" @@ -356,6 +270,7 @@ the default enter key. When this option is set, fzf will print the name of the key pressed as the first line of its output (or as the second line if \fB--print-query\fR is also used). The line will be empty if fzf is completed with the default enter key. + .RS e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR .RE @@ -363,11 +278,12 @@ e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR .B "--sync" Synchronous search for multi-staged filtering. If specified, fzf will launch ncurses finder only after the input stream is complete. + .RS e.g. \fBfzf --multi | fzf --sync\fR .RE -.SH ENVIRONMENT +.SH ENVIRONMENT VARIABLES .TP .B FZF_DEFAULT_COMMAND Default command to use when input is tty @@ -421,11 +337,11 @@ occurrences of the string. .SS Anchored-match A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an -anchored-match term. Then fzf will search for the items that start with or end +anchored-match term. Then fzf will search for the lines that start with or end with the given string. An anchored-match term is also an exact-match term. .SS Negation -If a term is prefixed by \fB!\fR, fzf will exclude the items that satisfy the +If a term is prefixed by \fB!\fR, fzf will exclude the lines that satisfy the term from the result. .SS Exact-match by default @@ -440,6 +356,119 @@ query matches entries that start with \fBcore\fR and end with either \fBgo\fR, e.g. \fB^core go$ | rb$ | py$\fR +.SH KEY BINDINGS +You can customize key bindings of fzf with \fB--bind\fR option which takes +a comma-separated list of key binding expressions. Each key binding expression +follows the following format: \fBKEY:ACTION\fR + +e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR + +.B AVAILABLE KEYS: (SYNONYMS) + \fIctrl-[a-z]\fR + \fIalt-[a-z]\fR + \fIf[1-10]\fR + \fIenter\fR (\fIreturn\fR \fIctrl-m\fR) + \fIspace\fR + \fIbspace\fR (\fIbs\fR) + \fIalt-enter\fR + \fIalt-space\fR + \fIalt-bspace\fR (\fIalt-bs\fR) + \fIalt-/\fR + \fItab\fR + \fIbtab\fR (\fIshift-tab\fR) + \fIesc\fR + \fIdel\fR + \fIup\fR + \fIdown\fR + \fIleft\fR + \fIright\fR + \fIhome\fR + \fIend\fR + \fIpgup\fR (\fIpage-up\fR) + \fIpgdn\fR (\fIpage-down\fR) + \fIshift-left\fR + \fIshift-right\fR + \fIdouble-click\fR + or any single character + + \fBACTION: DEFAULT BINDINGS (NOTES): + \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR + \fBaccept\fR \fIenter double-click\fR + \fBbackward-char\fR \fIctrl-b left\fR + \fBbackward-delete-char\fR \fIctrl-h bspace\fR + \fBbackward-kill-word\fR \fIalt-bs\fR + \fBbackward-word\fR \fIalt-b shift-left\fR + \fBbeginning-of-line\fR \fIctrl-a home\fR + \fBcancel\fR + \fBclear-screen\fR \fIctrl-l\fR + \fBdelete-char\fR \fIdel\fR + \fBdelete-char/eof\fR \fIctrl-d\fR + \fBdeselect-all\fR + \fBdown\fR \fIctrl-j ctrl-n down\fR + \fBend-of-line\fR \fIctrl-e end\fR + \fBexecute(...)\fR (see below for the details) + \fBexecute-multi(...)\fR (see below for the details) + \fBforward-char\fR \fIctrl-f right\fR + \fBforward-word\fR \fIalt-f shift-right\fR + \fBignore\fR + \fBjump\fR (EasyMotion-like 2-keystroke movement) + \fBjump-accept\fR (jump and accept) + \fBkill-line\fR + \fBkill-word\fR \fIalt-d\fR + \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) + \fBpage-down\fR \fIpgdn\fR + \fBpage-up\fR \fIpgup\fR + \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR) + \fBprint-query\fR (print query and exit) + \fBselect-all\fR + \fBtoggle\fR + \fBtoggle-all\fR + \fBtoggle-down\fR \fIctrl-i (tab)\fR + \fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle-up\fR : \fBtoggle-down\fR) + \fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle-down\fR : \fBtoggle-up\fR) + \fBtoggle-preview\fR + \fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR) + \fBtoggle-up\fR \fIbtab (shift-tab)\fR + \fBunix-line-discard\fR \fIctrl-u\fR + \fBunix-word-rubout\fR \fIctrl-w\fR + \fBup\fR \fIctrl-k ctrl-p up\fR + \fByank\fR \fIctrl-y\fR + +With \fBexecute(...)\fR action, you can execute arbitrary commands without +leaving fzf. For example, you can turn fzf into a simple file browser by +binding \fBenter\fR key to \fBless\fR command like follows. + + \fBfzf --bind "enter:execute(less {})"\fR + +\fB{}\fR is the placeholder for the quoted string of the current line. +If the command contains parentheses, you can use any of the following +alternative notations to avoid parse errors. + + \fBexecute[...]\fR + \fBexecute~...~\fR + \fBexecute!...!\fR + \fBexecute@...@\fR + \fBexecute#...#\fR + \fBexecute$...$\fR + \fBexecute%...%\fR + \fBexecute^...^\fR + \fBexecute&...&\fR + \fBexecute*...*\fR + \fBexecute;...;\fR + \fBexecute/.../\fR + \fBexecute|...|\fR + \fBexecute:...\fR +.RS +This is the special form that frees you from parse errors as it does not expect +the closing character. The catch is that it should be the last one in the +comma-separated list. +.RE + +\fBexecute-multi(...)\fR is an alternative action that executes the command +with the selected entries when multi-select is enabled (\fB--multi\fR). With +this action, \fB{}\fR is replaced with the quoted strings of the selected +entries separated by spaces. + .SH AUTHOR Junegunn Choi (\fIjunegunn.c@gmail.com\fR) diff --git a/src/constants.go b/src/constants.go index 2848c82..a9f14b7 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.12.2" + version = "0.13.0" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 19339e3a6d2e59108c4e087bbfca330033f7c99a Mon Sep 17 00:00:00 2001 From: Aleks Kamko Date: Sat, 11 Jun 2016 15:19:16 -0700 Subject: [PATCH 193/658] optimize fzf_default_completion binding --- shell/completion.zsh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index cbec114..60ef3d2 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -181,8 +181,11 @@ fzf-completion() { fi } -[ -z "$fzf_default_completion" ] && - fzf_default_completion=$(bindkey '^I' | \grep -v undefined-key | awk '{print $2}') +[ -z "$fzf_default_completion" ] && { + binding=$(bindkey '^I') + [[ $binding =~ 'undefined-key' ]] || fzf_default_completion=$binding[(w)2] + unset binding +} zle -N fzf-completion bindkey '^I' fzf-completion From c39c039e155c50bcf8fecc5956c4bdd3b5c6bea1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 12 Jun 2016 20:37:17 +0900 Subject: [PATCH 194/658] [shell] Add $FZF_CTRL_T_OPTS and $FZF_ALT_C_OPTS Close #596 --- README.md | 3 +++ shell/key-bindings.bash | 6 +++--- shell/key-bindings.fish | 4 ++-- shell/key-bindings.zsh | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 76a93ab..220a6cb 100644 --- a/README.md +++ b/README.md @@ -178,11 +178,14 @@ fish. - `CTRL-T` - Paste the selected files and directories onto the command line - Set `FZF_CTRL_T_COMMAND` to override the default command + - Set `FZF_CTRL_T_OPTS` to pass additional options - `CTRL-R` - Paste the selected command from history onto the command line - Sort is disabled by default to respect chronological ordering - Press `CTRL-R` again to toggle sort + - Set `FZF_CTRL_R_OPTS` to pass additional options - `ALT-C` - cd into the selected directory - Set `FZF_ALT_C_COMMAND` to override the default command + - Set `FZF_ALT_C_OPTS` to pass additional options If you're on a tmux session, fzf will start in a split pane. You may disable this tmux integration by setting `FZF_TMUX` to 0, or change the height of the diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index b87deca..3e9dc65 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -5,7 +5,7 @@ __fzf_select__() { -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" - eval "$cmd" | fzf -m | while read -r item; do + eval "$cmd | fzf -m $FZF_CTRL_T_OPTS" | while read -r item; do printf '%q ' "$item" done echo @@ -26,7 +26,7 @@ __fzf_select_tmux__() { height="-l $height" fi - tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") bash -c 'source \"${BASH_SOURCE[0]}\"; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" + tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" } fzf-file-widget() { @@ -43,7 +43,7 @@ __fzf_cd__() { local cmd dir cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" - dir=$(eval "$cmd" | $(__fzfcmd) +m) && printf 'cd %q' "$dir" + dir=$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS") && printf 'cd %q' "$dir" } __fzf_history__() ( diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index a02937e..6c76177 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -19,7 +19,7 @@ function fzf_key_bindings -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | sed 1d | cut -b3-" - eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m > $TMPDIR/fzf.result" + eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m $FZF_CTRL_T_OPTS > $TMPDIR/fzf.result" and for i in (seq 20); commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) 2> /dev/null; and break; sleep 0.1; end commandline -f repaint rm -f $TMPDIR/fzf.result @@ -37,7 +37,7 @@ function fzf_key_bindings command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-" # Fish hangs if the command before pipe redirects (2> /dev/null) - eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m > $TMPDIR/fzf.result" + eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m $FZF_ALT_C_OPTS > $TMPDIR/fzf.result" [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ] and cd (cat $TMPDIR/fzf.result) commandline -f repaint diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index f77e7ec..35e5f19 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -8,7 +8,7 @@ __fsel() { -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" - eval "$cmd" | $(__fzfcmd) -m | while read item; do + eval "$cmd | $(__fzfcmd) -m $FZF_CTRL_T_OPTS" | while read item; do echo -n "${(q)item} " done echo @@ -29,7 +29,7 @@ bindkey '^T' fzf-file-widget fzf-cd-widget() { local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" - cd "${$(eval "$cmd" | $(__fzfcmd) +m):-.}" + cd "${$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS"):-.}" zle reset-prompt } zle -N fzf-cd-widget From 24e1fabf2e805ad28901618fafe5fd389b599b4e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 14 Jun 2016 21:52:47 +0900 Subject: [PATCH 195/658] Do not process ANSI codes in --preview output at once Close #598 --- src/ansi.go | 11 +++++++++-- src/ansi_test.go | 2 +- src/core.go | 4 ++-- src/item.go | 2 +- src/terminal.go | 24 ++++++------------------ 5 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/ansi.go b/src/ansi.go index 876229f..debba47 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -36,7 +36,7 @@ func init() { ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]") } -func extractColor(str string, state *ansiState) (string, []ansiOffset, *ansiState) { +func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, []ansiOffset, *ansiState) { var offsets []ansiOffset var output bytes.Buffer @@ -46,7 +46,11 @@ func extractColor(str string, state *ansiState) (string, []ansiOffset, *ansiStat idx := 0 for _, offset := range ansiRegex.FindAllStringIndex(str, -1) { - output.WriteString(str[idx:offset[0]]) + prev := str[idx:offset[0]] + output.WriteString(prev) + if proc != nil && !proc(prev, state) { + break + } newState := interpretCode(str[offset[0]:offset[1]], state) if !newState.equals(state) { @@ -77,6 +81,9 @@ func extractColor(str string, state *ansiState) (string, []ansiOffset, *ansiStat (&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes())) } } + if proc != nil { + proc(rest, state) + } return output.String(), offsets, state } diff --git a/src/ansi_test.go b/src/ansi_test.go index e278fe9..31803f3 100644 --- a/src/ansi_test.go +++ b/src/ansi_test.go @@ -17,7 +17,7 @@ func TestExtractColor(t *testing.T) { var state *ansiState clean := "\x1b[0m" check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) { - output, ansiOffsets, newState := extractColor(src, state) + output, ansiOffsets, newState := extractColor(src, state, nil) state = newState if output != "hello world" { t.Errorf("Invalid output: {}", output) diff --git a/src/core.go b/src/core.go index 24af050..fd4bc6c 100644 --- a/src/core.go +++ b/src/core.go @@ -73,7 +73,7 @@ func Run(opts *Options) { if opts.Theme != nil { var state *ansiState ansiProcessor = func(data []byte) ([]rune, []ansiOffset) { - trimmed, offsets, newState := extractColor(string(data), state) + trimmed, offsets, newState := extractColor(string(data), state, nil) state = newState return []rune(trimmed), offsets } @@ -81,7 +81,7 @@ func Run(opts *Options) { // When color is disabled but ansi option is given, // we simply strip out ANSI codes from the input ansiProcessor = func(data []byte) ([]rune, []ansiOffset) { - trimmed, _, _ := extractColor(string(data), nil) + trimmed, _, _ := extractColor(string(data), nil, nil) return []rune(trimmed), nil } } diff --git a/src/item.go b/src/item.go index 66b3e4d..0641350 100644 --- a/src/item.go +++ b/src/item.go @@ -128,7 +128,7 @@ func (item *Item) AsString(stripAnsi bool) string { func (item *Item) StringPtr(stripAnsi bool) *string { if item.origText != nil { if stripAnsi { - trimmed, _, _ := extractColor(string(*item.origText), nil) + trimmed, _, _ := extractColor(string(*item.origText), nil, nil) return &trimmed } orig := string(*item.origText) diff --git a/src/terminal.go b/src/terminal.go index ce10ee7..18d5f0c 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -556,7 +556,7 @@ func (t *Terminal) printHeader() { if line >= max { continue } - trimmed, colors, newState := extractColor(lineStr, state) + trimmed, colors, newState := extractColor(lineStr, state, nil) state = newState item := &Item{ text: []rune(trimmed), @@ -730,25 +730,13 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c } func (t *Terminal) printPreview() { - trimmed, ansiOffsets, _ := extractColor(t.previewTxt, nil) - var index int32 t.pwindow.Erase() - for _, o := range ansiOffsets { - b := o.offset[0] - e := o.offset[1] - if b > index { - if !t.pwindow.Fill(trimmed[index:b]) { - return - } + extractColor(t.previewTxt, nil, func(str string, ansi *ansiState) bool { + if ansi != nil && ansi.colored() { + return t.pwindow.CFill(str, ansi.fg, ansi.bg, ansi.bold) } - if !t.pwindow.CFill(trimmed[b:e], o.color.fg, o.color.bg, o.color.bold) { - return - } - index = e - } - if int(index) < len(trimmed) { - t.pwindow.Fill(trimmed[index:]) - } + return t.pwindow.Fill(str) + }) } func processTabs(runes []rune, prefixWidth int) (string, int) { From 8a90f26c8a4ac34517eeed9f3498dd790d255474 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 14 Jun 2016 21:53:00 +0900 Subject: [PATCH 196/658] 0.13.1 --- CHANGELOG.md | 4 ++++ install | 4 ++-- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9844d71..fe2984a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ CHANGELOG ========= +0.13.1 +------ +- Fixed UI issue with large `--preview` output with many ANSI codes + 0.13.0 ------ - Added preview feature diff --git a/install b/install index cb09f1d..a08e328 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.13.0 pre=1 || - version=0.13.0 pre=0 +[[ "$@" =~ --pre ]] && version=0.13.1 pre=1 || + version=0.13.1 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index d8bb925..fa4fd8e 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "Jun 2016" "fzf 0.13.0" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jun 2016" "fzf 0.13.1" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index a9f14b7..5147b17 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.13.0" + version = "0.13.1" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From e455836cc98770df01e9cfb406795c868a266173 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 15 Jun 2016 13:03:42 +0900 Subject: [PATCH 197/658] Fix race condition where preview window is not properly cleared --- src/terminal.go | 37 +++++++++++++++++++++---------------- test/test_go.rb | 8 +++++++- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 18d5f0c..213d3b0 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -82,6 +82,11 @@ type selectedItem struct { type byTimeOrder []selectedItem +type previewRequest struct { + ok bool + str string +} + func (a byTimeOrder) Len() int { return len(a) } @@ -908,21 +913,23 @@ func (t *Terminal) Loop() { if t.hasPreviewWindow() { go func() { for { - focused := "" + request := previewRequest{false, ""} t.previewBox.Wait(func(events *util.Events) { for req, value := range *events { switch req { case reqPreviewEnqueue: - focused = value.(string) + request = value.(previewRequest) } } events.Clear() }) - if len(focused) > 0 { - command := strings.Replace(t.preview.command, "{}", quoteEntry(focused), -1) + if request.ok { + command := strings.Replace(t.preview.command, "{}", quoteEntry(request.str), -1) cmd := util.ExecCommand(command) out, _ := cmd.CombinedOutput() t.reqBox.Set(reqPreviewDisplay, string(out)) + } else { + t.reqBox.Set(reqPreviewDisplay, "") } } }() @@ -936,7 +943,7 @@ func (t *Terminal) Loop() { } go func() { - focused := "" + focused := previewRequest{false, ""} for { t.reqBox.Wait(func(events *util.Events) { defer events.Clear() @@ -953,19 +960,17 @@ func (t *Terminal) Loop() { case reqList: t.printList() cnt := t.merger.Length() + var currentFocus previewRequest if cnt > 0 && cnt > t.cy { - currentFocus := t.current() - if currentFocus != focused { - focused = currentFocus - if t.isPreviewEnabled() { - t.previewBox.Set(reqPreviewEnqueue, focused) - } - } + currentFocus = previewRequest{true, t.current()} } else { - if focused != "" && t.isPreviewEnabled() { - t.pwindow.Erase() + currentFocus = previewRequest{false, ""} + } + if currentFocus != focused { + focused = currentFocus + if t.isPreviewEnabled() { + t.previewBox.Set(reqPreviewEnqueue, focused) } - focused = "" } case reqJump: if t.merger.Length() == 0 { @@ -1076,7 +1081,7 @@ func (t *Terminal) Loop() { t.resizeWindows() cnt := t.merger.Length() if t.previewing && cnt > 0 && cnt > t.cy { - t.previewBox.Set(reqPreviewEnqueue, t.current()) + t.previewBox.Set(reqPreviewEnqueue, previewRequest{true, t.current()}) } req(reqList, reqInfo) } diff --git a/test/test_go.rb b/test/test_go.rb index a6d6719..c375fac 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1229,14 +1229,20 @@ class TestGoFZF < TestBase end def test_preview - tmux.send_keys %[seq 1000 | #{FZF} --preview 'echo {{}-{}}' --bind ?:toggle-preview], :Enter + tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} --preview 'sleep 0.2; echo {{}-{}}' --bind ?:toggle-preview], :Enter tmux.until { |lines| lines[1].include?(' {1-1}') } + tmux.send_keys :Up + tmux.until { |lines| lines[1].include?(' {-}') } tmux.send_keys '555' tmux.until { |lines| lines[1].include?(' {555-555}') } tmux.send_keys '?' tmux.until { |lines| !lines[1].include?(' {555-555}') } tmux.send_keys '?' tmux.until { |lines| lines[1].include?(' {555-555}') } + tmux.send_keys :BSpace + tmux.until { |lines| lines[-2].start_with? ' 28/1000' } + tmux.send_keys 'foobar' + tmux.until { |lines| !lines[1].include?('{') } end def test_preview_hidden From 5759d50d4a4ff05f826d1fe546b76315d3860a82 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 16 Jun 2016 02:16:13 +0900 Subject: [PATCH 198/658] 0.13.2 --- CHANGELOG.md | 4 ++++ install | 4 ++-- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe2984a..a835931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ CHANGELOG ========= +0.13.2 +------ +- Fixed race condition where preview window is not properly cleared + 0.13.1 ------ - Fixed UI issue with large `--preview` output with many ANSI codes diff --git a/install b/install index a08e328..b5ccb25 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.13.1 pre=1 || - version=0.13.1 pre=0 +[[ "$@" =~ --pre ]] && version=0.13.2 pre=1 || + version=0.13.2 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index fa4fd8e..bdd6d19 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "Jun 2016" "fzf 0.13.1" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jun 2016" "fzf 0.13.2" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 5147b17..bc06c05 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.13.1" + version = "0.13.2" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 38a2076b893213f6d29b91299c5edbe5237a4320 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 16 Jun 2016 01:03:48 +0200 Subject: [PATCH 199/658] zsh: pass through exit code from widgets This allows to have a custom widget like the following, which would additionally accept the line, but only in case of entries being selected: fzf-file-widget-with-accept() { zle fzf-file-widget if [[ "$?" == 0 ]] && (( $#BUFFER )); then zle accept-line fi } zle -N fzf-file-widget-with-accept bindkey '\e^T' fzf-file-widget-with-accept With this `t` will launch fzf, and simulate the pressing of "Enter" afterwards. --- shell/key-bindings.zsh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 35e5f19..ea65c0c 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -8,10 +8,13 @@ __fsel() { -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" + setopt localoptions pipefail eval "$cmd | $(__fzfcmd) -m $FZF_CTRL_T_OPTS" | while read item; do echo -n "${(q)item} " done + local ret=$? echo + return $ret } __fzfcmd() { @@ -20,7 +23,9 @@ __fzfcmd() { fzf-file-widget() { LBUFFER="${LBUFFER}$(__fsel)" + local ret=$? zle redisplay + return $ret } zle -N fzf-file-widget bindkey '^T' fzf-file-widget @@ -29,8 +34,11 @@ bindkey '^T' fzf-file-widget fzf-cd-widget() { local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" + setopt localoptions pipefail cd "${$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS"):-.}" + local ret=$? zle reset-prompt + return $ret } zle -N fzf-cd-widget bindkey '\ec' fzf-cd-widget @@ -38,8 +46,9 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { local selected num - setopt localoptions noglobsubst + setopt localoptions noglobsubst pipefail selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") ) + local ret=$? if [ -n "$selected" ]; then num=$selected[1] if [ -n "$num" ]; then @@ -47,6 +56,7 @@ fzf-history-widget() { fi fi zle redisplay + return $ret } zle -N fzf-history-widget bindkey '^R' fzf-history-widget From 34965edcdaf9ab363dc0ab3e6f5252328a19178a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 4 Jul 2016 01:40:24 +0900 Subject: [PATCH 200/658] [install] Fall back to wget if curl failed Close #605 --- install | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/install b/install index b5ccb25..ef0008a 100755 --- a/install +++ b/install @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -u +set -o pipefail [[ "$@" =~ --pre ]] && version=0.13.2 pre=1 || version=0.13.2 pre=0 @@ -109,6 +110,14 @@ link_fzf_in_path() { return 1 } +try_curl() { + command -v curl > /dev/null && curl -fL $1 | tar -xz +} + +try_wget() { + command -v wget > /dev/null && wget -O - $1 | tar -xz +} + download() { echo "Downloading bin/fzf ..." if [ $pre = 0 ]; then @@ -128,12 +137,8 @@ download() { fi local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz - if command -v curl > /dev/null; then - curl -fL $url | tar -xz - elif command -v wget > /dev/null; then - wget -O - $url | tar -xz - else - binary_error="curl or wget not found" + if ! (try_curl $url || try_wget $url); then + binary_error="Failed to download with curl and wget" return fi From 64747c2324dec24857d4ae6cdf0e949007438e35 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 4 Jul 2016 13:00:30 +0900 Subject: [PATCH 201/658] [install] Fix error in install script Close #608 --- install | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install b/install index ef0008a..e0f82e3 100755 --- a/install +++ b/install @@ -1,7 +1,6 @@ #!/usr/bin/env bash set -u -set -o pipefail [[ "$@" =~ --pre ]] && version=0.13.2 pre=1 || version=0.13.2 pre=0 @@ -137,10 +136,12 @@ download() { fi local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz + set -o pipefail if ! (try_curl $url || try_wget $url); then binary_error="Failed to download with curl and wget" return fi + set +o pipefail if [ ! -f $1 ]; then binary_error="Failed to download ${1}" From b864885753f67b3985406f7b901a9563100f768a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 4 Jul 2016 13:05:26 +0900 Subject: [PATCH 202/658] [install] Make sure to unset pipefail --- install | 1 + 1 file changed, 1 insertion(+) diff --git a/install b/install index e0f82e3..df6570c 100755 --- a/install +++ b/install @@ -138,6 +138,7 @@ download() { local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz set -o pipefail if ! (try_curl $url || try_wget $url); then + set +o pipefail binary_error="Failed to download with curl and wget" return fi From fed5e5d5afbc0a553a0116f77342f047df03ad7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Reu=C3=9Fe?= Date: Mon, 4 Jul 2016 12:13:02 +0200 Subject: [PATCH 203/658] [zsh] Re-initialize zle when widgets finish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zle automatically calls zle-line-init when it starts to read a new line. Many Zsh setups use this hook to set the terminal into application mode, since this will then allow defining keybinds based on the $terminfo variable (the escape codes in said variable are only valid in application mode). However, fzf resets the terminal into raw mode, rendering $terminfo values invalid once the widget has finished. Accordingly, keyboard bindings defined via $terminfo won’t work anymore. This fixes the issue by calling zle-line-init when widgets finish. Care is taken to not call this widget when it is undefined. Fixes #279 --- shell/completion.zsh | 3 +++ shell/key-bindings.zsh | 3 +++ 2 files changed, 6 insertions(+) diff --git a/shell/completion.zsh b/shell/completion.zsh index 60ef3d2..003f5c1 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -58,6 +58,7 @@ __fzf_generic_path_completion() { LBUFFER="$lbuf$matches$tail" fi zle redisplay + typeset -f zle-line-init >/dev/null && zle zle-line-init break fi dir=$(dirname "$dir") @@ -97,6 +98,7 @@ _fzf_complete() { LBUFFER="$lbuf$matches" fi zle redisplay + typeset -f zle-line-init >/dev/null && zle zle-line-init rm -f "$fifo" } @@ -161,6 +163,7 @@ fzf-completion() { LBUFFER="$LBUFFER$matches" fi zle redisplay + typeset -f zle-line-init >/dev/null && zle zle-line-init # Trigger sequence given elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index ea65c0c..b9fb6e9 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -25,6 +25,7 @@ fzf-file-widget() { LBUFFER="${LBUFFER}$(__fsel)" local ret=$? zle redisplay + typeset -f zle-line-init >/dev/null && zle zle-line-init return $ret } zle -N fzf-file-widget @@ -38,6 +39,7 @@ fzf-cd-widget() { cd "${$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS"):-.}" local ret=$? zle reset-prompt + typeset -f zle-line-init >/dev/null && zle zle-line-init return $ret } zle -N fzf-cd-widget @@ -56,6 +58,7 @@ fzf-history-widget() { fi fi zle redisplay + typeset -f zle-line-init >/dev/null && zle zle-line-init return $ret } zle -N fzf-history-widget From 942ba749c7971076a650b899b4d44e0cced5f167 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 6 Jul 2016 13:31:04 +0900 Subject: [PATCH 204/658] [vim] Restore working directory even when new window is opened Close #612 --- plugin/fzf.vim | 115 +++++++++++++++++++++++++++++-------------------- test/fzf.vader | 5 +++ 2 files changed, 73 insertions(+), 47 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index ba5fb25..a4dd60d 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -147,9 +147,9 @@ try return s:execute_term(dict, command, temps) endif - let ret = tmux ? s:execute_tmux(dict, command, temps) : s:execute(dict, command, temps) - call s:popd(dict, ret) - return ret + let lines = tmux ? s:execute_tmux(dict, command, temps) : s:execute(dict, command, temps) + call s:callback(dict, lines) + return lines finally let &shell = oshell endtry @@ -200,22 +200,17 @@ function! s:pushd(dict) return 0 endfunction -function! s:popd(dict, lines) - " Since anything can be done in the sink function, there is no telling that - " the change of the working directory was made by &autochdir setting. - " - " We use the following heuristic to determine whether to restore CWD: - " - Always restore the current directory when &autochdir is disabled. - " FIXME This makes it impossible to change directory from inside the sink - " function when &autochdir is not used. - " - In case of an error or an interrupt, a:lines will be empty. - " And it will be an array of a single empty string when fzf was finished - " without a match. In these cases, we presume that the change of the - " directory is not expected and should be undone. - if has_key(a:dict, 'prev_dir') && - \ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0]))) - execute 'lcd' s:escape(remove(a:dict, 'prev_dir')) +augroup fzf_popd + autocmd! + autocmd WinEnter * call s:dopopd() +augroup END + +function! s:dopopd() + if !exists('w:fzf_prev_dir') || exists('*haslocaldir') && !haslocaldir() + return endif + execute 'lcd' s:escape(w:fzf_prev_dir) + unlet w:fzf_prev_dir endfunction function! s:xterm_launcher() @@ -256,7 +251,7 @@ function! s:execute(dict, command, temps) abort endif execute 'silent !'.command redraw! - return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : [] + return s:exit_handler(v:shell_error, command) ? s:collect(a:temps) : [] endfunction function! s:execute_tmux(dict, command, temps) abort @@ -268,7 +263,7 @@ function! s:execute_tmux(dict, command, temps) abort call system(command) redraw! - return s:exit_handler(v:shell_error, command) ? s:callback(a:dict, a:temps) : [] + return s:exit_handler(v:shell_error, command) ? s:collect(a:temps) : [] endfunction function! s:calc_size(max, val, dict) @@ -361,31 +356,58 @@ function! s:execute_term(dict, command, temps) abort endif call s:pushd(self.dict) - let ret = [] - try - let ret = s:callback(self.dict, self.temps) - call self.switch_back(s:getpos() == self.ppos) - finally - call s:popd(self.dict, ret) - endtry + let lines = s:collect(self.temps) + call s:callback(self.dict, lines) + call self.switch_back(s:getpos() == self.ppos) endfunction - call s:pushd(a:dict) - call termopen(a:command, fzf) - call s:popd(a:dict, []) + try + if s:present(a:dict, 'dir') + execute 'lcd' s:escape(a:dict.dir) + endif + call termopen(a:command, fzf) + finally + if s:present(a:dict, 'dir') + lcd - + endif + endtry setlocal nospell bufhidden=wipe nobuflisted setf fzf startinsert return [] endfunction -function! s:callback(dict, temps) abort -let lines = [] -try - if filereadable(a:temps.result) - let lines = readfile(a:temps.result) +function! s:collect(temps) abort + try + return filereadable(a:temps.result) ? readfile(a:temps.result) : [] + finally + for tf in values(a:temps) + silent! call delete(tf) + endfor + endtry +endfunction + +function! s:callback(dict, lines) abort + " Since anything can be done in the sink function, there is no telling that + " the change of the working directory was made by &autochdir setting. + " + " We use the following heuristic to determine whether to restore CWD: + " - Always restore the current directory when &autochdir is disabled. + " FIXME This makes it impossible to change directory from inside the sink + " function when &autochdir is not used. + " - In case of an error or an interrupt, a:lines will be empty. + " And it will be an array of a single empty string when fzf was finished + " without a match. In these cases, we presume that the change of the + " directory is not expected and should be undone. + let popd = has_key(a:dict, 'prev_dir') && + \ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0]))) + if popd + let w:fzf_prev_dir = a:dict.prev_dir + endif + + try if has_key(a:dict, 'sink') - for line in lines + for line in a:lines if type(a:dict.sink) == 2 call a:dict.sink(line) else @@ -394,20 +416,19 @@ try endfor endif if has_key(a:dict, 'sink*') - call a:dict['sink*'](lines) + call a:dict['sink*'](a:lines) endif - endif + catch + if stridx(v:exception, ':E325:') < 0 + echoerr v:exception + endif + endtry - for tf in values(a:temps) - silent! call delete(tf) - endfor -catch - if stridx(v:exception, ':E325:') < 0 - echoerr v:exception + " We may have opened a new window or tab + if popd + let w:fzf_prev_dir = a:dict.prev_dir + call s:dopopd() endif -finally - return lines -endtry endfunction let s:default_action = { diff --git a/test/fzf.vader b/test/fzf.vader index 78bc6c5..47f2bfc 100644 --- a/test/fzf.vader +++ b/test/fzf.vader @@ -43,6 +43,11 @@ Execute (fzf#run with dir option and noautochdir): " No change in working directory AssertEqual cwd, getcwd() + call fzf#run({'source': ['/foobar'], 'sink': 'tabe', 'dir': '/tmp', 'options': '-1'}) + AssertEqual cwd, getcwd() + tabclose + AssertEqual cwd, getcwd() + Execute (Incomplete fzf#run with dir option and autochdir): set acd let cwd = getcwd() From 6081eac58a0c1fe7cf920dc41d7d6ec07293b0ee Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 7 Jul 2016 01:40:14 +0900 Subject: [PATCH 205/658] [shell] Suppress alias/function expansion Close #611 --- shell/completion.bash | 18 +++++++++--------- shell/completion.zsh | 16 ++++++++-------- shell/key-bindings.bash | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index a0040ce..9436e27 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -14,7 +14,7 @@ if ! declare -f _fzf_compgen_path > /dev/null; then _fzf_compgen_path() { echo "$1" - \find -L "$1" \ + command find -L "$1" \ -name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' } @@ -22,7 +22,7 @@ fi if ! declare -f _fzf_compgen_dir > /dev/null; then _fzf_compgen_dir() { - \find -L "$1" \ + command find -L "$1" \ -name .git -prune -o -name .svn -prune -o -type d \ -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' } @@ -108,7 +108,7 @@ _fzf_handle_dynamic_completion() { elif [ -n "$_fzf_completion_loader" ]; then _completion_loader "$@" ret=$? - eval "$(complete | \grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)" + eval "$(complete | command grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)" source "${BASH_SOURCE[0]}" return $ret fi @@ -213,16 +213,16 @@ _fzf_complete_kill() { _fzf_complete_telnet() { _fzf_complete '+m' "$@" < <( - \grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | + command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u ) } _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( - cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \ - <(\grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ - <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | + cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \ + <(command grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ + <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u ) } @@ -263,8 +263,8 @@ x_cmds="kill ssh telnet unset unalias export" # Preserve existing completion if [ "$_fzf_completion_loaded" != '0.11.3' ]; then # Really wish I could use associative array but OSX comes with bash 3.2 :( - eval $(complete | \grep '\-F' | \grep -v _fzf_ | - \grep -E " ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter) + eval $(complete | command grep '\-F' | command grep -v _fzf_ | + command grep -E " ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter) export _fzf_completion_loaded=0.11.3 fi diff --git a/shell/completion.zsh b/shell/completion.zsh index 003f5c1..1c80347 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -14,7 +14,7 @@ if ! declare -f _fzf_compgen_path > /dev/null; then _fzf_compgen_path() { echo "$1" - \find -L "$1" \ + command find -L "$1" \ -name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' } @@ -22,7 +22,7 @@ fi if ! declare -f _fzf_compgen_dir > /dev/null; then _fzf_compgen_dir() { - \find -L "$1" \ + command find -L "$1" \ -name .git -prune -o -name .svn -prune -o -type d \ -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' } @@ -77,7 +77,7 @@ _fzf_dir_completion() { } _fzf_feed_fifo() ( - rm -f "$1" + command rm -f "$1" mkfifo "$1" cat <&0 > "$1" & ) @@ -99,21 +99,21 @@ _fzf_complete() { fi zle redisplay typeset -f zle-line-init >/dev/null && zle zle-line-init - rm -f "$fifo" + command rm -f "$fifo" } _fzf_complete_telnet() { _fzf_complete '+m' "$@" < <( - \grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0' | + command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' | awk '{if (length($2) > 0) {print $2}}' | sort -u ) } _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( - cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | \grep -i '^host' | \grep -v '*') \ - <(\grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ - <(\grep -v '^\s*\(#\|$\)' /etc/hosts | \grep -Fv '0.0.0.0') | + cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \ + <(command grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ + <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u ) } diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 3e9dc65..27b5d5f 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -52,7 +52,7 @@ __fzf_history__() ( line=$( HISTTIMEFORMAT= history | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" | - \grep '^ *[0-9]') && + command grep '^ *[0-9]') && if [[ $- =~ H ]]; then sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" else From b36b0a91f5e53e5a90c0acff8d234d1214b8bb23 Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 9 Jul 2016 09:47:20 -0500 Subject: [PATCH 206/658] use tmux buffers for sending output to preserve character encoding --- shell/key-bindings.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 27b5d5f..9aa2918 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -26,7 +26,7 @@ __fzf_select_tmux__() { height="-l $height" fi - tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; tmux send-keys -t $TMUX_PANE \"\$(__fzf_select__)\"'" + tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; tmux setb -b \"fzf\" \"\$(__fzf_select__)\"; tmux pasteb -b \"fzf\" -t $TMUX_PANE; tmux deleteb -b \"fzf\"'" } fzf-file-widget() { From 05299a0fee32cb714f57a4f472412e63286e5d99 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 10 Jul 2016 12:27:01 +0900 Subject: [PATCH 207/658] [test] Use tmux buffer in unicode test cases Related #616 --- test/test_go.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index c375fac..b2c52c1 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -90,6 +90,10 @@ class Tmux go("send-keys -t #{target} #{args}") end + def paste str + %x[tmux setb '#{str.gsub("'", "'\\''")}' \\; pasteb -t #{win} \\; send-keys -t #{win} Enter] + end + def capture pane = 0 File.unlink TEMPNAME while File.exists? TEMPNAME wait do @@ -362,7 +366,7 @@ class TestGoFZF < TestBase end def test_query_unicode - tmux.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter + tmux.paste "(echo abc; echo 가나다) | #{fzf :query, '가다'}" tmux.until { |lines| lines[-2].include? '1/2' } tmux.send_keys :Enter assert_equal ['가나다'], readonce.split($/) @@ -1313,7 +1317,7 @@ module TestShell def test_ctrl_t_unicode FileUtils.mkdir_p '/tmp/fzf-test' - tmux.send_keys 'cd /tmp/fzf-test; echo -n test1 > "fzf-unicode 테스트1"; echo -n test2 > "fzf-unicode 테스트2"', :Enter + tmux.paste 'cd /tmp/fzf-test; echo -n test1 > "fzf-unicode 테스트1"; echo -n test2 > "fzf-unicode 테스트2"' tmux.prepare tmux.send_keys 'cat ', 'C-t', pane: 0 tmux.until(1) { |lines| lines.item_count >= 1 } @@ -1323,6 +1327,7 @@ module TestShell tmux.until(1) { |lines| lines[-2].include? '(2)' } tmux.send_keys :Enter, pane: 1 tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') } + tmux.until { |lines| lines[-1].include?('fzf-unicode') || lines[-2].include?('fzf-unicode') } tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'test1test2' } end @@ -1530,7 +1535,7 @@ module CompletionTest def test_file_completion_unicode FileUtils.mkdir_p '/tmp/fzf-test' - tmux.send_keys 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"', :Enter + tmux.paste 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"' tmux.prepare tmux.send_keys 'cat fzf-unicode**', :Tab, pane: 0 tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } From 0fb5b76c0d5f00008123004a0e3170f357ecfa76 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 10 Jul 2016 14:28:58 +0900 Subject: [PATCH 208/658] [fzf-tmux] Fail fast if fzf excutable is not found --- bin/fzf-tmux | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 4ab899a..54abf4c 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -2,6 +2,14 @@ # fzf-tmux: starts fzf in a tmux pane # usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] +fail() { + >&2 echo "$1" + exit 2 +} + +fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" +[[ -x "$fzf" ]] || fail 'fzf executable not found' + args=() opt="" skip="" @@ -117,13 +125,6 @@ cleanup() { } trap cleanup EXIT SIGINT SIGTERM -fail() { - >&2 echo "$1" - exit 2 -} -fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" -[ -x "$fzf" ] || fail "fzf executable not found" - envs="env TERM=$TERM " [ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" [ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" From 0f87b2d1e18a69d59753d9b4c33fb4494792f3d4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 10 Jul 2016 14:34:29 +0900 Subject: [PATCH 209/658] [fzf-tmux] Use double brackets For consistency and (negligible) performance improvement --- bin/fzf-tmux | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 54abf4c..0204826 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -16,7 +16,7 @@ skip="" swap="" close="" term="" -[ -n "$LINES" ] && lines=$LINES || lines=$(tput lines) +[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) while [ $# -gt 0 ]; do arg="$1" case "$arg" in @@ -91,8 +91,8 @@ while [ $# -gt 0 ]; do shift done -if ! [ -n "$TMUX" -a "$lines" -gt 15 ]; then - fzf "${args[@]}" +if [[ -z "$TMUX" ]] || [[ "$lines" -le 15 ]]; then + "$fzf" "${args[@]}" exit $? fi @@ -116,7 +116,7 @@ cleanup() { rm -f $argsf $fifo1 $fifo2 $fifo3 # Remove temp window if we were zoomed - if [ -n "$zoomed" ]; then + if [[ -n "$zoomed" ]]; then tmux swap-pane -t $original_window \; \ select-window -t $original_window \; \ kill-window -t $tmp_window \; \ @@ -126,8 +126,8 @@ cleanup() { trap cleanup EXIT SIGINT SIGTERM envs="env TERM=$TERM " -[ -n "$FZF_DEFAULT_OPTS" ] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" -[ -n "$FZF_DEFAULT_COMMAND" ] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" +[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" +[[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")" mkfifo -m o+w $fifo2 mkfifo -m o+w $fifo3 @@ -142,7 +142,7 @@ for arg in "${args[@]}"; do opts="$opts \"$arg\"" done -if [ -n "$term" -o -t 0 ]; then +if [[ -n "$term" ]] || [[ -t 0 ]]; then cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ From b461a555b88852b102702155c8aa4436e03a21d2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 10 Jul 2016 14:41:06 +0900 Subject: [PATCH 210/658] [fzf-tmux] Add --version and --help flags --- bin/fzf-tmux | 51 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 0204826..c7701e9 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -17,18 +17,36 @@ swap="" close="" term="" [[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) -while [ $# -gt 0 ]; do + +help() { + >&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] + + Layout + -u [HEIGHT[%]] Split above (up) + -d [HEIGHT[%]] Split below (down) + -l [WIDTH[%]] Split left + -r [WIDTH[%]] Split right + + (default: -d 50%) +' + exit +} + +while [[ $# -gt 0 ]]; do arg="$1" - case "$arg" in + shift + [[ -z "$skip" ]] && case "$arg" in -) term=1 ;; + --help) + help + ;; + --version) + echo "fzf-tmux (with fzf $("$fzf" --version))" + exit + ;; -w*|-h*|-d*|-u*|-r*|-l*) - if [ -n "$skip" ]; then - args+=("$1") - shift - continue - fi if [[ "$arg" =~ ^.[lrw] ]]; then opt="-h" if [[ "$arg" =~ ^.l ]]; then @@ -44,35 +62,33 @@ while [ $# -gt 0 ]; do close="; tmux swap-pane -D" fi fi - if [ ${#arg} -gt 2 ]; then + if [[ ${#arg} -gt 2 ]]; then size="${arg:2}" else - shift if [[ "$1" =~ ^[0-9]+%?$ ]]; then size="$1" - else - [ -n "$1" -a "$1" != "--" ] && args+=("$1") shift + else continue fi fi if [[ "$size" =~ %$ ]]; then size=${size:0:((${#size}-1))} - if [ -n "$swap" ]; then + if [[ -n "$swap" ]]; then opt="$opt -p $(( 100 - size ))" else opt="$opt -p $size" fi else - if [ -n "$swap" ]; then + if [[ -n "$swap" ]]; then if [[ "$arg" =~ ^.l ]]; then - [ -n "$COLUMNS" ] && max=$COLUMNS || max=$(tput cols) + [[ -n "$COLUMNS" ]] && max=$COLUMNS || max=$(tput cols) else max=$lines fi size=$(( max - size )) - [ $size -lt 0 ] && size=0 + [[ $size -lt 0 ]] && size=0 opt="$opt -l $size" else opt="$opt -l $size" @@ -83,12 +99,13 @@ while [ $# -gt 0 ]; do # "--" can be used to separate fzf-tmux options from fzf options to # avoid conflicts skip=1 + continue ;; *) - args+=("$1") + args+=("$arg") ;; esac - shift + [[ -n "$skip" ]] && args+=("$arg") done if [[ -z "$TMUX" ]] || [[ "$lines" -le 15 ]]; then From 049bc9ec685b823929a66678623da5dbad349df4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 10 Jul 2016 14:44:00 +0900 Subject: [PATCH 211/658] [fzf-tmux] Add man page --- man/man1/fzf-tmux.1 | 54 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 man/man1/fzf-tmux.1 diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 new file mode 100644 index 0000000..02ce21d --- /dev/null +++ b/man/man1/fzf-tmux.1 @@ -0,0 +1,54 @@ +.ig +The MIT License (MIT) + +Copyright (c) 2016 Junegunn Choi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +.. +.TH fzf-tmux 1 "Jun 2016" "fzf 0.13.2" "fzf-tmux - open fzf in tmux split pane" + +.SH NAME +fzf-tmux - open fzf in tmux split pane + +.SH SYNOPSIS +.B fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] + +.SH DESCRIPTION +fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane. It is +designed to work just like fzf except that it does not take up the whole +screen. You can safely use fzf-tmux instead of fzf in your scripts as the extra +options will be silently ignored if you're not on tmux. + +.SH OPTIONS +.SS Layout + +(default: \fB-d 50%\fR) + +.TP +.B "-u [height[%]]" +Split above (up) +.TP +.B "-d [height[%]]" +Split below (down) +.TP +.B "-l [width[%]]" +Split left +.TP +.B "-r [width[%]]" +Split right From 3a4e053af777f3e499bba2a3ee4e0185c1534a5c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 10 Jul 2016 15:21:28 +0900 Subject: [PATCH 212/658] [bash] Fall back to send-keys if named paste buffer is not supported Related: #616 --- shell/key-bindings.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 9aa2918..c251a0f 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -26,7 +26,7 @@ __fzf_select_tmux__() { height="-l $height" fi - tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; tmux setb -b \"fzf\" \"\$(__fzf_select__)\"; tmux pasteb -b \"fzf\" -t $TMUX_PANE; tmux deleteb -b \"fzf\"'" + tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'" } fzf-file-widget() { From 52cf5af91c523ae2955f3272d51a80ede6e0ad33 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 10 Jul 2016 15:44:44 +0900 Subject: [PATCH 213/658] [test] Fix test failure on Travis CI No guarantee in the order in which files are listed --- test/test_go.rb | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index b2c52c1..d66d39f 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1323,8 +1323,20 @@ module TestShell tmux.until(1) { |lines| lines.item_count >= 1 } tmux.send_keys 'fzf-unicode', pane: 1 tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } - tmux.send_keys :BTab, :BTab, pane: 1 + + tmux.send_keys '1', pane: 1 + tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } + tmux.send_keys :BTab, pane: 1 + tmux.until(1) { |lines| lines[-2].include? '(1)' } + + tmux.send_keys :BSpace, pane: 1 + tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } + + tmux.send_keys '2', pane: 1 + tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } + tmux.send_keys :BTab, pane: 1 tmux.until(1) { |lines| lines[-2].include? '(2)' } + tmux.send_keys :Enter, pane: 1 tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') } tmux.until { |lines| lines[-1].include?('fzf-unicode') || lines[-2].include?('fzf-unicode') } @@ -1539,8 +1551,20 @@ module CompletionTest tmux.prepare tmux.send_keys 'cat fzf-unicode**', :Tab, pane: 0 tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } - tmux.send_keys :BTab, :BTab, pane: 1 + + tmux.send_keys '1', pane: 1 + tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } + tmux.send_keys :BTab, pane: 1 + tmux.until(1) { |lines| lines[-2].include? '(1)' } + + tmux.send_keys :BSpace, pane: 1 + tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } + + tmux.send_keys '2', pane: 1 + tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } + tmux.send_keys :BTab, pane: 1 tmux.until(1) { |lines| lines[-2].include? '(2)' } + tmux.send_keys :Enter, pane: 1 tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') } tmux.send_keys :Enter From c36413fdf62d5f89c9586c81dc17d363ffeaa3dd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 11 Jul 2016 17:47:41 +0900 Subject: [PATCH 214/658] [zsh] Suppress error message when pipefail is not supported Close #615 --- shell/key-bindings.zsh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index b9fb6e9..0667a4c 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -8,7 +8,7 @@ __fsel() { -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" - setopt localoptions pipefail + setopt localoptions pipefail 2> /dev/null eval "$cmd | $(__fzfcmd) -m $FZF_CTRL_T_OPTS" | while read item; do echo -n "${(q)item} " done @@ -35,7 +35,7 @@ bindkey '^T' fzf-file-widget fzf-cd-widget() { local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" - setopt localoptions pipefail + setopt localoptions pipefail 2> /dev/null cd "${$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS"):-.}" local ret=$? zle reset-prompt @@ -48,7 +48,7 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { local selected num - setopt localoptions noglobsubst pipefail + setopt localoptions noglobsubst pipefail 2> /dev/null selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") ) local ret=$? if [ -n "$selected" ]; then From 5c2b96bd001df6cbe9994126c3ae8caa368d0510 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 13 Jul 2016 13:15:14 +0900 Subject: [PATCH 215/658] [vim] Fix error with multi-line $FZF_DEFAULT_COMMAND Close #620 --- plugin/fzf.vim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index a4dd60d..c425fbe 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -122,7 +122,9 @@ try endtry if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) - let dict.source = $FZF_DEFAULT_COMMAND + let temps.source = tempname() + call writefile(split($FZF_DEFAULT_COMMAND, "\n"), temps.source) + let dict.source = (empty($SHELL) ? 'sh' : $SHELL) . ' ' . s:shellesc(temps.source) endif if has_key(dict, 'source') From a1049328d68b2d4bf90976a07048153f255b0152 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 14 Jul 2016 13:35:18 +0900 Subject: [PATCH 216/658] [vim] Adjust split size when --header option is set Close #622 --- plugin/fzf.vim | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index c425fbe..76d9cfa 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -282,6 +282,7 @@ function! s:calc_size(max, val, dict) let opts = get(a:dict, 'options', '').$FZF_DEFAULT_OPTS let margin = stridx(opts, '--inline-info') > stridx(opts, '--no-inline-info') ? 1 : 2 + let margin += stridx(opts, '--header') > stridx(opts, '--no-header') return srcsz >= 0 ? min([srcsz + margin, size]) : size endfunction From 6a52f8b8dd622ff3f56d869fa616ba095a24ddee Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 15 Jul 2016 01:25:48 +0900 Subject: [PATCH 217/658] [zsh-completion] setopt localoptions noksh_arrays Close #607 --- shell/completion.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 1c80347..623602f 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -138,7 +138,7 @@ _fzf_complete_unalias() { fzf-completion() { local tokens cmd prefix trigger tail fzf matches lbuf d_cmds - setopt localoptions noshwordsplit + setopt localoptions noshwordsplit noksh_arrays # http://zsh.sourceforge.net/FAQ/zshfaq03.html # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags From a9a29dff4f7f2192ba5812e50b4c2efa23103efb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 15 Jul 2016 23:24:14 +0900 Subject: [PATCH 218/658] Fix duplicate rendering of the last line in preview window --- src/ansi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansi.go b/src/ansi.go index debba47..56831ee 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -49,7 +49,7 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo prev := str[idx:offset[0]] output.WriteString(prev) if proc != nil && !proc(prev, state) { - break + return "", nil, nil } newState := interpretCode(str[offset[0]:offset[1]], state) From 6439a138feff689ddf6c81c66fb7ad48d4b31a2a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 16 Jul 2016 00:36:35 +0900 Subject: [PATCH 219/658] [install] Build fzf if prebuilt binary doesn't work Close #617 --- install | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/install b/install index df6570c..f658e1b 100755 --- a/install +++ b/install @@ -9,6 +9,7 @@ auto_completion= key_bindings= update_config=2 binary_arch= +allow_legacy= help() { cat << EOF @@ -37,6 +38,7 @@ for opt in "$@"; do auto_completion=1 key_bindings=1 update_config=1 + allow_legacy=1 ;; --key-bindings) key_bindings=1 ;; --no-key-bindings) key_bindings=0 ;; @@ -165,6 +167,9 @@ case "$archi" in esac install_ruby_fzf() { + if [ -z "$allow_legacy" ]; then + ask "Do you want to install legacy Ruby version instead?" && exit 1 + fi echo "Installing legacy Ruby version ..." # ruby executable @@ -236,26 +241,25 @@ cd "$fzf_base" if [ -n "$binary_error" ]; then if [ $binary_available -eq 0 ]; then echo "No prebuilt binary for $archi ..." - if command -v go > /dev/null; then - echo -n "Building binary (go get -u github.com/junegunn/fzf/src/fzf) ... " - if [ -z "${GOPATH-}" ]; then - export GOPATH="${TMPDIR:-/tmp}/fzf-gopath" - mkdir -p "$GOPATH" - fi - if go get -u github.com/junegunn/fzf/src/fzf; then - echo "OK" - cp "$GOPATH/bin/fzf" "$fzf_base/bin/" - else - echo "Failed to build binary ..." - install_ruby_fzf - fi + else + echo " - $binary_error !!!" + fi + if command -v go > /dev/null; then + echo -n "Building binary (go get -u github.com/junegunn/fzf/src/fzf) ... " + if [ -z "${GOPATH-}" ]; then + export GOPATH="${TMPDIR:-/tmp}/fzf-gopath" + mkdir -p "$GOPATH" + fi + if go get -u github.com/junegunn/fzf/src/fzf; then + echo "OK" + cp "$GOPATH/bin/fzf" "$fzf_base/bin/" else - echo "go executable not found. Cannot build binary ..." + echo "Failed to build binary ..." install_ruby_fzf fi else - echo " - $binary_error !!!" - exit 1 + echo "go executable not found. Cannot build binary ..." + install_ruby_fzf fi fi From fccc93176bbad6658324ffe121775dc2e0d66c07 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 16 Jul 2016 01:06:53 +0900 Subject: [PATCH 220/658] 0.13.3 --- CHANGELOG.md | 4 ++++ install | 4 ++-- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a835931..effb27d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ CHANGELOG ========= +0.13.3 +------ +- Fixed duplicate rendering of the last line in preview window + 0.13.2 ------ - Fixed race condition where preview window is not properly cleared diff --git a/install b/install index f658e1b..d224607 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.13.2 pre=1 || - version=0.13.2 pre=0 +[[ "$@" =~ --pre ]] && version=0.13.3 pre=1 || + version=0.13.3 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 02ce21d..2b6d039 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Jun 2016" "fzf 0.13.2" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Jul 2016" "fzf 0.13.3" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index bdd6d19..500a253 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "Jun 2016" "fzf 0.13.2" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jul 2016" "fzf 0.13.3" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index bc06c05..3dfbf46 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.13.2" + version = "0.13.3" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From a7c9c08371d27b550f3896ab554f6490871a7a0a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 21 Jul 2016 01:46:45 +0900 Subject: [PATCH 221/658] [vim] Make :FZF command configurable with g:fzf_layout To make it consistent with the other commands in fzf.vim --- plugin/fzf.vim | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 76d9cfa..668af7d 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -21,7 +21,7 @@ " OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -let s:default_height = '40%' +let s:default_layout = { 'down': '~40%' } let s:fzf_go = expand(':h:h').'/bin/fzf' let s:install = expand(':h:h').'/install' let s:installed = 0 @@ -481,7 +481,12 @@ function! s:cmd(bang, ...) abort let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g') endif if !a:bang - let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height)) + " For backward compatibility + if !exists('g:fzf_layout') && exists('g:fzf_height') + let opts.down = g:fzf_height + else + let opts = extend(opts, get(g:, 'fzf_layout', s:default_layout)) + endif endif call fzf#run(extend({'options': join(args), 'sink*': function('cmd_callback')}, opts)) endfunction From f787f7e65108938fda981abb71f4fdfb08fd9f31 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 26 Jul 2016 02:35:40 +0900 Subject: [PATCH 222/658] [vim] Add fzf#wrap helper function Close #627 --- README.md | 17 +++++- plugin/fzf.vim | 147 +++++++++++++++++++++++++++++++++---------------- test/fzf.vader | 74 +++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 220a6cb..5de7303 100644 --- a/README.md +++ b/README.md @@ -320,10 +320,10 @@ customization. [fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim) -#### `fzf#run([options])` +#### `fzf#run` -For more advanced uses, you can use `fzf#run()` function with the following -options. +For more advanced uses, you can use `fzf#run([options])` function with the +following options. | Option name | Type | Description | | -------------------------- | ------------- | ---------------------------------------------------------------- | @@ -342,6 +342,17 @@ options. Examples can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Examples-(vim)). +#### `fzf#wrap` + +`fzf#wrap(name string, [opts dict, [fullscreen boolean]])` is a helper +function that decorates the options dictionary so that it understands +`g:fzf_layout`, `g:fzf_action`, and `g:fzf_history_dir` like `:FZF`. + +```vim +command! -bang MyStuff + \ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, 0)) +``` + Tips ---- diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 668af7d..53cb4cb 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -22,6 +22,7 @@ " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. let s:default_layout = { 'down': '~40%' } +let s:layout_keys = ['window', 'up', 'down', 'left', 'right'] let s:fzf_go = expand(':h:h').'/bin/fzf' let s:install = expand(':h:h').'/install' let s:installed = 0 @@ -104,6 +105,101 @@ function! s:warn(msg) echohl None endfunction +function! s:has_any(dict, keys) + for key in a:keys + if has_key(a:dict, key) + return 1 + endif + endfor + return 0 +endfunction + +function! s:open(cmd, target) + if stridx('edit', a:cmd) == 0 && fnamemodify(a:target, ':p') ==# expand('%:p') + return + endif + execute a:cmd s:escape(a:target) +endfunction + +function! s:common_sink(action, lines) abort + if len(a:lines) < 2 + return + endif + let key = remove(a:lines, 0) + let cmd = get(a:action, key, 'e') + if len(a:lines) > 1 + augroup fzf_swap + autocmd SwapExists * let v:swapchoice='o' + \| call s:warn('fzf: E325: swap file exists: '.expand('')) + augroup END + endif + try + let empty = empty(expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified + let autochdir = &autochdir + set noautochdir + for item in a:lines + if empty + execute 'e' s:escape(item) + let empty = 0 + else + call s:open(cmd, item) + endif + if exists('#BufEnter') && isdirectory(item) + doautocmd BufEnter + endif + endfor + finally + let &autochdir = autochdir + silent! autocmd! fzf_swap + endtry +endfunction + +" name string, [opts dict, [fullscreen boolean]] +function! fzf#wrap(name, ...) + if type(a:name) != type('') + throw 'invalid name type: string expected' + endif + let opts = copy(get(a:000, 0, {})) + let bang = get(a:000, 1, 0) + + " Layout: g:fzf_layout (and deprecated g:fzf_height) + if bang + for key in s:layout_keys + if has_key(opts, key) + call remove(opts, key) + endif + endfor + elseif !s:has_any(opts, s:layout_keys) + if !exists('g:fzf_layout') && exists('g:fzf_height') + let opts.down = g:fzf_height + else + let opts = extend(opts, get(g:, 'fzf_layout', s:default_layout)) + endif + endif + + " History: g:fzf_history_dir + let opts.options = get(opts, 'options', '') + if len(get(g:, 'fzf_history_dir', '')) + let dir = expand(g:fzf_history_dir) + if !isdirectory(dir) + call mkdir(dir, 'p') + endif + let opts.options = join(['--history', s:escape(dir.'/'.a:name), opts.options]) + endif + + " Action: g:fzf_action + if !s:has_any(opts, ['sink', 'sink*']) + let opts._action = get(g:, 'fzf_action', s:default_action) + let opts.options .= ' --expect='.join(keys(opts._action), ',') + function! opts.sink(lines) abort + return s:common_sink(self._action, a:lines) + endfunction + let opts['sink*'] = remove(opts, 'sink') + endif + + return opts +endfunction + function! fzf#run(...) abort try let oshell = &shell @@ -137,7 +233,7 @@ try call writefile(source, temps.input) let prefix = 'cat '.s:shellesc(temps.input).'|' else - throw 'Invalid source type' + throw 'invalid source type' endif else let prefix = '' @@ -435,60 +531,17 @@ function! s:callback(dict, lines) abort endfunction let s:default_action = { - \ 'ctrl-m': 'e', \ 'ctrl-t': 'tab split', \ 'ctrl-x': 'split', \ 'ctrl-v': 'vsplit' } -function! s:cmd_callback(lines) abort - if empty(a:lines) - return - endif - let key = remove(a:lines, 0) - let cmd = get(s:action, key, 'e') - if len(a:lines) > 1 - augroup fzf_swap - autocmd SwapExists * let v:swapchoice='o' - \| call s:warn('fzf: E325: swap file exists: '.expand('')) - augroup END - endif - try - let empty = empty(expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified - let autochdir = &autochdir - set noautochdir - for item in a:lines - if empty - execute 'e' s:escape(item) - let empty = 0 - else - execute cmd s:escape(item) - endif - if exists('#BufEnter') && isdirectory(item) - doautocmd BufEnter - endif - endfor - finally - let &autochdir = autochdir - silent! autocmd! fzf_swap - endtry -endfunction - function! s:cmd(bang, ...) abort - let s:action = get(g:, 'fzf_action', s:default_action) - let args = extend(['--expect='.join(keys(s:action), ',')], a:000) + let args = copy(a:000) let opts = {} - if len(args) > 0 && isdirectory(expand(args[-1])) + if len(args) && isdirectory(expand(args[-1])) let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g') endif - if !a:bang - " For backward compatibility - if !exists('g:fzf_layout') && exists('g:fzf_height') - let opts.down = g:fzf_height - else - let opts = extend(opts, get(g:, 'fzf_layout', s:default_layout)) - endif - endif - call fzf#run(extend({'options': join(args), 'sink*': function('cmd_callback')}, opts)) + call fzf#run(fzf#wrap('FZF', extend({'options': join(args)}, opts), a:bang)) endfunction command! -nargs=* -complete=dir -bang FZF call s:cmd(0, ) diff --git a/test/fzf.vader b/test/fzf.vader index 47f2bfc..bab5c16 100644 --- a/test/fzf.vader +++ b/test/fzf.vader @@ -1,5 +1,6 @@ Execute (Setup): let g:dir = fnamemodify(g:vader_file, ':p:h') + unlet! g:fzf_layout g:fzf_action g:fzf_history_dir Log 'Test directory: ' . g:dir Save &acd @@ -69,6 +70,79 @@ Execute (fzf#run with dir option and autochdir when final cwd is same as dir): " Working directory changed due to &acd AssertEqual '/', getcwd() +Execute (fzf#wrap): + AssertThrows fzf#wrap({'foo': 'bar'}) + + let opts = fzf#wrap('foobar') + Log opts + AssertEqual '~40%', opts.down + Assert opts.options =~ '--expect=' + Assert !has_key(opts, 'sink') + Assert has_key(opts, 'sink*') + + let opts = fzf#wrap('foobar', {}, 0) + Log opts + AssertEqual '~40%', opts.down + + let opts = fzf#wrap('foobar', {}, 1) + Log opts + Assert !has_key(opts, 'down') + + let opts = fzf#wrap('foobar', {'down': '50%'}) + Log opts + AssertEqual '50%', opts.down + + let opts = fzf#wrap('foobar', {'down': '50%'}, 1) + Log opts + Assert !has_key(opts, 'down') + + let opts = fzf#wrap('foobar', {'sink': 'e'}) + Log opts + AssertEqual 'e', opts.sink + Assert !has_key(opts, 'sink*') + + let opts = fzf#wrap('foobar', {'options': '--reverse'}) + Log opts + Assert opts.options =~ '--expect=' + Assert opts.options =~ '--reverse' + + let g:fzf_layout = {'window': 'enew'} + let opts = fzf#wrap('foobar') + Log opts + AssertEqual 'enew', opts.window + + let opts = fzf#wrap('foobar', {}, 1) + Log opts + Assert !has_key(opts, 'window') + + let opts = fzf#wrap('foobar', {'right': '50%'}) + Log opts + Assert !has_key(opts, 'window') + AssertEqual '50%', opts.right + + let opts = fzf#wrap('foobar', {'right': '50%'}, 1) + Log opts + Assert !has_key(opts, 'window') + Assert !has_key(opts, 'right') + + let g:fzf_action = {'a': 'tabe'} + let opts = fzf#wrap('foobar') + Log opts + Assert opts.options =~ '--expect=a' + Assert !has_key(opts, 'sink') + Assert has_key(opts, 'sink*') + + let opts = fzf#wrap('foobar', {'sink': 'e'}) + Log opts + AssertEqual 'e', opts.sink + Assert !has_key(opts, 'sink*') + + let g:fzf_history_dir = '/tmp' + let opts = fzf#wrap('foobar', {'options': '--color light'}) + Log opts + Assert opts.options =~ '--history /tmp/foobar' + Assert opts.options =~ '--color light' + Execute (Cleanup): unlet g:dir Restore From d6a99c0391b3859c5db9a0072b366caaf3278f18 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 28 Jul 2016 01:41:11 +0900 Subject: [PATCH 223/658] [vim] v:shell_error can change around redraw! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Patch suggested by Mariusz Atamańczuk --- plugin/fzf.vim | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 53cb4cb..1dc29dc 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -348,8 +348,9 @@ function! s:execute(dict, command, temps) abort let command = escaped endif execute 'silent !'.command + let exit_status = v:shell_error redraw! - return s:exit_handler(v:shell_error, command) ? s:collect(a:temps) : [] + return s:exit_handler(exit_status, command) ? s:collect(a:temps) : [] endfunction function! s:execute_tmux(dict, command, temps) abort @@ -360,8 +361,9 @@ function! s:execute_tmux(dict, command, temps) abort endif call system(command) + let exit_status = v:shell_error redraw! - return s:exit_handler(v:shell_error, command) ? s:collect(a:temps) : [] + return s:exit_handler(exit_status, command) ? s:collect(a:temps) : [] endfunction function! s:calc_size(max, val, dict) From 27444d6b1e17112e9241e85db07154f10d79cef8 Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Thu, 28 Jul 2016 14:03:27 +0100 Subject: [PATCH 224/658] Remove `name` option from `termopen`. `termopen` no longer accepts a `name` option, instead we should suffix the command with `;#NAME`. --- plugin/fzf.vim | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 1dc29dc..bf65cd0 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -204,7 +204,7 @@ function! fzf#run(...) abort try let oshell = &shell set shell=sh - if has('nvim') && bufexists('term://*:FZF') + if has('nvim') && len(filter(range(1, bufnr('$')), 'bufname(v:val) =~# ";#FZF"')) call s:warn('FZF is already running!') return [] endif @@ -424,7 +424,7 @@ endfunction function! s:execute_term(dict, command, temps) abort let [ppos, winopts] = s:split(a:dict) let fzf = { 'buf': bufnr('%'), 'ppos': ppos, 'dict': a:dict, 'temps': a:temps, - \ 'name': 'FZF', 'winopts': winopts, 'command': a:command } + \ 'winopts': winopts, 'command': a:command } function! fzf.switch_back(inplace) if a:inplace && bufnr('') == self.buf " FIXME: Can't re-enter normal mode from terminal mode @@ -466,7 +466,7 @@ function! s:execute_term(dict, command, temps) abort if s:present(a:dict, 'dir') execute 'lcd' s:escape(a:dict.dir) endif - call termopen(a:command, fzf) + call termopen(a:command . ';#FZF', fzf) finally if s:present(a:dict, 'dir') lcd - @@ -550,4 +550,3 @@ command! -nargs=* -complete=dir -bang FZF call s:cmd(0, ) let &cpo = s:cpo_save unlet s:cpo_save - From 42e4992f0685b7f304d035b549beef10119f44fd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 2 Aug 2016 02:25:02 +0900 Subject: [PATCH 225/658] [vim] Make sure to delete fzf buffer Close junegunn/fzf.vim#173 and #630 --- plugin/fzf.vim | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index bf65cd0..dc18470 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -456,6 +456,10 @@ function! s:execute_term(dict, command, temps) abort return endif + if bufexists(self.buf) + execute 'bd!' self.buf + endif + call s:pushd(self.dict) let lines = s:collect(self.temps) call s:callback(self.dict, lines) From 7cef92fffe4bf020fba08cdc8ad50b137e7b7974 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 2 Aug 2016 03:30:17 +0900 Subject: [PATCH 226/658] [vim] Delete fzf buffer even when exit status is non-zero Fix #183 --- plugin/fzf.vim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index dc18470..b7e7dc4 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -452,14 +452,14 @@ function! s:execute_term(dict, command, temps) abort execute self.ppos.win.'wincmd w' endif - if !s:exit_handler(a:code, self.command, 1) - return - endif - if bufexists(self.buf) execute 'bd!' self.buf endif + if !s:exit_handler(a:code, self.command, 1) + return + endif + call s:pushd(self.dict) let lines = s:collect(self.temps) call s:callback(self.dict, lines) From 1e74dbb93764e46d71199a44ae12d4b82bc0c0b2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 12 Aug 2016 01:16:59 +0900 Subject: [PATCH 227/658] :hidden property of previous --preview-window should be cleared Fix #636. Patch suggested by @edi9999. --- src/options.go | 1 + src/options_test.go | 42 +++++++++++++++++++++++++++++++++++------- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/options.go b/src/options.go index 2a72427..31ad497 100644 --- a/src/options.go +++ b/src/options.go @@ -724,6 +724,7 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec { func parsePreviewWindow(opts *previewOpts, input string) { layout := input + opts.hidden = false if strings.HasSuffix(layout, ":hidden") { opts.hidden = true layout = strings.TrimSuffix(layout, ":hidden") diff --git a/src/options_test.go b/src/options_test.go index f3e62f8..eb1dfa9 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -352,14 +352,14 @@ func TestDefaultCtrlNP(t *testing.T) { check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) } -func TestToggle(t *testing.T) { - optsFor := func(words ...string) *Options { - opts := defaultOptions() - parseOptions(opts, words) - postProcessOptions(opts) - return opts - } +func optsFor(words ...string) *Options { + opts := defaultOptions() + parseOptions(opts, words) + postProcessOptions(opts) + return opts +} +func TestToggle(t *testing.T) { opts := optsFor() if opts.ToggleSort { t.Error() @@ -375,3 +375,31 @@ func TestToggle(t *testing.T) { t.Error() } } + +func TestPreviewOpts(t *testing.T) { + opts := optsFor() + if !(opts.Preview.command == "" && + opts.Preview.hidden == false && + opts.Preview.position == posRight && + opts.Preview.size.percent == true && + opts.Preview.size.size == 50) { + t.Error() + } + opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden") + if !(opts.Preview.command == "cat {}" && + opts.Preview.hidden == true && + opts.Preview.position == posLeft && + opts.Preview.size.percent == false && + opts.Preview.size.size == 15+2) { + t.Error(opts.Preview) + } + + opts = optsFor("--preview-window=left:15:hidden", "--preview-window=down") + if !(opts.Preview.command == "" && + opts.Preview.hidden == false && + opts.Preview.position == posDown && + opts.Preview.size.percent == true && + opts.Preview.size.size == 50) { + t.Error(opts.Preview) + } +} From 822b86942c4ffb0dbf7fd096584d2970675f3ebc Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 13 Aug 2016 19:26:36 +0900 Subject: [PATCH 228/658] [test] Clear environment variables --- test/test_go.rb | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index d66d39f..00b9c88 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -36,6 +36,10 @@ end class Shell class << self + def unsets + 'unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS FZF_ALT_C_COMMAND FZF_ALT_C_OPTS FZF_CTRL_R_OPTS;' + end + def bash 'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.bash' end @@ -45,6 +49,10 @@ class Shell FileUtils.cp File.expand_path('~/.fzf.zsh'), '/tmp/fzf-zsh/.zshrc' 'PS1= PROMPT_COMMAND= HISTSIZE=100 ZDOTDIR=/tmp/fzf-zsh zsh' end + + def fish + 'fish' + end end end @@ -57,11 +65,11 @@ class Tmux @win = case shell when :bash - go("new-window -d -P -F '#I' '#{Shell.bash}'").first + go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.bash}'").first when :zsh - go("new-window -d -P -F '#I' '#{Shell.zsh}'").first + go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.zsh}'").first when :fish - go("new-window -d -P -F '#I' 'fish'").first + go("new-window -d -P -F '#I' '#{Shell.unsets + Shell.fish}'").first else raise "Unknown shell: #{shell}" end @@ -153,12 +161,6 @@ class TestBase < Minitest::Test @temp_suffix].join '-' end - def setup - ENV.delete 'FZF_DEFAULT_OPTS' - ENV.delete 'FZF_CTRL_T_COMMAND' - ENV.delete 'FZF_DEFAULT_COMMAND' - end - def readonce wait { File.exists?(tempname) } File.read(tempname) From 1d4057c20907b7d263d6f2b8cb4350a024859dfe Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Aug 2016 00:39:44 +0900 Subject: [PATCH 229/658] [perf] Avoid allocating rune array for ascii string In the best case (all ascii), this reduces the memory footprint by 60% and the response time by 15% to 20%. In the worst case (every line has non-ascii characters), 3 to 4% overhead is observed. --- src/algo/algo.go | 66 +++++++++++++----------- src/algo/algo_test.go | 6 ++- src/chunklist_test.go | 8 +-- src/core.go | 35 ++++++------- src/item.go | 15 +++--- src/item_test.go | 13 ++--- src/merger_test.go | 4 +- src/options_test.go | 21 ++++---- src/pattern.go | 8 +-- src/pattern_test.go | 15 +++--- src/terminal.go | 6 +-- src/tokenizer.go | 42 +++++++-------- src/tokenizer_test.go | 44 ++++++++-------- src/util/chars.go | 113 +++++++++++++++++++++++++++++++++++++++++ src/util/chars_test.go | 36 +++++++++++++ src/util/util.go | 29 ----------- 16 files changed, 303 insertions(+), 158 deletions(-) create mode 100644 src/util/chars.go create mode 100644 src/util/chars_test.go diff --git a/src/algo/algo.go b/src/algo/algo.go index d8e2fec..9bf476f 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -15,11 +15,18 @@ import ( * In short: They try to do as little work as possible. */ -func runeAt(runes []rune, index int, max int, forward bool) rune { +func indexAt(index int, max int, forward bool) int { if forward { - return runes[index] + return index } - return runes[max-index-1] + return max - index - 1 +} + +func runeAt(text util.Chars, index int, max int, forward bool) rune { + if forward { + return text.Get(index) + } + return text.Get(max - index - 1) } // Result conatins the results of running a match function. @@ -42,14 +49,14 @@ const ( charNumber ) -func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, eidx int) int32 { +func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int) int32 { var bonus int32 pidx := 0 lenPattern := len(pattern) consecutive := false prevClass := charNonWord for index := 0; index < eidx; index++ { - char := runes[index] + char := text.Get(index) var class charClass if unicode.IsLower(char) { class = charLower @@ -107,7 +114,7 @@ func evaluateBonus(caseSensitive bool, runes []rune, pattern []rune, sidx int, e } // FuzzyMatch performs fuzzy-match -func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { +func FuzzyMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result { if len(pattern) == 0 { return Result{0, 0, 0} } @@ -125,11 +132,11 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) sidx := -1 eidx := -1 - lenRunes := len(runes) + lenRunes := text.Length() lenPattern := len(pattern) - for index := range runes { - char := runeAt(runes, index, lenRunes, forward) + for index := 0; index < lenRunes; index++ { + char := runeAt(text, index, lenRunes, forward) // This is considerably faster than blindly applying strings.ToLower to the // whole string if !caseSensitive { @@ -142,7 +149,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) char = unicode.To(unicode.LowerCase, char) } } - pchar := runeAt(pattern, pidx, lenPattern, forward) + pchar := pattern[indexAt(pidx, lenPattern, forward)] if char == pchar { if sidx < 0 { sidx = index @@ -157,7 +164,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) if sidx >= 0 && eidx >= 0 { pidx-- for index := eidx - 1; index >= sidx; index-- { - char := runeAt(runes, index, lenRunes, forward) + char := runeAt(text, index, lenRunes, forward) if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 @@ -166,7 +173,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) } } - pchar := runeAt(pattern, pidx, lenPattern, forward) + pchar := pattern[indexAt(pidx, lenPattern, forward)] if char == pchar { if pidx--; pidx < 0 { sidx = index @@ -182,7 +189,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) } return Result{int32(sidx), int32(eidx), - evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} + evaluateBonus(caseSensitive, text, pattern, sidx, eidx)} } return Result{-1, -1, 0} } @@ -194,12 +201,12 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) // // We might try to implement better algorithms in the future: // http://en.wikipedia.org/wiki/String_searching_algorithm -func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { +func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result { if len(pattern) == 0 { return Result{0, 0, 0} } - lenRunes := len(runes) + lenRunes := text.Length() lenPattern := len(pattern) if lenRunes < lenPattern { @@ -208,7 +215,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r pidx := 0 for index := 0; index < lenRunes; index++ { - char := runeAt(runes, index, lenRunes, forward) + char := runeAt(text, index, lenRunes, forward) if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 @@ -216,7 +223,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r char = unicode.To(unicode.LowerCase, char) } } - pchar := runeAt(pattern, pidx, lenPattern, forward) + pchar := pattern[indexAt(pidx, lenPattern, forward)] if pchar == char { pidx++ if pidx == lenPattern { @@ -229,7 +236,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r eidx = lenRunes - (index - lenPattern + 1) } return Result{int32(sidx), int32(eidx), - evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} + evaluateBonus(caseSensitive, text, pattern, sidx, eidx)} } } else { index -= pidx @@ -240,13 +247,13 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r } // PrefixMatch performs prefix-match -func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { - if len(runes) < len(pattern) { +func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result { + if text.Length() < len(pattern) { return Result{-1, -1, 0} } for index, r := range pattern { - char := runes[index] + char := text.Get(index) if !caseSensitive { char = unicode.ToLower(char) } @@ -256,20 +263,19 @@ func PrefixMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) } lenPattern := len(pattern) return Result{0, int32(lenPattern), - evaluateBonus(caseSensitive, runes, pattern, 0, lenPattern)} + evaluateBonus(caseSensitive, text, pattern, 0, lenPattern)} } // SuffixMatch performs suffix-match -func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) Result { - runes := util.TrimRight(input) - trimmedLen := len(runes) +func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result { + trimmedLen := text.Length() - text.TrailingWhitespaces() diff := trimmedLen - len(pattern) if diff < 0 { return Result{-1, -1, 0} } for index, r := range pattern { - char := runes[index+diff] + char := text.Get(index + diff) if !caseSensitive { char = unicode.ToLower(char) } @@ -281,16 +287,16 @@ func SuffixMatch(caseSensitive bool, forward bool, input []rune, pattern []rune) sidx := trimmedLen - lenPattern eidx := trimmedLen return Result{int32(sidx), int32(eidx), - evaluateBonus(caseSensitive, runes, pattern, sidx, eidx)} + evaluateBonus(caseSensitive, text, pattern, sidx, eidx)} } // EqualMatch performs equal-match -func EqualMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) Result { +func EqualMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result { // Note: EqualMatch always return a zero bonus. - if len(runes) != len(pattern) { + if text.Length() != len(pattern) { return Result{-1, -1, 0} } - runesStr := string(runes) + runesStr := text.ToString() if !caseSensitive { runesStr = strings.ToLower(runesStr) } diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index 3c95458..d6a5d48 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -3,13 +3,15 @@ package algo import ( "strings" "testing" + + "github.com/junegunn/fzf/src/util" ) -func assertMatch(t *testing.T, fun func(bool, bool, []rune, []rune) Result, caseSensitive, forward bool, input, pattern string, sidx int32, eidx int32, bonus int32) { +func assertMatch(t *testing.T, fun func(bool, bool, util.Chars, []rune) Result, caseSensitive, forward bool, input, pattern string, sidx int32, eidx int32, bonus int32) { if !caseSensitive { pattern = strings.ToLower(pattern) } - res := fun(caseSensitive, forward, []rune(input), []rune(pattern)) + res := fun(caseSensitive, forward, util.RunesToChars([]rune(input)), []rune(pattern)) if res.Start != sidx { t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", res.Start, sidx, input, pattern) } diff --git a/src/chunklist_test.go b/src/chunklist_test.go index 5f7481d..2523675 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -3,6 +3,8 @@ package fzf import ( "fmt" "testing" + + "github.com/junegunn/fzf/src/util" ) func TestChunkList(t *testing.T) { @@ -10,7 +12,7 @@ func TestChunkList(t *testing.T) { sortCriteria = []criterion{byMatchLen, byLength} cl := NewChunkList(func(s []byte, i int) *Item { - return &Item{text: []rune(string(s)), rank: buildEmptyRank(int32(i * 2))} + return &Item{text: util.ToChars(s), rank: buildEmptyRank(int32(i * 2))} }) // Snapshot @@ -42,8 +44,8 @@ func TestChunkList(t *testing.T) { last := func(arr [5]int32) int32 { return arr[len(arr)-1] } - if string((*chunk1)[0].text) != "hello" || last((*chunk1)[0].rank) != 0 || - string((*chunk1)[1].text) != "world" || last((*chunk1)[1].rank) != 2 { + if (*chunk1)[0].text.ToString() != "hello" || last((*chunk1)[0].rank) != 0 || + (*chunk1)[1].text.ToString() != "world" || last((*chunk1)[1].rank) != 2 { t.Error("Invalid data") } if chunk1.IsFull() { diff --git a/src/core.go b/src/core.go index fd4bc6c..3df15ed 100644 --- a/src/core.go +++ b/src/core.go @@ -63,29 +63,29 @@ func Run(opts *Options) { eventBox := util.NewEventBox() // ANSI code processor - ansiProcessor := func(data []byte) ([]rune, []ansiOffset) { - return util.BytesToRunes(data), nil + ansiProcessor := func(data []byte) (util.Chars, []ansiOffset) { + return util.ToChars(data), nil } - ansiProcessorRunes := func(data []rune) ([]rune, []ansiOffset) { - return data, nil + ansiProcessorRunes := func(data []rune) (util.Chars, []ansiOffset) { + return util.RunesToChars(data), nil } if opts.Ansi { if opts.Theme != nil { var state *ansiState - ansiProcessor = func(data []byte) ([]rune, []ansiOffset) { + ansiProcessor = func(data []byte) (util.Chars, []ansiOffset) { trimmed, offsets, newState := extractColor(string(data), state, nil) state = newState - return []rune(trimmed), offsets + return util.RunesToChars([]rune(trimmed)), offsets } } else { // When color is disabled but ansi option is given, // we simply strip out ANSI codes from the input - ansiProcessor = func(data []byte) ([]rune, []ansiOffset) { + ansiProcessor = func(data []byte) (util.Chars, []ansiOffset) { trimmed, _, _ := extractColor(string(data), nil, nil) - return []rune(trimmed), nil + return util.RunesToChars([]rune(trimmed)), nil } } - ansiProcessorRunes = func(data []rune) ([]rune, []ansiOffset) { + ansiProcessorRunes = func(data []rune) (util.Chars, []ansiOffset) { return ansiProcessor([]byte(string(data))) } } @@ -100,29 +100,30 @@ func Run(opts *Options) { eventBox.Set(EvtHeader, header) return nil } - runes, colors := ansiProcessor(data) + chars, colors := ansiProcessor(data) return &Item{ - text: runes, + text: chars, colors: colors, rank: buildEmptyRank(int32(index))} }) } else { chunkList = NewChunkList(func(data []byte, index int) *Item { - runes := util.BytesToRunes(data) - tokens := Tokenize(runes, opts.Delimiter) + chars := util.ToChars(data) + tokens := Tokenize(chars, opts.Delimiter) trans := Transform(tokens, opts.WithNth) if len(header) < opts.HeaderLines { header = append(header, string(joinTokens(trans))) eventBox.Set(EvtHeader, header) return nil } + textRunes := joinTokens(trans) item := Item{ - text: joinTokens(trans), - origText: &runes, + text: util.RunesToChars(textRunes), + origText: &data, colors: nil, rank: buildEmptyRank(int32(index))} - trimmed, colors := ansiProcessorRunes(item.text) + trimmed, colors := ansiProcessorRunes(textRunes) item.text = trimmed item.colors = colors return &item @@ -170,7 +171,7 @@ func Run(opts *Options) { func(runes []byte) bool { item := chunkList.trans(runes, 0) if item != nil && pattern.MatchItem(item) { - fmt.Println(string(item.text)) + fmt.Println(item.text.ToString()) found = true } return false diff --git a/src/item.go b/src/item.go index 0641350..36f8c0a 100644 --- a/src/item.go +++ b/src/item.go @@ -4,6 +4,7 @@ import ( "math" "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/util" ) // Offset holds three 32-bit integers denoting the offsets of a matched substring @@ -17,8 +18,8 @@ type colorOffset struct { // Item represents each input line type Item struct { - text []rune - origText *[]rune + text util.Chars + origText *[]byte transformed []Token offsets []Offset colors []ansiOffset @@ -91,12 +92,14 @@ func (item *Item) Rank(cache bool) [5]int32 { // If offsets is empty, lenSum will be 0, but we don't care val = int32(lenSum) } else { - val = int32(len(item.text)) + val = int32(item.text.Length()) } case byBegin: // We can't just look at item.offsets[0][0] because it can be an inverse term whitePrefixLen := 0 - for idx, r := range item.text { + numChars := item.text.Length() + for idx := 0; idx < numChars; idx++ { + r := item.text.Get(idx) whitePrefixLen = idx if idx == minBegin || r != ' ' && r != '\t' { break @@ -105,7 +108,7 @@ func (item *Item) Rank(cache bool) [5]int32 { val = int32(minBegin - whitePrefixLen) case byEnd: if prevEnd > 0 { - val = int32(1 + len(item.text) - prevEnd) + val = int32(1 + item.text.Length() - prevEnd) } else { // Empty offsets due to inverse terms. val = 1 @@ -134,7 +137,7 @@ func (item *Item) StringPtr(stripAnsi bool) *string { orig := string(*item.origText) return &orig } - str := string(item.text) + str := item.text.ToString() return &str } diff --git a/src/item_test.go b/src/item_test.go index d1c30d7..36a436d 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/util" ) func TestOffsetSort(t *testing.T) { @@ -44,13 +45,13 @@ func TestItemRank(t *testing.T) { sortCriteria = []criterion{byMatchLen, byLength} strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} - item1 := Item{text: strs[0], offsets: []Offset{}, rank: [5]int32{0, 0, 0, 0, 1}} + item1 := Item{text: util.RunesToChars(strs[0]), offsets: []Offset{}, rank: [5]int32{0, 0, 0, 0, 1}} rank1 := item1.Rank(true) if rank1[0] != math.MaxInt32 || rank1[1] != 3 || rank1[4] != 1 { t.Error(item1.Rank(true)) } // Only differ in index - item2 := Item{text: strs[0], offsets: []Offset{}} + item2 := Item{text: util.RunesToChars(strs[0]), offsets: []Offset{}} items := []*Item{&item1, &item2} sort.Sort(ByRelevance(items)) @@ -66,10 +67,10 @@ func TestItemRank(t *testing.T) { } // Sort by relevance - item3 := Item{text: strs[1], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} - item4 := Item{text: strs[1], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} - item5 := Item{text: strs[2], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} - item6 := Item{text: strs[2], rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + item3 := Item{text: util.RunesToChars(strs[1]), rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item4 := Item{text: util.RunesToChars(strs[1]), rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} + item5 := Item{text: util.RunesToChars(strs[2]), rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} + item6 := Item{text: util.RunesToChars(strs[2]), rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6} sort.Sort(ByRelevance(items)) if items[0] != &item6 || items[1] != &item4 || diff --git a/src/merger_test.go b/src/merger_test.go index 472e204..f62f975 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -5,6 +5,8 @@ import ( "math/rand" "sort" "testing" + + "github.com/junegunn/fzf/src/util" ) func assert(t *testing.T, cond bool, msg ...string) { @@ -22,7 +24,7 @@ func randItem() *Item { offsets[idx] = Offset{sidx, eidx} } return &Item{ - text: []rune(str), + text: util.RunesToChars([]rune(str)), rank: buildEmptyRank(rand.Int31()), offsets: offsets} } diff --git a/src/options_test.go b/src/options_test.go index eb1dfa9..c1bc914 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/util" ) func TestDelimiterRegex(t *testing.T) { @@ -42,24 +43,24 @@ func TestDelimiterRegex(t *testing.T) { func TestDelimiterRegexString(t *testing.T) { delim := delimiterRegexp("*") - tokens := Tokenize([]rune("-*--*---**---"), delim) + tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim) if delim.regex != nil || - string(tokens[0].text) != "-*" || - string(tokens[1].text) != "--*" || - string(tokens[2].text) != "---*" || - string(tokens[3].text) != "*" || - string(tokens[4].text) != "---" { + tokens[0].text.ToString() != "-*" || + tokens[1].text.ToString() != "--*" || + tokens[2].text.ToString() != "---*" || + tokens[3].text.ToString() != "*" || + tokens[4].text.ToString() != "---" { t.Errorf("%s %s %d", delim, tokens, len(tokens)) } } func TestDelimiterRegexRegex(t *testing.T) { delim := delimiterRegexp("--\\*") - tokens := Tokenize([]rune("-*--*---**---"), delim) + tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim) if delim.str != nil || - string(tokens[0].text) != "-*--*" || - string(tokens[1].text) != "---*" || - string(tokens[2].text) != "*---" { + tokens[0].text.ToString() != "-*--*" || + tokens[1].text.ToString() != "---*" || + tokens[2].text.ToString() != "*---" { t.Errorf("%s %d", tokens, len(tokens)) } } diff --git a/src/pattern.go b/src/pattern.go index 42a341b..2df2879 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -49,7 +49,7 @@ type Pattern struct { cacheable bool delimiter Delimiter nth []Range - procFun map[termType]func(bool, bool, []rune, []rune) algo.Result + procFun map[termType]func(bool, bool, util.Chars, []rune) algo.Result } var ( @@ -125,7 +125,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool, cacheable: cacheable, nth: nth, delimiter: delimiter, - procFun: make(map[termType]func(bool, bool, []rune, []rune) algo.Result)} + procFun: make(map[termType]func(bool, bool, util.Chars, []rune) algo.Result)} ptr.procFun[termFuzzy] = algo.FuzzyMatch ptr.procFun[termEqual] = algo.EqualMatch @@ -361,13 +361,13 @@ func (p *Pattern) prepareInput(item *Item) []Token { tokens := Tokenize(item.text, p.delimiter) ret = Transform(tokens, p.nth) } else { - ret = []Token{Token{text: item.text, prefixLength: 0, trimLength: util.TrimLen(item.text)}} + ret = []Token{Token{text: item.text, prefixLength: 0, trimLength: item.text.TrimLength()}} } item.transformed = ret return ret } -func (p *Pattern) iter(pfun func(bool, bool, []rune, []rune) algo.Result, +func (p *Pattern) iter(pfun func(bool, bool, util.Chars, []rune) algo.Result, tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int32) { for _, part := range tokens { prefixLength := int32(part.prefixLength) diff --git a/src/pattern_test.go b/src/pattern_test.go index 2f27fda..26f9284 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/junegunn/fzf/src/algo" + "github.com/junegunn/fzf/src/util" ) func TestParseTermsExtended(t *testing.T) { @@ -71,7 +72,7 @@ func TestExact(t *testing.T) { pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("'abc")) res := algo.ExactMatchNaive( - pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.termSets[0][0].text) + pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text) if res.Start != 7 || res.End != 10 { t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) } @@ -84,7 +85,7 @@ func TestEqual(t *testing.T) { match := func(str string, sidxExpected int32, eidxExpected int32) { res := algo.EqualMatch( - pattern.caseSensitive, pattern.forward, []rune(str), pattern.termSets[0][0].text) + pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text) if res.Start != sidxExpected || res.End != eidxExpected { t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) } @@ -120,20 +121,20 @@ func TestCaseSensitivity(t *testing.T) { func TestOrigTextAndTransformed(t *testing.T) { pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg")) - tokens := Tokenize([]rune("junegunn"), Delimiter{}) + tokens := Tokenize(util.RunesToChars([]rune("junegunn")), Delimiter{}) trans := Transform(tokens, []Range{Range{1, 1}}) - origRunes := []rune("junegunn.choi") + origBytes := []byte("junegunn.choi") for _, extended := range []bool{false, true} { chunk := Chunk{ &Item{ - text: []rune("junegunn"), - origText: &origRunes, + text: util.RunesToChars([]rune("junegunn")), + origText: &origBytes, transformed: trans}, } pattern.extended = extended matches := pattern.matchChunk(&chunk) - if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" || + if matches[0].text.ToString() != "junegunn" || string(*matches[0].origText) != "junegunn.choi" || matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || !reflect.DeepEqual(matches[0].transformed, trans) { t.Error("Invalid match result", matches) diff --git a/src/terminal.go b/src/terminal.go index 213d3b0..ff1120a 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -564,7 +564,7 @@ func (t *Terminal) printHeader() { trimmed, colors, newState := extractColor(lineStr, state, nil) state = newState item := &Item{ - text: []rune(trimmed), + text: util.RunesToChars([]rune(trimmed)), colors: colors, rank: buildEmptyRank(0)} @@ -663,8 +663,8 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c } // Overflow - text := make([]rune, len(item.text)) - copy(text, item.text) + text := make([]rune, item.text.Length()) + copy(text, item.text.ToRunes()) offsets := item.colorOffsets(col2, bold, current) maxWidth := t.window.Width - 3 maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) diff --git a/src/tokenizer.go b/src/tokenizer.go index 4b89b38..05b890a 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -18,7 +18,7 @@ type Range struct { // Token contains the tokenized part of the strings and its prefix length type Token struct { - text []rune + text util.Chars prefixLength int trimLength int } @@ -75,15 +75,15 @@ func ParseRange(str *string) (Range, bool) { return newRange(n, n), true } -func withPrefixLengths(tokens [][]rune, begin int) []Token { +func withPrefixLengths(tokens []util.Chars, begin int) []Token { ret := make([]Token, len(tokens)) prefixLength := begin for idx, token := range tokens { // Need to define a new local variable instead of the reused token to take // the pointer to it - ret[idx] = Token{token, prefixLength, util.TrimLen(token)} - prefixLength += len(token) + ret[idx] = Token{token, prefixLength, token.TrimLength()} + prefixLength += token.Length() } return ret } @@ -94,13 +94,15 @@ const ( awkWhite ) -func awkTokenizer(input []rune) ([][]rune, int) { +func awkTokenizer(input util.Chars) ([]util.Chars, int) { // 9, 32 - ret := [][]rune{} + ret := []util.Chars{} str := []rune{} prefixLength := 0 state := awkNil - for _, r := range input { + numChars := input.Length() + for idx := 0; idx < numChars; idx++ { + r := input.Get(idx) white := r == 9 || r == 32 switch state { case awkNil: @@ -119,34 +121,34 @@ func awkTokenizer(input []rune) ([][]rune, int) { if white { str = append(str, r) } else { - ret = append(ret, str) + ret = append(ret, util.RunesToChars(str)) state = awkBlack str = []rune{r} } } } if len(str) > 0 { - ret = append(ret, str) + ret = append(ret, util.RunesToChars(str)) } return ret, prefixLength } // Tokenize tokenizes the given string with the delimiter -func Tokenize(runes []rune, delimiter Delimiter) []Token { +func Tokenize(text util.Chars, delimiter Delimiter) []Token { if delimiter.str == nil && delimiter.regex == nil { // AWK-style (\S+\s*) - tokens, prefixLength := awkTokenizer(runes) + tokens, prefixLength := awkTokenizer(text) return withPrefixLengths(tokens, prefixLength) } var tokens []string if delimiter.str != nil { - tokens = strings.Split(string(runes), *delimiter.str) + tokens = strings.Split(text.ToString(), *delimiter.str) for i := 0; i < len(tokens)-1; i++ { tokens[i] = tokens[i] + *delimiter.str } } else if delimiter.regex != nil { - str := string(runes) + str := text.ToString() for len(str) > 0 { loc := delimiter.regex.FindStringIndex(str) if loc == nil { @@ -157,9 +159,9 @@ func Tokenize(runes []rune, delimiter Delimiter) []Token { str = str[last:] } } - asRunes := make([][]rune, len(tokens)) + asRunes := make([]util.Chars, len(tokens)) for i, token := range tokens { - asRunes[i] = []rune(token) + asRunes[i] = util.RunesToChars([]rune(token)) } return withPrefixLengths(asRunes, 0) } @@ -167,7 +169,7 @@ func Tokenize(runes []rune, delimiter Delimiter) []Token { func joinTokens(tokens []Token) []rune { ret := []rune{} for _, token := range tokens { - ret = append(ret, token.text...) + ret = append(ret, token.text.ToRunes()...) } return ret } @@ -175,7 +177,7 @@ func joinTokens(tokens []Token) []rune { func joinTokensAsRunes(tokens []Token) []rune { ret := []rune{} for _, token := range tokens { - ret = append(ret, token.text...) + ret = append(ret, token.text.ToRunes()...) } return ret } @@ -197,7 +199,7 @@ func Transform(tokens []Token, withNth []Range) []Token { } if idx >= 1 && idx <= numTokens { minIdx = idx - 1 - part = append(part, tokens[idx-1].text...) + part = append(part, tokens[idx-1].text.ToRunes()...) } } } else { @@ -224,7 +226,7 @@ func Transform(tokens []Token, withNth []Range) []Token { minIdx = util.Max(0, begin-1) for idx := begin; idx <= end; idx++ { if idx >= 1 && idx <= numTokens { - part = append(part, tokens[idx-1].text...) + part = append(part, tokens[idx-1].text.ToRunes()...) } } } @@ -234,7 +236,7 @@ func Transform(tokens []Token, withNth []Range) []Token { } else { prefixLength = 0 } - transTokens[idx] = Token{part, prefixLength, util.TrimLen(part)} + transTokens[idx] = Token{util.RunesToChars(part), prefixLength, util.TrimLen(part)} } return transTokens } diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go index b092440..1dd4414 100644 --- a/src/tokenizer_test.go +++ b/src/tokenizer_test.go @@ -1,6 +1,10 @@ package fzf -import "testing" +import ( + "testing" + + "github.com/junegunn/fzf/src/util" +) func TestParseRange(t *testing.T) { { @@ -43,23 +47,23 @@ func TestParseRange(t *testing.T) { func TestTokenize(t *testing.T) { // AWK-style input := " abc: def: ghi " - tokens := Tokenize([]rune(input), Delimiter{}) - if string(tokens[0].text) != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 { + tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{}) + if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 { t.Errorf("%s", tokens) } // With delimiter - tokens = Tokenize([]rune(input), delimiterRegexp(":")) - if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 { + tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":")) + if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 { t.Errorf("%s", tokens) } // With delimiter regex - tokens = Tokenize([]rune(input), delimiterRegexp("\\s+")) - if string(tokens[0].text) != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 || - string(tokens[1].text) != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 || - string(tokens[2].text) != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 || - string(tokens[3].text) != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 { + tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp("\\s+")) + if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 || + tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 || + tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 || + tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 { t.Errorf("%s", tokens) } } @@ -67,7 +71,7 @@ func TestTokenize(t *testing.T) { func TestTransform(t *testing.T) { input := " abc: def: ghi: jkl" { - tokens := Tokenize([]rune(input), Delimiter{}) + tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{}) { ranges := splitNth("1,2,3") tx := Transform(tokens, ranges) @@ -80,25 +84,25 @@ func TestTransform(t *testing.T) { tx := Transform(tokens, ranges) if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " || len(tx) != 4 || - string(tx[0].text) != "abc: def: " || tx[0].prefixLength != 2 || - string(tx[1].text) != "ghi: " || tx[1].prefixLength != 14 || - string(tx[2].text) != "def: ghi: jkl" || tx[2].prefixLength != 8 || - string(tx[3].text) != "abc: " || tx[3].prefixLength != 2 { + tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 || + tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 || + tx[2].text.ToString() != "def: ghi: jkl" || tx[2].prefixLength != 8 || + tx[3].text.ToString() != "abc: " || tx[3].prefixLength != 2 { t.Errorf("%s", tx) } } } { - tokens := Tokenize([]rune(input), delimiterRegexp(":")) + tokens := Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":")) { ranges := splitNth("1..2,3,2..,1") tx := Transform(tokens, ranges) if string(joinTokens(tx)) != " abc: def: ghi: def: ghi: jkl abc:" || len(tx) != 4 || - string(tx[0].text) != " abc: def:" || tx[0].prefixLength != 0 || - string(tx[1].text) != " ghi:" || tx[1].prefixLength != 12 || - string(tx[2].text) != " def: ghi: jkl" || tx[2].prefixLength != 6 || - string(tx[3].text) != " abc:" || tx[3].prefixLength != 0 { + tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 || + tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 || + tx[2].text.ToString() != " def: ghi: jkl" || tx[2].prefixLength != 6 || + tx[3].text.ToString() != " abc:" || tx[3].prefixLength != 0 { t.Errorf("%s", tx) } } diff --git a/src/util/chars.go b/src/util/chars.go new file mode 100644 index 0000000..25a15dd --- /dev/null +++ b/src/util/chars.go @@ -0,0 +1,113 @@ +package util + +import ( + "unicode/utf8" +) + +type Chars struct { + runes []rune + bytes []byte +} + +// ToChars converts byte array into rune array +func ToChars(bytea []byte) Chars { + var runes []rune + ascii := true + numBytes := len(bytea) + for i := 0; i < numBytes; { + if bytea[i] < utf8.RuneSelf { + if !ascii { + runes = append(runes, rune(bytea[i])) + } + i++ + } else { + if ascii { + ascii = false + runes = make([]rune, i, numBytes) + for j := 0; j < i; j++ { + runes[j] = rune(bytea[j]) + } + } + r, sz := utf8.DecodeRune(bytea[i:]) + i += sz + runes = append(runes, r) + } + } + if ascii { + return Chars{bytes: bytea} + } + return Chars{runes: runes} +} + +func RunesToChars(runes []rune) Chars { + return Chars{runes: runes} +} + +func (chars *Chars) Get(i int) rune { + if chars.runes != nil { + return chars.runes[i] + } + return rune(chars.bytes[i]) +} + +func (chars *Chars) Length() int { + if chars.runes != nil { + return len(chars.runes) + } + return len(chars.bytes) +} + +// TrimLength returns the length after trimming leading and trailing whitespaces +func (chars *Chars) TrimLength() int { + var i int + len := chars.Length() + for i = len - 1; i >= 0; i-- { + char := chars.Get(i) + if char != ' ' && char != '\t' { + break + } + } + // Completely empty + if i < 0 { + return 0 + } + + var j int + for j = 0; j < len; j++ { + char := chars.Get(j) + if char != ' ' && char != '\t' { + break + } + } + return i - j + 1 +} + +func (chars *Chars) TrailingWhitespaces() int { + whitespaces := 0 + for i := chars.Length() - 1; i >= 0; i-- { + char := chars.Get(i) + if char != ' ' && char != '\t' { + break + } + whitespaces++ + } + return whitespaces +} + +func (chars *Chars) ToString() string { + if chars.runes != nil { + return string(chars.runes) + } + return string(chars.bytes) +} + +func (chars *Chars) ToRunes() []rune { + if chars.runes != nil { + return chars.runes + } + runes := make([]rune, len(chars.bytes)) + for idx, b := range chars.bytes { + runes[idx] = rune(b) + } + return runes +} diff --git a/src/util/chars_test.go b/src/util/chars_test.go new file mode 100644 index 0000000..e42cfb7 --- /dev/null +++ b/src/util/chars_test.go @@ -0,0 +1,36 @@ +package util + +import "testing" + +func TestToCharsNil(t *testing.T) { + bs := Chars{bytes: []byte{}} + if bs.bytes == nil || bs.runes != nil { + t.Error() + } + rs := RunesToChars([]rune{}) + if rs.bytes != nil || rs.runes == nil { + t.Error() + } +} + +func TestToCharsAscii(t *testing.T) { + chars := ToChars([]byte("foobar")) + if chars.ToString() != "foobar" || chars.runes != nil { + t.Error() + } +} + +func TestCharsLength(t *testing.T) { + chars := ToChars([]byte("\tabc한글 ")) + if chars.Length() != 8 || chars.TrimLength() != 5 { + t.Error() + } +} + +func TestCharsToString(t *testing.T) { + text := "\tabc한글 " + chars := ToChars([]byte(text)) + if chars.ToString() != text { + t.Error() + } +} diff --git a/src/util/util.go b/src/util/util.go index 4f3d409..90cc28b 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -7,7 +7,6 @@ import ( "os" "os/exec" "time" - "unicode/utf8" ) // Max returns the largest integer @@ -84,34 +83,6 @@ func IsTty() bool { return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 } -// TrimRight returns rune array with trailing white spaces cut off -func TrimRight(runes []rune) []rune { - var i int - for i = len(runes) - 1; i >= 0; i-- { - char := runes[i] - if char != ' ' && char != '\t' { - break - } - } - return runes[0 : i+1] -} - -// BytesToRunes converts byte array into rune array -func BytesToRunes(bytea []byte) []rune { - runes := make([]rune, 0, len(bytea)) - for i := 0; i < len(bytea); { - if bytea[i] < utf8.RuneSelf { - runes = append(runes, rune(bytea[i])) - i++ - } else { - r, sz := utf8.DecodeRune(bytea[i:]) - i += sz - runes = append(runes, r) - } - } - return runes -} - // TrimLen returns the length of trimmed rune array func TrimLen(runes []rune) int { var i int From ddc7bb9064042a0d5da9546eaf6ff888dca63f0c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Aug 2016 01:53:06 +0900 Subject: [PATCH 230/658] [perf] Optimize AWK-style tokenizer for --nth Approx. 50% less memory footprint and 40% improvement in query time --- src/tokenizer.go | 44 ++++++++++++++++++++++++++++-------------- src/util/chars.go | 7 +++++++ src/util/chars_test.go | 21 ++++++++++++++++++++ src/util/util.go | 24 ----------------------- src/util/util_test.go | 20 ------------------- 5 files changed, 57 insertions(+), 59 deletions(-) diff --git a/src/tokenizer.go b/src/tokenizer.go index 05b890a..eec1989 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -97,10 +97,11 @@ const ( func awkTokenizer(input util.Chars) ([]util.Chars, int) { // 9, 32 ret := []util.Chars{} - str := []rune{} prefixLength := 0 state := awkNil numChars := input.Length() + begin := 0 + end := 0 for idx := 0; idx < numChars; idx++ { r := input.Get(idx) white := r == 9 || r == 32 @@ -109,26 +110,24 @@ func awkTokenizer(input util.Chars) ([]util.Chars, int) { if white { prefixLength++ } else { - state = awkBlack - str = append(str, r) + state, begin, end = awkBlack, idx, idx+1 } case awkBlack: - str = append(str, r) + end = idx + 1 if white { state = awkWhite } case awkWhite: if white { - str = append(str, r) + end = idx + 1 } else { - ret = append(ret, util.RunesToChars(str)) - state = awkBlack - str = []rune{r} + ret = append(ret, input.Slice(begin, end)) + state, begin, end = awkBlack, idx, idx+1 } } } - if len(str) > 0 { - ret = append(ret, util.RunesToChars(str)) + if begin < end { + ret = append(ret, input.Slice(begin, end)) } return ret, prefixLength } @@ -187,19 +186,19 @@ func Transform(tokens []Token, withNth []Range) []Token { transTokens := make([]Token, len(withNth)) numTokens := len(tokens) for idx, r := range withNth { - part := []rune{} + parts := []util.Chars{} minIdx := 0 if r.begin == r.end { idx := r.begin if idx == rangeEllipsis { - part = append(part, joinTokensAsRunes(tokens)...) + parts = append(parts, util.RunesToChars(joinTokensAsRunes(tokens))) } else { if idx < 0 { idx += numTokens + 1 } if idx >= 1 && idx <= numTokens { minIdx = idx - 1 - part = append(part, tokens[idx-1].text.ToRunes()...) + parts = append(parts, tokens[idx-1].text) } } } else { @@ -226,17 +225,32 @@ func Transform(tokens []Token, withNth []Range) []Token { minIdx = util.Max(0, begin-1) for idx := begin; idx <= end; idx++ { if idx >= 1 && idx <= numTokens { - part = append(part, tokens[idx-1].text.ToRunes()...) + parts = append(parts, tokens[idx-1].text) } } } + // Merge multiple parts + var merged util.Chars + switch len(parts) { + case 0: + merged = util.RunesToChars([]rune{}) + case 1: + merged = parts[0] + default: + runes := []rune{} + for _, part := range parts { + runes = append(runes, part.ToRunes()...) + } + merged = util.RunesToChars(runes) + } + var prefixLength int if minIdx < numTokens { prefixLength = tokens[minIdx].prefixLength } else { prefixLength = 0 } - transTokens[idx] = Token{util.RunesToChars(part), prefixLength, util.TrimLen(part)} + transTokens[idx] = Token{merged, prefixLength, merged.TrimLength()} } return transTokens } diff --git a/src/util/chars.go b/src/util/chars.go index 25a15dd..6034ee5 100644 --- a/src/util/chars.go +++ b/src/util/chars.go @@ -111,3 +111,10 @@ func (chars *Chars) ToRunes() []rune { } return runes } + +func (chars *Chars) Slice(b int, e int) Chars { + if chars.runes != nil { + return Chars{runes: chars.runes[b:e]} + } + return Chars{bytes: chars.bytes[b:e]} +} diff --git a/src/util/chars_test.go b/src/util/chars_test.go index e42cfb7..2cb6fc7 100644 --- a/src/util/chars_test.go +++ b/src/util/chars_test.go @@ -34,3 +34,24 @@ func TestCharsToString(t *testing.T) { t.Error() } } + +func TestTrimLength(t *testing.T) { + check := func(str string, exp int) { + chars := ToChars([]byte(str)) + trimmed := chars.TrimLength() + if trimmed != exp { + t.Errorf("Invalid TrimLength result for '%s': %d (expected %d)", + str, trimmed, exp) + } + } + check("hello", 5) + check("hello ", 5) + check("hello ", 5) + check(" hello", 5) + check(" hello", 5) + check(" hello ", 5) + check(" hello ", 5) + check("h o", 5) + check(" h o ", 5) + check(" ", 0) +} diff --git a/src/util/util.go b/src/util/util.go index 90cc28b..a95340e 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -83,30 +83,6 @@ func IsTty() bool { return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 } -// TrimLen returns the length of trimmed rune array -func TrimLen(runes []rune) int { - var i int - for i = len(runes) - 1; i >= 0; i-- { - char := runes[i] - if char != ' ' && char != '\t' { - break - } - } - // Completely empty - if i < 0 { - return 0 - } - - var j int - for j = 0; j < len(runes); j++ { - char := runes[j] - if char != ' ' && char != '\t' { - break - } - } - return i - j + 1 -} - // ExecCommand executes the given command with $SHELL func ExecCommand(command string) *exec.Cmd { shell := os.Getenv("SHELL") diff --git a/src/util/util_test.go b/src/util/util_test.go index 8aeaeac..06cfd4f 100644 --- a/src/util/util_test.go +++ b/src/util/util_test.go @@ -20,23 +20,3 @@ func TestContrain(t *testing.T) { t.Error("Expected", 3) } } - -func TestTrimLen(t *testing.T) { - check := func(str string, exp int) { - trimmed := TrimLen([]rune(str)) - if trimmed != exp { - t.Errorf("Invalid TrimLen result for '%s': %d (expected %d)", - str, trimmed, exp) - } - } - check("hello", 5) - check("hello ", 5) - check("hello ", 5) - check(" hello", 5) - check(" hello", 5) - check(" hello ", 5) - check(" hello ", 5) - check("h o", 5) - check(" h o ", 5) - check(" ", 0) -} From d9c8a9a880dd9fc28c9fb466bd1c81cd6a8a159c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Aug 2016 04:23:37 +0900 Subject: [PATCH 231/658] [perf] Remove memory copy when using string delimiter --- src/tokenizer.go | 12 ++++++------ src/util/chars.go | 36 ++++++++++++++++++++++++++++++++++++ src/util/chars_test.go | 25 +++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/tokenizer.go b/src/tokenizer.go index eec1989..e48f48b 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -140,13 +140,13 @@ func Tokenize(text util.Chars, delimiter Delimiter) []Token { return withPrefixLengths(tokens, prefixLength) } - var tokens []string if delimiter.str != nil { - tokens = strings.Split(text.ToString(), *delimiter.str) - for i := 0; i < len(tokens)-1; i++ { - tokens[i] = tokens[i] + *delimiter.str - } - } else if delimiter.regex != nil { + return withPrefixLengths(text.Split(*delimiter.str), 0) + } + + // FIXME performance + var tokens []string + if delimiter.regex != nil { str := text.ToString() for len(str) > 0 { loc := delimiter.regex.FindStringIndex(str) diff --git a/src/util/chars.go b/src/util/chars.go index 6034ee5..12417c6 100644 --- a/src/util/chars.go +++ b/src/util/chars.go @@ -118,3 +118,39 @@ func (chars *Chars) Slice(b int, e int) Chars { } return Chars{bytes: chars.bytes[b:e]} } + +func (chars *Chars) Split(delimiter string) []Chars { + delim := []rune(delimiter) + numChars := chars.Length() + numDelim := len(delim) + begin := 0 + ret := make([]Chars, 0, 1) + + for index := 0; index < numChars; { + if index+numDelim <= numChars { + match := true + for off, d := range delim { + if chars.Get(index+off) != d { + match = false + break + } + } + // Found the delimiter + if match { + incr := Max(numDelim, 1) + ret = append(ret, chars.Slice(begin, index+incr)) + index += incr + begin = index + continue + } + } else { + // Impossible to find the delimiter in the remaining substring + break + } + index++ + } + if begin < numChars || len(ret) == 0 { + ret = append(ret, chars.Slice(begin, numChars)) + } + return ret +} diff --git a/src/util/chars_test.go b/src/util/chars_test.go index 2cb6fc7..12c629d 100644 --- a/src/util/chars_test.go +++ b/src/util/chars_test.go @@ -55,3 +55,28 @@ func TestTrimLength(t *testing.T) { check(" h o ", 5) check(" ", 0) } + +func TestSplit(t *testing.T) { + check := func(str string, delim string, tokens ...string) { + input := ToChars([]byte(str)) + result := input.Split(delim) + if len(result) != len(tokens) { + t.Errorf("Invalid Split result for '%s': %d tokens found (expected %d): %s", + str, len(result), len(tokens), result) + } + for idx, token := range tokens { + if result[idx].ToString() != token { + t.Errorf("Invalid Split result for '%s': %s (expected %s)", + str, result[idx].ToString(), token) + } + } + } + check("abc:def::", ":", "abc:", "def:", ":") + check("abc:def::", "-", "abc:def::") + check("abc", "", "a", "b", "c") + check("abc", "a", "a", "bc") + check("abc", "ab", "ab", "c") + check("abc", "abc", "abc") + check("abc", "abcd", "abc") + check("", "abcd", "") +} From 41e916a511f37848168a3cddf7cbd1ea1a9ba8da Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Aug 2016 11:58:47 +0900 Subject: [PATCH 232/658] [perf] evaluateBonus can start from sidx - 1 --- src/algo/algo.go | 2 +- src/util/util.go | 11 ++++------- src/util/util_test.go | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 9bf476f..63469c9 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -55,7 +55,7 @@ func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int lenPattern := len(pattern) consecutive := false prevClass := charNonWord - for index := 0; index < eidx; index++ { + for index := util.Max(0, sidx-1); index < eidx; index++ { char := text.Get(index) var class charClass if unicode.IsLower(char) { diff --git a/src/util/util.go b/src/util/util.go index a95340e..113fee9 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -10,14 +10,11 @@ import ( ) // Max returns the largest integer -func Max(first int, items ...int) int { - max := first - for _, item := range items { - if item > max { - max = item - } +func Max(first int, second int) int { + if first >= second { + return first } - return max + return second } // Min returns the smallest integer diff --git a/src/util/util_test.go b/src/util/util_test.go index 06cfd4f..d6a03d9 100644 --- a/src/util/util_test.go +++ b/src/util/util_test.go @@ -3,7 +3,7 @@ package util import "testing" func TestMax(t *testing.T) { - if Max(-2, 5, 1, 4, 3) != 5 { + if Max(-2, 5) != 5 { t.Error("Invalid result") } } From 8df7d962e60bce44adda674ef8f99a2a76a4a6e7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Aug 2016 17:44:11 +0900 Subject: [PATCH 233/658] Improve rendering time of long lines --- src/terminal.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index ff1120a..b6b6156 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -656,6 +656,17 @@ func trimLeft(runes []rune, width int) ([]rune, int32) { return runes, trimmed } +func overflow(runes []rune, max int) bool { + l := 0 + for _, r := range runes { + l += runeWidth(r, l) + if l > max { + return true + } + } + return false +} + func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) { var maxe int for _, offset := range item.offsets { @@ -668,17 +679,15 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c offsets := item.colorOffsets(col2, bold, current) maxWidth := t.window.Width - 3 maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) - fullWidth := displayWidth(text) - if fullWidth > maxWidth { + if overflow(text, maxWidth) { if t.hscroll { // Stri.. - matchEndWidth := displayWidth(text[:maxe]) - if matchEndWidth <= maxWidth-2 { + if !overflow(text[:maxe], maxWidth-2) { text, _ = trimRight(text, maxWidth-2) text = append(text, []rune("..")...) } else { // Stri.. - if matchEndWidth < fullWidth-2 { + if overflow(text[maxe:], 2) { text = append(text[:maxe], []rune("..")...) } // ..ri.. From d4b63381029b477d064d4c3ff38f4efb89ae950d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Aug 2016 17:51:34 +0900 Subject: [PATCH 234/658] Lint --- src/item.go | 1 + src/matcher.go | 2 +- src/merger.go | 2 +- src/pattern.go | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/item.go b/src/item.go index 36f8c0a..584dfd8 100644 --- a/src/item.go +++ b/src/item.go @@ -44,6 +44,7 @@ func buildEmptyRank(index int32) [5]int32 { return [5]int32{0, 0, 0, 0, index} } +// Index returns ordinal index of the Item func (item *Item) Index() int32 { return item.rank[4] } diff --git a/src/matcher.go b/src/matcher.go index d320697..d2a7b38 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -200,7 +200,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { } partialResults := make([][]*Item, numSlices) - for _, _ = range slices { + for _ = range slices { partialResult := <-resultChan partialResults[partialResult.index] = partialResult.matches } diff --git a/src/merger.go b/src/merger.go index 26ed17b..0d3fb80 100644 --- a/src/merger.go +++ b/src/merger.go @@ -2,7 +2,7 @@ package fzf import "fmt" -// Merger with no data +// EmptyMerger is a Merger with no data var EmptyMerger = NewMerger([][]*Item{}, false, false) // Merger holds a set of locally sorted lists of items and provides the view of diff --git a/src/pattern.go b/src/pattern.go index 2df2879..0bf9af3 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -372,8 +372,8 @@ func (p *Pattern) iter(pfun func(bool, bool, util.Chars, []rune) algo.Result, for _, part := range tokens { prefixLength := int32(part.prefixLength) if res := pfun(caseSensitive, forward, part.text, pattern); res.Start >= 0 { - var sidx int32 = res.Start + prefixLength - var eidx int32 = res.End + prefixLength + sidx := res.Start + prefixLength + eidx := res.End + prefixLength return Offset{sidx, eidx, int32(part.trimLength)}, res.Bonus } } From c4c92142a6001936420096d9f8e335cb25e116aa Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Aug 2016 18:06:00 +0900 Subject: [PATCH 235/658] 0.13.4 --- CHANGELOG.md | 8 ++++++++ install | 4 ++-- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index effb27d..22546f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +0.13.4 +------ +- Performance optimization + - Memory footprint for ascii string is reduced by 60% + - 15 to 20% improvement of query performance + - Up to 45% better performance of `--nth` with non-regex delimiters +- Fixed invalid handling of `hidden` property of `--preview-window` + 0.13.3 ------ - Fixed duplicate rendering of the last line in preview window diff --git a/install b/install index d224607..6e77d52 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.13.3 pre=1 || - version=0.13.3 pre=0 +[[ "$@" =~ --pre ]] && version=0.13.4 pre=1 || + version=0.13.4 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 2b6d039..5eabb5e 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Jul 2016" "fzf 0.13.3" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Aug 2016" "fzf 0.13.4" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 500a253..c6b2dea 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Jul 2016" "fzf 0.13.3" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Aug 2016" "fzf 0.13.4" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 3dfbf46..e87e393 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.13.3" + version = "0.13.4" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 3a9532c8fd9e38315566752db2f04049059f9ba8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 16 Aug 2016 01:52:24 +0900 Subject: [PATCH 236/658] Increase read buffer size to 64KB --- src/constants.go | 3 ++- src/reader.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/constants.go b/src/constants.go index e87e393..74c59fa 100644 --- a/src/constants.go +++ b/src/constants.go @@ -15,7 +15,8 @@ const ( coordinatorDelayStep time.Duration = 10 * time.Millisecond // Reader - defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//` + defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//` + readerBufferSize = 64 * 1024 // Terminal initialDelay = 20 * time.Millisecond diff --git a/src/reader.go b/src/reader.go index 85cbf8b..0542666 100644 --- a/src/reader.go +++ b/src/reader.go @@ -34,7 +34,7 @@ func (r *Reader) feed(src io.Reader) { if r.delimNil { delim = '\000' } - reader := bufio.NewReader(src) + reader := bufio.NewReaderSize(src, readerBufferSize) for { // ReadBytes returns err != nil if and only if the returned data does not // end in delim. From 935272824ebb7ec6de3cd981caf71c8b5208f9b3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 17 Aug 2016 02:21:33 +0900 Subject: [PATCH 237/658] Setting GOMAXPROCS is no longer needed https://golang.org/doc/go1.5 --- src/core.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/core.go b/src/core.go index 3df15ed..76d30d1 100644 --- a/src/core.go +++ b/src/core.go @@ -28,16 +28,11 @@ package fzf import ( "fmt" "os" - "runtime" "time" "github.com/junegunn/fzf/src/util" ) -func initProcs() { - runtime.GOMAXPROCS(runtime.NumCPU()) -} - /* Reader -> EvtReadFin Reader -> EvtReadNew -> Matcher (restart) @@ -49,8 +44,6 @@ Matcher -> EvtHeader -> Terminal (update header) // Run starts fzf func Run(opts *Options) { - initProcs() - sort := opts.Sort > 0 sortCriteria = opts.Criteria From babf877fd618dd4c442ca78e920198324527e943 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 18 Aug 2016 01:38:41 +0900 Subject: [PATCH 238/658] Increase the number of go routines for search Sort performance increases as the size of each sublist decreases (n in nlog(n) decreases). Merger is then responsible for merging the sorted lists in order, and since in most cases we are only interesed in the matches in the first page on the screen so the overhead in the process is negligible. --- src/matcher.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/matcher.go b/src/matcher.go index d2a7b38..d332b85 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -43,7 +43,7 @@ func NewMatcher(patternBuilder func([]rune) *Pattern, tac: tac, eventBox: eventBox, reqBox: util.NewEventBox(), - partitions: runtime.NumCPU(), + partitions: 16 * runtime.NumCPU(), mergerCache: make(map[string]*Merger)} } @@ -106,18 +106,19 @@ func (m *Matcher) Loop() { } func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk { - perSlice := len(chunks) / m.partitions + partitions := m.partitions + perSlice := len(chunks) / partitions - // No need to parallelize if perSlice == 0 { - return [][]*Chunk{chunks} + partitions = len(chunks) + perSlice = 1 } - slices := make([][]*Chunk, m.partitions) - for i := 0; i < m.partitions; i++ { + slices := make([][]*Chunk, partitions) + for i := 0; i < partitions; i++ { start := i * perSlice end := start + perSlice - if i == m.partitions-1 { + if i == partitions-1 { end = len(chunks) } slices[i] = chunks[start:end] From 0e0de29b87fa65c135244e0d1137c4614d4d2d7c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 18 Aug 2016 01:48:52 +0900 Subject: [PATCH 239/658] Inline function calls in tight loops By only using leaf functions --- src/algo/algo.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 63469c9..8825a05 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -22,13 +22,6 @@ func indexAt(index int, max int, forward bool) int { return max - index - 1 } -func runeAt(text util.Chars, index int, max int, forward bool) rune { - if forward { - return text.Get(index) - } - return text.Get(max - index - 1) -} - // Result conatins the results of running a match function. type Result struct { Start int32 @@ -136,7 +129,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, text util.Chars, pattern []run lenPattern := len(pattern) for index := 0; index < lenRunes; index++ { - char := runeAt(text, index, lenRunes, forward) + char := text.Get(indexAt(index, lenRunes, forward)) // This is considerably faster than blindly applying strings.ToLower to the // whole string if !caseSensitive { @@ -164,7 +157,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, text util.Chars, pattern []run if sidx >= 0 && eidx >= 0 { pidx-- for index := eidx - 1; index >= sidx; index-- { - char := runeAt(text, index, lenRunes, forward) + char := text.Get(indexAt(index, lenRunes, forward)) if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 @@ -215,7 +208,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern pidx := 0 for index := 0; index < lenRunes; index++ { - char := runeAt(text, index, lenRunes, forward) + char := text.Get(indexAt(index, lenRunes, forward)) if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 From 01ee335521485e82f0a2eb88cec79908b469b187 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 18 Aug 2016 03:11:54 +0900 Subject: [PATCH 240/658] Remove duplicate code --- src/tokenizer.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/tokenizer.go b/src/tokenizer.go index e48f48b..b6aa0c6 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -173,14 +173,6 @@ func joinTokens(tokens []Token) []rune { return ret } -func joinTokensAsRunes(tokens []Token) []rune { - ret := []rune{} - for _, token := range tokens { - ret = append(ret, token.text.ToRunes()...) - } - return ret -} - // Transform is used to transform the input when --with-nth option is given func Transform(tokens []Token, withNth []Range) []Token { transTokens := make([]Token, len(withNth)) @@ -191,7 +183,7 @@ func Transform(tokens []Token, withNth []Range) []Token { if r.begin == r.end { idx := r.begin if idx == rangeEllipsis { - parts = append(parts, util.RunesToChars(joinTokensAsRunes(tokens))) + parts = append(parts, util.RunesToChars(joinTokens(tokens))) } else { if idx < 0 { idx += numTokens + 1 From f7f01d109eb05c7eae82c243b6b6d5c5951ee707 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 19 Aug 2016 01:43:38 +0900 Subject: [PATCH 241/658] Set the upper limit of the number of search go routines --- src/matcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matcher.go b/src/matcher.go index d332b85..4c00db7 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -43,7 +43,7 @@ func NewMatcher(patternBuilder func([]rune) *Pattern, tac: tac, eventBox: eventBox, reqBox: util.NewEventBox(), - partitions: 16 * runtime.NumCPU(), + partitions: util.Min(8*runtime.NumCPU(), 32), mergerCache: make(map[string]*Merger)} } From 37dc273148df0893053bf5cda0582a23f5c2b2d2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 19 Aug 2016 02:39:32 +0900 Subject: [PATCH 242/658] Micro-optimizations - Make structs smaller - Introduce Result struct and use it to represent matched items instead of reusing Item struct for that purpose - Avoid unnecessary memory allocation - Avoid growing slice from the initial capacity - Code cleanup --- src/algo/algo.go | 22 ++-- src/algo/algo_test.go | 2 +- src/ansi.go | 7 +- src/ansi_test.go | 84 ++++++------- src/cache.go | 6 +- src/cache_test.go | 4 +- src/chunklist_test.go | 9 +- src/core.go | 28 ++--- src/item.go | 278 ++---------------------------------------- src/item_test.go | 112 ++--------------- src/matcher.go | 16 ++- src/merger.go | 22 ++-- src/merger_test.go | 26 ++-- src/options.go | 5 +- src/pattern.go | 110 ++++++++--------- src/pattern_test.go | 8 +- src/terminal.go | 35 +++--- src/tokenizer.go | 22 ++-- src/util/util.go | 10 ++ 19 files changed, 236 insertions(+), 570 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 8825a05..00265c6 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -24,12 +24,12 @@ func indexAt(index int, max int, forward bool) int { // Result conatins the results of running a match function. type Result struct { - Start int32 - End int32 + Start int + End int // Items are basically sorted by the lengths of matched substrings. // But we slightly adjust the score with bonus for better results. - Bonus int32 + Bonus int } type charClass int @@ -42,8 +42,8 @@ const ( charNumber ) -func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int) int32 { - var bonus int32 +func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int) int { + var bonus int pidx := 0 lenPattern := len(pattern) consecutive := false @@ -63,7 +63,7 @@ func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int class = charNonWord } - var point int32 + var point int if prevClass == charNonWord && class != charNonWord { // Word boundary point = 2 @@ -181,7 +181,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, text util.Chars, pattern []run sidx, eidx = lenRunes-eidx, lenRunes-sidx } - return Result{int32(sidx), int32(eidx), + return Result{sidx, eidx, evaluateBonus(caseSensitive, text, pattern, sidx, eidx)} } return Result{-1, -1, 0} @@ -228,7 +228,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern sidx = lenRunes - (index + 1) eidx = lenRunes - (index - lenPattern + 1) } - return Result{int32(sidx), int32(eidx), + return Result{sidx, eidx, evaluateBonus(caseSensitive, text, pattern, sidx, eidx)} } } else { @@ -255,7 +255,7 @@ func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru } } lenPattern := len(pattern) - return Result{0, int32(lenPattern), + return Result{0, lenPattern, evaluateBonus(caseSensitive, text, pattern, 0, lenPattern)} } @@ -279,7 +279,7 @@ func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru lenPattern := len(pattern) sidx := trimmedLen - lenPattern eidx := trimmedLen - return Result{int32(sidx), int32(eidx), + return Result{sidx, eidx, evaluateBonus(caseSensitive, text, pattern, sidx, eidx)} } @@ -294,7 +294,7 @@ func EqualMatch(caseSensitive bool, forward bool, text util.Chars, pattern []run runesStr = strings.ToLower(runesStr) } if runesStr == string(pattern) { - return Result{0, int32(len(pattern)), 0} + return Result{0, len(pattern), 0} } return Result{-1, -1, 0} } diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index d6a5d48..7034dce 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -7,7 +7,7 @@ import ( "github.com/junegunn/fzf/src/util" ) -func assertMatch(t *testing.T, fun func(bool, bool, util.Chars, []rune) Result, caseSensitive, forward bool, input, pattern string, sidx int32, eidx int32, bonus int32) { +func assertMatch(t *testing.T, fun func(bool, bool, util.Chars, []rune) Result, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, bonus int) { if !caseSensitive { pattern = strings.ToLower(pattern) } diff --git a/src/ansi.go b/src/ansi.go index 56831ee..0a52567 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -36,7 +36,7 @@ func init() { ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]") } -func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, []ansiOffset, *ansiState) { +func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) { var offsets []ansiOffset var output bytes.Buffer @@ -84,7 +84,10 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo if proc != nil { proc(rest, state) } - return output.String(), offsets, state + if len(offsets) == 0 { + return output.String(), nil, state + } + return output.String(), &offsets, state } func interpretCode(ansiCode string, prevState *ansiState) *ansiState { diff --git a/src/ansi_test.go b/src/ansi_test.go index 31803f3..a80e98a 100644 --- a/src/ansi_test.go +++ b/src/ansi_test.go @@ -16,7 +16,7 @@ func TestExtractColor(t *testing.T) { src := "hello world" var state *ansiState clean := "\x1b[0m" - check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) { + check := func(assertion func(ansiOffsets *[]ansiOffset, state *ansiState)) { output, ansiOffsets, newState := extractColor(src, state, nil) state = newState if output != "hello world" { @@ -26,127 +26,127 @@ func TestExtractColor(t *testing.T) { assertion(ansiOffsets, state) } - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) > 0 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if offsets != nil { t.Fail() } }) state = nil src = "\x1b[0mhello world" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) > 0 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if offsets != nil { t.Fail() } }) state = nil src = "\x1b[1mhello world" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } - assert(offsets[0], 0, 11, -1, -1, true) + assert((*offsets)[0], 0, 11, -1, -1, true) }) state = nil src = "\x1b[1mhello \x1b[mworld" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } - assert(offsets[0], 0, 6, -1, -1, true) + assert((*offsets)[0], 0, 6, -1, -1, true) }) state = nil src = "\x1b[1mhello \x1b[Kworld" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } - assert(offsets[0], 0, 11, -1, -1, true) + assert((*offsets)[0], 0, 11, -1, -1, true) }) state = nil src = "hello \x1b[34;45;1mworld" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } - assert(offsets[0], 6, 11, 4, 5, true) + assert((*offsets)[0], 6, 11, 4, 5, true) }) state = nil src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } - assert(offsets[0], 6, 11, 4, 5, true) + assert((*offsets)[0], 6, 11, 4, 5, true) }) state = nil src = "hello \x1b[34;45;1mwor\x1b[0mld" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } - assert(offsets[0], 6, 9, 4, 5, true) + assert((*offsets)[0], 6, 9, 4, 5, true) }) state = nil src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 3 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 3 { t.Fail() } - assert(offsets[0], 6, 8, 4, 233, true) - assert(offsets[1], 8, 9, 161, 233, true) - assert(offsets[2], 10, 11, 161, -1, false) + assert((*offsets)[0], 6, 8, 4, 233, true) + assert((*offsets)[1], 8, 9, 161, 233, true) + assert((*offsets)[2], 10, 11, 161, -1, false) }) // {38,48};5;{38,48} state = nil src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 2 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 2 { t.Fail() } - assert(offsets[0], 6, 9, 38, 48, true) - assert(offsets[1], 9, 10, 48, 38, true) + assert((*offsets)[0], 6, 9, 38, 48, true) + assert((*offsets)[1], 9, 10, 48, 38, true) }) src = "hello \x1b[32;1mworld" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } if state.fg != 2 || state.bg != -1 || !state.bold { t.Fail() } - assert(offsets[0], 6, 11, 2, -1, true) + assert((*offsets)[0], 6, 11, 2, -1, true) }) src = "hello world" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } if state.fg != 2 || state.bg != -1 || !state.bold { t.Fail() } - assert(offsets[0], 0, 11, 2, -1, true) + assert((*offsets)[0], 0, 11, 2, -1, true) }) src = "hello \x1b[0;38;5;200;48;5;100mworld" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 2 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 2 { t.Fail() } if state.fg != 200 || state.bg != 100 || state.bold { t.Fail() } - assert(offsets[0], 0, 6, 2, -1, true) - assert(offsets[1], 6, 11, 200, 100, false) + assert((*offsets)[0], 0, 6, 2, -1, true) + assert((*offsets)[1], 6, 11, 200, 100, false) }) } diff --git a/src/cache.go b/src/cache.go index d2ec00b..baf88dd 100644 --- a/src/cache.go +++ b/src/cache.go @@ -3,7 +3,7 @@ package fzf import "sync" // queryCache associates strings to lists of items -type queryCache map[string][]*Item +type queryCache map[string][]*Result // ChunkCache associates Chunk and query string to lists of items type ChunkCache struct { @@ -17,7 +17,7 @@ func NewChunkCache() ChunkCache { } // Add adds the list to the cache -func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) { +func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) { if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax { return } @@ -34,7 +34,7 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) { } // Find is called to lookup ChunkCache -func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Item, bool) { +func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Result, bool) { if len(key) == 0 || !chunk.IsFull() { return nil, false } diff --git a/src/cache_test.go b/src/cache_test.go index 05f904c..8703fc4 100644 --- a/src/cache_test.go +++ b/src/cache_test.go @@ -7,8 +7,8 @@ func TestChunkCache(t *testing.T) { chunk2 := make(Chunk, chunkSize) chunk1p := &Chunk{} chunk2p := &chunk2 - items1 := []*Item{&Item{}} - items2 := []*Item{&Item{}, &Item{}} + items1 := []*Result{&Result{}} + items2 := []*Result{&Result{}, &Result{}} cache.Add(chunk1p, "foo", items1) cache.Add(chunk2p, "foo", items1) cache.Add(chunk2p, "bar", items2) diff --git a/src/chunklist_test.go b/src/chunklist_test.go index 2523675..594daeb 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -12,7 +12,7 @@ func TestChunkList(t *testing.T) { sortCriteria = []criterion{byMatchLen, byLength} cl := NewChunkList(func(s []byte, i int) *Item { - return &Item{text: util.ToChars(s), rank: buildEmptyRank(int32(i * 2))} + return &Item{text: util.ToChars(s), index: int32(i * 2)} }) // Snapshot @@ -41,11 +41,8 @@ func TestChunkList(t *testing.T) { if len(*chunk1) != 2 { t.Error("Snapshot should contain only two items") } - last := func(arr [5]int32) int32 { - return arr[len(arr)-1] - } - if (*chunk1)[0].text.ToString() != "hello" || last((*chunk1)[0].rank) != 0 || - (*chunk1)[1].text.ToString() != "world" || last((*chunk1)[1].rank) != 2 { + if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].index != 0 || + (*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].index != 2 { t.Error("Invalid data") } if chunk1.IsFull() { diff --git a/src/core.go b/src/core.go index 76d30d1..290c0af 100644 --- a/src/core.go +++ b/src/core.go @@ -56,16 +56,16 @@ func Run(opts *Options) { eventBox := util.NewEventBox() // ANSI code processor - ansiProcessor := func(data []byte) (util.Chars, []ansiOffset) { + ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) { return util.ToChars(data), nil } - ansiProcessorRunes := func(data []rune) (util.Chars, []ansiOffset) { + ansiProcessorRunes := func(data []rune) (util.Chars, *[]ansiOffset) { return util.RunesToChars(data), nil } if opts.Ansi { if opts.Theme != nil { var state *ansiState - ansiProcessor = func(data []byte) (util.Chars, []ansiOffset) { + ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { trimmed, offsets, newState := extractColor(string(data), state, nil) state = newState return util.RunesToChars([]rune(trimmed)), offsets @@ -73,12 +73,12 @@ func Run(opts *Options) { } else { // When color is disabled but ansi option is given, // we simply strip out ANSI codes from the input - ansiProcessor = func(data []byte) (util.Chars, []ansiOffset) { + ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { trimmed, _, _ := extractColor(string(data), nil, nil) return util.RunesToChars([]rune(trimmed)), nil } } - ansiProcessorRunes = func(data []rune) (util.Chars, []ansiOffset) { + ansiProcessorRunes = func(data []rune) (util.Chars, *[]ansiOffset) { return ansiProcessor([]byte(string(data))) } } @@ -95,14 +95,13 @@ func Run(opts *Options) { } chars, colors := ansiProcessor(data) return &Item{ + index: int32(index), text: chars, - colors: colors, - rank: buildEmptyRank(int32(index))} + colors: colors} }) } else { chunkList = NewChunkList(func(data []byte, index int) *Item { - chars := util.ToChars(data) - tokens := Tokenize(chars, opts.Delimiter) + tokens := Tokenize(util.ToChars(data), opts.Delimiter) trans := Transform(tokens, opts.WithNth) if len(header) < opts.HeaderLines { header = append(header, string(joinTokens(trans))) @@ -111,10 +110,9 @@ func Run(opts *Options) { } textRunes := joinTokens(trans) item := Item{ - text: util.RunesToChars(textRunes), + index: int32(index), origText: &data, - colors: nil, - rank: buildEmptyRank(int32(index))} + colors: nil} trimmed, colors := ansiProcessorRunes(textRunes) item.text = trimmed @@ -163,7 +161,7 @@ func Run(opts *Options) { reader := Reader{ func(runes []byte) bool { item := chunkList.trans(runes, 0) - if item != nil && pattern.MatchItem(item) { + if item != nil && pattern.MatchItem(item) != nil { fmt.Println(item.text.ToString()) found = true } @@ -179,7 +177,7 @@ func Run(opts *Options) { chunks: snapshot, pattern: pattern}) for i := 0; i < merger.Length(); i++ { - fmt.Println(merger.Get(i).AsString(opts.Ansi)) + fmt.Println(merger.Get(i).item.AsString(opts.Ansi)) found = true } } @@ -259,7 +257,7 @@ func Run(opts *Options) { fmt.Println() } for i := 0; i < count; i++ { - fmt.Println(val.Get(i).AsString(opts.Ansi)) + fmt.Println(val.Get(i).item.AsString(opts.Ansi)) } if count > 0 { os.Exit(exitOk) diff --git a/src/item.go b/src/item.go index 584dfd8..4e60faf 100644 --- a/src/item.go +++ b/src/item.go @@ -1,295 +1,39 @@ package fzf import ( - "math" - - "github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/util" ) -// Offset holds three 32-bit integers denoting the offsets of a matched substring -type Offset [3]int32 - -type colorOffset struct { - offset [2]int32 - color int - bold bool -} - // Item represents each input line type Item struct { + index int32 text util.Chars origText *[]byte + colors *[]ansiOffset transformed []Token - offsets []Offset - colors []ansiOffset - rank [5]int32 - bonus int32 -} - -// Sort criteria to use. Never changes once fzf is started. -var sortCriteria []criterion - -func isRankValid(rank [5]int32) bool { - // Exclude ordinal index - for _, r := range rank[:4] { - if r > 0 { - return true - } - } - return false -} - -func buildEmptyRank(index int32) [5]int32 { - return [5]int32{0, 0, 0, 0, index} } // Index returns ordinal index of the Item func (item *Item) Index() int32 { - return item.rank[4] + return item.index } -// Rank calculates rank of the Item -func (item *Item) Rank(cache bool) [5]int32 { - if cache && isRankValid(item.rank) { - return item.rank +// Colors returns ansiOffsets of the Item +func (item *Item) Colors() []ansiOffset { + if item.colors == nil { + return []ansiOffset{} } - matchlen := 0 - prevEnd := 0 - lenSum := 0 - minBegin := math.MaxInt32 - for _, offset := range item.offsets { - begin := int(offset[0]) - end := int(offset[1]) - trimLen := int(offset[2]) - lenSum += trimLen - if prevEnd > begin { - begin = prevEnd - } - if end > prevEnd { - prevEnd = end - } - if end > begin { - if begin < minBegin { - minBegin = begin - } - matchlen += end - begin - } - } - rank := buildEmptyRank(item.Index()) - for idx, criterion := range sortCriteria { - var val int32 - switch criterion { - case byMatchLen: - if matchlen == 0 { - val = math.MaxInt32 - } else { - // It is extremely unlikely that bonus exceeds 128 - val = 128*int32(matchlen) - item.bonus - } - case byLength: - // It is guaranteed that .transformed in not null in normal execution - if item.transformed != nil { - // If offsets is empty, lenSum will be 0, but we don't care - val = int32(lenSum) - } else { - val = int32(item.text.Length()) - } - case byBegin: - // We can't just look at item.offsets[0][0] because it can be an inverse term - whitePrefixLen := 0 - numChars := item.text.Length() - for idx := 0; idx < numChars; idx++ { - r := item.text.Get(idx) - whitePrefixLen = idx - if idx == minBegin || r != ' ' && r != '\t' { - break - } - } - val = int32(minBegin - whitePrefixLen) - case byEnd: - if prevEnd > 0 { - val = int32(1 + item.text.Length() - prevEnd) - } else { - // Empty offsets due to inverse terms. - val = 1 - } - } - rank[idx] = val - } - if cache { - item.rank = rank - } - return rank + return *item.colors } // AsString returns the original string func (item *Item) AsString(stripAnsi bool) string { - return *item.StringPtr(stripAnsi) -} - -// StringPtr returns the pointer to the original string -func (item *Item) StringPtr(stripAnsi bool) *string { if item.origText != nil { if stripAnsi { trimmed, _, _ := extractColor(string(*item.origText), nil, nil) - return &trimmed + return trimmed } - orig := string(*item.origText) - return &orig + return string(*item.origText) } - str := item.text.ToString() - return &str -} - -func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset { - if len(item.colors) == 0 { - var offsets []colorOffset - for _, off := range item.offsets { - - offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold}) - } - return offsets - } - - // Find max column - var maxCol int32 - for _, off := range item.offsets { - if off[1] > maxCol { - maxCol = off[1] - } - } - for _, ansi := range item.colors { - if ansi.offset[1] > maxCol { - maxCol = ansi.offset[1] - } - } - cols := make([]int, maxCol) - - for colorIndex, ansi := range item.colors { - for i := ansi.offset[0]; i < ansi.offset[1]; i++ { - cols[i] = colorIndex + 1 // XXX - } - } - - for _, off := range item.offsets { - for i := off[0]; i < off[1]; i++ { - cols[i] = -1 - } - } - - // sort.Sort(ByOrder(offsets)) - - // Merge offsets - // ------------ ---- -- ---- - // ++++++++ ++++++++++ - // --++++++++-- --++++++++++--- - curr := 0 - start := 0 - var offsets []colorOffset - add := func(idx int) { - if curr != 0 && idx > start { - if curr == -1 { - offsets = append(offsets, colorOffset{ - offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold}) - } else { - ansi := item.colors[curr-1] - fg := ansi.color.fg - if fg == -1 { - if current { - fg = curses.CurrentFG - } else { - fg = curses.FG - } - } - bg := ansi.color.bg - if bg == -1 { - if current { - bg = curses.DarkBG - } else { - bg = curses.BG - } - } - offsets = append(offsets, colorOffset{ - offset: [2]int32{int32(start), int32(idx)}, - color: curses.PairFor(fg, bg), - bold: ansi.color.bold || bold}) - } - } - } - for idx, col := range cols { - if col != curr { - add(idx) - start = idx - curr = col - } - } - add(int(maxCol)) - return offsets -} - -// ByOrder is for sorting substring offsets -type ByOrder []Offset - -func (a ByOrder) Len() int { - return len(a) -} - -func (a ByOrder) Swap(i, j int) { - a[i], a[j] = a[j], a[i] -} - -func (a ByOrder) Less(i, j int) bool { - ioff := a[i] - joff := a[j] - return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1]) -} - -// ByRelevance is for sorting Items -type ByRelevance []*Item - -func (a ByRelevance) Len() int { - return len(a) -} - -func (a ByRelevance) Swap(i, j int) { - a[i], a[j] = a[j], a[i] -} - -func (a ByRelevance) Less(i, j int) bool { - irank := a[i].Rank(true) - jrank := a[j].Rank(true) - - return compareRanks(irank, jrank, false) -} - -// ByRelevanceTac is for sorting Items -type ByRelevanceTac []*Item - -func (a ByRelevanceTac) Len() int { - return len(a) -} - -func (a ByRelevanceTac) Swap(i, j int) { - a[i], a[j] = a[j], a[i] -} - -func (a ByRelevanceTac) Less(i, j int) bool { - irank := a[i].Rank(true) - jrank := a[j].Rank(true) - - return compareRanks(irank, jrank, true) -} - -func compareRanks(irank [5]int32, jrank [5]int32, tac bool) bool { - for idx := 0; idx < 4; idx++ { - left := irank[idx] - right := jrank[idx] - if left < right { - return true - } else if left > right { - return false - } - } - return (irank[4] <= jrank[4]) != tac + return item.text.ToString() } diff --git a/src/item_test.go b/src/item_test.go index 36a436d..1efb5f1 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -1,109 +1,23 @@ package fzf import ( - "math" - "sort" "testing" - "github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/util" ) -func TestOffsetSort(t *testing.T) { - offsets := []Offset{ - Offset{3, 5}, Offset{2, 7}, - Offset{1, 3}, Offset{2, 9}} - sort.Sort(ByOrder(offsets)) - - if offsets[0][0] != 1 || offsets[0][1] != 3 || - offsets[1][0] != 2 || offsets[1][1] != 7 || - offsets[2][0] != 2 || offsets[2][1] != 9 || - offsets[3][0] != 3 || offsets[3][1] != 5 { - t.Error("Invalid order:", offsets) +func TestStringPtr(t *testing.T) { + orig := []byte("\x1b[34mfoo") + text := []byte("\x1b[34mbar") + item := Item{origText: &orig, text: util.ToChars(text)} + if item.AsString(true) != "foo" || item.AsString(false) != string(orig) { + t.Fail() + } + if item.AsString(true) != "foo" { + t.Fail() + } + item.origText = nil + if item.AsString(true) != string(text) || item.AsString(false) != string(text) { + t.Fail() } } - -func TestRankComparison(t *testing.T) { - if compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{2, 0, 0, 0, 7}, false) || - !compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{3, 0, 0, 0, 6}, false) || - !compareRanks([5]int32{1, 2, 0, 0, 3}, [5]int32{1, 3, 0, 0, 2}, false) || - !compareRanks([5]int32{0, 0, 0, 0, 0}, [5]int32{0, 0, 0, 0, 0}, false) { - t.Error("Invalid order") - } - - if compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{2, 0, 0, 0, 7}, true) || - !compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{3, 0, 0, 0, 6}, false) || - !compareRanks([5]int32{1, 2, 0, 0, 3}, [5]int32{1, 3, 0, 0, 2}, true) || - !compareRanks([5]int32{0, 0, 0, 0, 0}, [5]int32{0, 0, 0, 0, 0}, false) { - t.Error("Invalid order (tac)") - } -} - -// Match length, string length, index -func TestItemRank(t *testing.T) { - // FIXME global - sortCriteria = []criterion{byMatchLen, byLength} - - strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} - item1 := Item{text: util.RunesToChars(strs[0]), offsets: []Offset{}, rank: [5]int32{0, 0, 0, 0, 1}} - rank1 := item1.Rank(true) - if rank1[0] != math.MaxInt32 || rank1[1] != 3 || rank1[4] != 1 { - t.Error(item1.Rank(true)) - } - // Only differ in index - item2 := Item{text: util.RunesToChars(strs[0]), offsets: []Offset{}} - - items := []*Item{&item1, &item2} - sort.Sort(ByRelevance(items)) - if items[0] != &item2 || items[1] != &item1 { - t.Error(items) - } - - items = []*Item{&item2, &item1, &item1, &item2} - sort.Sort(ByRelevance(items)) - if items[0] != &item2 || items[1] != &item2 || - items[2] != &item1 || items[3] != &item1 { - t.Error(items) - } - - // Sort by relevance - item3 := Item{text: util.RunesToChars(strs[1]), rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} - item4 := Item{text: util.RunesToChars(strs[1]), rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} - item5 := Item{text: util.RunesToChars(strs[2]), rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} - item6 := Item{text: util.RunesToChars(strs[2]), rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} - items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6} - sort.Sort(ByRelevance(items)) - if items[0] != &item6 || items[1] != &item4 || - items[2] != &item5 || items[3] != &item3 || - items[4] != &item2 || items[5] != &item1 { - t.Error(items) - } -} - -func TestColorOffset(t *testing.T) { - // ------------ 20 ---- -- ---- - // ++++++++ ++++++++++ - // --++++++++-- --++++++++++--- - item := Item{ - offsets: []Offset{Offset{5, 15}, Offset{25, 35}}, - colors: []ansiOffset{ - ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}}, - ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}}, - ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}}, - ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}} - // [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}] - - offsets := item.colorOffsets(99, false, true) - assert := func(idx int, b int32, e int32, c int, bold bool) { - o := offsets[idx] - if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold { - t.Error(o) - } - } - assert(0, 0, 5, curses.ColUser, false) - assert(1, 5, 15, 99, false) - assert(2, 15, 20, curses.ColUser, false) - assert(3, 22, 25, curses.ColUser+1, true) - assert(4, 25, 35, 99, false) - assert(5, 35, 40, curses.ColUser+2, true) -} diff --git a/src/matcher.go b/src/matcher.go index 4c00db7..8cd9a9f 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -128,7 +128,7 @@ func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk { type partialResult struct { index int - matches []*Item + matches []*Result } func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { @@ -155,15 +155,21 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { waitGroup.Add(1) go func(idx int, chunks []*Chunk) { defer func() { waitGroup.Done() }() - sliceMatches := []*Item{} - for _, chunk := range chunks { + count := 0 + allMatches := make([][]*Result, len(chunks)) + for idx, chunk := range chunks { matches := request.pattern.Match(chunk) - sliceMatches = append(sliceMatches, matches...) + allMatches[idx] = matches + count += len(matches) if cancelled.Get() { return } countChan <- len(matches) } + sliceMatches := make([]*Result, 0, count) + for _, matches := range allMatches { + sliceMatches = append(sliceMatches, matches...) + } if m.sort { if m.tac { sort.Sort(ByRelevanceTac(sliceMatches)) @@ -200,7 +206,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { } } - partialResults := make([][]*Item, numSlices) + partialResults := make([][]*Result, numSlices) for _ = range slices { partialResult := <-resultChan partialResults[partialResult.index] = partialResult.matches diff --git a/src/merger.go b/src/merger.go index 0d3fb80..3879ab7 100644 --- a/src/merger.go +++ b/src/merger.go @@ -3,13 +3,13 @@ package fzf import "fmt" // EmptyMerger is a Merger with no data -var EmptyMerger = NewMerger([][]*Item{}, false, false) +var EmptyMerger = NewMerger([][]*Result{}, false, false) // Merger holds a set of locally sorted lists of items and provides the view of // a single, globally-sorted list type Merger struct { - lists [][]*Item - merged []*Item + lists [][]*Result + merged []*Result chunks *[]*Chunk cursors []int sorted bool @@ -33,10 +33,10 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger { } // NewMerger returns a new Merger -func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger { +func NewMerger(lists [][]*Result, sorted bool, tac bool) *Merger { mg := Merger{ lists: lists, - merged: []*Item{}, + merged: []*Result{}, chunks: nil, cursors: make([]int, len(lists)), sorted: sorted, @@ -55,14 +55,14 @@ func (mg *Merger) Length() int { return mg.count } -// Get returns the pointer to the Item object indexed by the given integer -func (mg *Merger) Get(idx int) *Item { +// Get returns the pointer to the Result object indexed by the given integer +func (mg *Merger) Get(idx int) *Result { if mg.chunks != nil { if mg.tac { idx = mg.count - idx - 1 } chunk := (*mg.chunks)[idx/chunkSize] - return (*chunk)[idx%chunkSize] + return &Result{item: (*chunk)[idx%chunkSize]} } if mg.sorted { @@ -86,9 +86,9 @@ func (mg *Merger) cacheable() bool { return mg.count < mergerCacheMax } -func (mg *Merger) mergedGet(idx int) *Item { +func (mg *Merger) mergedGet(idx int) *Result { for i := len(mg.merged); i <= idx; i++ { - minRank := buildEmptyRank(0) + minRank := minRank() minIdx := -1 for listIdx, list := range mg.lists { cursor := mg.cursors[listIdx] @@ -97,7 +97,7 @@ func (mg *Merger) mergedGet(idx int) *Item { continue } if cursor >= 0 { - rank := list[cursor].Rank(false) + rank := list[cursor].rank if minIdx < 0 || compareRanks(rank, minRank, mg.tac) { minRank = rank minIdx = listIdx diff --git a/src/merger_test.go b/src/merger_test.go index f62f975..d50470f 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -15,7 +15,7 @@ func assert(t *testing.T, cond bool, msg ...string) { } } -func randItem() *Item { +func randResult() *Result { str := fmt.Sprintf("%d", rand.Uint32()) offsets := make([]Offset, rand.Int()%3) for idx := range offsets { @@ -23,10 +23,10 @@ func randItem() *Item { eidx := sidx + int32(rand.Uint32()%20) offsets[idx] = Offset{sidx, eidx} } - return &Item{ - text: util.RunesToChars([]rune(str)), - rank: buildEmptyRank(rand.Int31()), - offsets: offsets} + return &Result{ + item: &Item{text: util.RunesToChars([]rune(str))}, + offsets: offsets, + rank: rank{index: rand.Int31()}} } func TestEmptyMerger(t *testing.T) { @@ -36,23 +36,23 @@ func TestEmptyMerger(t *testing.T) { assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list") } -func buildLists(partiallySorted bool) ([][]*Item, []*Item) { +func buildLists(partiallySorted bool) ([][]*Result, []*Result) { numLists := 4 - lists := make([][]*Item, numLists) + lists := make([][]*Result, numLists) cnt := 0 for i := 0; i < numLists; i++ { - numItems := rand.Int() % 20 - cnt += numItems - lists[i] = make([]*Item, numItems) - for j := 0; j < numItems; j++ { - item := randItem() + numResults := rand.Int() % 20 + cnt += numResults + lists[i] = make([]*Result, numResults) + for j := 0; j < numResults; j++ { + item := randResult() lists[i][j] = item } if partiallySorted { sort.Sort(ByRelevance(lists[i])) } } - items := []*Item{} + items := []*Result{} for _, list := range lists { items = append(items, list...) } diff --git a/src/options.go b/src/options.go index 31ad497..a19ff6f 100644 --- a/src/options.go +++ b/src/options.go @@ -95,6 +95,7 @@ type criterion int const ( byMatchLen criterion = iota + byBonus byLength byBegin byEnd @@ -178,7 +179,7 @@ func defaultOptions() *Options { Delimiter: Delimiter{}, Sort: 1000, Tac: false, - Criteria: []criterion{byMatchLen, byLength}, + Criteria: []criterion{byMatchLen, byBonus, byLength}, Multi: false, Ansi: false, Mouse: true, @@ -406,7 +407,7 @@ func parseKeyChords(str string, message string) map[int]string { } func parseTiebreak(str string) []criterion { - criteria := []criterion{byMatchLen} + criteria := []criterion{byMatchLen, byBonus} hasIndex := false hasLength := false hasBegin := false diff --git a/src/pattern.go b/src/pattern.go index 0bf9af3..4c35a12 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -2,7 +2,6 @@ package fzf import ( "regexp" - "sort" "strings" "github.com/junegunn/fzf/src/algo" @@ -235,9 +234,7 @@ func (p *Pattern) CacheKey() string { } // Match returns the list of matches Items in the given Chunk -func (p *Pattern) Match(chunk *Chunk) []*Item { - space := chunk - +func (p *Pattern) Match(chunk *Chunk) []*Result { // ChunkCache: Exact match cacheKey := p.CacheKey() if p.cacheable { @@ -246,7 +243,8 @@ func (p *Pattern) Match(chunk *Chunk) []*Item { } } - // ChunkCache: Prefix/suffix match + // Prefix/suffix cache + var space []*Result Loop: for idx := 1; idx < len(cacheKey); idx++ { // [---------| ] | [ |---------] @@ -256,14 +254,13 @@ Loop: suffix := cacheKey[idx:] for _, substr := range [2]*string{&prefix, &suffix} { if cached, found := _cache.Find(chunk, *substr); found { - cachedChunk := Chunk(cached) - space = &cachedChunk + space = cached break Loop } } } - matches := p.matchChunk(space) + matches := p.matchChunk(chunk, space) if p.cacheable { _cache.Add(chunk, cacheKey, matches) @@ -271,20 +268,19 @@ Loop: return matches } -func (p *Pattern) matchChunk(chunk *Chunk) []*Item { - matches := []*Item{} - if !p.extended { +func (p *Pattern) matchChunk(chunk *Chunk, space []*Result) []*Result { + matches := []*Result{} + + if space == nil { for _, item := range *chunk { - offset, bonus := p.basicMatch(item) - if sidx := offset[0]; sidx >= 0 { - matches = append(matches, - dupItem(item, []Offset{offset}, bonus)) + if match := p.MatchItem(item); match != nil { + matches = append(matches, match) } } } else { - for _, item := range *chunk { - if offsets, bonus := p.extendedMatch(item); len(offsets) == len(p.termSets) { - matches = append(matches, dupItem(item, offsets, bonus)) + for _, result := range space { + if match := p.MatchItem(result.item); match != nil { + matches = append(matches, match) } } } @@ -292,29 +288,21 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item { } // MatchItem returns true if the Item is a match -func (p *Pattern) MatchItem(item *Item) bool { - if !p.extended { - offset, _ := p.basicMatch(item) - sidx := offset[0] - return sidx >= 0 +func (p *Pattern) MatchItem(item *Item) *Result { + if p.extended { + if offsets, bonus, trimLen := p.extendedMatch(item); len(offsets) == len(p.termSets) { + return buildResult(item, offsets, bonus, trimLen) + } + return nil } - offsets, _ := p.extendedMatch(item) - return len(offsets) == len(p.termSets) + offset, bonus, trimLen := p.basicMatch(item) + if sidx := offset[0]; sidx >= 0 { + return buildResult(item, []Offset{offset}, bonus, trimLen) + } + return nil } -func dupItem(item *Item, offsets []Offset, bonus int32) *Item { - sort.Sort(ByOrder(offsets)) - return &Item{ - text: item.text, - origText: item.origText, - transformed: item.transformed, - offsets: offsets, - bonus: bonus, - colors: item.colors, - rank: buildEmptyRank(item.Index())} -} - -func (p *Pattern) basicMatch(item *Item) (Offset, int32) { +func (p *Pattern) basicMatch(item *Item) (Offset, int, int) { input := p.prepareInput(item) if p.fuzzy { return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) @@ -322,33 +310,39 @@ func (p *Pattern) basicMatch(item *Item) (Offset, int32) { return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text) } -func (p *Pattern) extendedMatch(item *Item) ([]Offset, int32) { +func (p *Pattern) extendedMatch(item *Item) ([]Offset, int, int) { input := p.prepareInput(item) offsets := []Offset{} - var totalBonus int32 + var totalBonus int + var totalTrimLen int for _, termSet := range p.termSets { - var offset *Offset - var bonus int32 + var offset Offset + var bonus int + var trimLen int + matched := false for _, term := range termSet { pfun := p.procFun[term.typ] - off, pen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text) + off, pen, tLen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text) if sidx := off[0]; sidx >= 0 { if term.inv { continue } - offset, bonus = &off, pen + offset, bonus, trimLen = off, pen, tLen + matched = true break } else if term.inv { - offset, bonus = &Offset{0, 0, 0}, 0 + offset, bonus, trimLen = Offset{0, 0}, 0, 0 + matched = true continue } } - if offset != nil { - offsets = append(offsets, *offset) + if matched { + offsets = append(offsets, offset) totalBonus += bonus + totalTrimLen += trimLen } } - return offsets, totalBonus + return offsets, totalBonus, totalTrimLen } func (p *Pattern) prepareInput(item *Item) []Token { @@ -357,26 +351,24 @@ func (p *Pattern) prepareInput(item *Item) []Token { } var ret []Token - if len(p.nth) > 0 { + if len(p.nth) == 0 { + ret = []Token{Token{text: &item.text, prefixLength: 0, trimLength: int32(item.text.TrimLength())}} + } else { tokens := Tokenize(item.text, p.delimiter) ret = Transform(tokens, p.nth) - } else { - ret = []Token{Token{text: item.text, prefixLength: 0, trimLength: item.text.TrimLength()}} } item.transformed = ret return ret } func (p *Pattern) iter(pfun func(bool, bool, util.Chars, []rune) algo.Result, - tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int32) { + tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int, int) { for _, part := range tokens { - prefixLength := int32(part.prefixLength) - if res := pfun(caseSensitive, forward, part.text, pattern); res.Start >= 0 { - sidx := res.Start + prefixLength - eidx := res.End + prefixLength - return Offset{sidx, eidx, int32(part.trimLength)}, res.Bonus + if res := pfun(caseSensitive, forward, *part.text, pattern); res.Start >= 0 { + sidx := int32(res.Start) + part.prefixLength + eidx := int32(res.End) + part.prefixLength + return Offset{sidx, eidx}, res.Bonus, int(part.trimLength) } } - // TODO: math.MaxUint16 - return Offset{-1, -1, -1}, 0.0 + return Offset{-1, -1}, 0, -1 } diff --git a/src/pattern_test.go b/src/pattern_test.go index 26f9284..3069110 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -83,7 +83,7 @@ func TestEqual(t *testing.T) { clearPatternCache() pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$")) - match := func(str string, sidxExpected int32, eidxExpected int32) { + match := func(str string, sidxExpected int, eidxExpected int) { res := algo.EqualMatch( pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text) if res.Start != sidxExpected || res.End != eidxExpected { @@ -133,10 +133,10 @@ func TestOrigTextAndTransformed(t *testing.T) { transformed: trans}, } pattern.extended = extended - matches := pattern.matchChunk(&chunk) - if matches[0].text.ToString() != "junegunn" || string(*matches[0].origText) != "junegunn.choi" || + matches := pattern.matchChunk(&chunk, nil) // No cache + if matches[0].item.text.ToString() != "junegunn" || string(*matches[0].item.origText) != "junegunn.choi" || matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || - !reflect.DeepEqual(matches[0].transformed, trans) { + !reflect.DeepEqual(matches[0].item.transformed, trans) { t.Error("Invalid match result", matches) } } diff --git a/src/terminal.go b/src/terminal.go index b6b6156..f7bbccf 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -77,7 +77,7 @@ type Terminal struct { type selectedItem struct { at time.Time - text *string + text string } type byTimeOrder []selectedItem @@ -357,7 +357,7 @@ func (t *Terminal) output() bool { } } else { for _, sel := range t.sortSelected() { - fmt.Println(*sel.text) + fmt.Println(sel.text) } } return found @@ -565,11 +565,10 @@ func (t *Terminal) printHeader() { state = newState item := &Item{ text: util.RunesToChars([]rune(trimmed)), - colors: colors, - rank: buildEmptyRank(0)} + colors: colors} t.move(line, 2, true) - t.printHighlighted(item, false, C.ColHeader, 0, false) + t.printHighlighted(&Result{item: item}, false, C.ColHeader, 0, false) } } @@ -590,7 +589,8 @@ func (t *Terminal) printList() { } } -func (t *Terminal) printItem(item *Item, i int, current bool) { +func (t *Terminal) printItem(result *Result, i int, current bool) { + item := result.item _, selected := t.selected[item.Index()] label := " " if t.jumping != jumpDisabled { @@ -609,14 +609,14 @@ func (t *Terminal) printItem(item *Item, i int, current bool) { } else { t.window.CPrint(C.ColCurrent, true, " ") } - t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true) + t.printHighlighted(result, true, C.ColCurrent, C.ColCurrentMatch, true) } else { if selected { t.window.CPrint(C.ColSelected, true, ">") } else { t.window.Print(" ") } - t.printHighlighted(item, false, 0, C.ColMatch, false) + t.printHighlighted(result, false, 0, C.ColMatch, false) } } @@ -667,16 +667,17 @@ func overflow(runes []rune, max int) bool { return false } -func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) { +func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 int, current bool) { + item := result.item var maxe int - for _, offset := range item.offsets { + for _, offset := range result.offsets { maxe = util.Max(maxe, int(offset[1])) } // Overflow text := make([]rune, item.text.Length()) copy(text, item.text.ToRunes()) - offsets := item.colorOffsets(col2, bold, current) + offsets := result.colorOffsets(col2, bold, current) maxWidth := t.window.Width - 3 maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) if overflow(text, maxWidth) { @@ -866,7 +867,7 @@ func (t *Terminal) isPreviewEnabled() bool { } func (t *Terminal) current() string { - return t.merger.Get(t.cy).AsString(t.ansi) + return t.merger.Get(t.cy).item.AsString(t.ansi) } // Loop is called to start Terminal I/O @@ -1037,13 +1038,13 @@ func (t *Terminal) Loop() { } selectItem := func(item *Item) bool { if _, found := t.selected[item.Index()]; !found { - t.selected[item.Index()] = selectedItem{time.Now(), item.StringPtr(t.ansi)} + t.selected[item.Index()] = selectedItem{time.Now(), item.AsString(t.ansi)} return true } return false } toggleY := func(y int) { - item := t.merger.Get(y) + item := t.merger.Get(y).item if !selectItem(item) { delete(t.selected, item.Index()) } @@ -1068,14 +1069,14 @@ func (t *Terminal) Loop() { case actIgnore: case actExecute: if t.cy >= 0 && t.cy < t.merger.Length() { - item := t.merger.Get(t.cy) + item := t.merger.Get(t.cy).item t.executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi))) } case actExecuteMulti: if len(t.selected) > 0 { sels := make([]string, len(t.selected)) for i, sel := range t.sortSelected() { - sels[i] = quoteEntry(*sel.text) + sels[i] = quoteEntry(sel.text) } t.executeCommand(t.execmap[mapkey], strings.Join(sels, " ")) } else { @@ -1137,7 +1138,7 @@ func (t *Terminal) Loop() { case actSelectAll: if t.multi { for i := 0; i < t.merger.Length(); i++ { - item := t.merger.Get(i) + item := t.merger.Get(i).item selectItem(item) } req(reqList, reqInfo) diff --git a/src/tokenizer.go b/src/tokenizer.go index b6aa0c6..ed87362 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -18,9 +18,9 @@ type Range struct { // Token contains the tokenized part of the strings and its prefix length type Token struct { - text util.Chars - prefixLength int - trimLength int + text *util.Chars + prefixLength int32 + trimLength int32 } // Delimiter for tokenizing the input @@ -80,9 +80,8 @@ func withPrefixLengths(tokens []util.Chars, begin int) []Token { prefixLength := begin for idx, token := range tokens { - // Need to define a new local variable instead of the reused token to take - // the pointer to it - ret[idx] = Token{token, prefixLength, token.TrimLength()} + // NOTE: &tokens[idx] instead of &tokens + ret[idx] = Token{&tokens[idx], int32(prefixLength), int32(token.TrimLength())} prefixLength += token.Length() } return ret @@ -178,12 +177,13 @@ func Transform(tokens []Token, withNth []Range) []Token { transTokens := make([]Token, len(withNth)) numTokens := len(tokens) for idx, r := range withNth { - parts := []util.Chars{} + parts := []*util.Chars{} minIdx := 0 if r.begin == r.end { idx := r.begin if idx == rangeEllipsis { - parts = append(parts, util.RunesToChars(joinTokens(tokens))) + chars := util.RunesToChars(joinTokens(tokens)) + parts = append(parts, &chars) } else { if idx < 0 { idx += numTokens + 1 @@ -227,7 +227,7 @@ func Transform(tokens []Token, withNth []Range) []Token { case 0: merged = util.RunesToChars([]rune{}) case 1: - merged = parts[0] + merged = *parts[0] default: runes := []rune{} for _, part := range parts { @@ -236,13 +236,13 @@ func Transform(tokens []Token, withNth []Range) []Token { merged = util.RunesToChars(runes) } - var prefixLength int + var prefixLength int32 if minIdx < numTokens { prefixLength = tokens[minIdx].prefixLength } else { prefixLength = 0 } - transTokens[idx] = Token{merged, prefixLength, merged.TrimLength()} + transTokens[idx] = Token{&merged, prefixLength, int32(merged.TrimLength())} } return transTokens } diff --git a/src/util/util.go b/src/util/util.go index 113fee9..2ebd0f6 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -4,6 +4,7 @@ package util import "C" import ( + "math" "os" "os/exec" "time" @@ -63,6 +64,15 @@ func Constrain(val int, min int, max int) int { return val } +func AsUint16(val int) uint16 { + if val > math.MaxUint16 { + return math.MaxUint16 + } else if val < 0 { + return 0 + } + return uint16(val) +} + // DurWithin limits the given time.Duration with the upper and lower bounds func DurWithin( val time.Duration, min time.Duration, max time.Duration) time.Duration { From 62f6ff9d6cc414d7c424281c7a6f40e02776b044 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 19 Aug 2016 03:04:20 +0900 Subject: [PATCH 243/658] [vim] Make arguments to fzf#wrap() optional fzf#wrap([name string,] [opts dict,] [fullscreen boolean]) --- README.md | 2 +- plugin/fzf.vim | 25 ++++++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5de7303..7f0dd7a 100644 --- a/README.md +++ b/README.md @@ -344,7 +344,7 @@ page](https://github.com/junegunn/fzf/wiki/Examples-(vim)). #### `fzf#wrap` -`fzf#wrap(name string, [opts dict, [fullscreen boolean]])` is a helper +`fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper function that decorates the options dictionary so that it understands `g:fzf_layout`, `g:fzf_action`, and `g:fzf_history_dir` like `:FZF`. diff --git a/plugin/fzf.vim b/plugin/fzf.vim index b7e7dc4..b9d39cd 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -154,13 +154,20 @@ function! s:common_sink(action, lines) abort endtry endfunction -" name string, [opts dict, [fullscreen boolean]] -function! fzf#wrap(name, ...) - if type(a:name) != type('') - throw 'invalid name type: string expected' - endif - let opts = copy(get(a:000, 0, {})) - let bang = get(a:000, 1, 0) +" [name string,] [opts dict,] [fullscreen boolean] +function! fzf#wrap(...) + let args = ['', {}, 0] + let expects = map(copy(args), 'type(v:val)') + let tidx = 0 + for arg in copy(a:000) + let tidx = index(expects, type(arg), tidx) + if tidx < 0 + throw 'invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])' + endif + let args[tidx] = arg + let tidx += 1 + endfor + let [name, opts, bang] = args " Layout: g:fzf_layout (and deprecated g:fzf_height) if bang @@ -179,12 +186,12 @@ function! fzf#wrap(name, ...) " History: g:fzf_history_dir let opts.options = get(opts, 'options', '') - if len(get(g:, 'fzf_history_dir', '')) + if len(name) && len(get(g:, 'fzf_history_dir', '')) let dir = expand(g:fzf_history_dir) if !isdirectory(dir) call mkdir(dir, 'p') endif - let opts.options = join(['--history', s:escape(dir.'/'.a:name), opts.options]) + let opts.options = join(['--history', s:escape(dir.'/'.name), opts.options]) endif " Action: g:fzf_action From 608c41620755041d6fc216ae43de8b21d56c969c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 19 Aug 2016 03:27:42 +0900 Subject: [PATCH 244/658] Add missing sources --- src/result.go | 259 +++++++++++++++++++++++++++++++++++++++++++++ src/result_test.go | 114 ++++++++++++++++++++ 2 files changed, 373 insertions(+) create mode 100644 src/result.go create mode 100644 src/result_test.go diff --git a/src/result.go b/src/result.go new file mode 100644 index 0000000..c295e7a --- /dev/null +++ b/src/result.go @@ -0,0 +1,259 @@ +package fzf + +import ( + "math" + "sort" + + "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/util" +) + +// Offset holds two 32-bit integers denoting the offsets of a matched substring +type Offset [2]int32 + +type colorOffset struct { + offset [2]int32 + color int + bold bool + index int32 +} + +type rank struct { + index int32 + // byMatchLen, byBonus, ... + points [5]uint16 +} + +type Result struct { + item *Item + offsets []Offset + rank rank +} + +func buildResult(item *Item, offsets []Offset, bonus int, trimLen int) *Result { + if len(offsets) > 1 { + sort.Sort(ByOrder(offsets)) + } + + result := Result{item: item, offsets: offsets, rank: rank{index: item.index}} + + matchlen := 0 + prevEnd := 0 + minBegin := math.MaxInt32 + numChars := item.text.Length() + for _, offset := range offsets { + begin := int(offset[0]) + end := int(offset[1]) + if prevEnd > begin { + begin = prevEnd + } + if end > prevEnd { + prevEnd = end + } + if end > begin { + if begin < minBegin { + minBegin = begin + } + matchlen += end - begin + } + } + + for idx, criterion := range sortCriteria { + var val uint16 + switch criterion { + case byMatchLen: + if matchlen == 0 { + val = math.MaxUint16 + } else { + val = util.AsUint16(matchlen) + } + case byBonus: + // Higher is better + val = math.MaxUint16 - util.AsUint16(bonus) + case byLength: + // If offsets is empty, trimLen will be 0, but we don't care + val = util.AsUint16(trimLen) + case byBegin: + // We can't just look at item.offsets[0][0] because it can be an inverse term + whitePrefixLen := 0 + for idx := 0; idx < numChars; idx++ { + r := item.text.Get(idx) + whitePrefixLen = idx + if idx == minBegin || r != ' ' && r != '\t' { + break + } + } + val = util.AsUint16(minBegin - whitePrefixLen) + case byEnd: + if prevEnd > 0 { + val = util.AsUint16(1 + numChars - prevEnd) + } else { + // Empty offsets due to inverse terms. + val = 1 + } + } + result.rank.points[idx] = val + } + + return &result +} + +// Sort criteria to use. Never changes once fzf is started. +var sortCriteria []criterion + +// Index returns ordinal index of the Item +func (result *Result) Index() int32 { + return result.item.index +} + +func minRank() rank { + return rank{index: 0, points: [5]uint16{0, math.MaxUint16, 0, 0, 0}} +} + +func (result *Result) colorOffsets(color int, bold bool, current bool) []colorOffset { + itemColors := result.item.Colors() + + if len(itemColors) == 0 { + var offsets []colorOffset + for _, off := range result.offsets { + + offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold}) + } + return offsets + } + + // Find max column + var maxCol int32 + for _, off := range result.offsets { + if off[1] > maxCol { + maxCol = off[1] + } + } + for _, ansi := range itemColors { + if ansi.offset[1] > maxCol { + maxCol = ansi.offset[1] + } + } + cols := make([]int, maxCol) + + for colorIndex, ansi := range itemColors { + for i := ansi.offset[0]; i < ansi.offset[1]; i++ { + cols[i] = colorIndex + 1 // XXX + } + } + + for _, off := range result.offsets { + for i := off[0]; i < off[1]; i++ { + cols[i] = -1 + } + } + + // sort.Sort(ByOrder(offsets)) + + // Merge offsets + // ------------ ---- -- ---- + // ++++++++ ++++++++++ + // --++++++++-- --++++++++++--- + curr := 0 + start := 0 + var colors []colorOffset + add := func(idx int) { + if curr != 0 && idx > start { + if curr == -1 { + colors = append(colors, colorOffset{ + offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold}) + } else { + ansi := itemColors[curr-1] + fg := ansi.color.fg + if fg == -1 { + if current { + fg = curses.CurrentFG + } else { + fg = curses.FG + } + } + bg := ansi.color.bg + if bg == -1 { + if current { + bg = curses.DarkBG + } else { + bg = curses.BG + } + } + colors = append(colors, colorOffset{ + offset: [2]int32{int32(start), int32(idx)}, + color: curses.PairFor(fg, bg), + bold: ansi.color.bold || bold}) + } + } + } + for idx, col := range cols { + if col != curr { + add(idx) + start = idx + curr = col + } + } + add(int(maxCol)) + return colors +} + +// ByOrder is for sorting substring offsets +type ByOrder []Offset + +func (a ByOrder) Len() int { + return len(a) +} + +func (a ByOrder) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ByOrder) Less(i, j int) bool { + ioff := a[i] + joff := a[j] + return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1]) +} + +// ByRelevance is for sorting Items +type ByRelevance []*Result + +func (a ByRelevance) Len() int { + return len(a) +} + +func (a ByRelevance) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ByRelevance) Less(i, j int) bool { + return compareRanks((*a[i]).rank, (*a[j]).rank, false) +} + +// ByRelevanceTac is for sorting Items +type ByRelevanceTac []*Result + +func (a ByRelevanceTac) Len() int { + return len(a) +} + +func (a ByRelevanceTac) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func (a ByRelevanceTac) Less(i, j int) bool { + return compareRanks((*a[i]).rank, (*a[j]).rank, true) +} + +func compareRanks(irank rank, jrank rank, tac bool) bool { + for idx := 0; idx < 5; idx++ { + left := irank.points[idx] + right := jrank.points[idx] + if left < right { + return true + } else if left > right { + return false + } + } + return (irank.index <= jrank.index) != tac +} diff --git a/src/result_test.go b/src/result_test.go new file mode 100644 index 0000000..c8478fd --- /dev/null +++ b/src/result_test.go @@ -0,0 +1,114 @@ +package fzf + +import ( + "math" + "sort" + "testing" + + "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/util" +) + +func TestOffsetSort(t *testing.T) { + offsets := []Offset{ + Offset{3, 5}, Offset{2, 7}, + Offset{1, 3}, Offset{2, 9}} + sort.Sort(ByOrder(offsets)) + + if offsets[0][0] != 1 || offsets[0][1] != 3 || + offsets[1][0] != 2 || offsets[1][1] != 7 || + offsets[2][0] != 2 || offsets[2][1] != 9 || + offsets[3][0] != 3 || offsets[3][1] != 5 { + t.Error("Invalid order:", offsets) + } +} + +func TestRankComparison(t *testing.T) { + rank := func(vals ...uint16) rank { + return rank{ + points: [5]uint16{vals[0], 0, vals[1], vals[2], vals[3]}, + index: int32(vals[4])} + } + if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) || + !compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) || + !compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), false) || + !compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) { + t.Error("Invalid order") + } + + if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), true) || + !compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) || + !compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), true) || + !compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) { + t.Error("Invalid order (tac)") + } +} + +// Match length, string length, index +func TestResultRank(t *testing.T) { + // FIXME global + sortCriteria = []criterion{byMatchLen, byBonus, byLength} + + strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} + item1 := buildResult(&Item{text: util.RunesToChars(strs[0]), index: 1}, []Offset{}, 2, 3) + if item1.rank.points[0] != math.MaxUint16 || item1.rank.points[1] != math.MaxUint16-2 || item1.rank.points[2] != 3 || item1.item.index != 1 { + t.Error(item1.rank) + } + // Only differ in index + item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2, 3) + + items := []*Result{item1, item2} + sort.Sort(ByRelevance(items)) + if items[0] != item2 || items[1] != item1 { + t.Error(items) + } + + items = []*Result{item2, item1, item1, item2} + sort.Sort(ByRelevance(items)) + if items[0] != item2 || items[1] != item2 || + items[2] != item1 || items[3] != item1 { + t.Error(items, item1, item1.item.index, item2, item2.item.index) + } + + // Sort by relevance + item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 0, 0) + item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 0, 0) + item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 0, 0) + item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 0, 0) + items = []*Result{item1, item2, item3, item4, item5, item6} + sort.Sort(ByRelevance(items)) + if items[0] != item6 || items[1] != item4 || + items[2] != item5 || items[3] != item3 || + items[4] != item2 || items[5] != item1 { + t.Error(items) + } +} + +func TestColorOffset(t *testing.T) { + // ------------ 20 ---- -- ---- + // ++++++++ ++++++++++ + // --++++++++-- --++++++++++--- + item := Result{ + offsets: []Offset{Offset{5, 15}, Offset{25, 35}}, + item: &Item{ + colors: &[]ansiOffset{ + ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}}, + ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}}, + ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}}, + ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}} + // [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}] + + offsets := item.colorOffsets(99, false, true) + assert := func(idx int, b int32, e int32, c int, bold bool) { + o := offsets[idx] + if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold { + t.Error(o) + } + } + assert(0, 0, 5, curses.ColUser, false) + assert(1, 5, 15, 99, false) + assert(2, 15, 20, curses.ColUser, false) + assert(3, 22, 25, curses.ColUser+1, true) + assert(4, 25, 35, 99, false) + assert(5, 35, 40, curses.ColUser+2, true) +} From 3e888493866d42845f11c4d57fc8f093a6e28644 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 19 Aug 2016 18:02:32 +0900 Subject: [PATCH 245/658] [vim] Fix "E706: Variable type mismatch for: arg" --- plugin/fzf.vim | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index b9d39cd..2da98b5 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -166,6 +166,7 @@ function! fzf#wrap(...) endif let args[tidx] = arg let tidx += 1 + unlet arg endfor let [name, opts, bang] = args From 827a83efbc178390ddd8aaa36d26bce593d0d58f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 20 Aug 2016 01:46:54 +0900 Subject: [PATCH 246/658] Remove Offset slice from Result struct --- src/core.go | 8 +++++--- src/matcher.go | 2 +- src/merger.go | 13 ++++++++----- src/merger_test.go | 17 +++++------------ src/pattern.go | 15 ++++++++------- src/pattern_test.go | 8 +++++++- src/result.go | 17 ++++++++--------- src/result_test.go | 7 ++++--- src/terminal.go | 14 +++++++++----- 9 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/core.go b/src/core.go index 290c0af..78dddc0 100644 --- a/src/core.go +++ b/src/core.go @@ -161,9 +161,11 @@ func Run(opts *Options) { reader := Reader{ func(runes []byte) bool { item := chunkList.trans(runes, 0) - if item != nil && pattern.MatchItem(item) != nil { - fmt.Println(item.text.ToString()) - found = true + if item != nil { + if result, _ := pattern.MatchItem(item); result != nil { + fmt.Println(item.text.ToString()) + found = true + } } return false }, eventBox, opts.ReadZero} diff --git a/src/matcher.go b/src/matcher.go index 8cd9a9f..9814ba9 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -211,7 +211,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { partialResult := <-resultChan partialResults[partialResult.index] = partialResult.matches } - return NewMerger(partialResults, m.sort, m.tac), false + return NewMerger(pattern, partialResults, m.sort, m.tac), false } // Reset is called to interrupt/signal the ongoing search diff --git a/src/merger.go b/src/merger.go index 3879ab7..2c7675d 100644 --- a/src/merger.go +++ b/src/merger.go @@ -3,11 +3,12 @@ package fzf import "fmt" // EmptyMerger is a Merger with no data -var EmptyMerger = NewMerger([][]*Result{}, false, false) +var EmptyMerger = NewMerger(nil, [][]*Result{}, false, false) // Merger holds a set of locally sorted lists of items and provides the view of // a single, globally-sorted list type Merger struct { + pattern *Pattern lists [][]*Result merged []*Result chunks *[]*Chunk @@ -22,9 +23,10 @@ type Merger struct { // original order func PassMerger(chunks *[]*Chunk, tac bool) *Merger { mg := Merger{ - chunks: chunks, - tac: tac, - count: 0} + pattern: nil, + chunks: chunks, + tac: tac, + count: 0} for _, chunk := range *mg.chunks { mg.count += len(*chunk) @@ -33,8 +35,9 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger { } // NewMerger returns a new Merger -func NewMerger(lists [][]*Result, sorted bool, tac bool) *Merger { +func NewMerger(pattern *Pattern, lists [][]*Result, sorted bool, tac bool) *Merger { mg := Merger{ + pattern: pattern, lists: lists, merged: []*Result{}, chunks: nil, diff --git a/src/merger_test.go b/src/merger_test.go index d50470f..a4adee1 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -17,16 +17,9 @@ func assert(t *testing.T, cond bool, msg ...string) { func randResult() *Result { str := fmt.Sprintf("%d", rand.Uint32()) - offsets := make([]Offset, rand.Int()%3) - for idx := range offsets { - sidx := int32(rand.Uint32() % 20) - eidx := sidx + int32(rand.Uint32()%20) - offsets[idx] = Offset{sidx, eidx} - } return &Result{ - item: &Item{text: util.RunesToChars([]rune(str))}, - offsets: offsets, - rank: rank{index: rand.Int31()}} + item: &Item{text: util.RunesToChars([]rune(str))}, + rank: rank{index: rand.Int31()}} } func TestEmptyMerger(t *testing.T) { @@ -64,7 +57,7 @@ func TestMergerUnsorted(t *testing.T) { cnt := len(items) // Not sorted: same order - mg := NewMerger(lists, false, false) + mg := NewMerger(nil, lists, false, false) assert(t, cnt == mg.Length(), "Invalid Length") for i := 0; i < cnt; i++ { assert(t, items[i] == mg.Get(i), "Invalid Get") @@ -76,7 +69,7 @@ func TestMergerSorted(t *testing.T) { cnt := len(items) // Sorted sorted order - mg := NewMerger(lists, true, false) + mg := NewMerger(nil, lists, true, false) assert(t, cnt == mg.Length(), "Invalid Length") sort.Sort(ByRelevance(items)) for i := 0; i < cnt; i++ { @@ -86,7 +79,7 @@ func TestMergerSorted(t *testing.T) { } // Inverse order - mg2 := NewMerger(lists, true, false) + mg2 := NewMerger(nil, lists, true, false) for i := cnt - 1; i >= 0; i-- { if items[i] != mg2.Get(i) { t.Error("Not sorted", items[i], mg2.Get(i)) diff --git a/src/pattern.go b/src/pattern.go index 4c35a12..ef14826 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -273,13 +273,13 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []*Result) []*Result { if space == nil { for _, item := range *chunk { - if match := p.MatchItem(item); match != nil { + if match, _ := p.MatchItem(item); match != nil { matches = append(matches, match) } } } else { for _, result := range space { - if match := p.MatchItem(result.item); match != nil { + if match, _ := p.MatchItem(result.item); match != nil { matches = append(matches, match) } } @@ -288,18 +288,19 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []*Result) []*Result { } // MatchItem returns true if the Item is a match -func (p *Pattern) MatchItem(item *Item) *Result { +func (p *Pattern) MatchItem(item *Item) (*Result, []Offset) { if p.extended { if offsets, bonus, trimLen := p.extendedMatch(item); len(offsets) == len(p.termSets) { - return buildResult(item, offsets, bonus, trimLen) + return buildResult(item, offsets, bonus, trimLen), offsets } - return nil + return nil, nil } offset, bonus, trimLen := p.basicMatch(item) if sidx := offset[0]; sidx >= 0 { - return buildResult(item, []Offset{offset}, bonus, trimLen) + offsets := []Offset{offset} + return buildResult(item, offsets, bonus, trimLen), offsets } - return nil + return nil, nil } func (p *Pattern) basicMatch(item *Item) (Offset, int, int) { diff --git a/src/pattern_test.go b/src/pattern_test.go index 3069110..2a391f4 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -135,10 +135,16 @@ func TestOrigTextAndTransformed(t *testing.T) { pattern.extended = extended matches := pattern.matchChunk(&chunk, nil) // No cache if matches[0].item.text.ToString() != "junegunn" || string(*matches[0].item.origText) != "junegunn.choi" || - matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || !reflect.DeepEqual(matches[0].item.transformed, trans) { t.Error("Invalid match result", matches) } + + match, offsets := pattern.MatchItem(chunk[0]) + if match.item.text.ToString() != "junegunn" || string(*match.item.origText) != "junegunn.choi" || + offsets[0][0] != 0 || offsets[0][1] != 5 || + !reflect.DeepEqual(match.item.transformed, trans) { + t.Error("Invalid match result", match) + } } } diff --git a/src/result.go b/src/result.go index c295e7a..2b66a02 100644 --- a/src/result.go +++ b/src/result.go @@ -19,15 +19,14 @@ type colorOffset struct { } type rank struct { - index int32 // byMatchLen, byBonus, ... points [5]uint16 + index int32 } type Result struct { - item *Item - offsets []Offset - rank rank + item *Item + rank rank } func buildResult(item *Item, offsets []Offset, bonus int, trimLen int) *Result { @@ -35,7 +34,7 @@ func buildResult(item *Item, offsets []Offset, bonus int, trimLen int) *Result { sort.Sort(ByOrder(offsets)) } - result := Result{item: item, offsets: offsets, rank: rank{index: item.index}} + result := Result{item: item, rank: rank{index: item.index}} matchlen := 0 prevEnd := 0 @@ -110,12 +109,12 @@ func minRank() rank { return rank{index: 0, points: [5]uint16{0, math.MaxUint16, 0, 0, 0}} } -func (result *Result) colorOffsets(color int, bold bool, current bool) []colorOffset { +func (result *Result) colorOffsets(matchOffsets []Offset, color int, bold bool, current bool) []colorOffset { itemColors := result.item.Colors() if len(itemColors) == 0 { var offsets []colorOffset - for _, off := range result.offsets { + for _, off := range matchOffsets { offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold}) } @@ -124,7 +123,7 @@ func (result *Result) colorOffsets(color int, bold bool, current bool) []colorOf // Find max column var maxCol int32 - for _, off := range result.offsets { + for _, off := range matchOffsets { if off[1] > maxCol { maxCol = off[1] } @@ -142,7 +141,7 @@ func (result *Result) colorOffsets(color int, bold bool, current bool) []colorOf } } - for _, off := range result.offsets { + for _, off := range matchOffsets { for i := off[0]; i < off[1]; i++ { cols[i] = -1 } diff --git a/src/result_test.go b/src/result_test.go index c8478fd..60551fd 100644 --- a/src/result_test.go +++ b/src/result_test.go @@ -88,8 +88,9 @@ func TestColorOffset(t *testing.T) { // ------------ 20 ---- -- ---- // ++++++++ ++++++++++ // --++++++++-- --++++++++++--- + + offsets := []Offset{Offset{5, 15}, Offset{25, 35}} item := Result{ - offsets: []Offset{Offset{5, 15}, Offset{25, 35}}, item: &Item{ colors: &[]ansiOffset{ ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}}, @@ -98,9 +99,9 @@ func TestColorOffset(t *testing.T) { ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}} // [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}] - offsets := item.colorOffsets(99, false, true) + colors := item.colorOffsets(offsets, 99, false, true) assert := func(idx int, b int32, e int32, c int, bold bool) { - o := offsets[idx] + o := colors[idx] if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold { t.Error(o) } diff --git a/src/terminal.go b/src/terminal.go index f7bbccf..d87a20b 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -669,15 +669,19 @@ func overflow(runes []rune, max int) bool { func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 int, current bool) { item := result.item - var maxe int - for _, offset := range result.offsets { - maxe = util.Max(maxe, int(offset[1])) - } // Overflow text := make([]rune, item.text.Length()) copy(text, item.text.ToRunes()) - offsets := result.colorOffsets(col2, bold, current) + matchOffsets := []Offset{} + if t.merger.pattern != nil { + _, matchOffsets = t.merger.pattern.MatchItem(item) + } + var maxe int + for _, offset := range matchOffsets { + maxe = util.Max(maxe, int(offset[1])) + } + offsets := result.colorOffsets(matchOffsets, col2, bold, current) maxWidth := t.window.Width - 3 maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) if overflow(text, maxWidth) { From f8fdf9618adf831b614a9b69e90eca53b9e37a28 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 20 Aug 2016 02:06:57 +0900 Subject: [PATCH 247/658] No need to cache the result in filtering mode (--filter) --- src/core.go | 2 +- src/pattern.go | 6 +++--- src/pattern_test.go | 20 ++++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/core.go b/src/core.go index 78dddc0..70f2da5 100644 --- a/src/core.go +++ b/src/core.go @@ -143,7 +143,7 @@ func Run(opts *Options) { } patternBuilder := func(runes []rune) *Pattern { return BuildPattern( - opts.Fuzzy, opts.Extended, opts.Case, forward, + opts.Fuzzy, opts.Extended, opts.Case, forward, opts.Filter == nil, opts.Nth, opts.Delimiter, runes) } matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) diff --git a/src/pattern.go b/src/pattern.go index ef14826..faafa0b 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -75,7 +75,7 @@ func clearChunkCache() { // BuildPattern builds Pattern object from the given arguments func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool, - nth []Range, delimiter Delimiter, runes []rune) *Pattern { + cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { var asString string if extended { @@ -89,7 +89,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool, return cached } - caseSensitive, cacheable := true, true + caseSensitive := true termSets := []termSet{} if extended { @@ -99,7 +99,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool, for idx, term := range termSet { // If the query contains inverse search terms or OR operators, // we cannot cache the search scope - if idx > 0 || term.inv { + if !cacheable || idx > 0 || term.inv { cacheable = false break Loop } diff --git a/src/pattern_test.go b/src/pattern_test.go index 2a391f4..43bc5e0 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -69,7 +69,7 @@ func TestParseTermsEmpty(t *testing.T) { func TestExact(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(true, true, CaseSmart, true, + pattern := BuildPattern(true, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("'abc")) res := algo.ExactMatchNaive( pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text) @@ -81,7 +81,7 @@ func TestExact(t *testing.T) { func TestEqual(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$")) + pattern := BuildPattern(true, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("^AbC$")) match := func(str string, sidxExpected int, eidxExpected int) { res := algo.EqualMatch( @@ -97,17 +97,17 @@ func TestEqual(t *testing.T) { func TestCaseSensitivity(t *testing.T) { defer clearPatternCache() clearPatternCache() - pat1 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("abc")) + pat1 := BuildPattern(true, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat2 := BuildPattern(true, false, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc")) + pat2 := BuildPattern(true, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat3 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc")) + pat3 := BuildPattern(true, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat4 := BuildPattern(true, false, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc")) + pat4 := BuildPattern(true, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat5 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc")) + pat5 := BuildPattern(true, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat6 := BuildPattern(true, false, CaseRespect, true, []Range{}, Delimiter{}, []rune("Abc")) + pat6 := BuildPattern(true, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("Abc")) if string(pat1.text) != "abc" || pat1.caseSensitive != false || string(pat2.text) != "Abc" || pat2.caseSensitive != true || @@ -120,7 +120,7 @@ func TestCaseSensitivity(t *testing.T) { } func TestOrigTextAndTransformed(t *testing.T) { - pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg")) + pattern := BuildPattern(true, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("jg")) tokens := Tokenize(util.RunesToChars([]rune("junegunn")), Delimiter{}) trans := Transform(tokens, []Range{Range{1, 1}}) @@ -150,7 +150,7 @@ func TestOrigTextAndTransformed(t *testing.T) { func TestCacheKey(t *testing.T) { test := func(extended bool, patStr string, expected string, cacheable bool) { - pat := BuildPattern(true, extended, CaseSmart, true, []Range{}, Delimiter{}, []rune(patStr)) + pat := BuildPattern(true, extended, CaseSmart, true, true, []Range{}, Delimiter{}, []rune(patStr)) if pat.CacheKey() != expected { t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) } From 1f7d1f9b151131a01f661b4be70aac068db17f93 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 21 Aug 2016 04:54:53 +0900 Subject: [PATCH 248/658] Update Centos Dockerfile to use Go 1.7 --- src/Dockerfile.centos | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos index 2024482..4ae27b7 100644 --- a/src/Dockerfile.centos +++ b/src/Dockerfile.centos @@ -11,18 +11,18 @@ RUN cd / && curl \ https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ tar -xz && mv go go1.4 -# Install Go 1.5 +# Install Go 1.7 RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz | \ - tar -xz && mv go go1.5 + https://storage.googleapis.com/golang/go1.7.linux-amd64.tar.gz | \ + tar -xz && mv go go1.7 # Install RPMs for building static 32-bit binary -RUN curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.7/os/i386/Packages/ncurses-static-5.7-4.20090207.el6.i686.rpm -o rpm && rpm -i rpm && \ - curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.7/os/i386/Packages/gpm-static-1.20.6-12.el6.i686.rpm -o rpm && rpm -i rpm +RUN curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.8/os/i386/Packages/ncurses-static-5.7-4.20090207.el6.i686.rpm -o rpm && rpm -i rpm && \ + curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.8/os/i386/Packages/gpm-static-1.20.6-12.el6.i686.rpm -o rpm && rpm -i rpm ENV GOROOT_BOOTSTRAP /go1.4 -ENV GOROOT /go1.5 -ENV PATH /go1.5/bin:$PATH +ENV GOROOT /go1.7 +ENV PATH /go1.7/bin:$PATH # For i386 build RUN cd $GOROOT/src && GOARCH=386 ./make.bash From b86838c2b088ab7c7d80b6103a5fa59d6172ce10 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 21 Aug 2016 05:00:27 +0900 Subject: [PATCH 249/658] 0.13.5 --- CHANGELOG.md | 5 +++++ install | 4 ++-- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- src/update_assets.rb | 1 + 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22546f6..6feff3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +0.13.5 +------ +- Memory and performance optimization + - Up to 2x performance with half the amount of memory + 0.13.4 ------ - Performance optimization diff --git a/install b/install index 6e77d52..6744787 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.13.4 pre=1 || - version=0.13.4 pre=0 +[[ "$@" =~ --pre ]] && version=0.13.5 pre=1 || + version=0.13.5 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 5eabb5e..7be7317 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Aug 2016" "fzf 0.13.4" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Aug 2016" "fzf 0.13.5" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index c6b2dea..69c360f 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Aug 2016" "fzf 0.13.4" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Aug 2016" "fzf 0.13.5" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 74c59fa..c041086 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.13.4" + version = "0.13.5" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond diff --git a/src/update_assets.rb b/src/update_assets.rb index c849f9e..2fd9edc 100755 --- a/src/update_assets.rb +++ b/src/update_assets.rb @@ -2,6 +2,7 @@ # http://www.rubydoc.info/github/rest-client/rest-client/RestClient require 'rest_client' +require 'json' if ARGV.length < 3 puts "usage: #$0 " From f958c9daf59439240c36d74f42187333c23bd8a4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 24 Aug 2016 01:15:35 +0900 Subject: [PATCH 250/658] [vim] Tilde prefix is not allowed for `left` or `right` layout --- plugin/fzf.vim | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 2da98b5..abaa420 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -276,10 +276,10 @@ function! s:fzf_tmux(dict) if s:present(a:dict, o) let spec = a:dict[o] if (o == 'up' || o == 'down') && spec[0] == '~' - let size = '-'.o[0].s:calc_size(&lines, spec[1:], a:dict) + let size = '-'.o[0].s:calc_size(&lines, spec, a:dict) else " Legacy boolean option - let size = '-'.o[0].(spec == 1 ? '' : spec) + let size = '-'.o[0].(spec == 1 ? '' : substitute(spec, '^\~', '', '')) endif break endif @@ -375,10 +375,11 @@ function! s:execute_tmux(dict, command, temps) abort endfunction function! s:calc_size(max, val, dict) - if a:val =~ '%$' - let size = a:max * str2nr(a:val[:-2]) / 100 + let val = substitute(a:val, '^\~', '', '') + if val =~ '%$' + let size = a:max * str2nr(val[:-2]) / 100 else - let size = min([a:max, str2nr(a:val)]) + let size = min([a:max, str2nr(val)]) endif let srcsz = -1 @@ -409,7 +410,7 @@ function! s:split(dict) if !empty(val) let [cmd, resz, max] = triple if (dir == 'up' || dir == 'down') && val[0] == '~' - let sz = s:calc_size(max, val[1:], a:dict) + let sz = s:calc_size(max, val, a:dict) else let sz = s:calc_size(max, val, {}) endif From f025602841384fb10cc80d37492ec44c2b872860 Mon Sep 17 00:00:00 2001 From: Joshua Rubin Date: Thu, 25 Aug 2016 01:08:27 -0600 Subject: [PATCH 251/658] [vim] Reset window sizes on close Fix #520 Fix junegunn/fzf.vim#42 --- plugin/fzf.vim | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index abaa420..4b554e9 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -431,9 +431,11 @@ function! s:split(dict) endfunction function! s:execute_term(dict, command, temps) abort + let winrest = winrestcmd() let [ppos, winopts] = s:split(a:dict) let fzf = { 'buf': bufnr('%'), 'ppos': ppos, 'dict': a:dict, 'temps': a:temps, - \ 'winopts': winopts, 'command': a:command } + \ 'winopts': winopts, 'winrest': winrest, 'lines': &lines, + \ 'columns': &columns, 'command': a:command } function! fzf.switch_back(inplace) if a:inplace && bufnr('') == self.buf " FIXME: Can't re-enter normal mode from terminal mode @@ -465,6 +467,10 @@ function! s:execute_term(dict, command, temps) abort execute 'bd!' self.buf endif + if &lines <= self.lines && &columns <= self.columns && s:getpos() == self.ppos + execute self.winrest + endif + if !s:exit_handler(a:code, self.command, 1) return endif From d87390934e161ad2ae09b4dc37f4b529a84caa92 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 28 Aug 2016 19:26:47 +0900 Subject: [PATCH 252/658] [neovim] Do not resize if the size of the screen has changed Related #642 --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 4b554e9..8e3859d 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -467,7 +467,7 @@ function! s:execute_term(dict, command, temps) abort execute 'bd!' self.buf endif - if &lines <= self.lines && &columns <= self.columns && s:getpos() == self.ppos + if &lines == self.lines && &columns == self.columns && s:getpos() == self.ppos execute self.winrest endif From 639de4c27bb29425cd2c51680fa79d53fcffc4e4 Mon Sep 17 00:00:00 2001 From: Tobias Frilling Date: Sun, 28 Aug 2016 20:34:36 +0200 Subject: [PATCH 253/658] Split default zsh binding at the correct place The command substitution and following word splitting to determine the default zle widget for ^I formerly only works if the IFS parameter contains a space. Now it specifically splits at spaces, regardless of IFS. --- shell/completion.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 623602f..cef8afa 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -186,7 +186,7 @@ fzf-completion() { [ -z "$fzf_default_completion" ] && { binding=$(bindkey '^I') - [[ $binding =~ 'undefined-key' ]] || fzf_default_completion=$binding[(w)2] + [[ $binding =~ 'undefined-key' ]] || fzf_default_completion=$binding[(s: :w)2] unset binding } From 4d4447779fe4c3ff05c2e6809c1b0c5d754d9772 Mon Sep 17 00:00:00 2001 From: Bryan Head Date: Sun, 28 Aug 2016 16:57:38 -0500 Subject: [PATCH 254/658] Unset TMUX before splitting window Avoids confusing grouped sessions. Fixes #643 --- bin/fzf-tmux | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index c7701e9..f6742e4 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -161,14 +161,14 @@ done if [[ -n "$term" ]] || [[ -t 0 ]]; then cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf - tmux set-window-option synchronize-panes off \;\ + TMUX='' tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \ > /dev/null 2>&1 else mkfifo $fifo1 cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf - tmux set-window-option synchronize-panes off \;\ + TMUX='' tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ split-window $opt "$envs bash $argsf" $swap \ > /dev/null 2>&1 From 22b089e47ec58cb459d5129f0ab98cf3ce86ab45 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 31 Aug 2016 14:20:29 +0900 Subject: [PATCH 255/658] Revert "Unset TMUX before splitting window" (#648) This reverts commit 4d4447779fe4c3ff05c2e6809c1b0c5d754d9772. --- bin/fzf-tmux | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index f6742e4..c7701e9 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -161,14 +161,14 @@ done if [[ -n "$term" ]] || [[ -t 0 ]]; then cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf - TMUX='' tmux set-window-option synchronize-panes off \;\ + tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \ > /dev/null 2>&1 else mkfifo $fifo1 cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf - TMUX='' tmux set-window-option synchronize-panes off \;\ + tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ split-window $opt "$envs bash $argsf" $swap \ > /dev/null 2>&1 From 9baf7c48747f78a0bd850fc90610195c45da003f Mon Sep 17 00:00:00 2001 From: Bryan Head Date: Tue, 6 Sep 2016 13:03:07 -0500 Subject: [PATCH 256/658] Make fzf target correct session in group Fixes #643 Doesn't break #648 --- bin/fzf-tmux | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index c7701e9..ff923f0 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -161,14 +161,14 @@ done if [[ -n "$term" ]] || [[ -t 0 ]]; then cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf - tmux set-window-option synchronize-panes off \;\ + TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \ > /dev/null 2>&1 else mkfifo $fifo1 cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf - tmux set-window-option synchronize-panes off \;\ + TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ split-window $opt "$envs bash $argsf" $swap \ > /dev/null 2>&1 From f44d40f6b4591538c4eb15414f2024eaaa65a793 Mon Sep 17 00:00:00 2001 From: ishanray Date: Sat, 10 Sep 2016 23:40:55 +0400 Subject: [PATCH 257/658] Update algo.go --- src/algo/algo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 00265c6..8972396 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -22,7 +22,7 @@ func indexAt(index int, max int, forward bool) int { return max - index - 1 } -// Result conatins the results of running a match function. +// Result contains the results of running a match function. type Result struct { Start int End int From 8ef2420677abf5cca27b47bead6e70e42220c7aa Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 13 Sep 2016 04:11:28 +0900 Subject: [PATCH 258/658] Update README --- README.md | 37 ++++++++++++++++++++++--------------- src/README.md | 38 -------------------------------------- 2 files changed, 22 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 7f0dd7a..5a90bdb 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,15 @@ Pros - No dependencies - Blazingly fast - - e.g. `locate / | fzf` -- Flexible layout - - Runs in fullscreen or in horizontal/vertical split using tmux - The most comprehensive feature set - - Try `fzf --help` and be surprised +- Flexible layout using tmux panes - Batteries included - Vim/Neovim plugin, key bindings and fuzzy auto-completion Installation ------------ -fzf project consists of the following: +fzf project consists of the following components: - `fzf` executable - `fzf-tmux` script for launching fzf in a tmux pane @@ -30,12 +27,12 @@ fzf project consists of the following: - Fuzzy auto-completion (bash, zsh) - Vim/Neovim plugin -You can [download fzf executable][bin] alone, but it's recommended that you -install the extra stuff using the attached install script. +You can [download fzf executable][bin] alone if you don't need the extra +stuff. [bin]: https://github.com/junegunn/fzf-bin/releases -#### Using git (recommended) +### Using git Clone this repository and run [install](https://github.com/junegunn/fzf/blob/master/install) script. @@ -45,7 +42,7 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf ~/.fzf/install ``` -#### Using Homebrew +### Using Homebrew On OS X, you can use [Homebrew](http://brew.sh/) to install fzf. @@ -56,26 +53,30 @@ brew install fzf /usr/local/opt/fzf/install ``` -#### Install as Vim plugin +### Vim plugin -Once you have cloned the repository, add the following line to your .vimrc. +You can manually add the directory to `&runtimepath` as follows, ```vim +" If installed using git set rtp+=~/.fzf + +" If installed using Homebrew +set rtp+=/usr/local/opt/fzf ``` -Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf -(recommended): +But it's recommended that you use a plugin manager like +[vim-plug](https://github.com/junegunn/vim-plug). ```vim Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' } ``` -#### Upgrading fzf +### Upgrading fzf fzf is being actively developed and you might want to upgrade it once in a while. Please follow the instruction below depending on the installation -method. +method used. - git: `cd ~/.fzf && git pull && ./install` - brew: `brew update; brew reinstall fzf` @@ -390,6 +391,12 @@ fzf export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" ``` +If you don't want to exclude hidden files, use the following command: + +```sh +export FZF_DEFAULT_COMMAND='ag --hidden --ignore .git -g ""' +``` + #### `git ls-tree` for fast traversal If you're running fzf in a large git repository, `git ls-tree` can boost up the diff --git a/src/README.md b/src/README.md index 181b761..ced4296 100644 --- a/src/README.md +++ b/src/README.md @@ -47,33 +47,6 @@ proportional to the number of CPU cores. On my MacBook Pro (Mid 2012), the new version was shown to be an order of magnitude faster on certain cases. It also starts much faster though the difference may not be noticeable. -Differences with Ruby version ------------------------------ - -The Go version is designed to be perfectly compatible with the previous Ruby -version. The only behavioral difference is that the new version ignores the -numeric argument to `--sort=N` option and always sorts the result regardless -of the number of matches. The value was introduced to limit the response time -of the query, but the Go version is blazingly fast (almost instant response -even for 1M+ items) so I decided that it's no longer required. - -System requirements -------------------- - -Currently, prebuilt binaries are provided only for OS X and Linux. The install -script will fall back to the legacy Ruby version on the other systems, but if -you have Go 1.4 installed, you can try building it yourself. - -However, as pointed out in [golang.org/doc/install][req], the Go version may -not run on CentOS/RHEL 5.x, and if that's the case, the install script will -choose the Ruby version instead. - -The Go version depends on [ncurses][ncurses] and some Unix system calls, so it -shouldn't run natively on Windows at the moment. But it won't be impossible to -support Windows by falling back to a cross-platform alternative such as -[termbox][termbox] only on Windows. If you're interested in making fzf work on -Windows, please let me know. - Build ----- @@ -88,17 +61,6 @@ make install make linux ``` -Contribution ------------- - -For the time being, I will not add or accept any new features until we can be -sure that the implementation is stable and we have a sufficient number of test -cases. However, fixes for obvious bugs and new test cases are welcome. - -I also care much about the performance of the implementation, so please make -sure that your change does not result in performance regression. And please be -noted that we don't have a quantitative measure of the performance yet. - Third-party libraries used -------------------------- From 2fc7c18747250ebf8adf68d2057ec22af6976f29 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 7 Sep 2016 09:58:18 +0900 Subject: [PATCH 259/658] Revise ranking algorithm --- CHANGELOG.md | 7 + install | 4 +- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 12 +- src/README.md | 17 ++ src/algo/algo.go | 653 ++++++++++++++++++++++++++++++++++-------- src/algo/algo_test.go | 143 ++++++--- src/chunklist_test.go | 2 +- src/constants.go | 10 +- src/core.go | 7 +- src/matcher.go | 14 +- src/options.go | 29 +- src/pattern.go | 83 +++--- src/pattern_test.go | 61 ++-- src/result.go | 70 ++--- src/result_test.go | 26 +- src/terminal.go | 23 +- src/util/slab.go | 12 + src/util/util.go | 24 +- test/test_go.rb | 222 +++++--------- 20 files changed, 961 insertions(+), 460 deletions(-) create mode 100644 src/util/slab.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 6feff3e..dee7264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +0.15.0 +------ +- Improved fuzzy search algorithm + - Added `--algo=[v1|v2]` option so one can still choose the old algorithm + which values the search performance over the quality of the result +- Advanced scoring criteria + 0.13.5 ------ - Memory and performance optimization diff --git a/install b/install index 6744787..4d9e44a 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.13.5 pre=1 || - version=0.13.5 pre=0 +[[ "$@" =~ --pre ]] && version=0.15.0 pre=1 || + version=0.15.0 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 7be7317..92d25e9 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Aug 2016" "fzf 0.13.5" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Sep 2016" "fzf 0.15.0" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 69c360f..93882e3 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Aug 2016" "fzf 0.13.5" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Sep 2016" "fzf 0.15.0" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -47,6 +47,16 @@ Case-insensitive match (default: smart-case match) .TP .B "+i" Case-sensitive match +.TP +.BI "--algo=" TYPE +Fuzzy matching algorithm (default: v2) + +.br +.BR v2 " Optimal scoring algorithm (quality)" +.br +.BR v1 " Faster but not guaranteed to find the optimal result (performance)" +.br + .TP .BI "-n, --nth=" "N[,..]" Comma-separated list of field index expressions for limiting search scope. diff --git a/src/README.md b/src/README.md index ced4296..272c755 100644 --- a/src/README.md +++ b/src/README.md @@ -61,6 +61,23 @@ make install make linux ``` +Test +---- + +Unit tests can be run with `make test`. Integration tests are written in Ruby +script that should be run on tmux. + +```sh +# Unit tests +make test + +# Install the executable to ../bin directory +make install + +# Integration tests +ruby ../test/test_go.rb +``` + Third-party libraries used -------------------------- diff --git a/src/algo/algo.go b/src/algo/algo.go index 8972396..622c960 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -1,19 +1,91 @@ package algo +/* + +Algorithm +--------- + +FuzzyMatchV1 finds the first "fuzzy" occurrence of the pattern within the given +text in O(n) time where n is the length of the text. Once the position of the +last character is located, it traverses backwards to see if there's a shorter +substring that matches the pattern. + + a_____b___abc__ To find "abc" + *-----*-----*> 1. Forward scan + <*** 2. Backward scan + +The algorithm is simple and fast, but as it only sees the first occurrence, +it is not guaranteed to find the occurrence with the highest score. + + a_____b__c__abc + *-----*--* *** + +FuzzyMatchV2 implements a modified version of Smith-Waterman algorithm to find +the optimal solution (highest score) according to the scoring criteria. Unlike +the original algorithm, omission or mismatch of a character in the pattern is +not allowed. + +Performance +----------- + +The new V2 algorithm is slower than V1 as it examines all occurrences of the +pattern instead of stopping immediately after finding the first one. The time +complexity of the algorithm is O(nm) if a match is found and O(n) otherwise +where n is the length of the item and m is the length of the pattern. Thus, the +performance overhead may not be noticeable for a query with high selectivity. +However, if the performance is more important than the quality of the result, +you can still choose v1 algorithm with --algo=v1. + +Scoring criteria +---------------- + +- We prefer matches at special positions, such as the start of a word, or + uppercase character in camelCase words. + +- That is, we prefer an occurrence of the pattern with more characters + matching at special positions, even if the total match length is longer. + e.g. "fuzzyfinder" vs. "fuzzy-finder" on "ff" + ```````````` +- Also, if the first character in the pattern appears at one of the special + positions, the bonus point for the position is multiplied by a constant + as it is extremely likely that the first character in the typed pattern + has more significance than the rest. + e.g. "fo-bar" vs. "foob-r" on "br" + `````` +- But since fzf is still a fuzzy finder, not an acronym finder, we should also + consider the total length of the matched substring. This is why we have the + gap penalty. The gap penalty increases as the length of the gap (distance + between the matching characters) increases, so the effect of the bonus is + eventually cancelled at some point. + e.g. "fuzzyfinder" vs. "fuzzy-blurry-finder" on "ff" + ``````````` +- Consequently, it is crucial to find the right balance between the bonus + and the gap penalty. The parameters were chosen that the bonus is cancelled + when the gap size increases beyond 8 characters. + +- The bonus mechanism can have the undesirable side effect where consecutive + matches are ranked lower than the ones with gaps. + e.g. "foobar" vs. "foo-bar" on "foob" + ``````` +- To correct this anomaly, we also give extra bonus point to each character + in a consecutive matching chunk. + e.g. "foobar" vs. "foo-bar" on "foob" + `````` +- The amount of consecutive bonus is primarily determined by the bonus of the + first character in the chunk. + e.g. "foobar" vs. "out-of-bound" on "oob" + ```````````` +*/ + import ( + "fmt" "strings" "unicode" "github.com/junegunn/fzf/src/util" ) -/* - * String matching algorithms here do not use strings.ToLower to avoid - * performance penalty. And they assume pattern runes are given in lowercase - * letters when caseSensitive is false. - * - * In short: They try to do as little work as possible. - */ +var DEBUG bool func indexAt(index int, max int, forward bool) int { if forward { @@ -24,14 +96,48 @@ func indexAt(index int, max int, forward bool) int { // Result contains the results of running a match function. type Result struct { + // TODO int32 should suffice Start int End int - - // Items are basically sorted by the lengths of matched substrings. - // But we slightly adjust the score with bonus for better results. - Bonus int + Score int } +const ( + scoreMatch = 16 + scoreGapStart = -3 + scoreGapExtention = -1 + + // We prefer matches at the beginning of a word, but the bonus should not be + // too great to prevent the longer acronym matches from always winning over + // shorter fuzzy matches. The bonus point here was specifically chosen that + // the bonus is cancelled when the gap between the acronyms grows over + // 8 characters, which is approximately the average length of the words found + // in web2 dictionary and my file system. + bonusBoundary = scoreMatch / 2 + + // Although bonus point for non-word characters is non-contextual, we need it + // for computing bonus points for consecutive chunks starting with a non-word + // character. + bonusNonWord = scoreMatch / 2 + + // Edge-triggered bonus for matches in camelCase words. + // Compared to word-boundary case, they don't accompany single-character gaps + // (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly. + bonusCamel123 = bonusBoundary + scoreGapExtention + + // Minimum bonus point given to characters in consecutive chunks. + // Note that bonus points for consecutive matches shouldn't have needed if we + // used fixed match score as in the original algorithm. + bonusConsecutive = -(scoreGapStart + scoreGapExtention) + + // The first character in the typed pattern usually has more significance + // than the rest so it's important that it appears at special positions where + // bonus points are given. e.g. "to-go" vs. "ongoing" on "og" or on "ogo". + // The amount of the extra bonus should be limited so that the gap penalty is + // still respected. + bonusFirstCharMultiplier = 2 +) + type charClass int const ( @@ -42,85 +148,350 @@ const ( charNumber ) -func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int) int { - var bonus int - pidx := 0 - lenPattern := len(pattern) - consecutive := false - prevClass := charNonWord - for index := util.Max(0, sidx-1); index < eidx; index++ { - char := text.Get(index) - var class charClass - if unicode.IsLower(char) { - class = charLower - } else if unicode.IsUpper(char) { - class = charUpper - } else if unicode.IsLetter(char) { - class = charLetter - } else if unicode.IsNumber(char) { - class = charNumber - } else { - class = charNonWord - } - - var point int - if prevClass == charNonWord && class != charNonWord { - // Word boundary - point = 2 - } else if prevClass == charLower && class == charUpper || - prevClass != charNumber && class == charNumber { - // camelCase letter123 - point = 1 - } - prevClass = class - - if index >= sidx { - if !caseSensitive { - if char >= 'A' && char <= 'Z' { - char += 32 - } else if char > unicode.MaxASCII { - char = unicode.To(unicode.LowerCase, char) - } - } - pchar := pattern[pidx] - if pchar == char { - // Boost bonus for the first character in the pattern - if pidx == 0 { - point *= 2 - } - // Bonus to consecutive matching chars - if consecutive { - point++ - } - bonus += point - - if pidx++; pidx == lenPattern { - break - } - consecutive = true - } else { - consecutive = false - } - } +func posArray(withPos bool, len int) *[]int { + if withPos { + pos := make([]int, 0, len) + return &pos } - return bonus + return nil } -// FuzzyMatch performs fuzzy-match -func FuzzyMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result { - if len(pattern) == 0 { - return Result{0, 0, 0} +func alloc16(offset int, slab *util.Slab, size int, clear bool) (int, []int16) { + if slab != nil && cap(slab.I16) > offset+size { + slice := slab.I16[offset : offset+size] + if clear { + for idx := range slice { + slice[idx] = 0 + } + } + return offset + size, slice + } + return offset, make([]int16, size) +} + +func alloc32(offset int, slab *util.Slab, size int, clear bool) (int, []int32) { + if slab != nil && cap(slab.I32) > offset+size { + slice := slab.I32[offset : offset+size] + if clear { + for idx := range slice { + slice[idx] = 0 + } + } + return offset + size, slice + } + return offset, make([]int32, size) +} + +func charClassOfAscii(char rune) charClass { + if char >= 'a' && char <= 'z' { + return charLower + } else if char >= 'A' && char <= 'Z' { + return charUpper + } else if char >= '0' && char <= '9' { + return charNumber + } + return charNonWord +} + +func charClassOfNonAscii(char rune) charClass { + if unicode.IsLower(char) { + return charLower + } else if unicode.IsUpper(char) { + return charUpper + } else if unicode.IsNumber(char) { + return charNumber + } else if unicode.IsLetter(char) { + return charLetter + } + return charNonWord +} + +func charClassOf(char rune) charClass { + if char <= unicode.MaxASCII { + return charClassOfAscii(char) + } + return charClassOfNonAscii(char) +} + +func bonusFor(prevClass charClass, class charClass) int16 { + if prevClass == charNonWord && class != charNonWord { + // Word boundary + return bonusBoundary + } else if prevClass == charLower && class == charUpper || + prevClass != charNumber && class == charNumber { + // camelCase letter123 + return bonusCamel123 + } else if class == charNonWord { + return bonusNonWord + } + return 0 +} + +func bonusAt(input util.Chars, idx int) int16 { + if idx == 0 { + return bonusBoundary + } + return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx))) +} + +type Algo func(caseSensitive bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) + +func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { + // Assume that pattern is given in lowercase if case-insensitive. + // First check if there's a match and calculate bonus for each position. + // If the input string is too long, consider finding the matching chars in + // this phase as well (non-optimal alignment). + N := input.Length() + M := len(pattern) + switch M { + case 0: + return Result{0, 0, 0}, posArray(withPos, M) + case 1: + return ExactMatchNaive(caseSensitive, forward, input, pattern[0:1], withPos, slab) + } + + // Since O(nm) algorithm can be prohibitively expensive for large input, + // we fall back to the greedy algorithm. + if slab != nil && N*M > cap(slab.I16) { + return FuzzyMatchV1(caseSensitive, forward, input, pattern, withPos, slab) + } + + // Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages + offset := 0 + // Bonus point for each position + offset, B := alloc16(offset, slab, N, false) + // The first occurrence of each character in the pattern + offset, F := alloc16(offset, slab, M, false) + // Rune array + _, T := alloc32(0, slab, N, false) + + // Phase 1. Check if there's a match and calculate bonus for each point + pidx, lastIdx, prevClass := 0, 0, charNonWord + for idx := 0; idx < N; idx++ { + char := input.Get(idx) + var class charClass + if char <= unicode.MaxASCII { + class = charClassOfAscii(char) + } else { + class = charClassOfNonAscii(char) + } + + if !caseSensitive && class == charUpper { + if char <= unicode.MaxASCII { + char += 32 + } else { + char = unicode.To(unicode.LowerCase, char) + } + } + + T[idx] = char + B[idx] = bonusFor(prevClass, class) + prevClass = class + + if pidx < M { + if char == pattern[pidx] { + lastIdx = idx + F[pidx] = int16(idx) + pidx++ + } + } else { + if char == pattern[M-1] { + lastIdx = idx + } + } + } + if pidx != M { + return Result{-1, -1, 0}, nil + } + + // Phase 2. Fill in score matrix (H) + // Unlike the original algorithm, we do not allow omission. + width := lastIdx - int(F[0]) + 1 + offset, H := alloc16(offset, slab, width*M, false) + + // Possible length of consecutive chunk at each position. + offset, C := alloc16(offset, slab, width*M, false) + + maxScore, maxScorePos := int16(0), 0 + for i := 0; i < M; i++ { + I := i * width + inGap := false + for j := int(F[i]); j <= lastIdx; j++ { + j0 := j - int(F[0]) + var s1, s2, consecutive int16 + + if j > int(F[i]) { + if inGap { + s2 = H[I+j0-1] + scoreGapExtention + } else { + s2 = H[I+j0-1] + scoreGapStart + } + } + + if pattern[i] == T[j] { + var diag int16 + if i > 0 && j0 > 0 { + diag = H[I-width+j0-1] + } + s1 = diag + scoreMatch + b := B[j] + if i > 0 { + // j > 0 if i > 0 + consecutive = C[I-width+j0-1] + 1 + // Break consecutive chunk + if b == bonusBoundary { + consecutive = 1 + } else if consecutive > 1 { + b = util.Max16(b, util.Max16(bonusConsecutive, B[j-int(consecutive)+1])) + } + } else { + consecutive = 1 + b *= bonusFirstCharMultiplier + } + if s1+b < s2 { + s1 += B[j] + consecutive = 0 + } else { + s1 += b + } + } + C[I+j0] = consecutive + + inGap = s1 < s2 + score := util.Max16(util.Max16(s1, s2), 0) + if i == M-1 && (forward && score > maxScore || !forward && score >= maxScore) { + maxScore, maxScorePos = score, j + } + H[I+j0] = score + } + + if DEBUG { + if i == 0 { + fmt.Print(" ") + for j := int(F[i]); j <= lastIdx; j++ { + fmt.Printf(" " + string(input.Get(j)) + " ") + } + fmt.Println() + } + fmt.Print(string(pattern[i]) + " ") + for idx := int(F[0]); idx < int(F[i]); idx++ { + fmt.Print(" 0 ") + } + for idx := int(F[i]); idx <= lastIdx; idx++ { + fmt.Printf("%2d ", H[i*width+idx-int(F[0])]) + } + fmt.Println() + + fmt.Print(" ") + for idx, p := range C[I : I+width] { + if idx+int(F[0]) < int(F[i]) { + p = 0 + } + fmt.Printf("%2d ", p) + } + fmt.Println() + } + } + + // Phase 3. (Optional) Backtrace to find character positions + pos := posArray(withPos, M) + j := int(F[0]) + if withPos { + i := M - 1 + j = maxScorePos + preferMatch := true + for { + I := i * width + j0 := j - int(F[0]) + s := H[I+j0] + + var s1, s2 int16 + if i > 0 && j >= int(F[i]) { + s1 = H[I-width+j0-1] + } + if j > int(F[i]) { + s2 = H[I+j0-1] + } + + if s > s1 && (s > s2 || s == s2 && preferMatch) { + *pos = append(*pos, j) + if i == 0 { + break + } + i-- + } + preferMatch = C[I+j0] > 1 || I+width+j0+1 < len(C) && C[I+width+j0+1] > 0 + j-- + } + } + // Start offset we return here is only relevant when begin tiebreak is used. + // However finding the accurate offset requires backtracking, and we don't + // want to pay extra cost for the option that has lost its importance. + return Result{j, maxScorePos + 1, int(maxScore)}, pos +} + +// Implement the same sorting criteria as V2 +func calculateScore(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) { + pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0) + pos := posArray(withPos, len(pattern)) + prevClass := charNonWord + if sidx > 0 { + prevClass = charClassOf(text.Get(sidx - 1)) + } + for idx := sidx; idx < eidx; idx++ { + char := text.Get(idx) + class := charClassOf(char) + if !caseSensitive { + if char >= 'A' && char <= 'Z' { + char += 32 + } else if char > unicode.MaxASCII { + char = unicode.To(unicode.LowerCase, char) + } + } + if char == pattern[pidx] { + if withPos { + *pos = append(*pos, idx) + } + score += scoreMatch + bonus := bonusFor(prevClass, class) + if consecutive == 0 { + firstBonus = bonus + } else { + // Break consecutive chunk + if bonus == bonusBoundary { + firstBonus = bonus + } + bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive) + } + if pidx == 0 { + score += int(bonus * bonusFirstCharMultiplier) + } else { + score += int(bonus) + } + inGap = false + consecutive++ + pidx++ + } else { + if inGap { + score += scoreGapExtention + } else { + score += scoreGapStart + } + inGap = true + consecutive = 0 + firstBonus = 0 + } + prevClass = class + } + return score, pos +} + +// FuzzyMatchV1 performs fuzzy-match +func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { + if len(pattern) == 0 { + return Result{0, 0, 0}, nil } - // 0. (FIXME) How to find the shortest match? - // a_____b__c__abc - // ^^^^^^^^^^ ^^^ - // 1. forward scan (abc) - // *-----*-----*> - // a_____b___abc__ - // 2. reverse scan (cba) - // a_____b___abc__ - // <*** pidx := 0 sidx := -1 eidx := -1 @@ -157,7 +528,8 @@ func FuzzyMatch(caseSensitive bool, forward bool, text util.Chars, pattern []run if sidx >= 0 && eidx >= 0 { pidx-- for index := eidx - 1; index >= sidx; index-- { - char := text.Get(indexAt(index, lenRunes, forward)) + tidx := indexAt(index, lenRunes, forward) + char := text.Get(tidx) if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 @@ -166,7 +538,8 @@ func FuzzyMatch(caseSensitive bool, forward bool, text util.Chars, pattern []run } } - pchar := pattern[indexAt(pidx, lenPattern, forward)] + pidx_ := indexAt(pidx, lenPattern, forward) + pchar := pattern[pidx_] if char == pchar { if pidx--; pidx < 0 { sidx = index @@ -175,16 +548,14 @@ func FuzzyMatch(caseSensitive bool, forward bool, text util.Chars, pattern []run } } - // Calculate the bonus. This can't be done at the same time as the - // pattern scan above because 'forward' may be false. if !forward { sidx, eidx = lenRunes-eidx, lenRunes-sidx } - return Result{sidx, eidx, - evaluateBonus(caseSensitive, text, pattern, sidx, eidx)} + score, pos := calculateScore(caseSensitive, text, pattern, sidx, eidx, withPos) + return Result{sidx, eidx, score}, pos } - return Result{-1, -1, 0} + return Result{-1, -1, 0}, nil } // ExactMatchNaive is a basic string searching algorithm that handles case @@ -192,23 +563,28 @@ func FuzzyMatch(caseSensitive bool, forward bool, text util.Chars, pattern []run // of strings.ToLower + strings.Index for typical fzf use cases where input // strings and patterns are not very long. // -// We might try to implement better algorithms in the future: -// http://en.wikipedia.org/wiki/String_searching_algorithm -func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result { +// Since 0.15.0, this function searches for the match with the highest +// bonus point, instead of stopping immediately after finding the first match. +// The solution is much cheaper since there is only one possible alignment of +// the pattern. +func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { if len(pattern) == 0 { - return Result{0, 0, 0} + return Result{0, 0, 0}, nil } lenRunes := text.Length() lenPattern := len(pattern) if lenRunes < lenPattern { - return Result{-1, -1, 0} + return Result{-1, -1, 0}, nil } + // For simplicity, only look at the bonus at the first character position pidx := 0 + bestPos, bonus, bestBonus := -1, int16(0), int16(-1) for index := 0; index < lenRunes; index++ { - char := text.Get(indexAt(index, lenRunes, forward)) + index_ := indexAt(index, lenRunes, forward) + char := text.Get(index_) if !caseSensitive { if char >= 'A' && char <= 'Z' { char += 32 @@ -216,33 +592,51 @@ func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern char = unicode.To(unicode.LowerCase, char) } } - pchar := pattern[indexAt(pidx, lenPattern, forward)] + pidx_ := indexAt(pidx, lenPattern, forward) + pchar := pattern[pidx_] if pchar == char { + if pidx_ == 0 { + bonus = bonusAt(text, index_) + } pidx++ if pidx == lenPattern { - var sidx, eidx int - if forward { - sidx = index - lenPattern + 1 - eidx = index + 1 - } else { - sidx = lenRunes - (index + 1) - eidx = lenRunes - (index - lenPattern + 1) + if bonus > bestBonus { + bestPos, bestBonus = index, bonus } - return Result{sidx, eidx, - evaluateBonus(caseSensitive, text, pattern, sidx, eidx)} + if bonus == bonusBoundary { + break + } + index -= pidx - 1 + pidx, bonus = 0, 0 } } else { index -= pidx - pidx = 0 + pidx, bonus = 0, 0 } } - return Result{-1, -1, 0} + if bestPos >= 0 { + var sidx, eidx int + if forward { + sidx = bestPos - lenPattern + 1 + eidx = bestPos + 1 + } else { + sidx = lenRunes - (bestPos + 1) + eidx = lenRunes - (bestPos - lenPattern + 1) + } + score, _ := calculateScore(caseSensitive, text, pattern, sidx, eidx, false) + return Result{sidx, eidx, score}, nil + } + return Result{-1, -1, 0}, nil } // PrefixMatch performs prefix-match -func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result { +func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { + if len(pattern) == 0 { + return Result{0, 0, 0}, nil + } + if text.Length() < len(pattern) { - return Result{-1, -1, 0} + return Result{-1, -1, 0}, nil } for index, r := range pattern { @@ -251,20 +645,24 @@ func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru char = unicode.ToLower(char) } if char != r { - return Result{-1, -1, 0} + return Result{-1, -1, 0}, nil } } lenPattern := len(pattern) - return Result{0, lenPattern, - evaluateBonus(caseSensitive, text, pattern, 0, lenPattern)} + score, _ := calculateScore(caseSensitive, text, pattern, 0, lenPattern, false) + return Result{0, lenPattern, score}, nil } // SuffixMatch performs suffix-match -func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result { - trimmedLen := text.Length() - text.TrailingWhitespaces() +func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { + lenRunes := text.Length() + trimmedLen := lenRunes - text.TrailingWhitespaces() + if len(pattern) == 0 { + return Result{trimmedLen, trimmedLen, 0}, nil + } diff := trimmedLen - len(pattern) if diff < 0 { - return Result{-1, -1, 0} + return Result{-1, -1, 0}, nil } for index, r := range pattern { @@ -273,28 +671,29 @@ func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru char = unicode.ToLower(char) } if char != r { - return Result{-1, -1, 0} + return Result{-1, -1, 0}, nil } } lenPattern := len(pattern) sidx := trimmedLen - lenPattern eidx := trimmedLen - return Result{sidx, eidx, - evaluateBonus(caseSensitive, text, pattern, sidx, eidx)} + score, _ := calculateScore(caseSensitive, text, pattern, sidx, eidx, false) + return Result{sidx, eidx, score}, nil } // EqualMatch performs equal-match -func EqualMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune) Result { - // Note: EqualMatch always return a zero bonus. - if text.Length() != len(pattern) { - return Result{-1, -1, 0} +func EqualMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { + lenPattern := len(pattern) + if text.Length() != lenPattern { + return Result{-1, -1, 0}, nil } runesStr := text.ToString() if !caseSensitive { runesStr = strings.ToLower(runesStr) } if runesStr == string(pattern) { - return Result{0, len(pattern), 0} + return Result{0, lenPattern, (scoreMatch+bonusBoundary)*lenPattern + + (bonusFirstCharMultiplier-1)*bonusBoundary}, nil } - return Result{-1, -1, 0} + return Result{-1, -1, 0}, nil } diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index 7034dce..7317eb1 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -1,95 +1,154 @@ package algo import ( + "sort" "strings" "testing" "github.com/junegunn/fzf/src/util" ) -func assertMatch(t *testing.T, fun func(bool, bool, util.Chars, []rune) Result, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, bonus int) { +func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) { if !caseSensitive { pattern = strings.ToLower(pattern) } - res := fun(caseSensitive, forward, util.RunesToChars([]rune(input)), []rune(pattern)) - if res.Start != sidx { - t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", res.Start, sidx, input, pattern) + res, pos := fun(caseSensitive, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil) + var start, end int + if pos == nil || len(*pos) == 0 { + start = res.Start + end = res.End + } else { + sort.Ints(*pos) + start = (*pos)[0] + end = (*pos)[len(*pos)-1] + 1 } - if res.End != eidx { - t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", res.End, eidx, input, pattern) + if start != sidx { + t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", start, sidx, input, pattern) } - if res.Bonus != bonus { - t.Errorf("Invalid bonus: %d (expected: %d, %s / %s)", res.Bonus, bonus, input, pattern) + if end != eidx { + t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", end, eidx, input, pattern) + } + if res.Score != score { + t.Errorf("Invalid score: %d (expected: %d, %s / %s)", res.Score, score, input, pattern) } } func TestFuzzyMatch(t *testing.T) { - assertMatch(t, FuzzyMatch, false, true, "fooBarbaz", "oBZ", 2, 9, 2) - assertMatch(t, FuzzyMatch, false, true, "foo bar baz", "fbb", 0, 9, 8) - assertMatch(t, FuzzyMatch, false, true, "/AutomatorDocument.icns", "rdoc", 9, 13, 4) - assertMatch(t, FuzzyMatch, false, true, "/man1/zshcompctl.1", "zshc", 6, 10, 7) - assertMatch(t, FuzzyMatch, false, true, "/.oh-my-zsh/cache", "zshc", 8, 13, 8) - assertMatch(t, FuzzyMatch, false, true, "ab0123 456", "12356", 3, 10, 3) - assertMatch(t, FuzzyMatch, false, true, "abc123 456", "12356", 3, 10, 5) + for _, fn := range []Algo{FuzzyMatchV1, FuzzyMatchV2} { + for _, forward := range []bool{true, false} { + assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9, + scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3) + assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9, + scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+ + bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention) + assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13, + scoreMatch*4+bonusCamel123+bonusConsecutive*2) + assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10, + scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3) + assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13, + scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart) + assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10, + scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtention) + assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10, + scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtention) + assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9, + scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+ + bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention) + assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7, + scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+ + bonusCamel123*2+2*scoreGapStart+2*scoreGapExtention) + assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8, + scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+ + scoreGapStart*2+scoreGapExtention*3) + assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4, + scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3) + assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6, + scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+ + bonusNonWord+bonusBoundary) - assertMatch(t, FuzzyMatch, false, true, "foo/bar/baz", "fbb", 0, 9, 8) - assertMatch(t, FuzzyMatch, false, true, "fooBarBaz", "fbb", 0, 7, 6) - assertMatch(t, FuzzyMatch, false, true, "foo barbaz", "fbb", 0, 8, 6) - assertMatch(t, FuzzyMatch, false, true, "fooBar Baz", "foob", 0, 4, 8) - assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBZ", -1, -1, 0) - assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "oBz", 2, 9, 2) - assertMatch(t, FuzzyMatch, true, true, "Foo Bar Baz", "fbb", -1, -1, 0) - assertMatch(t, FuzzyMatch, true, true, "Foo/Bar/Baz", "FBB", 0, 9, 8) - assertMatch(t, FuzzyMatch, true, true, "FooBarBaz", "FBB", 0, 7, 6) - assertMatch(t, FuzzyMatch, true, true, "foo BarBaz", "fBB", 0, 8, 7) - assertMatch(t, FuzzyMatch, true, true, "FooBar Baz", "FooB", 0, 4, 8) - assertMatch(t, FuzzyMatch, true, true, "fooBarbaz", "fooBarbazz", -1, -1, 0) + assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9, + scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3) + assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9, + scoreMatch*3+bonusBoundary*(bonusFirstCharMultiplier+2)+ + scoreGapStart*2+scoreGapExtention*4) + assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7, + scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+ + scoreGapStart*2+scoreGapExtention*2) + assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4, + scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+ + util.Max(bonusCamel123, bonusBoundary)) + + // Consecutive bonus updated + assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6, + scoreMatch*4+bonusBoundary*3) + + // Non-match + assertMatch(t, fn, true, forward, "fooBarbaz", "oBZ", -1, -1, 0) + assertMatch(t, fn, true, forward, "Foo Bar Baz", "fbb", -1, -1, 0) + assertMatch(t, fn, true, forward, "fooBarbaz", "fooBarbazz", -1, -1, 0) + } + } } func TestFuzzyMatchBackward(t *testing.T) { - assertMatch(t, FuzzyMatch, false, true, "foobar fb", "fb", 0, 4, 4) - assertMatch(t, FuzzyMatch, false, false, "foobar fb", "fb", 7, 9, 5) + assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4, + scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+ + scoreGapStart+scoreGapExtention) + assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9, + scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary) } func TestExactMatchNaive(t *testing.T) { for _, dir := range []bool{true, false} { - assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5, 3) assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1, 0) assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1, 0) - assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13, 4) - assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10, 7) - assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13, 10) + assertMatch(t, ExactMatchNaive, false, dir, "fooBarbaz", "oBA", 2, 5, + scoreMatch*3+bonusCamel123+bonusConsecutive) + assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13, + scoreMatch*4+bonusCamel123+bonusConsecutive*2) + assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10, + scoreMatch*4+bonusBoundary*(bonusFirstCharMultiplier+3)) + assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13, + scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+4)) } } func TestExactMatchNaiveBackward(t *testing.T) { - assertMatch(t, ExactMatchNaive, false, true, "foobar foob", "oo", 1, 3, 1) - assertMatch(t, ExactMatchNaive, false, false, "foobar foob", "oo", 8, 10, 1) + assertMatch(t, ExactMatchNaive, false, true, "foobar foob", "oo", 1, 3, + scoreMatch*2+bonusConsecutive) + assertMatch(t, ExactMatchNaive, false, false, "foobar foob", "oo", 8, 10, + scoreMatch*2+bonusConsecutive) } func TestPrefixMatch(t *testing.T) { + score := (scoreMatch+bonusBoundary)*3 + bonusBoundary*(bonusFirstCharMultiplier-1) + for _, dir := range []bool{true, false} { assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0) assertMatch(t, PrefixMatch, false, dir, "fooBarBaz", "baz", -1, -1, 0) - assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, 6) - assertMatch(t, PrefixMatch, false, dir, "foOBarBaZ", "foo", 0, 3, 7) - assertMatch(t, PrefixMatch, false, dir, "f-oBarbaz", "f-o", 0, 3, 8) + assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, score) + assertMatch(t, PrefixMatch, false, dir, "foOBarBaZ", "foo", 0, 3, score) + assertMatch(t, PrefixMatch, false, dir, "f-oBarbaz", "f-o", 0, 3, score) } } func TestSuffixMatch(t *testing.T) { for _, dir := range []bool{true, false} { - assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1, 0) - assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9, 2) - assertMatch(t, SuffixMatch, false, dir, "fooBarBaZ", "baz", 6, 9, 5) assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1, 0) + assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "Foo", -1, -1, 0) + + assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9, + scoreMatch*3+bonusConsecutive*2) + assertMatch(t, SuffixMatch, false, dir, "fooBarBaZ", "baz", 6, 9, + (scoreMatch+bonusCamel123)*3+bonusCamel123*(bonusFirstCharMultiplier-1)) } } func TestEmptyPattern(t *testing.T) { for _, dir := range []bool{true, false} { - assertMatch(t, FuzzyMatch, true, dir, "foobar", "", 0, 0, 0) + assertMatch(t, FuzzyMatchV1, true, dir, "foobar", "", 0, 0, 0) + assertMatch(t, FuzzyMatchV2, true, dir, "foobar", "", 0, 0, 0) assertMatch(t, ExactMatchNaive, true, dir, "foobar", "", 0, 0, 0) assertMatch(t, PrefixMatch, true, dir, "foobar", "", 0, 0, 0) assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6, 0) diff --git a/src/chunklist_test.go b/src/chunklist_test.go index 594daeb..56559d8 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -9,7 +9,7 @@ import ( func TestChunkList(t *testing.T) { // FIXME global - sortCriteria = []criterion{byMatchLen, byLength} + sortCriteria = []criterion{byScore, byLength} cl := NewChunkList(func(s []byte, i int) *Item { return &Item{text: util.ToChars(s), index: int32(i * 2)} diff --git a/src/constants.go b/src/constants.go index c041086..04b1090 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.13.5" + version = "0.15.0" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond @@ -24,11 +24,17 @@ const ( spinnerDuration = 200 * time.Millisecond // Matcher - progressMinDuration = 200 * time.Millisecond + numPartitionsMultiplier = 8 + maxPartitions = 32 + progressMinDuration = 200 * time.Millisecond // Capacity of each chunk chunkSize int = 100 + // Pre-allocated memory slices to minimize GC + slab16Size int = 100 * 1024 // 200KB * 32 = 12.8MB + slab32Size int = 2048 // 8KB * 32 = 256KB + // Do not cache results of low selectivity queries queryCacheMax int = chunkSize / 5 diff --git a/src/core.go b/src/core.go index 70f2da5..e707ad5 100644 --- a/src/core.go +++ b/src/core.go @@ -143,8 +143,8 @@ func Run(opts *Options) { } patternBuilder := func(runes []rune) *Pattern { return BuildPattern( - opts.Fuzzy, opts.Extended, opts.Case, forward, opts.Filter == nil, - opts.Nth, opts.Delimiter, runes) + opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, forward, + opts.Filter == nil, opts.Nth, opts.Delimiter, runes) } matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) @@ -158,11 +158,12 @@ func Run(opts *Options) { found := false if streamingFilter { + slab := util.MakeSlab(slab16Size, slab32Size) reader := Reader{ func(runes []byte) bool { item := chunkList.trans(runes, 0) if item != nil { - if result, _ := pattern.MatchItem(item); result != nil { + if result, _, _ := pattern.MatchItem(item, false, slab); result != nil { fmt.Println(item.text.ToString()) found = true } diff --git a/src/matcher.go b/src/matcher.go index 9814ba9..57c263a 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -26,6 +26,7 @@ type Matcher struct { eventBox *util.EventBox reqBox *util.EventBox partitions int + slab []*util.Slab mergerCache map[string]*Merger } @@ -37,13 +38,15 @@ const ( // NewMatcher returns a new Matcher func NewMatcher(patternBuilder func([]rune) *Pattern, sort bool, tac bool, eventBox *util.EventBox) *Matcher { + partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions) return &Matcher{ patternBuilder: patternBuilder, sort: sort, tac: tac, eventBox: eventBox, reqBox: util.NewEventBox(), - partitions: util.Min(8*runtime.NumCPU(), 32), + partitions: partitions, + slab: make([]*util.Slab, partitions), mergerCache: make(map[string]*Merger)} } @@ -153,12 +156,15 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { for idx, chunks := range slices { waitGroup.Add(1) - go func(idx int, chunks []*Chunk) { + if m.slab[idx] == nil { + m.slab[idx] = util.MakeSlab(slab16Size, slab32Size) + } + go func(idx int, slab *util.Slab, chunks []*Chunk) { defer func() { waitGroup.Done() }() count := 0 allMatches := make([][]*Result, len(chunks)) for idx, chunk := range chunks { - matches := request.pattern.Match(chunk) + matches := request.pattern.Match(chunk, slab) allMatches[idx] = matches count += len(matches) if cancelled.Get() { @@ -178,7 +184,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { } } resultChan <- partialResult{idx, sliceMatches} - }(idx, chunks) + }(idx, m.slab[idx], chunks) } wait := func() bool { diff --git a/src/options.go b/src/options.go index a19ff6f..2629c28 100644 --- a/src/options.go +++ b/src/options.go @@ -8,6 +8,7 @@ import ( "strings" "unicode/utf8" + "github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/curses" "github.com/junegunn/go-shellwords" @@ -19,6 +20,7 @@ const usage = `usage: fzf [options] -x, --extended Extended-search mode (enabled by default; +x or --no-extended to disable) -e, --exact Enable Exact-match + --algo=TYPE Fuzzy matching algorithm: [v1|v2] (default: v2) -i Case-insensitive match (default: smart-case match) +i Case-sensitive match -n, --nth=N[,..] Comma-separated list of field index expressions @@ -94,8 +96,7 @@ const ( type criterion int const ( - byMatchLen criterion = iota - byBonus + byScore criterion = iota byLength byBegin byEnd @@ -129,6 +130,7 @@ type previewOpts struct { // Options stores the values of command-line options type Options struct { Fuzzy bool + FuzzyAlgo algo.Algo Extended bool Case Case Nth []Range @@ -172,6 +174,7 @@ type Options struct { func defaultOptions() *Options { return &Options{ Fuzzy: true, + FuzzyAlgo: algo.FuzzyMatchV2, Extended: true, Case: CaseSmart, Nth: make([]Range, 0), @@ -179,7 +182,7 @@ func defaultOptions() *Options { Delimiter: Delimiter{}, Sort: 1000, Tac: false, - Criteria: []criterion{byMatchLen, byBonus, byLength}, + Criteria: []criterion{byScore, byLength}, Multi: false, Ansi: false, Mouse: true, @@ -322,6 +325,18 @@ func isAlphabet(char uint8) bool { return char >= 'a' && char <= 'z' } +func parseAlgo(str string) algo.Algo { + switch str { + case "v1": + return algo.FuzzyMatchV1 + case "v2": + return algo.FuzzyMatchV2 + default: + errorExit("invalid algorithm (expected: v1 or v2)") + } + return algo.FuzzyMatchV2 +} + func parseKeyChords(str string, message string) map[int]string { if len(str) == 0 { errorExit(message) @@ -407,7 +422,7 @@ func parseKeyChords(str string, message string) map[int]string { } func parseTiebreak(str string) []criterion { - criteria := []criterion{byMatchLen, byBonus} + criteria := []criterion{byScore} hasIndex := false hasLength := false hasBegin := false @@ -834,6 +849,8 @@ func parseOptions(opts *Options, allArgs []string) { case "-f", "--filter": filter := nextString(allArgs, &i, "query string required") opts.Filter = &filter + case "--algo": + opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)")) case "--expect": opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") case "--tiebreak": @@ -962,7 +979,9 @@ func parseOptions(opts *Options, allArgs []string) { case "--version": opts.Version = true default: - if match, value := optString(arg, "-q", "--query="); match { + if match, value := optString(arg, "--algo="); match { + opts.FuzzyAlgo = parseAlgo(value) + } else if match, value := optString(arg, "-q", "--query="); match { opts.Query = value } else if match, value := optString(arg, "-f", "--filter="); match { opts.Filter = &value diff --git a/src/pattern.go b/src/pattern.go index faafa0b..7e5f428 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -40,6 +40,7 @@ type termSet []term // Pattern represents search pattern type Pattern struct { fuzzy bool + fuzzyAlgo algo.Algo extended bool caseSensitive bool forward bool @@ -48,7 +49,7 @@ type Pattern struct { cacheable bool delimiter Delimiter nth []Range - procFun map[termType]func(bool, bool, util.Chars, []rune) algo.Result + procFun map[termType]algo.Algo } var ( @@ -74,7 +75,7 @@ func clearChunkCache() { } // BuildPattern builds Pattern object from the given arguments -func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool, +func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, forward bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { var asString string @@ -116,6 +117,7 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool, ptr := &Pattern{ fuzzy: fuzzy, + fuzzyAlgo: fuzzyAlgo, extended: extended, caseSensitive: caseSensitive, forward: forward, @@ -124,9 +126,9 @@ func BuildPattern(fuzzy bool, extended bool, caseMode Case, forward bool, cacheable: cacheable, nth: nth, delimiter: delimiter, - procFun: make(map[termType]func(bool, bool, util.Chars, []rune) algo.Result)} + procFun: make(map[termType]algo.Algo)} - ptr.procFun[termFuzzy] = algo.FuzzyMatch + ptr.procFun[termFuzzy] = fuzzyAlgo ptr.procFun[termEqual] = algo.EqualMatch ptr.procFun[termExact] = algo.ExactMatchNaive ptr.procFun[termPrefix] = algo.PrefixMatch @@ -234,7 +236,7 @@ func (p *Pattern) CacheKey() string { } // Match returns the list of matches Items in the given Chunk -func (p *Pattern) Match(chunk *Chunk) []*Result { +func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []*Result { // ChunkCache: Exact match cacheKey := p.CacheKey() if p.cacheable { @@ -260,7 +262,7 @@ Loop: } } - matches := p.matchChunk(chunk, space) + matches := p.matchChunk(chunk, space, slab) if p.cacheable { _cache.Add(chunk, cacheKey, matches) @@ -268,18 +270,18 @@ Loop: return matches } -func (p *Pattern) matchChunk(chunk *Chunk, space []*Result) []*Result { +func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []*Result { matches := []*Result{} if space == nil { for _, item := range *chunk { - if match, _ := p.MatchItem(item); match != nil { + if match, _, _ := p.MatchItem(item, false, slab); match != nil { matches = append(matches, match) } } } else { for _, result := range space { - if match, _ := p.MatchItem(result.item); match != nil { + if match, _, _ := p.MatchItem(result.item, false, slab); match != nil { matches = append(matches, match) } } @@ -288,62 +290,75 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []*Result) []*Result { } // MatchItem returns true if the Item is a match -func (p *Pattern) MatchItem(item *Item) (*Result, []Offset) { +func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) { if p.extended { - if offsets, bonus, trimLen := p.extendedMatch(item); len(offsets) == len(p.termSets) { - return buildResult(item, offsets, bonus, trimLen), offsets + if offsets, bonus, trimLen, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) { + return buildResult(item, offsets, bonus, trimLen), offsets, pos } - return nil, nil + return nil, nil, nil } - offset, bonus, trimLen := p.basicMatch(item) + offset, bonus, trimLen, pos := p.basicMatch(item, withPos, slab) if sidx := offset[0]; sidx >= 0 { offsets := []Offset{offset} - return buildResult(item, offsets, bonus, trimLen), offsets + return buildResult(item, offsets, bonus, trimLen), offsets, pos } - return nil, nil + return nil, nil, nil } -func (p *Pattern) basicMatch(item *Item) (Offset, int, int) { +func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) { input := p.prepareInput(item) if p.fuzzy { - return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) + return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.forward, p.text, withPos, slab) } - return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text) + return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text, withPos, slab) } -func (p *Pattern) extendedMatch(item *Item) ([]Offset, int, int) { +func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, int, *[]int) { input := p.prepareInput(item) offsets := []Offset{} - var totalBonus int + var totalScore int var totalTrimLen int + var allPos *[]int + if withPos { + allPos = &[]int{} + } for _, termSet := range p.termSets { var offset Offset - var bonus int + var currentScore int var trimLen int matched := false for _, term := range termSet { pfun := p.procFun[term.typ] - off, pen, tLen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text) + off, score, tLen, pos := p.iter(pfun, input, term.caseSensitive, p.forward, term.text, withPos, slab) if sidx := off[0]; sidx >= 0 { if term.inv { continue } - offset, bonus, trimLen = off, pen, tLen + offset, currentScore, trimLen = off, score, tLen matched = true + if withPos { + if pos != nil { + *allPos = append(*allPos, *pos...) + } else { + for idx := off[0]; idx < off[1]; idx++ { + *allPos = append(*allPos, int(idx)) + } + } + } break } else if term.inv { - offset, bonus, trimLen = Offset{0, 0}, 0, 0 + offset, currentScore, trimLen = Offset{0, 0}, 0, 0 matched = true continue } } if matched { offsets = append(offsets, offset) - totalBonus += bonus + totalScore += currentScore totalTrimLen += trimLen } } - return offsets, totalBonus, totalTrimLen + return offsets, totalScore, totalTrimLen, allPos } func (p *Pattern) prepareInput(item *Item) []Token { @@ -362,14 +377,18 @@ func (p *Pattern) prepareInput(item *Item) []Token { return ret } -func (p *Pattern) iter(pfun func(bool, bool, util.Chars, []rune) algo.Result, - tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int, int) { +func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) { for _, part := range tokens { - if res := pfun(caseSensitive, forward, *part.text, pattern); res.Start >= 0 { + if res, pos := pfun(caseSensitive, forward, *part.text, pattern, withPos, slab); res.Start >= 0 { sidx := int32(res.Start) + part.prefixLength eidx := int32(res.End) + part.prefixLength - return Offset{sidx, eidx}, res.Bonus, int(part.trimLength) + if pos != nil { + for idx := range *pos { + (*pos)[idx] += int(part.prefixLength) + } + } + return Offset{sidx, eidx}, res.Score, int(part.trimLength), pos } } - return Offset{-1, -1}, 0, -1 + return Offset{-1, -1}, 0, -1, nil } diff --git a/src/pattern_test.go b/src/pattern_test.go index 43bc5e0..a3aa596 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -8,6 +8,12 @@ import ( "github.com/junegunn/fzf/src/util" ) +var slab *util.Slab + +func init() { + slab = util.MakeSlab(slab16Size, slab32Size) +} + func TestParseTermsExtended(t *testing.T) { terms := parseTerms(true, CaseSmart, "| aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | | zzz$ | !ZZZ |") @@ -69,26 +75,32 @@ func TestParseTermsEmpty(t *testing.T) { func TestExact(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(true, true, CaseSmart, true, true, + pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("'abc")) - res := algo.ExactMatchNaive( - pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text) + res, pos := algo.ExactMatchNaive( + pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text, true, nil) if res.Start != 7 || res.End != 10 { t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) } + if pos != nil { + t.Errorf("pos is expected to be nil") + } } func TestEqual(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(true, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("^AbC$")) + pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("^AbC$")) match := func(str string, sidxExpected int, eidxExpected int) { - res := algo.EqualMatch( - pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text) + res, pos := algo.EqualMatch( + pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text, true, nil) if res.Start != sidxExpected || res.End != eidxExpected { t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) } + if pos != nil { + t.Errorf("pos is expected to be nil") + } } match("ABC", -1, -1) match("AbC", 0, 3) @@ -97,17 +109,17 @@ func TestEqual(t *testing.T) { func TestCaseSensitivity(t *testing.T) { defer clearPatternCache() clearPatternCache() - pat1 := BuildPattern(true, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("abc")) + pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat2 := BuildPattern(true, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("Abc")) + pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat3 := BuildPattern(true, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("abc")) + pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat4 := BuildPattern(true, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("Abc")) + pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat5 := BuildPattern(true, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("abc")) + pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat6 := BuildPattern(true, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("Abc")) + pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("Abc")) if string(pat1.text) != "abc" || pat1.caseSensitive != false || string(pat2.text) != "Abc" || pat2.caseSensitive != true || @@ -120,7 +132,7 @@ func TestCaseSensitivity(t *testing.T) { } func TestOrigTextAndTransformed(t *testing.T) { - pattern := BuildPattern(true, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("jg")) + pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("jg")) tokens := Tokenize(util.RunesToChars([]rune("junegunn")), Delimiter{}) trans := Transform(tokens, []Range{Range{1, 1}}) @@ -133,24 +145,29 @@ func TestOrigTextAndTransformed(t *testing.T) { transformed: trans}, } pattern.extended = extended - matches := pattern.matchChunk(&chunk, nil) // No cache - if matches[0].item.text.ToString() != "junegunn" || string(*matches[0].item.origText) != "junegunn.choi" || - !reflect.DeepEqual(matches[0].item.transformed, trans) { + matches := pattern.matchChunk(&chunk, nil, slab) // No cache + if !(matches[0].item.text.ToString() == "junegunn" && + string(*matches[0].item.origText) == "junegunn.choi" && + reflect.DeepEqual(matches[0].item.transformed, trans)) { t.Error("Invalid match result", matches) } - match, offsets := pattern.MatchItem(chunk[0]) - if match.item.text.ToString() != "junegunn" || string(*match.item.origText) != "junegunn.choi" || - offsets[0][0] != 0 || offsets[0][1] != 5 || - !reflect.DeepEqual(match.item.transformed, trans) { - t.Error("Invalid match result", match) + match, offsets, pos := pattern.MatchItem(chunk[0], true, slab) + if !(match.item.text.ToString() == "junegunn" && + string(*match.item.origText) == "junegunn.choi" && + offsets[0][0] == 0 && offsets[0][1] == 5 && + reflect.DeepEqual(match.item.transformed, trans)) { + t.Error("Invalid match result", match, offsets, extended) + } + if !((*pos)[0] == 4 && (*pos)[1] == 0) { + t.Error("Invalid pos array", *pos) } } } func TestCacheKey(t *testing.T) { test := func(extended bool, patStr string, expected string, cacheable bool) { - pat := BuildPattern(true, extended, CaseSmart, true, true, []Range{}, Delimiter{}, []rune(patStr)) + pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, true, true, []Range{}, Delimiter{}, []rune(patStr)) if pat.CacheKey() != expected { t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) } diff --git a/src/result.go b/src/result.go index 2b66a02..87478ab 100644 --- a/src/result.go +++ b/src/result.go @@ -19,8 +19,7 @@ type colorOffset struct { } type rank struct { - // byMatchLen, byBonus, ... - points [5]uint16 + points [4]uint16 index int32 } @@ -29,66 +28,49 @@ type Result struct { rank rank } -func buildResult(item *Item, offsets []Offset, bonus int, trimLen int) *Result { +func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result { if len(offsets) > 1 { sort.Sort(ByOrder(offsets)) } result := Result{item: item, rank: rank{index: item.index}} - - matchlen := 0 - prevEnd := 0 - minBegin := math.MaxInt32 numChars := item.text.Length() + minBegin := math.MaxUint16 + maxEnd := 0 + validOffsetFound := false for _, offset := range offsets { - begin := int(offset[0]) - end := int(offset[1]) - if prevEnd > begin { - begin = prevEnd - } - if end > prevEnd { - prevEnd = end - } - if end > begin { - if begin < minBegin { - minBegin = begin - } - matchlen += end - begin + b, e := int(offset[0]), int(offset[1]) + if b < e { + minBegin = util.Min(b, minBegin) + maxEnd = util.Max(e, maxEnd) + validOffsetFound = true } } for idx, criterion := range sortCriteria { - var val uint16 + val := uint16(math.MaxUint16) switch criterion { - case byMatchLen: - if matchlen == 0 { - val = math.MaxUint16 - } else { - val = util.AsUint16(matchlen) - } - case byBonus: + case byScore: // Higher is better - val = math.MaxUint16 - util.AsUint16(bonus) + val = math.MaxUint16 - util.AsUint16(score) case byLength: // If offsets is empty, trimLen will be 0, but we don't care val = util.AsUint16(trimLen) case byBegin: - // We can't just look at item.offsets[0][0] because it can be an inverse term - whitePrefixLen := 0 - for idx := 0; idx < numChars; idx++ { - r := item.text.Get(idx) - whitePrefixLen = idx - if idx == minBegin || r != ' ' && r != '\t' { - break + if validOffsetFound { + whitePrefixLen := 0 + for idx := 0; idx < numChars; idx++ { + r := item.text.Get(idx) + whitePrefixLen = idx + if idx == minBegin || r != ' ' && r != '\t' { + break + } } + val = util.AsUint16(minBegin - whitePrefixLen) } - val = util.AsUint16(minBegin - whitePrefixLen) case byEnd: - if prevEnd > 0 { - val = util.AsUint16(1 + numChars - prevEnd) - } else { - // Empty offsets due to inverse terms. - val = 1 + if validOffsetFound { + val = util.AsUint16(1 + numChars - maxEnd) } } result.rank.points[idx] = val @@ -106,7 +88,7 @@ func (result *Result) Index() int32 { } func minRank() rank { - return rank{index: 0, points: [5]uint16{0, math.MaxUint16, 0, 0, 0}} + return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}} } func (result *Result) colorOffsets(matchOffsets []Offset, color int, bold bool, current bool) []colorOffset { @@ -245,7 +227,7 @@ func (a ByRelevanceTac) Less(i, j int) bool { } func compareRanks(irank rank, jrank rank, tac bool) bool { - for idx := 0; idx < 5; idx++ { + for idx := 0; idx < 4; idx++ { left := irank.points[idx] right := jrank.points[idx] if left < right { diff --git a/src/result_test.go b/src/result_test.go index 60551fd..c6832f1 100644 --- a/src/result_test.go +++ b/src/result_test.go @@ -26,7 +26,7 @@ func TestOffsetSort(t *testing.T) { func TestRankComparison(t *testing.T) { rank := func(vals ...uint16) rank { return rank{ - points: [5]uint16{vals[0], 0, vals[1], vals[2], vals[3]}, + points: [4]uint16{vals[0], vals[1], vals[2], vals[3]}, index: int32(vals[4])} } if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) || @@ -47,11 +47,15 @@ func TestRankComparison(t *testing.T) { // Match length, string length, index func TestResultRank(t *testing.T) { // FIXME global - sortCriteria = []criterion{byMatchLen, byBonus, byLength} + sortCriteria = []criterion{byScore, byLength} strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} item1 := buildResult(&Item{text: util.RunesToChars(strs[0]), index: 1}, []Offset{}, 2, 3) - if item1.rank.points[0] != math.MaxUint16 || item1.rank.points[1] != math.MaxUint16-2 || item1.rank.points[2] != 3 || item1.item.index != 1 { + if item1.rank.points[0] != math.MaxUint16-2 || // Bonus + item1.rank.points[1] != 3 || // Length + item1.rank.points[2] != 0 || // Unused + item1.rank.points[3] != 0 || // Unused + item1.item.index != 1 { t.Error(item1.rank) } // Only differ in index @@ -71,16 +75,16 @@ func TestResultRank(t *testing.T) { } // Sort by relevance - item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 0, 0) - item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 0, 0) - item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 0, 0) - item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 0, 0) + item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 3, 0) + item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 4, 0) + item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 5, 0) + item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 6, 0) items = []*Result{item1, item2, item3, item4, item5, item6} sort.Sort(ByRelevance(items)) - if items[0] != item6 || items[1] != item4 || - items[2] != item5 || items[3] != item3 || - items[4] != item2 || items[5] != item1 { - t.Error(items) + if !(items[0] == item6 && items[1] == item5 && + items[2] == item4 && items[3] == item3 && + items[4] == item2 && items[5] == item1) { + t.Error(items, item1, item2, item3, item4, item5, item6) } } diff --git a/src/terminal.go b/src/terminal.go index d87a20b..5b26eeb 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -18,6 +18,8 @@ import ( "github.com/junegunn/go-runewidth" ) +// import "github.com/pkg/profile" + type jumpMode int const ( @@ -73,6 +75,7 @@ type Terminal struct { initFunc func() suppress bool startChan chan bool + slab *util.Slab } type selectedItem struct { @@ -276,6 +279,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { eventBox: eventBox, mutex: sync.Mutex{}, suppress: true, + slab: util.MakeSlab(slab16Size, slab32Size), startChan: make(chan bool, 1), initFunc: func() { C.Init(opts.Theme, opts.Black, opts.Mouse) @@ -674,14 +678,25 @@ func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 in text := make([]rune, item.text.Length()) copy(text, item.text.ToRunes()) matchOffsets := []Offset{} + var pos *[]int if t.merger.pattern != nil { - _, matchOffsets = t.merger.pattern.MatchItem(item) + _, matchOffsets, pos = t.merger.pattern.MatchItem(item, true, t.slab) + } + charOffsets := matchOffsets + if pos != nil { + charOffsets = make([]Offset, len(*pos)) + for idx, p := range *pos { + offset := Offset{int32(p), int32(p + 1)} + charOffsets[idx] = offset + } + sort.Sort(ByOrder(charOffsets)) } var maxe int - for _, offset := range matchOffsets { + for _, offset := range charOffsets { maxe = util.Max(maxe, int(offset[1])) } - offsets := result.colorOffsets(matchOffsets, col2, bold, current) + + offsets := result.colorOffsets(charOffsets, col2, bold, current) maxWidth := t.window.Width - 3 maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) if overflow(text, maxWidth) { @@ -876,6 +891,7 @@ func (t *Terminal) current() string { // Loop is called to start Terminal I/O func (t *Terminal) Loop() { + // prof := profile.Start(profile.ProfilePath("/tmp/")) <-t.startChan { // Late initialization intChan := make(chan os.Signal, 1) @@ -953,6 +969,7 @@ func (t *Terminal) Loop() { if code <= exitNoMatch && t.history != nil { t.history.append(string(t.input)) } + // prof.Stop() os.Exit(code) } diff --git a/src/util/slab.go b/src/util/slab.go new file mode 100644 index 0000000..0c49d2d --- /dev/null +++ b/src/util/slab.go @@ -0,0 +1,12 @@ +package util + +type Slab struct { + I16 []int16 + I32 []int32 +} + +func MakeSlab(size16 int, size32 int) *Slab { + return &Slab{ + I16: make([]int16, size16), + I32: make([]int32, size32)} +} diff --git a/src/util/util.go b/src/util/util.go index 2ebd0f6..e273882 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -18,6 +18,22 @@ func Max(first int, second int) int { return second } +// Max16 returns the largest integer +func Max16(first int16, second int16) int16 { + if first >= second { + return first + } + return second +} + +// Max32 returns the largest 32-bit integer +func Max32(first int32, second int32) int32 { + if first > second { + return first + } + return second +} + // Min returns the smallest integer func Min(first int, second int) int { if first <= second { @@ -34,14 +50,6 @@ func Min32(first int32, second int32) int32 { return second } -// Max32 returns the largest 32-bit integer -func Max32(first int32, second int32) int32 { - if first > second { - return first - } - return second -} - // Constrain32 limits the given 32-bit integer with the upper and lower bounds func Constrain32(val int32, min int32, max int32) int32 { if val < min { diff --git a/test/test_go.rb b/test/test_go.rb index 00b9c88..2c01818 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -517,162 +517,91 @@ class TestGoFZF < TestBase assert_equal input, `#{FZF} -f"!z" -x --tiebreak end < #{tempname}`.split($/) end - # Since 0.11.2 - def test_tiebreak_list - input = %w[ - f-o-o-b-a-r - foobar---- - --foobar - ----foobar - foobar-- - --foobar-- - foobar + def test_tiebreak_index_begin + writelines tempname, [ + 'xoxxxxxoxx', + 'xoxxxxxox', + 'xxoxxxoxx', + 'xxxoxoxxx', + 'xxxxoxox', + ' xxoxoxxx', ] - writelines tempname, input - assert_equal %w[ - foobar---- - --foobar - ----foobar - foobar-- - --foobar-- - foobar - f-o-o-b-a-r - ], `#{FZF} -ffb --tiebreak=index < #{tempname}`.split($/) + assert_equal [ + 'xxxxoxox', + ' xxoxoxxx', + 'xxxoxoxxx', + 'xxoxxxoxx', + 'xoxxxxxox', + 'xoxxxxxoxx', + ], `#{FZF} -foo < #{tempname}`.split($/) - by_length = %w[ - foobar - --foobar - foobar-- - foobar---- - ----foobar - --foobar-- - f-o-o-b-a-r - ] - assert_equal by_length, `#{FZF} -ffb < #{tempname}`.split($/) - assert_equal by_length, `#{FZF} -ffb --tiebreak=length < #{tempname}`.split($/) + assert_equal [ + 'xxxoxoxxx', + 'xxxxoxox', + ' xxoxoxxx', + 'xxoxxxoxx', + 'xoxxxxxoxx', + 'xoxxxxxox', + ], `#{FZF} -foo --tiebreak=index < #{tempname}`.split($/) - assert_equal %w[ - foobar - foobar-- - --foobar - foobar---- - --foobar-- - ----foobar - f-o-o-b-a-r - ], `#{FZF} -ffb --tiebreak=length,begin < #{tempname}`.split($/) + # Note that --tiebreak=begin is now based on the first occurrence of the + # first character on the pattern + assert_equal [ + ' xxoxoxxx', + 'xxxoxoxxx', + 'xxxxoxox', + 'xxoxxxoxx', + 'xoxxxxxoxx', + 'xoxxxxxox', + ], `#{FZF} -foo --tiebreak=begin < #{tempname}`.split($/) - assert_equal %w[ - foobar - --foobar - foobar-- - ----foobar - --foobar-- - foobar---- - f-o-o-b-a-r - ], `#{FZF} -ffb --tiebreak=length,end < #{tempname}`.split($/) - - assert_equal %w[ - foobar---- - foobar-- - foobar - --foobar - --foobar-- - ----foobar - f-o-o-b-a-r - ], `#{FZF} -ffb --tiebreak=begin < #{tempname}`.split($/) - - by_begin_end = %w[ - foobar - foobar-- - foobar---- - --foobar - --foobar-- - ----foobar - f-o-o-b-a-r - ] - assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=begin,length < #{tempname}`.split($/) - assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=begin,end < #{tempname}`.split($/) - - assert_equal %w[ - --foobar - ----foobar - foobar - foobar-- - --foobar-- - foobar---- - f-o-o-b-a-r - ], `#{FZF} -ffb --tiebreak=end < #{tempname}`.split($/) - - by_begin_end = %w[ - foobar - --foobar - ----foobar - foobar-- - --foobar-- - foobar---- - f-o-o-b-a-r - ] - assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=end,begin < #{tempname}`.split($/) - assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=end,length < #{tempname}`.split($/) + assert_equal [ + ' xxoxoxxx', + 'xxxoxoxxx', + 'xxxxoxox', + 'xxoxxxoxx', + 'xoxxxxxox', + 'xoxxxxxoxx', + ], `#{FZF} -foo --tiebreak=begin,length < #{tempname}`.split($/) end - def test_tiebreak_white_prefix + def test_tiebreak_end writelines tempname, [ - 'f o o b a r', - ' foo bar', - ' foobar', - '----foo bar', - '----foobar', - ' foo bar', - ' foobar--', - ' foobar', - '--foo bar', - '--foobar', - 'foobar', + 'xoxxxxxxxx', + 'xxoxxxxxxx', + 'xxxoxxxxxx', + 'xxxxoxxxx', + 'xxxxxoxxx', + ' xxxxoxxx', ] assert_equal [ - ' foobar', - ' foobar', - 'foobar', - ' foobar--', - '--foobar', - '----foobar', - ' foo bar', - ' foo bar', - '--foo bar', - '----foo bar', - 'f o o b a r', - ], `#{FZF} -ffb < #{tempname}`.split($/) + ' xxxxoxxx', + 'xxxxoxxxx', + 'xxxxxoxxx', + 'xoxxxxxxxx', + 'xxoxxxxxxx', + 'xxxoxxxxxx', + ], `#{FZF} -fo < #{tempname}`.split($/) assert_equal [ - ' foobar', - ' foobar--', - ' foobar', - 'foobar', - '--foobar', - '----foobar', - ' foo bar', - ' foo bar', - '--foo bar', - '----foo bar', - 'f o o b a r', - ], `#{FZF} -ffb --tiebreak=begin < #{tempname}`.split($/) + 'xxxxxoxxx', + ' xxxxoxxx', + 'xxxxoxxxx', + 'xxxoxxxxxx', + 'xxoxxxxxxx', + 'xoxxxxxxxx', + ], `#{FZF} -fo --tiebreak=end < #{tempname}`.split($/) assert_equal [ - ' foobar', - ' foobar', - 'foobar', - ' foobar--', - '--foobar', - '----foobar', - ' foo bar', - ' foo bar', - '--foo bar', - '----foo bar', - 'f o o b a r', - ], `#{FZF} -ffb --tiebreak=begin,length < #{tempname}`.split($/) + ' xxxxoxxx', + 'xxxxxoxxx', + 'xxxxoxxxx', + 'xxxoxxxxxx', + 'xxoxxxxxxx', + 'xoxxxxxxxx', + ], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.split($/) end def test_tiebreak_length_with_nth @@ -748,17 +677,6 @@ class TestGoFZF < TestBase assert_equal output, `#{FZF} -fi -n2,1..2 < #{tempname}`.split($/) end - def test_tiebreak_end_backward_scan - input = %w[ - foobar-fb - fubar - ] - writelines tempname, input - - assert_equal input.reverse, `#{FZF} -f fb < #{tempname}`.split($/) - assert_equal input, `#{FZF} -f fb --tiebreak=end < #{tempname}`.split($/) - end - def test_invalid_cache tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter tmux.until { |lines| lines[-2].include? '2/3' } From 1854922f0cca9959838b73938f10d6d8c968d4c9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 18 Sep 2016 04:20:29 +0900 Subject: [PATCH 260/658] Truncate the query string if it's too long Use hard-coded limit to keep it simple. An alternative is to dynamically calculate the width of the visible area and use it as the limit, but it can cause unwanted truncation of the query on screen resize/split. --- src/constants.go | 7 ++++--- src/terminal.go | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/constants.go b/src/constants.go index 04b1090..acb3b4c 100644 --- a/src/constants.go +++ b/src/constants.go @@ -19,9 +19,10 @@ const ( readerBufferSize = 64 * 1024 // Terminal - initialDelay = 20 * time.Millisecond - initialDelayTac = 100 * time.Millisecond - spinnerDuration = 200 * time.Millisecond + initialDelay = 20 * time.Millisecond + initialDelayTac = 100 * time.Millisecond + spinnerDuration = 200 * time.Millisecond + maxPatternLength = 100 // Matcher numPartitionsMultiplier = 8 diff --git a/src/terminal.go b/src/terminal.go index 5b26eeb..fa758e9 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1337,6 +1337,11 @@ func (t *Terminal) Loop() { if !doAction(action, mapkey) { continue } + // Truncate the query if it's too long + if len(t.input) > maxPatternLength { + t.input = t.input[:maxPatternLength] + t.cx = util.Constrain(t.cx, 0, maxPatternLength) + } changed = string(previousInput) != string(t.input) } else { if mapkey == C.Rune { From 401a5fd5ff0e509a4ea56c9dba567e17be6c494f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 18 Sep 2016 04:45:21 +0900 Subject: [PATCH 261/658] Printable character in --expect set should not affect --print-query --- src/terminal.go | 5 +++-- test/test_go.rb | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index fa758e9..2a02ff8 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1079,8 +1079,9 @@ func (t *Terminal) Loop() { for key, ret := range t.expect { if keyMatch(key, event) { t.pressed = ret - req(reqClose) - break + t.reqBox.Set(reqClose, nil) + t.mutex.Unlock() + return } } diff --git a/test/test_go.rb b/test/test_go.rb index 2c01818..ff4b173 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -452,6 +452,15 @@ class TestGoFZF < TestBase assert_equal ['55', 'alt-z', '55'], readonce.split($/) end + def test_expect_printable_character_print_query + tmux.send_keys "seq 1 100 | #{fzf '--expect=z --print-query'}", :Enter + tmux.until { |lines| lines[-2].include? '100/100' } + tmux.send_keys '55' + tmux.until { |lines| lines[-2].include? '1/100' } + tmux.send_keys 'z' + assert_equal ['55', 'z', '55'], readonce.split($/) + end + def test_expect_print_query_select_1 tmux.send_keys "seq 1 100 | #{fzf '-q55 -1 --expect=alt-z --print-query'}", :Enter assert_equal ['55', '', '55'], readonce.split($/) From 37f43fbb35b819501fe4db7844d17231789c55cd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 18 Sep 2016 04:52:47 +0900 Subject: [PATCH 262/658] Add --print0 option Related: #660 --- CHANGELOG.md | 2 ++ man/man1/fzf.1 | 6 ++++++ src/core.go | 12 ++++++------ src/options.go | 6 ++++++ src/terminal.go | 12 +++++++----- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dee7264..0a8ea8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ CHANGELOG - Added `--algo=[v1|v2]` option so one can still choose the old algorithm which values the search performance over the quality of the result - Advanced scoring criteria +- `--read0` to read input delimited by ASCII NUL character +- `--print0` to print output delimited by ASCII NUL character 0.13.5 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 93882e3..13feb37 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -285,6 +285,12 @@ with the default enter key. e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR .RE .TP +.B "--read0" +Read input delimited by ASCII NUL character instead of newline character +.TP +.B "--print0" +Print output delimited by ASCII NUL character instead of newline character +.TP .B "--sync" Synchronous search for multi-staged filtering. If specified, fzf will launch ncurses finder only after the input stream is complete. diff --git a/src/core.go b/src/core.go index e707ad5..f7dae1a 100644 --- a/src/core.go +++ b/src/core.go @@ -151,7 +151,7 @@ func Run(opts *Options) { // Filtering mode if opts.Filter != nil { if opts.PrintQuery { - fmt.Println(*opts.Filter) + opts.Printer(*opts.Filter) } pattern := patternBuilder([]rune(*opts.Filter)) @@ -164,7 +164,7 @@ func Run(opts *Options) { item := chunkList.trans(runes, 0) if item != nil { if result, _, _ := pattern.MatchItem(item, false, slab); result != nil { - fmt.Println(item.text.ToString()) + opts.Printer(item.text.ToString()) found = true } } @@ -180,7 +180,7 @@ func Run(opts *Options) { chunks: snapshot, pattern: pattern}) for i := 0; i < merger.Length(); i++ { - fmt.Println(merger.Get(i).item.AsString(opts.Ansi)) + opts.Printer(merger.Get(i).item.AsString(opts.Ansi)) found = true } } @@ -254,13 +254,13 @@ func Run(opts *Options) { } else if val.final { if opts.Exit0 && count == 0 || opts.Select1 && count == 1 { if opts.PrintQuery { - fmt.Println(opts.Query) + opts.Printer(opts.Query) } if len(opts.Expect) > 0 { - fmt.Println() + opts.Printer("") } for i := 0; i < count; i++ { - fmt.Println(val.Get(i).item.AsString(opts.Ansi)) + opts.Printer(val.Get(i).item.AsString(opts.Ansi)) } if count > 0 { os.Exit(exitOk) diff --git a/src/options.go b/src/options.go index 2629c28..6ff71c0 100644 --- a/src/options.go +++ b/src/options.go @@ -162,6 +162,7 @@ type Options struct { Preview previewOpts PrintQuery bool ReadZero bool + Printer func(string) Sync bool History *History Header []string @@ -206,6 +207,7 @@ func defaultOptions() *Options { Preview: previewOpts{"", posRight, sizeSpec{50, true}, false}, PrintQuery: false, ReadZero: false, + Printer: func(str string) { fmt.Println(str) }, Sync: false, History: nil, Header: make([]string, 0), @@ -935,6 +937,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.ReadZero = true case "--no-read0": opts.ReadZero = false + case "--print0": + opts.Printer = func(str string) { fmt.Print(str, "\x00") } + case "--no-print0": + opts.Printer = func(str string) { fmt.Println(str) } case "--print-query": opts.PrintQuery = true case "--no-print-query": diff --git a/src/terminal.go b/src/terminal.go index 2a02ff8..c036117 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -63,6 +63,7 @@ type Terminal struct { reading bool jumping jumpMode jumpLabels string + printer func(string) merger *Merger selected map[int32]selectedItem reqBox *util.EventBox @@ -269,6 +270,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { reading: true, jumping: jumpDisabled, jumpLabels: opts.JumpLabels, + printer: opts.Printer, merger: EmptyMerger, selected: make(map[int32]selectedItem), reqBox: util.NewEventBox(), @@ -347,21 +349,21 @@ func (t *Terminal) UpdateList(merger *Merger) { func (t *Terminal) output() bool { if t.printQuery { - fmt.Println(string(t.input)) + t.printer(string(t.input)) } if len(t.expect) > 0 { - fmt.Println(t.pressed) + t.printer(t.pressed) } found := len(t.selected) > 0 if !found { cnt := t.merger.Length() if cnt > 0 && cnt > t.cy { - fmt.Println(t.current()) + t.printer(t.current()) found = true } } else { for _, sel := range t.sortSelected() { - fmt.Println(sel.text) + t.printer(sel.text) } } return found @@ -1028,7 +1030,7 @@ func (t *Terminal) Loop() { t.printPreview() case reqPrintQuery: C.Close() - fmt.Println(string(t.input)) + t.printer(string(t.input)) exit(exitOk) case reqQuit: C.Close() From 791076d3662f04a20386f6ea9d166f6eccbd1a81 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 21 Sep 2016 01:15:06 +0900 Subject: [PATCH 263/658] Fix panic when pattern occurs after 2^15-th column Fix #666 --- src/algo/algo.go | 15 ++++++++------- src/algo/algo_test.go | 10 ++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 622c960..1b85594 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -257,13 +257,14 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern [] } // Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages - offset := 0 + offset16 := 0 + offset32 := 0 // Bonus point for each position - offset, B := alloc16(offset, slab, N, false) + offset16, B := alloc16(offset16, slab, N, false) // The first occurrence of each character in the pattern - offset, F := alloc16(offset, slab, M, false) + offset32, F := alloc32(offset32, slab, M, false) // Rune array - _, T := alloc32(0, slab, N, false) + offset32, T := alloc32(offset32, slab, N, false) // Phase 1. Check if there's a match and calculate bonus for each point pidx, lastIdx, prevClass := 0, 0, charNonWord @@ -291,7 +292,7 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern [] if pidx < M { if char == pattern[pidx] { lastIdx = idx - F[pidx] = int16(idx) + F[pidx] = int32(idx) pidx++ } } else { @@ -307,10 +308,10 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern [] // Phase 2. Fill in score matrix (H) // Unlike the original algorithm, we do not allow omission. width := lastIdx - int(F[0]) + 1 - offset, H := alloc16(offset, slab, width*M, false) + offset16, H := alloc16(offset16, slab, width*M, false) // Possible length of consecutive chunk at each position. - offset, C := alloc16(offset, slab, width*M, false) + offset16, C := alloc16(offset16, slab, width*M, false) maxScore, maxScorePos := int16(0), 0 for i := 0; i < M; i++ { diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index 7317eb1..fc24f6d 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -1,6 +1,7 @@ package algo import ( + "math" "sort" "strings" "testing" @@ -154,3 +155,12 @@ func TestEmptyPattern(t *testing.T) { assertMatch(t, SuffixMatch, true, dir, "foobar", "", 6, 6, 0) } } + +func TestLongString(t *testing.T) { + bytes := make([]byte, math.MaxUint16*2) + for i := range bytes { + bytes[i] = 'x' + } + bytes[math.MaxUint16] = 'z' + assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive) +} From a749e6bd16a4fa1374dd454868a4084288ff78dd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 21 Sep 2016 01:15:35 +0900 Subject: [PATCH 264/658] Fix temp directory in a test case --- src/options_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/options_test.go b/src/options_test.go index c1bc914..cc4d07c 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -342,7 +342,7 @@ func TestDefaultCtrlNP(t *testing.T) { check([]string{"--bind=ctrl-n:accept"}, curses.CtrlN, actAccept) check([]string{"--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) - hist := "--history=/tmp/foo" + hist := "--history=/tmp/fzf-history" check([]string{hist}, curses.CtrlN, actNextHistory) check([]string{hist}, curses.CtrlP, actPreviousHistory) From 00f96aae769be244ea5f14811d58d8adddf587f7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 21 Sep 2016 01:23:41 +0900 Subject: [PATCH 265/658] Avoid rendering delay when displaying extremely long lines Related #666 --- src/terminal.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/terminal.go b/src/terminal.go index c036117..0e9436a 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -401,6 +401,8 @@ func displayWidth(runes []rune) int { const ( minWidth = 16 minHeight = 4 + + maxDisplayWidthCalc = 1024 ) func calculateSize(base int, size sizeSpec, margin int, minSize int) int { @@ -651,6 +653,11 @@ func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int { } func trimLeft(runes []rune, width int) ([]rune, int32) { + if len(runes) > maxDisplayWidthCalc && len(runes) > width { + trimmed := len(runes) - width + return runes[trimmed:], int32(trimmed) + } + currentWidth := displayWidth(runes) var trimmed int32 From 7fa5e6c86184b3127f64ce0f7a45b945df077e77 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 21 Sep 2016 01:28:24 +0900 Subject: [PATCH 266/658] 0.15.1 --- CHANGELOG.md | 5 +++++ install | 4 ++-- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a8ea8b..2280955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +0.15.1 +------ +- Fixed panic when the pattern occurs after 2^15-th column +- Fixed rendering delay when displaying extremely long lines + 0.15.0 ------ - Improved fuzzy search algorithm diff --git a/install b/install index 4d9e44a..579fcd3 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.15.0 pre=1 || - version=0.15.0 pre=0 +[[ "$@" =~ --pre ]] && version=0.15.1 pre=1 || + version=0.15.1 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 92d25e9..a6d674b 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Sep 2016" "fzf 0.15.0" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Sep 2016" "fzf 0.15.1" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 13feb37..f8e8239 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Sep 2016" "fzf 0.15.0" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Sep 2016" "fzf 0.15.1" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index acb3b4c..ca8b475 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.15.0" + version = "0.15.1" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 66d55fd89349b38e61ba2180ea8e7d8e5656c878 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 25 Sep 2016 02:02:00 +0900 Subject: [PATCH 267/658] Make preview windows scrollable Close #669 You can use your mouse or binadble preview-up and preview-down actions to scroll the content of the preview window. fzf --preview 'highlight -O ansi {}' --bind alt-j:preview-down,alt-k:preview-up --- src/options.go | 4 +++ src/terminal.go | 72 ++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/options.go b/src/options.go index 6ff71c0..3b412f8 100644 --- a/src/options.go +++ b/src/options.go @@ -663,6 +663,10 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) keymap[key] = actTogglePreview case "toggle-sort": keymap[key] = actToggleSort + case "preview-up": + keymap[key] = actPreviewUp + case "preview-down": + keymap[key] = actPreviewDown default: if isExecuteAction(actLower) { var offset int diff --git a/src/terminal.go b/src/terminal.go index 0e9436a..9bb9262 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -28,6 +28,13 @@ const ( jumpAcceptEnabled ) +type previewer struct { + text string + lines int + offset int + enabled bool +} + // Terminal represents terminal input/output type Terminal struct { initDelay time.Duration @@ -68,8 +75,7 @@ type Terminal struct { selected map[int32]selectedItem reqBox *util.EventBox preview previewOpts - previewing bool - previewTxt string + previewer previewer previewBox *util.EventBox eventBox *util.EventBox mutex sync.Mutex @@ -119,6 +125,7 @@ const ( reqPrintQuery reqPreviewEnqueue reqPreviewDisplay + reqPreviewRefresh reqQuit ) @@ -165,6 +172,8 @@ const ( actPrintQuery actToggleSort actTogglePreview + actPreviewUp + actPreviewDown actPreviousHistory actNextHistory actExecute @@ -275,8 +284,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { selected: make(map[int32]selectedItem), reqBox: util.NewEventBox(), preview: opts.Preview, - previewing: previewBox != nil && !opts.Preview.hidden, - previewTxt: "", + previewer: previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden}, previewBox: previewBox, eventBox: eventBox, mutex: sync.Mutex{}, @@ -772,9 +780,35 @@ func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 in } } +func numLinesMax(str string, max int) int { + lines := 0 + for lines < max { + idx := strings.Index(str, "\n") + if idx < 0 { + break + } + str = str[idx+1:] + lines++ + } + return lines +} + func (t *Terminal) printPreview() { t.pwindow.Erase() - extractColor(t.previewTxt, nil, func(str string, ansi *ansiState) bool { + skip := t.previewer.offset + extractColor(t.previewer.text, nil, func(str string, ansi *ansiState) bool { + if skip > 0 { + newlines := numLinesMax(str, skip) + if skip <= newlines { + for i := 0; i < skip; i++ { + str = str[strings.Index(str, "\n")+1:] + } + skip = 0 + } else { + skip -= newlines + return true + } + } if ansi != nil && ansi.colored() { return t.pwindow.CFill(str, ansi.fg, ansi.bg, ansi.bold) } @@ -891,7 +925,7 @@ func (t *Terminal) hasPreviewWindow() bool { } func (t *Terminal) isPreviewEnabled() bool { - return t.previewBox != nil && t.previewing + return t.previewBox != nil && t.previewer.enabled } func (t *Terminal) current() string { @@ -1033,7 +1067,11 @@ func (t *Terminal) Loop() { } exit(exitNoMatch) case reqPreviewDisplay: - t.previewTxt = value.(string) + t.previewer.text = value.(string) + t.previewer.lines = strings.Count(t.previewer.text, "\n") + t.previewer.offset = 0 + t.printPreview() + case reqPreviewRefresh: t.printPreview() case reqPrintQuery: C.Close() @@ -1118,10 +1156,10 @@ func (t *Terminal) Loop() { return false case actTogglePreview: if t.hasPreviewWindow() { - t.previewing = !t.previewing + t.previewer.enabled = !t.previewer.enabled t.resizeWindows() cnt := t.merger.Length() - if t.previewing && cnt > 0 && cnt > t.cy { + if t.previewer.enabled && cnt > 0 && cnt > t.cy { t.previewBox.Set(reqPreviewEnqueue, previewRequest{true, t.current()}) } req(reqList, reqInfo) @@ -1131,6 +1169,18 @@ func (t *Terminal) Loop() { t.eventBox.Set(EvtSearchNew, t.sort) t.mutex.Unlock() return false + case actPreviewUp: + if t.isPreviewEnabled() { + t.previewer.offset = util.Constrain( + t.previewer.offset-1, 0, t.previewer.lines-t.pwindow.Height) + req(reqPreviewRefresh) + } + case actPreviewDown: + if t.isPreviewEnabled() { + t.previewer.offset = util.Constrain( + t.previewer.offset+1, 0, t.previewer.lines-t.pwindow.Height) + req(reqPreviewRefresh) + } case actBeginningOfLine: t.cx = 0 case actBackwardChar: @@ -1299,6 +1349,10 @@ func (t *Terminal) Loop() { } t.vmove(me.S) req(reqList) + } else if t.isPreviewEnabled() && t.pwindow.Enclose(my, mx) { + t.previewer.offset = util.Constrain( + t.previewer.offset-me.S, 0, t.previewer.lines-t.pwindow.Height) + req(reqPreviewRefresh) } } else if t.window.Enclose(my, mx) { mx -= t.window.Left From 8c8b5b313e85cc3c13a1ebe33a6d718577d15a2f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 25 Sep 2016 04:12:44 +0900 Subject: [PATCH 268/658] Add preview-page-up and preview-page-down actions --- src/options.go | 4 ++++ src/terminal.go | 27 ++++++++++++++++++--------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/options.go b/src/options.go index 3b412f8..f586e5e 100644 --- a/src/options.go +++ b/src/options.go @@ -667,6 +667,10 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) keymap[key] = actPreviewUp case "preview-down": keymap[key] = actPreviewDown + case "preview-page-up": + keymap[key] = actPreviewPageUp + case "preview-page-down": + keymap[key] = actPreviewPageDown default: if isExecuteAction(actLower) { var offset int diff --git a/src/terminal.go b/src/terminal.go index 9bb9262..2d7f4c6 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -174,6 +174,8 @@ const ( actTogglePreview actPreviewUp actPreviewDown + actPreviewPageUp + actPreviewPageDown actPreviousHistory actNextHistory actExecute @@ -1123,6 +1125,11 @@ func (t *Terminal) Loop() { req(reqInfo) } } + scrollPreview := func(amount int) { + t.previewer.offset = util.Constrain( + t.previewer.offset+amount, 0, t.previewer.lines-t.pwindow.Height) + req(reqPreviewRefresh) + } for key, ret := range t.expect { if keyMatch(key, event) { t.pressed = ret @@ -1171,15 +1178,19 @@ func (t *Terminal) Loop() { return false case actPreviewUp: if t.isPreviewEnabled() { - t.previewer.offset = util.Constrain( - t.previewer.offset-1, 0, t.previewer.lines-t.pwindow.Height) - req(reqPreviewRefresh) + scrollPreview(-1) } case actPreviewDown: if t.isPreviewEnabled() { - t.previewer.offset = util.Constrain( - t.previewer.offset+1, 0, t.previewer.lines-t.pwindow.Height) - req(reqPreviewRefresh) + scrollPreview(1) + } + case actPreviewPageUp: + if t.isPreviewEnabled() { + scrollPreview(-t.pwindow.Height) + } + case actPreviewPageDown: + if t.isPreviewEnabled() { + scrollPreview(t.pwindow.Height) } case actBeginningOfLine: t.cx = 0 @@ -1350,9 +1361,7 @@ func (t *Terminal) Loop() { t.vmove(me.S) req(reqList) } else if t.isPreviewEnabled() && t.pwindow.Enclose(my, mx) { - t.previewer.offset = util.Constrain( - t.previewer.offset-me.S, 0, t.previewer.lines-t.pwindow.Height) - req(reqPreviewRefresh) + scrollPreview(-me.S) } } else if t.window.Enclose(my, mx) { mx -= t.window.Left From 2c8479a7c57e0e2662fc8b3f1e0a21d5da6b95b1 Mon Sep 17 00:00:00 2001 From: Maverick Woo Date: Sat, 24 Sep 2016 15:39:13 -0400 Subject: [PATCH 269/658] Fix #668 Handle uppercase letters in program names. This also deals with `-` and `.`, both of which are quite common in program names, e.g., `xdg-open` and `foo.sh`. --- shell/completion.bash | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 9436e27..31ebc60 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -32,7 +32,7 @@ fi _fzf_orig_completion_filter() { sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' | - awk -F= '{gsub(/[^a-z0-9_= ;]/, "_", $1); print $1"="$2}' + awk -F= '{gsub(/[^A-Za-z0-9_= ;]/, "_", $1); print $1"="$2}' } _fzf_opts_completion() { @@ -117,7 +117,7 @@ _fzf_handle_dynamic_completion() { __fzf_generic_path_completion() { local cur base dir leftover matches trigger cmd fzf [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" - cmd=$(echo "${COMP_WORDS[0]}" | sed 's/[^a-z0-9_=]/_/g') + cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}" COMPREPLY=() trigger=${FZF_COMPLETION_TRIGGER-'**'} cur="${COMP_WORDS[COMP_CWORD]}" @@ -162,7 +162,7 @@ _fzf_complete() { type -t "$post" > /dev/null 2>&1 || post=cat [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" - cmd=$(echo "${COMP_WORDS[0]}" | sed 's/[^a-z0-9_=]/_/g') + cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}" trigger=${FZF_COMPLETION_TRIGGER-'**'} cur="${COMP_WORDS[COMP_CWORD]}" if [[ "$cur" == *"$trigger" ]]; then @@ -277,7 +277,7 @@ _fzf_defc() { cmd="$1" func="$2" opts="$3" - orig_var="_fzf_orig_completion_$cmd" + orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" orig="${!orig_var}" if [ -n "$orig" ]; then printf -v def "$orig" "$func" From 0612074abee2392da57a0ed0a79cd3046ec5f298 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 25 Sep 2016 18:11:35 +0900 Subject: [PATCH 270/658] Support high intensity colors Close #671 --- src/ansi.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ansi.go b/src/ansi.go index 0a52567..b870fe6 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -140,6 +140,10 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { state.fg = num - 30 } else if num >= 40 && num <= 47 { state.bg = num - 40 + } else if num >= 90 && num <= 97 { + state.fg = num - 90 + 8 + } else if num >= 100 && num <= 107 { + state.bg = num - 100 + 8 } } case 1: From bef405bfa5f4ea3ec37505f1f97b4eac0645dd55 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 25 Sep 2016 19:03:08 +0900 Subject: [PATCH 271/658] Ignore VT100-related escape codes --- src/ansi.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ansi.go b/src/ansi.go index b870fe6..2bf4c62 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -33,7 +33,7 @@ func (s *ansiState) equals(t *ansiState) bool { var ansiRegex *regexp.Regexp func init() { - ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]") + ansiRegex = regexp.MustCompile("\x1b.[0-9;]*.") } func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) { @@ -98,7 +98,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { } else { state = &ansiState{prevState.fg, prevState.bg, prevState.bold} } - if ansiCode[len(ansiCode)-1] == 'K' { + if ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' { return state } From 1bc223d4b3849818b14c87e886359c2e4a617fb2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 25 Sep 2016 22:20:43 +0900 Subject: [PATCH 272/658] 0.15.2 --- CHANGELOG.md | 11 +++++++++++ install | 4 ++-- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2280955..bd978f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ CHANGELOG ========= +0.15.2 +------ +- Preview window is now scrollable + - With mouse scroll or with bindable actions + - `preview-up` + - `preview-down` + - `preview-page-up` + - `preview-page-down` +- Updated ANSI processor to support high intensity colors and ignore + some VT100-related escape sequences + 0.15.1 ------ - Fixed panic when the pattern occurs after 2^15-th column diff --git a/install b/install index 579fcd3..60041b5 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.15.1 pre=1 || - version=0.15.1 pre=0 +[[ "$@" =~ --pre ]] && version=0.15.2 pre=1 || + version=0.15.2 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index a6d674b..107d497 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Sep 2016" "fzf 0.15.1" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Sep 2016" "fzf 0.15.2" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index f8e8239..ac8e5e4 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Sep 2016" "fzf 0.15.1" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Sep 2016" "fzf 0.15.2" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index ca8b475..1582c9d 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.15.1" + version = "0.15.2" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 1acd2adce271ffaf0bfb303c6dc27fe2b03c9e04 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 26 Sep 2016 15:33:46 +0900 Subject: [PATCH 273/658] Update man page: missing actions --- man/man1/fzf.1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index ac8e5e4..2686414 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -434,6 +434,10 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) \fBpage-down\fR \fIpgdn\fR \fBpage-up\fR \fIpgup\fR + \fBpreview-down\fR + \fBpreview-up\fR + \fBpreview-page-down\fR + \fBpreview-page-up\fR \fBprevious-history\fR (\fIctrl-p\fR on \fB--history\fR) \fBprint-query\fR (print query and exit) \fBselect-all\fR From 1fc565984244bdaf21e736bb9a129fff0de7cab1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 29 Sep 2016 00:54:27 +0900 Subject: [PATCH 274/658] Add support for more ANSI color attributes (#674) Dim, underline, blink, reverse --- src/ansi.go | 24 +++++++++++++++++------- src/curses/curses.go | 35 +++++++++++++++++++++-------------- src/result.go | 10 +++++----- src/terminal.go | 38 +++++++++++++++++++------------------- 4 files changed, 62 insertions(+), 45 deletions(-) diff --git a/src/ansi.go b/src/ansi.go index 2bf4c62..e5ac81c 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -6,6 +6,8 @@ import ( "strconv" "strings" "unicode/utf8" + + "github.com/junegunn/fzf/src/curses" ) type ansiOffset struct { @@ -16,18 +18,18 @@ type ansiOffset struct { type ansiState struct { fg int bg int - bold bool + attr curses.Attr } func (s *ansiState) colored() bool { - return s.fg != -1 || s.bg != -1 || s.bold + return s.fg != -1 || s.bg != -1 || s.attr > 0 } func (s *ansiState) equals(t *ansiState) bool { if t == nil { return !s.colored() } - return s.fg == t.fg && s.bg == t.bg && s.bold == t.bold + return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr } var ansiRegex *regexp.Regexp @@ -94,9 +96,9 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { // State var state *ansiState if prevState == nil { - state = &ansiState{-1, -1, false} + state = &ansiState{-1, -1, 0} } else { - state = &ansiState{prevState.fg, prevState.bg, prevState.bold} + state = &ansiState{prevState.fg, prevState.bg, prevState.attr} } if ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' { return state @@ -108,7 +110,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { init := func() { state.fg = -1 state.bg = -1 - state.bold = false + state.attr = 0 state256 = 0 } @@ -132,7 +134,15 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { case 49: state.bg = -1 case 1: - state.bold = true + state.attr = curses.Bold + case 2: + state.attr = curses.Dim + case 4: + state.attr = curses.Underline + case 5: + state.attr = curses.Blink + case 7: + state.attr = curses.Reverse case 0: init() default: diff --git a/src/curses/curses.go b/src/curses/curses.go index 7a9ccd4..700e667 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -23,6 +23,16 @@ import ( "unicode/utf8" ) +const ( + Bold = C.A_BOLD + Dim = C.A_DIM + Blink = C.A_BLINK + Reverse = C.A_REVERSE + Underline = C.A_UNDERLINE +) + +type Attr C.int + // Types of user action const ( Rune = iota @@ -158,7 +168,7 @@ type MouseEvent struct { var ( _buf []byte _in *os.File - _color func(int, bool) C.int + _color func(int, Attr) C.int _colorMap map[int]int _prevDownTime time.Time _clickY []int @@ -183,7 +193,7 @@ type Window struct { func NewWindow(top int, left int, width int, height int, border bool) *Window { win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left)) if border { - attr := _color(ColBorder, false) + attr := _color(ColBorder, 0) C.wattron(win, attr) C.box(win, 0, 0) C.wattroff(win, attr) @@ -266,22 +276,19 @@ func init() { Border: 145} } -func attrColored(pair int, bold bool) C.int { +func attrColored(pair int, a Attr) C.int { var attr C.int if pair > ColNormal { attr = C.COLOR_PAIR(C.int(pair)) } - if bold { - attr = attr | C.A_BOLD - } - return attr + return attr | C.int(a) } -func attrMono(pair int, bold bool) C.int { +func attrMono(pair int, a Attr) C.int { var attr C.int switch pair { case ColCurrent: - if bold { + if a&C.A_BOLD == C.A_BOLD { attr = C.A_REVERSE } case ColMatch: @@ -289,7 +296,7 @@ func attrMono(pair int, bold bool) C.int { case ColCurrentMatch: attr = C.A_UNDERLINE | C.A_REVERSE } - if bold { + if a&C.A_BOLD == C.A_BOLD { attr = attr | C.A_BOLD } return attr @@ -648,8 +655,8 @@ func (w *Window) Print(text string) { }, text))) } -func (w *Window) CPrint(pair int, bold bool, text string) { - attr := _color(pair, bold) +func (w *Window) CPrint(pair int, a Attr, text string) { + attr := _color(pair, a) C.wattron(w.win, attr) w.Print(text) C.wattroff(w.win, attr) @@ -675,8 +682,8 @@ func (w *Window) Fill(str string) bool { return C.waddstr(w.win, C.CString(str)) == C.OK } -func (w *Window) CFill(str string, fg int, bg int, bold bool) bool { - attr := _color(PairFor(fg, bg), bold) +func (w *Window) CFill(str string, fg int, bg int, a Attr) bool { + attr := _color(PairFor(fg, bg), a) C.wattron(w.win, attr) ret := w.Fill(str) C.wattroff(w.win, attr) diff --git a/src/result.go b/src/result.go index 87478ab..9152f67 100644 --- a/src/result.go +++ b/src/result.go @@ -14,7 +14,7 @@ type Offset [2]int32 type colorOffset struct { offset [2]int32 color int - bold bool + attr curses.Attr index int32 } @@ -91,14 +91,14 @@ func minRank() rank { return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}} } -func (result *Result) colorOffsets(matchOffsets []Offset, color int, bold bool, current bool) []colorOffset { +func (result *Result) colorOffsets(matchOffsets []Offset, color int, attr curses.Attr, current bool) []colorOffset { itemColors := result.item.Colors() if len(itemColors) == 0 { var offsets []colorOffset for _, off := range matchOffsets { - offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold}) + offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, attr: attr}) } return offsets } @@ -142,7 +142,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, color int, bold bool, if curr != 0 && idx > start { if curr == -1 { colors = append(colors, colorOffset{ - offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold}) + offset: [2]int32{int32(start), int32(idx)}, color: color, attr: attr}) } else { ansi := itemColors[curr-1] fg := ansi.color.fg @@ -164,7 +164,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, color int, bold bool, colors = append(colors, colorOffset{ offset: [2]int32{int32(start), int32(idx)}, color: curses.PairFor(fg, bg), - bold: ansi.color.bold || bold}) + attr: ansi.color.attr | attr}) } } } diff --git a/src/terminal.go b/src/terminal.go index 2d7f4c6..14dcd28 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -526,24 +526,24 @@ func (t *Terminal) placeCursor() { func (t *Terminal) printPrompt() { t.move(0, 0, true) - t.window.CPrint(C.ColPrompt, true, t.prompt) - t.window.CPrint(C.ColNormal, true, string(t.input)) + t.window.CPrint(C.ColPrompt, C.Bold, t.prompt) + t.window.CPrint(C.ColNormal, C.Bold, string(t.input)) } func (t *Terminal) printInfo() { if t.inlineInfo { t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true) if t.reading { - t.window.CPrint(C.ColSpinner, true, " < ") + t.window.CPrint(C.ColSpinner, C.Bold, " < ") } else { - t.window.CPrint(C.ColPrompt, true, " < ") + t.window.CPrint(C.ColPrompt, C.Bold, " < ") } } else { t.move(1, 0, true) if t.reading { duration := int64(spinnerDuration) idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration - t.window.CPrint(C.ColSpinner, true, _spinner[idx]) + t.window.CPrint(C.ColSpinner, C.Bold, _spinner[idx]) } t.move(1, 2, false) } @@ -562,7 +562,7 @@ func (t *Terminal) printInfo() { if t.progress > 0 && t.progress < 100 { output += fmt.Sprintf(" (%d%%)", t.progress) } - t.window.CPrint(C.ColInfo, false, output) + t.window.CPrint(C.ColInfo, 0, output) } func (t *Terminal) printHeader() { @@ -586,7 +586,7 @@ func (t *Terminal) printHeader() { colors: colors} t.move(line, 2, true) - t.printHighlighted(&Result{item: item}, false, C.ColHeader, 0, false) + t.printHighlighted(&Result{item: item}, 0, C.ColHeader, 0, false) } } @@ -620,21 +620,21 @@ func (t *Terminal) printItem(result *Result, i int, current bool) { } else if current { label = ">" } - t.window.CPrint(C.ColCursor, true, label) + t.window.CPrint(C.ColCursor, C.Bold, label) if current { if selected { - t.window.CPrint(C.ColSelected, true, ">") + t.window.CPrint(C.ColSelected, C.Bold, ">") } else { - t.window.CPrint(C.ColCurrent, true, " ") + t.window.CPrint(C.ColCurrent, C.Bold, " ") } - t.printHighlighted(result, true, C.ColCurrent, C.ColCurrentMatch, true) + t.printHighlighted(result, C.Bold, C.ColCurrent, C.ColCurrentMatch, true) } else { if selected { - t.window.CPrint(C.ColSelected, true, ">") + t.window.CPrint(C.ColSelected, C.Bold, ">") } else { t.window.Print(" ") } - t.printHighlighted(result, false, 0, C.ColMatch, false) + t.printHighlighted(result, 0, 0, C.ColMatch, false) } } @@ -690,7 +690,7 @@ func overflow(runes []rune, max int) bool { return false } -func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 int, current bool) { +func (t *Terminal) printHighlighted(result *Result, attr C.Attr, col1 int, col2 int, current bool) { item := result.item // Overflow @@ -715,7 +715,7 @@ func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 in maxe = util.Max(maxe, int(offset[1])) } - offsets := result.colorOffsets(charOffsets, col2, bold, current) + offsets := result.colorOffsets(charOffsets, col2, attr, current) maxWidth := t.window.Width - 3 maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) if overflow(text, maxWidth) { @@ -764,11 +764,11 @@ func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 in e := util.Constrain32(offset.offset[1], index, maxOffset) substr, prefixWidth = processTabs(text[index:b], prefixWidth) - t.window.CPrint(col1, bold, substr) + t.window.CPrint(col1, attr, substr) if b < e { substr, prefixWidth = processTabs(text[b:e], prefixWidth) - t.window.CPrint(offset.color, offset.bold, substr) + t.window.CPrint(offset.color, offset.attr, substr) } index = e @@ -778,7 +778,7 @@ func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 in } if index < maxOffset { substr, _ = processTabs(text[index:], prefixWidth) - t.window.CPrint(col1, bold, substr) + t.window.CPrint(col1, attr, substr) } } @@ -812,7 +812,7 @@ func (t *Terminal) printPreview() { } } if ansi != nil && ansi.colored() { - return t.pwindow.CFill(str, ansi.fg, ansi.bg, ansi.bold) + return t.pwindow.CFill(str, ansi.fg, ansi.bg, ansi.attr) } return t.pwindow.Fill(str) }) From 3b5ae0f8a2ccdbad2656109c44ec7fb7322c7a67 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 29 Sep 2016 01:06:35 +0900 Subject: [PATCH 275/658] Fix failing unit tests on ANSI attributes --- src/ansi_test.go | 16 +++++++++++----- src/result_test.go | 16 ++++++++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/ansi_test.go b/src/ansi_test.go index a80e98a..0ba9e40 100644 --- a/src/ansi_test.go +++ b/src/ansi_test.go @@ -3,13 +3,19 @@ package fzf import ( "fmt" "testing" + + "github.com/junegunn/fzf/src/curses" ) func TestExtractColor(t *testing.T) { assert := func(offset ansiOffset, b int32, e int32, fg int, bg int, bold bool) { + var attr curses.Attr + if bold { + attr = curses.Bold + } if offset.offset[0] != b || offset.offset[1] != e || - offset.color.fg != fg || offset.color.bg != bg || offset.color.bold != bold { - t.Error(offset, b, e, fg, bg, bold) + offset.color.fg != fg || offset.color.bg != bg || offset.color.attr != attr { + t.Error(offset, b, e, fg, bg, attr) } } @@ -121,7 +127,7 @@ func TestExtractColor(t *testing.T) { if len(*offsets) != 1 { t.Fail() } - if state.fg != 2 || state.bg != -1 || !state.bold { + if state.fg != 2 || state.bg != -1 || state.attr == 0 { t.Fail() } assert((*offsets)[0], 6, 11, 2, -1, true) @@ -132,7 +138,7 @@ func TestExtractColor(t *testing.T) { if len(*offsets) != 1 { t.Fail() } - if state.fg != 2 || state.bg != -1 || !state.bold { + if state.fg != 2 || state.bg != -1 || state.attr == 0 { t.Fail() } assert((*offsets)[0], 0, 11, 2, -1, true) @@ -143,7 +149,7 @@ func TestExtractColor(t *testing.T) { if len(*offsets) != 2 { t.Fail() } - if state.fg != 200 || state.bg != 100 || state.bold { + if state.fg != 200 || state.bg != 100 || state.attr > 0 { t.Fail() } assert((*offsets)[0], 0, 6, 2, -1, true) diff --git a/src/result_test.go b/src/result_test.go index c6832f1..46c20fd 100644 --- a/src/result_test.go +++ b/src/result_test.go @@ -97,16 +97,20 @@ func TestColorOffset(t *testing.T) { item := Result{ item: &Item{ colors: &[]ansiOffset{ - ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}}, - ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}}, - ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}}, - ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}} + ansiOffset{[2]int32{0, 20}, ansiState{1, 5, 0}}, + ansiOffset{[2]int32{22, 27}, ansiState{2, 6, curses.Bold}}, + ansiOffset{[2]int32{30, 32}, ansiState{3, 7, 0}}, + ansiOffset{[2]int32{33, 40}, ansiState{4, 8, curses.Bold}}}}} // [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}] - colors := item.colorOffsets(offsets, 99, false, true) + colors := item.colorOffsets(offsets, 99, 0, true) assert := func(idx int, b int32, e int32, c int, bold bool) { + var attr curses.Attr + if bold { + attr = curses.Bold + } o := colors[idx] - if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold { + if o.offset[0] != b || o.offset[1] != e || o.color != c || o.attr != attr { t.Error(o) } } From 957c12e7d702a30c925b6d9c02b6bd07acdaa3ff Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 29 Sep 2016 02:53:05 +0900 Subject: [PATCH 276/658] Fix SEGV when trying to render preview but the window is closed Close #677 --- CHANGELOG.md | 5 +++++ src/terminal.go | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd978f3..f5b9954 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +0.15.3 +------ +- Added support for more ANSI attributes: dim, underline, blink, and reverse +- Fixed race condition in `toggle-preview` + 0.15.2 ------ - Preview window is now scrollable diff --git a/src/terminal.go b/src/terminal.go index 14dcd28..02fe731 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -796,6 +796,9 @@ func numLinesMax(str string, max int) int { } func (t *Terminal) printPreview() { + if !t.isPreviewEnabled() { + return + } t.pwindow.Erase() skip := t.previewer.offset extractColor(t.previewer.text, nil, func(str string, ansi *ansiState) bool { @@ -839,9 +842,7 @@ func (t *Terminal) printAll() { t.printPrompt() t.printInfo() t.printHeader() - if t.isPreviewEnabled() { - t.printPreview() - } + t.printPreview() } func (t *Terminal) refresh() { From 8b0d0342d43ed410e8542655932b60c9c0d817c6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 29 Sep 2016 03:05:20 +0900 Subject: [PATCH 277/658] 0.15.3 --- install | 4 ++-- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/install b/install index 60041b5..3b7ca05 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.15.2 pre=1 || - version=0.15.2 pre=0 +[[ "$@" =~ --pre ]] && version=0.15.3 pre=1 || + version=0.15.3 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 107d497..6c2557d 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Sep 2016" "fzf 0.15.2" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Sep 2016" "fzf 0.15.3" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 2686414..2c399a1 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Sep 2016" "fzf 0.15.2" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Sep 2016" "fzf 0.15.3" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 1582c9d..35818cd 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.15.2" + version = "0.15.3" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 04492bab10aeb0d081eeb8806792ccb77a8c5cb6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 29 Sep 2016 22:40:22 +0900 Subject: [PATCH 278/658] Use unicode.IsSpace to cover more whitespace characters --- src/result.go | 3 ++- src/util/chars.go | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/result.go b/src/result.go index 9152f67..bc1deb4 100644 --- a/src/result.go +++ b/src/result.go @@ -3,6 +3,7 @@ package fzf import ( "math" "sort" + "unicode" "github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/util" @@ -62,7 +63,7 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result { for idx := 0; idx < numChars; idx++ { r := item.text.Get(idx) whitePrefixLen = idx - if idx == minBegin || r != ' ' && r != '\t' { + if idx == minBegin || !unicode.IsSpace(r) { break } } diff --git a/src/util/chars.go b/src/util/chars.go index 12417c6..061120e 100644 --- a/src/util/chars.go +++ b/src/util/chars.go @@ -1,6 +1,7 @@ package util import ( + "unicode" "unicode/utf8" ) @@ -63,7 +64,7 @@ func (chars *Chars) TrimLength() int { len := chars.Length() for i = len - 1; i >= 0; i-- { char := chars.Get(i) - if char != ' ' && char != '\t' { + if !unicode.IsSpace(char) { break } } @@ -75,7 +76,7 @@ func (chars *Chars) TrimLength() int { var j int for j = 0; j < len; j++ { char := chars.Get(j) - if char != ' ' && char != '\t' { + if !unicode.IsSpace(char) { break } } @@ -86,7 +87,7 @@ func (chars *Chars) TrailingWhitespaces() int { whitespaces := 0 for i := chars.Length() - 1; i >= 0; i-- { char := chars.Get(i) - if char != ' ' && char != '\t' { + if !unicode.IsSpace(char) { break } whitespaces++ From 3066b206af11258c06cc0b549e2056db9549855a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 3 Oct 2016 14:16:10 +0900 Subject: [PATCH 279/658] Support field index expressions in preview and execute action Also close #679. The placeholder for the current query is {q}. --- src/terminal.go | 109 +++++++++++++++++++++++++++++++++---------- src/terminal_test.go | 73 +++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 25 deletions(-) create mode 100644 src/terminal_test.go diff --git a/src/terminal.go b/src/terminal.go index 02fe731..59cb8a6 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -20,6 +20,12 @@ import ( // import "github.com/pkg/profile" +var placeholder *regexp.Regexp + +func init() { + placeholder = regexp.MustCompile("\\\\?(?:{[0-9,-.]*}|{q})") +} + type jumpMode int const ( @@ -51,6 +57,7 @@ type Terminal struct { multi bool sort bool toggleSort bool + delimiter Delimiter expect map[int]string keymap map[int]actionType execmap map[int]string @@ -87,16 +94,11 @@ type Terminal struct { type selectedItem struct { at time.Time - text string + item *Item } type byTimeOrder []selectedItem -type previewRequest struct { - ok bool - str string -} - func (a byTimeOrder) Len() int { return len(a) } @@ -267,6 +269,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { multi: opts.Multi, sort: opts.Sort > 0, toggleSort: opts.ToggleSort, + delimiter: opts.Delimiter, expect: opts.Expect, keymap: opts.Keymap, execmap: opts.Execmap, @@ -373,7 +376,7 @@ func (t *Terminal) output() bool { } } else { for _, sel := range t.sortSelected() { - t.printer(sel.text) + t.printer(sel.item.AsString(t.ansi)) } } return found @@ -912,8 +915,60 @@ func quoteEntry(entry string) string { return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" } -func (t *Terminal) executeCommand(template string, replacement string) { - command := strings.Replace(template, "{}", replacement, -1) +func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, query string, items []*Item) string { + return placeholder.ReplaceAllStringFunc(template, func(match string) string { + // Escaped pattern + if match[0] == '\\' { + return match[1:] + } + + // Current query + if match == "{q}" { + return quoteEntry(query) + } + + replacements := make([]string, len(items)) + + if match == "{}" { + for idx, item := range items { + replacements[idx] = quoteEntry(item.AsString(stripAnsi)) + } + return strings.Join(replacements, " ") + } + + tokens := strings.Split(match[1:len(match)-1], ",") + ranges := make([]Range, len(tokens)) + for idx, s := range tokens { + r, ok := ParseRange(&s) + if !ok { + // Invalid expression, just return the original string in the template + return match + } + ranges[idx] = r + } + + for idx, item := range items { + chars := util.RunesToChars([]rune(item.AsString(stripAnsi))) + tokens := Tokenize(chars, delimiter) + trans := Transform(tokens, ranges) + str := string(joinTokens(trans)) + if delimiter.str != nil { + str = strings.TrimSuffix(str, *delimiter.str) + } else if delimiter.regex != nil { + delims := delimiter.regex.FindAllStringIndex(str, -1) + if len(delims) > 0 && delims[len(delims)-1][1] == len(str) { + str = str[:delims[len(delims)-1][0]] + } + } + str = strings.TrimSpace(str) + replacements[idx] = quoteEntry(str) + } + return strings.Join(replacements, " ") + }) +} + +func (t *Terminal) executeCommand(template string, items []*Item) { + command := replacePlaceholder(template, t.ansi, t.delimiter, string(t.input), items) cmd := util.ExecCommand(command) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -931,8 +986,12 @@ func (t *Terminal) isPreviewEnabled() bool { return t.previewBox != nil && t.previewer.enabled } +func (t *Terminal) currentItem() *Item { + return t.merger.Get(t.cy).item +} + func (t *Terminal) current() string { - return t.merger.Get(t.cy).item.AsString(t.ansi) + return t.currentItem().AsString(t.ansi) } // Loop is called to start Terminal I/O @@ -989,18 +1048,19 @@ func (t *Terminal) Loop() { if t.hasPreviewWindow() { go func() { for { - request := previewRequest{false, ""} + var request *Item t.previewBox.Wait(func(events *util.Events) { for req, value := range *events { switch req { case reqPreviewEnqueue: - request = value.(previewRequest) + request = value.(*Item) } } events.Clear() }) - if request.ok { - command := strings.Replace(t.preview.command, "{}", quoteEntry(request.str), -1) + if request != nil { + command := replacePlaceholder(t.preview.command, + t.ansi, t.delimiter, string(t.input), []*Item{request}) cmd := util.ExecCommand(command) out, _ := cmd.CombinedOutput() t.reqBox.Set(reqPreviewDisplay, string(out)) @@ -1020,7 +1080,7 @@ func (t *Terminal) Loop() { } go func() { - focused := previewRequest{false, ""} + var focused *Item for { t.reqBox.Wait(func(events *util.Events) { defer events.Clear() @@ -1037,11 +1097,11 @@ func (t *Terminal) Loop() { case reqList: t.printList() cnt := t.merger.Length() - var currentFocus previewRequest + var currentFocus *Item if cnt > 0 && cnt > t.cy { - currentFocus = previewRequest{true, t.current()} + currentFocus = t.currentItem() } else { - currentFocus = previewRequest{false, ""} + currentFocus = nil } if currentFocus != focused { focused = currentFocus @@ -1109,7 +1169,7 @@ func (t *Terminal) Loop() { } selectItem := func(item *Item) bool { if _, found := t.selected[item.Index()]; !found { - t.selected[item.Index()] = selectedItem{time.Now(), item.AsString(t.ansi)} + t.selected[item.Index()] = selectedItem{time.Now(), item} return true } return false @@ -1146,16 +1206,15 @@ func (t *Terminal) Loop() { case actIgnore: case actExecute: if t.cy >= 0 && t.cy < t.merger.Length() { - item := t.merger.Get(t.cy).item - t.executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi))) + t.executeCommand(t.execmap[mapkey], []*Item{t.currentItem()}) } case actExecuteMulti: if len(t.selected) > 0 { - sels := make([]string, len(t.selected)) + sels := make([]*Item, len(t.selected)) for i, sel := range t.sortSelected() { - sels[i] = quoteEntry(sel.text) + sels[i] = sel.item } - t.executeCommand(t.execmap[mapkey], strings.Join(sels, " ")) + t.executeCommand(t.execmap[mapkey], sels) } else { return doAction(actExecute, mapkey) } @@ -1168,7 +1227,7 @@ func (t *Terminal) Loop() { t.resizeWindows() cnt := t.merger.Length() if t.previewer.enabled && cnt > 0 && cnt > t.cy { - t.previewBox.Set(reqPreviewEnqueue, previewRequest{true, t.current()}) + t.previewBox.Set(reqPreviewEnqueue, t.currentItem()) } req(reqList, reqInfo) } diff --git a/src/terminal_test.go b/src/terminal_test.go new file mode 100644 index 0000000..5afafaa --- /dev/null +++ b/src/terminal_test.go @@ -0,0 +1,73 @@ +package fzf + +import ( + "regexp" + "testing" + + "github.com/junegunn/fzf/src/util" +) + +func newItem(str string) *Item { + bytes := []byte(str) + trimmed, _, _ := extractColor(str, nil, nil) + return &Item{origText: &bytes, text: util.RunesToChars([]rune(trimmed))} +} + +func TestReplacePlaceholder(t *testing.T) { + items1 := []*Item{newItem(" foo'bar \x1b[31mbaz\x1b[m")} + items2 := []*Item{ + newItem("foo'bar \x1b[31mbaz\x1b[m"), + newItem("FOO'BAR \x1b[31mBAZ\x1b[m")} + + var result string + check := func(expected string) { + if result != expected { + t.Errorf("expected: %s, actual: %s", expected, result) + } + } + + // {}, preserve ansi + result = replacePlaceholder("echo {}", false, Delimiter{}, "query", items1) + check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'") + + // {}, strip ansi + result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items1) + check("echo ' foo'\\''bar baz'") + + // {}, with multiple items + result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items2) + check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ'") + + // {..}, strip leading whitespaces, preserve ansi + result = replacePlaceholder("echo {..}", false, Delimiter{}, "query", items1) + check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'") + + // {..}, strip leading whitespaces, strip ansi + result = replacePlaceholder("echo {..}", true, Delimiter{}, "query", items1) + check("echo 'foo'\\''bar baz'") + + // {q} + result = replacePlaceholder("echo {} {q}", true, Delimiter{}, "query", items1) + check("echo ' foo'\\''bar baz' 'query'") + + // {q}, multiple items + result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, "query 'string'", items2) + check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'") + + result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "query", items1) + check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") + + result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "query", items2) + check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''") + + // String delimiter + delim := "'" + result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, "query", items1) + check("echo ' foo'\\''bar baz'/'foo'/'bar baz'") + + // Regex delimiter + regex := regexp.MustCompile("[oa]+") + // foo'bar baz + result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, "query", items1) + check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'") +} From 01b88539ba912da4f49cfba48e2a736c04ec6006 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 4 Oct 2016 00:20:36 +0900 Subject: [PATCH 280/658] [vim] Apply --multi and --prompt to :FZF command --- README.md | 2 +- plugin/fzf.vim | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5a90bdb..997ec7c 100644 --- a/README.md +++ b/README.md @@ -305,7 +305,7 @@ If you have set up fzf for Vim, `:FZF` command will be added. :FZF ~ " With options -:FZF --no-sort -m /tmp +:FZF --no-sort --reverse --inline-info /tmp " Bang version starts in fullscreen instead of using tmux pane or Neovim split :FZF! diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 8e3859d..2eb98d4 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -558,11 +558,15 @@ let s:default_action = { function! s:cmd(bang, ...) abort let args = copy(a:000) - let opts = {} + let opts = { 'options': '--multi ' } if len(args) && isdirectory(expand(args[-1])) - let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g') + let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '/*$', '/', '') + let opts.options .= ' --prompt '.shellescape(opts.dir) + else + let opts.options .= ' --prompt '.shellescape(pathshorten(getcwd()).'/') endif - call fzf#run(fzf#wrap('FZF', extend({'options': join(args)}, opts), a:bang)) + let opts.options .= ' '.join(args) + call fzf#run(fzf#wrap('FZF', opts, a:bang)) endfunction command! -nargs=* -complete=dir -bang FZF call s:cmd(0, ) From 51f532697ebd73f7e48a73b0fe2d2bf62271bcc5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 4 Oct 2016 01:39:48 +0900 Subject: [PATCH 281/658] Adjust maximum scroll offset It was possible that a few lines at the bottom may not be visible if there are lines above that span multiple lines. --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 59cb8a6..20e8441 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1188,7 +1188,7 @@ func (t *Terminal) Loop() { } scrollPreview := func(amount int) { t.previewer.offset = util.Constrain( - t.previewer.offset+amount, 0, t.previewer.lines-t.pwindow.Height) + t.previewer.offset+amount, 0, t.previewer.lines-1) req(reqPreviewRefresh) } for key, ret := range t.expect { From 154cf22ffaf8f629113e5b83a34c72805df02548 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 4 Oct 2016 01:40:45 +0900 Subject: [PATCH 282/658] Display scroll indicator in preview window --- src/terminal.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/terminal.go b/src/terminal.go index 20e8441..e07eb3c 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -822,6 +822,11 @@ func (t *Terminal) printPreview() { } return t.pwindow.Fill(str) }) + if t.previewer.offset > 0 { + offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines) + t.pwindow.Move(0, t.pwindow.Width-len(offset)) + t.pwindow.CPrint(C.ColInfo, C.Reverse, offset) + } } func processTabs(runes []rune, prefixWidth int) (string, int) { From aeb957a2852555b2fce037aac4c3cb2b9d2f5c39 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 4 Oct 2016 02:09:03 +0900 Subject: [PATCH 283/658] Use exact match by default for inverse search term This is a breaking change, but I believe it makes much more sense. It is almost impossible to predict which entries will be filtered out due to a fuzzy inverse term. You can still perform inverse-fuzzy-match by prepending `!'` to the term. | Token | Match type | Description | | -------- | -------------------------- | --------------------------------- | | `sbtrkt` | fuzzy-match | Items that match `sbtrkt` | | `^music` | prefix-exact-match | Items that start with `music` | | `.mp3$` | suffix-exact-match | Items that end with `.mp3` | | `'wild` | exact-match (quoted) | Items that include `wild` | | `!fire` | inverse-exact-match | Items that do not include `fire` | | `!.mp3$` | inverse-suffix-exact-match | Items that do not end with `.mp3` | --- src/pattern.go | 3 ++- src/pattern_test.go | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index 7e5f428..82272af 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -163,12 +163,13 @@ func parseTerms(fuzzy bool, caseMode Case, str string) []termSet { if strings.HasPrefix(text, "!") { inv = true + typ = termExact text = text[1:] } if strings.HasPrefix(text, "'") { // Flip exactness - if fuzzy { + if fuzzy && !inv { typ = termExact text = text[1:] } else { diff --git a/src/pattern_test.go b/src/pattern_test.go index a3aa596..9b6d394 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -22,15 +22,15 @@ func TestParseTermsExtended(t *testing.T) { terms[1][0].typ != termExact || terms[1][0].inv || terms[2][0].typ != termPrefix || terms[2][0].inv || terms[3][0].typ != termSuffix || terms[3][0].inv || - terms[4][0].typ != termFuzzy || !terms[4][0].inv || - terms[5][0].typ != termExact || !terms[5][0].inv || + terms[4][0].typ != termExact || !terms[4][0].inv || + terms[5][0].typ != termFuzzy || !terms[5][0].inv || terms[6][0].typ != termPrefix || !terms[6][0].inv || terms[7][0].typ != termSuffix || !terms[7][0].inv || terms[7][1].typ != termEqual || terms[7][1].inv || terms[8][0].typ != termPrefix || terms[8][0].inv || terms[8][1].typ != termExact || terms[8][1].inv || terms[8][2].typ != termSuffix || terms[8][2].inv || - terms[8][3].typ != termFuzzy || !terms[8][3].inv { + terms[8][3].typ != termExact || !terms[8][3].inv { t.Errorf("%s", terms) } for idx, termSet := range terms[:8] { From 3222d62ddf0b8072e43ac2b5abd52b1d6d3f6eef Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 4 Oct 2016 02:17:36 +0900 Subject: [PATCH 284/658] 0.15.4 --- CHANGELOG.md | 13 +++++++++++++ README.md | 18 +++++++++--------- install | 4 ++-- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 24 ++++++++++++++++-------- src/constants.go | 2 +- 6 files changed, 42 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5b9954..7bbe4f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ CHANGELOG ========= +0.15.4 +------ +- Added support for range expression in preview and execute action + - e.g. `ls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1` + - `{q}` will be replaced to the single-quoted string of the current query +- Fixed to properly handle unicode whitespace characters +- Display scroll indicator in preview window +- Inverse search term will use exact matcher by default + - This is a breaking change, but I believe it makes much more sense. It is + almost impossible to predict which entries will be filtered out due to + a fuzzy inverse term. You can still perform inverse-fuzzy-match by + prepending `!'` to the term. + 0.15.3 ------ - Added support for more ANSI attributes: dim, underline, blink, and reverse diff --git a/README.md b/README.md index 997ec7c..a5d2e95 100644 --- a/README.md +++ b/README.md @@ -113,16 +113,16 @@ vim $(fzf) Unless otherwise specified, fzf starts in "extended-search mode" where you can type in multiple search terms delimited by spaces. e.g. `^music .mp3$ sbtrkt -!rmx` +!fire` -| Token | Match type | Description | -| -------- | -------------------- | -------------------------------- | -| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` | -| `^music` | prefix-exact-match | Items that start with `music` | -| `.mp3$` | suffix-exact-match | Items that end with `.mp3` | -| `'wild` | exact-match (quoted) | Items that include `wild` | -| `!rmx` | inverse-fuzzy-match | Items that do not match `rmx` | -| `!'fire` | inverse-exact-match | Items that do not include `fire` | +| Token | Match type | Description | +| -------- | -------------------------- | --------------------------------- | +| `sbtrkt` | fuzzy-match | Items that match `sbtrkt` | +| `^music` | prefix-exact-match | Items that start with `music` | +| `.mp3$` | suffix-exact-match | Items that end with `.mp3` | +| `'wild` | exact-match (quoted) | Items that include `wild` | +| `!fire` | inverse-exact-match | Items that do not include `fire` | +| `!.mp3$` | inverse-suffix-exact-match | Items that do not end with `.mp3` | If you don't prefer fuzzy matching and do not wish to "quote" every word, start fzf with `-e` or `--exact` option. Note that when `--exact` is set, diff --git a/install b/install index 3b7ca05..7a5b797 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.15.3 pre=1 || - version=0.15.3 pre=0 +[[ "$@" =~ --pre ]] && version=0.15.4 pre=1 || + version=0.15.4 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 6c2557d..b01f0b7 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Sep 2016" "fzf 0.15.3" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Oct 2016" "fzf 0.15.4" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 2c399a1..9616701 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Sep 2016" "fzf 0.15.3" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Oct 2016" "fzf 0.15.4" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -232,11 +232,17 @@ automatically truncated when the number of the lines exceeds the value. .TP .BI "--preview=" "COMMAND" Execute the given command for the current line and display the result on the -preview window. \fB{}\fR is the placeholder for the quoted string of the -current line. +preview window. \fB{}\fR in the command is the placeholder that is replaced to +the single-quoted string of the current line. To transform the replacement +string, specify field index expressions between the braces (See \fBFIELD INDEX +EXPRESSION\fR for the details). Also, \fB{q}\fR is replaced to the current +query string. .RS e.g. \fBfzf --preview="head -$LINES {}"\fR + \fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR + +Note that you can escape a placeholder pattern by prepending a backslash. .RE .TP .BI "--preview-window=" "[POSITION][:SIZE[%]][:hidden]" @@ -358,7 +364,7 @@ with the given string. An anchored-match term is also an exact-match term. .SS Negation If a term is prefixed by \fB!\fR, fzf will exclude the lines that satisfy the -term from the result. +term from the result. In this case, fzf performs exact match by default. .SS Exact-match by default If you don't prefer fuzzy matching and do not wish to "quote" (prefixing with @@ -460,9 +466,11 @@ binding \fBenter\fR key to \fBless\fR command like follows. \fBfzf --bind "enter:execute(less {})"\fR -\fB{}\fR is the placeholder for the quoted string of the current line. -If the command contains parentheses, you can use any of the following -alternative notations to avoid parse errors. +You can use the same placeholder expressions as in \fB--preview\fR. + +If the command contains parentheses, fzf may fail to parse the expression. In +that case, you can use any of the following alternative notations to avoid +parse errors. \fBexecute[...]\fR \fBexecute~...~\fR @@ -481,7 +489,7 @@ alternative notations to avoid parse errors. .RS This is the special form that frees you from parse errors as it does not expect the closing character. The catch is that it should be the last one in the -comma-separated list. +comma-separated list of key-action pairs. .RE \fBexecute-multi(...)\fR is an alternative action that executes the command diff --git a/src/constants.go b/src/constants.go index 35818cd..a96c362 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.15.3" + version = "0.15.4" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 2134c0c8a95dc0c385def157be75e36465ae624b Mon Sep 17 00:00:00 2001 From: Pierre Neidhardt Date: Tue, 11 Oct 2016 21:12:31 +0530 Subject: [PATCH 285/658] key-bindings.fish: Yank commandline in fzf-history-widget --- shell/key-bindings.fish | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index 6c76177..eaec912 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -26,8 +26,8 @@ function fzf_key_bindings end function fzf-history-widget - history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS > $TMPDIR/fzf.result - and commandline (cat $TMPDIR/fzf.result) + history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q '(commandline)' > $TMPDIR/fzf.result + and commandline -- (cat $TMPDIR/fzf.result) commandline -f repaint rm -f $TMPDIR/fzf.result end From 053af9a1c8db1c0cc9236da305d07bf382f8e7ab Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 12 Oct 2016 23:09:21 +0900 Subject: [PATCH 286/658] [fzf-tmux/vim/nvim] Do not split small window Close #699 --- bin/fzf-tmux | 5 +++-- plugin/fzf.vim | 34 ++++++++++++++++++---------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index ff923f0..1c17921 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -17,6 +17,7 @@ swap="" close="" term="" [[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) +[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) help() { >&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] @@ -83,7 +84,7 @@ while [[ $# -gt 0 ]]; do else if [[ -n "$swap" ]]; then if [[ "$arg" =~ ^.l ]]; then - [[ -n "$COLUMNS" ]] && max=$COLUMNS || max=$(tput cols) + max=$columns else max=$lines fi @@ -108,7 +109,7 @@ while [[ $# -gt 0 ]]; do [[ -n "$skip" ]] && args+=("$arg") done -if [[ -z "$TMUX" ]] || [[ "$lines" -le 15 ]]; then +if [[ -z "$TMUX" ]] || [[ "$opt" =~ ^-h ]] && [[ "$columns" -le 40 ]] || [[ ! "$opt" =~ ^-h ]] && [[ "$lines" -le 15 ]]; then "$fzf" "${args[@]}" exit $? fi diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 2eb98d4..42dbcda 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -289,7 +289,8 @@ function! s:fzf_tmux(dict) endfunction function! s:splittable(dict) - return s:present(a:dict, 'up', 'down', 'left', 'right') + return s:present(a:dict, 'up', 'down') && &lines > 15 || + \ s:present(a:dict, 'left', 'right') && &columns > 40 endfunction function! s:pushd(dict) @@ -405,24 +406,25 @@ function! s:split(dict) \ 'right': ['vertical botright', 'vertical resize', &columns] } let ppos = s:getpos() try - for [dir, triple] in items(directions) - let val = get(a:dict, dir, '') - if !empty(val) - let [cmd, resz, max] = triple - if (dir == 'up' || dir == 'down') && val[0] == '~' - let sz = s:calc_size(max, val, a:dict) - else - let sz = s:calc_size(max, val, {}) - endif - execute cmd sz.'new' - execute resz sz - return [ppos, {}] - endif - endfor if s:present(a:dict, 'window') execute a:dict.window - else + elseif !s:splittable(a:dict) execute (tabpagenr()-1).'tabnew' + else + for [dir, triple] in items(directions) + let val = get(a:dict, dir, '') + if !empty(val) + let [cmd, resz, max] = triple + if (dir == 'up' || dir == 'down') && val[0] == '~' + let sz = s:calc_size(max, val, a:dict) + else + let sz = s:calc_size(max, val, {}) + endif + execute cmd sz.'new' + execute resz sz + return [ppos, {}] + endif + endfor endif return [ppos, { '&l:wfw': &l:wfw, '&l:wfh': &l:wfh }] finally From ac549a853ad4203f3e2601c9ca489b53e20ffeff Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 13 Oct 2016 10:42:26 +0900 Subject: [PATCH 287/658] [fzf-tmux] Fix bash condition Fix #702 --- bin/fzf-tmux | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 1c17921..dd335d2 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -109,7 +109,7 @@ while [[ $# -gt 0 ]]; do [[ -n "$skip" ]] && args+=("$arg") done -if [[ -z "$TMUX" ]] || [[ "$opt" =~ ^-h ]] && [[ "$columns" -le 40 ]] || [[ ! "$opt" =~ ^-h ]] && [[ "$lines" -le 15 ]]; then +if [[ -z "$TMUX" || "$opt" =~ ^-h && "$columns" -le 40 || ! "$opt" =~ ^-h && "$lines" -le 15 ]]; then "$fzf" "${args[@]}" exit $? fi From 93e0a6a9deaf76f59b74f0f60d6a628dd21ced3d Mon Sep 17 00:00:00 2001 From: Niraj Thapaliya Date: Wed, 12 Oct 2016 11:18:03 -0600 Subject: [PATCH 288/658] Gnu `[` evaluates both sides of a -o condition regardless It doesn't short circuit like we expect, causing trouble when $dir is empty Use shell builtin instead --- shell/completion.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index cef8afa..cc2ac9c 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -44,7 +44,7 @@ __fzf_generic_path_completion() { setopt localoptions nonomatch dir="$base" while [ 1 ]; do - if [ -z "$dir" -o -d ${~dir} ]; then + if [[ -z "$dir" || -d ${~dir} ]]; then leftover=${base/#"$dir"} leftover=${leftover/#\/} [ -z "$dir" ] && dir='.' From d3da310b926f1aa4c297d34d4157d064e4a7e379 Mon Sep 17 00:00:00 2001 From: Niraj Thapaliya Date: Thu, 13 Oct 2016 09:48:05 -0600 Subject: [PATCH 289/658] Use `command` to ignore shell function --- shell/completion.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index cc2ac9c..0be5ad1 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -111,7 +111,7 @@ _fzf_complete_telnet() { _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( - cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \ + command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \ <(command grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u From 0541c0dbcf96ff40bd80cb8359191dc0fa01d83d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 18 Oct 2016 01:11:04 +0900 Subject: [PATCH 290/658] Use relative position instead of absolute distance for --tiebreak=end Fix unintuitive result where `*fzf*/install` is ranked higher than `fzf/src/fzf/*fzf*-linux_386` on --tiebreak=end. --- src/result.go | 12 ++++++------ test/test_go.rb | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/result.go b/src/result.go index bc1deb4..347cf59 100644 --- a/src/result.go +++ b/src/result.go @@ -57,7 +57,7 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result { case byLength: // If offsets is empty, trimLen will be 0, but we don't care val = util.AsUint16(trimLen) - case byBegin: + case byBegin, byEnd: if validOffsetFound { whitePrefixLen := 0 for idx := 0; idx < numChars; idx++ { @@ -67,11 +67,11 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result { break } } - val = util.AsUint16(minBegin - whitePrefixLen) - } - case byEnd: - if validOffsetFound { - val = util.AsUint16(1 + numChars - maxEnd) + if criterion == byBegin { + val = util.AsUint16(minBegin - whitePrefixLen) + } else { + val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/trimLen) + } } } result.rank.points[idx] = val diff --git a/test/test_go.rb b/test/test_go.rb index ff4b173..dd132d3 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -604,8 +604,8 @@ class TestGoFZF < TestBase ], `#{FZF} -fo --tiebreak=end < #{tempname}`.split($/) assert_equal [ - ' xxxxoxxx', 'xxxxxoxxx', + ' xxxxoxxx', 'xxxxoxxxx', 'xxxoxxxxxx', 'xxoxxxxxxx', From 9b9ad3914380dccefa9280805522d3ee37638bb6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 18 Oct 2016 15:00:47 +0900 Subject: [PATCH 291/658] [vim] Set g:loaded_fzf --- plugin/fzf.vim | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 42dbcda..36a1480 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -21,6 +21,11 @@ " OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +if exists('g:loaded_fzf') + finish +endif +let g:loaded_fzf = 1 + let s:default_layout = { 'down': '~40%' } let s:layout_keys = ['window', 'up', 'down', 'left', 'right'] let s:fzf_go = expand(':h:h').'/bin/fzf' From cfdb00b9718822b6ab2018a47288d5be3a32d07c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 21 Oct 2016 19:20:00 +0900 Subject: [PATCH 292/658] Allow other options to follow --color without spec --- src/options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/options.go b/src/options.go index f586e5e..a7f0188 100644 --- a/src/options.go +++ b/src/options.go @@ -246,7 +246,7 @@ func nextString(args []string, i *int, message string) string { } func optionalNextString(args []string, i *int) string { - if len(args) > *i+1 { + if len(args) > *i+1 && !strings.HasPrefix(args[*i+1], "-") { *i++ return args[*i] } From 0a8d2996dcb3cd3ebf55b783b08667fa3740433b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 21 Oct 2016 19:35:59 +0900 Subject: [PATCH 293/658] Set foreground color without affecting background Close #712 --- src/curses/curses.go | 117 ++++++++++++++++++++++--------------------- src/options.go | 2 - src/options_test.go | 10 +--- src/result.go | 12 ++--- src/result_test.go | 2 +- src/terminal.go | 6 ++- 6 files changed, 73 insertions(+), 76 deletions(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index 700e667..638a862 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -113,7 +113,8 @@ const ( // Pallete const ( - ColNormal = iota + _ = iota + ColNormal ColPrompt ColMatch ColCurrent @@ -134,7 +135,6 @@ const ( ) type ColorTheme struct { - UseDefault bool Fg int16 Bg int16 DarkBg int16 @@ -168,7 +168,8 @@ type MouseEvent struct { var ( _buf []byte _in *os.File - _color func(int, Attr) C.int + _color bool + _colorFn func(int, Attr) C.int _colorMap map[int]int _prevDownTime time.Time _clickY []int @@ -176,10 +177,6 @@ var ( Default16 *ColorTheme Dark256 *ColorTheme Light256 *ColorTheme - FG int - CurrentFG int - BG int - DarkBG int ) type Window struct { @@ -192,12 +189,16 @@ type Window struct { func NewWindow(top int, left int, width int, height int, border bool) *Window { win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left)) + if _color { + C.wbkgd(win, C.chtype(C.COLOR_PAIR(ColNormal))) + } if border { - attr := _color(ColBorder, 0) + attr := _colorFn(ColBorder, 0) C.wattron(win, attr) C.box(win, 0, 0) C.wattroff(win, attr) } + return &Window{ win: win, Top: top, @@ -209,7 +210,6 @@ func NewWindow(top int, left int, width int, height int, border bool) *Window { func EmptyTheme() *ColorTheme { return &ColorTheme{ - UseDefault: true, Fg: colUndefined, Bg: colUndefined, DarkBg: colUndefined, @@ -230,9 +230,8 @@ func init() { _clickY = []int{} _colorMap = make(map[int]int) Default16 = &ColorTheme{ - UseDefault: true, - Fg: 15, - Bg: 0, + Fg: colDefault, + Bg: colDefault, DarkBg: C.COLOR_BLACK, Prompt: C.COLOR_BLUE, Match: C.COLOR_GREEN, @@ -245,9 +244,8 @@ func init() { Header: C.COLOR_CYAN, Border: C.COLOR_BLACK} Dark256 = &ColorTheme{ - UseDefault: true, - Fg: 15, - Bg: 0, + Fg: colDefault, + Bg: colDefault, DarkBg: 236, Prompt: 110, Match: 108, @@ -260,9 +258,8 @@ func init() { Header: 109, Border: 59} Light256 = &ColorTheme{ - UseDefault: true, - Fg: 15, - Bg: 0, + Fg: colDefault, + Bg: colDefault, DarkBg: 251, Prompt: 25, Match: 66, @@ -278,7 +275,7 @@ func init() { func attrColored(pair int, a Attr) C.int { var attr C.int - if pair > ColNormal { + if pair > 0 { attr = C.COLOR_PAIR(C.int(pair)) } return attr | C.int(a) @@ -344,7 +341,8 @@ func Init(theme *ColorTheme, black bool, mouse bool) { C.noecho() C.raw() // stty dsusp undef - if theme != nil { + _color = theme != nil + if _color { C.start_color() var baseTheme *ColorTheme if C.tigetnum(C.CString("colors")) >= 256 { @@ -353,52 +351,57 @@ func Init(theme *ColorTheme, black bool, mouse bool) { baseTheme = Default16 } initPairs(baseTheme, theme, black) - _color = attrColored + C.bkgd(C.chtype(C.COLOR_PAIR(ColNormal))) + _colorFn = attrColored } else { - _color = attrMono + _colorFn = attrMono } } -func override(a int16, b int16) C.short { - if b == colUndefined { - return C.short(a) +func override(baseTheme *ColorTheme, theme *ColorTheme) { + o := func(a int16, b int16) int16 { + if b == colUndefined { + return a + } + return b } - return C.short(b) + theme.Fg = o(baseTheme.Fg, theme.Fg) + theme.Bg = o(baseTheme.Bg, theme.Bg) + theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg) + theme.Prompt = o(baseTheme.Prompt, theme.Prompt) + theme.Match = o(baseTheme.Match, theme.Match) + theme.Current = o(baseTheme.Current, theme.Current) + theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch) + theme.Spinner = o(baseTheme.Spinner, theme.Spinner) + theme.Info = o(baseTheme.Info, theme.Info) + theme.Cursor = o(baseTheme.Cursor, theme.Cursor) + theme.Selected = o(baseTheme.Selected, theme.Selected) + theme.Header = o(baseTheme.Header, theme.Header) + theme.Border = o(baseTheme.Border, theme.Border) } func initPairs(baseTheme *ColorTheme, theme *ColorTheme, black bool) { - fg := override(baseTheme.Fg, theme.Fg) - bg := override(baseTheme.Bg, theme.Bg) if black { - bg = C.COLOR_BLACK - } else if theme.UseDefault { - fg = colDefault - bg = colDefault - C.use_default_colors() - } - if theme.UseDefault { - FG = colDefault - BG = colDefault - } else { - FG = int(fg) - BG = int(bg) - C.assume_default_colors(C.int(override(baseTheme.Fg, theme.Fg)), C.int(bg)) + theme.Bg = C.COLOR_BLACK } + // Updates theme + override(baseTheme, theme) - currentFG := override(baseTheme.Current, theme.Current) - darkBG := override(baseTheme.DarkBg, theme.DarkBg) - CurrentFG = int(currentFG) - DarkBG = int(darkBG) - C.init_pair(ColPrompt, override(baseTheme.Prompt, theme.Prompt), bg) - C.init_pair(ColMatch, override(baseTheme.Match, theme.Match), bg) - C.init_pair(ColCurrent, currentFG, darkBG) - C.init_pair(ColCurrentMatch, override(baseTheme.CurrentMatch, theme.CurrentMatch), darkBG) - C.init_pair(ColSpinner, override(baseTheme.Spinner, theme.Spinner), bg) - C.init_pair(ColInfo, override(baseTheme.Info, theme.Info), bg) - C.init_pair(ColCursor, override(baseTheme.Cursor, theme.Cursor), darkBG) - C.init_pair(ColSelected, override(baseTheme.Selected, theme.Selected), darkBG) - C.init_pair(ColHeader, override(baseTheme.Header, theme.Header), bg) - C.init_pair(ColBorder, override(baseTheme.Border, theme.Border), bg) + C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg)) + initPair := func(group C.short, fg int16, bg int16) { + C.init_pair(group, C.short(fg), C.short(bg)) + } + initPair(ColNormal, theme.Fg, theme.Bg) + initPair(ColPrompt, theme.Prompt, theme.Bg) + initPair(ColMatch, theme.Match, theme.Bg) + initPair(ColCurrent, theme.Current, theme.DarkBg) + initPair(ColCurrentMatch, theme.CurrentMatch, theme.DarkBg) + initPair(ColSpinner, theme.Spinner, theme.Bg) + initPair(ColInfo, theme.Info, theme.Bg) + initPair(ColCursor, theme.Cursor, theme.DarkBg) + initPair(ColSelected, theme.Selected, theme.DarkBg) + initPair(ColHeader, theme.Header, theme.Bg) + initPair(ColBorder, theme.Border, theme.Bg) } func Close() { @@ -656,7 +659,7 @@ func (w *Window) Print(text string) { } func (w *Window) CPrint(pair int, a Attr, text string) { - attr := _color(pair, a) + attr := _colorFn(pair, a) C.wattron(w.win, attr) w.Print(text) C.wattroff(w.win, attr) @@ -683,7 +686,7 @@ func (w *Window) Fill(str string) bool { } func (w *Window) CFill(str string, fg int, bg int, a Attr) bool { - attr := _color(PairFor(fg, bg), a) + attr := _colorFn(PairFor(fg, bg), a) C.wattron(w.win, attr) ret := w.Fill(str) C.wattroff(w.win, attr) diff --git a/src/options.go b/src/options.go index a7f0188..c5e6d7b 100644 --- a/src/options.go +++ b/src/options.go @@ -499,10 +499,8 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme switch pair[0] { case "fg": theme.Fg = ansi - theme.UseDefault = theme.UseDefault && ansi < 0 case "bg": theme.Bg = ansi - theme.UseDefault = theme.UseDefault && ansi < 0 case "fg+": theme.Current = ansi case "bg+": diff --git a/src/options_test.go b/src/options_test.go index cc4d07c..6013617 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -299,20 +299,14 @@ func TestColorSpec(t *testing.T) { } customized.Fg = curses.Dark256.Fg customized.Bg = curses.Dark256.Bg - if *curses.Dark256 == *customized { - t.Errorf("colors should now be equivalent") + if *curses.Dark256 != *customized { + t.Errorf("colors should now be equivalent: %v, %v", curses.Dark256, customized) } customized = parseTheme(theme, "fg:231,dark,bg:232") if customized.Fg != curses.Dark256.Fg || customized.Bg == curses.Dark256.Bg { t.Errorf("color not customized") } - if customized.UseDefault { - t.Errorf("not using default colors") - } - if !curses.Dark256.UseDefault { - t.Errorf("using default colors") - } } func TestParseNilTheme(t *testing.T) { diff --git a/src/result.go b/src/result.go index 347cf59..69e83f8 100644 --- a/src/result.go +++ b/src/result.go @@ -92,13 +92,13 @@ func minRank() rank { return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}} } -func (result *Result) colorOffsets(matchOffsets []Offset, color int, attr curses.Attr, current bool) []colorOffset { +func (result *Result) colorOffsets(matchOffsets []Offset, theme *curses.ColorTheme, color int, attr curses.Attr, current bool) []colorOffset { itemColors := result.item.Colors() + // No ANSI code, or --color=no if len(itemColors) == 0 { var offsets []colorOffset for _, off := range matchOffsets { - offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, attr: attr}) } return offsets @@ -149,17 +149,17 @@ func (result *Result) colorOffsets(matchOffsets []Offset, color int, attr curses fg := ansi.color.fg if fg == -1 { if current { - fg = curses.CurrentFG + fg = int(theme.Current) } else { - fg = curses.FG + fg = int(theme.Fg) } } bg := ansi.color.bg if bg == -1 { if current { - bg = curses.DarkBG + bg = int(theme.DarkBg) } else { - bg = curses.BG + bg = int(theme.Bg) } } colors = append(colors, colorOffset{ diff --git a/src/result_test.go b/src/result_test.go index 46c20fd..645684d 100644 --- a/src/result_test.go +++ b/src/result_test.go @@ -103,7 +103,7 @@ func TestColorOffset(t *testing.T) { ansiOffset{[2]int32{33, 40}, ansiState{4, 8, curses.Bold}}}}} // [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}] - colors := item.colorOffsets(offsets, 99, 0, true) + colors := item.colorOffsets(offsets, curses.Dark256, 99, 0, true) assert := func(idx int, b int32, e int32, c int, bold bool) { var attr curses.Attr if bold { diff --git a/src/terminal.go b/src/terminal.go index e07eb3c..2abe62e 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -90,6 +90,7 @@ type Terminal struct { suppress bool startChan chan bool slab *util.Slab + theme *C.ColorTheme } type selectedItem struct { @@ -295,6 +296,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { mutex: sync.Mutex{}, suppress: true, slab: util.MakeSlab(slab16Size, slab32Size), + theme: opts.Theme, startChan: make(chan bool, 1), initFunc: func() { C.Init(opts.Theme, opts.Black, opts.Mouse) @@ -637,7 +639,7 @@ func (t *Terminal) printItem(result *Result, i int, current bool) { } else { t.window.Print(" ") } - t.printHighlighted(result, 0, 0, C.ColMatch, false) + t.printHighlighted(result, 0, C.ColNormal, C.ColMatch, false) } } @@ -718,7 +720,7 @@ func (t *Terminal) printHighlighted(result *Result, attr C.Attr, col1 int, col2 maxe = util.Max(maxe, int(offset[1])) } - offsets := result.colorOffsets(charOffsets, col2, attr, current) + offsets := result.colorOffsets(charOffsets, t.theme, col2, attr, current) maxWidth := t.window.Width - 3 maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) if overflow(text, maxWidth) { From 400e443a0a397e559250fa090b5c437e44d01bf8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 22 Oct 2016 00:01:21 +0900 Subject: [PATCH 294/658] Make test cases less susceptible to timeout errors --- test/test_go.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index dd132d3..f34d8b3 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -136,8 +136,10 @@ class Tmux def prepare tries = 0 begin - self.send_keys 'C-u', 'hello', 'Right' - self.until { |lines| lines[-1].end_with?('hello') } + self.until do |lines| + self.send_keys 'C-u', 'hello' + lines[-1].end_with?('hello') + end rescue Exception (tries += 1) < 5 ? retry : raise end From 37f2d8f7952879bffd1326b88e4f0069ec1188d7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 22 Oct 2016 01:14:04 +0900 Subject: [PATCH 295/658] [vim] Respect g:fzf_colors Close #711 --- README.md | 5 +++-- plugin/fzf.vim | 20 +++++++++++++++++++- test/fzf.vader | 4 ++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a5d2e95..ab57fb9 100644 --- a/README.md +++ b/README.md @@ -319,7 +319,7 @@ Note that the environment variables `FZF_DEFAULT_COMMAND` and `FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for customization. -[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim) +[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-Vim-plugin #### `fzf#run` @@ -347,7 +347,8 @@ page](https://github.com/junegunn/fzf/wiki/Examples-(vim)). `fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper function that decorates the options dictionary so that it understands -`g:fzf_layout`, `g:fzf_action`, and `g:fzf_history_dir` like `:FZF`. +`g:fzf_layout`, `g:fzf_action`, `g:fzf_colors`, and `g:fzf_history_dir` like +`:FZF`. ```vim command! -bang MyStuff diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 36a1480..96e30c4 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -159,6 +159,22 @@ function! s:common_sink(action, lines) abort endtry endfunction +function! s:get_color(attr, ...) + for group in a:000 + let code = synIDattr(synIDtrans(hlID(group)), a:attr, 'cterm') + if code =~ '^[0-9]\+$' + return code + endif + endfor + return '' +endfunction + +function! s:defaults() + let rules = copy(get(g:, 'fzf_colors', {})) + let colors = join(map(items(filter(map(rules, 'call("s:get_color", v:val)'), '!empty(v:val)')), 'join(v:val, ":")'), ',') + return empty(colors) ? '' : ('--color='.colors) +endfunction + " [name string,] [opts dict,] [fullscreen boolean] function! fzf#wrap(...) let args = ['', {}, 0] @@ -190,8 +206,10 @@ function! fzf#wrap(...) endif endif + " Colors: g:fzf_colors + let opts.options = s:defaults() .' '. get(opts, 'options', '') + " History: g:fzf_history_dir - let opts.options = get(opts, 'options', '') if len(name) && len(get(g:, 'fzf_history_dir', '')) let dir = expand(g:fzf_history_dir) if !isdirectory(dir) diff --git a/test/fzf.vader b/test/fzf.vader index bab5c16..c6f899f 100644 --- a/test/fzf.vader +++ b/test/fzf.vader @@ -143,6 +143,10 @@ Execute (fzf#wrap): Assert opts.options =~ '--history /tmp/foobar' Assert opts.options =~ '--color light' + let g:fzf_colors = { 'fg': ['fg', 'Error'] } + let opts = fzf#wrap({}) + Assert opts.options =~ '^--color=fg:' + Execute (Cleanup): unlet g:dir Restore From 9f30ca292355db7af200c41fac45882b529a56f9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 23 Oct 2016 22:00:32 +0900 Subject: [PATCH 296/658] 0.15.5 --- CHANGELOG.md | 7 +++++++ install | 4 ++-- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bbe4f4..d7f4df9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +0.15.5 +------ +- Setting foreground color will no longer set background color to black + - e.g. `fzf --color fg:153` +- `--tiebreak=end` will consider relative position instead of absolute distance +- Updated `fzf#wrap` function to respect `g:fzf_colors` + 0.15.4 ------ - Added support for range expression in preview and execute action diff --git a/install b/install index 7a5b797..5d3baf8 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.15.4 pre=1 || - version=0.15.4 pre=0 +[[ "$@" =~ --pre ]] && version=0.15.5 pre=1 || + version=0.15.5 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index b01f0b7..68efe99 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Oct 2016" "fzf 0.15.4" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Oct 2016" "fzf 0.15.5" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 9616701..8479cec 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Oct 2016" "fzf 0.15.4" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Oct 2016" "fzf 0.15.5" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index a96c362..bec3bb6 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.15.4" + version = "0.15.5" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 9f321cbe138fa2d19d13be078f2a5d4ba7412787 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 28 Oct 2016 03:13:32 +0900 Subject: [PATCH 297/658] Fix header lines being cleared on toggle-preview Close #722 --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 2abe62e..376c640 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1236,7 +1236,7 @@ func (t *Terminal) Loop() { if t.previewer.enabled && cnt > 0 && cnt > t.cy { t.previewBox.Set(reqPreviewEnqueue, t.currentItem()) } - req(reqList, reqInfo) + req(reqList, reqInfo, reqHeader) } case actToggleSort: t.sort = !t.sort From 02c6ad0e59be75981baeb1f41cb0bad03aad1c6b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 30 Oct 2016 11:43:06 +0900 Subject: [PATCH 298/658] Strip ^N and ^O from preview output https://github.com/junegunn/fzf/issues/391#issuecomment-257090266 e.g. fzf --preview 'printf "$(tput setaf 2)foo$(tput sgr0)bar\nbar\n"' --- src/curses/curses.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/curses/curses.go b/src/curses/curses.go index 638a862..e0e728c 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -682,7 +682,13 @@ func (w *Window) Erase() { } func (w *Window) Fill(str string) bool { - return C.waddstr(w.win, C.CString(str)) == C.OK + return C.waddstr(w.win, C.CString(strings.Map(func(r rune) rune { + // Remove ^N and ^O (set and unset altcharset) + if r == 14 || r == 15 { + return -1 + } + return r + }, str))) == C.OK } func (w *Window) CFill(str string, fg int, bg int, a Attr) bool { From 06a6ad8bcafdd2e8a07fdc9c39ad09ab212a95d7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 30 Oct 2016 12:29:29 +0900 Subject: [PATCH 299/658] Update ANSI processor to ignore ^N and ^O This reverts commit 02c6ad0e59be75981baeb1f41cb0bad03aad1c6b. --- src/ansi.go | 4 ++-- src/curses/curses.go | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/ansi.go b/src/ansi.go index e5ac81c..02be18c 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -35,7 +35,7 @@ func (s *ansiState) equals(t *ansiState) bool { var ansiRegex *regexp.Regexp func init() { - ansiRegex = regexp.MustCompile("\x1b.[0-9;]*.") + ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*.|[\x0e\x0f]") } func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) { @@ -100,7 +100,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { } else { state = &ansiState{prevState.fg, prevState.bg, prevState.attr} } - if ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' { + if ansiCode[0] != '\x1b' || ansiCode[len(ansiCode)-1] != 'm' { return state } diff --git a/src/curses/curses.go b/src/curses/curses.go index e0e728c..638a862 100644 --- a/src/curses/curses.go +++ b/src/curses/curses.go @@ -682,13 +682,7 @@ func (w *Window) Erase() { } func (w *Window) Fill(str string) bool { - return C.waddstr(w.win, C.CString(strings.Map(func(r rune) rune { - // Remove ^N and ^O (set and unset altcharset) - if r == 14 || r == 15 { - return -1 - } - return r - }, str))) == C.OK + return C.waddstr(w.win, C.CString(str)) == C.OK } func (w *Window) CFill(str string, fg int, bg int, a Attr) bool { From 2cff00dce24df2a7e5add001423c6d8200b57c87 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 1 Nov 2016 00:39:02 +0900 Subject: [PATCH 300/658] `man fzf` in README Close #726 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index ab57fb9..82d059a 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,10 @@ or `py`. - Default options - e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"` +#### Options + +See the man page (`man fzf`) for the full list of options. + Examples -------- From 0c573b3dffe806253e1df2447754a5f3939a11f0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 24 Oct 2016 09:44:56 +0900 Subject: [PATCH 301/658] Prepare for termbox/windows build `TAGS=termbox make` (or `go build -tags termbox`) --- src/Makefile | 2 +- src/README.md | 6 +- src/ansi.go | 28 +- src/ansi_test.go | 8 +- src/constants.go | 1 - src/constants_unix.go | 8 + src/constants_windows.go | 8 + src/options.go | 88 +-- src/options_test.go | 172 +++--- src/result.go | 20 +- src/result_test.go | 24 +- src/terminal.go | 181 +++--- src/terminal_unix.go | 13 + src/terminal_windows.go | 11 + src/{curses/curses.go => tui/ncurses.go} | 559 ++++++------------ src/tui/termbox.go | 151 +++++ src/tui/tui.go | 250 ++++++++ .../curses_test.go => tui/tui_test.go} | 2 +- src/util/util.go | 17 +- src/util/util_unix.go | 17 + src/util/util_windows.go | 17 + 21 files changed, 924 insertions(+), 659 deletions(-) create mode 100644 src/constants_unix.go create mode 100644 src/constants_windows.go create mode 100644 src/terminal_unix.go create mode 100644 src/terminal_windows.go rename src/{curses/curses.go => tui/ncurses.go} (61%) create mode 100644 src/tui/termbox.go create mode 100644 src/tui/tui.go rename src/{curses/curses_test.go => tui/tui_test.go} (91%) create mode 100644 src/util/util_unix.go create mode 100644 src/util/util_windows.go diff --git a/src/Makefile b/src/Makefile index f7aa0b2..edc77fa 100644 --- a/src/Makefile +++ b/src/Makefile @@ -43,7 +43,7 @@ $(SRCDIR): ln -s $(ROOTDIR) $(SRCDIR) deps: $(SRCDIR) $(SOURCES) - cd $(SRCDIR) && go get + cd $(SRCDIR) && go get -tags "$(TAGS)" android-build: $(SRCDIR) cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get diff --git a/src/README.md b/src/README.md index 272c755..e8b8deb 100644 --- a/src/README.md +++ b/src/README.md @@ -83,9 +83,11 @@ Third-party libraries used - [ncurses][ncurses] - [mattn/go-runewidth](https://github.com/mattn/go-runewidth) - - Licensed under [MIT](http://mattn.mit-license.org/2013) + - Licensed under [MIT](http://mattn.mit-license.org) - [mattn/go-shellwords](https://github.com/mattn/go-shellwords) - - Licensed under [MIT](http://mattn.mit-license.org/2014) + - Licensed under [MIT](http://mattn.mit-license.org) +- [mattn/go-isatty](https://github.com/mattn/go-isatty) + - Licensed under [MIT](http://mattn.mit-license.org) License ------- diff --git a/src/ansi.go b/src/ansi.go index 02be18c..b7d3a9e 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -7,7 +7,7 @@ import ( "strings" "unicode/utf8" - "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/tui" ) type ansiOffset struct { @@ -16,9 +16,9 @@ type ansiOffset struct { } type ansiState struct { - fg int - bg int - attr curses.Attr + fg tui.Color + bg tui.Color + attr tui.Attr } func (s *ansiState) colored() bool { @@ -134,26 +134,26 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { case 49: state.bg = -1 case 1: - state.attr = curses.Bold + state.attr = tui.Bold case 2: - state.attr = curses.Dim + state.attr = tui.Dim case 4: - state.attr = curses.Underline + state.attr = tui.Underline case 5: - state.attr = curses.Blink + state.attr = tui.Blink case 7: - state.attr = curses.Reverse + state.attr = tui.Reverse case 0: init() default: if num >= 30 && num <= 37 { - state.fg = num - 30 + state.fg = tui.Color(num - 30) } else if num >= 40 && num <= 47 { - state.bg = num - 40 + state.bg = tui.Color(num - 40) } else if num >= 90 && num <= 97 { - state.fg = num - 90 + 8 + state.fg = tui.Color(num - 90 + 8) } else if num >= 100 && num <= 107 { - state.bg = num - 100 + 8 + state.bg = tui.Color(num - 100 + 8) } } case 1: @@ -164,7 +164,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { state256 = 0 } case 2: - *ptr = num + *ptr = tui.Color(num) state256 = 0 } } diff --git a/src/ansi_test.go b/src/ansi_test.go index 0ba9e40..3afcc6c 100644 --- a/src/ansi_test.go +++ b/src/ansi_test.go @@ -4,14 +4,14 @@ import ( "fmt" "testing" - "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/tui" ) func TestExtractColor(t *testing.T) { - assert := func(offset ansiOffset, b int32, e int32, fg int, bg int, bold bool) { - var attr curses.Attr + assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) { + var attr tui.Attr if bold { - attr = curses.Bold + attr = tui.Bold } if offset.offset[0] != b || offset.offset[1] != e || offset.color.fg != fg || offset.color.bg != bg || offset.color.attr != attr { diff --git a/src/constants.go b/src/constants.go index bec3bb6..778d219 100644 --- a/src/constants.go +++ b/src/constants.go @@ -15,7 +15,6 @@ const ( coordinatorDelayStep time.Duration = 10 * time.Millisecond // Reader - defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//` readerBufferSize = 64 * 1024 // Terminal diff --git a/src/constants_unix.go b/src/constants_unix.go new file mode 100644 index 0000000..52677e6 --- /dev/null +++ b/src/constants_unix.go @@ -0,0 +1,8 @@ +// +build !windows + +package fzf + +const ( + // Reader + defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//` +) diff --git a/src/constants_windows.go b/src/constants_windows.go new file mode 100644 index 0000000..efd3f11 --- /dev/null +++ b/src/constants_windows.go @@ -0,0 +1,8 @@ +// +build windows + +package fzf + +const ( + // Reader + defaultCommand = `dir /s/b` +) diff --git a/src/options.go b/src/options.go index c5e6d7b..a3ed22d 100644 --- a/src/options.go +++ b/src/options.go @@ -9,7 +9,7 @@ import ( "unicode/utf8" "github.com/junegunn/fzf/src/algo" - "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/tui" "github.com/junegunn/go-shellwords" ) @@ -142,7 +142,7 @@ type Options struct { Multi bool Ansi bool Mouse bool - Theme *curses.ColorTheme + Theme *tui.ColorTheme Black bool Reverse bool Cycle bool @@ -187,7 +187,7 @@ func defaultOptions() *Options { Multi: false, Ansi: false, Mouse: true, - Theme: curses.EmptyTheme(), + Theme: tui.EmptyTheme(), Black: false, Reverse: false, Cycle: false, @@ -358,60 +358,60 @@ func parseKeyChords(str string, message string) map[int]string { chord := 0 switch lkey { case "up": - chord = curses.Up + chord = tui.Up case "down": - chord = curses.Down + chord = tui.Down case "left": - chord = curses.Left + chord = tui.Left case "right": - chord = curses.Right + chord = tui.Right case "enter", "return": - chord = curses.CtrlM + chord = tui.CtrlM case "space": - chord = curses.AltZ + int(' ') + chord = tui.AltZ + int(' ') case "bspace", "bs": - chord = curses.BSpace + chord = tui.BSpace case "alt-enter", "alt-return": - chord = curses.AltEnter + chord = tui.AltEnter case "alt-space": - chord = curses.AltSpace + chord = tui.AltSpace case "alt-/": - chord = curses.AltSlash + chord = tui.AltSlash case "alt-bs", "alt-bspace": - chord = curses.AltBS + chord = tui.AltBS case "tab": - chord = curses.Tab + chord = tui.Tab case "btab", "shift-tab": - chord = curses.BTab + chord = tui.BTab case "esc": - chord = curses.ESC + chord = tui.ESC case "del": - chord = curses.Del + chord = tui.Del case "home": - chord = curses.Home + chord = tui.Home case "end": - chord = curses.End + chord = tui.End case "pgup", "page-up": - chord = curses.PgUp + chord = tui.PgUp case "pgdn", "page-down": - chord = curses.PgDn + chord = tui.PgDn case "shift-left": - chord = curses.SLeft + chord = tui.SLeft case "shift-right": - chord = curses.SRight + chord = tui.SRight case "double-click": - chord = curses.DoubleClick + chord = tui.DoubleClick case "f10": - chord = curses.F10 + chord = tui.F10 default: if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { - chord = curses.CtrlA + int(lkey[5]) - 'a' + chord = tui.CtrlA + int(lkey[5]) - 'a' } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { - chord = curses.AltA + int(lkey[4]) - 'a' + chord = tui.AltA + int(lkey[4]) - 'a' } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' { - chord = curses.F1 + int(key[1]) - '1' + chord = tui.F1 + int(key[1]) - '1' } else if utf8.RuneCountInString(key) == 1 { - chord = curses.AltZ + int([]rune(key)[0]) + chord = tui.AltZ + int([]rune(key)[0]) } else { errorExit("unsupported key: " + key) } @@ -458,7 +458,7 @@ func parseTiebreak(str string) []criterion { return criteria } -func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { +func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme { if theme != nil { dupe := *theme return &dupe @@ -466,16 +466,16 @@ func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { return nil } -func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme { +func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { theme := dupeTheme(defaultTheme) for _, str := range strings.Split(strings.ToLower(str), ",") { switch str { case "dark": - theme = dupeTheme(curses.Dark256) + theme = dupeTheme(tui.Dark256) case "light": - theme = dupeTheme(curses.Light256) + theme = dupeTheme(tui.Light256) case "16": - theme = dupeTheme(curses.Default16) + theme = dupeTheme(tui.Default16) case "bw", "no": theme = nil default: @@ -495,7 +495,7 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme if err != nil || ansi32 < -1 || ansi32 > 255 { fail() } - ansi := int16(ansi32) + ansi := tui.Color(ansi32) switch pair[0] { case "fg": theme.Fg = ansi @@ -572,9 +572,9 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) } var key int if len(pair[0]) == 1 && pair[0][0] == escapedColon { - key = ':' + curses.AltZ + key = ':' + tui.AltZ } else if len(pair[0]) == 1 && pair[0][0] == escapedComma { - key = ',' + curses.AltZ + key = ',' + tui.AltZ } else { keys := parseKeyChords(pair[0], "key name required") key = firstKey(keys) @@ -868,7 +868,7 @@ func parseOptions(opts *Options, allArgs []string) { case "--color": spec := optionalNextString(allArgs, &i) if len(spec) == 0 { - opts.Theme = curses.EmptyTheme() + opts.Theme = tui.EmptyTheme() } else { opts.Theme = parseTheme(opts.Theme, spec) } @@ -905,7 +905,7 @@ func parseOptions(opts *Options, allArgs []string) { case "+c", "--no-color": opts.Theme = nil case "+2", "--no-256": - opts.Theme = curses.Default16 + opts.Theme = tui.Default16 case "--black": opts.Black = true case "--no-black": @@ -1071,11 +1071,11 @@ func parseOptions(opts *Options, allArgs []string) { func postProcessOptions(opts *Options) { // Default actions for CTRL-N / CTRL-P when --history is set if opts.History != nil { - if _, prs := opts.Keymap[curses.CtrlP]; !prs { - opts.Keymap[curses.CtrlP] = actPreviousHistory + if _, prs := opts.Keymap[tui.CtrlP]; !prs { + opts.Keymap[tui.CtrlP] = actPreviousHistory } - if _, prs := opts.Keymap[curses.CtrlN]; !prs { - opts.Keymap[curses.CtrlN] = actNextHistory + if _, prs := opts.Keymap[tui.CtrlN]; !prs { + opts.Keymap[tui.CtrlN] = actNextHistory } } diff --git a/src/options_test.go b/src/options_test.go index 6013617..f16569e 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" ) @@ -133,48 +133,48 @@ func TestParseKeys(t *testing.T) { if len(pairs) != 11 { t.Error(11) } - check(curses.CtrlZ, "ctrl-z") - check(curses.AltZ, "alt-z") - check(curses.F2, "f2") - check(curses.AltZ+'@', "@") - check(curses.AltA, "Alt-a") - check(curses.AltZ+'!', "!") - check(curses.CtrlA+'g'-'a', "ctrl-G") - check(curses.AltZ+'J', "J") - check(curses.AltZ+'g', "g") - check(curses.AltEnter, "ALT-enter") - check(curses.AltSpace, "alt-SPACE") + check(tui.CtrlZ, "ctrl-z") + check(tui.AltZ, "alt-z") + check(tui.F2, "f2") + check(tui.AltZ+'@', "@") + check(tui.AltA, "Alt-a") + check(tui.AltZ+'!', "!") + check(tui.CtrlA+'g'-'a', "ctrl-G") + check(tui.AltZ+'J', "J") + check(tui.AltZ+'g', "g") + check(tui.AltEnter, "ALT-enter") + check(tui.AltSpace, "alt-SPACE") // Synonyms pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") if len(pairs) != 9 { t.Error(9) } - check(curses.CtrlM, "Return") - check(curses.AltZ+' ', "space") - check(curses.Tab, "tab") - check(curses.BTab, "btab") - check(curses.ESC, "esc") - check(curses.Up, "up") - check(curses.Down, "down") - check(curses.Left, "left") - check(curses.Right, "right") + check(tui.CtrlM, "Return") + check(tui.AltZ+' ', "space") + check(tui.Tab, "tab") + check(tui.BTab, "btab") + check(tui.ESC, "esc") + check(tui.Up, "up") + check(tui.Down, "down") + check(tui.Left, "left") + check(tui.Right, "right") pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "") if len(pairs) != 11 { t.Error(11) } - check(curses.Tab, "Ctrl-I") - check(curses.PgUp, "page-up") - check(curses.PgDn, "Page-Down") - check(curses.Home, "Home") - check(curses.End, "End") - check(curses.AltBS, "Alt-BSpace") - check(curses.SLeft, "shift-left") - check(curses.SRight, "shift-right") - check(curses.BTab, "shift-tab") - check(curses.CtrlM, "Enter") - check(curses.BSpace, "bspace") + check(tui.Tab, "Ctrl-I") + check(tui.PgUp, "page-up") + check(tui.PgDn, "Page-Down") + check(tui.Home, "Home") + check(tui.End, "End") + check(tui.AltBS, "Alt-BSpace") + check(tui.SLeft, "shift-left") + check(tui.SRight, "shift-right") + check(tui.BTab, "shift-tab") + check(tui.CtrlM, "Enter") + check(tui.BSpace, "bspace") } func TestParseKeysWithComma(t *testing.T) { @@ -191,36 +191,36 @@ func TestParseKeysWithComma(t *testing.T) { pairs := parseKeyChords(",", "") checkN(len(pairs), 1) - check(pairs, curses.AltZ+',', ",") + check(pairs, tui.AltZ+',', ",") pairs = parseKeyChords(",,a,b", "") checkN(len(pairs), 3) - check(pairs, curses.AltZ+'a', "a") - check(pairs, curses.AltZ+'b', "b") - check(pairs, curses.AltZ+',', ",") + check(pairs, tui.AltZ+'a', "a") + check(pairs, tui.AltZ+'b', "b") + check(pairs, tui.AltZ+',', ",") pairs = parseKeyChords("a,b,,", "") checkN(len(pairs), 3) - check(pairs, curses.AltZ+'a', "a") - check(pairs, curses.AltZ+'b', "b") - check(pairs, curses.AltZ+',', ",") + check(pairs, tui.AltZ+'a', "a") + check(pairs, tui.AltZ+'b', "b") + check(pairs, tui.AltZ+',', ",") pairs = parseKeyChords("a,,,b", "") checkN(len(pairs), 3) - check(pairs, curses.AltZ+'a', "a") - check(pairs, curses.AltZ+'b', "b") - check(pairs, curses.AltZ+',', ",") + check(pairs, tui.AltZ+'a', "a") + check(pairs, tui.AltZ+'b', "b") + check(pairs, tui.AltZ+',', ",") pairs = parseKeyChords("a,,,b,c", "") checkN(len(pairs), 4) - check(pairs, curses.AltZ+'a', "a") - check(pairs, curses.AltZ+'b', "b") - check(pairs, curses.AltZ+'c', "c") - check(pairs, curses.AltZ+',', ",") + check(pairs, tui.AltZ+'a', "a") + check(pairs, tui.AltZ+'b', "b") + check(pairs, tui.AltZ+'c', "c") + check(pairs, tui.AltZ+',', ",") pairs = parseKeyChords(",,,", "") checkN(len(pairs), 1) - check(pairs, curses.AltZ+',', ",") + check(pairs, tui.AltZ+',', ",") } func TestBind(t *testing.T) { @@ -236,41 +236,41 @@ func TestBind(t *testing.T) { } keymap := defaultKeymap() execmap := make(map[int]string) - check(actBeginningOfLine, keymap[curses.CtrlA]) + check(actBeginningOfLine, keymap[tui.CtrlA]) parseKeymap(keymap, execmap, "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 {};,"+ "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ ",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)") - check(actKillLine, keymap[curses.CtrlA]) - check(actToggleSort, keymap[curses.CtrlB]) - check(actPageUp, keymap[curses.AltZ+'c']) - check(actAbort, keymap[curses.AltZ+',']) - check(actAccept, keymap[curses.AltZ+':']) - check(actPageDown, keymap[curses.AltZ]) - check(actExecute, keymap[curses.F1]) - check(actExecute, keymap[curses.F2]) - check(actExecute, keymap[curses.F3]) - check(actExecute, keymap[curses.F4]) - checkString("ls {}", execmap[curses.F1]) - checkString("echo {}, {}, {}", execmap[curses.F2]) - checkString("echo '({})'", execmap[curses.F3]) - checkString("less {}", execmap[curses.F4]) - checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA]) - checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB]) - checkString("\nfoobar,Y:execute(baz)", execmap[curses.AltZ+'X']) + check(actKillLine, keymap[tui.CtrlA]) + check(actToggleSort, keymap[tui.CtrlB]) + check(actPageUp, keymap[tui.AltZ+'c']) + check(actAbort, keymap[tui.AltZ+',']) + check(actAccept, keymap[tui.AltZ+':']) + check(actPageDown, keymap[tui.AltZ]) + check(actExecute, keymap[tui.F1]) + check(actExecute, keymap[tui.F2]) + check(actExecute, keymap[tui.F3]) + check(actExecute, keymap[tui.F4]) + checkString("ls {}", execmap[tui.F1]) + checkString("echo {}, {}, {}", execmap[tui.F2]) + checkString("echo '({})'", execmap[tui.F3]) + checkString("less {}", execmap[tui.F4]) + checkString("echo (,),[,],/,:,;,%,{}", execmap[tui.AltA]) + checkString("echo (,),[,],/,:,@,%,{}", execmap[tui.AltB]) + checkString("\nfoobar,Y:execute(baz)", execmap[tui.AltZ+'X']) for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) - checkString("foobar", execmap[curses.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) + checkString("foobar", execmap[tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) } parseKeymap(keymap, execmap, "f1:abort") - check(actAbort, keymap[curses.F1]) + check(actAbort, keymap[tui.F1]) } func TestColorSpec(t *testing.T) { - theme := curses.Dark256 + theme := tui.Dark256 dark := parseTheme(theme, "dark") if *dark != *theme { t.Errorf("colors should be equivalent") @@ -283,7 +283,7 @@ func TestColorSpec(t *testing.T) { if *light == *theme { t.Errorf("should not be equivalent") } - if *light != *curses.Light256 { + if *light != *tui.Light256 { t.Errorf("colors should be equivalent") } if light == theme { @@ -294,23 +294,23 @@ func TestColorSpec(t *testing.T) { if customized.Fg != 231 || customized.Bg != 232 { t.Errorf("color not customized") } - if *curses.Dark256 == *customized { + if *tui.Dark256 == *customized { t.Errorf("colors should not be equivalent") } - customized.Fg = curses.Dark256.Fg - customized.Bg = curses.Dark256.Bg - if *curses.Dark256 != *customized { - t.Errorf("colors should now be equivalent: %v, %v", curses.Dark256, customized) + customized.Fg = tui.Dark256.Fg + customized.Bg = tui.Dark256.Bg + if *tui.Dark256 != *customized { + t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized) } customized = parseTheme(theme, "fg:231,dark,bg:232") - if customized.Fg != curses.Dark256.Fg || customized.Bg == curses.Dark256.Bg { + if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg { t.Errorf("color not customized") } } func TestParseNilTheme(t *testing.T) { - var theme *curses.ColorTheme + var theme *tui.ColorTheme newTheme := parseTheme(theme, "prompt:12") if newTheme != nil { t.Errorf("color is disabled. keep it that way.") @@ -330,21 +330,21 @@ func TestDefaultCtrlNP(t *testing.T) { t.Error() } } - check([]string{}, curses.CtrlN, actDown) - check([]string{}, curses.CtrlP, actUp) + check([]string{}, tui.CtrlN, actDown) + check([]string{}, tui.CtrlP, actUp) - check([]string{"--bind=ctrl-n:accept"}, curses.CtrlN, actAccept) - check([]string{"--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) + check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) + check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) hist := "--history=/tmp/fzf-history" - check([]string{hist}, curses.CtrlN, actNextHistory) - check([]string{hist}, curses.CtrlP, actPreviousHistory) + check([]string{hist}, tui.CtrlN, actNextHistory) + check([]string{hist}, tui.CtrlP, actPreviousHistory) - check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlN, actAccept) - check([]string{hist, "--bind=ctrl-n:accept"}, curses.CtrlP, actPreviousHistory) + check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) + check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPreviousHistory) - check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlN, actNextHistory) - check([]string{hist, "--bind=ctrl-p:accept"}, curses.CtrlP, actAccept) + check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory) + check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) } func optsFor(words ...string) *Options { diff --git a/src/result.go b/src/result.go index 69e83f8..c0cf5d6 100644 --- a/src/result.go +++ b/src/result.go @@ -5,7 +5,7 @@ import ( "sort" "unicode" - "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" ) @@ -14,8 +14,8 @@ type Offset [2]int32 type colorOffset struct { offset [2]int32 - color int - attr curses.Attr + color tui.ColorPair + attr tui.Attr index int32 } @@ -92,7 +92,7 @@ func minRank() rank { return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}} } -func (result *Result) colorOffsets(matchOffsets []Offset, theme *curses.ColorTheme, color int, attr curses.Attr, current bool) []colorOffset { +func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset { itemColors := result.item.Colors() // No ANSI code, or --color=no @@ -149,23 +149,23 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *curses.ColorThe fg := ansi.color.fg if fg == -1 { if current { - fg = int(theme.Current) + fg = theme.Current } else { - fg = int(theme.Fg) + fg = theme.Fg } } bg := ansi.color.bg if bg == -1 { if current { - bg = int(theme.DarkBg) + bg = theme.DarkBg } else { - bg = int(theme.Bg) + bg = theme.Bg } } colors = append(colors, colorOffset{ offset: [2]int32{int32(start), int32(idx)}, - color: curses.PairFor(fg, bg), - attr: ansi.color.attr | attr}) + color: tui.PairFor(fg, bg), + attr: ansi.color.attr.Merge(attr)}) } } } diff --git a/src/result_test.go b/src/result_test.go index 645684d..06402c1 100644 --- a/src/result_test.go +++ b/src/result_test.go @@ -1,3 +1,5 @@ +// +build !termbox + package fzf import ( @@ -5,7 +7,7 @@ import ( "sort" "testing" - "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" ) @@ -98,26 +100,26 @@ func TestColorOffset(t *testing.T) { item: &Item{ colors: &[]ansiOffset{ ansiOffset{[2]int32{0, 20}, ansiState{1, 5, 0}}, - ansiOffset{[2]int32{22, 27}, ansiState{2, 6, curses.Bold}}, + ansiOffset{[2]int32{22, 27}, ansiState{2, 6, tui.Bold}}, ansiOffset{[2]int32{30, 32}, ansiState{3, 7, 0}}, - ansiOffset{[2]int32{33, 40}, ansiState{4, 8, curses.Bold}}}}} + ansiOffset{[2]int32{33, 40}, ansiState{4, 8, tui.Bold}}}}} // [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}] - colors := item.colorOffsets(offsets, curses.Dark256, 99, 0, true) - assert := func(idx int, b int32, e int32, c int, bold bool) { - var attr curses.Attr + colors := item.colorOffsets(offsets, tui.Dark256, 99, 0, true) + assert := func(idx int, b int32, e int32, c tui.ColorPair, bold bool) { + var attr tui.Attr if bold { - attr = curses.Bold + attr = tui.Bold } o := colors[idx] if o.offset[0] != b || o.offset[1] != e || o.color != c || o.attr != attr { t.Error(o) } } - assert(0, 0, 5, curses.ColUser, false) + assert(0, 0, 5, tui.ColUser, false) assert(1, 5, 15, 99, false) - assert(2, 15, 20, curses.ColUser, false) - assert(3, 22, 25, curses.ColUser+1, true) + assert(2, 15, 20, tui.ColUser, false) + assert(3, 22, 25, tui.ColUser+1, true) assert(4, 25, 35, 99, false) - assert(5, 35, 40, curses.ColUser+2, true) + assert(5, 35, 40, tui.ColUser+2, true) } diff --git a/src/terminal.go b/src/terminal.go index 376c640..39c7823 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -12,7 +12,7 @@ import ( "syscall" "time" - C "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" "github.com/junegunn/go-runewidth" @@ -69,9 +69,9 @@ type Terminal struct { header0 []string ansi bool margin [4]sizeSpec - window *C.Window - bwindow *C.Window - pwindow *C.Window + window *tui.Window + bwindow *tui.Window + pwindow *tui.Window count int progress int reading bool @@ -90,7 +90,7 @@ type Terminal struct { suppress bool startChan chan bool slab *util.Slab - theme *C.ColorTheme + theme *tui.ColorTheme } type selectedItem struct { @@ -187,51 +187,51 @@ const ( func defaultKeymap() map[int]actionType { keymap := make(map[int]actionType) - keymap[C.Invalid] = actInvalid - keymap[C.CtrlA] = actBeginningOfLine - keymap[C.CtrlB] = actBackwardChar - keymap[C.CtrlC] = actAbort - keymap[C.CtrlG] = actAbort - keymap[C.CtrlQ] = actAbort - keymap[C.ESC] = actAbort - keymap[C.CtrlD] = actDeleteCharEOF - keymap[C.CtrlE] = actEndOfLine - keymap[C.CtrlF] = actForwardChar - keymap[C.CtrlH] = actBackwardDeleteChar - keymap[C.BSpace] = actBackwardDeleteChar - keymap[C.Tab] = actToggleDown - keymap[C.BTab] = actToggleUp - keymap[C.CtrlJ] = actDown - keymap[C.CtrlK] = actUp - keymap[C.CtrlL] = actClearScreen - keymap[C.CtrlM] = actAccept - keymap[C.CtrlN] = actDown - keymap[C.CtrlP] = actUp - keymap[C.CtrlU] = actUnixLineDiscard - keymap[C.CtrlW] = actUnixWordRubout - keymap[C.CtrlY] = actYank + keymap[tui.Invalid] = actInvalid + keymap[tui.CtrlA] = actBeginningOfLine + keymap[tui.CtrlB] = actBackwardChar + keymap[tui.CtrlC] = actAbort + keymap[tui.CtrlG] = actAbort + keymap[tui.CtrlQ] = actAbort + keymap[tui.ESC] = actAbort + keymap[tui.CtrlD] = actDeleteCharEOF + keymap[tui.CtrlE] = actEndOfLine + keymap[tui.CtrlF] = actForwardChar + keymap[tui.CtrlH] = actBackwardDeleteChar + keymap[tui.BSpace] = actBackwardDeleteChar + keymap[tui.Tab] = actToggleDown + keymap[tui.BTab] = actToggleUp + keymap[tui.CtrlJ] = actDown + keymap[tui.CtrlK] = actUp + keymap[tui.CtrlL] = actClearScreen + keymap[tui.CtrlM] = actAccept + keymap[tui.CtrlN] = actDown + keymap[tui.CtrlP] = actUp + keymap[tui.CtrlU] = actUnixLineDiscard + keymap[tui.CtrlW] = actUnixWordRubout + keymap[tui.CtrlY] = actYank - keymap[C.AltB] = actBackwardWord - keymap[C.SLeft] = actBackwardWord - keymap[C.AltF] = actForwardWord - keymap[C.SRight] = actForwardWord - keymap[C.AltD] = actKillWord - keymap[C.AltBS] = actBackwardKillWord + keymap[tui.AltB] = actBackwardWord + keymap[tui.SLeft] = actBackwardWord + keymap[tui.AltF] = actForwardWord + keymap[tui.SRight] = actForwardWord + keymap[tui.AltD] = actKillWord + keymap[tui.AltBS] = actBackwardKillWord - keymap[C.Up] = actUp - keymap[C.Down] = actDown - keymap[C.Left] = actBackwardChar - keymap[C.Right] = actForwardChar + keymap[tui.Up] = actUp + keymap[tui.Down] = actDown + keymap[tui.Left] = actBackwardChar + keymap[tui.Right] = actForwardChar - keymap[C.Home] = actBeginningOfLine - keymap[C.End] = actEndOfLine - keymap[C.Del] = actDeleteChar - keymap[C.PgUp] = actPageUp - keymap[C.PgDn] = actPageDown + keymap[tui.Home] = actBeginningOfLine + keymap[tui.End] = actEndOfLine + keymap[tui.Del] = actDeleteChar + keymap[tui.PgUp] = actPageUp + keymap[tui.PgDn] = actPageDown - keymap[C.Rune] = actRune - keymap[C.Mouse] = actMouse - keymap[C.DoubleClick] = actAccept + keymap[tui.Rune] = actRune + keymap[tui.Mouse] = actMouse + keymap[tui.DoubleClick] = actAccept return keymap } @@ -299,7 +299,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { theme: opts.Theme, startChan: make(chan bool, 1), initFunc: func() { - C.Init(opts.Theme, opts.Black, opts.Mouse) + tui.Init(opts.Theme, opts.Black, opts.Mouse) }} } @@ -429,8 +429,8 @@ func calculateSize(base int, size sizeSpec, margin int, minSize int) int { } func (t *Terminal) resizeWindows() { - screenWidth := C.MaxX() - screenHeight := C.MaxY() + screenWidth := tui.MaxX() + screenHeight := tui.MaxY() marginInt := [4]int{} for idx, sizeSpec := range t.margin { if sizeSpec.percent { @@ -479,33 +479,33 @@ func (t *Terminal) resizeWindows() { height := screenHeight - marginInt[0] - marginInt[2] if t.isPreviewEnabled() { createPreviewWindow := func(y int, x int, w int, h int) { - t.bwindow = C.NewWindow(y, x, w, h, true) - t.pwindow = C.NewWindow(y+1, x+2, w-4, h-2, false) + t.bwindow = tui.NewWindow(y, x, w, h, true) + t.pwindow = tui.NewWindow(y+1, x+2, w-4, h-2, false) } switch t.preview.position { case posUp: pheight := calculateSize(height, t.preview.size, minHeight, 3) - t.window = C.NewWindow( + t.window = tui.NewWindow( marginInt[0]+pheight, marginInt[3], width, height-pheight, false) createPreviewWindow(marginInt[0], marginInt[3], width, pheight) case posDown: pheight := calculateSize(height, t.preview.size, minHeight, 3) - t.window = C.NewWindow( + t.window = tui.NewWindow( marginInt[0], marginInt[3], width, height-pheight, false) createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) case posLeft: pwidth := calculateSize(width, t.preview.size, minWidth, 5) - t.window = C.NewWindow( + t.window = tui.NewWindow( marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) case posRight: pwidth := calculateSize(width, t.preview.size, minWidth, 5) - t.window = C.NewWindow( + t.window = tui.NewWindow( marginInt[0], marginInt[3], width-pwidth, height, false) createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) } } else { - t.window = C.NewWindow( + t.window = tui.NewWindow( marginInt[0], marginInt[3], width, @@ -531,24 +531,24 @@ func (t *Terminal) placeCursor() { func (t *Terminal) printPrompt() { t.move(0, 0, true) - t.window.CPrint(C.ColPrompt, C.Bold, t.prompt) - t.window.CPrint(C.ColNormal, C.Bold, string(t.input)) + t.window.CPrint(tui.ColPrompt, tui.Bold, t.prompt) + t.window.CPrint(tui.ColNormal, tui.Bold, string(t.input)) } func (t *Terminal) printInfo() { if t.inlineInfo { t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true) if t.reading { - t.window.CPrint(C.ColSpinner, C.Bold, " < ") + t.window.CPrint(tui.ColSpinner, tui.Bold, " < ") } else { - t.window.CPrint(C.ColPrompt, C.Bold, " < ") + t.window.CPrint(tui.ColPrompt, tui.Bold, " < ") } } else { t.move(1, 0, true) if t.reading { duration := int64(spinnerDuration) idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration - t.window.CPrint(C.ColSpinner, C.Bold, _spinner[idx]) + t.window.CPrint(tui.ColSpinner, tui.Bold, _spinner[idx]) } t.move(1, 2, false) } @@ -567,7 +567,7 @@ func (t *Terminal) printInfo() { if t.progress > 0 && t.progress < 100 { output += fmt.Sprintf(" (%d%%)", t.progress) } - t.window.CPrint(C.ColInfo, 0, output) + t.window.CPrint(tui.ColInfo, 0, output) } func (t *Terminal) printHeader() { @@ -591,7 +591,8 @@ func (t *Terminal) printHeader() { colors: colors} t.move(line, 2, true) - t.printHighlighted(&Result{item: item}, 0, C.ColHeader, 0, false) + t.printHighlighted(&Result{item: item}, + tui.AttrRegular, tui.ColHeader, tui.ColDefault, false) } } @@ -625,21 +626,21 @@ func (t *Terminal) printItem(result *Result, i int, current bool) { } else if current { label = ">" } - t.window.CPrint(C.ColCursor, C.Bold, label) + t.window.CPrint(tui.ColCursor, tui.Bold, label) if current { if selected { - t.window.CPrint(C.ColSelected, C.Bold, ">") + t.window.CPrint(tui.ColSelected, tui.Bold, ">") } else { - t.window.CPrint(C.ColCurrent, C.Bold, " ") + t.window.CPrint(tui.ColCurrent, tui.Bold, " ") } - t.printHighlighted(result, C.Bold, C.ColCurrent, C.ColCurrentMatch, true) + t.printHighlighted(result, tui.Bold, tui.ColCurrent, tui.ColCurrentMatch, true) } else { if selected { - t.window.CPrint(C.ColSelected, C.Bold, ">") + t.window.CPrint(tui.ColSelected, tui.Bold, ">") } else { t.window.Print(" ") } - t.printHighlighted(result, 0, C.ColNormal, C.ColMatch, false) + t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false) } } @@ -695,7 +696,7 @@ func overflow(runes []rune, max int) bool { return false } -func (t *Terminal) printHighlighted(result *Result, attr C.Attr, col1 int, col2 int, current bool) { +func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool) { item := result.item // Overflow @@ -827,7 +828,7 @@ func (t *Terminal) printPreview() { if t.previewer.offset > 0 { offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines) t.pwindow.Move(0, t.pwindow.Width-len(offset)) - t.pwindow.CPrint(C.ColInfo, C.Reverse, offset) + t.pwindow.CPrint(tui.ColInfo, tui.Reverse, offset) } } @@ -858,11 +859,10 @@ func (t *Terminal) printAll() { func (t *Terminal) refresh() { if !t.suppress { if t.isPreviewEnabled() { - t.bwindow.Refresh() - t.pwindow.Refresh() + tui.RefreshWindows([]*tui.Window{t.bwindow, t.pwindow, t.window}) + } else { + tui.RefreshWindows([]*tui.Window{t.window}) } - t.window.Refresh() - C.DoUpdate() } } @@ -912,10 +912,10 @@ func (t *Terminal) rubout(pattern string) { t.input = append(t.input[:t.cx], after...) } -func keyMatch(key int, event C.Event) bool { +func keyMatch(key int, event tui.Event) bool { return event.Type == key || - event.Type == C.Rune && int(event.Char) == key-C.AltZ || - event.Type == C.Mouse && key == C.DoubleClick && event.MouseEvent.Double + event.Type == tui.Rune && int(event.Char) == key-tui.AltZ || + event.Type == tui.Mouse && key == tui.DoubleClick && event.MouseEvent.Double } func quoteEntry(entry string) string { @@ -980,7 +980,7 @@ func (t *Terminal) executeCommand(template string, items []*Item) { cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - C.Endwin() + tui.Pause() cmd.Run() t.refresh() } @@ -1014,7 +1014,7 @@ func (t *Terminal) Loop() { }() resizeChan := make(chan os.Signal, 1) - signal.Notify(resizeChan, syscall.SIGWINCH) + notifyOnResize(resizeChan) // Non-portable go func() { for { <-resizeChan @@ -1126,12 +1126,11 @@ func (t *Terminal) Loop() { case reqRefresh: t.suppress = false case reqRedraw: - C.Clear() - C.Endwin() - C.Refresh() + tui.Clear() + tui.Refresh() t.printAll() case reqClose: - C.Close() + tui.Close() if t.output() { exit(exitOk) } @@ -1144,11 +1143,11 @@ func (t *Terminal) Loop() { case reqPreviewRefresh: t.printPreview() case reqPrintQuery: - C.Close() + tui.Close() t.printer(string(t.input)) exit(exitOk) case reqQuit: - C.Close() + tui.Close() exit(exitInterrupt) } } @@ -1161,7 +1160,7 @@ func (t *Terminal) Loop() { looping := true for looping { - event := C.GetChar() + event := tui.GetChar() t.mutex.Lock() previousInput := t.input @@ -1445,7 +1444,7 @@ func (t *Terminal) Loop() { // Double-click if my >= min { if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { - return doAction(t.keymap[C.DoubleClick], C.DoubleClick) + return doAction(t.keymap[tui.DoubleClick], tui.DoubleClick) } } } else if me.Down { @@ -1468,8 +1467,8 @@ func (t *Terminal) Loop() { mapkey := event.Type if t.jumping == jumpDisabled { action := t.keymap[mapkey] - if mapkey == C.Rune { - mapkey = int(event.Char) + int(C.AltZ) + if mapkey == tui.Rune { + mapkey = int(event.Char) + int(tui.AltZ) if act, prs := t.keymap[mapkey]; prs { action = act } @@ -1484,7 +1483,7 @@ func (t *Terminal) Loop() { } changed = string(previousInput) != string(t.input) } else { - if mapkey == C.Rune { + if mapkey == tui.Rune { if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() { t.cy = idx + t.offset if t.jumping == jumpAcceptEnabled { diff --git a/src/terminal_unix.go b/src/terminal_unix.go new file mode 100644 index 0000000..6284c22 --- /dev/null +++ b/src/terminal_unix.go @@ -0,0 +1,13 @@ +// +build !windows + +package fzf + +import ( + "os" + "os/signal" + "syscall" +) + +func notifyOnResize(resizeChan chan<- os.Signal) { + signal.Notify(resizeChan, syscall.SIGWINCH) +} diff --git a/src/terminal_windows.go b/src/terminal_windows.go new file mode 100644 index 0000000..5512bba --- /dev/null +++ b/src/terminal_windows.go @@ -0,0 +1,11 @@ +// +build windows + +package fzf + +import ( + "os" +) + +func notifyOnResize(resizeChan chan<- os.Signal) { + // TODO +} diff --git a/src/curses/curses.go b/src/tui/ncurses.go similarity index 61% rename from src/curses/curses.go rename to src/tui/ncurses.go index 638a862..7a44340 100644 --- a/src/curses/curses.go +++ b/src/tui/ncurses.go @@ -1,4 +1,7 @@ -package curses +// +build !windows +// +build !termbox + +package tui /* #include @@ -10,7 +13,6 @@ package curses SCREEN *c_newterm () { return newterm(NULL, stderr, stdin); } - */ import "C" @@ -23,6 +25,10 @@ import ( "unicode/utf8" ) +type ColorPair int16 +type Attr C.int +type WindowImpl C.WINDOW + const ( Bold = C.A_BOLD Dim = C.A_DIM @@ -31,89 +37,13 @@ const ( Underline = C.A_UNDERLINE ) -type Attr C.int - -// Types of user action const ( - Rune = iota - - CtrlA - CtrlB - CtrlC - CtrlD - CtrlE - CtrlF - CtrlG - CtrlH - Tab - CtrlJ - CtrlK - CtrlL - CtrlM - CtrlN - CtrlO - CtrlP - CtrlQ - CtrlR - CtrlS - CtrlT - CtrlU - CtrlV - CtrlW - CtrlX - CtrlY - CtrlZ - ESC - - Invalid - Mouse - DoubleClick - - BTab - BSpace - - Del - PgUp - PgDn - - Up - Down - Left - Right - Home - End - - SLeft - SRight - - F1 - F2 - F3 - F4 - F5 - F6 - F7 - F8 - F9 - F10 - - AltEnter - AltSpace - AltSlash - AltBS - AltA - AltB - AltC - AltD - AltE - AltF - - AltZ = AltA + 'z' - 'a' + AttrRegular Attr = 0 ) // Pallete const ( - _ = iota + ColDefault ColorPair = iota ColNormal ColPrompt ColMatch @@ -128,193 +58,26 @@ const ( ColUser // Should be the last entry ) -const ( - doubleClickDuration = 500 * time.Millisecond - colDefault = -1 - colUndefined = -2 -) - -type ColorTheme struct { - Fg int16 - Bg int16 - DarkBg int16 - Prompt int16 - Match int16 - Current int16 - CurrentMatch int16 - Spinner int16 - Info int16 - Cursor int16 - Selected int16 - Header int16 - Border int16 -} - -type Event struct { - Type int - Char rune - MouseEvent *MouseEvent -} - -type MouseEvent struct { - Y int - X int - S int - Down bool - Double bool - Mod bool -} - var ( - _buf []byte - _in *os.File - _color bool - _colorFn func(int, Attr) C.int - _colorMap map[int]int - _prevDownTime time.Time - _clickY []int - _screen *C.SCREEN - Default16 *ColorTheme - Dark256 *ColorTheme - Light256 *ColorTheme + _in *os.File + _screen *C.SCREEN + _colorMap map[int]ColorPair + _colorFn func(ColorPair, Attr) C.int ) -type Window struct { - win *C.WINDOW - Top int - Left int - Width int - Height int -} - -func NewWindow(top int, left int, width int, height int, border bool) *Window { - win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left)) - if _color { - C.wbkgd(win, C.chtype(C.COLOR_PAIR(ColNormal))) - } - if border { - attr := _colorFn(ColBorder, 0) - C.wattron(win, attr) - C.box(win, 0, 0) - C.wattroff(win, attr) - } - - return &Window{ - win: win, - Top: top, - Left: left, - Width: width, - Height: height, - } -} - -func EmptyTheme() *ColorTheme { - return &ColorTheme{ - Fg: colUndefined, - Bg: colUndefined, - DarkBg: colUndefined, - Prompt: colUndefined, - Match: colUndefined, - Current: colUndefined, - CurrentMatch: colUndefined, - Spinner: colUndefined, - Info: colUndefined, - Cursor: colUndefined, - Selected: colUndefined, - Header: colUndefined, - Border: colUndefined} -} - func init() { - _prevDownTime = time.Unix(0, 0) - _clickY = []int{} - _colorMap = make(map[int]int) - Default16 = &ColorTheme{ - Fg: colDefault, - Bg: colDefault, - DarkBg: C.COLOR_BLACK, - Prompt: C.COLOR_BLUE, - Match: C.COLOR_GREEN, - Current: C.COLOR_YELLOW, - CurrentMatch: C.COLOR_GREEN, - Spinner: C.COLOR_GREEN, - Info: C.COLOR_WHITE, - Cursor: C.COLOR_RED, - Selected: C.COLOR_MAGENTA, - Header: C.COLOR_CYAN, - Border: C.COLOR_BLACK} - Dark256 = &ColorTheme{ - Fg: colDefault, - Bg: colDefault, - DarkBg: 236, - Prompt: 110, - Match: 108, - Current: 254, - CurrentMatch: 151, - Spinner: 148, - Info: 144, - Cursor: 161, - Selected: 168, - Header: 109, - Border: 59} - Light256 = &ColorTheme{ - Fg: colDefault, - Bg: colDefault, - DarkBg: 251, - Prompt: 25, - Match: 66, - Current: 237, - CurrentMatch: 23, - Spinner: 65, - Info: 101, - Cursor: 161, - Selected: 168, - Header: 31, - Border: 145} + _colorMap = make(map[int]ColorPair) } -func attrColored(pair int, a Attr) C.int { - var attr C.int - if pair > 0 { - attr = C.COLOR_PAIR(C.int(pair)) +func (a Attr) Merge(b Attr) Attr { + return a | b +} + +func DefaultTheme() *ColorTheme { + if C.tigetnum(C.CString("colors")) >= 256 { + return Dark256 } - return attr | C.int(a) -} - -func attrMono(pair int, a Attr) C.int { - var attr C.int - switch pair { - case ColCurrent: - if a&C.A_BOLD == C.A_BOLD { - attr = C.A_REVERSE - } - case ColMatch: - attr = C.A_UNDERLINE - case ColCurrentMatch: - attr = C.A_UNDERLINE | C.A_REVERSE - } - if a&C.A_BOLD == C.A_BOLD { - attr = attr | C.A_BOLD - } - return attr -} - -func MaxX() int { - return int(C.COLS) -} - -func MaxY() int { - return int(C.LINES) -} - -func getch(nonblock bool) int { - b := make([]byte, 1) - syscall.SetNonblock(int(_in.Fd()), nonblock) - _, err := _in.Read(b) - if err != nil { - return -1 - } - return int(b[0]) + return Default16 } func Init(theme *ColorTheme, black bool, mouse bool) { @@ -344,52 +107,19 @@ func Init(theme *ColorTheme, black bool, mouse bool) { _color = theme != nil if _color { C.start_color() - var baseTheme *ColorTheme - if C.tigetnum(C.CString("colors")) >= 256 { - baseTheme = Dark256 - } else { - baseTheme = Default16 - } - initPairs(baseTheme, theme, black) - C.bkgd(C.chtype(C.COLOR_PAIR(ColNormal))) + InitTheme(theme, black) + initPairs(theme) + C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal)))) _colorFn = attrColored } else { _colorFn = attrMono } } -func override(baseTheme *ColorTheme, theme *ColorTheme) { - o := func(a int16, b int16) int16 { - if b == colUndefined { - return a - } - return b - } - theme.Fg = o(baseTheme.Fg, theme.Fg) - theme.Bg = o(baseTheme.Bg, theme.Bg) - theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg) - theme.Prompt = o(baseTheme.Prompt, theme.Prompt) - theme.Match = o(baseTheme.Match, theme.Match) - theme.Current = o(baseTheme.Current, theme.Current) - theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch) - theme.Spinner = o(baseTheme.Spinner, theme.Spinner) - theme.Info = o(baseTheme.Info, theme.Info) - theme.Cursor = o(baseTheme.Cursor, theme.Cursor) - theme.Selected = o(baseTheme.Selected, theme.Selected) - theme.Header = o(baseTheme.Header, theme.Header) - theme.Border = o(baseTheme.Border, theme.Border) -} - -func initPairs(baseTheme *ColorTheme, theme *ColorTheme, black bool) { - if black { - theme.Bg = C.COLOR_BLACK - } - // Updates theme - override(baseTheme, theme) - +func initPairs(theme *ColorTheme) { C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg)) - initPair := func(group C.short, fg int16, bg int16) { - C.init_pair(group, C.short(fg), C.short(bg)) + initPair := func(group ColorPair, fg Color, bg Color) { + C.init_pair(C.short(group), C.short(fg), C.short(bg)) } initPair(ColNormal, theme.Fg, theme.Bg) initPair(ColPrompt, theme.Prompt, theme.Bg) @@ -404,11 +134,161 @@ func initPairs(baseTheme *ColorTheme, theme *ColorTheme, black bool) { initPair(ColBorder, theme.Border, theme.Bg) } +func Pause() { + C.endwin() +} + func Close() { C.endwin() C.delscreen(_screen) } +func NewWindow(top int, left int, width int, height int, border bool) *Window { + win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left)) + if _color { + C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal)))) + } + if border { + attr := _colorFn(ColBorder, 0) + C.wattron(win, attr) + C.box(win, 0, 0) + C.wattroff(win, attr) + } + + return &Window{ + impl: (*WindowImpl)(win), + Top: top, + Left: left, + Width: width, + Height: height, + } +} + +func attrColored(pair ColorPair, a Attr) C.int { + var attr C.int + if pair > 0 { + attr = C.COLOR_PAIR(C.int(pair)) + } + return attr | C.int(a) +} + +func attrMono(pair ColorPair, a Attr) C.int { + var attr C.int + switch pair { + case ColCurrent: + if C.int(a)&C.A_BOLD == C.A_BOLD { + attr = C.A_REVERSE + } + case ColMatch: + attr = C.A_UNDERLINE + case ColCurrentMatch: + attr = C.A_UNDERLINE | C.A_REVERSE + } + if C.int(a)&C.A_BOLD == C.A_BOLD { + attr = attr | C.A_BOLD + } + return attr +} + +func MaxX() int { + return int(C.COLS) +} + +func MaxY() int { + return int(C.LINES) +} + +func (w *Window) win() *C.WINDOW { + return (*C.WINDOW)(w.impl) +} + +func (w *Window) Close() { + C.delwin(w.win()) +} + +func (w *Window) Enclose(y int, x int) bool { + return bool(C.wenclose(w.win(), C.int(y), C.int(x))) +} + +func (w *Window) Move(y int, x int) { + C.wmove(w.win(), C.int(y), C.int(x)) +} + +func (w *Window) MoveAndClear(y int, x int) { + w.Move(y, x) + C.wclrtoeol(w.win()) +} + +func (w *Window) Print(text string) { + C.waddstr(w.win(), C.CString(strings.Map(func(r rune) rune { + if r < 32 { + return -1 + } + return r + }, text))) +} + +func (w *Window) CPrint(pair ColorPair, a Attr, text string) { + attr := _colorFn(pair, a) + C.wattron(w.win(), attr) + w.Print(text) + C.wattroff(w.win(), attr) +} + +func Clear() { + C.clear() + C.endwin() +} + +func Refresh() { + C.refresh() +} + +func (w *Window) Erase() { + C.werase(w.win()) +} + +func (w *Window) Fill(str string) bool { + return C.waddstr(w.win(), C.CString(str)) == C.OK +} + +func (w *Window) CFill(str string, fg Color, bg Color, a Attr) bool { + attr := _colorFn(PairFor(fg, bg), a) + C.wattron(w.win(), attr) + ret := w.Fill(str) + C.wattroff(w.win(), attr) + return ret +} + +func RefreshWindows(windows []*Window) { + for _, w := range windows { + C.wnoutrefresh(w.win()) + } + C.doupdate() +} + +func PairFor(fg Color, bg Color) ColorPair { + key := (int(fg) << 8) + int(bg) + if found, prs := _colorMap[key]; prs { + return found + } + + id := ColorPair(len(_colorMap) + int(ColUser)) + C.init_pair(C.short(id), C.short(fg), C.short(bg)) + _colorMap[key] = id + return id +} + +func getch(nonblock bool) int { + b := make([]byte, 1) + syscall.SetNonblock(int(_in.Fd()), nonblock) + _, err := _in.Read(b) + if err != nil { + return -1 + } + return int(b[0]) +} + func GetBytes() []byte { c := getch(false) _buf = append(_buf, byte(c)) @@ -631,84 +511,3 @@ func GetChar() Event { sz = rsz return Event{Rune, r, nil} } - -func (w *Window) Close() { - C.delwin(w.win) -} - -func (w *Window) Enclose(y int, x int) bool { - return bool(C.wenclose(w.win, C.int(y), C.int(x))) -} - -func (w *Window) Move(y int, x int) { - C.wmove(w.win, C.int(y), C.int(x)) -} - -func (w *Window) MoveAndClear(y int, x int) { - w.Move(y, x) - C.wclrtoeol(w.win) -} - -func (w *Window) Print(text string) { - C.waddstr(w.win, C.CString(strings.Map(func(r rune) rune { - if r < 32 { - return -1 - } - return r - }, text))) -} - -func (w *Window) CPrint(pair int, a Attr, text string) { - attr := _colorFn(pair, a) - C.wattron(w.win, attr) - w.Print(text) - C.wattroff(w.win, attr) -} - -func Clear() { - C.clear() -} - -func Endwin() { - C.endwin() -} - -func Refresh() { - C.refresh() -} - -func (w *Window) Erase() { - C.werase(w.win) -} - -func (w *Window) Fill(str string) bool { - return C.waddstr(w.win, C.CString(str)) == C.OK -} - -func (w *Window) CFill(str string, fg int, bg int, a Attr) bool { - attr := _colorFn(PairFor(fg, bg), a) - C.wattron(w.win, attr) - ret := w.Fill(str) - C.wattroff(w.win, attr) - return ret -} - -func (w *Window) Refresh() { - C.wnoutrefresh(w.win) -} - -func DoUpdate() { - C.doupdate() -} - -func PairFor(fg int, bg int) int { - key := (fg << 8) + bg - if found, prs := _colorMap[key]; prs { - return found - } - - id := len(_colorMap) + ColUser - C.init_pair(C.short(id), C.short(fg), C.short(bg)) - _colorMap[key] = id - return id -} diff --git a/src/tui/termbox.go b/src/tui/termbox.go new file mode 100644 index 0000000..c49512c --- /dev/null +++ b/src/tui/termbox.go @@ -0,0 +1,151 @@ +// +build termbox windows + +package tui + +import ( + "github.com/nsf/termbox-go" +) + +type ColorPair [2]Color +type Attr uint16 +type WindowImpl int // FIXME + +const ( + // TODO + _ = iota + Bold + Dim + Blink + Reverse + Underline +) + +const ( + AttrRegular Attr = 0 +) + +var ( + ColDefault = ColorPair{colDefault, colDefault} + ColNormal ColorPair + ColPrompt ColorPair + ColMatch ColorPair + ColCurrent ColorPair + ColCurrentMatch ColorPair + ColSpinner ColorPair + ColInfo ColorPair + ColCursor ColorPair + ColSelected ColorPair + ColHeader ColorPair + ColBorder ColorPair + ColUser ColorPair +) + +func DefaultTheme() *ColorTheme { + if termbox.SetOutputMode(termbox.OutputCurrent) == termbox.Output256 { + return Dark256 + } + return Default16 +} + +func PairFor(fg Color, bg Color) ColorPair { + return [2]Color{fg, bg} +} + +func (a Attr) Merge(b Attr) Attr { + return a | b +} + +func Init(theme *ColorTheme, black bool, mouse bool) { + ColNormal = ColorPair{theme.Fg, theme.Bg} + ColPrompt = ColorPair{theme.Prompt, theme.Bg} + ColMatch = ColorPair{theme.Match, theme.Bg} + ColCurrent = ColorPair{theme.Current, theme.DarkBg} + ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg} + ColSpinner = ColorPair{theme.Spinner, theme.Bg} + ColInfo = ColorPair{theme.Info, theme.Bg} + ColCursor = ColorPair{theme.Cursor, theme.DarkBg} + ColSelected = ColorPair{theme.Selected, theme.DarkBg} + ColHeader = ColorPair{theme.Header, theme.Bg} + ColBorder = ColorPair{theme.Border, theme.Bg} + + // TODO +} + +func MaxX() int { + // TODO + return 80 +} + +func MaxY() int { + // TODO + return 24 +} + +func Clear() { + // TODO +} + +func Refresh() { + // TODO +} + +func GetChar() Event { + // TODO + return Event{} +} + +func Pause() { + // TODO +} + +func Close() { + // TODO +} + +func RefreshWindows(windows []*Window) { + // TODO +} + +func NewWindow(top int, left int, width int, height int, border bool) *Window { + // TODO + return &Window{} +} + +func (w *Window) Close() { + // TODO +} + +func (w *Window) Erase() { + // TODO +} + +func (w *Window) Enclose(y int, x int) bool { + // TODO + return false +} + +func (w *Window) Move(y int, x int) { + // TODO +} + +func (w *Window) MoveAndClear(y int, x int) { + // TODO +} + +func (w *Window) Print(text string) { + // TODO +} + +func (w *Window) CPrint(pair ColorPair, a Attr, text string) { + // TODO +} + +func (w *Window) Fill(str string) bool { + // TODO + return false +} + +func (w *Window) CFill(str string, fg Color, bg Color, a Attr) bool { + // TODO + return false +} diff --git a/src/tui/tui.go b/src/tui/tui.go new file mode 100644 index 0000000..a33baa4 --- /dev/null +++ b/src/tui/tui.go @@ -0,0 +1,250 @@ +package tui + +import ( + "time" +) + +// Types of user action +const ( + Rune = iota + + CtrlA + CtrlB + CtrlC + CtrlD + CtrlE + CtrlF + CtrlG + CtrlH + Tab + CtrlJ + CtrlK + CtrlL + CtrlM + CtrlN + CtrlO + CtrlP + CtrlQ + CtrlR + CtrlS + CtrlT + CtrlU + CtrlV + CtrlW + CtrlX + CtrlY + CtrlZ + ESC + + Invalid + Mouse + DoubleClick + + BTab + BSpace + + Del + PgUp + PgDn + + Up + Down + Left + Right + Home + End + + SLeft + SRight + + F1 + F2 + F3 + F4 + F5 + F6 + F7 + F8 + F9 + F10 + + AltEnter + AltSpace + AltSlash + AltBS + AltA + AltB + AltC + AltD + AltE + AltF + + AltZ = AltA + 'z' - 'a' +) + +const ( + doubleClickDuration = 500 * time.Millisecond +) + +type Color int16 + +const ( + colUndefined Color = -2 + colDefault = -1 +) + +const ( + colBlack Color = iota + colRed + colGreen + colYellow + colBlue + colMagenta + colCyan + colWhite +) + +type ColorTheme struct { + Fg Color + Bg Color + DarkBg Color + Prompt Color + Match Color + Current Color + CurrentMatch Color + Spinner Color + Info Color + Cursor Color + Selected Color + Header Color + Border Color +} + +type Event struct { + Type int + Char rune + MouseEvent *MouseEvent +} + +type MouseEvent struct { + Y int + X int + S int + Down bool + Double bool + Mod bool +} + +var ( + _buf []byte + _color bool + _prevDownTime time.Time + _clickY []int + Default16 *ColorTheme + Dark256 *ColorTheme + Light256 *ColorTheme +) + +type Window struct { + impl *WindowImpl + Top int + Left int + Width int + Height int +} + +func EmptyTheme() *ColorTheme { + return &ColorTheme{ + Fg: colUndefined, + Bg: colUndefined, + DarkBg: colUndefined, + Prompt: colUndefined, + Match: colUndefined, + Current: colUndefined, + CurrentMatch: colUndefined, + Spinner: colUndefined, + Info: colUndefined, + Cursor: colUndefined, + Selected: colUndefined, + Header: colUndefined, + Border: colUndefined} +} + +func init() { + _prevDownTime = time.Unix(0, 0) + _clickY = []int{} + Default16 = &ColorTheme{ + Fg: colDefault, + Bg: colDefault, + DarkBg: colBlack, + Prompt: colBlue, + Match: colGreen, + Current: colYellow, + CurrentMatch: colGreen, + Spinner: colGreen, + Info: colWhite, + Cursor: colRed, + Selected: colMagenta, + Header: colCyan, + Border: colBlack} + Dark256 = &ColorTheme{ + Fg: colDefault, + Bg: colDefault, + DarkBg: 236, + Prompt: 110, + Match: 108, + Current: 254, + CurrentMatch: 151, + Spinner: 148, + Info: 144, + Cursor: 161, + Selected: 168, + Header: 109, + Border: 59} + Light256 = &ColorTheme{ + Fg: colDefault, + Bg: colDefault, + DarkBg: 251, + Prompt: 25, + Match: 66, + Current: 237, + CurrentMatch: 23, + Spinner: 65, + Info: 101, + Cursor: 161, + Selected: 168, + Header: 31, + Border: 145} +} + +func InitTheme(theme *ColorTheme, black bool) { + _color = theme != nil + if !_color { + return + } + + baseTheme := DefaultTheme() + if black { + theme.Bg = colBlack + } + + o := func(a Color, b Color) Color { + if b == colUndefined { + return a + } + return b + } + theme.Fg = o(baseTheme.Fg, theme.Fg) + theme.Bg = o(baseTheme.Bg, theme.Bg) + theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg) + theme.Prompt = o(baseTheme.Prompt, theme.Prompt) + theme.Match = o(baseTheme.Match, theme.Match) + theme.Current = o(baseTheme.Current, theme.Current) + theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch) + theme.Spinner = o(baseTheme.Spinner, theme.Spinner) + theme.Info = o(baseTheme.Info, theme.Info) + theme.Cursor = o(baseTheme.Cursor, theme.Cursor) + theme.Selected = o(baseTheme.Selected, theme.Selected) + theme.Header = o(baseTheme.Header, theme.Header) + theme.Border = o(baseTheme.Border, theme.Border) +} diff --git a/src/curses/curses_test.go b/src/tui/tui_test.go similarity index 91% rename from src/curses/curses_test.go rename to src/tui/tui_test.go index db75c40..4a2fee9 100644 --- a/src/curses/curses_test.go +++ b/src/tui/tui_test.go @@ -1,4 +1,4 @@ -package curses +package tui import ( "testing" diff --git a/src/util/util.go b/src/util/util.go index e273882..2a1607c 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -1,13 +1,11 @@ package util -// #include -import "C" - import ( "math" "os" - "os/exec" "time" + + "github.com/junegunn/go-isatty" ) // Max returns the largest integer @@ -95,14 +93,5 @@ func DurWithin( // IsTty returns true is stdin is a terminal func IsTty() bool { - return int(C.isatty(C.int(os.Stdin.Fd()))) != 0 -} - -// ExecCommand executes the given command with $SHELL -func ExecCommand(command string) *exec.Cmd { - shell := os.Getenv("SHELL") - if len(shell) == 0 { - shell = "sh" - } - return exec.Command(shell, "-c", command) + return isatty.IsTerminal(os.Stdin.Fd()) } diff --git a/src/util/util_unix.go b/src/util/util_unix.go new file mode 100644 index 0000000..dcc5cb5 --- /dev/null +++ b/src/util/util_unix.go @@ -0,0 +1,17 @@ +// +build !windows + +package util + +import ( + "os" + "os/exec" +) + +// ExecCommand executes the given command with $SHELL +func ExecCommand(command string) *exec.Cmd { + shell := os.Getenv("SHELL") + if len(shell) == 0 { + shell = "sh" + } + return exec.Command(shell, "-c", command) +} diff --git a/src/util/util_windows.go b/src/util/util_windows.go new file mode 100644 index 0000000..a660f39 --- /dev/null +++ b/src/util/util_windows.go @@ -0,0 +1,17 @@ +// +build windows + +package util + +import ( + "os" + "os/exec" +) + +// ExecCommand executes the given command with $SHELL +func ExecCommand(command string) *exec.Cmd { + shell := os.Getenv("SHELL") + if len(shell) == 0 { + shell = "cmd" + } + return exec.Command(shell, "/c", command) +} From 26895da96918f9b1956a04981c8af5f3e42fcbd8 Mon Sep 17 00:00:00 2001 From: Michael Kelley Date: Sun, 23 Oct 2016 20:45:45 -0700 Subject: [PATCH 302/658] Implement tcell-based renderer --- src/history_test.go | 28 ++- src/options_test.go | 5 +- src/reader.go | 9 +- src/result_test.go | 2 +- src/tui/ncurses.go | 2 +- src/tui/tcell.go | 523 ++++++++++++++++++++++++++++++++++++++++++++ src/tui/termbox.go | 151 ------------- 7 files changed, 558 insertions(+), 162 deletions(-) create mode 100644 src/tui/tcell.go delete mode 100644 src/tui/termbox.go diff --git a/src/history_test.go b/src/history_test.go index fa6f106..0a01413 100644 --- a/src/history_test.go +++ b/src/history_test.go @@ -1,7 +1,10 @@ package fzf import ( + "io/ioutil" + "os" "os/user" + "runtime" "testing" ) @@ -10,23 +13,34 @@ func TestHistory(t *testing.T) { // Invalid arguments user, _ := user.Current() - paths := []string{"/etc", "/proc"} - if user.Name != "root" { - paths = append(paths, "/etc/sudoers") + var paths []string + if runtime.GOOS == "windows" { + // GOPATH should exist, so we shouldn't be able to override it + paths = []string{os.Getenv("GOPATH")} + } else { + paths = []string{"/etc", "/proc"} + if user.Name != "root" { + paths = append(paths, "/etc/sudoers") + } } + for _, path := range paths { if _, e := NewHistory(path, maxHistory); e == nil { t.Error("Error expected for: " + path) } } + + f, _ := ioutil.TempFile("", "fzf-history") + f.Close() + { // Append lines - h, _ := NewHistory("/tmp/fzf-history", maxHistory) + h, _ := NewHistory(f.Name(), maxHistory) for i := 0; i < maxHistory+10; i++ { h.append("foobar") } } { // Read lines - h, _ := NewHistory("/tmp/fzf-history", maxHistory) + h, _ := NewHistory(f.Name(), maxHistory) if len(h.lines) != maxHistory+1 { t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) } @@ -37,13 +51,13 @@ func TestHistory(t *testing.T) { } } { // Append lines - h, _ := NewHistory("/tmp/fzf-history", maxHistory) + h, _ := NewHistory(f.Name(), maxHistory) h.append("barfoo") h.append("") h.append("foobarbaz") } { // Read lines again - h, _ := NewHistory("/tmp/fzf-history", maxHistory) + h, _ := NewHistory(f.Name(), maxHistory) if len(h.lines) != maxHistory+1 { t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) } diff --git a/src/options_test.go b/src/options_test.go index f16569e..092efe4 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -2,6 +2,7 @@ package fzf import ( "fmt" + "io/ioutil" "testing" "github.com/junegunn/fzf/src/tui" @@ -336,7 +337,9 @@ func TestDefaultCtrlNP(t *testing.T) { check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept) check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept) - hist := "--history=/tmp/fzf-history" + f, _ := ioutil.TempFile("", "fzf-history") + f.Close() + hist := "--history=" + f.Name() check([]string{hist}, tui.CtrlN, actNextHistory) check([]string{hist}, tui.CtrlP, actPreviousHistory) diff --git a/src/reader.go b/src/reader.go index 0542666..15a0f44 100644 --- a/src/reader.go +++ b/src/reader.go @@ -4,6 +4,7 @@ import ( "bufio" "io" "os" + "runtime" "github.com/junegunn/fzf/src/util" ) @@ -39,9 +40,15 @@ func (r *Reader) feed(src io.Reader) { // ReadBytes returns err != nil if and only if the returned data does not // end in delim. bytea, err := reader.ReadBytes(delim) + byteaLen := len(bytea) if len(bytea) > 0 { if err == nil { - bytea = bytea[:len(bytea)-1] + // get rid of carriage return if under Windows: + if runtime.GOOS == "windows" && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') { + bytea = bytea[:byteaLen-2] + } else { + bytea = bytea[:byteaLen-1] + } } if r.pusher(bytea) { r.eventBox.Set(EvtReadNew, nil) diff --git a/src/result_test.go b/src/result_test.go index 06402c1..15b1bdb 100644 --- a/src/result_test.go +++ b/src/result_test.go @@ -1,4 +1,4 @@ -// +build !termbox +// +build !tcell package fzf diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 7a44340..3f52f3f 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -1,5 +1,5 @@ // +build !windows -// +build !termbox +// +build !tcell package tui diff --git a/src/tui/tcell.go b/src/tui/tcell.go new file mode 100644 index 0000000..591b366 --- /dev/null +++ b/src/tui/tcell.go @@ -0,0 +1,523 @@ +// +build tcell windows + +package tui + +import ( + "time" + "unicode/utf8" + + "fmt" + "os" + + "runtime" + + "github.com/gdamore/tcell" + "github.com/gdamore/tcell/encoding" +) + +type ColorPair [2]Color + +func (p ColorPair) fg() Color { + return p[0] +} + +func (p ColorPair) bg() Color { + return p[1] +} + +func (p ColorPair) style() tcell.Style { + style := tcell.StyleDefault + return style.Foreground(tcell.Color(p.fg())).Background(tcell.Color(p.bg())) +} + +type Attr tcell.Style + +type WindowTcell struct { + LastX int + LastY int + MoveCursor bool + Border bool +} +type WindowImpl WindowTcell + +const ( + Bold = Attr(tcell.AttrBold) + Dim = Attr(tcell.AttrDim) + Blink = Attr(tcell.AttrBlink) + Reverse = Attr(tcell.AttrReverse) + Underline = Attr(tcell.AttrUnderline) +) + +const ( + AttrRegular Attr = 0 +) + +var ( + ColDefault = ColorPair{colDefault, colDefault} + ColNormal ColorPair + ColPrompt ColorPair + ColMatch ColorPair + ColCurrent ColorPair + ColCurrentMatch ColorPair + ColSpinner ColorPair + ColInfo ColorPair + ColCursor ColorPair + ColSelected ColorPair + ColHeader ColorPair + ColBorder ColorPair + ColUser ColorPair +) + +func DefaultTheme() *ColorTheme { + if _screen.Colors() >= 256 { + return Dark256 + } + return Default16 +} + +func PairFor(fg Color, bg Color) ColorPair { + return [2]Color{fg, bg} +} + +var ( + _colorToAttribute = []tcell.Color{ + tcell.ColorBlack, + tcell.ColorRed, + tcell.ColorGreen, + tcell.ColorYellow, + tcell.ColorBlue, + tcell.ColorDarkMagenta, + tcell.ColorLightCyan, + tcell.ColorWhite, + } +) + +func (c Color) Style() tcell.Color { + if c <= colDefault { + return tcell.ColorDefault + } else if c >= colBlack && c <= colWhite { + return _colorToAttribute[int(c)] + } else { + return tcell.Color(c) + } +} + +func (a Attr) Merge(b Attr) Attr { + return a | b +} + +var ( + _screen tcell.Screen +) + +func Init(theme *ColorTheme, black bool, mouse bool) { + encoding.Register() + + s, e := tcell.NewScreen() + if e != nil { + fmt.Fprintf(os.Stderr, "%v\n", e) + os.Exit(1) + } + if e = s.Init(); e != nil { + fmt.Fprintf(os.Stderr, "%v\n", e) + os.Exit(1) + } + _screen = s + + _color = theme != nil + if _color { + InitTheme(theme, black) + } + ColNormal = ColorPair{theme.Fg, theme.Bg} + ColPrompt = ColorPair{theme.Prompt, theme.Bg} + ColMatch = ColorPair{theme.Match, theme.Bg} + ColCurrent = ColorPair{theme.Current, theme.DarkBg} + ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg} + ColSpinner = ColorPair{theme.Spinner, theme.Bg} + ColInfo = ColorPair{theme.Info, theme.Bg} + ColCursor = ColorPair{theme.Cursor, theme.DarkBg} + ColSelected = ColorPair{theme.Selected, theme.DarkBg} + ColHeader = ColorPair{theme.Header, theme.Bg} + ColBorder = ColorPair{theme.Border, theme.Bg} + + if mouse { + _screen.EnableMouse() + } else { + _screen.DisableMouse() + } +} + +func MaxX() int { + ncols, _ := _screen.Size() + return int(ncols) +} + +func MaxY() int { + _, nlines := _screen.Size() + return int(nlines) +} + +func (w *Window) win() *WindowTcell { + return (*WindowTcell)(w.impl) +} + +func Clear() { + _screen.Clear() +} + +func Refresh() { + // noop +} + +func GetChar() Event { + ev := _screen.PollEvent() + switch ev := ev.(type) { + case *tcell.EventResize: + return Event{Invalid, 0, nil} + + // process mouse events: + case *tcell.EventMouse: + x, y := ev.Position() + button := ev.Buttons() + mod := ev.Modifiers() != 0 + if button&tcell.WheelDown != 0 { + return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}} + } else if button&tcell.WheelUp != 0 { + return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, mod}} + } else if runtime.GOOS != "windows" { + // double and single taps on Windows don't quite work due to + // the console acting on the events and not allowing us + // to consume them. + + down := button&tcell.Button1 != 0 // left + double := false + if down { + now := time.Now() + if now.Sub(_prevDownTime) < doubleClickDuration { + _clickY = append(_clickY, x) + } else { + _clickY = []int{x} + } + _prevDownTime = now + } else { + if len(_clickY) > 1 && _clickY[0] == _clickY[1] && + time.Now().Sub(_prevDownTime) < doubleClickDuration { + double = true + } + } + + return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}} + } + + // process keyboard: + case *tcell.EventKey: + switch ev.Key() { + case tcell.KeyCtrlA: + return Event{CtrlA, 0, nil} + case tcell.KeyCtrlB: + return Event{CtrlB, 0, nil} + case tcell.KeyCtrlC: + return Event{CtrlC, 0, nil} + case tcell.KeyCtrlD: + return Event{CtrlD, 0, nil} + case tcell.KeyCtrlE: + return Event{CtrlE, 0, nil} + case tcell.KeyCtrlF: + return Event{CtrlF, 0, nil} + case tcell.KeyCtrlG: + return Event{CtrlG, 0, nil} + case tcell.KeyCtrlJ: + return Event{CtrlJ, 0, nil} + case tcell.KeyCtrlK: + return Event{CtrlK, 0, nil} + case tcell.KeyCtrlL: + return Event{CtrlL, 0, nil} + case tcell.KeyCtrlM: + return Event{CtrlM, 0, nil} + case tcell.KeyCtrlN: + return Event{CtrlN, 0, nil} + case tcell.KeyCtrlO: + return Event{CtrlO, 0, nil} + case tcell.KeyCtrlP: + return Event{CtrlP, 0, nil} + case tcell.KeyCtrlQ: + return Event{CtrlQ, 0, nil} + case tcell.KeyCtrlR: + return Event{CtrlR, 0, nil} + case tcell.KeyCtrlS: + return Event{CtrlS, 0, nil} + case tcell.KeyCtrlT: + return Event{CtrlT, 0, nil} + case tcell.KeyCtrlU: + return Event{CtrlU, 0, nil} + case tcell.KeyCtrlV: + return Event{CtrlV, 0, nil} + case tcell.KeyCtrlW: + return Event{CtrlW, 0, nil} + case tcell.KeyCtrlX: + return Event{CtrlX, 0, nil} + case tcell.KeyCtrlY: + return Event{CtrlY, 0, nil} + case tcell.KeyCtrlZ: + return Event{CtrlZ, 0, nil} + case tcell.KeyBackspace, tcell.KeyBackspace2: + return Event{BSpace, 0, nil} + + case tcell.KeyUp: + return Event{Up, 0, nil} + case tcell.KeyDown: + return Event{Down, 0, nil} + case tcell.KeyLeft: + return Event{Left, 0, nil} + case tcell.KeyRight: + return Event{Right, 0, nil} + + case tcell.KeyHome: + return Event{Home, 0, nil} + case tcell.KeyDelete: + return Event{Del, 0, nil} + case tcell.KeyEnd: + return Event{End, 0, nil} + /*case tcell.KeyPgUp: + return Event{PgUp, 0, nil} + case tcell.KeyPgdn: + return Event{PgDn, 0, nil}*/ + + case tcell.KeyTab: + return Event{Tab, 0, nil} + + case tcell.KeyF1: + return Event{F1, 0, nil} + case tcell.KeyF2: + return Event{F2, 0, nil} + case tcell.KeyF3: + return Event{F3, 0, nil} + case tcell.KeyF4: + return Event{F4, 0, nil} + case tcell.KeyF5: + return Event{F5, 0, nil} + case tcell.KeyF6: + return Event{F6, 0, nil} + case tcell.KeyF7: + return Event{F7, 0, nil} + case tcell.KeyF8: + return Event{F8, 0, nil} + case tcell.KeyF9: + return Event{F9, 0, nil} + case tcell.KeyF10: + return Event{F10, 0, nil} + case tcell.KeyF11: + return Event{Invalid, 0, nil} + case tcell.KeyF12: + return Event{Invalid, 0, nil} + + // ev.Ch doesn't work for some reason for space: + case tcell.KeyRune: + return Event{Rune, ev.Rune(), nil} + + case tcell.KeyEsc: + return Event{ESC, 0, nil} + + } + } + + return Event{Invalid, 0, nil} +} + +func Pause() { + // TODO +} + +func Close() { + _screen.Fini() +} + +func RefreshWindows(windows []*Window) { + // TODO + for _, w := range windows { + if w.win().MoveCursor { + _screen.ShowCursor(w.Left+w.win().LastX, w.Top+w.win().LastY) + w.win().MoveCursor = false + } + w.win().LastX = 0 + w.win().LastY = 0 + if w.win().Border { + w.DrawBorder() + } + } + _screen.Show() +} + +func NewWindow(top int, left int, width int, height int, border bool) *Window { + // TODO + win := new(WindowTcell) + win.Border = border + return &Window{ + impl: (*WindowImpl)(win), + Top: top, + Left: left, + Width: width, + Height: height, + } +} + +func (w *Window) Close() { + // TODO +} + +func fill(x, y, w, h int, r rune) { + for ly := 0; ly <= h; ly++ { + for lx := 0; lx <= w; lx++ { + _screen.SetContent(x+lx, y+ly, r, nil, ColDefault.style()) + } + } +} + +func (w *Window) Erase() { + // TODO + fill(w.Left, w.Top, w.Width, w.Height, ' ') +} + +func (w *Window) Enclose(y int, x int) bool { + return x >= w.Left && x <= (w.Left+w.Width) && + y >= w.Top && y <= (w.Top+w.Height) +} + +func (w *Window) Move(y int, x int) { + w.win().LastX = x + w.win().LastY = y + w.win().MoveCursor = true +} + +func (w *Window) MoveAndClear(y int, x int) { + w.Move(y, x) + r, _ := utf8.DecodeRuneInString(" ") + for i := w.win().LastX; i < w.Width; i++ { + _screen.SetContent(i+w.Left, w.win().LastY+w.Top, r, nil, ColDefault.style()) + } + w.win().LastX = 0 +} + +func (w *Window) Print(text string) { + w.PrintString(text, ColDefault, 0) +} + +func (w *Window) PrintString(text string, pair ColorPair, a Attr) { + t := text + lx := 0 + + // TODO respect attr + style := pair.style(). + Blink(a&Attr(tcell.AttrBlink) != 0). + Bold(a&Attr(tcell.AttrBold) != 0). + Dim(a&Attr(tcell.AttrDim) != 0). + Reverse(a&Attr(tcell.AttrReverse) != 0). + Underline(a&Attr(tcell.AttrUnderline) != 0) + + for { + if len(t) == 0 { + break + } + r, size := utf8.DecodeRuneInString(t) + t = t[size:] + + if r < rune(' ') { // ignore control characters + continue + } + + if r == '\n' { + w.win().LastY++ + lx = 0 + } else { + + if r == '\u000D' { // skip carriage return + continue + } + + var xPos = w.Left + w.win().LastX + lx + var yPos = w.Top + w.win().LastY + if xPos < (w.Left+w.Width) && yPos < (w.Top+w.Height) { + _screen.SetContent(xPos, yPos, r, nil, style) + } + lx++ + } + } + w.win().LastX += lx +} + +func (w *Window) CPrint(pair ColorPair, a Attr, text string) { + w.PrintString(text, pair, a) +} + +func (w *Window) FillString(text string, pair ColorPair, a Attr) bool { + lx := 0 + + //TODO: respect attr + style := pair.style(). + Blink(a&Attr(tcell.AttrBlink) != 0). + Bold(a&Attr(tcell.AttrBold) != 0). + Dim(a&Attr(tcell.AttrDim) != 0). + Reverse(a&Attr(tcell.AttrReverse) != 0). + Underline(a&Attr(tcell.AttrUnderline) != 0) + + for _, r := range text { + if r == '\n' { + w.win().LastY++ + w.win().LastX = 0 + lx = 0 + } else { + var xPos = w.Left + w.win().LastX + lx + + // word wrap: + if xPos > (w.Left + w.Width) { + w.win().LastY++ + w.win().LastX = 0 + lx = 0 + xPos = w.Left + } + var yPos = w.Top + w.win().LastY + + if yPos >= (w.Top + w.Height) { + return false + } + + _screen.SetContent(xPos, yPos, r, nil, style) + lx++ + } + } + w.win().LastX += lx + + return true +} + +func (w *Window) Fill(str string) bool { + return w.FillString(str, ColDefault, 0) +} + +func (w *Window) CFill(str string, fg Color, bg Color, a Attr) bool { + return w.FillString(str, ColorPair{fg, bg}, a) +} + +func (w *Window) DrawBorder() { + left := w.Left + right := left + w.Width + top := w.Top + bot := top + w.Height + + style := ColBorder.style() + + for x := left; x < right; x++ { + _screen.SetContent(x, top, tcell.RuneHLine, nil, style) + _screen.SetContent(x, bot-1, tcell.RuneHLine, nil, style) + } + + for y := top; y < bot; y++ { + _screen.SetContent(left, y, tcell.RuneVLine, nil, style) + _screen.SetContent(right-1, y, tcell.RuneVLine, nil, style) + } + + _screen.SetContent(left, top, tcell.RuneULCorner, nil, style) + _screen.SetContent(right-1, top, tcell.RuneURCorner, nil, style) + _screen.SetContent(left, bot-1, tcell.RuneLLCorner, nil, style) + _screen.SetContent(right-1, bot-1, tcell.RuneLRCorner, nil, style) +} diff --git a/src/tui/termbox.go b/src/tui/termbox.go deleted file mode 100644 index c49512c..0000000 --- a/src/tui/termbox.go +++ /dev/null @@ -1,151 +0,0 @@ -// +build termbox windows - -package tui - -import ( - "github.com/nsf/termbox-go" -) - -type ColorPair [2]Color -type Attr uint16 -type WindowImpl int // FIXME - -const ( - // TODO - _ = iota - Bold - Dim - Blink - Reverse - Underline -) - -const ( - AttrRegular Attr = 0 -) - -var ( - ColDefault = ColorPair{colDefault, colDefault} - ColNormal ColorPair - ColPrompt ColorPair - ColMatch ColorPair - ColCurrent ColorPair - ColCurrentMatch ColorPair - ColSpinner ColorPair - ColInfo ColorPair - ColCursor ColorPair - ColSelected ColorPair - ColHeader ColorPair - ColBorder ColorPair - ColUser ColorPair -) - -func DefaultTheme() *ColorTheme { - if termbox.SetOutputMode(termbox.OutputCurrent) == termbox.Output256 { - return Dark256 - } - return Default16 -} - -func PairFor(fg Color, bg Color) ColorPair { - return [2]Color{fg, bg} -} - -func (a Attr) Merge(b Attr) Attr { - return a | b -} - -func Init(theme *ColorTheme, black bool, mouse bool) { - ColNormal = ColorPair{theme.Fg, theme.Bg} - ColPrompt = ColorPair{theme.Prompt, theme.Bg} - ColMatch = ColorPair{theme.Match, theme.Bg} - ColCurrent = ColorPair{theme.Current, theme.DarkBg} - ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg} - ColSpinner = ColorPair{theme.Spinner, theme.Bg} - ColInfo = ColorPair{theme.Info, theme.Bg} - ColCursor = ColorPair{theme.Cursor, theme.DarkBg} - ColSelected = ColorPair{theme.Selected, theme.DarkBg} - ColHeader = ColorPair{theme.Header, theme.Bg} - ColBorder = ColorPair{theme.Border, theme.Bg} - - // TODO -} - -func MaxX() int { - // TODO - return 80 -} - -func MaxY() int { - // TODO - return 24 -} - -func Clear() { - // TODO -} - -func Refresh() { - // TODO -} - -func GetChar() Event { - // TODO - return Event{} -} - -func Pause() { - // TODO -} - -func Close() { - // TODO -} - -func RefreshWindows(windows []*Window) { - // TODO -} - -func NewWindow(top int, left int, width int, height int, border bool) *Window { - // TODO - return &Window{} -} - -func (w *Window) Close() { - // TODO -} - -func (w *Window) Erase() { - // TODO -} - -func (w *Window) Enclose(y int, x int) bool { - // TODO - return false -} - -func (w *Window) Move(y int, x int) { - // TODO -} - -func (w *Window) MoveAndClear(y int, x int) { - // TODO -} - -func (w *Window) Print(text string) { - // TODO -} - -func (w *Window) CPrint(pair ColorPair, a Attr, text string) { - // TODO -} - -func (w *Window) Fill(str string) bool { - // TODO - return false -} - -func (w *Window) CFill(str string, fg Color, bg Color, a Attr) bool { - // TODO - return false -} From 898d8d94c858774b02668c1490068cf086a1f9f0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 7 Nov 2016 02:15:34 +0900 Subject: [PATCH 303/658] Fix issues in tcell renderer and Windows build - Fix display of CJK wide characters - Fix horizontal offset of header lines - Add support for keys with ALT modifier, shift-tab, page-up and down - Fix util.ExecCommand to properly parse command-line arguments - Fix redraw on resize - Implement Pause/Resume for execute action - Remove runtime check of GOOS - Change exit status to 2 when tcell failed to start - TBD: Travis CI build for tcell renderer - Pending. tcell cannot reliably ingest keys from tmux send-keys --- .travis.yml | 8 +++- src/Makefile | 2 +- src/reader.go | 3 +- src/terminal.go | 7 ++++ src/tui/ncurses.go | 4 ++ src/tui/tcell.go | 85 ++++++++++++++++++++++++++++------------ src/util/util_unix.go | 5 +++ src/util/util_windows.go | 13 +++++- test/test_go.rb | 2 +- 9 files changed, 96 insertions(+), 33 deletions(-) diff --git a/.travis.yml b/.travis.yml index 597c1f6..a1a6497 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,10 @@ language: ruby -rvm: -- 2.2.0 +matrix: + include: + - env: TAGS= + rvm: 2.2.0 +# - env: TAGS=tcell +# rvm: 2.2.0 install: - sudo apt-get update diff --git a/src/Makefile b/src/Makefile index edc77fa..15f4c73 100644 --- a/src/Makefile +++ b/src/Makefile @@ -52,7 +52,7 @@ android-build: $(SRCDIR) rm -f $(RELEASEARM7) test: deps - SHELL=/bin/sh go test -v ./... + SHELL=/bin/sh go test -v -tags "$(TAGS)" ./... install: $(BINDIR)/fzf diff --git a/src/reader.go b/src/reader.go index 15a0f44..7e8e2e0 100644 --- a/src/reader.go +++ b/src/reader.go @@ -4,7 +4,6 @@ import ( "bufio" "io" "os" - "runtime" "github.com/junegunn/fzf/src/util" ) @@ -44,7 +43,7 @@ func (r *Reader) feed(src io.Reader) { if len(bytea) > 0 { if err == nil { // get rid of carriage return if under Windows: - if runtime.GOOS == "windows" && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') { + if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') { bytea = bytea[:byteaLen-2] } else { bytea = bytea[:byteaLen-1] diff --git a/src/terminal.go b/src/terminal.go index 39c7823..6d1a1ab 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -7,6 +7,7 @@ import ( "os/signal" "regexp" "sort" + "strconv" "strings" "sync" "syscall" @@ -919,6 +920,9 @@ func keyMatch(key int, event tui.Event) bool { } func quoteEntry(entry string) string { + if util.IsWindows() { + return strconv.Quote(strings.Replace(entry, "\"", "\\\"", -1)) + } return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" } @@ -982,6 +986,9 @@ func (t *Terminal) executeCommand(template string, items []*Item) { cmd.Stderr = os.Stderr tui.Pause() cmd.Run() + if tui.Resume() { + t.printAll() + } t.refresh() } diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 3f52f3f..d55c1c8 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -138,6 +138,10 @@ func Pause() { C.endwin() } +func Resume() bool { + return false +} + func Close() { C.endwin() C.delscreen(_screen) diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 591b366..0bb06e9 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -13,6 +13,8 @@ import ( "github.com/gdamore/tcell" "github.com/gdamore/tcell/encoding" + + "github.com/junegunn/go-runewidth" ) type ColorPair [2]Color @@ -108,21 +110,32 @@ func (a Attr) Merge(b Attr) Attr { var ( _screen tcell.Screen + _mouse bool ) +func initScreen() { + s, e := tcell.NewScreen() + if e != nil { + fmt.Fprintf(os.Stderr, "%v\n", e) + os.Exit(2) + } + if e = s.Init(); e != nil { + fmt.Fprintf(os.Stderr, "%v\n", e) + os.Exit(2) + } + if _mouse { + s.EnableMouse() + } else { + s.DisableMouse() + } + _screen = s +} + func Init(theme *ColorTheme, black bool, mouse bool) { encoding.Register() - s, e := tcell.NewScreen() - if e != nil { - fmt.Fprintf(os.Stderr, "%v\n", e) - os.Exit(1) - } - if e = s.Init(); e != nil { - fmt.Fprintf(os.Stderr, "%v\n", e) - os.Exit(1) - } - _screen = s + _mouse = mouse + initScreen() _color = theme != nil if _color { @@ -139,12 +152,6 @@ func Init(theme *ColorTheme, black bool, mouse bool) { ColSelected = ColorPair{theme.Selected, theme.DarkBg} ColHeader = ColorPair{theme.Header, theme.Bg} ColBorder = ColorPair{theme.Border, theme.Bg} - - if mouse { - _screen.EnableMouse() - } else { - _screen.DisableMouse() - } } func MaxX() int { @@ -162,6 +169,7 @@ func (w *Window) win() *WindowTcell { } func Clear() { + _screen.Sync() _screen.Clear() } @@ -211,6 +219,7 @@ func GetChar() Event { // process keyboard: case *tcell.EventKey: + alt := (ev.Modifiers() & tcell.ModAlt) > 0 switch ev.Key() { case tcell.KeyCtrlA: return Event{CtrlA, 0, nil} @@ -233,6 +242,9 @@ func GetChar() Event { case tcell.KeyCtrlL: return Event{CtrlL, 0, nil} case tcell.KeyCtrlM: + if alt { + return Event{AltEnter, 0, nil} + } return Event{CtrlM, 0, nil} case tcell.KeyCtrlN: return Event{CtrlN, 0, nil} @@ -261,6 +273,9 @@ func GetChar() Event { case tcell.KeyCtrlZ: return Event{CtrlZ, 0, nil} case tcell.KeyBackspace, tcell.KeyBackspace2: + if alt { + return Event{AltBS, 0, nil} + } return Event{BSpace, 0, nil} case tcell.KeyUp: @@ -278,13 +293,15 @@ func GetChar() Event { return Event{Del, 0, nil} case tcell.KeyEnd: return Event{End, 0, nil} - /*case tcell.KeyPgUp: + case tcell.KeyPgUp: return Event{PgUp, 0, nil} - case tcell.KeyPgdn: - return Event{PgDn, 0, nil}*/ + case tcell.KeyPgDn: + return Event{PgDn, 0, nil} case tcell.KeyTab: return Event{Tab, 0, nil} + case tcell.KeyBacktab: + return Event{BTab, 0, nil} case tcell.KeyF1: return Event{F1, 0, nil} @@ -313,7 +330,19 @@ func GetChar() Event { // ev.Ch doesn't work for some reason for space: case tcell.KeyRune: - return Event{Rune, ev.Rune(), nil} + r := ev.Rune() + if alt { + switch r { + case ' ': + return Event{AltSpace, 0, nil} + case '/': + return Event{AltSlash, 0, nil} + } + if r >= 'a' && r <= 'z' { + return Event{AltA + int(r) - 'a', 0, nil} + } + } + return Event{Rune, r, nil} case tcell.KeyEsc: return Event{ESC, 0, nil} @@ -325,7 +354,12 @@ func GetChar() Event { } func Pause() { - // TODO + _screen.Fini() +} + +func Resume() bool { + initScreen() + return true } func Close() { @@ -391,11 +425,10 @@ func (w *Window) Move(y int, x int) { func (w *Window) MoveAndClear(y int, x int) { w.Move(y, x) - r, _ := utf8.DecodeRuneInString(" ") for i := w.win().LastX; i < w.Width; i++ { - _screen.SetContent(i+w.Left, w.win().LastY+w.Top, r, nil, ColDefault.style()) + _screen.SetContent(i+w.Left, w.win().LastY+w.Top, rune(' '), nil, ColDefault.style()) } - w.win().LastX = 0 + w.win().LastX = x } func (w *Window) Print(text string) { @@ -439,7 +472,7 @@ func (w *Window) PrintString(text string, pair ColorPair, a Attr) { if xPos < (w.Left+w.Width) && yPos < (w.Top+w.Height) { _screen.SetContent(xPos, yPos, r, nil, style) } - lx++ + lx += runewidth.RuneWidth(r) } } w.win().LastX += lx @@ -482,7 +515,7 @@ func (w *Window) FillString(text string, pair ColorPair, a Attr) bool { } _screen.SetContent(xPos, yPos, r, nil, style) - lx++ + lx += runewidth.RuneWidth(r) } } w.win().LastX += lx diff --git a/src/util/util_unix.go b/src/util/util_unix.go index dcc5cb5..29e0d30 100644 --- a/src/util/util_unix.go +++ b/src/util/util_unix.go @@ -15,3 +15,8 @@ func ExecCommand(command string) *exec.Cmd { } return exec.Command(shell, "-c", command) } + +// IsWindows returns true on Windows +func IsWindows() bool { + return false +} diff --git a/src/util/util_windows.go b/src/util/util_windows.go index a660f39..3aa8660 100644 --- a/src/util/util_windows.go +++ b/src/util/util_windows.go @@ -5,6 +5,8 @@ package util import ( "os" "os/exec" + + "github.com/junegunn/go-shellwords" ) // ExecCommand executes the given command with $SHELL @@ -13,5 +15,14 @@ func ExecCommand(command string) *exec.Cmd { if len(shell) == 0 { shell = "cmd" } - return exec.Command(shell, "/c", command) + args, _ := shellwords.Parse(command) + allArgs := make([]string, len(args)+1) + allArgs[0] = "/c" + copy(allArgs[1:], args) + return exec.Command(shell, allArgs...) +} + +// IsWindows returns true on Windows +func IsWindows() bool { + return true } diff --git a/test/test_go.rb b/test/test_go.rb index f34d8b3..3fdfddd 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1058,7 +1058,7 @@ class TestGoFZF < TestBase def test_invalid_term lines = `TERM=xxx #{FZF}` assert_equal 2, $?.exitstatus - assert lines.include?('Invalid $TERM: xxx') + assert lines.include?('Invalid $TERM: xxx') || lines.include?('terminal entry not found') end def test_invalid_option From 4accc6902212fde0bf4fa53e1dbe4de96978a11a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 8 Nov 2016 02:19:05 +0900 Subject: [PATCH 304/658] Fix flaky test cases --- test/test_go.rb | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 3fdfddd..4cd4a4f 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1253,24 +1253,29 @@ module TestShell tmux.send_keys 'cat ', 'C-t', pane: 0 tmux.until(1) { |lines| lines.item_count >= 1 } tmux.send_keys 'fzf-unicode', pane: 1 - tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } + redraw = ->() { tmux.send_keys 'C-l', pane: 1 } + tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) } tmux.send_keys '1', pane: 1 - tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } + tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) } tmux.send_keys :BTab, pane: 1 - tmux.until(1) { |lines| lines[-2].include? '(1)' } + tmux.until(1) { |lines| redraw.(); lines[-2].include? '(1)' } tmux.send_keys :BSpace, pane: 1 - tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } + tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) } tmux.send_keys '2', pane: 1 - tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } + tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) } tmux.send_keys :BTab, pane: 1 - tmux.until(1) { |lines| lines[-2].include? '(2)' } + tmux.until(1) { |lines| redraw.(); lines[-2].include? '(2)' } tmux.send_keys :Enter, pane: 1 - tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') } - tmux.until { |lines| lines[-1].include?('fzf-unicode') || lines[-2].include?('fzf-unicode') } + tmux.until do |lines| + tmux.send_keys 'C-l' + [-1, -2].map { |offset| lines[offset] }.any? do |line| + line.start_with?('cat') && line.include?('fzf-unicode') + end + end tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'test1test2' } end @@ -1481,23 +1486,27 @@ module CompletionTest tmux.paste 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"' tmux.prepare tmux.send_keys 'cat fzf-unicode**', :Tab, pane: 0 - tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } + redraw = ->() { tmux.send_keys 'C-l', pane: 1 } + tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) } tmux.send_keys '1', pane: 1 - tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } + tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) } tmux.send_keys :BTab, pane: 1 - tmux.until(1) { |lines| lines[-2].include? '(1)' } + tmux.until(1) { |lines| redraw.(); lines[-2].include? '(1)' } tmux.send_keys :BSpace, pane: 1 - tmux.until(1) { |lines| lines[-2].start_with? ' 2/' } + tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) } tmux.send_keys '2', pane: 1 - tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } + tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) } tmux.send_keys :BTab, pane: 1 - tmux.until(1) { |lines| lines[-2].include? '(2)' } + tmux.until(1) { |lines| redraw.(); lines[-2].include? '(2)' } tmux.send_keys :Enter, pane: 1 - tmux.until { |lines| lines[-1].include?('cat') || lines[-2].include?('cat') } + tmux.until do |lines| + tmux.send_keys 'C-l' + lines[-1].include?('cat') || lines[-2].include?('cat') + end tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'test3test4' } end From d206949f6258db36060af3e84fdae15cb7b9a45c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 8 Nov 2016 03:07:26 +0900 Subject: [PATCH 305/658] Wait for additional keys after ESC for up to 100ms Close #661 --- src/tui/ncurses.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index d55c1c8..16a29fc 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -295,13 +295,24 @@ func getch(nonblock bool) int { func GetBytes() []byte { c := getch(false) + retries := 0 + if c == 27 { + // Wait for additional keys after ESC for 100ms (10 * 10ms) + retries = 10 + } _buf = append(_buf, byte(c)) for { c = getch(true) if c == -1 { + if retries > 0 { + retries-- + time.Sleep(10 * time.Millisecond) + continue + } break } + retries = 0 _buf = append(_buf, byte(c)) } From 2192d8d816fd8ba02c5b4d4fc04e0284d081ce99 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 8 Nov 2016 03:32:41 +0900 Subject: [PATCH 306/658] GOOS=windows make release --- src/Makefile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Makefile b/src/Makefile index 15f4c73..4d4bd1b 100644 --- a/src/Makefile +++ b/src/Makefile @@ -33,10 +33,17 @@ endif all: fzf/$(BINARY) +ifeq ($(GOOS),windows) +release: fzf/$(BINARY32) fzf/$(BINARY64) + -cd fzf && cp $(BINARY32) $(RELEASE32).exe && zip $(RELEASE32).zip $(RELEASE32).exe + cd fzf && cp $(BINARY64) $(RELEASE64).exe && zip $(RELEASE64).zip $(RELEASE64).exe && \ + rm -f $(RELEASE32).exe $(RELEASE64).exe +else release: test fzf/$(BINARY32) fzf/$(BINARY64) -cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ rm -f $(RELEASE32) $(RELEASE64) +endif $(SRCDIR): mkdir -p $(shell dirname $(SRCDIR)) @@ -52,7 +59,7 @@ android-build: $(SRCDIR) rm -f $(RELEASEARM7) test: deps - SHELL=/bin/sh go test -v -tags "$(TAGS)" ./... + SHELL=/bin/sh GOOS=$(GOOS) go test -v -tags "$(TAGS)" ./... install: $(BINDIR)/fzf From 2d87896939af9a72033d114e9d7e06cd47c19e71 Mon Sep 17 00:00:00 2001 From: Laur Aliste Date: Tue, 8 Nov 2016 15:41:46 +0100 Subject: [PATCH 307/658] Minor README markup fix. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 82d059a..a490011 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ vim $(fzf) #### Using the finder -- `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P)` to move cursor up and down +- `CTRL-J` / `CTRL-K` (or `CTRL-N` / `CTRL-P`) to move cursor up and down - `Enter` key to select the item, `CTRL-C` / `CTRL-G` / `ESC` to exit - On multi-select mode (`-m`), `TAB` and `Shift-TAB` to mark multiple items - Emacs style key bindings From f87d382ec831ea35a982fa7776000c43654e1538 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 9 Nov 2016 01:45:06 +0900 Subject: [PATCH 308/658] Fix --color=bw on tcell build --- src/tui/tcell.go | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 0bb06e9..da104b3 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -140,6 +140,8 @@ func Init(theme *ColorTheme, black bool, mouse bool) { _color = theme != nil if _color { InitTheme(theme, black) + } else { + theme = DefaultTheme() } ColNormal = ColorPair{theme.Fg, theme.Bg} ColPrompt = ColorPair{theme.Prompt, theme.Bg} @@ -439,13 +441,20 @@ func (w *Window) PrintString(text string, pair ColorPair, a Attr) { t := text lx := 0 - // TODO respect attr - style := pair.style(). + var style tcell.Style + if _color { + style = pair.style(). + Reverse(a&Attr(tcell.AttrReverse) != 0). + Underline(a&Attr(tcell.AttrUnderline) != 0) + } else { + style = ColDefault.style(). + Reverse(a&Attr(tcell.AttrReverse) != 0 || pair == ColCurrent || pair == ColCurrentMatch). + Underline(a&Attr(tcell.AttrUnderline) != 0 || pair == ColMatch || pair == ColCurrentMatch) + } + style = style. Blink(a&Attr(tcell.AttrBlink) != 0). Bold(a&Attr(tcell.AttrBold) != 0). - Dim(a&Attr(tcell.AttrDim) != 0). - Reverse(a&Attr(tcell.AttrReverse) != 0). - Underline(a&Attr(tcell.AttrUnderline) != 0) + Dim(a&Attr(tcell.AttrDim) != 0) for { if len(t) == 0 { @@ -485,8 +494,13 @@ func (w *Window) CPrint(pair ColorPair, a Attr, text string) { func (w *Window) FillString(text string, pair ColorPair, a Attr) bool { lx := 0 - //TODO: respect attr - style := pair.style(). + var style tcell.Style + if _color { + style = pair.style() + } else { + style = ColDefault.style() + } + style = style. Blink(a&Attr(tcell.AttrBlink) != 0). Bold(a&Attr(tcell.AttrBold) != 0). Dim(a&Attr(tcell.AttrDim) != 0). @@ -537,7 +551,12 @@ func (w *Window) DrawBorder() { top := w.Top bot := top + w.Height - style := ColBorder.style() + var style tcell.Style + if _color { + style = ColBorder.style() + } else { + style = ColDefault.style() + } for x := left; x < right; x++ { _screen.SetContent(x, top, tcell.RuneHLine, nil, style) From a221c672fbd968ea4cd83557a228065a8fc129f3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 9 Nov 2016 01:45:27 +0900 Subject: [PATCH 309/658] 0.15.6 --- CHANGELOG.md | 8 ++++++++ README.md | 9 +++++++++ install | 4 ++-- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 6 files changed, 22 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7f4df9..85e38a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +0.15.6 +------ +- Windows binaries! (@kelleyma49) +- Fixed the bug where header lines are cleared when preview window is toggled +- Fixed not to display ^N and ^O on screen +- Fixed cursor keys (or any key sequence that starts with ESC) on WSL by + making fzf wait for additional keystrokes after ESC for up to 100ms + 0.15.5 ------ - Setting foreground color will no longer set background color to black diff --git a/README.md b/README.md index a490011..1704bc0 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,15 @@ method used. - brew: `brew update; brew reinstall fzf` - vim-plug: `:PlugUpdate fzf` +### Windows + +Pre-built binaries for Windows can be downloaded [here][bin]. However, other +components of the project may not work on Windows. You might want to consider +installing fzf on [Windows Subsystem for Linux][wsl] where everything runs +flawlessly. + +[wsl]: https://blogs.msdn.microsoft.com/wsl/ + Usage ----- diff --git a/install b/install index 5d3baf8..72c9c68 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.15.5 pre=1 || - version=0.15.5 pre=0 +[[ "$@" =~ --pre ]] && version=0.15.6 pre=1 || + version=0.15.6 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 68efe99..4ec84af 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Oct 2016" "fzf 0.15.5" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Nov 2016" "fzf 0.15.6" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 8479cec..55552e5 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Oct 2016" "fzf 0.15.5" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Nov 2016" "fzf 0.15.6" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 778d219..e0740af 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.15.5" + version = "0.15.6" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 7ce427ff47fa613b43b9b5d251fca10768f5da01 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 9 Nov 2016 12:05:45 +0900 Subject: [PATCH 310/658] Fix panic when color is disabled and header lines contain ANSI colors Close #732 --- src/result.go | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/result.go b/src/result.go index c0cf5d6..e2d7c75 100644 --- a/src/result.go +++ b/src/result.go @@ -147,19 +147,21 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, } else { ansi := itemColors[curr-1] fg := ansi.color.fg - if fg == -1 { - if current { - fg = theme.Current - } else { - fg = theme.Fg - } - } bg := ansi.color.bg - if bg == -1 { - if current { - bg = theme.DarkBg - } else { - bg = theme.Bg + if theme != nil { + if fg == -1 { + if current { + fg = theme.Current + } else { + fg = theme.Fg + } + } + if bg == -1 { + if current { + bg = theme.DarkBg + } else { + bg = theme.Bg + } } } colors = append(colors, colorOffset{ From a2beb159f1757961f1314b2739b5707ea2af2ea0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 9 Nov 2016 12:41:46 +0900 Subject: [PATCH 311/658] 0.15.7 --- CHANGELOG.md | 4 ++++ install | 4 ++-- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85e38a1..0a2ee59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ CHANGELOG ========= +0.15.7 +------ +- Fixed panic when color is disabled and header lines contain ANSI colors + 0.15.6 ------ - Windows binaries! (@kelleyma49) diff --git a/install b/install index 72c9c68..97d3aae 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.15.6 pre=1 || - version=0.15.6 pre=0 +[[ "$@" =~ --pre ]] && version=0.15.7 pre=1 || + version=0.15.7 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 4ec84af..4839183 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Nov 2016" "fzf 0.15.6" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Nov 2016" "fzf 0.15.7" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 55552e5..a527b9b 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Nov 2016" "fzf 0.15.6" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Nov 2016" "fzf 0.15.7" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index e0740af..2353705 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.15.6" + version = "0.15.7" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From dc557c0d4ca3b4770757078308f966e8a90eb3c3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 14 Nov 2016 02:13:16 +0900 Subject: [PATCH 312/658] Update ANSI processor to handle more VT-100 escape sequences The updated regular expression should include not all but most of the frequently used ANSI sequences. Close #735. --- src/ansi.go | 13 +++++++++++-- src/ansi_test.go | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/ansi.go b/src/ansi.go index b7d3a9e..427f7f4 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -35,7 +35,16 @@ func (s *ansiState) equals(t *ansiState) bool { var ansiRegex *regexp.Regexp func init() { - ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*.|[\x0e\x0f]") + /* + 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 + */ + // 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.|[\x08\x0e\x0f]") } func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) { @@ -100,7 +109,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { } else { state = &ansiState{prevState.fg, prevState.bg, prevState.attr} } - if ansiCode[0] != '\x1b' || ansiCode[len(ansiCode)-1] != 'm' { + if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' { return state } diff --git a/src/ansi_test.go b/src/ansi_test.go index 3afcc6c..a536677 100644 --- a/src/ansi_test.go +++ b/src/ansi_test.go @@ -26,7 +26,7 @@ func TestExtractColor(t *testing.T) { output, ansiOffsets, newState := extractColor(src, state, nil) state = newState if output != "hello world" { - t.Errorf("Invalid output: {}", output) + t.Errorf("Invalid output: %s %s", output, []rune(output)) } fmt.Println(src, ansiOffsets, clean) assertion(ansiOffsets, state) @@ -56,7 +56,7 @@ func TestExtractColor(t *testing.T) { }) state = nil - src = "\x1b[1mhello \x1b[mworld" + src = "\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d" check(func(offsets *[]ansiOffset, state *ansiState) { if len(*offsets) != 1 { t.Fail() From 2e8814bb578f672daa3afb0424666b67a87bd34e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 14 Nov 2016 12:26:46 +0900 Subject: [PATCH 313/658] Add WSL to .github/ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index a876372..a0c8554 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -11,6 +11,7 @@ - [ ] Linux - [ ] Mac OS X - [ ] Windows + - [ ] Windows Subsystem for Linux - [ ] Etc. - Shell - [ ] bash From c49e65d92694136fbeb3e341f33f9d69395620e8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 15 Nov 2016 01:51:25 +0900 Subject: [PATCH 314/658] [shell] Fix pruning condition of find command for CTRL-T and ALT-C `-fstype dev` is invalid. It's devfs on macOS and devtmpfs on Linux. --- shell/key-bindings.bash | 4 ++-- shell/key-bindings.fish | 4 ++-- shell/key-bindings.zsh | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index c251a0f..d41eb5b 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -1,7 +1,7 @@ # Key bindings # ------------ __fzf_select__() { - local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ + local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" @@ -41,7 +41,7 @@ fzf-file-widget() { __fzf_cd__() { local cmd dir - cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ + cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" dir=$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS") && printf 'cd %q' "$dir" } diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index eaec912..b63ee89 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -15,7 +15,7 @@ function fzf_key_bindings function fzf-file-widget set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND " - command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ + command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | sed 1d | cut -b3-" @@ -34,7 +34,7 @@ function fzf_key_bindings function fzf-cd-widget set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND " - command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ + command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-" # Fish hangs if the command before pipe redirects (2> /dev/null) eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m $FZF_ALT_C_OPTS > $TMPDIR/fzf.result" diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 0667a4c..fc694cb 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -4,7 +4,7 @@ if [[ $- == *i* ]]; then # CTRL-T - Paste the selected file path(s) into the command line __fsel() { - local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ + local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" @@ -33,7 +33,7 @@ bindkey '^T' fzf-file-widget # ALT-C - cd into the selected directory fzf-cd-widget() { - local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'dev' -o -fstype 'proc' \\) -prune \ + local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" setopt localoptions pipefail 2> /dev/null cd "${$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS"):-.}" From 22487810ba8484b2e2c64874bcbdc3e854c6be99 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 15 Nov 2016 23:44:04 +0900 Subject: [PATCH 315/658] Update README: link to wiki page --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1704bc0..9051f67 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,8 @@ If you use vi mode on bash, you need to add `set -o vi` *before* `source ~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi mode. +More tips can be found on [the wiki page](https://github.com/junegunn/fzf/wiki/Configuring-shell-key-bindings). + Fuzzy completion for bash and zsh --------------------------------- From 4b332d831e3b5cb6dc70484b1a621eae9b162317 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 15 Nov 2016 23:57:32 +0900 Subject: [PATCH 316/658] Add --no-bold option --- src/options.go | 7 +++++++ src/terminal.go | 26 ++++++++++++++++---------- src/tui/ncurses.go | 10 +++++----- src/tui/tcell.go | 10 +++++----- 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/options.go b/src/options.go index a3ed22d..6e63d4e 100644 --- a/src/options.go +++ b/src/options.go @@ -57,6 +57,7 @@ const usage = `usage: fzf [options] --ansi Enable processing of ANSI color codes --tabstop=SPACES Number of spaces for a tab character (default: 8) --color=COLSPEC Base scheme (dark|light|16|bw) and/or custom colors + --no-bold Do not use bold text History --history=FILE History file @@ -144,6 +145,7 @@ type Options struct { Mouse bool Theme *tui.ColorTheme Black bool + Bold bool Reverse bool Cycle bool Hscroll bool @@ -189,6 +191,7 @@ func defaultOptions() *Options { Mouse: true, Theme: tui.EmptyTheme(), Black: false, + Bold: true, Reverse: false, Cycle: false, Hscroll: true, @@ -910,6 +913,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Black = true case "--no-black": opts.Black = false + case "--bold": + opts.Bold = true + case "--no-bold": + opts.Bold = false case "--reverse": opts.Reverse = true case "--no-reverse": diff --git a/src/terminal.go b/src/terminal.go index 6d1a1ab..5d4eef8 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -70,6 +70,7 @@ type Terminal struct { header0 []string ansi bool margin [4]sizeSpec + strong tui.Attr window *tui.Window bwindow *tui.Window pwindow *tui.Window @@ -256,6 +257,10 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { if len(opts.Preview.command) > 0 { previewBox = util.NewEventBox() } + strongAttr := tui.Bold + if !opts.Bold { + strongAttr = tui.AttrRegular + } return &Terminal{ initDelay: delay, inlineInfo: opts.InlineInfo, @@ -279,6 +284,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { printQuery: opts.PrintQuery, history: opts.History, margin: opts.Margin, + strong: strongAttr, cycle: opts.Cycle, header: header, header0: header, @@ -532,24 +538,24 @@ func (t *Terminal) placeCursor() { func (t *Terminal) printPrompt() { t.move(0, 0, true) - t.window.CPrint(tui.ColPrompt, tui.Bold, t.prompt) - t.window.CPrint(tui.ColNormal, tui.Bold, string(t.input)) + t.window.CPrint(tui.ColPrompt, t.strong, t.prompt) + t.window.CPrint(tui.ColNormal, t.strong, string(t.input)) } func (t *Terminal) printInfo() { if t.inlineInfo { t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true) if t.reading { - t.window.CPrint(tui.ColSpinner, tui.Bold, " < ") + t.window.CPrint(tui.ColSpinner, t.strong, " < ") } else { - t.window.CPrint(tui.ColPrompt, tui.Bold, " < ") + t.window.CPrint(tui.ColPrompt, t.strong, " < ") } } else { t.move(1, 0, true) if t.reading { duration := int64(spinnerDuration) idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration - t.window.CPrint(tui.ColSpinner, tui.Bold, _spinner[idx]) + t.window.CPrint(tui.ColSpinner, t.strong, _spinner[idx]) } t.move(1, 2, false) } @@ -627,17 +633,17 @@ func (t *Terminal) printItem(result *Result, i int, current bool) { } else if current { label = ">" } - t.window.CPrint(tui.ColCursor, tui.Bold, label) + t.window.CPrint(tui.ColCursor, t.strong, label) if current { if selected { - t.window.CPrint(tui.ColSelected, tui.Bold, ">") + t.window.CPrint(tui.ColSelected, t.strong, ">") } else { - t.window.CPrint(tui.ColCurrent, tui.Bold, " ") + t.window.CPrint(tui.ColCurrent, t.strong, " ") } - t.printHighlighted(result, tui.Bold, tui.ColCurrent, tui.ColCurrentMatch, true) + t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true) } else { if selected { - t.window.CPrint(tui.ColSelected, tui.Bold, ">") + t.window.CPrint(tui.ColSelected, t.strong, ">") } else { t.window.Print(" ") } diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 16a29fc..1e77c89 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -30,11 +30,11 @@ type Attr C.int type WindowImpl C.WINDOW const ( - Bold = C.A_BOLD - Dim = C.A_DIM - Blink = C.A_BLINK - Reverse = C.A_REVERSE - Underline = C.A_UNDERLINE + Bold Attr = C.A_BOLD + Dim = C.A_DIM + Blink = C.A_BLINK + Reverse = C.A_REVERSE + Underline = C.A_UNDERLINE ) const ( diff --git a/src/tui/tcell.go b/src/tui/tcell.go index da104b3..0fe066d 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -43,11 +43,11 @@ type WindowTcell struct { type WindowImpl WindowTcell const ( - Bold = Attr(tcell.AttrBold) - Dim = Attr(tcell.AttrDim) - Blink = Attr(tcell.AttrBlink) - Reverse = Attr(tcell.AttrReverse) - Underline = Attr(tcell.AttrUnderline) + Bold Attr = Attr(tcell.AttrBold) + Dim = Attr(tcell.AttrDim) + Blink = Attr(tcell.AttrBlink) + Reverse = Attr(tcell.AttrReverse) + Underline = Attr(tcell.AttrUnderline) ) const ( From 8c661d4e8c8a4b643155aa46481ac92f6ebf75b9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 19 Nov 2016 22:40:28 +0900 Subject: [PATCH 317/658] Revamp escape sequence processing for WSL Also add support for alt-[0-9] and f1[12] --- src/options.go | 10 ++ src/tui/ncurses.go | 397 ++++++++++++++++++--------------------------- src/tui/tcell.go | 9 +- src/tui/tui.go | 11 +- 4 files changed, 184 insertions(+), 243 deletions(-) diff --git a/src/options.go b/src/options.go index 6e63d4e..694084e 100644 --- a/src/options.go +++ b/src/options.go @@ -330,6 +330,10 @@ func isAlphabet(char uint8) bool { return char >= 'a' && char <= 'z' } +func isNumeric(char uint8) bool { + return char >= '0' && char <= '9' +} + func parseAlgo(str string) algo.Algo { switch str { case "v1": @@ -406,11 +410,17 @@ func parseKeyChords(str string, message string) map[int]string { chord = tui.DoubleClick case "f10": chord = tui.F10 + case "f11": + chord = tui.F11 + case "f12": + chord = tui.F12 default: if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { chord = tui.CtrlA + int(lkey[5]) - 'a' } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { chord = tui.AltA + int(lkey[4]) - 'a' + } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isNumeric(lkey[4]) { + chord = tui.Alt0 + int(lkey[4]) - '0' } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' { chord = tui.F1 + int(key[1]) - '1' } else if utf8.RuneCountInString(key) == 1 { diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 1e77c89..205e643 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -10,8 +10,12 @@ package tui #cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl #cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch -SCREEN *c_newterm () { - return newterm(NULL, stderr, stdin); +FILE* c_tty() { + return fopen("/dev/tty", "r"); +} + +SCREEN* c_newterm(FILE* tty) { + return newterm(NULL, stderr, tty); } */ import "C" @@ -20,7 +24,6 @@ import ( "fmt" "os" "strings" - "syscall" "time" "unicode/utf8" ) @@ -59,7 +62,6 @@ const ( ) var ( - _in *os.File _screen *C.SCREEN _colorMap map[int]ColorPair _colorFn func(ColorPair, Attr) C.int @@ -81,18 +83,13 @@ func DefaultTheme() *ColorTheme { } func Init(theme *ColorTheme, black bool, mouse bool) { - { - in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0) - if err != nil { - panic("Failed to open /dev/tty") - } - _in = in - // Break STDIN - // syscall.Dup2(int(in.Fd()), int(os.Stdin.Fd())) - } - C.setlocale(C.LC_ALL, C.CString("")) - _screen = C.c_newterm() + tty := C.c_tty() + if tty == nil { + fmt.Println("Failed to open /dev/tty") + os.Exit(2) + } + _screen = C.c_newterm(tty) if _screen == nil { fmt.Println("Invalid $TERM: " + os.Getenv("TERM")) os.Exit(2) @@ -100,9 +97,14 @@ func Init(theme *ColorTheme, black bool, mouse bool) { C.set_term(_screen) if mouse { C.mousemask(C.ALL_MOUSE_EVENTS, nil) + C.mouseinterval(0) } C.noecho() C.raw() // stty dsusp undef + C.nonl() + C.keypad(C.stdscr, true) + C.set_escdelay(200) + C.timeout(100) // ESCDELAY 200ms + timeout 100ms _color = theme != nil if _color { @@ -283,246 +285,167 @@ func PairFor(fg Color, bg Color) ColorPair { return id } -func getch(nonblock bool) int { - b := make([]byte, 1) - syscall.SetNonblock(int(_in.Fd()), nonblock) - _, err := _in.Read(b) - if err != nil { - return -1 - } - return int(b[0]) -} - -func GetBytes() []byte { - c := getch(false) - retries := 0 - if c == 27 { - // Wait for additional keys after ESC for 100ms (10 * 10ms) - retries = 10 - } - _buf = append(_buf, byte(c)) - - for { - c = getch(true) - if c == -1 { - if retries > 0 { - retries-- - time.Sleep(10 * time.Millisecond) - continue - } - break +func consume(expects ...rune) bool { + for _, r := range expects { + if int(C.getch()) != int(r) { + return false } - retries = 0 - _buf = append(_buf, byte(c)) } - - return _buf + return true } -// 27 (91 79) 77 type x y -func mouseSequence(sz *int) Event { - if len(_buf) < 6 { - return Event{Invalid, 0, nil} - } - *sz = 6 - switch _buf[3] { - case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl - 35, 39, 43, 51: // mouse-up / shift / cmd / ctrl - mod := _buf[3] >= 36 - down := _buf[3]%2 == 0 - x := int(_buf[4] - 33) - y := int(_buf[5] - 33) - double := false - if down { - now := time.Now() - if now.Sub(_prevDownTime) < doubleClickDuration { - _clickY = append(_clickY, y) - } else { - _clickY = []int{y} - } - _prevDownTime = now - } else { - if len(_clickY) > 1 && _clickY[0] == _clickY[1] && - time.Now().Sub(_prevDownTime) < doubleClickDuration { - double = true - } - } - return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}} - case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl - 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl - mod := _buf[3] >= 100 - s := 1 - int(_buf[3]%2)*2 - x := int(_buf[4] - 33) - y := int(_buf[5] - 33) - return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}} - } - return Event{Invalid, 0, nil} -} - -func escSequence(sz *int) Event { - if len(_buf) < 2 { +func escSequence() Event { + // nodelay is not thread-safe (e.g. ) + // C.nodelay(C.stdscr, true) + c := C.getch() + switch c { + case C.ERR: return Event{ESC, 0, nil} - } - *sz = 2 - switch _buf[1] { - case 13: + case CtrlM: return Event{AltEnter, 0, nil} - case 32: - return Event{AltSpace, 0, nil} - case 47: + case '/': return Event{AltSlash, 0, nil} - case 98: - return Event{AltB, 0, nil} - case 100: - return Event{AltD, 0, nil} - case 102: - return Event{AltF, 0, nil} - case 127: + case ' ': + return Event{AltSpace, 0, nil} + case 127, C.KEY_BACKSPACE: return Event{AltBS, 0, nil} - case 91, 79: - if len(_buf) < 3 { + case '[': + // Bracketed paste mode (printf "\e[?2004h") + // \e[200~ TEXT \e[201~ + if consume('2', '0', '0', '~') { return Event{Invalid, 0, nil} } - *sz = 3 - switch _buf[2] { - case 68: - return Event{Left, 0, nil} - case 67: - return Event{Right, 0, nil} - case 66: - return Event{Down, 0, nil} - case 65: - return Event{Up, 0, nil} - case 90: - return Event{BTab, 0, nil} - case 72: - return Event{Home, 0, nil} - case 70: - return Event{End, 0, nil} - case 77: - return mouseSequence(sz) - case 80: - return Event{F1, 0, nil} - case 81: - return Event{F2, 0, nil} - case 82: - return Event{F3, 0, nil} - case 83: - return Event{F4, 0, nil} - case 49, 50, 51, 52, 53, 54: - if len(_buf) < 4 { - return Event{Invalid, 0, nil} - } - *sz = 4 - switch _buf[2] { - case 50: - if len(_buf) == 5 && _buf[4] == 126 { - *sz = 5 - switch _buf[3] { - case 48: - return Event{F9, 0, nil} - case 49: - return Event{F10, 0, nil} - } - } - // Bracketed paste mode \e[200~ / \e[201 - if _buf[3] == 48 && (_buf[4] == 48 || _buf[4] == 49) && _buf[5] == 126 { - *sz = 6 - return Event{Invalid, 0, nil} - } - return Event{Invalid, 0, nil} // INS - case 51: - return Event{Del, 0, nil} - case 52: - return Event{End, 0, nil} - case 53: - return Event{PgUp, 0, nil} - case 54: - return Event{PgDn, 0, nil} - case 49: - switch _buf[3] { - case 126: - return Event{Home, 0, nil} - case 53, 55, 56, 57: - if len(_buf) == 5 && _buf[4] == 126 { - *sz = 5 - switch _buf[3] { - case 53: - return Event{F5, 0, nil} - case 55: - return Event{F6, 0, nil} - case 56: - return Event{F7, 0, nil} - case 57: - return Event{F8, 0, nil} - } - } - return Event{Invalid, 0, nil} - case 59: - if len(_buf) != 6 { - return Event{Invalid, 0, nil} - } - *sz = 6 - switch _buf[4] { - case 50: - switch _buf[5] { - case 68: - return Event{Home, 0, nil} - case 67: - return Event{End, 0, nil} - } - case 53: - switch _buf[5] { - case 68: - return Event{SLeft, 0, nil} - case 67: - return Event{SRight, 0, nil} - } - } // _buf[4] - } // _buf[3] - } // _buf[2] - } // _buf[2] - } // _buf[1] - if _buf[1] >= 'a' && _buf[1] <= 'z' { - return Event{AltA + int(_buf[1]) - 'a', 0, nil} + } + if c >= 'a' && c <= 'z' { + return Event{AltA + int(c) - 'a', 0, nil} + } + + if c >= '0' && c <= '9' { + return Event{Alt0 + int(c) - '0', 0, nil} + } + + // Don't care. Ignore the rest. + for ; c != C.ERR; c = C.getch() { } return Event{Invalid, 0, nil} } func GetChar() Event { - if len(_buf) == 0 { - _buf = GetBytes() - } - if len(_buf) == 0 { - panic("Empty _buffer") - } - - sz := 1 - defer func() { - _buf = _buf[sz:] - }() - - switch _buf[0] { - case CtrlC: - return Event{CtrlC, 0, nil} - case CtrlG: - return Event{CtrlG, 0, nil} - case CtrlQ: - return Event{CtrlQ, 0, nil} + c := C.getch() + switch c { + case C.ERR: + return Event{Invalid, 0, nil} + case C.KEY_UP: + return Event{Up, 0, nil} + case C.KEY_DOWN: + return Event{Down, 0, nil} + case C.KEY_LEFT: + return Event{Left, 0, nil} + case C.KEY_RIGHT: + return Event{Right, 0, nil} + case C.KEY_HOME: + return Event{Home, 0, nil} + case C.KEY_END: + return Event{End, 0, nil} + case C.KEY_BACKSPACE: + return Event{BSpace, 0, nil} + case C.KEY_F0 + 1: + return Event{F1, 0, nil} + case C.KEY_F0 + 2: + return Event{F2, 0, nil} + case C.KEY_F0 + 3: + return Event{F3, 0, nil} + case C.KEY_F0 + 4: + return Event{F4, 0, nil} + case C.KEY_F0 + 5: + return Event{F5, 0, nil} + case C.KEY_F0 + 6: + return Event{F6, 0, nil} + case C.KEY_F0 + 7: + return Event{F7, 0, nil} + case C.KEY_F0 + 8: + return Event{F8, 0, nil} + case C.KEY_F0 + 9: + return Event{F9, 0, nil} + case C.KEY_F0 + 10: + return Event{F10, 0, nil} + case C.KEY_F0 + 11: + return Event{F11, 0, nil} + case C.KEY_F0 + 12: + return Event{F12, 0, nil} + case C.KEY_DC: + return Event{Del, 0, nil} + case C.KEY_PPAGE: + return Event{PgUp, 0, nil} + case C.KEY_NPAGE: + return Event{PgDn, 0, nil} + case C.KEY_BTAB: + return Event{BTab, 0, nil} + case C.KEY_ENTER: + return Event{CtrlM, 0, nil} + case C.KEY_SLEFT: + return Event{SLeft, 0, nil} + case C.KEY_SRIGHT: + return Event{SRight, 0, nil} + case C.KEY_MOUSE: + var me C.MEVENT + if C.getmouse(&me) != C.ERR { + mod := ((me.bstate & C.BUTTON_SHIFT) | (me.bstate & C.BUTTON_CTRL) | (me.bstate & C.BUTTON_ALT)) > 0 + x := int(me.x) + y := int(me.y) + /* Cannot use BUTTON1_DOUBLE_CLICKED due to mouseinterval(0) */ + if (me.bstate & C.BUTTON1_PRESSED) > 0 { + now := time.Now() + if now.Sub(_prevDownTime) < doubleClickDuration { + _clickY = append(_clickY, y) + } else { + _clickY = []int{y} + _prevDownTime = now + } + return Event{Mouse, 0, &MouseEvent{y, x, 0, true, false, mod}} + } else if (me.bstate & C.BUTTON1_RELEASED) > 0 { + double := false + if len(_clickY) > 1 && _clickY[0] == _clickY[1] && + time.Now().Sub(_prevDownTime) < doubleClickDuration { + double = true + } + return Event{Mouse, 0, &MouseEvent{y, x, 0, false, double, mod}} + } else if (me.bstate&0x8000000) > 0 || (me.bstate&0x80) > 0 { + return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}} + } else if (me.bstate & C.BUTTON4_PRESSED) > 0 { + return Event{Mouse, 0, &MouseEvent{y, x, 1, false, false, mod}} + } + } + return Event{Invalid, 0, nil} + case C.KEY_RESIZE: + return Event{Invalid, 0, nil} + case ESC: + return escSequence() case 127: return Event{BSpace, 0, nil} - case ESC: - return escSequence(&sz) + } + // CTRL-A ~ CTRL-Z + if c >= CtrlA && c <= CtrlZ { + return Event{int(c), 0, nil} } - // CTRL-A ~ CTRL-Z - if _buf[0] <= CtrlZ { - return Event{int(_buf[0]), 0, nil} + // Multi-byte character + buffer := []byte{byte(c)} + for { + r, _ := utf8.DecodeRune(buffer) + if r != utf8.RuneError { + return Event{Rune, r, nil} + } + + c := C.getch() + if c == C.ERR { + break + } + if c >= C.KEY_CODE_YES { + C.ungetch(c) + break + } + buffer = append(buffer, byte(c)) } - r, rsz := utf8.DecodeRune(_buf) - if r == utf8.RuneError { - return Event{ESC, 0, nil} - } - sz = rsz - return Event{Rune, r, nil} + return Event{Invalid, 0, nil} } diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 0fe066d..3ea0412 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -207,8 +207,8 @@ func GetChar() Event { _clickY = append(_clickY, x) } else { _clickY = []int{x} + _prevDownTime = now } - _prevDownTime = now } else { if len(_clickY) > 1 && _clickY[0] == _clickY[1] && time.Now().Sub(_prevDownTime) < doubleClickDuration { @@ -326,9 +326,9 @@ func GetChar() Event { case tcell.KeyF10: return Event{F10, 0, nil} case tcell.KeyF11: - return Event{Invalid, 0, nil} + return Event{F11, 0, nil} case tcell.KeyF12: - return Event{Invalid, 0, nil} + return Event{F12, 0, nil} // ev.Ch doesn't work for some reason for space: case tcell.KeyRune: @@ -343,6 +343,9 @@ func GetChar() Event { if r >= 'a' && r <= 'z' { return Event{AltA + int(r) - 'a', 0, nil} } + if r >= '0' && r <= '9' { + return Event{Alt0 + int(r) - '0', 0, nil} + } } return Event{Rune, r, nil} diff --git a/src/tui/tui.go b/src/tui/tui.go index a33baa4..7513b6f 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -67,18 +67,24 @@ const ( F8 F9 F10 + F11 + F12 AltEnter AltSpace AltSlash AltBS - AltA + + Alt0 +) + +const ( // Reset iota + AltA = Alt0 + 'a' - '0' + iota AltB AltC AltD AltE AltF - AltZ = AltA + 'z' - 'a' ) @@ -136,7 +142,6 @@ type MouseEvent struct { } var ( - _buf []byte _color bool _prevDownTime time.Time _clickY []int From 3cd2547e91fb3fee1b331c5fa97244d6a20e9d0b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 19 Nov 2016 23:03:27 +0900 Subject: [PATCH 318/658] Reduce ESC delay to 100ms --- src/tui/ncurses.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 205e643..fc15b80 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -103,8 +103,8 @@ func Init(theme *ColorTheme, black bool, mouse bool) { C.raw() // stty dsusp undef C.nonl() C.keypad(C.stdscr, true) - C.set_escdelay(200) - C.timeout(100) // ESCDELAY 200ms + timeout 100ms + C.set_escdelay(100) + C.timeout(100) // ESCDELAY 100ms + timeout 100ms _color = theme != nil if _color { From d75ed841a939481a8c74d63c8d463fa566560ca3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 19 Nov 2016 23:12:28 +0900 Subject: [PATCH 319/658] Fix --no-bold on --no-color --- src/tui/ncurses.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index fc15b80..5c192e6 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -182,9 +182,7 @@ func attrMono(pair ColorPair, a Attr) C.int { var attr C.int switch pair { case ColCurrent: - if C.int(a)&C.A_BOLD == C.A_BOLD { - attr = C.A_REVERSE - } + attr = C.A_REVERSE case ColMatch: attr = C.A_UNDERLINE case ColCurrentMatch: From 6a65006f55c527d86d6924ee296828a50e77a7e9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 19 Nov 2016 23:13:26 +0900 Subject: [PATCH 320/658] 0.15.8 --- CHANGELOG.md | 7 +++++++ install | 4 ++-- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 8 ++++++-- src/constants.go | 2 +- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a2ee59..20ad20d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +0.15.8 +------ +- Updated ANSI processor to handle more VT-100 escape sequences +- Added `--no-bold` (and `--bold`) option +- Improved escape sequence processing for WSL +- Added support for `alt-[0-9]`, `f11`, and `f12` for `--bind` and `--expect` + 0.15.7 ------ - Fixed panic when color is disabled and header lines contain ANSI colors diff --git a/install b/install index 97d3aae..75524b9 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.15.7 pre=1 || - version=0.15.7 pre=0 +[[ "$@" =~ --pre ]] && version=0.15.8 pre=1 || + version=0.15.8 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 4839183..18cc1be 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Nov 2016" "fzf 0.15.7" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Nov 2016" "fzf 0.15.8" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index a527b9b..2f107e7 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Nov 2016" "fzf 0.15.7" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Nov 2016" "fzf 0.15.8" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -216,6 +216,9 @@ e.g. \fBfzf --color=bg+:24\fR \fBheader \fRHeader .RE .TP +.B "--no-bold" +Do not use bold text +.TP .B "--black" Use black background .SS History @@ -388,7 +391,8 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR .B AVAILABLE KEYS: (SYNONYMS) \fIctrl-[a-z]\fR \fIalt-[a-z]\fR - \fIf[1-10]\fR + \fIalt-[0-9]\fR + \fIf[1-12]\fR \fIenter\fR (\fIreturn\fR \fIctrl-m\fR) \fIspace\fR \fIbspace\fR (\fIbs\fR) diff --git a/src/constants.go b/src/constants.go index 2353705..52884a7 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.15.7" + version = "0.15.8" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 8524ea74414cfe0773a4318a6b4d9da87e230a07 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 23 Nov 2016 01:58:46 +0900 Subject: [PATCH 321/658] Do not ignore resize event from ncurses and tcell --- src/terminal.go | 1 + src/tui/ncurses.go | 2 +- src/tui/tcell.go | 2 +- src/tui/tui.go | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 5d4eef8..315d9f1 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -190,6 +190,7 @@ const ( func defaultKeymap() map[int]actionType { keymap := make(map[int]actionType) keymap[tui.Invalid] = actInvalid + keymap[tui.Resize] = actClearScreen keymap[tui.CtrlA] = actBeginningOfLine keymap[tui.CtrlB] = actBackwardChar keymap[tui.CtrlC] = actAbort diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 5c192e6..5a4d538 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -416,7 +416,7 @@ func GetChar() Event { } return Event{Invalid, 0, nil} case C.KEY_RESIZE: - return Event{Invalid, 0, nil} + return Event{Resize, 0, nil} case ESC: return escSequence() case 127: diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 3ea0412..efa112a 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -183,7 +183,7 @@ func GetChar() Event { ev := _screen.PollEvent() switch ev := ev.(type) { case *tcell.EventResize: - return Event{Invalid, 0, nil} + return Event{Resize, 0, nil} // process mouse events: case *tcell.EventMouse: diff --git a/src/tui/tui.go b/src/tui/tui.go index 7513b6f..6fcaaa4 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -37,6 +37,7 @@ const ( ESC Invalid + Resize Mouse DoubleClick From 43425158f4fbb961d5c365d39664912a7ebf9342 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 23 Nov 2016 02:18:18 +0900 Subject: [PATCH 322/658] Make escape delay configurable via ncurses standard $ESCDELAY Also reduce the default delay to 50ms. We should not set it to 0ms as it breaks escape sequences on WSL. If 50ms is not enough, one can increase the delay by setting $ESCDELAY to a larger value. --- src/tui/ncurses.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 5a4d538..8603fd3 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -23,6 +23,7 @@ import "C" import ( "fmt" "os" + "strconv" "strings" "time" "unicode/utf8" @@ -103,8 +104,16 @@ func Init(theme *ColorTheme, black bool, mouse bool) { C.raw() // stty dsusp undef C.nonl() C.keypad(C.stdscr, true) - C.set_escdelay(100) - C.timeout(100) // ESCDELAY 100ms + timeout 100ms + + delay := 50 + delayEnv := os.Getenv("ESCDELAY") + if len(delayEnv) > 0 { + num, err := strconv.Atoi(delayEnv) + if err == nil && num >= 0 { + delay = num + } + } + C.set_escdelay(C.int(delay)) _color = theme != nil if _color { @@ -293,8 +302,10 @@ func consume(expects ...rune) bool { } func escSequence() Event { - // nodelay is not thread-safe (e.g. ) - // C.nodelay(C.stdscr, true) + C.nodelay(C.stdscr, true) + defer func() { + C.nodelay(C.stdscr, false) + }() c := C.getch() switch c { case C.ERR: From af31088481f6898069c3c579f21da447025a389b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 23 Nov 2016 22:46:04 +0900 Subject: [PATCH 323/658] [ncurses6] Use wcolor_set to support more than 256 color pairs To build fzf with ncurses 6 on macOS: brew install homebrew/dupes/ncurses LDFLAGS="-L/usr/local/opt/ncurses/lib" make install --- src/Makefile | 4 ++-- src/tui/ncurses.go | 38 ++++++++++++++++++++------------------ 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/Makefile b/src/Makefile index 4d4bd1b..00df49d 100644 --- a/src/Makefile +++ b/src/Makefile @@ -70,10 +70,10 @@ clean: cd fzf && rm -f fzf-* fzf/$(BINARY32): deps - cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -ldflags -w -tags "$(TAGS)" -o $(BINARY32) + cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY32) fzf/$(BINARY64): deps - cd fzf && go build -a -ldflags -w -tags "$(TAGS)" -o $(BINARY64) + cd fzf && go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY64) $(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR) cp -f fzf/$(BINARY) $(BINDIR) diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 8603fd3..ac75513 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -65,7 +65,7 @@ const ( var ( _screen *C.SCREEN _colorMap map[int]ColorPair - _colorFn func(ColorPair, Attr) C.int + _colorFn func(ColorPair, Attr) (C.short, C.int) ) func init() { @@ -164,10 +164,12 @@ func NewWindow(top int, left int, width int, height int, border bool) *Window { C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal)))) } if border { - attr := _colorFn(ColBorder, 0) + pair, attr := _colorFn(ColBorder, 0) + C.wcolor_set(win, pair, nil) C.wattron(win, attr) C.box(win, 0, 0) C.wattroff(win, attr) + C.wcolor_set(win, 0, nil) } return &Window{ @@ -179,15 +181,11 @@ func NewWindow(top int, left int, width int, height int, border bool) *Window { } } -func attrColored(pair ColorPair, a Attr) C.int { - var attr C.int - if pair > 0 { - attr = C.COLOR_PAIR(C.int(pair)) - } - return attr | C.int(a) +func attrColored(pair ColorPair, a Attr) (C.short, C.int) { + return C.short(pair), C.int(a) } -func attrMono(pair ColorPair, a Attr) C.int { +func attrMono(pair ColorPair, a Attr) (C.short, C.int) { var attr C.int switch pair { case ColCurrent: @@ -200,7 +198,7 @@ func attrMono(pair ColorPair, a Attr) C.int { if C.int(a)&C.A_BOLD == C.A_BOLD { attr = attr | C.A_BOLD } - return attr + return 0, attr } func MaxX() int { @@ -241,11 +239,13 @@ func (w *Window) Print(text string) { }, text))) } -func (w *Window) CPrint(pair ColorPair, a Attr, text string) { - attr := _colorFn(pair, a) - C.wattron(w.win(), attr) +func (w *Window) CPrint(pair ColorPair, attr Attr, text string) { + p, a := _colorFn(pair, attr) + C.wcolor_set(w.win(), p, nil) + C.wattron(w.win(), a) w.Print(text) - C.wattroff(w.win(), attr) + C.wattroff(w.win(), a) + C.wcolor_set(w.win(), 0, nil) } func Clear() { @@ -265,11 +265,13 @@ func (w *Window) Fill(str string) bool { return C.waddstr(w.win(), C.CString(str)) == C.OK } -func (w *Window) CFill(str string, fg Color, bg Color, a Attr) bool { - attr := _colorFn(PairFor(fg, bg), a) - C.wattron(w.win(), attr) +func (w *Window) CFill(str string, fg Color, bg Color, attr Attr) bool { + pair := PairFor(fg, bg) + C.wcolor_set(w.win(), C.short(pair), nil) + C.wattron(w.win(), C.int(attr)) ret := w.Fill(str) - C.wattroff(w.win(), attr) + C.wattroff(w.win(), C.int(attr)) + C.wcolor_set(w.win(), 0, nil) return ret } From 182a6d99fd550e2106298a19b542bdf940621ed1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 24 Nov 2016 00:13:10 +0900 Subject: [PATCH 324/658] [ncurses6] Support italics --- src/ansi.go | 12 +++++++----- src/tui/ncurses.go | 3 ++- src/tui/tcell.go | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/ansi.go b/src/ansi.go index 427f7f4..f8fe56a 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -143,15 +143,17 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { case 49: state.bg = -1 case 1: - state.attr = tui.Bold + state.attr = state.attr | tui.Bold case 2: - state.attr = tui.Dim + state.attr = state.attr | tui.Dim + case 3: + state.attr = state.attr | tui.Italic case 4: - state.attr = tui.Underline + state.attr = state.attr | tui.Underline case 5: - state.attr = tui.Blink + state.attr = state.attr | tui.Blink case 7: - state.attr = tui.Reverse + state.attr = state.attr | tui.Reverse case 0: init() default: diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index ac75513..6e9ddb4 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -30,7 +30,7 @@ import ( ) type ColorPair int16 -type Attr C.int +type Attr C.uint type WindowImpl C.WINDOW const ( @@ -39,6 +39,7 @@ const ( Blink = C.A_BLINK Reverse = C.A_REVERSE Underline = C.A_UNDERLINE + Italic = C.A_VERTICAL << 1 // FIXME ) const ( diff --git a/src/tui/tcell.go b/src/tui/tcell.go index efa112a..3fe1161 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -48,6 +48,7 @@ const ( Blink = Attr(tcell.AttrBlink) Reverse = Attr(tcell.AttrReverse) Underline = Attr(tcell.AttrUnderline) + Italic = Attr(tcell.AttrNone) // Not supported ) const ( From 746961bf43bb49a1d0857dead54666f9779b4286 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 24 Nov 2016 13:42:14 +0900 Subject: [PATCH 325/658] [ncurses6] Suppress tui.Italic on ncurses 5 --- src/tui/ncurses.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 6e9ddb4..dd2bc90 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -39,9 +39,10 @@ const ( Blink = C.A_BLINK Reverse = C.A_REVERSE Underline = C.A_UNDERLINE - Italic = C.A_VERTICAL << 1 // FIXME ) +var Italic Attr = C.A_VERTICAL << 1 // FIXME + const ( AttrRegular Attr = 0 ) @@ -71,6 +72,9 @@ var ( func init() { _colorMap = make(map[int]ColorPair) + if strings.HasPrefix(C.GoString(C.curses_version()), "ncurses 5") { + Italic = C.A_NORMAL + } } func (a Attr) Merge(b Attr) Attr { From 6f17f412bad6447520dd3487aa5235887cb1fd19 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 25 Nov 2016 14:05:37 +0900 Subject: [PATCH 326/658] Workaround for rendering glitch in case of short-lived input process : | fzf --preview 'echo foo' --- src/tui/ncurses.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index dd2bc90..051a042 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -130,6 +130,13 @@ func Init(theme *ColorTheme, black bool, mouse bool) { } else { _colorFn = attrMono } + + C.nodelay(C.stdscr, true) + ch := C.getch() + if ch != C.ERR { + C.ungetch(ch) + } + C.nodelay(C.stdscr, false) } func initPairs(theme *ColorTheme) { From de1c6b87272581eec4bfd39771e573c2bb772b60 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 26 Nov 2016 00:36:38 +0900 Subject: [PATCH 327/658] [tcell] 24-bit color support TAGS=tcell make install printf "\x1b[38;2;100;200;250mTRUECOLOR\x1b[m\n" | TERM=xterm-truecolor fzf --ansi --- src/ansi.go | 14 ++++++++++++++ src/tui/ncurses.go | 4 ++++ src/tui/tcell.go | 5 +++-- src/tui/tui.go | 6 +++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/ansi.go b/src/ansi.go index f8fe56a..f550670 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -169,6 +169,8 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { } case 1: switch num { + case 2: + state256 = 10 // MAGIC case 5: state256++ default: @@ -177,8 +179,20 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState { case 2: *ptr = tui.Color(num) state256 = 0 + case 10: + *ptr = tui.Color(1<<24) | tui.Color(num<<16) + state256++ + case 11: + *ptr = *ptr | tui.Color(num<<8) + state256++ + case 12: + *ptr = *ptr | tui.Color(num) + state256 = 0 } } } + if state256 > 0 { + *ptr = -1 + } return state } diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 051a042..6a09b24 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -295,6 +295,10 @@ func RefreshWindows(windows []*Window) { } func PairFor(fg Color, bg Color) ColorPair { + // ncurses does not support 24-bit colors + if fg.is24() || bg.is24() { + return ColDefault + } key := (int(fg) << 8) + int(bg) if found, prs := _colorMap[key]; prs { return found diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 3fe1161..4a8f502 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -11,8 +11,9 @@ import ( "runtime" - "github.com/gdamore/tcell" - "github.com/gdamore/tcell/encoding" + // https://github.com/gdamore/tcell/pull/135 + "github.com/junegunn/tcell" + "github.com/junegunn/tcell/encoding" "github.com/junegunn/go-runewidth" ) diff --git a/src/tui/tui.go b/src/tui/tui.go index 6fcaaa4..125611c 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -93,7 +93,11 @@ const ( doubleClickDuration = 500 * time.Millisecond ) -type Color int16 +type Color int32 + +func (c Color) is24() bool { + return c > 0 && (c&(1<<24)) > 0 +} const ( colUndefined Color = -2 From dffef3d9f33b1de54944493c386e4891624a2a4b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 26 Nov 2016 11:41:57 +0900 Subject: [PATCH 328/658] Update build instructions for ncurses 6 and tcell Close #357 Close #738 --- src/README.md | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/README.md b/src/README.md index e8b8deb..9f7a558 100644 --- a/src/README.md +++ b/src/README.md @@ -61,6 +61,45 @@ make install make linux ``` +### With ncurses 6 + +The official binaries of fzf are built with ncurses 5 because it's widely +supported by different platforms. However ncurses 5 is old and has a number of +limitations. + +1. Does not support more than 256 color pairs (See [357][357]) +2. Does not support italics +3. Does not support 24-bit color + +[357]: https://github.com/junegunn/fzf/issues/357 + +But you can manually build fzf with ncurses 6 to overcome some of these +limitations. ncurses 6 supports up to 32767 color pairs (1), and supports +italics (2). To build fzf with ncurses 6, you have to install it first. On +macOS, you can use Homebrew to install it. + +```sh +brew install homebrew/dupes/ncurses +LDFLAGS="-L/usr/local/opt/ncurses/lib" make install +``` + +### With tcell + +[tcell][tcell] is a portable alternative to ncurses and we currently use it to +build Windows binaries. tcell has many benefits but most importantly, it +supports 24-bit colors. To build fzf with tcell: + +```sh +TAGS=tcell make install +``` + +However, note that tcell has its own issues. + +- Poor rendering performance compared to ncurses +- Does not support bracketed-paste mode +- Does not support italics unlike ncurses 6 +- Some wide characters are not correctly displayed + Test ---- @@ -88,6 +127,8 @@ Third-party libraries used - Licensed under [MIT](http://mattn.mit-license.org) - [mattn/go-isatty](https://github.com/mattn/go-isatty) - Licensed under [MIT](http://mattn.mit-license.org) +- [tcell](https://github.com/gdamore/tcell) + - Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE) License ------- @@ -99,4 +140,4 @@ License [gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock [ncurses]: https://www.gnu.org/software/ncurses/ [req]: http://golang.org/doc/install -[termbox]: https://github.com/nsf/termbox-go +[tcell]: https://github.com/gdamore/tcell From 3858086047e0de96ffe81ab966107b022c8adc2a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 26 Nov 2016 12:34:00 +0900 Subject: [PATCH 329/658] Always print scroll indicator in preview window --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 315d9f1..971cd2b 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -833,7 +833,7 @@ func (t *Terminal) printPreview() { } return t.pwindow.Fill(str) }) - if t.previewer.offset > 0 { + if t.previewer.lines > t.pwindow.Height { offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines) t.pwindow.Move(0, t.pwindow.Width-len(offset)) t.pwindow.CPrint(tui.ColInfo, tui.Reverse, offset) From a71c47140575b324afeb054a4fbc0e3fa505d4d0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 26 Nov 2016 12:25:27 +0900 Subject: [PATCH 330/658] 0.15.9 --- CHANGELOG.md | 15 +++++++++++++++ install | 4 ++-- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20ad20d..33a67a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ CHANGELOG ========= +0.15.9 +------ +- Fixed rendering glitches introduced in 0.15.8 +- The default escape delay is reduced to 50ms and is configurable via + `$ESCDELAY` +- Scroll indicator at the top-right corner of the preview window is always + displayed when there's overflow +- Can now be built with ncurses 6 or tcell to support extra features + - *ncurses 6* + - Supports more than 256 color pairs + - Supports italics + - *tcell* + - 24-bit color support + - See https://github.com/junegunn/fzf/blob/master/src/README.md#build + 0.15.8 ------ - Updated ANSI processor to handle more VT-100 escape sequences diff --git a/install b/install index 75524b9..1289a34 100755 --- a/install +++ b/install @@ -2,8 +2,8 @@ set -u -[[ "$@" =~ --pre ]] && version=0.15.8 pre=1 || - version=0.15.8 pre=0 +[[ "$@" =~ --pre ]] && version=0.15.9 pre=1 || + version=0.15.9 pre=0 auto_completion= key_bindings= diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 18cc1be..6f21139 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Nov 2016" "fzf 0.15.8" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Nov 2016" "fzf 0.15.9" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 2f107e7..4e19ce9 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Nov 2016" "fzf 0.15.8" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Nov 2016" "fzf 0.15.9" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 52884a7..b9def74 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.15.8" + version = "0.15.9" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 67026718c1d3d980f5cbe4a6caa137ce169768f9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 27 Nov 2016 15:16:53 +0900 Subject: [PATCH 331/658] Add BUILD.md --- BUILD.md | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 2 +- README.md | 5 +++ src/README.md | 50 +----------------------- 4 files changed, 113 insertions(+), 50 deletions(-) create mode 100644 BUILD.md diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..ca97e09 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,106 @@ +Building fzf +============ + +Build instructions +------------------ + +### Prerequisites + +- `go` executable in $PATH + +### Using Makefile + +Makefile will set up and use its own `$GOPATH` under the project root. + +```sh +# Source files are located in src directory +cd src + +# Build fzf binary for your platform in src/fzf +make + +# Build fzf binary and copy it to bin directory +make install + +# Build 32-bit and 64-bit executables and tarballs +make release + +# Build executables and tarballs for Linux using Docker +make linux +``` + +### Using `go get` + +Alternatively, you can build fzf directly with `go get` command without +cloning the repository. + +```sh +go get -u github.com/junegunn/fzf/src/fzf +``` + +Build options +------------- + +### With ncurses 6 + +The official binaries of fzf are built with ncurses 5 because it's widely +supported by different platforms. However ncurses 5 is old and has a number of +limitations. + +1. Does not support more than 256 color pairs (See [357][357]) +2. Does not support italics +3. Does not support 24-bit color + +[357]: https://github.com/junegunn/fzf/issues/357 + +But you can manually build fzf with ncurses 6 to overcome some of these +limitations. ncurses 6 supports up to 32767 color pairs (1), and supports +italics (2). To build fzf with ncurses 6, you have to install it first. On +macOS, you can use Homebrew to install it. + +```sh +brew install homebrew/dupes/ncurses +LDFLAGS="-L/usr/local/opt/ncurses/lib" make install +``` + +### With tcell + +[tcell][tcell] is a portable alternative to ncurses and we currently use it to +build Windows binaries. tcell has many benefits but most importantly, it +supports 24-bit colors. To build fzf with tcell: + +```sh +TAGS=tcell make install +``` + +However, note that tcell has its own issues. + +- Poor rendering performance compared to ncurses +- Does not support bracketed-paste mode +- Does not support italics unlike ncurses 6 +- Some wide characters are not correctly displayed + +Third-party libraries used +-------------------------- + +- [ncurses][ncurses] +- [mattn/go-runewidth](https://github.com/mattn/go-runewidth) + - Licensed under [MIT](http://mattn.mit-license.org) +- [mattn/go-shellwords](https://github.com/mattn/go-shellwords) + - Licensed under [MIT](http://mattn.mit-license.org) +- [mattn/go-isatty](https://github.com/mattn/go-isatty) + - Licensed under [MIT](http://mattn.mit-license.org) +- [tcell](https://github.com/gdamore/tcell) + - Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE) + +License +------- + +[MIT](LICENSE) + +[install]: https://github.com/junegunn/fzf#installation +[go]: https://golang.org/ +[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock +[ncurses]: https://www.gnu.org/software/ncurses/ +[req]: http://golang.org/doc/install +[tcell]: https://github.com/gdamore/tcell diff --git a/CHANGELOG.md b/CHANGELOG.md index 33a67a2..a37b74c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ CHANGELOG - Supports italics - *tcell* - 24-bit color support - - See https://github.com/junegunn/fzf/blob/master/src/README.md#build + - See https://github.com/junegunn/fzf/blob/master/BUILD.md 0.15.8 ------ diff --git a/README.md b/README.md index 9051f67..4535a48 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,11 @@ flawlessly. [wsl]: https://blogs.msdn.microsoft.com/wsl/ +Building fzf +------------ + +See [BUILD.md](BUILD.md). + Usage ----- diff --git a/src/README.md b/src/README.md index 9f7a558..2d065a9 100644 --- a/src/README.md +++ b/src/README.md @@ -50,55 +50,7 @@ starts much faster though the difference may not be noticeable. Build ----- -```sh -# Build fzf executables and tarballs -make release - -# Install the executable to ../bin directory -make install - -# Build executables and tarballs for Linux using Docker -make linux -``` - -### With ncurses 6 - -The official binaries of fzf are built with ncurses 5 because it's widely -supported by different platforms. However ncurses 5 is old and has a number of -limitations. - -1. Does not support more than 256 color pairs (See [357][357]) -2. Does not support italics -3. Does not support 24-bit color - -[357]: https://github.com/junegunn/fzf/issues/357 - -But you can manually build fzf with ncurses 6 to overcome some of these -limitations. ncurses 6 supports up to 32767 color pairs (1), and supports -italics (2). To build fzf with ncurses 6, you have to install it first. On -macOS, you can use Homebrew to install it. - -```sh -brew install homebrew/dupes/ncurses -LDFLAGS="-L/usr/local/opt/ncurses/lib" make install -``` - -### With tcell - -[tcell][tcell] is a portable alternative to ncurses and we currently use it to -build Windows binaries. tcell has many benefits but most importantly, it -supports 24-bit colors. To build fzf with tcell: - -```sh -TAGS=tcell make install -``` - -However, note that tcell has its own issues. - -- Poor rendering performance compared to ncurses -- Does not support bracketed-paste mode -- Does not support italics unlike ncurses 6 -- Some wide characters are not correctly displayed +See [BUILD.md](../BUILD.md) Test ---- From d9a51030eaeb601edb0a7d9673655a216045046d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 2 Dec 2016 21:07:23 +0900 Subject: [PATCH 332/658] [vim] Display relative path in prompt --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 96e30c4..49dc192 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -588,7 +588,7 @@ function! s:cmd(bang, ...) abort let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '/*$', '/', '') let opts.options .= ' --prompt '.shellescape(opts.dir) else - let opts.options .= ' --prompt '.shellescape(pathshorten(getcwd()).'/') + let opts.options .= ' --prompt '.shellescape(pathshorten(fnamemodify(getcwd(), ':~:.')).'/') endif let opts.options .= ' '.join(args) call fzf#run(fzf#wrap('FZF', opts, a:bang)) From ab9ae4f643ed47538bae6cebddc4c529f8700b45 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 3 Dec 2016 01:08:09 +0900 Subject: [PATCH 333/658] [vim] Fix path display in FZF when cwd is ~ --- plugin/fzf.vim | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 49dc192..0999964 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -581,6 +581,11 @@ let s:default_action = { \ 'ctrl-x': 'split', \ 'ctrl-v': 'vsplit' } +function! s:shortpath() + let short = pathshorten(fnamemodify(getcwd(), ':~:.')) + return empty(short) ? '~/' : short . (short =~ '/$' ? '' : '/') +endfunction + function! s:cmd(bang, ...) abort let args = copy(a:000) let opts = { 'options': '--multi ' } @@ -588,7 +593,7 @@ function! s:cmd(bang, ...) abort let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '/*$', '/', '') let opts.options .= ' --prompt '.shellescape(opts.dir) else - let opts.options .= ' --prompt '.shellescape(pathshorten(fnamemodify(getcwd(), ':~:.')).'/') + let opts.options .= ' --prompt '.shellescape(s:shortpath()) endif let opts.options .= ' '.join(args) call fzf#run(fzf#wrap('FZF', opts, a:bang)) From a442fe0fd0b42c76c5506e84cbf60ac1f8bec9c3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Dec 2016 02:13:47 +0900 Subject: [PATCH 334/658] Truncate long lines in preview window Add `:wrap` to --preview-window to wrap lines instead Close #756 --- src/options.go | 65 ++++++++++++++++++++++++--------------------- src/options_test.go | 19 ++++++++++--- src/terminal.go | 26 ++++++++++++++++-- src/tui/ncurses.go | 8 ++++++ src/tui/tcell.go | 10 ++++++- 5 files changed, 91 insertions(+), 37 deletions(-) diff --git a/src/options.go b/src/options.go index 694084e..6fd3f6c 100644 --- a/src/options.go +++ b/src/options.go @@ -66,7 +66,7 @@ const usage = `usage: fzf [options] Preview --preview=COMMAND Command to preview highlighted line ({}) --preview-window=OPT Preview window layout (default: right:50%) - [up|down|left|right][:SIZE[%]][:hidden] + [up|down|left|right][:SIZE[%]][:wrap][:hidden] Scripting -q, --query=STR Start the finder with the given query @@ -126,6 +126,7 @@ type previewOpts struct { position windowPosition size sizeSpec hidden bool + wrap bool } // Options stores the values of command-line options @@ -207,7 +208,7 @@ func defaultOptions() *Options { Expect: make(map[int]string), Keymap: make(map[int]actionType), Execmap: make(map[int]string), - Preview: previewOpts{"", posRight, sizeSpec{50, true}, false}, + Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false}, PrintQuery: false, ReadZero: false, Printer: func(str string) { fmt.Println(str) }, @@ -760,39 +761,43 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec { } func parsePreviewWindow(opts *previewOpts, input string) { - layout := input + // Default + opts.position = posRight + opts.size = sizeSpec{50, true} opts.hidden = false - if strings.HasSuffix(layout, ":hidden") { - opts.hidden = true - layout = strings.TrimSuffix(layout, ":hidden") - } + opts.wrap = false - tokens := strings.Split(layout, ":") - if len(tokens) == 0 || len(tokens) > 2 { - errorExit("invalid window layout: " + input) - } - - if len(tokens) > 1 { - opts.size = parseSize(tokens[1], 99, "window size") - } else { - opts.size = sizeSpec{50, true} + tokens := strings.Split(input, ":") + sizeRegex := regexp.MustCompile("^[1-9][0-9]*%?$") + for _, token := range tokens { + switch token { + case "hidden": + opts.hidden = true + case "wrap": + opts.wrap = true + case "up", "top": + opts.position = posUp + case "down", "bottom": + opts.position = posDown + case "left": + opts.position = posLeft + case "right": + opts.position = posRight + default: + if sizeRegex.MatchString(token) { + opts.size = parseSize(token, 99, "window size") + } else { + errorExit("invalid preview window layout: " + input) + } + } } if !opts.size.percent && opts.size.size > 0 { // Adjust size for border opts.size.size += 2 - } - - switch tokens[0] { - case "up": - opts.position = posUp - case "down": - opts.position = posDown - case "left": - opts.position = posLeft - case "right": - opts.position = posRight - default: - errorExit("invalid window position: " + input) + // And padding + if opts.position == posLeft || opts.position == posRight { + opts.size.size += 2 + } } } @@ -997,7 +1002,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[%]]")) + nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:wrap][:hidden]")) case "--no-margin": opts.Margin = defaultMargin() case "--margin": diff --git a/src/options_test.go b/src/options_test.go index 092efe4..07616fc 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -378,26 +378,37 @@ func TestPreviewOpts(t *testing.T) { opts := optsFor() if !(opts.Preview.command == "" && opts.Preview.hidden == false && + opts.Preview.wrap == false && opts.Preview.position == posRight && opts.Preview.size.percent == true && opts.Preview.size.size == 50) { t.Error() } - opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden") + opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden:wrap") if !(opts.Preview.command == "cat {}" && opts.Preview.hidden == true && + opts.Preview.wrap == true && opts.Preview.position == posLeft && opts.Preview.size.percent == false && - opts.Preview.size.size == 15+2) { + opts.Preview.size.size == 15+2+2) { t.Error(opts.Preview) } - - opts = optsFor("--preview-window=left:15:hidden", "--preview-window=down") + opts = optsFor("--preview-window=up:15:wrap:hidden", "--preview-window=down") if !(opts.Preview.command == "" && opts.Preview.hidden == false && + opts.Preview.wrap == false && opts.Preview.position == posDown && opts.Preview.size.percent == true && opts.Preview.size.size == 50) { t.Error(opts.Preview) } + opts = optsFor("--preview-window=up:15:wrap:hidden") + if !(opts.Preview.command == "" && + opts.Preview.hidden == true && + opts.Preview.wrap == true && + opts.Preview.position == posUp && + opts.Preview.size.percent == false && + opts.Preview.size.size == 15+2) { + t.Error(opts.Preview) + } } diff --git a/src/terminal.go b/src/terminal.go index 971cd2b..8a52980 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -488,7 +488,14 @@ func (t *Terminal) resizeWindows() { if t.isPreviewEnabled() { createPreviewWindow := func(y int, x int, w int, h int) { t.bwindow = tui.NewWindow(y, x, w, h, true) - t.pwindow = tui.NewWindow(y+1, x+2, w-4, h-2, false) + pwidth := w - 4 + // ncurses auto-wraps the line when the cursor reaches the right-end of + // the window. To prevent unintended line-wraps, we use the width one + // column larger than the desired value. + if !t.preview.wrap && tui.DoesAutoWrap() { + pwidth += 1 + } + t.pwindow = tui.NewWindow(y+1, x+2, pwidth, h-2, false) } switch t.preview.position { case posUp: @@ -657,7 +664,7 @@ func trimRight(runes []rune, width int) ([]rune, int) { l := 0 for idx, r := range runes { l += runeWidth(r, l) - if idx > 0 && l > width { + if l > width { return runes[:idx], len(runes) - idx } } @@ -828,6 +835,21 @@ func (t *Terminal) printPreview() { return true } } + if !t.preview.wrap { + lines := strings.Split(str, "\n") + for i, line := range lines { + limit := t.pwindow.Width + if tui.DoesAutoWrap() { + limit -= 1 + } + if i == 0 { + limit -= t.pwindow.X() + } + trimmed, _ := trimRight([]rune(line), limit) + lines[i] = string(trimmed) + } + str = strings.Join(lines, "\n") + } if ansi != nil && ansi.colored() { return t.pwindow.CFill(str, ansi.fg, ansi.bg, ansi.attr) } diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 6a09b24..2c1947f 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -273,6 +273,14 @@ func (w *Window) Erase() { C.werase(w.win()) } +func (w *Window) X() int { + return int(C.getcurx(w.win())) +} + +func DoesAutoWrap() bool { + return true +} + func (w *Window) Fill(str string) bool { return C.waddstr(w.win(), C.CString(str)) == C.OK } diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 4a8f502..1793836 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -172,6 +172,14 @@ func (w *Window) win() *WindowTcell { return (*WindowTcell)(w.impl) } +func (w *Window) X() int { + return w.impl.LastX +} + +func DoesAutoWrap() bool { + return false +} + func Clear() { _screen.Sync() _screen.Clear() @@ -521,7 +529,7 @@ func (w *Window) FillString(text string, pair ColorPair, a Attr) bool { var xPos = w.Left + w.win().LastX + lx // word wrap: - if xPos > (w.Left + w.Width) { + if xPos >= (w.Left + w.Width) { w.win().LastY++ w.win().LastX = 0 lx = 0 From 16b5902aa278639fd99be89abe8ac50eb49b34aa Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Dec 2016 02:27:38 +0900 Subject: [PATCH 335/658] Fix Linux build (#756) --- src/tui/ncurses.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 2c1947f..7f515b2 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -17,6 +17,10 @@ FILE* c_tty() { SCREEN* c_newterm(FILE* tty) { return newterm(NULL, stderr, tty); } + +int c_getcurx(WINDOW* win) { + return getcurx(win); +} */ import "C" @@ -274,7 +278,7 @@ func (w *Window) Erase() { } func (w *Window) X() int { - return int(C.getcurx(w.win())) + return int(C.c_getcurx(w.win())) } func DoesAutoWrap() bool { From c60bfb2b0f96b50790f66c2d534e17098e6e690a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 11 Dec 2016 22:32:40 +0900 Subject: [PATCH 336/658] [neovim] Keep alternate file unchanged Close https://github.com/junegunn/fzf.vim/issues/265 --- plugin/fzf.vim | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 0999964..065701c 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -430,7 +430,7 @@ function! s:split(dict) let ppos = s:getpos() try if s:present(a:dict, 'window') - execute a:dict.window + execute 'keepalt' a:dict.window elseif !s:splittable(a:dict) execute (tabpagenr()-1).'tabnew' else @@ -457,15 +457,14 @@ endfunction function! s:execute_term(dict, command, temps) abort let winrest = winrestcmd() + let pbuf = bufnr('') let [ppos, winopts] = s:split(a:dict) - let fzf = { 'buf': bufnr('%'), 'ppos': ppos, 'dict': a:dict, 'temps': a:temps, + let fzf = { 'buf': bufnr(''), 'pbuf': pbuf, 'ppos': ppos, 'dict': a:dict, 'temps': a:temps, \ 'winopts': winopts, 'winrest': winrest, 'lines': &lines, \ 'columns': &columns, 'command': a:command } function! fzf.switch_back(inplace) if a:inplace && bufnr('') == self.buf - " FIXME: Can't re-enter normal mode from terminal mode - " execute "normal! \" - b # + execute 'keepalt b' self.pbuf " No other listed buffer if bufnr('') == self.buf enew From 8a502af4c126bc5649450d25d9c847da2711a739 Mon Sep 17 00:00:00 2001 From: Marco Hinz Date: Tue, 13 Dec 2016 17:56:53 +0100 Subject: [PATCH 337/658] Neovim: event handlers always expect three arguments (#768) --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 065701c..0cce53a 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -471,7 +471,7 @@ function! s:execute_term(dict, command, temps) abort endif endif endfunction - function! fzf.on_exit(id, code) + function! fzf.on_exit(id, code, _event) if s:getpos() == self.ppos " {'window': 'enew'} for [opt, val] in items(self.winopts) execute 'let' opt '=' val From 0508e70f9bf941e3429668fcef955919a71e03c2 Mon Sep 17 00:00:00 2001 From: Pierre Neidhardt Date: Wed, 14 Dec 2016 12:07:27 +0530 Subject: [PATCH 338/658] Overhaul fish functions (#759) Replace the "temp file" workaround with the "read" function: it's simpler and faster. Use proper escaping, remove the custom function. The "file" widget uses last token as root for the "find" command. This replaces the equivalent of '**' completion in bash/zsh. The "$dir" non-expanded variable can be used in FZF_CTRL_T_COMMAND to set the root. --- README.md | 31 ++++++++++++++++-- shell/key-bindings.fish | 69 +++++++++++++++++++++++------------------ 2 files changed, 66 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 4535a48..6381279 100644 --- a/README.md +++ b/README.md @@ -434,11 +434,36 @@ export FZF_DEFAULT_COMMAND=' It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362) that it doesn't allow reading from STDIN in command substitution, which means -simple `vim (fzf)` won't work as expected. The workaround is to store the result -of fzf to a temporary file. +simple `vim (fzf)` won't work as expected. The workaround is to use the `read` +fish command: ```sh -fzf > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result) +fzf | read -l result; and vim $result +``` + +or, for multiple results: + +```sh +fzf -m | while read -l r; set result $result $r; end; and vim $result +``` + +The globbing system is different in fish and thus `**` completion will not work. +However, the `CTRL-T` command will use the last token on the commandline as the +root folder for the recursive search. For instance, hitting `CTRL-T` at the end +of the following commandline + +```sh +ls /var/ +``` + +will list all files and folders under `/var/`. + +When using a custom `FZF_CTRL_T_COMMAND`, use the unexpanded `$dir` variable to +make use of this feature. `$dir` defaults to `.` when the last token is not a +valid directory. Example: + +```sh +set -l FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'" ``` License diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index b63ee89..b520d8b 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -1,52 +1,60 @@ # Key bindings # ------------ function fzf_key_bindings - # Due to a bug of fish, we cannot use command substitution, - # so we use temporary file instead - if [ -z "$TMPDIR" ] - set -g TMPDIR /tmp - end - function __fzf_escape - while read item - echo -n (echo -n "$item" | sed -E 's/([ "$~'\''([{<>})])/\\\\\\1/g')' ' + # Store last token in $dir as root for the 'find' command + function fzf-file-widget -d "List files and folders" + set -l dir (commandline -t) + # The commandline token might be escaped, we need to unescape it. + set dir (eval "printf '%s' $dir") + if [ ! -d "$dir" ] + set dir . end - end + # Some 'find' versions print undesired duplicated slashes if the path ends with slashes. + set dir (string replace --regex '(.)/+$' '$1' "$dir") - function fzf-file-widget + # "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not + # $dir itself, even if hidden. set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND " - command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ - -o -type f -print \ - -o -type d -print \ - -o -type l -print 2> /dev/null | sed 1d | cut -b3-" - eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m $FZF_CTRL_T_OPTS > $TMPDIR/fzf.result" - and for i in (seq 20); commandline -i (cat $TMPDIR/fzf.result | __fzf_escape) 2> /dev/null; and break; sleep 0.1; end + command find -L \$dir \\( -path \$dir'*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ + -o -type f -print \ + -o -type d -print \ + -o -type l -print 2> /dev/null | sed '1d; s#^\./##'" + + eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m $FZF_CTRL_T_OPTS" | while read -l r; set result $result $r; end + if [ -z "$result" ] + commandline -f repaint + return + end + + if [ "$dir" != . ] + # Remove last token from commandline. + commandline -t "" + end + for i in $result + commandline -it -- (string escape $i) + commandline -it -- ' ' + end commandline -f repaint - rm -f $TMPDIR/fzf.result end - function fzf-history-widget - history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q '(commandline)' > $TMPDIR/fzf.result - and commandline -- (cat $TMPDIR/fzf.result) + function fzf-history-widget -d "Show command history" + history | eval (__fzfcmd) +s +m --tiebreak=index $FZF_CTRL_R_OPTS -q '(commandline)' | read -l result + and commandline -- $result commandline -f repaint - rm -f $TMPDIR/fzf.result end - function fzf-cd-widget + function fzf-cd-widget -d "Change directory" set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND " - command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ - -o -type d -print 2> /dev/null | sed 1d | cut -b3-" - # Fish hangs if the command before pipe redirects (2> /dev/null) - eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m $FZF_ALT_C_OPTS > $TMPDIR/fzf.result" - [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ] - and cd (cat $TMPDIR/fzf.result) + command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ + -o -type d -print 2> /dev/null | sed 1d | cut -b3-" + eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m $FZF_ALT_C_OPTS" | read -l result + [ "$result" ]; and cd $result commandline -f repaint - rm -f $TMPDIR/fzf.result end function __fzfcmd set -q FZF_TMUX; or set FZF_TMUX 1 - if [ $FZF_TMUX -eq 1 ] if set -q FZF_TMUX_HEIGHT echo "fzf-tmux -d$FZF_TMUX_HEIGHT" @@ -68,4 +76,3 @@ function fzf_key_bindings bind -M insert \ec fzf-cd-widget end end - From 97330ee8fc2c07c04ad3fe4746140d476347ee5c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 17 Dec 2016 11:20:28 +0900 Subject: [PATCH 339/658] No need to set MANPATH Close #774 --- install | 6 ------ 1 file changed, 6 deletions(-) diff --git a/install b/install index 1289a34..58a42e3 100755 --- a/install +++ b/install @@ -301,12 +301,6 @@ if [[ ! "\$PATH" == *$fzf_base/bin* ]]; then export PATH="\$PATH:$fzf_base/bin" fi -# Man path -# -------- -if [[ ! "\$MANPATH" == *$fzf_base/man* && -d "$fzf_base/man" ]]; then - export MANPATH="\$MANPATH:$fzf_base/man" -fi - # Auto-completion # --------------- $fzf_completion From 847c512539f9909ae69a5067c1a64cb9bb485ea3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 18 Dec 2016 18:51:19 +0100 Subject: [PATCH 340/658] s:execute_term: switch_back: check that self.pbuf exists (#776) With a `bufhidden=wipe` buffer (e.g. vim-startify) the buffer would not exist anymore, resulting in an error. --- plugin/fzf.vim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 0cce53a..2574a0f 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -464,7 +464,9 @@ function! s:execute_term(dict, command, temps) abort \ 'columns': &columns, 'command': a:command } function! fzf.switch_back(inplace) if a:inplace && bufnr('') == self.buf - execute 'keepalt b' self.pbuf + if bufexists(self.pbuf) + execute 'keepalt b' self.pbuf + endif " No other listed buffer if bufnr('') == self.buf enew From c7b076400297e53b3fdb9bb0605b0c688a39fa40 Mon Sep 17 00:00:00 2001 From: Pierre Neidhardt Date: Sat, 24 Dec 2016 09:23:07 +0530 Subject: [PATCH 341/658] [shell] Use '-mindepth 1' to omit root folder in 'find' output (#779) This removes the need for the 'sed' call. Faster, cleaner. --- shell/key-bindings.bash | 4 ++-- shell/key-bindings.fish | 4 ++-- shell/key-bindings.zsh | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index d41eb5b..8c3e6f7 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -1,10 +1,10 @@ # Key bindings # ------------ __fzf_select__() { - local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ + local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type f -print \ -o -type d -print \ - -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" + -o -type l -print 2> /dev/null | cut -b3-"}" eval "$cmd | fzf -m $FZF_CTRL_T_OPTS" | while read -r item; do printf '%q ' "$item" done diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index b520d8b..dd75fec 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -16,10 +16,10 @@ function fzf_key_bindings # "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not # $dir itself, even if hidden. set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND " - command find -L \$dir \\( -path \$dir'*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ + command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ -o -type f -print \ -o -type d -print \ - -o -type l -print 2> /dev/null | sed '1d; s#^\./##'" + -o -type l -print 2> /dev/null | sed 's#^\./##'" eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m $FZF_CTRL_T_OPTS" | while read -l r; set result $result $r; end if [ -z "$result" ] diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index fc694cb..fed0153 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -4,10 +4,10 @@ if [[ $- == *i* ]]; then # CTRL-T - Paste the selected file path(s) into the command line __fsel() { - local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ + local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type f -print \ -o -type d -print \ - -o -type l -print 2> /dev/null | sed 1d | cut -b3-"}" + -o -type l -print 2> /dev/null | cut -b3-"}" setopt localoptions pipefail 2> /dev/null eval "$cmd | $(__fzfcmd) -m $FZF_CTRL_T_OPTS" | while read item; do echo -n "${(q)item} " From 7b0d9e1e077dfc271b98e24e99961619a8ea00cf Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 27 Dec 2016 01:34:55 +0900 Subject: [PATCH 342/658] Apply --tabstop to preview window --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 8a52980..5b482b0 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -846,7 +846,7 @@ func (t *Terminal) printPreview() { limit -= t.pwindow.X() } trimmed, _ := trimRight([]rune(line), limit) - lines[i] = string(trimmed) + lines[i], _ = processTabs(trimmed, 0) } str = strings.Join(lines, "\n") } From 73eacf11371ba8ce8c996b8569e8752637bcb633 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 31 Dec 2016 00:30:00 +0900 Subject: [PATCH 343/658] [bash-completion] Always backup existing completion definitions _fzf_completion_loaded is no longer checked. This change increases the load time by a few milliseconds, but I can't think of a better way to handle the issue. Close #783. --- shell/completion.bash | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 31ebc60..fc8471b 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -261,12 +261,9 @@ a_cmds=" x_cmds="kill ssh telnet unset unalias export" # Preserve existing completion -if [ "$_fzf_completion_loaded" != '0.11.3' ]; then - # Really wish I could use associative array but OSX comes with bash 3.2 :( - eval $(complete | command grep '\-F' | command grep -v _fzf_ | - command grep -E " ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g' | sed 's/+/\\+/g'))$" | _fzf_orig_completion_filter) - export _fzf_completion_loaded=0.11.3 -fi +eval $(complete | + sed -E '/-F/!d; / _fzf/d; '"/ ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g; s/+/\\+/g'))$/"'!d' | + _fzf_orig_completion_filter) if type _completion_loader > /dev/null 2>&1; then _fzf_completion_loader=1 From 45faad7e0439adfc8551d69e8f4b9bd65169fabc Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 1 Jan 2017 02:23:20 +0900 Subject: [PATCH 344/658] [bash] Addendum fix for #580 --- shell/key-bindings.bash | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 8c3e6f7..33d8bc8 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -76,9 +76,9 @@ if [[ ! -o vi ]]; then if [ $__use_bind_x -eq 1 ]; then bind -x '"\C-t": "fzf-file-widget"' elif [ $__use_tmux -eq 1 ]; then - bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select_tmux__)\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"' + bind '"\C-t": " \C-u \C-a\C-k`__fzf_select_tmux__`\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"' else - bind '"\C-t": " \C-u \C-a\C-k$(__fzf_select__)\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"' + bind '"\C-t": " \C-u \C-a\C-k`__fzf_select__`\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"' fi # CTRL-R - Paste the selected command from history into the command line @@ -105,19 +105,19 @@ else if [ $__use_bind_x -eq 1 ]; then bind -x '"\C-t": "fzf-file-widget"' elif [ $__use_tmux -eq 1 ]; then - bind '"\C-t": "\C-x\C-a$a \C-x\C-addi$(__fzf_select_tmux__)\C-x\C-e\C-x\C-a0P$xa"' + bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select_tmux__`\C-x\C-e\C-x\C-a0P$xa"' else - bind '"\C-t": "\C-x\C-a$a \C-x\C-addi$(__fzf_select__)\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "' + bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select__`\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "' fi bind -m vi-command '"\C-t": "i\C-t"' # CTRL-R - Paste the selected command from history into the command line - bind '"\C-r": "\C-x\C-addi$(__fzf_history__)\C-x\C-e\C-x^\C-x\C-a$a\C-x\C-r"' + bind '"\C-r": "\C-x\C-addi`__fzf_history__`\C-x\C-e\C-x^\C-x\C-a$a\C-x\C-r"' bind -m vi-command '"\C-r": "i\C-r"' # ALT-C - cd into the selected directory - bind '"\ec": "\C-x\C-addi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"' - bind -m vi-command '"\ec": "ddi$(__fzf_cd__)\C-x\C-e\C-x\C-r\C-m"' + bind '"\ec": "\C-x\C-addi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"' + bind -m vi-command '"\ec": "ddi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"' fi unset -v __use_tmux __use_bind_x From 42a2371d264b4f9b33d0e14847766c49b045df53 Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Sat, 31 Dec 2016 21:48:15 -0500 Subject: [PATCH 345/658] [vim] Use cmd.exe in Windows (#785) --- plugin/fzf.vim | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 2574a0f..a873a0c 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -80,7 +80,13 @@ function! s:shellesc(arg) endfunction function! s:escape(path) - return escape(a:path, ' $%#''"\') + let escaped_chars = '$%#''"' + + if has('unix') + let escaped_chars .= ' \' + endif + + return escape(a:path, escaped_chars) endfunction " Upgrade legacy options @@ -234,7 +240,15 @@ endfunction function! fzf#run(...) abort try let oshell = &shell - set shell=sh + let useshellslash = &shellslash + + if has('win32') || has('win64') + set shell=cmd.exe + set shellslash + else + set shell=sh + endif + if has('nvim') && len(filter(range(1, bufnr('$')), 'bufname(v:val) =~# ";#FZF"')) call s:warn('FZF is already running!') return [] @@ -251,7 +265,7 @@ try if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) let temps.source = tempname() call writefile(split($FZF_DEFAULT_COMMAND, "\n"), temps.source) - let dict.source = (empty($SHELL) ? 'sh' : $SHELL) . ' ' . s:shellesc(temps.source) + let dict.source = (empty($SHELL) ? &shell : $SHELL) . ' ' . s:shellesc(temps.source) endif if has_key(dict, 'source') @@ -281,6 +295,7 @@ try return lines finally let &shell = oshell + let &shellslash = useshellslash endtry endfunction @@ -353,7 +368,11 @@ function! s:xterm_launcher() \ &columns, &lines/2, getwinposx(), getwinposy()) endfunction unlet! s:launcher -let s:launcher = function('s:xterm_launcher') +if has('win32') || has('win64') + let s:launcher = 'cmd.exe /C %s' +else + let s:launcher = function('s:xterm_launcher') +endif function! s:exit_handler(code, command, ...) if a:code == 130 @@ -370,12 +389,17 @@ endfunction function! s:execute(dict, command, temps) abort call s:pushd(a:dict) - silent! !clear 2> /dev/null + if has('unix') + silent! !clear 2> /dev/null + endif let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#') if has('gui_running') let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher))) let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher - let command = printf(fmt, "'".substitute(escaped, "'", "'\"'\"'", 'g')."'") + if has('unix') + let escaped = "'".substitute(escaped, "'", "'\"'\"'", 'g')."'" + endif + let command = printf(fmt, escaped) else let command = escaped endif From 6c0fd7f9ca663f2057a114f1aa86bfe5f6a78c68 Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Sat, 31 Dec 2016 23:11:06 -0500 Subject: [PATCH 346/658] [vim] FZF command to handle Windows paths with spaces - Use noshellslash for strict path expansion in fzf#run and s:cmd (shellescape depends on shellslash) - Double-quote the fzf command for cmd.exe - Add fzf#shellescape to encapsulate the logic - Close #786 --- plugin/fzf.vim | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index a873a0c..819a6c9 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -237,6 +237,19 @@ function! fzf#wrap(...) return opts endfunction +function! fzf#shellescape(path) + if has('win32') || has('win64') + let shellslash = &shellslash + try + set noshellslash + return shellescape(a:path) + finally + let &shellslash = shellslash + endtry + endif + return shellescape(a:path) +endfunction + function! fzf#run(...) abort try let oshell = &shell @@ -244,7 +257,7 @@ try if has('win32') || has('win64') set shell=cmd.exe - set shellslash + set noshellslash else set shell=sh endif @@ -398,6 +411,8 @@ function! s:execute(dict, command, temps) abort let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher if has('unix') let escaped = "'".substitute(escaped, "'", "'\"'\"'", 'g')."'" + elseif has('win32') || has('win64') + let escaped = '"'.(escaped).'"' endif let command = printf(fmt, escaped) else @@ -615,10 +630,10 @@ function! s:cmd(bang, ...) abort let args = copy(a:000) let opts = { 'options': '--multi ' } if len(args) && isdirectory(expand(args[-1])) - let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '/*$', '/', '') - let opts.options .= ' --prompt '.shellescape(opts.dir) + let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '') + let opts.options .= ' --prompt '.fzf#shellescape(opts.dir) else - let opts.options .= ' --prompt '.shellescape(s:shortpath()) + let opts.options .= ' --prompt '.fzf#shellescape(s:shortpath()) endif let opts.options .= ' '.join(args) call fzf#run(fzf#wrap('FZF', opts, a:bang)) From 36702737191c2fc26290dee531095f7d66f3461e Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Wed, 4 Jan 2017 00:07:01 -0500 Subject: [PATCH 347/658] [vim] Use cmd.exe directly on GVim (launcher='%s') (#787) --- plugin/fzf.vim | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 819a6c9..06af648 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -382,7 +382,7 @@ function! s:xterm_launcher() endfunction unlet! s:launcher if has('win32') || has('win64') - let s:launcher = 'cmd.exe /C %s' + let s:launcher = '%s' else let s:launcher = function('s:xterm_launcher') endif @@ -411,8 +411,6 @@ function! s:execute(dict, command, temps) abort let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher if has('unix') let escaped = "'".substitute(escaped, "'", "'\"'\"'", 'g')."'" - elseif has('win32') || has('win64') - let escaped = '"'.(escaped).'"' endif let command = printf(fmt, escaped) else From fd137a9e875ba1fd9feed4903e102951f8098c33 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 7 Jan 2017 22:46:34 +0900 Subject: [PATCH 348/658] [bash/zsh-completion] Filter ~/.ssh/known_hosts Close #791 --- shell/completion.bash | 2 +- shell/completion.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index fc8471b..392aee2 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -221,7 +221,7 @@ _fzf_complete_telnet() { _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \ - <(command grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ + <(command grep -oE '^[a-z0-9.,-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u ) diff --git a/shell/completion.zsh b/shell/completion.zsh index 0be5ad1..d3faef8 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -112,7 +112,7 @@ _fzf_complete_telnet() { _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \ - <(command grep -oE '^[^ ]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ + <(command grep -oE '^[a-z0-9.,-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u ) From 1448d631a7c72905f62dbb343a8f231a1c3cc52c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 8 Jan 2017 01:30:31 +0900 Subject: [PATCH 349/658] Add --height option --- .travis.yml | 2 +- CHANGELOG.md | 6 + README.md | 36 +- bin/fzf-tmux | 3 + man/man1/fzf.1 | 9 +- plugin/fzf.vim | 26 +- shell/completion.bash | 57 +-- shell/completion.zsh | 14 +- shell/key-bindings.bash | 33 +- shell/key-bindings.fish | 13 +- shell/key-bindings.zsh | 10 +- src/options.go | 18 + src/result.go | 2 +- src/result_test.go | 15 +- src/terminal.go | 225 +++++++----- src/tui/light.go | 764 +++++++++++++++++++++++++++++++++++++++ src/tui/ncurses.go | 248 ++++++------- src/tui/tcell.go | 275 +++++++------- src/tui/tui.go | 170 +++++++-- src/tui/tui_test.go | 14 - src/util/util.go | 16 + src/util/util_unix.go | 6 + src/util/util_windows.go | 6 + test/test_go.rb | 264 +++++++------- 24 files changed, 1624 insertions(+), 608 deletions(-) create mode 100644 src/tui/light.go delete mode 100644 src/tui/tui_test.go diff --git a/.travis.yml b/.travis.yml index a1a6497..3f2a67f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: ruby matrix: include: - env: TAGS= - rvm: 2.2.0 + rvm: 2.3.3 # - env: TAGS=tcell # rvm: 2.2.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index a37b74c..f78270c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +0.16.0 +------ +- Added `--height HEIGHT[%]` option +- Preview window will truncate long lines by default. Line wrap can be enabled + by `:wrap` flag in `--preview-window`. + 0.15.9 ------ - Fixed rendering glitches introduced in 0.15.8 diff --git a/README.md b/README.md index 6381279..550ea77 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,29 @@ vim $(fzf) - Mouse: scroll, click, double-click; shift-click and shift-scroll on multi-select mode +#### Layout + +fzf by default starts in fullscreen mode, but you can make it start below the +cursor with `--height` option. + +```sh +vim $(fzf --height 40%) +``` + +Also check out `--reverse` option if you prefer "top-down" layout instead of +the default "bottom-up" layout. + +```sh +vim $(fzf --height 40% --reverse) +``` + +You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by +default. + +```sh +export FZF_DEFAULT_OPTS='--height 40% --reverse' +``` + #### Search syntax Unless otherwise specified, fzf starts in "extended-search mode" where you can @@ -189,6 +212,13 @@ cat /usr/share/dict/words | fzf-tmux -l 20% --multi --reverse It will still work even when you're not on tmux, silently ignoring `-[udlr]` options, so you can invariably use `fzf-tmux` in your scripts. +Alternatively, you can use `--height HEIGHT[%]` option not to start fzf in +fullscreen mode. + +```sh +fzf --height 40% +``` + Key bindings for command line ----------------------------- @@ -206,9 +236,9 @@ fish. - Set `FZF_ALT_C_COMMAND` to override the default command - Set `FZF_ALT_C_OPTS` to pass additional options -If you're on a tmux session, fzf will start in a split pane. You may disable -this tmux integration by setting `FZF_TMUX` to 0, or change the height of the -pane with `FZF_TMUX_HEIGHT` (e.g. `20`, `50%`). +If you're on a tmux session, you can start fzf in a split pane by setting +`FZF_TMUX` to 1, and change the height of the pane with `FZF_TMUX_HEIGHT` +(e.g. `20`, `50%`). If you use vi mode on bash, you need to add `set -o vi` *before* `source ~/.fzf.bash` in your .bashrc, so that it correctly sets up key bindings for vi diff --git a/bin/fzf-tmux b/bin/fzf-tmux index dd335d2..f201199 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -114,6 +114,9 @@ if [[ -z "$TMUX" || "$opt" =~ ^-h && "$columns" -le 40 || ! "$opt" =~ ^-h && "$l exit $? fi +# --height option is not allowed +args+=("--no-height") + # Handle zoomed tmux pane by moving it to a temp window if tmux list-panes -F '#F' | grep -q Z; then zoomed=1 diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 4e19ce9..fac2aab 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -130,6 +130,10 @@ on the center of the screen. Label characters for \fBjump\fR and \fBjump-accept\fR .SS Layout .TP +.BI "--height=" "HEIGHT[%]" +Display fzf window below the cursor with the given height instead of using +fullscreen. +.TP .B "--reverse" Reverse orientation .TP @@ -248,10 +252,11 @@ e.g. \fBfzf --preview="head -$LINES {}"\fR Note that you can escape a placeholder pattern by prepending a backslash. .RE .TP -.BI "--preview-window=" "[POSITION][:SIZE[%]][:hidden]" +.BI "--preview-window=" "[POSITION][:SIZE[%]][:wrap][:hidden]" Determine the layout of the preview window. If the argument ends with \fB:hidden\fR, the preview window will be hidden by default until -\fBtoggle-preview\fR action is triggered. +\fBtoggle-preview\fR action is triggered. Long lines are truncated by default. +Line wrap can be enabled with \fB:wrap\fR flag. .RS .B POSITION: (default: right) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 06af648..c8c6c91 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -296,14 +296,24 @@ try else let prefix = '' endif - let tmux = (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict) + + let use_height = has_key(dict, 'down') && + \ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right')) + let tmux = !use_height && (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict) + let term = has('nvim') && !tmux + if use_height + let optstr .= ' --height='.s:calc_size(&lines, dict.down, dict) + elseif term + let optstr .= ' --no-height' + endif let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result - if has('nvim') && !tmux + if term return s:execute_term(dict, command, temps) endif - let lines = tmux ? s:execute_tmux(dict, command, temps) : s:execute(dict, command, temps) + let lines = tmux ? s:execute_tmux(dict, command, temps) + \ : s:execute(dict, command, use_height, temps) call s:callback(dict, lines) return lines finally @@ -400,9 +410,9 @@ function! s:exit_handler(code, command, ...) return 1 endfunction -function! s:execute(dict, command, temps) abort +function! s:execute(dict, command, use_height, temps) abort call s:pushd(a:dict) - if has('unix') + if has('unix') && !a:use_height silent! !clear 2> /dev/null endif let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#') @@ -416,7 +426,11 @@ function! s:execute(dict, command, temps) abort else let command = escaped endif - execute 'silent !'.command + if a:use_height + call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s 2> /dev/tty', &lines, command)) + else + execute 'silent !'.command + endif let exit_status = v:shell_error redraw! return s:exit_handler(exit_status, command) ? s:collect(a:temps) : [] diff --git a/shell/completion.bash b/shell/completion.bash index 392aee2..d6d7238 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -5,7 +5,7 @@ # / __/ / /_/ __/ # /_/ /___/_/-completion.bash # -# - $FZF_TMUX (default: 1) +# - $FZF_TMUX (default: 0) # - $FZF_TMUX_HEIGHT (default: '40%') # - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_OPTS (default: empty) @@ -30,6 +30,15 @@ fi ########################################################### +# To redraw line after fzf closes (printf '\e[5n') +bind '"\e[0n": redraw-current-line' + +__fzfcmd_complete() { + [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] && + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || + echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse" +} + _fzf_orig_completion_filter() { sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' | awk -F= '{gsub(/[^A-Za-z0-9_= ;]/, "_", $1); print $1"="$2}' @@ -43,35 +52,42 @@ _fzf_opts_completion() { opts=" -x --extended -e --exact + --algo -i +i -n --nth + --with-nth -d --delimiter +s --no-sort --tac --tiebreak - --bind -m --multi --no-mouse - --color - --black - --reverse + --bind + --cycle --no-hscroll + --jump-labels + --height + --reverse + --margin --inline-info --prompt + --header + --header-lines + --ansi + --tabstop + --color + --no-bold + --history + --history-size + --preview + --preview-window -q --query -1 --select-1 -0 --exit-0 -f --filter --print-query --expect - --toggle-sort - --sync - --cycle - --history - --history-size - --header - --header-lines - --margin" + --sync" case "${prev}" in --tiebreak) @@ -116,7 +132,7 @@ _fzf_handle_dynamic_completion() { __fzf_generic_path_completion() { local cur base dir leftover matches trigger cmd fzf - [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + fzf="$(__fzfcmd_complete)" cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}" COMPREPLY=() trigger=${FZF_COMPLETION_TRIGGER-'**'} @@ -132,7 +148,6 @@ __fzf_generic_path_completion() { leftover=${leftover/#\/} [ -z "$dir" ] && dir='.' [ "$dir" != "/" ] && dir="${dir/%\//}" - tput sc matches=$(eval "$1 $(printf %q "$dir")" | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read -r item; do printf "%q$3 " "$item" done) @@ -142,7 +157,7 @@ __fzf_generic_path_completion() { else COMPREPLY=( "$cur" ) fi - tput rc + printf '\e[5n' return 0 fi dir=$(dirname "$dir") @@ -160,7 +175,7 @@ _fzf_complete() { local cur selected trigger cmd fzf post post="$(caller 0 | awk '{print $2}')_post" type -t "$post" > /dev/null 2>&1 || post=cat - [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + fzf="$(__fzfcmd_complete)" cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}" trigger=${FZF_COMPLETION_TRIGGER-'**'} @@ -168,10 +183,9 @@ _fzf_complete() { if [[ "$cur" == *"$trigger" ]]; then cur=${cur:0:${#cur}-${#trigger}} - tput sc selected=$(cat | $fzf $FZF_COMPLETION_OPTS $1 -q "$cur" | $post | tr '\n' ' ') selected=${selected% } # Strip trailing space not to repeat "-o nospace" - tput rc + printf '\e[5n' if [ -n "$selected" ]; then COMPREPLY=("$selected") @@ -200,10 +214,9 @@ _fzf_complete_kill() { [ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1 local selected fzf - [ "${FZF_TMUX:-1}" != 0 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" - tput sc + fzf="$(__fzfcmd_complete)" selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ') - tput rc + printf '\e[5n' if [ -n "$selected" ]; then COMPREPLY=( "$selected" ) diff --git a/shell/completion.zsh b/shell/completion.zsh index d3faef8..fb2c16a 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -5,7 +5,7 @@ # / __/ / /_/ __/ # /_/ /___/_/-completion.zsh # -# - $FZF_TMUX (default: 1) +# - $FZF_TMUX (default: 0) # - $FZF_TMUX_HEIGHT (default: '40%') # - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_OPTS (default: empty) @@ -30,6 +30,12 @@ fi ########################################################### +__fzfcmd_complete() { + [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] && + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || + echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse" +} + __fzf_generic_path_completion() { local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches # (Q) flag removes a quoting level: "foo\ bar" => "foo bar" @@ -39,7 +45,7 @@ __fzf_generic_path_completion() { fzf_opts=$4 suffix=$5 tail=$6 - [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + fzf="$(__fzfcmd_complete)" setopt localoptions nonomatch dir="$base" @@ -90,7 +96,7 @@ _fzf_complete() { post="${funcstack[2]}_post" type $post > /dev/null 2>&1 || post=cat - [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + fzf="$(__fzfcmd_complete)" _fzf_feed_fifo "$fifo" matches=$(cat "$fifo" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') @@ -157,7 +163,7 @@ fzf-completion() { tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} # Kill completion (do not require trigger sequence) if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then - [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf" + fzf="$(__fzfcmd_complete)" matches=$(ps -ef | sed 1d | ${=fzf} ${=FZF_COMPLETION_OPTS} -m | awk '{print $2}' | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$LBUFFER$matches" diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 33d8bc8..382302d 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -5,7 +5,7 @@ __fzf_select__() { -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | cut -b3-"}" - eval "$cmd | fzf -m $FZF_CTRL_T_OPTS" | while read -r item; do + eval "$cmd | fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse -m $@ $FZF_CTRL_T_OPTS" | while read -r item; do printf '%q ' "$item" done echo @@ -13,8 +13,14 @@ __fzf_select__() { if [[ $- =~ i ]]; then +__fzf_use_tmux__() { + [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] +} + __fzfcmd() { - [ "${FZF_TMUX:-1}" != 0 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" + __fzf_use_tmux__ && + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || + echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse" } __fzf_select_tmux__() { @@ -26,14 +32,14 @@ __fzf_select_tmux__() { height="-l $height" fi - tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'" + tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__ --no-height)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'" } fzf-file-widget() { if __fzf_use_tmux__; then __fzf_select_tmux__ else - local selected="$(__fzf_select__)" + local selected="$(__fzf_select__ --height ${FZF_TMUX_HEIGHT:-40%} --reverse)" READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}" READLINE_POINT=$(( READLINE_POINT + ${#selected} )) fi @@ -51,7 +57,7 @@ __fzf_history__() ( shopt -u nocaseglob nocasematch line=$( HISTTIMEFORMAT= history | - eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" | + eval "$(__fzfcmd) +s --tac --no-reverse +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" | command grep '^ *[0-9]') && if [[ $- =~ H ]]; then sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" @@ -60,22 +66,15 @@ __fzf_history__() ( fi ) -__fzf_use_tmux__() { - [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-1}" != 0 ] && [ ${LINES:-40} -gt 15 ] -} - -[ $BASH_VERSINFO -gt 3 ] && __use_bind_x=1 || __use_bind_x=0 -__fzf_use_tmux__ && __use_tmux=1 || __use_tmux=0 - if [[ ! -o vi ]]; then # Required to refresh the prompt after fzf bind '"\er": redraw-current-line' bind '"\e^": history-expand-line' # CTRL-T - Paste the selected file path into the command line - if [ $__use_bind_x -eq 1 ]; then + if [ $BASH_VERSINFO -gt 3 ]; then bind -x '"\C-t": "fzf-file-widget"' - elif [ $__use_tmux -eq 1 ]; then + elif __fzf_use_tmux__; then bind '"\C-t": " \C-u \C-a\C-k`__fzf_select_tmux__`\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"' else bind '"\C-t": " \C-u \C-a\C-k`__fzf_select__`\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"' @@ -102,9 +101,9 @@ else # CTRL-T - Paste the selected file path into the command line # - FIXME: Selected items are attached to the end regardless of cursor position - if [ $__use_bind_x -eq 1 ]; then + if [ $BASH_VERSINFO -gt 3 ]; then bind -x '"\C-t": "fzf-file-widget"' - elif [ $__use_tmux -eq 1 ]; then + elif __fzf_use_tmux__; then bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select_tmux__`\C-x\C-e\C-x\C-a0P$xa"' else bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select__`\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "' @@ -120,6 +119,4 @@ else bind -m vi-command '"\ec": "ddi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"' fi -unset -v __use_tmux __use_bind_x - fi diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index dd75fec..fc61844 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -39,7 +39,7 @@ function fzf_key_bindings end function fzf-history-widget -d "Show command history" - history | eval (__fzfcmd) +s +m --tiebreak=index $FZF_CTRL_R_OPTS -q '(commandline)' | read -l result + history | eval (__fzfcmd) +s +m --no-reverse --tiebreak=index $FZF_CTRL_R_OPTS -q '(commandline)' | read -l result and commandline -- $result commandline -f repaint end @@ -54,15 +54,12 @@ function fzf_key_bindings end function __fzfcmd - set -q FZF_TMUX; or set FZF_TMUX 1 + set -q FZF_TMUX; or set FZF_TMUX 0 + set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% if [ $FZF_TMUX -eq 1 ] - if set -q FZF_TMUX_HEIGHT - echo "fzf-tmux -d$FZF_TMUX_HEIGHT" - else - echo "fzf-tmux -d40%" - end + echo "fzf-tmux -d$FZF_TMUX_HEIGHT" else - echo "fzf" + echo "fzf --height $FZF_TMUX_HEIGHT --reverse" end end diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index fed0153..7e24d92 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -17,8 +17,14 @@ __fsel() { return $ret } +__fzf_use_tmux__() { + [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] +} + __fzfcmd() { - [ ${FZF_TMUX:-1} -eq 1 ] && echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" + __fzf_use_tmux__ && + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || + echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse" } fzf-file-widget() { @@ -49,7 +55,7 @@ bindkey '\ec' fzf-cd-widget fzf-history-widget() { local selected num setopt localoptions noglobsubst pipefail 2> /dev/null - selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") ) + selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac --no-reverse +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") ) local ret=$? if [ -n "$selected" ]; then num=$selected[1] diff --git a/src/options.go b/src/options.go index 6fd3f6c..0c6661f 100644 --- a/src/options.go +++ b/src/options.go @@ -10,6 +10,7 @@ import ( "github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/tui" + "github.com/junegunn/fzf/src/util" "github.com/junegunn/go-shellwords" ) @@ -46,6 +47,8 @@ const usage = `usage: fzf [options] --jump-labels=CHARS Label characters for jump and jump-accept Layout + --height=HEIGHT[%] Display fzf window below the cursor with the given + height instead of using fullscreen --reverse Reverse orientation --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) --inline-info Display finder info inline with the query @@ -147,6 +150,7 @@ type Options struct { Theme *tui.ColorTheme Black bool Bold bool + Height sizeSpec Reverse bool Cycle bool Hscroll bool @@ -760,6 +764,14 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec { return sizeSpec{val, percent} } +func parseHeight(str string) sizeSpec { + if util.IsWindows() { + errorExit("--height options is currently not supported on Windows") + } + size := parseSize(str, 100, "height") + return size +} + func parsePreviewWindow(opts *previewOpts, input string) { // Default opts.position = posRight @@ -1003,6 +1015,10 @@ func parseOptions(opts *Options, allArgs []string) { case "--preview-window": parsePreviewWindow(&opts.Preview, nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:wrap][:hidden]")) + case "--height": + opts.Height = parseHeight(nextString(allArgs, &i, "height required: [HEIGHT[%]]")) + case "--no-height": + opts.Height = sizeSpec{} case "--no-margin": opts.Margin = defaultMargin() case "--margin": @@ -1029,6 +1045,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.WithNth = splitNth(value) } else if match, _ := optString(arg, "-s", "--sort="); match { opts.Sort = 1 // Don't care + } else if match, value := optString(arg, "--height="); match { + opts.Height = parseHeight(value) } else if match, value := optString(arg, "--toggle-sort="); match { parseToggleSort(opts.Keymap, value) } else if match, value := optString(arg, "--expect="); match { diff --git a/src/result.go b/src/result.go index e2d7c75..3d79176 100644 --- a/src/result.go +++ b/src/result.go @@ -166,7 +166,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, } colors = append(colors, colorOffset{ offset: [2]int32{int32(start), int32(idx)}, - color: tui.PairFor(fg, bg), + color: tui.NewColorPair(fg, bg), attr: ansi.color.attr.Merge(attr)}) } } diff --git a/src/result_test.go b/src/result_test.go index 15b1bdb..0e91fc8 100644 --- a/src/result_test.go +++ b/src/result_test.go @@ -105,7 +105,8 @@ func TestColorOffset(t *testing.T) { ansiOffset{[2]int32{33, 40}, ansiState{4, 8, tui.Bold}}}}} // [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}] - colors := item.colorOffsets(offsets, tui.Dark256, 99, 0, true) + pair := tui.NewColorPair(99, 199) + colors := item.colorOffsets(offsets, tui.Dark256, pair, tui.AttrRegular, true) assert := func(idx int, b int32, e int32, c tui.ColorPair, bold bool) { var attr tui.Attr if bold { @@ -116,10 +117,10 @@ func TestColorOffset(t *testing.T) { t.Error(o) } } - assert(0, 0, 5, tui.ColUser, false) - assert(1, 5, 15, 99, false) - assert(2, 15, 20, tui.ColUser, false) - assert(3, 22, 25, tui.ColUser+1, true) - assert(4, 25, 35, 99, false) - assert(5, 35, 40, tui.ColUser+2, true) + assert(0, 0, 5, tui.NewColorPair(1, 5), false) + assert(1, 5, 15, pair, false) + assert(2, 15, 20, tui.NewColorPair(1, 5), false) + assert(3, 22, 25, tui.NewColorPair(2, 6), true) + assert(4, 25, 35, pair, false) + assert(5, 35, 40, tui.NewColorPair(4, 8), true) } diff --git a/src/terminal.go b/src/terminal.go index 5b482b0..8002923 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -15,8 +15,6 @@ import ( "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" - - "github.com/junegunn/go-runewidth" ) // import "github.com/pkg/profile" @@ -42,6 +40,14 @@ type previewer struct { enabled bool } +type itemLine struct { + current bool + label string + result Result +} + +var emptyLine = itemLine{} + // Terminal represents terminal input/output type Terminal struct { initDelay time.Duration @@ -69,11 +75,12 @@ type Terminal struct { header []string header0 []string ansi bool + tabstop int margin [4]sizeSpec strong tui.Attr - window *tui.Window - bwindow *tui.Window - pwindow *tui.Window + window tui.Window + bwindow tui.Window + pwindow tui.Window count int progress int reading bool @@ -89,10 +96,12 @@ type Terminal struct { eventBox *util.EventBox mutex sync.Mutex initFunc func() + prevLines []itemLine suppress bool startChan chan bool slab *util.Slab theme *tui.ColorTheme + tui tui.Renderer } type selectedItem struct { @@ -115,7 +124,6 @@ func (a byTimeOrder) Less(i, j int) bool { } var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} -var _runeWidths = make(map[rune]int) var _tabStop int const ( @@ -247,7 +255,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { } else { header = reverseStringArray(opts.Header) } - _tabStop = opts.Tabstop var delay time.Duration if opts.Tac { delay = initialDelayTac @@ -262,6 +269,24 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { if !opts.Bold { strongAttr = tui.AttrRegular } + var renderer tui.Renderer + if opts.Height.size > 0 { + maxHeightFunc := func(termHeight int) int { + var maxHeight int + if opts.Height.percent { + maxHeight = int(opts.Height.size * float64(termHeight) / 100.0) + } else { + maxHeight = util.Min(int(opts.Height.size), termHeight) + } + if opts.InlineInfo { + return util.Max(maxHeight, 3) + } + return util.Max(maxHeight, 4) + } + renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, maxHeightFunc) + } else { + renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) + } return &Terminal{ initDelay: delay, inlineInfo: opts.InlineInfo, @@ -290,6 +315,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { header: header, header0: header, ansi: opts.Ansi, + tabstop: opts.Tabstop, reading: true, jumping: jumpDisabled, jumpLabels: opts.JumpLabels, @@ -306,9 +332,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { slab: util.MakeSlab(slab16Size, slab32Size), theme: opts.Theme, startChan: make(chan bool, 1), - initFunc: func() { - tui.Init(opts.Theme, opts.Black, opts.Mouse) - }} + tui: renderer, + initFunc: func() { renderer.Init() }} } // Input returns current query string @@ -401,22 +426,10 @@ func (t *Terminal) sortSelected() []selectedItem { return sels } -func runeWidth(r rune, prefixWidth int) int { - if r == '\t' { - return _tabStop - prefixWidth%_tabStop - } else if w, found := _runeWidths[r]; found { - return w - } else { - w := runewidth.RuneWidth(r) - _runeWidths[r] = w - return w - } -} - -func displayWidth(runes []rune) int { +func (t *Terminal) displayWidth(runes []rune) int { l := 0 for _, r := range runes { - l += runeWidth(r, l) + l += util.RuneWidth(r, l, t.tabstop) } return l } @@ -437,9 +450,10 @@ func calculateSize(base int, size sizeSpec, margin int, minSize int) int { } func (t *Terminal) resizeWindows() { - screenWidth := tui.MaxX() - screenHeight := tui.MaxY() + screenWidth := t.tui.MaxX() + screenHeight := t.tui.MaxY() marginInt := [4]int{} + t.prevLines = make([]itemLine, screenHeight) for idx, sizeSpec := range t.margin { if sizeSpec.percent { var max float64 @@ -487,40 +501,40 @@ func (t *Terminal) resizeWindows() { height := screenHeight - marginInt[0] - marginInt[2] if t.isPreviewEnabled() { createPreviewWindow := func(y int, x int, w int, h int) { - t.bwindow = tui.NewWindow(y, x, w, h, true) + t.bwindow = t.tui.NewWindow(y, x, w, h, true) pwidth := w - 4 // ncurses auto-wraps the line when the cursor reaches the right-end of // the window. To prevent unintended line-wraps, we use the width one // column larger than the desired value. - if !t.preview.wrap && tui.DoesAutoWrap() { + if !t.preview.wrap && t.tui.DoesAutoWrap() { pwidth += 1 } - t.pwindow = tui.NewWindow(y+1, x+2, pwidth, h-2, false) + t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, false) } switch t.preview.position { case posUp: pheight := calculateSize(height, t.preview.size, minHeight, 3) - t.window = tui.NewWindow( + t.window = t.tui.NewWindow( marginInt[0]+pheight, marginInt[3], width, height-pheight, false) createPreviewWindow(marginInt[0], marginInt[3], width, pheight) case posDown: pheight := calculateSize(height, t.preview.size, minHeight, 3) - t.window = tui.NewWindow( + t.window = t.tui.NewWindow( marginInt[0], marginInt[3], width, height-pheight, false) createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) case posLeft: pwidth := calculateSize(width, t.preview.size, minWidth, 5) - t.window = tui.NewWindow( + t.window = t.tui.NewWindow( marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) case posRight: pwidth := calculateSize(width, t.preview.size, minWidth, 5) - t.window = tui.NewWindow( + t.window = t.tui.NewWindow( marginInt[0], marginInt[3], width-pwidth, height, false) createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) } } else { - t.window = tui.NewWindow( + t.window = t.tui.NewWindow( marginInt[0], marginInt[3], width, @@ -530,7 +544,7 @@ func (t *Terminal) resizeWindows() { func (t *Terminal) move(y int, x int, clear bool) { if !t.reverse { - y = t.window.Height - y - 1 + y = t.window.Height() - y - 1 } if clear { @@ -541,7 +555,7 @@ func (t *Terminal) move(y int, x int, clear bool) { } func (t *Terminal) placeCursor() { - t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input[:t.cx]), false) + t.move(0, t.displayWidth([]rune(t.prompt))+t.displayWidth(t.input[:t.cx]), false) } func (t *Terminal) printPrompt() { @@ -552,7 +566,7 @@ func (t *Terminal) printPrompt() { func (t *Terminal) printInfo() { if t.inlineInfo { - t.move(0, displayWidth([]rune(t.prompt))+displayWidth(t.input)+1, true) + t.move(0, t.displayWidth([]rune(t.prompt))+t.displayWidth(t.input)+1, true) if t.reading { t.window.CPrint(tui.ColSpinner, t.strong, " < ") } else { @@ -589,7 +603,7 @@ func (t *Terminal) printHeader() { if len(t.header) == 0 { return } - max := t.window.Height + max := t.window.Height() var state *ansiState for idx, lineStr := range t.header { line := idx + 2 @@ -616,19 +630,25 @@ func (t *Terminal) printList() { maxy := t.maxItems() count := t.merger.Length() - t.offset - for i := 0; i < maxy; i++ { + for j := 0; j < maxy; j++ { + i := j + if !t.reverse { + i = maxy - 1 - j + } line := i + 2 + len(t.header) if t.inlineInfo { line-- } - t.move(line, 0, true) if i < count { - t.printItem(t.merger.Get(i+t.offset), i, i == t.cy-t.offset) + t.printItem(t.merger.Get(i+t.offset), line, i, i == t.cy-t.offset) + } else if t.prevLines[i] != emptyLine { + t.prevLines[i] = emptyLine + t.move(line, 0, true) } } } -func (t *Terminal) printItem(result *Result, i int, current bool) { +func (t *Terminal) printItem(result *Result, line int, i int, current bool) { item := result.item _, selected := t.selected[item.Index()] label := " " @@ -641,6 +661,15 @@ func (t *Terminal) printItem(result *Result, i int, current bool) { } else if current { label = ">" } + + // Avoid unnecessary redraw + newLine := itemLine{current, label, *result} + if t.prevLines[i] == newLine { + return + } + t.prevLines[i] = newLine + + t.move(line, 0, true) t.window.CPrint(tui.ColCursor, t.strong, label) if current { if selected { @@ -659,11 +688,11 @@ func (t *Terminal) printItem(result *Result, i int, current bool) { } } -func trimRight(runes []rune, width int) ([]rune, int) { +func (t *Terminal) trimRight(runes []rune, width int) ([]rune, int) { // We start from the beginning to handle tab characters l := 0 for idx, r := range runes { - l += runeWidth(r, l) + l += util.RuneWidth(r, l, t.tabstop) if l > width { return runes[:idx], len(runes) - idx } @@ -671,10 +700,10 @@ func trimRight(runes []rune, width int) ([]rune, int) { return runes, 0 } -func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int { +func (t *Terminal) displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int { l := 0 for _, r := range runes { - l += runeWidth(r, l+prefixWidth) + l += util.RuneWidth(r, l+prefixWidth, t.tabstop) if l > limit { // Early exit return l @@ -683,27 +712,27 @@ func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int { return l } -func trimLeft(runes []rune, width int) ([]rune, int32) { +func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) { if len(runes) > maxDisplayWidthCalc && len(runes) > width { trimmed := len(runes) - width return runes[trimmed:], int32(trimmed) } - currentWidth := displayWidth(runes) + currentWidth := t.displayWidth(runes) var trimmed int32 for currentWidth > width && len(runes) > 0 { runes = runes[1:] trimmed++ - currentWidth = displayWidthWithLimit(runes, 2, width) + currentWidth = t.displayWidthWithLimit(runes, 2, width) } return runes, trimmed } -func overflow(runes []rune, max int) bool { +func (t *Terminal) overflow(runes []rune, max int) bool { l := 0 for _, r := range runes { - l += runeWidth(r, l) + l += util.RuneWidth(r, l, t.tabstop) if l > max { return true } @@ -737,22 +766,22 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo } offsets := result.colorOffsets(charOffsets, t.theme, col2, attr, current) - maxWidth := t.window.Width - 3 + maxWidth := t.window.Width() - 3 maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) - if overflow(text, maxWidth) { + if t.overflow(text, maxWidth) { if t.hscroll { // Stri.. - if !overflow(text[:maxe], maxWidth-2) { - text, _ = trimRight(text, maxWidth-2) + if !t.overflow(text[:maxe], maxWidth-2) { + text, _ = t.trimRight(text, maxWidth-2) text = append(text, []rune("..")...) } else { // Stri.. - if overflow(text[maxe:], 2) { + if t.overflow(text[maxe:], 2) { text = append(text[:maxe], []rune("..")...) } // ..ri.. var diff int32 - text, diff = trimLeft(text, maxWidth-2) + text, diff = t.trimLeft(text, maxWidth-2) // Transform offsets for idx, offset := range offsets { @@ -766,7 +795,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo text = append([]rune(".."), text...) } } else { - text, _ = trimRight(text, maxWidth-2) + text, _ = t.trimRight(text, maxWidth-2) text = append(text, []rune("..")...) for idx, offset := range offsets { @@ -784,11 +813,11 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo b := util.Constrain32(offset.offset[0], index, maxOffset) e := util.Constrain32(offset.offset[1], index, maxOffset) - substr, prefixWidth = processTabs(text[index:b], prefixWidth) + substr, prefixWidth = t.processTabs(text[index:b], prefixWidth) t.window.CPrint(col1, attr, substr) if b < e { - substr, prefixWidth = processTabs(text[b:e], prefixWidth) + substr, prefixWidth = t.processTabs(text[b:e], prefixWidth) t.window.CPrint(offset.color, offset.attr, substr) } @@ -798,7 +827,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo } } if index < maxOffset { - substr, _ = processTabs(text[index:], prefixWidth) + substr, _ = t.processTabs(text[index:], prefixWidth) t.window.CPrint(col1, attr, substr) } } @@ -835,38 +864,44 @@ func (t *Terminal) printPreview() { return true } } - if !t.preview.wrap { - lines := strings.Split(str, "\n") - for i, line := range lines { - limit := t.pwindow.Width - if tui.DoesAutoWrap() { - limit -= 1 - } - if i == 0 { - limit -= t.pwindow.X() - } - trimmed, _ := trimRight([]rune(line), limit) - lines[i], _ = processTabs(trimmed, 0) + lines := strings.Split(str, "\n") + for i, line := range lines { + limit := t.pwindow.Width() + if t.tui.DoesAutoWrap() { + limit -= 1 } + if i == 0 { + limit -= t.pwindow.X() + } + trimmed := []rune(line) + if !t.preview.wrap { + trimmed, _ = t.trimRight(trimmed, limit) + } + lines[i], _ = t.processTabs(trimmed, 0) str = strings.Join(lines, "\n") } if ansi != nil && ansi.colored() { - return t.pwindow.CFill(str, ansi.fg, ansi.bg, ansi.attr) + return t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str) } return t.pwindow.Fill(str) }) - if t.previewer.lines > t.pwindow.Height { + t.pwindow.FinishFill() + if t.previewer.lines > t.pwindow.Height() { offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines) - t.pwindow.Move(0, t.pwindow.Width-len(offset)) + pos := t.pwindow.Width() - len(offset) + if t.tui.DoesAutoWrap() { + pos -= 1 + } + t.pwindow.Move(0, pos) t.pwindow.CPrint(tui.ColInfo, tui.Reverse, offset) } } -func processTabs(runes []rune, prefixWidth int) (string, int) { +func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) { var strbuf bytes.Buffer l := prefixWidth for _, r := range runes { - w := runeWidth(r, l) + w := util.RuneWidth(r, l, t.tabstop) l += w if r == '\t' { strbuf.WriteString(strings.Repeat(" ", w)) @@ -889,9 +924,9 @@ func (t *Terminal) printAll() { func (t *Terminal) refresh() { if !t.suppress { if t.isPreviewEnabled() { - tui.RefreshWindows([]*tui.Window{t.bwindow, t.pwindow, t.window}) + t.tui.RefreshWindows([]tui.Window{t.bwindow, t.pwindow, t.window}) } else { - tui.RefreshWindows([]*tui.Window{t.window}) + t.tui.RefreshWindows([]tui.Window{t.window}) } } } @@ -1013,9 +1048,9 @@ func (t *Terminal) executeCommand(template string, items []*Item) { cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - tui.Pause() + t.tui.Pause() cmd.Run() - if tui.Resume() { + if t.tui.Resume() { t.printAll() } t.refresh() @@ -1162,11 +1197,11 @@ func (t *Terminal) Loop() { case reqRefresh: t.suppress = false case reqRedraw: - tui.Clear() - tui.Refresh() + t.tui.Clear() + t.tui.Refresh() t.printAll() case reqClose: - tui.Close() + t.tui.Close() if t.output() { exit(exitOk) } @@ -1179,11 +1214,11 @@ func (t *Terminal) Loop() { case reqPreviewRefresh: t.printPreview() case reqPrintQuery: - tui.Close() + t.tui.Close() t.printer(string(t.input)) exit(exitOk) case reqQuit: - tui.Close() + t.tui.Close() exit(exitInterrupt) } } @@ -1196,7 +1231,7 @@ func (t *Terminal) Loop() { looping := true for looping { - event := tui.GetChar() + event := t.tui.GetChar() t.mutex.Lock() previousInput := t.input @@ -1288,11 +1323,11 @@ func (t *Terminal) Loop() { } case actPreviewPageUp: if t.isPreviewEnabled() { - scrollPreview(-t.pwindow.Height) + scrollPreview(-t.pwindow.Height()) } case actPreviewPageDown: if t.isPreviewEnabled() { - scrollPreview(t.pwindow.Height) + scrollPreview(t.pwindow.Height()) } case actBeginningOfLine: t.cx = 0 @@ -1466,11 +1501,11 @@ func (t *Terminal) Loop() { scrollPreview(-me.S) } } else if t.window.Enclose(my, mx) { - mx -= t.window.Left - my -= t.window.Top - mx = util.Constrain(mx-displayWidth([]rune(t.prompt)), 0, len(t.input)) + mx -= t.window.Left() + my -= t.window.Top() + mx = util.Constrain(mx-t.displayWidth([]rune(t.prompt)), 0, len(t.input)) if !t.reverse { - my = t.window.Height - my - 1 + my = t.window.Height() - my - 1 } min := 2 + len(t.header) if t.inlineInfo { @@ -1582,7 +1617,7 @@ func (t *Terminal) vset(o int) bool { } func (t *Terminal) maxItems() int { - max := t.window.Height - 2 - len(t.header) + max := t.window.Height() - 2 - len(t.header) if t.inlineInfo { max++ } diff --git a/src/tui/light.go b/src/tui/light.go new file mode 100644 index 0000000..1273c8f --- /dev/null +++ b/src/tui/light.go @@ -0,0 +1,764 @@ +package tui + +import ( + "fmt" + "os" + "strconv" + "strings" + "syscall" + "time" + "unicode/utf8" + + "github.com/junegunn/fzf/src/util" +) + +const ( + defaultWidth = 80 + defaultHeight = 24 + + escPollInterval = 5 +) + +func openTtyIn() *os.File { + in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0) + if err != nil { + panic("Failed to open /dev/tty") + } + return in +} + +// FIXME: Need better handling of non-displayable characters +func (r *LightRenderer) stderr(str string) { + bytes := []byte(str) + runes := []rune{} + for len(bytes) > 0 { + r, sz := utf8.DecodeRune(bytes) + if r == utf8.RuneError || r != '\x1b' && r != '\n' && r < 32 { + runes = append(runes, '?') + } else { + runes = append(runes, r) + } + bytes = bytes[sz:] + } + r.queued += string(runes) +} + +func (r *LightRenderer) csi(code string) { + r.stderr("\x1b[" + code) +} + +func (r *LightRenderer) flush() { + if len(r.queued) > 0 { + fmt.Fprint(os.Stderr, r.queued) + r.queued = "" + } +} + +// Light renderer +type LightRenderer struct { + theme *ColorTheme + mouse bool + forceBlack bool + prevDownTime time.Time + clickY []int + ttyin *os.File + buffer []byte + ostty string + width int + height int + yoffset int + tabstop int + escDelay int + upOneLine bool + queued string + maxHeightFunc func(int) int +} + +type LightWindow struct { + renderer *LightRenderer + colored bool + border bool + top int + left int + width int + height int + posx int + posy int + tabstop int + bg Color +} + +func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, maxHeightFunc func(int) int) Renderer { + r := LightRenderer{ + theme: theme, + forceBlack: forceBlack, + mouse: mouse, + ttyin: openTtyIn(), + yoffset: -1, + tabstop: tabstop, + upOneLine: false, + maxHeightFunc: maxHeightFunc} + return &r +} + +func (r *LightRenderer) defaultTheme() *ColorTheme { + colors, err := util.ExecCommand("tput colors").Output() + if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 { + return Dark256 + } + return Default16 +} + +func stty(cmd string) string { + out, err := util.ExecCommand("stty " + cmd + " < /dev/tty").Output() + if err != nil { + // Not sure how to handle this + panic("stty " + cmd + ": " + err.Error()) + } + return strings.TrimSpace(string(out)) +} + +func (r *LightRenderer) findOffset() (row int, col int) { + r.csi("6n") + r.flush() + bytes := r.getBytesInternal([]byte{}) + + // ^[[*;*R + if len(bytes) > 5 && bytes[0] == 27 && bytes[1] == 91 && bytes[len(bytes)-1] == 'R' { + nums := strings.Split(string(bytes[2:len(bytes)-1]), ";") + if len(nums) == 2 { + return atoi(nums[0], 0) - 1, atoi(nums[1], 0) - 1 + } + return -1, -1 + } + + // No idea + return -1, -1 +} + +func repeat(s string, times int) string { + if times > 0 { + return strings.Repeat(s, times) + } + return "" +} + +func atoi(s string, defaultValue int) int { + value, err := strconv.Atoi(s) + if err != nil { + return defaultValue + } + return value +} + +func (r *LightRenderer) Init() { + delay := 100 + delayEnv := os.Getenv("ESCDELAY") + if len(delayEnv) > 0 { + num, err := strconv.Atoi(delayEnv) + if err == nil && num >= 0 { + delay = num + } + } + r.escDelay = delay + + r.ostty = stty("-g") + stty("raw") + r.updateTerminalSize() + initTheme(r.theme, r.defaultTheme(), r.forceBlack) + + _, x := r.findOffset() + if x > 0 { + r.upOneLine = true + r.stderr("\n") + } + for i := 1; i < r.MaxY(); i++ { + r.stderr("\n") + r.csi("G") + } + + if r.mouse { + r.csi("?1000h") + } + r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) + r.csi("G") + r.csi("s") + r.yoffset, _ = r.findOffset() +} + +func (r *LightRenderer) updateTerminalSize() { + sizes := strings.Split(stty("size"), " ") + if len(sizes) < 2 { + r.width = defaultWidth + r.height = r.maxHeightFunc(defaultHeight) + } else { + r.width = atoi(sizes[1], defaultWidth) + r.height = r.maxHeightFunc(atoi(sizes[0], defaultHeight)) + } +} + +func (r *LightRenderer) getch(nonblock bool) int { + b := make([]byte, 1) + util.SetNonblock(r.ttyin, nonblock) + _, err := r.ttyin.Read(b) + if err != nil { + return -1 + } + return int(b[0]) +} + +func (r *LightRenderer) getBytes() []byte { + return r.getBytesInternal(r.buffer) +} + +func (r *LightRenderer) getBytesInternal(buffer []byte) []byte { + c := r.getch(false) + + retries := 0 + if c == ESC { + retries = r.escDelay / escPollInterval + } + buffer = append(buffer, byte(c)) + + for { + c = r.getch(true) + if c == -1 { + if retries > 0 { + retries-- + time.Sleep(escPollInterval * time.Millisecond) + continue + } + break + } + retries = 0 + buffer = append(buffer, byte(c)) + } + + return buffer +} + +func (r *LightRenderer) GetChar() Event { + if len(r.buffer) == 0 { + r.buffer = r.getBytes() + } + if len(r.buffer) == 0 { + panic("Empty buffer") + } + + sz := 1 + defer func() { + r.buffer = r.buffer[sz:] + }() + + switch r.buffer[0] { + case CtrlC: + return Event{CtrlC, 0, nil} + case CtrlG: + return Event{CtrlG, 0, nil} + case CtrlQ: + return Event{CtrlQ, 0, nil} + case 127: + return Event{BSpace, 0, nil} + case ESC: + ev := r.escSequence(&sz) + // Second chance + if ev.Type == Invalid { + r.buffer = r.getBytes() + ev = r.escSequence(&sz) + } + return ev + } + + // CTRL-A ~ CTRL-Z + if r.buffer[0] <= CtrlZ { + return Event{int(r.buffer[0]), 0, nil} + } + char, rsz := utf8.DecodeRune(r.buffer) + if char == utf8.RuneError { + return Event{ESC, 0, nil} + } + sz = rsz + return Event{Rune, char, nil} +} + +func (r *LightRenderer) escSequence(sz *int) Event { + if len(r.buffer) < 2 { + return Event{ESC, 0, nil} + } + *sz = 2 + switch r.buffer[1] { + case 13: + return Event{AltEnter, 0, nil} + case 32: + return Event{AltSpace, 0, nil} + case 47: + return Event{AltSlash, 0, nil} + case 98: + return Event{AltB, 0, nil} + case 100: + return Event{AltD, 0, nil} + case 102: + return Event{AltF, 0, nil} + case 127: + return Event{AltBS, 0, nil} + case 91, 79: + if len(r.buffer) < 3 { + return Event{Invalid, 0, nil} + } + *sz = 3 + switch r.buffer[2] { + case 68: + return Event{Left, 0, nil} + case 67: + return Event{Right, 0, nil} + case 66: + return Event{Down, 0, nil} + case 65: + return Event{Up, 0, nil} + case 90: + return Event{BTab, 0, nil} + case 72: + return Event{Home, 0, nil} + case 70: + return Event{End, 0, nil} + case 77: + return r.mouseSequence(sz) + case 80: + return Event{F1, 0, nil} + case 81: + return Event{F2, 0, nil} + case 82: + return Event{F3, 0, nil} + case 83: + return Event{F4, 0, nil} + case 49, 50, 51, 52, 53, 54: + if len(r.buffer) < 4 { + return Event{Invalid, 0, nil} + } + *sz = 4 + switch r.buffer[2] { + case 50: + if len(r.buffer) == 5 && r.buffer[4] == 126 { + *sz = 5 + switch r.buffer[3] { + case 48: + return Event{F9, 0, nil} + case 49: + return Event{F10, 0, nil} + case 51: + return Event{F11, 0, nil} + case 52: + return Event{F12, 0, nil} + } + } + // Bracketed paste mode \e[200~ / \e[201 + if r.buffer[3] == 48 && (r.buffer[4] == 48 || r.buffer[4] == 49) && r.buffer[5] == 126 { + *sz = 6 + return Event{Invalid, 0, nil} + } + return Event{Invalid, 0, nil} // INS + case 51: + return Event{Del, 0, nil} + case 52: + return Event{End, 0, nil} + case 53: + return Event{PgUp, 0, nil} + case 54: + return Event{PgDn, 0, nil} + case 49: + switch r.buffer[3] { + case 126: + return Event{Home, 0, nil} + case 53, 55, 56, 57: + if len(r.buffer) == 5 && r.buffer[4] == 126 { + *sz = 5 + switch r.buffer[3] { + case 53: + return Event{F5, 0, nil} + case 55: + return Event{F6, 0, nil} + case 56: + return Event{F7, 0, nil} + case 57: + return Event{F8, 0, nil} + } + } + return Event{Invalid, 0, nil} + case 59: + if len(r.buffer) != 6 { + return Event{Invalid, 0, nil} + } + *sz = 6 + switch r.buffer[4] { + case 50: + switch r.buffer[5] { + case 68: + return Event{Home, 0, nil} + case 67: + return Event{End, 0, nil} + } + case 53: + switch r.buffer[5] { + case 68: + return Event{SLeft, 0, nil} + case 67: + return Event{SRight, 0, nil} + } + } // r.buffer[4] + } // r.buffer[3] + } // r.buffer[2] + } // r.buffer[2] + } // r.buffer[1] + if r.buffer[1] >= 'a' && r.buffer[1] <= 'z' { + return Event{AltA + int(r.buffer[1]) - 'a', 0, nil} + } + return Event{Invalid, 0, nil} +} + +func (r *LightRenderer) mouseSequence(sz *int) Event { + if len(r.buffer) < 6 || r.yoffset < 0 { + return Event{Invalid, 0, nil} + } + *sz = 6 + switch r.buffer[3] { + case 32, 36, 40, 48, // mouse-down / shift / cmd / ctrl + 35, 39, 43, 51: // mouse-up / shift / cmd / ctrl + mod := r.buffer[3] >= 36 + down := r.buffer[3]%2 == 0 + x := int(r.buffer[4] - 33) + y := int(r.buffer[5]-33) - r.yoffset + double := false + if down { + now := time.Now() + if now.Sub(r.prevDownTime) < doubleClickDuration { + r.clickY = append(r.clickY, y) + } else { + r.clickY = []int{y} + } + r.prevDownTime = now + } else { + if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && + time.Now().Sub(r.prevDownTime) < doubleClickDuration { + double = true + } + } + + return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}} + case 96, 100, 104, 112, // scroll-up / shift / cmd / ctrl + 97, 101, 105, 113: // scroll-down / shift / cmd / ctrl + mod := r.buffer[3] >= 100 + s := 1 - int(r.buffer[3]%2)*2 + x := int(r.buffer[4] - 33) + y := int(r.buffer[5]-33) - r.yoffset + return Event{Mouse, 0, &MouseEvent{y, x, s, false, false, mod}} + } + return Event{Invalid, 0, nil} +} + +func (r *LightRenderer) Pause() { + stty(fmt.Sprintf("%q", r.ostty)) + r.csi("?1049h") + r.flush() +} + +func (r *LightRenderer) Resume() bool { + stty("raw") + r.csi("?1049l") + r.flush() + // Should redraw + return true +} + +func (r *LightRenderer) Clear() { + r.csi("u") + r.csi("J") + r.flush() +} + +func (r *LightRenderer) RefreshWindows(windows []Window) { + r.flush() +} + +func (r *LightRenderer) Refresh() { + r.updateTerminalSize() +} + +func (r *LightRenderer) Close() { + r.csi("u") + r.csi("J") + if r.mouse { + r.csi("?1000l") + } + if r.upOneLine { + r.csi("A") + } + r.flush() + stty(fmt.Sprintf("%q", r.ostty)) +} + +func (r *LightRenderer) MaxX() int { + return r.width +} + +func (r *LightRenderer) MaxY() int { + return r.height +} + +func (r *LightRenderer) DoesAutoWrap() bool { + return true +} + +func (r *LightRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { + w := &LightWindow{ + renderer: r, + colored: r.theme != nil, + border: border, + top: top, + left: left, + width: width, + height: height, + tabstop: r.tabstop, + bg: colDefault} + if r.theme != nil { + w.bg = r.theme.Bg + } + if w.border { + w.drawBorder() + } + return w +} + +func (w *LightWindow) drawBorder() { + w.Move(0, 0) + w.CPrint(ColBorder, AttrRegular, "┌"+repeat("─", w.width-2)+"┐") + for y := 1; y < w.height-1; y++ { + w.Move(y, 0) + w.CPrint(ColBorder, AttrRegular, "│") + w.cprint2(colDefault, w.bg, AttrRegular, repeat(" ", w.width-2)) + w.CPrint(ColBorder, AttrRegular, "│") + } + w.Move(w.height-1, 0) + w.CPrint(ColBorder, AttrRegular, "└"+repeat("─", w.width-2)+"┘") +} + +func (w *LightWindow) csi(code string) { + w.renderer.csi(code) +} + +func (w *LightWindow) stderr(str string) { + w.renderer.stderr(str) +} + +func (w *LightWindow) Top() int { + return w.top +} + +func (w *LightWindow) Left() int { + return w.left +} + +func (w *LightWindow) Width() int { + return w.width +} + +func (w *LightWindow) Height() int { + return w.height +} + +func (w *LightWindow) Refresh() { +} + +func (w *LightWindow) Close() { +} + +func (w *LightWindow) X() int { + return w.posx +} + +func (w *LightWindow) Enclose(y int, x int) bool { + return x >= w.left && x < (w.left+w.width) && + y >= w.top && y < (w.top+w.height) +} + +func (w *LightWindow) Move(y int, x int) { + w.posx = x + w.posy = y + + w.csi("u") + y += w.Top() + if y > 0 { + w.csi(fmt.Sprintf("%dB", y)) + } + x += w.Left() + if x > 0 { + w.csi(fmt.Sprintf("%dC", x)) + } +} + +func (w *LightWindow) MoveAndClear(y int, x int) { + w.Move(y, x) + // We should not delete preview window on the right + // csi("K") + w.Print(repeat(" ", w.width-x)) + w.Move(y, x) +} + +func attrCodes(attr Attr) []string { + codes := []string{} + if (attr & Bold) > 0 { + codes = append(codes, "1") + } + if (attr & Dim) > 0 { + codes = append(codes, "2") + } + if (attr & Italic) > 0 { + codes = append(codes, "3") + } + if (attr & Underline) > 0 { + codes = append(codes, "4") + } + if (attr & Blink) > 0 { + codes = append(codes, "5") + } + if (attr & Reverse) > 0 { + codes = append(codes, "7") + } + return codes +} + +func colorCodes(fg Color, bg Color) []string { + codes := []string{} + appendCode := func(c Color, offset int) { + if c == colDefault { + return + } + if c.is24() { + r := (c >> 16) & 0xff + g := (c >> 8) & 0xff + b := (c) & 0xff + codes = append(codes, fmt.Sprintf("%d;2;%d;%d;%d", 38+offset, r, g, b)) + } else if c >= colBlack && c <= colWhite { + codes = append(codes, fmt.Sprintf("%d", int(c)+30+offset)) + } else if c > colWhite && c < 16 { + codes = append(codes, fmt.Sprintf("%d", int(c)+90+offset-8)) + } else if c >= 16 && c < 256 { + codes = append(codes, fmt.Sprintf("%d;5;%d", 38+offset, c)) + } + } + appendCode(fg, 0) + appendCode(bg, 10) + return codes +} + +func (w *LightWindow) csiColor(fg Color, bg Color, attr Attr) bool { + codes := append(attrCodes(attr), colorCodes(fg, bg)...) + w.csi(";" + strings.Join(codes, ";") + "m") + return len(codes) > 0 +} + +func (w *LightWindow) Print(text string) { + w.cprint2(colDefault, w.bg, AttrRegular, text) +} + +func (w *LightWindow) CPrint(pair ColorPair, attr Attr, text string) { + if !w.colored { + w.csiColor(colDefault, colDefault, attrFor(pair, attr)) + } else { + w.csiColor(pair.Fg(), pair.Bg(), attr) + } + w.stderr(text) + w.csi("m") +} + +func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) { + if w.csiColor(fg, bg, attr) { + defer w.csi("m") + } + w.stderr(text) +} + +type wrappedLine struct { + text string + displayWidth int +} + +func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine { + lines := []wrappedLine{} + width := 0 + line := "" + for _, r := range input { + w := util.Max(util.RuneWidth(r, prefixLength+width, 8), 1) + width += w + str := string(r) + if r == '\t' { + str = repeat(" ", w) + } + if prefixLength+width <= max { + line += str + } else { + lines = append(lines, wrappedLine{string(line), width - w}) + line = str + prefixLength = 0 + width = util.RuneWidth(r, prefixLength, 8) + } + } + lines = append(lines, wrappedLine{string(line), width}) + return lines +} + +func (w *LightWindow) fill(str string, onMove func()) bool { + allLines := strings.Split(str, "\n") + for i, line := range allLines { + lines := wrapLine(line, w.posx, w.width, w.tabstop) + for j, wl := range lines { + w.stderr(wl.text) + w.posx += wl.displayWidth + if j < len(lines)-1 || i < len(allLines)-1 { + if w.posy+1 >= w.height { + return false + } + w.MoveAndClear(w.posy+1, 0) + onMove() + } + } + } + return true +} + +func (w *LightWindow) setBg() { + if w.bg != colDefault { + w.csiColor(colDefault, w.bg, AttrRegular) + } +} + +func (w *LightWindow) Fill(text string) bool { + w.MoveAndClear(w.posy, w.posx) + w.setBg() + return w.fill(text, w.setBg) +} + +func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) bool { + w.MoveAndClear(w.posy, w.posx) + if bg == colDefault { + bg = w.bg + } + if w.csiColor(fg, bg, attr) { + return w.fill(text, func() { w.csiColor(fg, bg, attr) }) + defer w.csi("m") + } + return w.fill(text, w.setBg) +} + +func (w *LightWindow) FinishFill() { + for y := w.posy + 1; y < w.height; y++ { + w.MoveAndClear(y, 0) + } +} + +func (w *LightWindow) Erase() { + if w.border { + w.drawBorder() + } + // We don't erase the window here to avoid flickering during scroll + w.Move(0, 0) +} diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 7f515b2..b160692 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -33,9 +33,39 @@ import ( "unicode/utf8" ) -type ColorPair int16 type Attr C.uint -type WindowImpl C.WINDOW + +type CursesWindow struct { + impl *C.WINDOW + top int + left int + width int + height int +} + +func (w *CursesWindow) Top() int { + return w.top +} + +func (w *CursesWindow) Left() int { + return w.left +} + +func (w *CursesWindow) Width() int { + return w.width +} + +func (w *CursesWindow) Height() int { + return w.height +} + +func (w *CursesWindow) Refresh() { + C.wnoutrefresh(w.impl) +} + +func (w *CursesWindow) FinishFill() { + // NO-OP +} const ( Bold Attr = C.A_BOLD @@ -51,31 +81,14 @@ const ( AttrRegular Attr = 0 ) -// Pallete -const ( - ColDefault ColorPair = iota - ColNormal - ColPrompt - ColMatch - ColCurrent - ColCurrentMatch - ColSpinner - ColInfo - ColCursor - ColSelected - ColHeader - ColBorder - ColUser // Should be the last entry -) - var ( _screen *C.SCREEN - _colorMap map[int]ColorPair + _colorMap map[int]int16 _colorFn func(ColorPair, Attr) (C.short, C.int) ) func init() { - _colorMap = make(map[int]ColorPair) + _colorMap = make(map[int]int16) if strings.HasPrefix(C.GoString(C.curses_version()), "ncurses 5") { Italic = C.A_NORMAL } @@ -85,14 +98,14 @@ func (a Attr) Merge(b Attr) Attr { return a | b } -func DefaultTheme() *ColorTheme { +func (r *FullscreenRenderer) defaultTheme() *ColorTheme { if C.tigetnum(C.CString("colors")) >= 256 { return Dark256 } return Default16 } -func Init(theme *ColorTheme, black bool, mouse bool) { +func (r *FullscreenRenderer) Init() { C.setlocale(C.LC_ALL, C.CString("")) tty := C.c_tty() if tty == nil { @@ -105,7 +118,7 @@ func Init(theme *ColorTheme, black bool, mouse bool) { os.Exit(2) } C.set_term(_screen) - if mouse { + if r.mouse { C.mousemask(C.ALL_MOUSE_EVENTS, nil) C.mouseinterval(0) } @@ -124,14 +137,14 @@ func Init(theme *ColorTheme, black bool, mouse bool) { } C.set_escdelay(C.int(delay)) - _color = theme != nil - if _color { + if r.theme != nil { C.start_color() - InitTheme(theme, black) - initPairs(theme) - C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal)))) + initTheme(r.theme, r.defaultTheme(), r.forceBlack) + initPairs(r.theme) + C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal.index())))) _colorFn = attrColored } else { + initTheme(r.theme, nil, r.forceBlack) _colorFn = attrMono } @@ -145,39 +158,39 @@ func Init(theme *ColorTheme, black bool, mouse bool) { func initPairs(theme *ColorTheme) { C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg)) - initPair := func(group ColorPair, fg Color, bg Color) { - C.init_pair(C.short(group), C.short(fg), C.short(bg)) + for _, pair := range []ColorPair{ + ColNormal, + ColPrompt, + ColMatch, + ColCurrent, + ColCurrentMatch, + ColSpinner, + ColInfo, + ColCursor, + ColSelected, + ColHeader, + ColBorder} { + C.init_pair(C.short(pair.index()), C.short(pair.Fg()), C.short(pair.Bg())) } - initPair(ColNormal, theme.Fg, theme.Bg) - initPair(ColPrompt, theme.Prompt, theme.Bg) - initPair(ColMatch, theme.Match, theme.Bg) - initPair(ColCurrent, theme.Current, theme.DarkBg) - initPair(ColCurrentMatch, theme.CurrentMatch, theme.DarkBg) - initPair(ColSpinner, theme.Spinner, theme.Bg) - initPair(ColInfo, theme.Info, theme.Bg) - initPair(ColCursor, theme.Cursor, theme.DarkBg) - initPair(ColSelected, theme.Selected, theme.DarkBg) - initPair(ColHeader, theme.Header, theme.Bg) - initPair(ColBorder, theme.Border, theme.Bg) } -func Pause() { +func (r *FullscreenRenderer) Pause() { C.endwin() } -func Resume() bool { +func (r *FullscreenRenderer) Resume() bool { return false } -func Close() { +func (r *FullscreenRenderer) Close() { C.endwin() C.delscreen(_screen) } -func NewWindow(top int, left int, width int, height int, border bool) *Window { +func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left)) - if _color { - C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal)))) + if r.theme != nil { + C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index())))) } if border { pair, attr := _colorFn(ColBorder, 0) @@ -188,66 +201,50 @@ func NewWindow(top int, left int, width int, height int, border bool) *Window { C.wcolor_set(win, 0, nil) } - return &Window{ - impl: (*WindowImpl)(win), - Top: top, - Left: left, - Width: width, - Height: height, + return &CursesWindow{ + impl: win, + top: top, + left: left, + width: width, + height: height, } } -func attrColored(pair ColorPair, a Attr) (C.short, C.int) { - return C.short(pair), C.int(a) +func attrColored(color ColorPair, a Attr) (C.short, C.int) { + return C.short(color.index()), C.int(a) } -func attrMono(pair ColorPair, a Attr) (C.short, C.int) { - var attr C.int - switch pair { - case ColCurrent: - attr = C.A_REVERSE - case ColMatch: - attr = C.A_UNDERLINE - case ColCurrentMatch: - attr = C.A_UNDERLINE | C.A_REVERSE - } - if C.int(a)&C.A_BOLD == C.A_BOLD { - attr = attr | C.A_BOLD - } - return 0, attr +func attrMono(color ColorPair, a Attr) (C.short, C.int) { + return 0, C.int(attrFor(color, a)) } -func MaxX() int { +func (r *FullscreenRenderer) MaxX() int { return int(C.COLS) } -func MaxY() int { +func (r *FullscreenRenderer) MaxY() int { return int(C.LINES) } -func (w *Window) win() *C.WINDOW { - return (*C.WINDOW)(w.impl) +func (w *CursesWindow) Close() { + C.delwin(w.impl) } -func (w *Window) Close() { - C.delwin(w.win()) +func (w *CursesWindow) Enclose(y int, x int) bool { + return bool(C.wenclose(w.impl, C.int(y), C.int(x))) } -func (w *Window) Enclose(y int, x int) bool { - return bool(C.wenclose(w.win(), C.int(y), C.int(x))) +func (w *CursesWindow) Move(y int, x int) { + C.wmove(w.impl, C.int(y), C.int(x)) } -func (w *Window) Move(y int, x int) { - C.wmove(w.win(), C.int(y), C.int(x)) -} - -func (w *Window) MoveAndClear(y int, x int) { +func (w *CursesWindow) MoveAndClear(y int, x int) { w.Move(y, x) - C.wclrtoeol(w.win()) + C.wclrtoeol(w.impl) } -func (w *Window) Print(text string) { - C.waddstr(w.win(), C.CString(strings.Map(func(r rune) rune { +func (w *CursesWindow) Print(text string) { + C.waddstr(w.impl, C.CString(strings.Map(func(r rune) rune { if r < 32 { return -1 } @@ -255,69 +252,74 @@ func (w *Window) Print(text string) { }, text))) } -func (w *Window) CPrint(pair ColorPair, attr Attr, text string) { - p, a := _colorFn(pair, attr) - C.wcolor_set(w.win(), p, nil) - C.wattron(w.win(), a) +func (w *CursesWindow) CPrint(color ColorPair, attr Attr, text string) { + p, a := _colorFn(color, attr) + C.wcolor_set(w.impl, p, nil) + C.wattron(w.impl, a) w.Print(text) - C.wattroff(w.win(), a) - C.wcolor_set(w.win(), 0, nil) + C.wattroff(w.impl, a) + C.wcolor_set(w.impl, 0, nil) } -func Clear() { +func (r *FullscreenRenderer) Clear() { C.clear() C.endwin() } -func Refresh() { +func (r *FullscreenRenderer) Refresh() { C.refresh() } -func (w *Window) Erase() { - C.werase(w.win()) +func (w *CursesWindow) Erase() { + C.werase(w.impl) } -func (w *Window) X() int { - return int(C.c_getcurx(w.win())) +func (w *CursesWindow) X() int { + return int(C.c_getcurx(w.impl)) } -func DoesAutoWrap() bool { +func (r *FullscreenRenderer) DoesAutoWrap() bool { return true } -func (w *Window) Fill(str string) bool { - return C.waddstr(w.win(), C.CString(str)) == C.OK +func (w *CursesWindow) Fill(str string) bool { + return C.waddstr(w.impl, C.CString(str)) == C.OK } -func (w *Window) CFill(str string, fg Color, bg Color, attr Attr) bool { - pair := PairFor(fg, bg) - C.wcolor_set(w.win(), C.short(pair), nil) - C.wattron(w.win(), C.int(attr)) +func (w *CursesWindow) CFill(fg Color, bg Color, attr Attr, str string) bool { + index := ColorPair{fg, bg, -1}.index() + C.wcolor_set(w.impl, C.short(index), nil) + C.wattron(w.impl, C.int(attr)) ret := w.Fill(str) - C.wattroff(w.win(), C.int(attr)) - C.wcolor_set(w.win(), 0, nil) + C.wattroff(w.impl, C.int(attr)) + C.wcolor_set(w.impl, 0, nil) return ret } -func RefreshWindows(windows []*Window) { +func (r *FullscreenRenderer) RefreshWindows(windows []Window) { for _, w := range windows { - C.wnoutrefresh(w.win()) + w.Refresh() } C.doupdate() } -func PairFor(fg Color, bg Color) ColorPair { - // ncurses does not support 24-bit colors - if fg.is24() || bg.is24() { - return ColDefault +func (p ColorPair) index() int16 { + if p.id >= 0 { + return p.id } - key := (int(fg) << 8) + int(bg) + + // ncurses does not support 24-bit colors + if p.is24() { + return ColDefault.index() + } + + key := p.key() if found, prs := _colorMap[key]; prs { return found } - id := ColorPair(len(_colorMap) + int(ColUser)) - C.init_pair(C.short(id), C.short(fg), C.short(bg)) + id := int16(len(_colorMap)) + ColUser.id + C.init_pair(C.short(id), C.short(p.Fg()), C.short(p.Bg())) _colorMap[key] = id return id } @@ -369,7 +371,7 @@ func escSequence() Event { return Event{Invalid, 0, nil} } -func GetChar() Event { +func (r *FullscreenRenderer) GetChar() Event { c := C.getch() switch c { case C.ERR: @@ -435,17 +437,17 @@ func GetChar() Event { /* Cannot use BUTTON1_DOUBLE_CLICKED due to mouseinterval(0) */ if (me.bstate & C.BUTTON1_PRESSED) > 0 { now := time.Now() - if now.Sub(_prevDownTime) < doubleClickDuration { - _clickY = append(_clickY, y) + if now.Sub(r.prevDownTime) < doubleClickDuration { + r.clickY = append(r.clickY, y) } else { - _clickY = []int{y} - _prevDownTime = now + r.clickY = []int{y} + r.prevDownTime = now } return Event{Mouse, 0, &MouseEvent{y, x, 0, true, false, mod}} } else if (me.bstate & C.BUTTON1_RELEASED) > 0 { double := false - if len(_clickY) > 1 && _clickY[0] == _clickY[1] && - time.Now().Sub(_prevDownTime) < doubleClickDuration { + if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && + time.Now().Sub(r.prevDownTime) < doubleClickDuration { double = true } return Event{Mouse, 0, &MouseEvent{y, x, 0, false, double, mod}} diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 1793836..460bfd5 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -18,30 +18,56 @@ import ( "github.com/junegunn/go-runewidth" ) -type ColorPair [2]Color - -func (p ColorPair) fg() Color { - return p[0] -} - -func (p ColorPair) bg() Color { - return p[1] -} - func (p ColorPair) style() tcell.Style { style := tcell.StyleDefault - return style.Foreground(tcell.Color(p.fg())).Background(tcell.Color(p.bg())) + return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg())) } type Attr tcell.Style -type WindowTcell struct { - LastX int - LastY int - MoveCursor bool - Border bool +type TcellWindow struct { + color bool + top int + left int + width int + height int + lastX int + lastY int + moveCursor bool + border bool +} + +func (w *TcellWindow) Top() int { + return w.top +} + +func (w *TcellWindow) Left() int { + return w.left +} + +func (w *TcellWindow) Width() int { + return w.width +} + +func (w *TcellWindow) Height() int { + return w.height +} + +func (w *TcellWindow) Refresh() { + if w.moveCursor { + _screen.ShowCursor(w.left+w.lastX, w.top+w.lastY) + w.moveCursor = false + } + w.lastX = 0 + w.lastY = 0 + if w.border { + w.drawBorder() + } +} + +func (w *TcellWindow) FinishFill() { + // NO-OP } -type WindowImpl WindowTcell const ( Bold Attr = Attr(tcell.AttrBold) @@ -56,33 +82,13 @@ const ( AttrRegular Attr = 0 ) -var ( - ColDefault = ColorPair{colDefault, colDefault} - ColNormal ColorPair - ColPrompt ColorPair - ColMatch ColorPair - ColCurrent ColorPair - ColCurrentMatch ColorPair - ColSpinner ColorPair - ColInfo ColorPair - ColCursor ColorPair - ColSelected ColorPair - ColHeader ColorPair - ColBorder ColorPair - ColUser ColorPair -) - -func DefaultTheme() *ColorTheme { +func (r *FullscreenRenderer) defaultTheme() *ColorTheme { if _screen.Colors() >= 256 { return Dark256 } return Default16 } -func PairFor(fg Color, bg Color) ColorPair { - return [2]Color{fg, bg} -} - var ( _colorToAttribute = []tcell.Color{ tcell.ColorBlack, @@ -112,10 +118,9 @@ func (a Attr) Merge(b Attr) Attr { var ( _screen tcell.Screen - _mouse bool ) -func initScreen() { +func (r *FullscreenRenderer) initScreen() { s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) @@ -125,7 +130,7 @@ func initScreen() { fmt.Fprintf(os.Stderr, "%v\n", e) os.Exit(2) } - if _mouse { + if r.mouse { s.EnableMouse() } else { s.DisableMouse() @@ -133,63 +138,41 @@ func initScreen() { _screen = s } -func Init(theme *ColorTheme, black bool, mouse bool) { +func (r *FullscreenRenderer) Init() { encoding.Register() - _mouse = mouse - initScreen() - - _color = theme != nil - if _color { - InitTheme(theme, black) - } else { - theme = DefaultTheme() - } - ColNormal = ColorPair{theme.Fg, theme.Bg} - ColPrompt = ColorPair{theme.Prompt, theme.Bg} - ColMatch = ColorPair{theme.Match, theme.Bg} - ColCurrent = ColorPair{theme.Current, theme.DarkBg} - ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg} - ColSpinner = ColorPair{theme.Spinner, theme.Bg} - ColInfo = ColorPair{theme.Info, theme.Bg} - ColCursor = ColorPair{theme.Cursor, theme.DarkBg} - ColSelected = ColorPair{theme.Selected, theme.DarkBg} - ColHeader = ColorPair{theme.Header, theme.Bg} - ColBorder = ColorPair{theme.Border, theme.Bg} + r.initScreen() + initTheme(r.theme, r.defaultTheme(), r.forceBlack) } -func MaxX() int { +func (r *FullscreenRenderer) MaxX() int { ncols, _ := _screen.Size() return int(ncols) } -func MaxY() int { +func (r *FullscreenRenderer) MaxY() int { _, nlines := _screen.Size() return int(nlines) } -func (w *Window) win() *WindowTcell { - return (*WindowTcell)(w.impl) +func (w *TcellWindow) X() int { + return w.lastX } -func (w *Window) X() int { - return w.impl.LastX -} - -func DoesAutoWrap() bool { +func (r *FullscreenRenderer) DoesAutoWrap() bool { return false } -func Clear() { +func (r *FullscreenRenderer) Clear() { _screen.Sync() _screen.Clear() } -func Refresh() { +func (r *FullscreenRenderer) Refresh() { // noop } -func GetChar() Event { +func (r *FullscreenRenderer) GetChar() Event { ev := _screen.PollEvent() switch ev := ev.(type) { case *tcell.EventResize: @@ -213,15 +196,15 @@ func GetChar() Event { double := false if down { now := time.Now() - if now.Sub(_prevDownTime) < doubleClickDuration { - _clickY = append(_clickY, x) + if now.Sub(r.prevDownTime) < doubleClickDuration { + r.clickY = append(r.clickY, x) } else { - _clickY = []int{x} - _prevDownTime = now + r.clickY = []int{x} + r.prevDownTime = now } } else { - if len(_clickY) > 1 && _clickY[0] == _clickY[1] && - time.Now().Sub(_prevDownTime) < doubleClickDuration { + if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && + time.Now().Sub(r.prevDownTime) < doubleClickDuration { double = true } } @@ -368,49 +351,39 @@ func GetChar() Event { return Event{Invalid, 0, nil} } -func Pause() { +func (r *FullscreenRenderer) Pause() { _screen.Fini() } -func Resume() bool { - initScreen() +func (r *FullscreenRenderer) Resume() bool { + r.initScreen() return true } -func Close() { +func (r *FullscreenRenderer) Close() { _screen.Fini() } -func RefreshWindows(windows []*Window) { +func (r *FullscreenRenderer) RefreshWindows(windows []Window) { // TODO for _, w := range windows { - if w.win().MoveCursor { - _screen.ShowCursor(w.Left+w.win().LastX, w.Top+w.win().LastY) - w.win().MoveCursor = false - } - w.win().LastX = 0 - w.win().LastY = 0 - if w.win().Border { - w.DrawBorder() - } + w.Refresh() } _screen.Show() } -func NewWindow(top int, left int, width int, height int, border bool) *Window { +func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { // TODO - win := new(WindowTcell) - win.Border = border - return &Window{ - impl: (*WindowImpl)(win), - Top: top, - Left: left, - Width: width, - Height: height, - } + return &TcellWindow{ + color: r.theme != nil, + top: top, + left: left, + width: width, + height: height, + border: border} } -func (w *Window) Close() { +func (w *TcellWindow) Close() { // TODO } @@ -422,40 +395,40 @@ func fill(x, y, w, h int, r rune) { } } -func (w *Window) Erase() { +func (w *TcellWindow) Erase() { // TODO - fill(w.Left, w.Top, w.Width, w.Height, ' ') + fill(w.left, w.top, w.width, w.height, ' ') } -func (w *Window) Enclose(y int, x int) bool { - return x >= w.Left && x <= (w.Left+w.Width) && - y >= w.Top && y <= (w.Top+w.Height) +func (w *TcellWindow) Enclose(y int, x int) bool { + return x >= w.left && x < (w.left+w.width) && + y >= w.top && y < (w.top+w.height) } -func (w *Window) Move(y int, x int) { - w.win().LastX = x - w.win().LastY = y - w.win().MoveCursor = true +func (w *TcellWindow) Move(y int, x int) { + w.lastX = x + w.lastY = y + w.moveCursor = true } -func (w *Window) MoveAndClear(y int, x int) { +func (w *TcellWindow) MoveAndClear(y int, x int) { w.Move(y, x) - for i := w.win().LastX; i < w.Width; i++ { - _screen.SetContent(i+w.Left, w.win().LastY+w.Top, rune(' '), nil, ColDefault.style()) + for i := w.lastX; i < w.width; i++ { + _screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, ColDefault.style()) } - w.win().LastX = x + w.lastX = x } -func (w *Window) Print(text string) { - w.PrintString(text, ColDefault, 0) +func (w *TcellWindow) Print(text string) { + w.printString(text, ColDefault, 0) } -func (w *Window) PrintString(text string, pair ColorPair, a Attr) { +func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) { t := text lx := 0 var style tcell.Style - if _color { + if w.color { style = pair.style(). Reverse(a&Attr(tcell.AttrReverse) != 0). Underline(a&Attr(tcell.AttrUnderline) != 0) @@ -481,7 +454,7 @@ func (w *Window) PrintString(text string, pair ColorPair, a Attr) { } if r == '\n' { - w.win().LastY++ + w.lastY++ lx = 0 } else { @@ -489,26 +462,26 @@ func (w *Window) PrintString(text string, pair ColorPair, a Attr) { continue } - var xPos = w.Left + w.win().LastX + lx - var yPos = w.Top + w.win().LastY - if xPos < (w.Left+w.Width) && yPos < (w.Top+w.Height) { + var xPos = w.left + w.lastX + lx + var yPos = w.top + w.lastY + if xPos < (w.left+w.width) && yPos < (w.top+w.height) { _screen.SetContent(xPos, yPos, r, nil, style) } lx += runewidth.RuneWidth(r) } } - w.win().LastX += lx + w.lastX += lx } -func (w *Window) CPrint(pair ColorPair, a Attr, text string) { - w.PrintString(text, pair, a) +func (w *TcellWindow) CPrint(pair ColorPair, attr Attr, text string) { + w.printString(text, pair, attr) } -func (w *Window) FillString(text string, pair ColorPair, a Attr) bool { +func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) bool { lx := 0 var style tcell.Style - if _color { + if w.color { style = pair.style() } else { style = ColDefault.style() @@ -522,22 +495,22 @@ func (w *Window) FillString(text string, pair ColorPair, a Attr) bool { for _, r := range text { if r == '\n' { - w.win().LastY++ - w.win().LastX = 0 + w.lastY++ + w.lastX = 0 lx = 0 } else { - var xPos = w.Left + w.win().LastX + lx + var xPos = w.left + w.lastX + lx // word wrap: - if xPos >= (w.Left + w.Width) { - w.win().LastY++ - w.win().LastX = 0 + if xPos >= (w.left + w.width) { + w.lastY++ + w.lastX = 0 lx = 0 - xPos = w.Left + xPos = w.left } - var yPos = w.Top + w.win().LastY + var yPos = w.top + w.lastY - if yPos >= (w.Top + w.Height) { + if yPos >= (w.top + w.height) { return false } @@ -545,27 +518,27 @@ func (w *Window) FillString(text string, pair ColorPair, a Attr) bool { lx += runewidth.RuneWidth(r) } } - w.win().LastX += lx + w.lastX += lx return true } -func (w *Window) Fill(str string) bool { - return w.FillString(str, ColDefault, 0) +func (w *TcellWindow) Fill(str string) bool { + return w.fillString(str, ColDefault, 0) } -func (w *Window) CFill(str string, fg Color, bg Color, a Attr) bool { - return w.FillString(str, ColorPair{fg, bg}, a) +func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) bool { + return w.fillString(str, ColorPair{fg, bg, -1}, a) } -func (w *Window) DrawBorder() { - left := w.Left - right := left + w.Width - top := w.Top - bot := top + w.Height +func (w *TcellWindow) drawBorder() { + left := w.left + right := left + w.width + top := w.top + bot := top + w.height var style tcell.Style - if _color { + if w.color { style = ColBorder.style() } else { style = ColDefault.style() diff --git a/src/tui/tui.go b/src/tui/tui.go index 125611c..859eed7 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -115,6 +115,32 @@ const ( colWhite ) +type ColorPair struct { + fg Color + bg Color + id int16 +} + +func NewColorPair(fg Color, bg Color) ColorPair { + return ColorPair{fg, bg, -1} +} + +func (p ColorPair) Fg() Color { + return p.fg +} + +func (p ColorPair) Bg() Color { + return p.bg +} + +func (p ColorPair) key() int { + return (int(p.Fg()) << 8) + int(p.Bg()) +} + +func (p ColorPair) is24() bool { + return p.Fg().is24() || p.Bg().is24() +} + type ColorTheme struct { Fg Color Bg Color @@ -146,23 +172,84 @@ type MouseEvent struct { Mod bool } -var ( - _color bool - _prevDownTime time.Time - _clickY []int - Default16 *ColorTheme - Dark256 *ColorTheme - Light256 *ColorTheme -) +type Renderer interface { + Init() + Pause() + Resume() bool + Clear() + RefreshWindows(windows []Window) + Refresh() + Close() -type Window struct { - impl *WindowImpl - Top int - Left int - Width int - Height int + GetChar() Event + + MaxX() int + MaxY() int + DoesAutoWrap() bool + + NewWindow(top int, left int, width int, height int, border bool) Window } +type Window interface { + Top() int + Left() int + Width() int + Height() int + + Refresh() + FinishFill() + Close() + + X() int + Enclose(y int, x int) bool + + Move(y int, x int) + MoveAndClear(y int, x int) + Print(text string) + CPrint(color ColorPair, attr Attr, text string) + Fill(text string) bool + CFill(fg Color, bg Color, attr Attr, text string) bool + Erase() +} + +type FullscreenRenderer struct { + theme *ColorTheme + mouse bool + forceBlack bool + prevDownTime time.Time + clickY []int +} + +func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer { + r := &FullscreenRenderer{ + theme: theme, + mouse: mouse, + forceBlack: forceBlack, + prevDownTime: time.Unix(0, 0), + clickY: []int{}} + return r +} + +var ( + Default16 *ColorTheme + Dark256 *ColorTheme + Light256 *ColorTheme + + ColDefault ColorPair + ColNormal ColorPair + ColPrompt ColorPair + ColMatch ColorPair + ColCurrent ColorPair + ColCurrentMatch ColorPair + ColSpinner ColorPair + ColInfo ColorPair + ColCursor ColorPair + ColSelected ColorPair + ColHeader ColorPair + ColBorder ColorPair + ColUser ColorPair +) + func EmptyTheme() *ColorTheme { return &ColorTheme{ Fg: colUndefined, @@ -181,8 +268,6 @@ func EmptyTheme() *ColorTheme { } func init() { - _prevDownTime = time.Unix(0, 0) - _clickY = []int{} Default16 = &ColorTheme{ Fg: colDefault, Bg: colDefault, @@ -227,14 +312,13 @@ func init() { Border: 145} } -func InitTheme(theme *ColorTheme, black bool) { - _color = theme != nil - if !_color { +func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) { + if theme == nil { + initPalette(theme) return } - baseTheme := DefaultTheme() - if black { + if forceBlack { theme.Bg = colBlack } @@ -257,4 +341,48 @@ func InitTheme(theme *ColorTheme, black bool) { theme.Selected = o(baseTheme.Selected, theme.Selected) theme.Header = o(baseTheme.Header, theme.Header) theme.Border = o(baseTheme.Border, theme.Border) + + initPalette(theme) +} + +func initPalette(theme *ColorTheme) { + ColDefault = ColorPair{colDefault, colDefault, 0} + if theme != nil { + ColNormal = ColorPair{theme.Fg, theme.Bg, 1} + ColPrompt = ColorPair{theme.Prompt, theme.Bg, 2} + ColMatch = ColorPair{theme.Match, theme.Bg, 3} + ColCurrent = ColorPair{theme.Current, theme.DarkBg, 4} + ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg, 5} + ColSpinner = ColorPair{theme.Spinner, theme.Bg, 6} + ColInfo = ColorPair{theme.Info, theme.Bg, 7} + ColCursor = ColorPair{theme.Cursor, theme.DarkBg, 8} + ColSelected = ColorPair{theme.Selected, theme.DarkBg, 9} + ColHeader = ColorPair{theme.Header, theme.Bg, 10} + ColBorder = ColorPair{theme.Border, theme.Bg, 11} + } else { + ColNormal = ColorPair{colDefault, colDefault, 1} + ColPrompt = ColorPair{colDefault, colDefault, 2} + ColMatch = ColorPair{colDefault, colDefault, 3} + ColCurrent = ColorPair{colDefault, colDefault, 4} + ColCurrentMatch = ColorPair{colDefault, colDefault, 5} + ColSpinner = ColorPair{colDefault, colDefault, 6} + ColInfo = ColorPair{colDefault, colDefault, 7} + ColCursor = ColorPair{colDefault, colDefault, 8} + ColSelected = ColorPair{colDefault, colDefault, 9} + ColHeader = ColorPair{colDefault, colDefault, 10} + ColBorder = ColorPair{colDefault, colDefault, 11} + } + ColUser = ColorPair{colDefault, colDefault, 12} +} + +func attrFor(color ColorPair, attr Attr) Attr { + switch color { + case ColCurrent: + return attr | Reverse + case ColMatch: + return attr | Underline + case ColCurrentMatch: + return attr | Underline | Reverse + } + return attr } diff --git a/src/tui/tui_test.go b/src/tui/tui_test.go deleted file mode 100644 index 4a2fee9..0000000 --- a/src/tui/tui_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package tui - -import ( - "testing" -) - -func TestPairFor(t *testing.T) { - if PairFor(30, 50) != PairFor(30, 50) { - t.Fail() - } - if PairFor(-1, 10) != PairFor(-1, 10) { - t.Fail() - } -} diff --git a/src/util/util.go b/src/util/util.go index 2a1607c..29e8017 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -6,8 +6,24 @@ import ( "time" "github.com/junegunn/go-isatty" + "github.com/junegunn/go-runewidth" ) +var _runeWidths = make(map[rune]int) + +// RuneWidth returns rune width +func RuneWidth(r rune, prefixWidth int, tabstop int) int { + if r == '\t' { + return tabstop - prefixWidth%tabstop + } else if w, found := _runeWidths[r]; found { + return w + } else { + w := runewidth.RuneWidth(r) + _runeWidths[r] = w + return w + } +} + // Max returns the largest integer func Max(first int, second int) int { if first >= second { diff --git a/src/util/util_unix.go b/src/util/util_unix.go index 29e0d30..bc1b7b5 100644 --- a/src/util/util_unix.go +++ b/src/util/util_unix.go @@ -5,6 +5,7 @@ package util import ( "os" "os/exec" + "syscall" ) // ExecCommand executes the given command with $SHELL @@ -20,3 +21,8 @@ func ExecCommand(command string) *exec.Cmd { func IsWindows() bool { return false } + +// SetNonBlock executes syscall.SetNonblock on file descriptor +func SetNonblock(file *os.File, nonblock bool) { + syscall.SetNonblock(int(file.Fd()), nonblock) +} diff --git a/src/util/util_windows.go b/src/util/util_windows.go index 3aa8660..9ba4f79 100644 --- a/src/util/util_windows.go +++ b/src/util/util_windows.go @@ -5,6 +5,7 @@ package util import ( "os" "os/exec" + "syscall" "github.com/junegunn/go-shellwords" ) @@ -26,3 +27,8 @@ func ExecCommand(command string) *exec.Cmd { func IsWindows() bool { return true } + +// SetNonBlock executes syscall.SetNonblock on file descriptor +func SetNonblock(file *os.File, nonblock bool) { + syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock) +} diff --git a/test/test_go.rb b/test/test_go.rb index 4cd4a4f..55422aa 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -117,8 +117,28 @@ class Tmux wait do lines = capture(pane) class << lines + def counts + self.lazy + .map { |l| l.scan /^. ([0-9]+)\/([0-9]+)( \(([0-9]+)\))?/ } + .reject(&:empty?) + .first&.first&.map(&:to_i)&.values_at(0, 1, 3) || [0, 0, 0] + end + + def match_count + counts[0] + end + def item_count - self[-2] ? self[-2].strip.split('/').last.to_i : 0 + counts[1] + end + + def select_count + counts[2] + end + + def any_include? val + method = val.is_a?(Regexp) ? :match : :include? + self.select { |line| line.send method, val }.first end end yield lines @@ -163,6 +183,11 @@ class TestBase < Minitest::Test @temp_suffix].join '-' end + def writelines path, lines + File.unlink path while File.exists? path + File.open(path, 'w') { |f| f << lines.join($/) + $/ } + end + def readonce wait { File.exists?(tempname) } File.read(tempname) @@ -1190,12 +1215,6 @@ class TestGoFZF < TestBase tmux.send_keys '?' tmux.until { |lines| lines[-1] == '> 555' } end - -private - def writelines path, lines - File.unlink path while File.exists? path - File.open(path, 'w') { |f| f << lines.join($/) + $/ } - end end module TestShell @@ -1213,79 +1232,60 @@ module TestShell tmux.prepare end - def test_ctrl_t + def unset_var name + tmux.prepare + tmux.send_keys "unset #{name}", :Enter tmux.prepare - tmux.send_keys 'C-t', pane: 0 - lines = tmux.until(1) { |lines| lines.item_count > 1 } - expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') - 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.send_keys 'C-c' - - # FZF_TMUX=0 - new_shell - tmux.send_keys 'C-t', pane: 0 - lines = tmux.until(0) { |lines| lines.item_count > 1 } - expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ') - 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.send_keys 'C-c', 'C-d' end - def test_ctrl_t_command + def test_ctrl_t 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' } + + tmux.prepare + tmux.send_keys 'C-t' + lines = tmux.until { |lines| lines.item_count == 100 } + tmux.send_keys :Tab, :Tab, :Tab + tmux.until { |lines| lines.any_include? ' (3)' } + tmux.send_keys :Enter + tmux.until { |lines| lines.any_include? '1 2 3' } + tmux.send_keys 'C-c' end def test_ctrl_t_unicode - FileUtils.mkdir_p '/tmp/fzf-test' - tmux.paste 'cd /tmp/fzf-test; echo -n test1 > "fzf-unicode 테스트1"; echo -n test2 > "fzf-unicode 테스트2"' + writelines tempname, ['fzf-unicode 테스트1', 'fzf-unicode 테스트2'] + set_var "FZF_CTRL_T_COMMAND", "cat #{tempname}" + tmux.prepare - tmux.send_keys 'cat ', 'C-t', pane: 0 - tmux.until(1) { |lines| lines.item_count >= 1 } - tmux.send_keys 'fzf-unicode', pane: 1 - redraw = ->() { tmux.send_keys 'C-l', pane: 1 } - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) } + tmux.send_keys 'echo ', 'C-t' + tmux.until { |lines| lines.item_count == 2 } + tmux.send_keys 'fzf-unicode' + tmux.until { |lines| lines.match_count == 2 } - tmux.send_keys '1', pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) } - tmux.send_keys :BTab, pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2].include? '(1)' } + tmux.send_keys '1' + tmux.until { |lines| lines.match_count == 1 } + tmux.send_keys :Tab + tmux.until { |lines| lines.select_count == 1 } - tmux.send_keys :BSpace, pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) } + tmux.send_keys :BSpace + tmux.until { |lines| lines.match_count == 2 } - tmux.send_keys '2', pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) } - tmux.send_keys :BTab, pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2].include? '(2)' } + tmux.send_keys '2' + tmux.until { |lines| lines.match_count == 1 } + tmux.send_keys :Tab + tmux.until { |lines| lines.select_count == 2 } - tmux.send_keys :Enter, pane: 1 - tmux.until do |lines| - tmux.send_keys 'C-l' - [-1, -2].map { |offset| lines[offset] }.any? do |line| - line.start_with?('cat') && line.include?('fzf-unicode') - end - end tmux.send_keys :Enter - tmux.until { |lines| lines[-1].include? 'test1test2' } + tmux.until { |lines| lines.any_include? /echo.*fzf-unicode.*1.*fzf-unicode.*2/ } + tmux.send_keys :Enter + tmux.until { |lines| lines.any_include? /^fzf-unicode.*1.*fzf-unicode.*2/ } end def test_alt_c tmux.prepare - tmux.send_keys :Escape, :c, pane: 0 - lines = tmux.until(1) { |lines| lines.item_count > 0 && lines[-3][2..-1] } - expected = lines[-3][2..-1] - tmux.send_keys :Enter, pane: 1 + tmux.send_keys :Escape, :c + lines = tmux.until { |lines| lines.item_count > 0 } + expected = lines.reverse.select { |l| l.start_with? '>' }.first[2..-1] + tmux.send_keys :Enter tmux.prepare tmux.send_keys :pwd, :Enter tmux.until { |lines| lines[-1].end_with?(expected) } @@ -1298,9 +1298,9 @@ module TestShell tmux.send_keys 'cd /', :Enter tmux.prepare - tmux.send_keys :Escape, :c, pane: 0 - lines = tmux.until(1) { |lines| lines.item_count == 1 } - tmux.send_keys :Enter, pane: 1 + tmux.send_keys :Escape, :c + lines = tmux.until { |lines| lines.item_count == 1 } + tmux.send_keys :Enter tmux.prepare tmux.send_keys :pwd, :Enter @@ -1314,11 +1314,11 @@ module TestShell tmux.send_keys 'echo 3d', :Enter; tmux.prepare tmux.send_keys 'echo 3rd', :Enter; tmux.prepare tmux.send_keys 'echo 4th', :Enter; tmux.prepare - tmux.send_keys 'C-r', pane: 0 - tmux.until(1) { |lines| lines.item_count > 0 } - tmux.send_keys '3d', pane: 1 - tmux.until(1) { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort - tmux.send_keys :Enter, pane: 1 + tmux.send_keys 'C-r' + tmux.until { |lines| lines.item_count > 0 } + tmux.send_keys '3d' + tmux.until { |lines| lines[-3].end_with? 'echo 3rd' } + tmux.send_keys :Enter tmux.until { |lines| lines[-1] == 'echo 3rd' } tmux.send_keys :Enter tmux.until { |lines| lines[-1] == '3rd' } @@ -1334,12 +1334,12 @@ module CompletionTest FileUtils.touch File.expand_path(f) end tmux.prepare - tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab, pane: 0 - tmux.until(1) { |lines| lines.item_count > 0 } + tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab + tmux.until { |lines| lines.item_count > 0 } tmux.send_keys ' !d' - tmux.until(1) { |lines| lines[-2].include?(' 2/') } - tmux.send_keys :BTab, :BTab - tmux.until(1) { |lines| lines[-2].include?('(2)') } + tmux.until { |lines| lines.match_count == 2 } + tmux.send_keys :Tab, :Tab + tmux.until { |lines| lines.select_count == 2 } tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' @@ -1349,10 +1349,10 @@ module CompletionTest # ~USERNAME** tmux.send_keys 'C-u' - tmux.send_keys "cat ~#{ENV['USER']}**", :Tab, pane: 0 - tmux.until(1) { |lines| lines.item_count > 0 } - tmux.send_keys '.fzf-home' - tmux.until(1) { |lines| lines[-3].end_with? '.fzf-home' } + tmux.send_keys "cat ~#{ENV['USER']}**", :Tab + tmux.until { |lines| lines.item_count > 0 } + tmux.send_keys "'.fzf-home" + tmux.until { |lines| lines.select { |l| l.include? '.fzf-home' }.count > 1 } tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' @@ -1361,8 +1361,8 @@ module CompletionTest # ~INVALID_USERNAME** tmux.send_keys 'C-u' - tmux.send_keys "cat ~such**", :Tab, pane: 0 - tmux.until(1) { |lines| lines[-3].end_with? 'no~such~user' } + tmux.send_keys "cat ~such**", :Tab + tmux.until { |lines| lines.any_include? 'no~such~user' } tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' @@ -1371,9 +1371,11 @@ module CompletionTest # /tmp/fzf\ test** tmux.send_keys 'C-u' - tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab, pane: 0 - tmux.until(1) { |lines| lines.item_count > 0 } - tmux.send_keys 'C-K', :Enter + tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab + tmux.until { |lines| lines.item_count > 0 } + tmux.send_keys 'foobar$' + tmux.until { |lines| lines.match_count == 1 } + tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' lines[-1].end_with?('/tmp/fzf\ test/foobar') @@ -1382,11 +1384,10 @@ module CompletionTest # Should include hidden files (1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" } tmux.send_keys 'C-u' - tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab, pane: 0 - tmux.until(1) do |lines| + tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab + tmux.until do |lines| tmux.send_keys 'C-L' - lines[-2].include?('100/') && - lines[-3].include?('/tmp/fzf-test/.hidden-') + lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-') end tmux.send_keys :Enter ensure @@ -1396,19 +1397,22 @@ module CompletionTest end def test_file_completion_root - tmux.send_keys 'ls /**', :Tab, pane: 0 - tmux.until(1) { |lines| lines.item_count > 0 } + tmux.send_keys 'ls /**', :Tab + tmux.until { |lines| lines.item_count > 0 } tmux.send_keys :Enter end def test_dir_completion - tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}; touch /tmp/fzf-test/d55/xxx', :Enter + (1..100).each do |idx| + FileUtils.mkdir_p "/tmp/fzf-test/d#{idx}" + end + FileUtils.touch '/tmp/fzf-test/d55/xxx' tmux.prepare - tmux.send_keys 'cd /tmp/fzf-test/**', :Tab, pane: 0 - tmux.until(1) { |lines| lines.item_count > 0 } - tmux.send_keys :BTab, :BTab # BTab does not work here + tmux.send_keys 'cd /tmp/fzf-test/**', :Tab + tmux.until { |lines| lines.item_count > 0 } + tmux.send_keys :Tab, :Tab # Tab does not work here tmux.send_keys 55 - tmux.until(1) { |lines| lines[-2].start_with? ' 1/' } + tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' @@ -1435,14 +1439,15 @@ module CompletionTest lines = tmux.until { |lines| lines[-1].start_with? '[1]' } pid = lines[-1].split.last tmux.prepare - tmux.send_keys 'kill ', :Tab, pane: 0 - tmux.until(1) { |lines| lines.item_count > 0 } + tmux.send_keys 'C-L' + tmux.send_keys 'kill ', :Tab + tmux.until { |lines| lines.item_count > 0 } tmux.send_keys 'sleep12345' - tmux.until(1) { |lines| lines[-3].include? 'sleep 12345' } + tmux.until { |lines| lines.any_include? 'sleep 12345' } tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' - lines[-1] == "kill #{pid}" + lines[-1].include? "kill #{pid}" end ensure Process.kill 'KILL', pid.to_i rescue nil if pid @@ -1451,10 +1456,10 @@ module CompletionTest def test_custom_completion tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter tmux.prepare - tmux.send_keys 'ls /tmp/**', :Tab, pane: 0 - tmux.until(1) { |lines| lines.item_count == 11 } - tmux.send_keys :BTab, :BTab, :BTab - tmux.until(1) { |lines| lines[-2].include? '(3)' } + tmux.send_keys 'ls /tmp/**', :Tab + tmux.until { |lines| lines.item_count == 11 } + tmux.send_keys :Tab, :Tab, :Tab + tmux.until { |lines| lines.select_count == 3 } tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-L' @@ -1463,49 +1468,48 @@ module CompletionTest end def test_unset_completion - tmux.send_keys 'export FOO=BAR', :Enter + tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter tmux.prepare # Using tmux - tmux.send_keys 'unset FOO**', :Tab, pane: 0 - tmux.until(1) { |lines| lines[-2].include? ' 1/' } + tmux.send_keys 'unset FZFFOO**', :Tab + tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Enter - tmux.until { |lines| lines[-1] == 'unset FOO' } + tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' } tmux.send_keys 'C-c' - # FZF_TMUX=0 + # FZF_TMUX=1 new_shell - tmux.send_keys 'unset FOO**', :Tab - tmux.until { |lines| lines[-2].include? ' 1/' } + tmux.send_keys 'unset FZFFO**', :Tab, pane: 0 + tmux.until(1) { |lines| lines.match_count == 1 } tmux.send_keys :Enter - tmux.until { |lines| lines[-1] == 'unset FOO' } + tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' } end def test_file_completion_unicode FileUtils.mkdir_p '/tmp/fzf-test' tmux.paste 'cd /tmp/fzf-test; echo -n test3 > "fzf-unicode 테스트1"; echo -n test4 > "fzf-unicode 테스트2"' tmux.prepare - tmux.send_keys 'cat fzf-unicode**', :Tab, pane: 0 - redraw = ->() { tmux.send_keys 'C-l', pane: 1 } - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) } + tmux.send_keys 'cat fzf-unicode**', :Tab + tmux.until { |lines| lines.match_count == 2 } - tmux.send_keys '1', pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) } - tmux.send_keys :BTab, pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2].include? '(1)' } + tmux.send_keys '1' + tmux.until { |lines| lines.match_count == 1 } + tmux.send_keys :Tab + tmux.until { |lines| lines.select_count == 1 } - tmux.send_keys :BSpace, pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *2/) } + tmux.send_keys :BSpace + tmux.until { |lines| lines.match_count == 2 } - tmux.send_keys '2', pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2] =~ %r(^ *1/) } - tmux.send_keys :BTab, pane: 1 - tmux.until(1) { |lines| redraw.(); lines[-2].include? '(2)' } + tmux.send_keys '2' + tmux.until { |lines| lines.select_count == 1 } + tmux.send_keys :Tab + tmux.until { |lines| lines.select_count == 2 } - tmux.send_keys :Enter, pane: 1 + tmux.send_keys :Enter tmux.until do |lines| tmux.send_keys 'C-l' - lines[-1].include?('cat') || lines[-2].include?('cat') + lines.any_include? 'cat' end tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'test3test4' } @@ -1518,7 +1522,7 @@ class TestBash < TestBase def new_shell tmux.prepare - tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter + tmux.send_keys "FZF_TMUX=1 #{Shell.bash}", :Enter tmux.prepare end @@ -1533,7 +1537,7 @@ class TestZsh < TestBase include CompletionTest def new_shell - tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter + tmux.send_keys "FZF_TMUX=1 #{Shell.zsh}", :Enter tmux.prepare end @@ -1547,7 +1551,7 @@ class TestFish < TestBase include TestShell def new_shell - tmux.send_keys 'env FZF_TMUX=0 fish', :Enter + tmux.send_keys 'env FZF_TMUX=1 fish', :Enter tmux.send_keys 'function fish_prompt; end; clear', :Enter tmux.until { |lines| lines.empty? } end From 1a50f1eca1ce81da30cdf47291a485aea1a6431a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 8 Jan 2017 02:06:39 +0900 Subject: [PATCH 350/658] [vim] Use --height instead of fzf-tmux --- plugin/fzf.vim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index c8c6c91..9c9c6db 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -298,7 +298,8 @@ try endif let use_height = has_key(dict, 'down') && - \ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right')) + \ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right')) && + \ executable('tput') && filereadable('/dev/tty') let tmux = !use_height && (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict) let term = has('nvim') && !tmux if use_height From a30999a78528b3c773b86ca5bd21a0220116c9de Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 8 Jan 2017 02:09:31 +0900 Subject: [PATCH 351/658] Prepare for 0.16.0 release --- README.md | 11 ++++------- install | 13 +++++++------ man/man1/fzf-tmux.1 | 4 ++-- man/man1/fzf.1 | 4 ++-- src/constants.go | 2 +- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 550ea77..de69d01 100644 --- a/README.md +++ b/README.md @@ -496,12 +496,9 @@ valid directory. Example: set -l FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'" ``` -License -------- +[License](LICENSE) +------------------ -[MIT](LICENSE) +The MIT License (MIT) -Author ------- - -Junegunn Choi +Copyright (c) 2017 Junegunn Choi diff --git a/install b/install index 58a42e3..7a2481f 100755 --- a/install +++ b/install @@ -2,9 +2,7 @@ set -u -[[ "$@" =~ --pre ]] && version=0.15.9 pre=1 || - version=0.15.9 pre=0 - +version=0.16.0-alpha auto_completion= key_bindings= update_config=2 @@ -48,7 +46,7 @@ for opt in "$@"; do --no-update-rc) update_config=0 ;; --32) binary_arch=386 ;; --64) binary_arch=amd64 ;; - --bin|--pre) ;; + --bin) ;; *) echo "unknown option: $opt" help @@ -121,7 +119,7 @@ try_wget() { download() { echo "Downloading bin/fzf ..." - if [ $pre = 0 ]; then + if [[ ! "$version" =~ alpha ]]; then if [ -x "$fzf_base"/bin/fzf ]; then echo " - Already exists" check_binary && return @@ -137,7 +135,10 @@ download() { return fi - local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz + local url + [[ "$version" =~ alpha ]] && + url=https://github.com/junegunn/fzf-bin/releases/download/alpha/${1}.tgz || + url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz set -o pipefail if ! (try_curl $url || try_wget $url); then set +o pipefail diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 6f21139..6ccdb66 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -1,7 +1,7 @@ .ig The MIT License (MIT) -Copyright (c) 2016 Junegunn Choi +Copyright (c) 2017 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -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 "Nov 2016" "fzf 0.15.9" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Jan 2017" "fzf 0.16.0" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index fac2aab..b6540e1 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -1,7 +1,7 @@ .ig The MIT License (MIT) -Copyright (c) 2016 Junegunn Choi +Copyright (c) 2017 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -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 "Nov 2016" "fzf 0.15.9" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jan 2017" "fzf 0.16.0" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index b9def74..9640dbe 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.15.9" + version = "0.16.0-alpha" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 9d545f9578b7cf154543bb645cb6da0f01801e5d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 8 Jan 2017 02:29:31 +0900 Subject: [PATCH 352/658] Fix update of multi-select pointer --- src/terminal.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 8002923..c653327 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -41,9 +41,10 @@ type previewer struct { } type itemLine struct { - current bool - label string - result Result + current bool + selected bool + label string + result Result } var emptyLine = itemLine{} @@ -663,7 +664,7 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) { } // Avoid unnecessary redraw - newLine := itemLine{current, label, *result} + newLine := itemLine{current, selected, label, *result} if t.prevLines[i] == newLine { return } From 45793d75c2d168d51be3578b8d785159ec81ff92 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Jan 2017 03:12:23 +0900 Subject: [PATCH 353/658] Add --normalize option to normalize latin script characters Close #790 --- CHANGELOG.md | 2 + man/man1/fzf.1 | 4 + src/algo/algo.go | 119 ++++++++++-- src/algo/algo_test.go | 21 ++- src/algo/normalize.go | 408 ++++++++++++++++++++++++++++++++++++++++++ src/core.go | 2 +- src/options.go | 7 + src/pattern.go | 14 +- src/pattern_test.go | 24 +-- 9 files changed, 563 insertions(+), 38 deletions(-) create mode 100644 src/algo/normalize.go diff --git a/CHANGELOG.md b/CHANGELOG.md index f78270c..fbc1e41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ CHANGELOG - Added `--height HEIGHT[%]` option - Preview window will truncate long lines by default. Line wrap can be enabled by `:wrap` flag in `--preview-window`. +- Added `--normalize` option to normalize latin script letters before + matching. e.g. `sodanco` can match `Só Danço Samba`. 0.15.9 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index b6540e1..1bcfb63 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -48,6 +48,10 @@ Case-insensitive match (default: smart-case match) .B "+i" Case-sensitive match .TP +.B "--normalize" +Normalize latin script letters before matching. This is not enabled by default +to avoid performance overhead. +.TP .BI "--algo=" TYPE Fuzzy matching algorithm (default: v2) diff --git a/src/algo/algo.go b/src/algo/algo.go index 1b85594..2a3bc9d 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -234,9 +234,36 @@ func bonusAt(input util.Chars, idx int) int16 { return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx))) } -type Algo func(caseSensitive bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) +func normalizeRune(r rune) rune { + if r < 0x00C0 || r > 0x2184 { + return r + } -func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { + n := normalized[r] + if n > 0 { + return n + } + return r +} + +func normalizeRunes(runes []rune) []rune { + ret := make([]rune, len(runes)) + copy(ret, runes) + for idx, r := range runes { + if r < 0x00C0 || r > 0x2184 { + continue + } + n := normalized[r] + if n > 0 { + ret[idx] = normalized[r] + } + } + return ret +} + +type Algo func(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) + +func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { // Assume that pattern is given in lowercase if case-insensitive. // First check if there's a match and calculate bonus for each position. // If the input string is too long, consider finding the matching chars in @@ -247,13 +274,17 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern [] case 0: return Result{0, 0, 0}, posArray(withPos, M) case 1: - return ExactMatchNaive(caseSensitive, forward, input, pattern[0:1], withPos, slab) + return ExactMatchNaive(caseSensitive, normalize, forward, input, pattern[0:1], withPos, slab) } // Since O(nm) algorithm can be prohibitively expensive for large input, // we fall back to the greedy algorithm. if slab != nil && N*M > cap(slab.I16) { - return FuzzyMatchV1(caseSensitive, forward, input, pattern, withPos, slab) + return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab) + } + + if normalize { + pattern = normalizeRunes(pattern) } // Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages @@ -285,6 +316,10 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern [] } } + if normalize { + char = normalizeRune(char) + } + T[idx] = char B[idx] = bonusFor(prevClass, class) prevClass = class @@ -432,7 +467,7 @@ func FuzzyMatchV2(caseSensitive bool, forward bool, input util.Chars, pattern [] } // Implement the same sorting criteria as V2 -func calculateScore(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) { +func calculateScore(caseSensitive bool, normalize bool, text util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) { pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0) pos := posArray(withPos, len(pattern)) prevClass := charNonWord @@ -449,6 +484,10 @@ func calculateScore(caseSensitive bool, text util.Chars, pattern []rune, sidx in char = unicode.To(unicode.LowerCase, char) } } + // pattern is already normalized + if normalize { + char = normalizeRune(char) + } if char == pattern[pidx] { if withPos { *pos = append(*pos, idx) @@ -488,7 +527,7 @@ func calculateScore(caseSensitive bool, text util.Chars, pattern []rune, sidx in } // FuzzyMatchV1 performs fuzzy-match -func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { if len(pattern) == 0 { return Result{0, 0, 0}, nil } @@ -500,6 +539,10 @@ func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []r lenRunes := text.Length() lenPattern := len(pattern) + if normalize { + pattern = normalizeRunes(pattern) + } + for index := 0; index < lenRunes; index++ { char := text.Get(indexAt(index, lenRunes, forward)) // This is considerably faster than blindly applying strings.ToLower to the @@ -514,6 +557,9 @@ func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []r char = unicode.To(unicode.LowerCase, char) } } + if normalize { + char = normalizeRune(char) + } pchar := pattern[indexAt(pidx, lenPattern, forward)] if char == pchar { if sidx < 0 { @@ -553,7 +599,7 @@ func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []r sidx, eidx = lenRunes-eidx, lenRunes-sidx } - score, pos := calculateScore(caseSensitive, text, pattern, sidx, eidx, withPos) + score, pos := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, withPos) return Result{sidx, eidx, score}, pos } return Result{-1, -1, 0}, nil @@ -568,7 +614,7 @@ func FuzzyMatchV1(caseSensitive bool, forward bool, text util.Chars, pattern []r // bonus point, instead of stopping immediately after finding the first match. // The solution is much cheaper since there is only one possible alignment of // the pattern. -func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { if len(pattern) == 0 { return Result{0, 0, 0}, nil } @@ -580,6 +626,10 @@ func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern return Result{-1, -1, 0}, nil } + if normalize { + pattern = normalizeRunes(pattern) + } + // For simplicity, only look at the bonus at the first character position pidx := 0 bestPos, bonus, bestBonus := -1, int16(0), int16(-1) @@ -593,6 +643,9 @@ func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern char = unicode.To(unicode.LowerCase, char) } } + if normalize { + char = normalizeRune(char) + } pidx_ := indexAt(pidx, lenPattern, forward) pchar := pattern[pidx_] if pchar == char { @@ -624,14 +677,14 @@ func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern sidx = lenRunes - (bestPos + 1) eidx = lenRunes - (bestPos - lenPattern + 1) } - score, _ := calculateScore(caseSensitive, text, pattern, sidx, eidx, false) + score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false) return Result{sidx, eidx, score}, nil } return Result{-1, -1, 0}, nil } // PrefixMatch performs prefix-match -func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { if len(pattern) == 0 { return Result{0, 0, 0}, nil } @@ -640,22 +693,29 @@ func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru return Result{-1, -1, 0}, nil } + if normalize { + pattern = normalizeRunes(pattern) + } + for index, r := range pattern { char := text.Get(index) if !caseSensitive { char = unicode.ToLower(char) } + if normalize { + char = normalizeRune(char) + } if char != r { return Result{-1, -1, 0}, nil } } lenPattern := len(pattern) - score, _ := calculateScore(caseSensitive, text, pattern, 0, lenPattern, false) + score, _ := calculateScore(caseSensitive, normalize, text, pattern, 0, lenPattern, false) return Result{0, lenPattern, score}, nil } // SuffixMatch performs suffix-match -func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { lenRunes := text.Length() trimmedLen := lenRunes - text.TrailingWhitespaces() if len(pattern) == 0 { @@ -666,11 +726,18 @@ func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru return Result{-1, -1, 0}, nil } + if normalize { + pattern = normalizeRunes(pattern) + } + for index, r := range pattern { char := text.Get(index + diff) if !caseSensitive { char = unicode.ToLower(char) } + if normalize { + char = normalizeRune(char) + } if char != r { return Result{-1, -1, 0}, nil } @@ -678,21 +745,37 @@ func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru lenPattern := len(pattern) sidx := trimmedLen - lenPattern eidx := trimmedLen - score, _ := calculateScore(caseSensitive, text, pattern, sidx, eidx, false) + score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false) return Result{sidx, eidx, score}, nil } // EqualMatch performs equal-match -func EqualMatch(caseSensitive bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func EqualMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { lenPattern := len(pattern) if text.Length() != lenPattern { return Result{-1, -1, 0}, nil } - runesStr := text.ToString() - if !caseSensitive { - runesStr = strings.ToLower(runesStr) + match := true + if normalize { + runes := text.ToRunes() + for idx, pchar := range pattern { + char := runes[idx] + if !caseSensitive { + char = unicode.To(unicode.LowerCase, char) + } + if normalizeRune(pchar) != normalizeRune(char) { + match = false + break + } + } + } else { + runesStr := text.ToString() + if !caseSensitive { + runesStr = strings.ToLower(runesStr) + } + match = runesStr == string(pattern) } - if runesStr == string(pattern) { + if match { return Result{0, lenPattern, (scoreMatch+bonusBoundary)*lenPattern + (bonusFirstCharMultiplier-1)*bonusBoundary}, nil } diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index fc24f6d..df8b227 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -10,10 +10,14 @@ import ( ) func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) { + assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score) +} + +func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool, input, pattern string, sidx int, eidx int, score int) { if !caseSensitive { pattern = strings.ToLower(pattern) } - res, pos := fun(caseSensitive, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil) + res, pos := fun(caseSensitive, normalize, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil) var start, end int if pos == nil || len(*pos) == 0 { start = res.Start @@ -156,6 +160,21 @@ func TestEmptyPattern(t *testing.T) { } } +func TestNormalize(t *testing.T) { + caseSensitive := false + normalize := true + forward := true + test := func(input, pattern string, sidx, eidx, score int, funs ...Algo) { + for _, fun := range funs { + assertMatch2(t, fun, caseSensitive, normalize, forward, + input, pattern, sidx, eidx, score) + } + } + test("Só Danço Samba", "So", 0, 2, 56, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive) + test("Só Danço Samba", "sodc", 0, 7, 89, FuzzyMatchV1, FuzzyMatchV2) + test("Danço", "danco", 0, 5, 128, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch) +} + func TestLongString(t *testing.T) { bytes := make([]byte, math.MaxUint16*2) for i := range bytes { diff --git a/src/algo/normalize.go b/src/algo/normalize.go new file mode 100644 index 0000000..1168a64 --- /dev/null +++ b/src/algo/normalize.go @@ -0,0 +1,408 @@ +// Normalization of latin script letters +// Reference: http://www.unicode.org/Public/UCD/latest/ucd/Index.txt + +package algo + +var normalized map[rune]rune = map[rune]rune{ + 0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER + 0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER + 0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER + 0x00E2: 'a', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x00E4: 'a', // WITH DIAERESIS, LATIN SMALL LETTER + 0x0227: 'a', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1EA1: 'a', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0201: 'a', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x00E0: 'a', // WITH GRAVE, LATIN SMALL LETTER + 0x1EA3: 'a', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x0203: 'a', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x0101: 'a', // WITH MACRON, LATIN SMALL LETTER + 0x0105: 'a', // WITH OGONEK, LATIN SMALL LETTER + 0x1E9A: 'a', // WITH RIGHT HALF RING, LATIN SMALL LETTER + 0x00E5: 'a', // WITH RING ABOVE, LATIN SMALL LETTER + 0x1E01: 'a', // WITH RING BELOW, LATIN SMALL LETTER + 0x00E3: 'a', // WITH TILDE, LATIN SMALL LETTER + 0x0363: 'a', // , COMBINING LATIN SMALL LETTER + 0x0250: 'a', // , LATIN SMALL LETTER TURNED + 0x1E03: 'b', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E05: 'b', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0253: 'b', // WITH HOOK, LATIN SMALL LETTER + 0x1E07: 'b', // WITH LINE BELOW, LATIN SMALL LETTER + 0x0180: 'b', // WITH STROKE, LATIN SMALL LETTER + 0x0183: 'b', // WITH TOPBAR, LATIN SMALL LETTER + 0x0107: 'c', // WITH ACUTE, LATIN SMALL LETTER + 0x010D: 'c', // WITH CARON, LATIN SMALL LETTER + 0x00E7: 'c', // WITH CEDILLA, LATIN SMALL LETTER + 0x0109: 'c', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x0255: 'c', // WITH CURL, LATIN SMALL LETTER + 0x010B: 'c', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x0188: 'c', // WITH HOOK, LATIN SMALL LETTER + 0x023C: 'c', // WITH STROKE, LATIN SMALL LETTER + 0x0368: 'c', // , COMBINING LATIN SMALL LETTER + 0x0297: 'c', // , LATIN LETTER STRETCHED + 0x2184: 'c', // , LATIN SMALL LETTER REVERSED + 0x010F: 'd', // WITH CARON, LATIN SMALL LETTER + 0x1E11: 'd', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E13: 'd', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x0221: 'd', // WITH CURL, LATIN SMALL LETTER + 0x1E0B: 'd', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E0D: 'd', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0257: 'd', // WITH HOOK, LATIN SMALL LETTER + 0x1E0F: 'd', // WITH LINE BELOW, LATIN SMALL LETTER + 0x0111: 'd', // WITH STROKE, LATIN SMALL LETTER + 0x0256: 'd', // WITH TAIL, LATIN SMALL LETTER + 0x018C: 'd', // WITH TOPBAR, LATIN SMALL LETTER + 0x0369: 'd', // , COMBINING LATIN SMALL LETTER + 0x00E9: 'e', // WITH ACUTE, LATIN SMALL LETTER + 0x0115: 'e', // WITH BREVE, LATIN SMALL LETTER + 0x011B: 'e', // WITH CARON, LATIN SMALL LETTER + 0x0229: 'e', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E19: 'e', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x00EA: 'e', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x00EB: 'e', // WITH DIAERESIS, LATIN SMALL LETTER + 0x0117: 'e', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1EB9: 'e', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0205: 'e', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x00E8: 'e', // WITH GRAVE, LATIN SMALL LETTER + 0x1EBB: 'e', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x025D: 'e', // WITH HOOK, LATIN SMALL LETTER REVERSED OPEN + 0x0207: 'e', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x0113: 'e', // WITH MACRON, LATIN SMALL LETTER + 0x0119: 'e', // WITH OGONEK, LATIN SMALL LETTER + 0x0247: 'e', // WITH STROKE, LATIN SMALL LETTER + 0x1E1B: 'e', // WITH TILDE BELOW, LATIN SMALL LETTER + 0x1EBD: 'e', // WITH TILDE, LATIN SMALL LETTER + 0x0364: 'e', // , COMBINING LATIN SMALL LETTER + 0x029A: 'e', // , LATIN SMALL LETTER CLOSED OPEN + 0x025E: 'e', // , LATIN SMALL LETTER CLOSED REVERSED OPEN + 0x025B: 'e', // , LATIN SMALL LETTER OPEN + 0x0258: 'e', // , LATIN SMALL LETTER REVERSED + 0x025C: 'e', // , LATIN SMALL LETTER REVERSED OPEN + 0x01DD: 'e', // , LATIN SMALL LETTER TURNED + 0x1D08: 'e', // , LATIN SMALL LETTER TURNED OPEN + 0x1E1F: 'f', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x0192: 'f', // WITH HOOK, LATIN SMALL LETTER + 0x01F5: 'g', // WITH ACUTE, LATIN SMALL LETTER + 0x011F: 'g', // WITH BREVE, LATIN SMALL LETTER + 0x01E7: 'g', // WITH CARON, LATIN SMALL LETTER + 0x0123: 'g', // WITH CEDILLA, LATIN SMALL LETTER + 0x011D: 'g', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x0121: 'g', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x0260: 'g', // WITH HOOK, LATIN SMALL LETTER + 0x1E21: 'g', // WITH MACRON, LATIN SMALL LETTER + 0x01E5: 'g', // WITH STROKE, LATIN SMALL LETTER + 0x0261: 'g', // , LATIN SMALL LETTER SCRIPT + 0x1E2B: 'h', // WITH BREVE BELOW, LATIN SMALL LETTER + 0x021F: 'h', // WITH CARON, LATIN SMALL LETTER + 0x1E29: 'h', // WITH CEDILLA, LATIN SMALL LETTER + 0x0125: 'h', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x1E27: 'h', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1E23: 'h', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E25: 'h', // WITH DOT BELOW, LATIN SMALL LETTER + 0x02AE: 'h', // WITH FISHHOOK, LATIN SMALL LETTER TURNED + 0x0266: 'h', // WITH HOOK, LATIN SMALL LETTER + 0x1E96: 'h', // WITH LINE BELOW, LATIN SMALL LETTER + 0x0127: 'h', // WITH STROKE, LATIN SMALL LETTER + 0x036A: 'h', // , COMBINING LATIN SMALL LETTER + 0x0265: 'h', // , LATIN SMALL LETTER TURNED + 0x2095: 'h', // , LATIN SUBSCRIPT SMALL LETTER + 0x00ED: 'i', // WITH ACUTE, LATIN SMALL LETTER + 0x012D: 'i', // WITH BREVE, LATIN SMALL LETTER + 0x01D0: 'i', // WITH CARON, LATIN SMALL LETTER + 0x00EE: 'i', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x00EF: 'i', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1ECB: 'i', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0209: 'i', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x00EC: 'i', // WITH GRAVE, LATIN SMALL LETTER + 0x1EC9: 'i', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x020B: 'i', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x012B: 'i', // WITH MACRON, LATIN SMALL LETTER + 0x012F: 'i', // WITH OGONEK, LATIN SMALL LETTER + 0x0268: 'i', // WITH STROKE, LATIN SMALL LETTER + 0x1E2D: 'i', // WITH TILDE BELOW, LATIN SMALL LETTER + 0x0129: 'i', // WITH TILDE, LATIN SMALL LETTER + 0x0365: 'i', // , COMBINING LATIN SMALL LETTER + 0x0131: 'i', // , LATIN SMALL LETTER DOTLESS + 0x1D09: 'i', // , LATIN SMALL LETTER TURNED + 0x1D62: 'i', // , LATIN SUBSCRIPT SMALL LETTER + 0x2071: 'i', // , SUPERSCRIPT LATIN SMALL LETTER + 0x01F0: 'j', // WITH CARON, LATIN SMALL LETTER + 0x0135: 'j', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x029D: 'j', // WITH CROSSED-TAIL, LATIN SMALL LETTER + 0x0249: 'j', // WITH STROKE, LATIN SMALL LETTER + 0x025F: 'j', // WITH STROKE, LATIN SMALL LETTER DOTLESS + 0x0237: 'j', // , LATIN SMALL LETTER DOTLESS + 0x1E31: 'k', // WITH ACUTE, LATIN SMALL LETTER + 0x01E9: 'k', // WITH CARON, LATIN SMALL LETTER + 0x0137: 'k', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E33: 'k', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0199: 'k', // WITH HOOK, LATIN SMALL LETTER + 0x1E35: 'k', // WITH LINE BELOW, LATIN SMALL LETTER + 0x029E: 'k', // , LATIN SMALL LETTER TURNED + 0x2096: 'k', // , LATIN SUBSCRIPT SMALL LETTER + 0x013A: 'l', // WITH ACUTE, LATIN SMALL LETTER + 0x019A: 'l', // WITH BAR, LATIN SMALL LETTER + 0x026C: 'l', // WITH BELT, LATIN SMALL LETTER + 0x013E: 'l', // WITH CARON, LATIN SMALL LETTER + 0x013C: 'l', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E3D: 'l', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x0234: 'l', // WITH CURL, LATIN SMALL LETTER + 0x1E37: 'l', // WITH DOT BELOW, LATIN SMALL LETTER + 0x1E3B: 'l', // WITH LINE BELOW, LATIN SMALL LETTER + 0x0140: 'l', // WITH MIDDLE DOT, LATIN SMALL LETTER + 0x026B: 'l', // WITH MIDDLE TILDE, LATIN SMALL LETTER + 0x026D: 'l', // WITH RETROFLEX HOOK, LATIN SMALL LETTER + 0x0142: 'l', // WITH STROKE, LATIN SMALL LETTER + 0x2097: 'l', // , LATIN SUBSCRIPT SMALL LETTER + 0x1E3F: 'm', // WITH ACUTE, LATIN SMALL LETTER + 0x1E41: 'm', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E43: 'm', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0271: 'm', // WITH HOOK, LATIN SMALL LETTER + 0x0270: 'm', // WITH LONG LEG, LATIN SMALL LETTER TURNED + 0x036B: 'm', // , COMBINING LATIN SMALL LETTER + 0x1D1F: 'm', // , LATIN SMALL LETTER SIDEWAYS TURNED + 0x026F: 'm', // , LATIN SMALL LETTER TURNED + 0x2098: 'm', // , LATIN SUBSCRIPT SMALL LETTER + 0x0144: 'n', // WITH ACUTE, LATIN SMALL LETTER + 0x0148: 'n', // WITH CARON, LATIN SMALL LETTER + 0x0146: 'n', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E4B: 'n', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x0235: 'n', // WITH CURL, LATIN SMALL LETTER + 0x1E45: 'n', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E47: 'n', // WITH DOT BELOW, LATIN SMALL LETTER + 0x01F9: 'n', // WITH GRAVE, LATIN SMALL LETTER + 0x0272: 'n', // WITH LEFT HOOK, LATIN SMALL LETTER + 0x1E49: 'n', // WITH LINE BELOW, LATIN SMALL LETTER + 0x019E: 'n', // WITH LONG RIGHT LEG, LATIN SMALL LETTER + 0x0273: 'n', // WITH RETROFLEX HOOK, LATIN SMALL LETTER + 0x00F1: 'n', // WITH TILDE, LATIN SMALL LETTER + 0x2099: 'n', // , LATIN SUBSCRIPT SMALL LETTER + 0x00F3: 'o', // WITH ACUTE, LATIN SMALL LETTER + 0x014F: 'o', // WITH BREVE, LATIN SMALL LETTER + 0x01D2: 'o', // WITH CARON, LATIN SMALL LETTER + 0x00F4: 'o', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x00F6: 'o', // WITH DIAERESIS, LATIN SMALL LETTER + 0x022F: 'o', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1ECD: 'o', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0151: 'o', // WITH DOUBLE ACUTE, LATIN SMALL LETTER + 0x020D: 'o', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x00F2: 'o', // WITH GRAVE, LATIN SMALL LETTER + 0x1ECF: 'o', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x01A1: 'o', // WITH HORN, LATIN SMALL LETTER + 0x020F: 'o', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x014D: 'o', // WITH MACRON, LATIN SMALL LETTER + 0x01EB: 'o', // WITH OGONEK, LATIN SMALL LETTER + 0x00F8: 'o', // WITH STROKE, LATIN SMALL LETTER + 0x1D13: 'o', // WITH STROKE, LATIN SMALL LETTER SIDEWAYS + 0x00F5: 'o', // WITH TILDE, LATIN SMALL LETTER + 0x0366: 'o', // , COMBINING LATIN SMALL LETTER + 0x0275: 'o', // , LATIN SMALL LETTER BARRED + 0x1D17: 'o', // , LATIN SMALL LETTER BOTTOM HALF + 0x0254: 'o', // , LATIN SMALL LETTER OPEN + 0x1D11: 'o', // , LATIN SMALL LETTER SIDEWAYS + 0x1D12: 'o', // , LATIN SMALL LETTER SIDEWAYS OPEN + 0x1D16: 'o', // , LATIN SMALL LETTER TOP HALF + 0x1E55: 'p', // WITH ACUTE, LATIN SMALL LETTER + 0x1E57: 'p', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x01A5: 'p', // WITH HOOK, LATIN SMALL LETTER + 0x209A: 'p', // , LATIN SUBSCRIPT SMALL LETTER + 0x024B: 'q', // WITH HOOK TAIL, LATIN SMALL LETTER + 0x02A0: 'q', // WITH HOOK, LATIN SMALL LETTER + 0x0155: 'r', // WITH ACUTE, LATIN SMALL LETTER + 0x0159: 'r', // WITH CARON, LATIN SMALL LETTER + 0x0157: 'r', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E59: 'r', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E5B: 'r', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0211: 'r', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x027E: 'r', // WITH FISHHOOK, LATIN SMALL LETTER + 0x027F: 'r', // WITH FISHHOOK, LATIN SMALL LETTER REVERSED + 0x027B: 'r', // WITH HOOK, LATIN SMALL LETTER TURNED + 0x0213: 'r', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x1E5F: 'r', // WITH LINE BELOW, LATIN SMALL LETTER + 0x027C: 'r', // WITH LONG LEG, LATIN SMALL LETTER + 0x027A: 'r', // WITH LONG LEG, LATIN SMALL LETTER TURNED + 0x024D: 'r', // WITH STROKE, LATIN SMALL LETTER + 0x027D: 'r', // WITH TAIL, LATIN SMALL LETTER + 0x036C: 'r', // , COMBINING LATIN SMALL LETTER + 0x0279: 'r', // , LATIN SMALL LETTER TURNED + 0x1D63: 'r', // , LATIN SUBSCRIPT SMALL LETTER + 0x015B: 's', // WITH ACUTE, LATIN SMALL LETTER + 0x0161: 's', // WITH CARON, LATIN SMALL LETTER + 0x015F: 's', // WITH CEDILLA, LATIN SMALL LETTER + 0x015D: 's', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x0219: 's', // WITH COMMA BELOW, LATIN SMALL LETTER + 0x1E61: 's', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E9B: 's', // WITH DOT ABOVE, LATIN SMALL LETTER LONG + 0x1E63: 's', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0282: 's', // WITH HOOK, LATIN SMALL LETTER + 0x023F: 's', // WITH SWASH TAIL, LATIN SMALL LETTER + 0x017F: 's', // , LATIN SMALL LETTER LONG + 0x00DF: 's', // , LATIN SMALL LETTER SHARP + 0x209B: 's', // , LATIN SUBSCRIPT SMALL LETTER + 0x0165: 't', // WITH CARON, LATIN SMALL LETTER + 0x0163: 't', // WITH CEDILLA, LATIN SMALL LETTER + 0x1E71: 't', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x021B: 't', // WITH COMMA BELOW, LATIN SMALL LETTER + 0x0236: 't', // WITH CURL, LATIN SMALL LETTER + 0x1E97: 't', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1E6B: 't', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E6D: 't', // WITH DOT BELOW, LATIN SMALL LETTER + 0x01AD: 't', // WITH HOOK, LATIN SMALL LETTER + 0x1E6F: 't', // WITH LINE BELOW, LATIN SMALL LETTER + 0x01AB: 't', // WITH PALATAL HOOK, LATIN SMALL LETTER + 0x0288: 't', // WITH RETROFLEX HOOK, LATIN SMALL LETTER + 0x0167: 't', // WITH STROKE, LATIN SMALL LETTER + 0x036D: 't', // , COMBINING LATIN SMALL LETTER + 0x0287: 't', // , LATIN SMALL LETTER TURNED + 0x209C: 't', // , LATIN SUBSCRIPT SMALL LETTER + 0x0289: 'u', // BAR, LATIN SMALL LETTER + 0x00FA: 'u', // WITH ACUTE, LATIN SMALL LETTER + 0x016D: 'u', // WITH BREVE, LATIN SMALL LETTER + 0x01D4: 'u', // WITH CARON, LATIN SMALL LETTER + 0x1E77: 'u', // WITH CIRCUMFLEX BELOW, LATIN SMALL LETTER + 0x00FB: 'u', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x1E73: 'u', // WITH DIAERESIS BELOW, LATIN SMALL LETTER + 0x00FC: 'u', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1EE5: 'u', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0171: 'u', // WITH DOUBLE ACUTE, LATIN SMALL LETTER + 0x0215: 'u', // WITH DOUBLE GRAVE, LATIN SMALL LETTER + 0x00F9: 'u', // WITH GRAVE, LATIN SMALL LETTER + 0x1EE7: 'u', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x01B0: 'u', // WITH HORN, LATIN SMALL LETTER + 0x0217: 'u', // WITH INVERTED BREVE, LATIN SMALL LETTER + 0x016B: 'u', // WITH MACRON, LATIN SMALL LETTER + 0x0173: 'u', // WITH OGONEK, LATIN SMALL LETTER + 0x016F: 'u', // WITH RING ABOVE, LATIN SMALL LETTER + 0x1E75: 'u', // WITH TILDE BELOW, LATIN SMALL LETTER + 0x0169: 'u', // WITH TILDE, LATIN SMALL LETTER + 0x0367: 'u', // , COMBINING LATIN SMALL LETTER + 0x1D1D: 'u', // , LATIN SMALL LETTER SIDEWAYS + 0x1D1E: 'u', // , LATIN SMALL LETTER SIDEWAYS DIAERESIZED + 0x1D64: 'u', // , LATIN SUBSCRIPT SMALL LETTER + 0x1E7F: 'v', // WITH DOT BELOW, LATIN SMALL LETTER + 0x028B: 'v', // WITH HOOK, LATIN SMALL LETTER + 0x1E7D: 'v', // WITH TILDE, LATIN SMALL LETTER + 0x036E: 'v', // , COMBINING LATIN SMALL LETTER + 0x028C: 'v', // , LATIN SMALL LETTER TURNED + 0x1D65: 'v', // , LATIN SUBSCRIPT SMALL LETTER + 0x1E83: 'w', // WITH ACUTE, LATIN SMALL LETTER + 0x0175: 'w', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x1E85: 'w', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1E87: 'w', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E89: 'w', // WITH DOT BELOW, LATIN SMALL LETTER + 0x1E81: 'w', // WITH GRAVE, LATIN SMALL LETTER + 0x1E98: 'w', // WITH RING ABOVE, LATIN SMALL LETTER + 0x028D: 'w', // , LATIN SMALL LETTER TURNED + 0x1E8D: 'x', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1E8B: 'x', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x036F: 'x', // , COMBINING LATIN SMALL LETTER + 0x00FD: 'y', // WITH ACUTE, LATIN SMALL LETTER + 0x0177: 'y', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x00FF: 'y', // WITH DIAERESIS, LATIN SMALL LETTER + 0x1E8F: 'y', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1EF5: 'y', // WITH DOT BELOW, LATIN SMALL LETTER + 0x1EF3: 'y', // WITH GRAVE, LATIN SMALL LETTER + 0x1EF7: 'y', // WITH HOOK ABOVE, LATIN SMALL LETTER + 0x01B4: 'y', // WITH HOOK, LATIN SMALL LETTER + 0x0233: 'y', // WITH MACRON, LATIN SMALL LETTER + 0x1E99: 'y', // WITH RING ABOVE, LATIN SMALL LETTER + 0x024F: 'y', // WITH STROKE, LATIN SMALL LETTER + 0x1EF9: 'y', // WITH TILDE, LATIN SMALL LETTER + 0x028E: 'y', // , LATIN SMALL LETTER TURNED + 0x017A: 'z', // WITH ACUTE, LATIN SMALL LETTER + 0x017E: 'z', // WITH CARON, LATIN SMALL LETTER + 0x1E91: 'z', // WITH CIRCUMFLEX, LATIN SMALL LETTER + 0x0291: 'z', // WITH CURL, LATIN SMALL LETTER + 0x017C: 'z', // WITH DOT ABOVE, LATIN SMALL LETTER + 0x1E93: 'z', // WITH DOT BELOW, LATIN SMALL LETTER + 0x0225: 'z', // WITH HOOK, LATIN SMALL LETTER + 0x1E95: 'z', // WITH LINE BELOW, LATIN SMALL LETTER + 0x0290: 'z', // WITH RETROFLEX HOOK, LATIN SMALL LETTER + 0x01B6: 'z', // WITH STROKE, LATIN SMALL LETTER + 0x0240: 'z', // WITH SWASH TAIL, LATIN SMALL LETTER + 0x0251: 'a', // , latin small letter script + 0x00C1: 'A', // WITH ACUTE, LATIN CAPITAL LETTER + 0x00C2: 'A', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER + 0x00C4: 'A', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x00C0: 'A', // WITH GRAVE, LATIN CAPITAL LETTER + 0x00C5: 'A', // WITH RING ABOVE, LATIN CAPITAL LETTER + 0x023A: 'A', // WITH STROKE, LATIN CAPITAL LETTER + 0x00C3: 'A', // WITH TILDE, LATIN CAPITAL LETTER + 0x1D00: 'A', // , LATIN LETTER SMALL CAPITAL + 0x0181: 'B', // WITH HOOK, LATIN CAPITAL LETTER + 0x0243: 'B', // WITH STROKE, LATIN CAPITAL LETTER + 0x0299: 'B', // , LATIN LETTER SMALL CAPITAL + 0x1D03: 'B', // , LATIN LETTER SMALL CAPITAL BARRED + 0x00C7: 'C', // WITH CEDILLA, LATIN CAPITAL LETTER + 0x023B: 'C', // WITH STROKE, LATIN CAPITAL LETTER + 0x1D04: 'C', // , LATIN LETTER SMALL CAPITAL + 0x018A: 'D', // WITH HOOK, LATIN CAPITAL LETTER + 0x0189: 'D', // , LATIN CAPITAL LETTER AFRICAN + 0x1D05: 'D', // , LATIN LETTER SMALL CAPITAL + 0x00C9: 'E', // WITH ACUTE, LATIN CAPITAL LETTER + 0x00CA: 'E', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER + 0x00CB: 'E', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x00C8: 'E', // WITH GRAVE, LATIN CAPITAL LETTER + 0x0246: 'E', // WITH STROKE, LATIN CAPITAL LETTER + 0x0190: 'E', // , LATIN CAPITAL LETTER OPEN + 0x018E: 'E', // , LATIN CAPITAL LETTER REVERSED + 0x1D07: 'E', // , LATIN LETTER SMALL CAPITAL + 0x0193: 'G', // WITH HOOK, LATIN CAPITAL LETTER + 0x029B: 'G', // WITH HOOK, LATIN LETTER SMALL CAPITAL + 0x0262: 'G', // , LATIN LETTER SMALL CAPITAL + 0x029C: 'H', // , LATIN LETTER SMALL CAPITAL + 0x00CD: 'I', // WITH ACUTE, LATIN CAPITAL LETTER + 0x00CE: 'I', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER + 0x00CF: 'I', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x0130: 'I', // WITH DOT ABOVE, LATIN CAPITAL LETTER + 0x00CC: 'I', // WITH GRAVE, LATIN CAPITAL LETTER + 0x0197: 'I', // WITH STROKE, LATIN CAPITAL LETTER + 0x026A: 'I', // , LATIN LETTER SMALL CAPITAL + 0x0248: 'J', // WITH STROKE, LATIN CAPITAL LETTER + 0x1D0A: 'J', // , LATIN LETTER SMALL CAPITAL + 0x1D0B: 'K', // , LATIN LETTER SMALL CAPITAL + 0x023D: 'L', // WITH BAR, LATIN CAPITAL LETTER + 0x1D0C: 'L', // WITH STROKE, LATIN LETTER SMALL CAPITAL + 0x029F: 'L', // , LATIN LETTER SMALL CAPITAL + 0x019C: 'M', // , LATIN CAPITAL LETTER TURNED + 0x1D0D: 'M', // , LATIN LETTER SMALL CAPITAL + 0x019D: 'N', // WITH LEFT HOOK, LATIN CAPITAL LETTER + 0x0220: 'N', // WITH LONG RIGHT LEG, LATIN CAPITAL LETTER + 0x00D1: 'N', // WITH TILDE, LATIN CAPITAL LETTER + 0x0274: 'N', // , LATIN LETTER SMALL CAPITAL + 0x1D0E: 'N', // , LATIN LETTER SMALL CAPITAL REVERSED + 0x00D3: 'O', // WITH ACUTE, LATIN CAPITAL LETTER + 0x00D4: 'O', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER + 0x00D6: 'O', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x00D2: 'O', // WITH GRAVE, LATIN CAPITAL LETTER + 0x019F: 'O', // WITH MIDDLE TILDE, LATIN CAPITAL LETTER + 0x00D8: 'O', // WITH STROKE, LATIN CAPITAL LETTER + 0x00D5: 'O', // WITH TILDE, LATIN CAPITAL LETTER + 0x0186: 'O', // , LATIN CAPITAL LETTER OPEN + 0x1D0F: 'O', // , LATIN LETTER SMALL CAPITAL + 0x1D10: 'O', // , LATIN LETTER SMALL CAPITAL OPEN + 0x1D18: 'P', // , LATIN LETTER SMALL CAPITAL + 0x024A: 'Q', // WITH HOOK TAIL, LATIN CAPITAL LETTER SMALL + 0x024C: 'R', // WITH STROKE, LATIN CAPITAL LETTER + 0x0280: 'R', // , LATIN LETTER SMALL CAPITAL + 0x0281: 'R', // , LATIN LETTER SMALL CAPITAL INVERTED + 0x1D19: 'R', // , LATIN LETTER SMALL CAPITAL REVERSED + 0x1D1A: 'R', // , LATIN LETTER SMALL CAPITAL TURNED + 0x023E: 'T', // WITH DIAGONAL STROKE, LATIN CAPITAL LETTER + 0x01AE: 'T', // WITH RETROFLEX HOOK, LATIN CAPITAL LETTER + 0x1D1B: 'T', // , LATIN LETTER SMALL CAPITAL + 0x0244: 'U', // BAR, LATIN CAPITAL LETTER + 0x00DA: 'U', // WITH ACUTE, LATIN CAPITAL LETTER + 0x00DB: 'U', // WITH CIRCUMFLEX, LATIN CAPITAL LETTER + 0x00DC: 'U', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x00D9: 'U', // WITH GRAVE, LATIN CAPITAL LETTER + 0x1D1C: 'U', // , LATIN LETTER SMALL CAPITAL + 0x01B2: 'V', // WITH HOOK, LATIN CAPITAL LETTER + 0x0245: 'V', // , LATIN CAPITAL LETTER TURNED + 0x1D20: 'V', // , LATIN LETTER SMALL CAPITAL + 0x1D21: 'W', // , LATIN LETTER SMALL CAPITAL + 0x00DD: 'Y', // WITH ACUTE, LATIN CAPITAL LETTER + 0x0178: 'Y', // WITH DIAERESIS, LATIN CAPITAL LETTER + 0x024E: 'Y', // WITH STROKE, LATIN CAPITAL LETTER + 0x028F: 'Y', // , LATIN LETTER SMALL CAPITAL + 0x1D22: 'Z', // , LATIN LETTER SMALL CAPITAL +} diff --git a/src/core.go b/src/core.go index f7dae1a..3e27ed7 100644 --- a/src/core.go +++ b/src/core.go @@ -143,7 +143,7 @@ func Run(opts *Options) { } patternBuilder := func(runes []rune) *Pattern { return BuildPattern( - opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, forward, + opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, opts.Filter == nil, opts.Nth, opts.Delimiter, runes) } matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) diff --git a/src/options.go b/src/options.go index 0c6661f..f34ee84 100644 --- a/src/options.go +++ b/src/options.go @@ -24,6 +24,7 @@ const usage = `usage: fzf [options] --algo=TYPE Fuzzy matching algorithm: [v1|v2] (default: v2) -i Case-insensitive match (default: smart-case match) +i Case-sensitive match + --normalize Normalize latin script letters before matching -n, --nth=N[,..] Comma-separated list of field index expressions for limiting search scope. Each can be a non-zero integer or a range expression ([BEGIN]..[END]). @@ -138,6 +139,7 @@ type Options struct { FuzzyAlgo algo.Algo Extended bool Case Case + Normalize bool Nth []Range WithNth []Range Delimiter Delimiter @@ -185,6 +187,7 @@ func defaultOptions() *Options { FuzzyAlgo: algo.FuzzyMatchV2, Extended: true, Case: CaseSmart, + Normalize: false, Nth: make([]Range, 0), WithNth: make([]Range, 0), Delimiter: Delimiter{}, @@ -887,6 +890,10 @@ func parseOptions(opts *Options, allArgs []string) { case "-f", "--filter": filter := nextString(allArgs, &i, "query string required") opts.Filter = &filter + case "--normalize": + opts.Normalize = true + case "--no-normalize": + opts.Normalize = false case "--algo": opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)")) case "--expect": diff --git a/src/pattern.go b/src/pattern.go index 82272af..8f1d9bc 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -43,6 +43,7 @@ type Pattern struct { fuzzyAlgo algo.Algo extended bool caseSensitive bool + normalize bool forward bool text []rune termSets []termSet @@ -75,7 +76,7 @@ func clearChunkCache() { } // BuildPattern builds Pattern object from the given arguments -func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, forward bool, +func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern { var asString string @@ -120,6 +121,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, fuzzyAlgo: fuzzyAlgo, extended: extended, caseSensitive: caseSensitive, + normalize: normalize, forward: forward, text: []rune(asString), termSets: termSets, @@ -309,9 +311,9 @@ 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, *[]int) { input := p.prepareInput(item) if p.fuzzy { - return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.forward, p.text, withPos, slab) + return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab) } - return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text, withPos, slab) + return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab) } func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, int, *[]int) { @@ -330,7 +332,7 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of matched := false for _, term := range termSet { pfun := p.procFun[term.typ] - off, score, tLen, pos := p.iter(pfun, input, term.caseSensitive, p.forward, term.text, withPos, slab) + off, score, tLen, pos := p.iter(pfun, input, term.caseSensitive, p.normalize, p.forward, term.text, withPos, slab) if sidx := off[0]; sidx >= 0 { if term.inv { continue @@ -378,9 +380,9 @@ func (p *Pattern) prepareInput(item *Item) []Token { return ret } -func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) { +func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) { for _, part := range tokens { - if res, pos := pfun(caseSensitive, forward, *part.text, pattern, withPos, slab); res.Start >= 0 { + if res, pos := pfun(caseSensitive, normalize, forward, *part.text, pattern, withPos, slab); res.Start >= 0 { sidx := int32(res.Start) + part.prefixLength eidx := int32(res.End) + part.prefixLength if pos != nil { diff --git a/src/pattern_test.go b/src/pattern_test.go index 9b6d394..66c0041 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -75,10 +75,10 @@ func TestParseTermsEmpty(t *testing.T) { func TestExact(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, true, true, + pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("'abc")) res, pos := algo.ExactMatchNaive( - pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text, true, nil) + pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text, true, nil) if res.Start != 7 || res.End != 10 { t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) } @@ -90,11 +90,11 @@ func TestExact(t *testing.T) { func TestEqual(t *testing.T) { defer clearPatternCache() clearPatternCache() - pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("^AbC$")) + pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("^AbC$")) match := func(str string, sidxExpected int, eidxExpected int) { res, pos := algo.EqualMatch( - pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text, true, nil) + pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text, true, nil) if res.Start != sidxExpected || res.End != eidxExpected { t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) } @@ -109,17 +109,17 @@ func TestEqual(t *testing.T) { func TestCaseSensitivity(t *testing.T) { defer clearPatternCache() clearPatternCache() - pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("abc")) + pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("Abc")) + pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("abc")) + pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, true, true, []Range{}, Delimiter{}, []rune("Abc")) + pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("Abc")) clearPatternCache() - pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("abc")) + pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("abc")) clearPatternCache() - pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, true, true, []Range{}, Delimiter{}, []rune("Abc")) + pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("Abc")) if string(pat1.text) != "abc" || pat1.caseSensitive != false || string(pat2.text) != "Abc" || pat2.caseSensitive != true || @@ -132,7 +132,7 @@ func TestCaseSensitivity(t *testing.T) { } func TestOrigTextAndTransformed(t *testing.T) { - pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, true, true, []Range{}, Delimiter{}, []rune("jg")) + pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg")) tokens := Tokenize(util.RunesToChars([]rune("junegunn")), Delimiter{}) trans := Transform(tokens, []Range{Range{1, 1}}) @@ -167,7 +167,7 @@ func TestOrigTextAndTransformed(t *testing.T) { func TestCacheKey(t *testing.T) { test := func(extended bool, patStr string, expected string, cacheable bool) { - pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, true, true, []Range{}, Delimiter{}, []rune(patStr)) + pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune(patStr)) if pat.CacheKey() != expected { t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) } From a16d8f66a99ae365a57d114d5ee7d0a7ebc4cf5f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Jan 2017 09:52:17 +0900 Subject: [PATCH 354/658] Normalize pattern string before passing it to Algo function --- src/algo/algo.go | 38 +++----------------------------------- src/algo/normalize.go | 16 ++++++++++++++++ src/pattern.go | 10 +++++++--- src/pattern_test.go | 6 +++--- 4 files changed, 29 insertions(+), 41 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 2a3bc9d..ac6c66d 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -246,21 +246,9 @@ func normalizeRune(r rune) rune { return r } -func normalizeRunes(runes []rune) []rune { - ret := make([]rune, len(runes)) - copy(ret, runes) - for idx, r := range runes { - if r < 0x00C0 || r > 0x2184 { - continue - } - n := normalized[r] - if n > 0 { - ret[idx] = normalized[r] - } - } - return ret -} - +// Algo functions make two assumptions +// 1. "pattern" is given in lowercase if "caseSensitive" is false +// 2. "pattern" is already normalized if "normalize" is true type Algo func(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { @@ -283,10 +271,6 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab) } - if normalize { - pattern = normalizeRunes(pattern) - } - // Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages offset16 := 0 offset32 := 0 @@ -539,10 +523,6 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text util.Ch lenRunes := text.Length() lenPattern := len(pattern) - if normalize { - pattern = normalizeRunes(pattern) - } - for index := 0; index < lenRunes; index++ { char := text.Get(indexAt(index, lenRunes, forward)) // This is considerably faster than blindly applying strings.ToLower to the @@ -626,10 +606,6 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util return Result{-1, -1, 0}, nil } - if normalize { - pattern = normalizeRunes(pattern) - } - // For simplicity, only look at the bonus at the first character position pidx := 0 bestPos, bonus, bestBonus := -1, int16(0), int16(-1) @@ -693,10 +669,6 @@ func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text util.Cha return Result{-1, -1, 0}, nil } - if normalize { - pattern = normalizeRunes(pattern) - } - for index, r := range pattern { char := text.Get(index) if !caseSensitive { @@ -726,10 +698,6 @@ func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text util.Cha return Result{-1, -1, 0}, nil } - if normalize { - pattern = normalizeRunes(pattern) - } - for index, r := range pattern { char := text.Get(index + diff) if !caseSensitive { diff --git a/src/algo/normalize.go b/src/algo/normalize.go index 1168a64..7a49644 100644 --- a/src/algo/normalize.go +++ b/src/algo/normalize.go @@ -406,3 +406,19 @@ var normalized map[rune]rune = map[rune]rune{ 0x028F: 'Y', // , LATIN LETTER SMALL CAPITAL 0x1D22: 'Z', // , LATIN LETTER SMALL CAPITAL } + +// NormalizeRunes normalizes latin script letters +func NormalizeRunes(runes []rune) []rune { + ret := make([]rune, len(runes)) + copy(ret, runes) + for idx, r := range runes { + if r < 0x00C0 || r > 0x2184 { + continue + } + n := normalized[r] + if n > 0 { + ret[idx] = normalized[r] + } + } + return ret +} diff --git a/src/pattern.go b/src/pattern.go index 8f1d9bc..731104f 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -95,7 +95,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, termSets := []termSet{} if extended { - termSets = parseTerms(fuzzy, caseMode, asString) + termSets = parseTerms(fuzzy, caseMode, normalize, asString) Loop: for _, termSet := range termSets { for idx, term := range termSet { @@ -140,7 +140,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, return ptr } -func parseTerms(fuzzy bool, caseMode Case, str string) []termSet { +func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet { tokens := _splitRegex.Split(str, -1) sets := []termSet{} set := termSet{} @@ -196,10 +196,14 @@ func parseTerms(fuzzy bool, caseMode Case, str string) []termSet { sets = append(sets, set) set = termSet{} } + textRunes := []rune(text) + if normalize { + textRunes = algo.NormalizeRunes(textRunes) + } set = append(set, term{ typ: typ, inv: inv, - text: []rune(text), + text: textRunes, caseSensitive: caseSensitive, origText: origText}) switchSet = true diff --git a/src/pattern_test.go b/src/pattern_test.go index 66c0041..ea0082f 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -15,7 +15,7 @@ func init() { } func TestParseTermsExtended(t *testing.T) { - terms := parseTerms(true, CaseSmart, + terms := parseTerms(true, CaseSmart, false, "| aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | | zzz$ | !ZZZ |") if len(terms) != 9 || terms[0][0].typ != termFuzzy || terms[0][0].inv || @@ -50,7 +50,7 @@ func TestParseTermsExtended(t *testing.T) { } func TestParseTermsExtendedExact(t *testing.T) { - terms := parseTerms(false, CaseSmart, + terms := parseTerms(false, CaseSmart, false, "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$") if len(terms) != 8 || terms[0][0].typ != termExact || terms[0][0].inv || len(terms[0][0].text) != 3 || @@ -66,7 +66,7 @@ func TestParseTermsExtendedExact(t *testing.T) { } func TestParseTermsEmpty(t *testing.T) { - terms := parseTerms(true, CaseSmart, "' $ ^ !' !^ !$") + terms := parseTerms(true, CaseSmart, false, "' $ ^ !' !^ !$") if len(terms) != 0 { t.Errorf("%s", terms) } From 6c6c0a47782328d21cc2899ac9c9f0e6edb1fb40 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Jan 2017 10:45:05 +0900 Subject: [PATCH 355/658] Make util.RuneWidth return 1 for non-displayable characters Fix line wrapping in preview window --- src/util/util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/util.go b/src/util/util.go index 29e8017..15ec0ad 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -18,7 +18,7 @@ func RuneWidth(r rune, prefixWidth int, tabstop int) int { } else if w, found := _runeWidths[r]; found { return w } else { - w := runewidth.RuneWidth(r) + w := Max(runewidth.RuneWidth(r), 1) _runeWidths[r] = w return w } From d18b8e0d2c9987322e3a17b72a1db6c497cee947 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Jan 2017 13:22:24 +0900 Subject: [PATCH 356/658] Retry flaky test cases --- test/test_go.rb | 51 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 55422aa..494d3cd 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1241,9 +1241,11 @@ module TestShell def test_ctrl_t set_var "FZF_CTRL_T_COMMAND", "seq 100" - tmux.prepare - tmux.send_keys 'C-t' - lines = tmux.until { |lines| lines.item_count == 100 } + lines = retries do + tmux.prepare + tmux.send_keys 'C-t' + tmux.until { |lines| lines.item_count == 100 } + end tmux.send_keys :Tab, :Tab, :Tab tmux.until { |lines| lines.any_include? ' (3)' } tmux.send_keys :Enter @@ -1255,9 +1257,11 @@ module TestShell writelines tempname, ['fzf-unicode 테스트1', 'fzf-unicode 테스트2'] set_var "FZF_CTRL_T_COMMAND", "cat #{tempname}" - tmux.prepare - tmux.send_keys 'echo ', 'C-t' - tmux.until { |lines| lines.item_count == 2 } + retries do + tmux.prepare + tmux.send_keys 'echo ', 'C-t' + tmux.until { |lines| lines.item_count == 2 } + end tmux.send_keys 'fzf-unicode' tmux.until { |lines| lines.match_count == 2 } @@ -1281,9 +1285,11 @@ module TestShell end def test_alt_c - tmux.prepare - tmux.send_keys :Escape, :c - lines = tmux.until { |lines| lines.item_count > 0 } + lines = retries do + tmux.prepare + tmux.send_keys :Escape, :c + tmux.until { |lines| lines.item_count > 0 } + end expected = lines.reverse.select { |l| l.start_with? '>' }.first[2..-1] tmux.send_keys :Enter tmux.prepare @@ -1297,9 +1303,11 @@ module TestShell tmux.prepare tmux.send_keys 'cd /', :Enter - tmux.prepare - tmux.send_keys :Escape, :c - lines = tmux.until { |lines| lines.item_count == 1 } + retries do + tmux.prepare + tmux.send_keys :Escape, :c + lines = tmux.until { |lines| lines.item_count == 1 } + end tmux.send_keys :Enter tmux.prepare @@ -1313,9 +1321,12 @@ module TestShell tmux.send_keys 'echo 2nd', :Enter; tmux.prepare tmux.send_keys 'echo 3d', :Enter; tmux.prepare tmux.send_keys 'echo 3rd', :Enter; tmux.prepare - tmux.send_keys 'echo 4th', :Enter; tmux.prepare - tmux.send_keys 'C-r' - tmux.until { |lines| lines.item_count > 0 } + tmux.send_keys 'echo 4th', :Enter + retries do + tmux.prepare + tmux.send_keys 'C-r' + tmux.until { |lines| lines.item_count > 0 } + end tmux.send_keys '3d' tmux.until { |lines| lines[-3].end_with? 'echo 3rd' } tmux.send_keys :Enter @@ -1323,6 +1334,16 @@ module TestShell tmux.send_keys :Enter tmux.until { |lines| lines[-1] == '3rd' } end + + def retries times = 3, &block + (times - 1).times do |t| + begin + return block.call + rescue RuntimeError + end + end + block.call + end end module CompletionTest From 78a3f81972312d13d50e5b24dcabaa40284566b5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 9 Jan 2017 19:09:30 +0900 Subject: [PATCH 357/658] Do not use \e[s and \e[u Excerpt from http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x361.html: > - Save cursor position: > \033[s > - Restore cursor position: > \033[u > > The latter two codes are NOT honoured by many terminal emulators. The > only ones that I'm aware of that do are xterm and nxterm - even though > the majority of terminal emulators are based on xterm code. As far as > I can tell, rxvt, kvt, xiterm, and Eterm do not support them. They are > supported on the console. They are also unsupported by Neovim terminal. --- src/tui/light.go | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index 1273c8f..379680c 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -33,7 +33,7 @@ func (r *LightRenderer) stderr(str string) { runes := []rune{} for len(bytes) > 0 { r, sz := utf8.DecodeRune(bytes) - if r == utf8.RuneError || r != '\x1b' && r != '\n' && r < 32 { + if r == utf8.RuneError || r != '\x1b' && r != '\n' && r != '\r' && r < 32 { runes = append(runes, '?') } else { runes = append(runes, r) @@ -71,6 +71,8 @@ type LightRenderer struct { escDelay int upOneLine bool queued string + y int + x int maxHeightFunc func(int) int } @@ -182,10 +184,29 @@ func (r *LightRenderer) Init() { } r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi("G") - r.csi("s") + // r.csi("s") r.yoffset, _ = r.findOffset() } +func (r *LightRenderer) move(y int, x int) { + // w.csi("u") + if r.y < y { + r.csi(fmt.Sprintf("%dB", y-r.y)) + } else if r.y > y { + r.csi(fmt.Sprintf("%dA", r.y-y)) + } + r.stderr("\r") + if x > 0 { + r.csi(fmt.Sprintf("%dC", x)) + } + r.y = y + r.x = x +} + +func (r *LightRenderer) origin() { + r.move(0, 0) +} + func (r *LightRenderer) updateTerminalSize() { sizes := strings.Split(stty("size"), " ") if len(sizes) < 2 { @@ -470,7 +491,8 @@ func (r *LightRenderer) Resume() bool { } func (r *LightRenderer) Clear() { - r.csi("u") + // r.csi("u") + r.origin() r.csi("J") r.flush() } @@ -484,7 +506,8 @@ func (r *LightRenderer) Refresh() { } func (r *LightRenderer) Close() { - r.csi("u") + // r.csi("u") + r.origin() r.csi("J") if r.mouse { r.csi("?1000l") @@ -584,15 +607,7 @@ func (w *LightWindow) Move(y int, x int) { w.posx = x w.posy = y - w.csi("u") - y += w.Top() - if y > 0 { - w.csi(fmt.Sprintf("%dB", y)) - } - x += w.Left() - if x > 0 { - w.csi(fmt.Sprintf("%dC", x)) - } + w.renderer.move(w.Top()+y, w.Left()+x) } func (w *LightWindow) MoveAndClear(y int, x int) { From 340af463cd9c52c0a3a7ea5688035bba1cd29f6a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 10 Jan 2017 01:04:36 +0900 Subject: [PATCH 358/658] Add --min-height option for percent --height --- man/man1/fzf.1 | 4 ++++ src/options.go | 10 +++++++++- src/terminal.go | 9 +++++---- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 1bcfb63..98c1c79 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -138,6 +138,10 @@ Label characters for \fBjump\fR and \fBjump-accept\fR Display fzf window below the cursor with the given height instead of using fullscreen. .TP +.BI "--min-height=" "HEIGHT" +Minimum height when \fB--height\fR is given in percent (default: 10). +Ignored when \fB--height\fR is not specified. +.TP .B "--reverse" Reverse orientation .TP diff --git a/src/options.go b/src/options.go index f34ee84..cbb0155 100644 --- a/src/options.go +++ b/src/options.go @@ -50,6 +50,8 @@ const usage = `usage: fzf [options] Layout --height=HEIGHT[%] Display fzf window below the cursor with the given height instead of using fullscreen + --min-height=HEIGHT Minimum height when --height is given in percent + (default: 10) --reverse Reverse orientation --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) --inline-info Display finder info inline with the query @@ -153,6 +155,7 @@ type Options struct { Black bool Bold bool Height sizeSpec + MinHeight int Reverse bool Cycle bool Hscroll bool @@ -200,6 +203,7 @@ func defaultOptions() *Options { Theme: tui.EmptyTheme(), Black: false, Bold: true, + MinHeight: 10, Reverse: false, Cycle: false, Hscroll: true, @@ -1023,7 +1027,9 @@ func parseOptions(opts *Options, allArgs []string) { parsePreviewWindow(&opts.Preview, nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:wrap][:hidden]")) case "--height": - opts.Height = parseHeight(nextString(allArgs, &i, "height required: [HEIGHT[%]]")) + opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]")) + case "--min-height": + opts.MinHeight = nextInt(allArgs, &i, "height required: HEIGHT") case "--no-height": opts.Height = sizeSpec{} case "--no-margin": @@ -1054,6 +1060,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.Sort = 1 // Don't care } else if match, value := optString(arg, "--height="); match { opts.Height = parseHeight(value) + } else if match, value := optString(arg, "--min-height="); match { + opts.MinHeight = atoi(value) } else if match, value := optString(arg, "--toggle-sort="); match { parseToggleSort(opts.Keymap, value) } else if match, value := optString(arg, "--expect="); match { diff --git a/src/terminal.go b/src/terminal.go index c653327..ce63adf 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -275,14 +275,15 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { maxHeightFunc := func(termHeight int) int { var maxHeight int if opts.Height.percent { - maxHeight = int(opts.Height.size * float64(termHeight) / 100.0) + maxHeight = util.Min(termHeight, + util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight)) } else { - maxHeight = util.Min(int(opts.Height.size), termHeight) + maxHeight = util.Min(termHeight, int(opts.Height.size)) } if opts.InlineInfo { - return util.Max(maxHeight, 3) + return util.Max(maxHeight, minHeight-1) } - return util.Max(maxHeight, 4) + return util.Max(maxHeight, minHeight) } renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, maxHeightFunc) } else { From ae274158de38181bca27f2ce54c8b4fc0b688eff Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 10 Jan 2017 02:16:12 +0900 Subject: [PATCH 359/658] Add experimental support for 24-bit colors --- man/man1/fzf.1 | 4 +++- src/options.go | 15 +++++++++++---- src/tui/tui.go | 8 ++++++++ src/tui/tui_test.go | 20 ++++++++++++++++++++ 4 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 src/tui/tui_test.go diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 98c1c79..2ef3cdd 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -197,7 +197,9 @@ Number of spaces for a tab character (default: 8) .BI "--color=" "[BASE_SCHEME][,COLOR:ANSI]" Color configuration. The name of the base color scheme is followed by custom color mappings. Ansi color code of -1 denotes terminal default -foreground/background color. +foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR +format, but the support for 24-bit colors is experimental and only works when +\fB--height\fR option is used. .RS e.g. \fBfzf --color=bg+:24\fR diff --git a/src/options.go b/src/options.go index cbb0155..bcd2458 100644 --- a/src/options.go +++ b/src/options.go @@ -493,6 +493,7 @@ func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme { func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { theme := dupeTheme(defaultTheme) + rrggbb := regexp.MustCompile("^#[0-9a-fA-F]{6}$") for _, str := range strings.Split(strings.ToLower(str), ",") { switch str { case "dark": @@ -516,11 +517,17 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { if len(pair) != 2 { fail() } - ansi32, err := strconv.Atoi(pair[1]) - if err != nil || ansi32 < -1 || ansi32 > 255 { - fail() + + var ansi tui.Color + if rrggbb.MatchString(pair[1]) { + ansi = tui.HexToColor(pair[1]) + } else { + ansi32, err := strconv.Atoi(pair[1]) + if err != nil || ansi32 < -1 || ansi32 > 255 { + fail() + } + ansi = tui.Color(ansi32) } - ansi := tui.Color(ansi32) switch pair[0] { case "fg": theme.Fg = ansi diff --git a/src/tui/tui.go b/src/tui/tui.go index 859eed7..11ac1e7 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -1,6 +1,7 @@ package tui import ( + "strconv" "time" ) @@ -121,6 +122,13 @@ type ColorPair struct { id int16 } +func HexToColor(rrggbb string) Color { + r, _ := strconv.ParseInt(rrggbb[1:3], 16, 0) + g, _ := strconv.ParseInt(rrggbb[3:5], 16, 0) + b, _ := strconv.ParseInt(rrggbb[5:7], 16, 0) + return Color((1 << 24) + (r << 16) + (g << 8) + b) +} + func NewColorPair(fg Color, bg Color) ColorPair { return ColorPair{fg, bg, -1} } diff --git a/src/tui/tui_test.go b/src/tui/tui_test.go new file mode 100644 index 0000000..3ba9bf3 --- /dev/null +++ b/src/tui/tui_test.go @@ -0,0 +1,20 @@ +package tui + +import "testing" + +func TestHexToColor(t *testing.T) { + assert := func(expr string, r, g, b int) { + color := HexToColor(expr) + if !color.is24() || + int((color>>16)&0xff) != r || + int((color>>8)&0xff) != g || + int((color)&0xff) != b { + t.Fail() + } + } + + assert("#ff0000", 255, 0, 0) + assert("#010203", 1, 2, 3) + assert("#102030", 16, 32, 48) + assert("#ffffff", 255, 255, 255) +} From 0c127cfdc1b62a3b5ca1730fd1e60a406b9ee54b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 10 Jan 2017 22:55:55 +0900 Subject: [PATCH 360/658] No need to query row position of the cursor if mouse is disabled --- src/tui/light.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tui/light.go b/src/tui/light.go index 379680c..574d161 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -185,7 +185,9 @@ func (r *LightRenderer) Init() { r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi("G") // r.csi("s") - r.yoffset, _ = r.findOffset() + if r.mouse { + r.yoffset, _ = r.findOffset() + } } func (r *LightRenderer) move(y int, x int) { From 996dcb14a3fae6a640bff072149352cd82fd8392 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 Jan 2017 02:12:32 +0900 Subject: [PATCH 361/658] Make fzf immediately quit when failed to read /dev/tty Close #798 --- src/tui/light.go | 34 ++++++++++++++++++++-------------- src/tui/ncurses.go | 8 +++++--- src/tui/tcell.go | 4 ++-- src/tui/tui.go | 5 +++++ 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index 574d161..b9e90d6 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -111,8 +111,10 @@ func (r *LightRenderer) defaultTheme() *ColorTheme { return Default16 } -func stty(cmd string) string { - out, err := util.ExecCommand("stty " + cmd + " < /dev/tty").Output() +func (r *LightRenderer) stty(cmd string) string { + proc := util.ExecCommand("stty " + cmd) + proc.Stdin = r.ttyin + out, err := proc.Output() if err != nil { // Not sure how to handle this panic("stty " + cmd + ": " + err.Error()) @@ -164,8 +166,8 @@ func (r *LightRenderer) Init() { } r.escDelay = delay - r.ostty = stty("-g") - stty("raw") + r.ostty = r.stty("-g") + r.stty("raw") r.updateTerminalSize() initTheme(r.theme, r.defaultTheme(), r.forceBlack) @@ -210,7 +212,7 @@ func (r *LightRenderer) origin() { } func (r *LightRenderer) updateTerminalSize() { - sizes := strings.Split(stty("size"), " ") + sizes := strings.Split(r.stty("size"), " ") if len(sizes) < 2 { r.width = defaultWidth r.height = r.maxHeightFunc(defaultHeight) @@ -220,14 +222,14 @@ func (r *LightRenderer) updateTerminalSize() { } } -func (r *LightRenderer) getch(nonblock bool) int { +func (r *LightRenderer) getch(nonblock bool) (int, bool) { b := make([]byte, 1) util.SetNonblock(r.ttyin, nonblock) _, err := r.ttyin.Read(b) if err != nil { - return -1 + return 0, false } - return int(b[0]) + return int(b[0]), true } func (r *LightRenderer) getBytes() []byte { @@ -235,7 +237,11 @@ func (r *LightRenderer) getBytes() []byte { } func (r *LightRenderer) getBytesInternal(buffer []byte) []byte { - c := r.getch(false) + c, ok := r.getch(false) + if !ok { + r.Close() + errorExit() + } retries := 0 if c == ESC { @@ -244,8 +250,8 @@ func (r *LightRenderer) getBytesInternal(buffer []byte) []byte { buffer = append(buffer, byte(c)) for { - c = r.getch(true) - if c == -1 { + c, ok = r.getch(true) + if !ok { if retries > 0 { retries-- time.Sleep(escPollInterval * time.Millisecond) @@ -479,13 +485,13 @@ func (r *LightRenderer) mouseSequence(sz *int) Event { } func (r *LightRenderer) Pause() { - stty(fmt.Sprintf("%q", r.ostty)) + r.stty(fmt.Sprintf("%q", r.ostty)) r.csi("?1049h") r.flush() } func (r *LightRenderer) Resume() bool { - stty("raw") + r.stty("raw") r.csi("?1049l") r.flush() // Should redraw @@ -518,7 +524,7 @@ func (r *LightRenderer) Close() { r.csi("A") } r.flush() - stty(fmt.Sprintf("%q", r.ostty)) + r.stty(fmt.Sprintf("%q", r.ostty)) } func (r *LightRenderer) MaxX() int { diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index b160692..f6feefc 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -110,12 +110,12 @@ func (r *FullscreenRenderer) Init() { tty := C.c_tty() if tty == nil { fmt.Println("Failed to open /dev/tty") - os.Exit(2) + errorExit() } _screen = C.c_newterm(tty) if _screen == nil { fmt.Println("Invalid $TERM: " + os.Getenv("TERM")) - os.Exit(2) + errorExit() } C.set_term(_screen) if r.mouse { @@ -375,7 +375,9 @@ func (r *FullscreenRenderer) GetChar() Event { c := C.getch() switch c { case C.ERR: - return Event{Invalid, 0, nil} + // Unexpected error from blocking read + r.Close() + errorExit() case C.KEY_UP: return Event{Up, 0, nil} case C.KEY_DOWN: diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 460bfd5..b6f0819 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -124,11 +124,11 @@ func (r *FullscreenRenderer) initScreen() { s, e := tcell.NewScreen() if e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) - os.Exit(2) + errorExit() } if e = s.Init(); e != nil { fmt.Fprintf(os.Stderr, "%v\n", e) - os.Exit(2) + errorExit() } if r.mouse { s.EnableMouse() diff --git a/src/tui/tui.go b/src/tui/tui.go index 11ac1e7..eb504f8 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -1,6 +1,7 @@ package tui import ( + "os" "strconv" "time" ) @@ -275,6 +276,10 @@ func EmptyTheme() *ColorTheme { Border: colUndefined} } +func errorExit() { + os.Exit(2) +} + func init() { Default16 = &ColorTheme{ Fg: colDefault, From f8082bc53a5bc736558d5952a34b527f12815878 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 Jan 2017 21:48:36 +0900 Subject: [PATCH 362/658] No need to use /bin/sh to execute stty and tput --- src/tui/light.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index b9e90d6..4725ef5 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -3,6 +3,7 @@ package tui import ( "fmt" "os" + "os/exec" "strconv" "strings" "syscall" @@ -104,7 +105,7 @@ func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop in } func (r *LightRenderer) defaultTheme() *ColorTheme { - colors, err := util.ExecCommand("tput colors").Output() + colors, err := exec.Command("tput", "colors").Output() if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 { return Dark256 } @@ -112,7 +113,7 @@ func (r *LightRenderer) defaultTheme() *ColorTheme { } func (r *LightRenderer) stty(cmd string) string { - proc := util.ExecCommand("stty " + cmd) + proc := exec.Command("stty", cmd) proc.Stdin = r.ttyin out, err := proc.Output() if err != nil { @@ -485,7 +486,7 @@ func (r *LightRenderer) mouseSequence(sz *int) Event { } func (r *LightRenderer) Pause() { - r.stty(fmt.Sprintf("%q", r.ostty)) + r.stty(r.ostty) r.csi("?1049h") r.flush() } @@ -524,7 +525,7 @@ func (r *LightRenderer) Close() { r.csi("A") } r.flush() - r.stty(fmt.Sprintf("%q", r.ostty)) + r.stty(r.ostty) } func (r *LightRenderer) MaxX() int { From 9977a3e9fcdc2d2feda2f8cd2990a89c21804e56 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 Jan 2017 22:13:40 +0900 Subject: [PATCH 363/658] Make preview renderer suspend early on line wrap --- src/terminal.go | 72 ++++++++++++++++++++++++++-------------------- src/tui/light.go | 16 +++++++---- src/tui/ncurses.go | 9 ++++-- src/tui/tcell.go | 10 +++---- src/tui/tui.go | 12 ++++++-- 5 files changed, 73 insertions(+), 46 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index ce63adf..0353157 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1,8 +1,10 @@ package fzf import ( + "bufio" "bytes" "fmt" + "io" "os" "os/signal" "regexp" @@ -852,41 +854,49 @@ func (t *Terminal) printPreview() { return } t.pwindow.Erase() - skip := t.previewer.offset - extractColor(t.previewer.text, nil, func(str string, ansi *ansiState) bool { - if skip > 0 { - newlines := numLinesMax(str, skip) - if skip <= newlines { - for i := 0; i < skip; i++ { - str = str[strings.Index(str, "\n")+1:] + + maxWidth := t.pwindow.Width() + if t.tui.DoesAutoWrap() { + maxWidth -= 1 + } + reader := bufio.NewReader(strings.NewReader(t.previewer.text)) + lineNo := -t.previewer.offset + for { + line, err := reader.ReadString('\n') + eof := err == io.EOF + if !eof { + line = line[:len(line)-1] + } + lineNo++ + if lineNo > t.pwindow.Height() { + break + } else if lineNo > 0 { + var fillRet tui.FillReturn + extractColor(line, nil, func(str string, ansi *ansiState) bool { + trimmed := []rune(str) + if !t.preview.wrap { + trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X()) } - skip = 0 - } else { - skip -= newlines - return true + str, _ = t.processTabs(trimmed, 0) + if ansi != nil && ansi.colored() { + fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str) + } else { + fillRet = t.pwindow.Fill(str) + } + return fillRet == tui.FillContinue + }) + switch fillRet { + case tui.FillNextLine: + continue + case tui.FillSuspend: + break } + t.pwindow.Fill("\n") } - lines := strings.Split(str, "\n") - for i, line := range lines { - limit := t.pwindow.Width() - if t.tui.DoesAutoWrap() { - limit -= 1 - } - if i == 0 { - limit -= t.pwindow.X() - } - trimmed := []rune(line) - if !t.preview.wrap { - trimmed, _ = t.trimRight(trimmed, limit) - } - lines[i], _ = t.processTabs(trimmed, 0) - str = strings.Join(lines, "\n") + if eof { + break } - if ansi != nil && ansi.colored() { - return t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str) - } - return t.pwindow.Fill(str) - }) + } t.pwindow.FinishFill() if t.previewer.lines > t.pwindow.Height() { offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines) diff --git a/src/tui/light.go b/src/tui/light.go index 4725ef5..248477c 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -730,23 +730,29 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin return lines } -func (w *LightWindow) fill(str string, onMove func()) bool { +func (w *LightWindow) fill(str string, onMove func()) FillReturn { allLines := strings.Split(str, "\n") 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.MoveAndClear(w.posy+1, 0) + } + return FillNextLine + } w.stderr(wl.text) w.posx += wl.displayWidth if j < len(lines)-1 || i < len(allLines)-1 { if w.posy+1 >= w.height { - return false + return FillSuspend } w.MoveAndClear(w.posy+1, 0) onMove() } } } - return true + return FillContinue } func (w *LightWindow) setBg() { @@ -755,13 +761,13 @@ func (w *LightWindow) setBg() { } } -func (w *LightWindow) Fill(text string) bool { +func (w *LightWindow) Fill(text string) FillReturn { w.MoveAndClear(w.posy, w.posx) w.setBg() return w.fill(text, w.setBg) } -func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) bool { +func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn { w.MoveAndClear(w.posy, w.posx) if bg == colDefault { bg = w.bg diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index f6feefc..3e636ba 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -282,11 +282,14 @@ func (r *FullscreenRenderer) DoesAutoWrap() bool { return true } -func (w *CursesWindow) Fill(str string) bool { - return C.waddstr(w.impl, C.CString(str)) == C.OK +func (w *CursesWindow) Fill(str string) FillReturn { + if C.waddstr(w.impl, C.CString(str)) == C.OK { + return FillContinue + } + return FillSuspend } -func (w *CursesWindow) CFill(fg Color, bg Color, attr Attr, str string) bool { +func (w *CursesWindow) CFill(fg Color, bg Color, attr Attr, str string) FillReturn { index := ColorPair{fg, bg, -1}.index() C.wcolor_set(w.impl, C.short(index), nil) C.wattron(w.impl, C.int(attr)) diff --git a/src/tui/tcell.go b/src/tui/tcell.go index b6f0819..3399d32 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -477,7 +477,7 @@ func (w *TcellWindow) CPrint(pair ColorPair, attr Attr, text string) { w.printString(text, pair, attr) } -func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) bool { +func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn { lx := 0 var style tcell.Style @@ -511,7 +511,7 @@ func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) bool { var yPos = w.top + w.lastY if yPos >= (w.top + w.height) { - return false + return FillSuspend } _screen.SetContent(xPos, yPos, r, nil, style) @@ -520,14 +520,14 @@ func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) bool { } w.lastX += lx - return true + return FillContinue } -func (w *TcellWindow) Fill(str string) bool { +func (w *TcellWindow) Fill(str string) FillReturn { return w.fillString(str, ColDefault, 0) } -func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) bool { +func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn { return w.fillString(str, ColorPair{fg, bg, -1}, a) } diff --git a/src/tui/tui.go b/src/tui/tui.go index eb504f8..33358e8 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -117,6 +117,14 @@ const ( colWhite ) +type FillReturn int + +const ( + FillContinue FillReturn = iota + FillNextLine + FillSuspend +) + type ColorPair struct { fg Color bg Color @@ -216,8 +224,8 @@ type Window interface { MoveAndClear(y int, x int) Print(text string) CPrint(color ColorPair, attr Attr, text string) - Fill(text string) bool - CFill(fg Color, bg Color, attr Attr, text string) bool + Fill(text string) FillReturn + CFill(fg Color, bg Color, attr Attr, text string) FillReturn Erase() } From 2aa739be81c7786eb9fd53606e2cfb4525344e8e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 Jan 2017 22:47:26 +0900 Subject: [PATCH 364/658] Fix bug where occurrence of the pattern in header lines are highlighted --- src/terminal.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 0353157..3a89bb4 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -625,7 +625,7 @@ func (t *Terminal) printHeader() { t.move(line, 2, true) t.printHighlighted(&Result{item: item}, - tui.AttrRegular, tui.ColHeader, tui.ColDefault, false) + tui.AttrRegular, tui.ColHeader, tui.ColDefault, false, false) } } @@ -681,14 +681,14 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) { } else { t.window.CPrint(tui.ColCurrent, t.strong, " ") } - t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true) + t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true) } else { if selected { t.window.CPrint(tui.ColSelected, t.strong, ">") } else { t.window.Print(" ") } - t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false) + t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false, true) } } @@ -744,7 +744,7 @@ func (t *Terminal) overflow(runes []rune, max int) bool { return false } -func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool) { +func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) { item := result.item // Overflow @@ -752,7 +752,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo copy(text, item.text.ToRunes()) matchOffsets := []Offset{} var pos *[]int - if t.merger.pattern != nil { + if match && t.merger.pattern != nil { _, matchOffsets, pos = t.merger.pattern.MatchItem(item, true, t.slab) } charOffsets := matchOffsets From d64828ce6d974c016050ec6f3b39c7e6108c39ae Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 11 Jan 2017 23:01:56 +0900 Subject: [PATCH 365/658] Print error message to stderr on unexpected exit --- src/tui/light.go | 2 +- src/tui/ncurses.go | 9 +++------ src/tui/tcell.go | 9 ++------- src/tui/tui.go | 4 +++- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index 248477c..0bfc57f 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -241,7 +241,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte) []byte { c, ok := r.getch(false) if !ok { r.Close() - errorExit() + errorExit("Failed to read /dev/tty") } retries := 0 diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 3e636ba..db7cd83 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -25,7 +25,6 @@ int c_getcurx(WINDOW* win) { import "C" import ( - "fmt" "os" "strconv" "strings" @@ -109,13 +108,11 @@ func (r *FullscreenRenderer) Init() { C.setlocale(C.LC_ALL, C.CString("")) tty := C.c_tty() if tty == nil { - fmt.Println("Failed to open /dev/tty") - errorExit() + errorExit("Failed to open /dev/tty") } _screen = C.c_newterm(tty) if _screen == nil { - fmt.Println("Invalid $TERM: " + os.Getenv("TERM")) - errorExit() + errorExit("Invalid $TERM: " + os.Getenv("TERM")) } C.set_term(_screen) if r.mouse { @@ -380,7 +377,7 @@ func (r *FullscreenRenderer) GetChar() Event { case C.ERR: // Unexpected error from blocking read r.Close() - errorExit() + errorExit("Failed to read /dev/tty") case C.KEY_UP: return Event{Up, 0, nil} case C.KEY_DOWN: diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 3399d32..8de6fe3 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -6,9 +6,6 @@ import ( "time" "unicode/utf8" - "fmt" - "os" - "runtime" // https://github.com/gdamore/tcell/pull/135 @@ -123,12 +120,10 @@ var ( func (r *FullscreenRenderer) initScreen() { s, e := tcell.NewScreen() if e != nil { - fmt.Fprintf(os.Stderr, "%v\n", e) - errorExit() + errorExit(e.Error()) } if e = s.Init(); e != nil { - fmt.Fprintf(os.Stderr, "%v\n", e) - errorExit() + errorExit(e.Error()) } if r.mouse { s.EnableMouse() diff --git a/src/tui/tui.go b/src/tui/tui.go index 33358e8..4760a38 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -1,6 +1,7 @@ package tui import ( + "fmt" "os" "strconv" "time" @@ -284,7 +285,8 @@ func EmptyTheme() *ColorTheme { Border: colUndefined} } -func errorExit() { +func errorExit(message string) { + fmt.Fprintln(os.Stderr, message) os.Exit(2) } From 48863ac55cc25f8e940cf21f5c32943de90f50c9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 14 Jan 2017 01:04:03 +0900 Subject: [PATCH 366/658] Update invalid $TERM test case --- test/test_go.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_go.rb b/test/test_go.rb index 494d3cd..e41201f 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1081,7 +1081,7 @@ class TestGoFZF < TestBase end def test_invalid_term - lines = `TERM=xxx #{FZF}` + lines = `TERM=xxx #{FZF} 2>&1` assert_equal 2, $?.exitstatus assert lines.include?('Invalid $TERM: xxx') || lines.include?('terminal entry not found') end From 5b68027bee72cf32d61795694a07750d6477ee92 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 14 Jan 2017 01:10:34 +0900 Subject: [PATCH 367/658] Fix $FZF_COMPLETION_OPTS evaluation Close #799 --- shell/completion.bash | 6 +++--- shell/completion.zsh | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index d6d7238..9c4cdb1 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -148,7 +148,7 @@ __fzf_generic_path_completion() { leftover=${leftover/#\/} [ -z "$dir" ] && dir='.' [ "$dir" != "/" ] && dir="${dir/%\//}" - matches=$(eval "$1 $(printf %q "$dir")" | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read -r item; do + matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $2 -q "$leftover" | while read -r item; do printf "%q$3 " "$item" done) matches=${matches% } @@ -183,7 +183,7 @@ _fzf_complete() { if [[ "$cur" == *"$trigger" ]]; then cur=${cur:0:${#cur}-${#trigger}} - selected=$(cat | $fzf $FZF_COMPLETION_OPTS $1 -q "$cur" | $post | tr '\n' ' ') + selected=$(cat | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ') selected=${selected% } # Strip trailing space not to repeat "-o nospace" printf '\e[5n' @@ -215,7 +215,7 @@ _fzf_complete_kill() { local selected fzf fzf="$(__fzfcmd_complete)" - selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ') + selected=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ') printf '\e[5n' if [ -n "$selected" ]; then diff --git a/shell/completion.zsh b/shell/completion.zsh index fb2c16a..d32eff1 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -56,7 +56,7 @@ __fzf_generic_path_completion() { [ -z "$dir" ] && dir='.' [ "$dir" != "/" ] && dir="${dir/%\//}" dir=${~dir} - matches=$(eval "$compgen $(printf %q "$dir")" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$leftover" | while read item; do + matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "$leftover" | while read item; do echo -n "${(q)item}$suffix " done) matches=${matches% } @@ -99,7 +99,7 @@ _fzf_complete() { fzf="$(__fzfcmd_complete)" _fzf_feed_fifo "$fifo" - matches=$(cat "$fifo" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') + matches=$(cat "$fifo" | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$lbuf$matches" fi @@ -164,7 +164,7 @@ fzf-completion() { # Kill completion (do not require trigger sequence) if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then fzf="$(__fzfcmd_complete)" - matches=$(ps -ef | sed 1d | ${=fzf} ${=FZF_COMPLETION_OPTS} -m | awk '{print $2}' | tr '\n' ' ') + matches=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$LBUFFER$matches" fi From 1896aa174877da2e5c6be35219a0a1abcdc4fbf1 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sat, 14 Jan 2017 12:55:30 +0100 Subject: [PATCH 368/658] s:common_sink(): Avoid duplicate BufEnter. (#803) Later versions of Vim/Nvim handle `:edit ` inside try-catch. https://github.com/vim/vim/commit/e13b9afe1283f5ae43232b5992372a0eb570666c https://github.com/vim/vim/pull/1375 --- plugin/fzf.vim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 06af648..046c324 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -155,7 +155,8 @@ function! s:common_sink(action, lines) abort else call s:open(cmd, item) endif - if exists('#BufEnter') && isdirectory(item) + if !has('patch-8.0.0177') && !has('nvim-0.2') && exists('#BufEnter') + \ && isdirectory(item) doautocmd BufEnter endif endfor From 2720816266013793da99f75df4ab90fb3f4013c2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Jan 2017 04:32:39 +0900 Subject: [PATCH 369/658] [vim] Use /dev/tty as STDIN when using --height w/o explicit source --- plugin/fzf.vim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 9c9c6db..d1a81fc 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -428,7 +428,8 @@ function! s:execute(dict, command, use_height, temps) abort let command = escaped endif if a:use_height - call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s 2> /dev/tty', &lines, command)) + let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty' + call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin)) else execute 'silent !'.command endif From 03f5ef08c8276f034269dbb8a6e6fd9decf58439 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Jan 2017 13:10:59 +0900 Subject: [PATCH 370/658] Use crypto/ssh/terminal instead of external stty command --- src/tui/light.go | 65 +++++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index 0bfc57f..2e99aa3 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -11,6 +11,8 @@ import ( "unicode/utf8" "github.com/junegunn/fzf/src/util" + + "golang.org/x/crypto/ssh/terminal" ) const ( @@ -20,10 +22,12 @@ const ( escPollInterval = 5 ) +const consoleDevice string = "/dev/tty" + func openTtyIn() *os.File { - in, err := os.OpenFile("/dev/tty", syscall.O_RDONLY, 0) + in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) if err != nil { - panic("Failed to open /dev/tty") + panic("Failed to open " + consoleDevice) } return in } @@ -64,7 +68,7 @@ type LightRenderer struct { clickY []int ttyin *os.File buffer []byte - ostty string + origState *terminal.State width int height int yoffset int @@ -104,7 +108,14 @@ func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop in return &r } +func (r *LightRenderer) fd() int { + return int(r.ttyin.Fd()) +} + func (r *LightRenderer) defaultTheme() *ColorTheme { + if strings.Contains(os.Getenv("TERM"), "256") { + return Dark256 + } colors, err := exec.Command("tput", "colors").Output() if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 { return Dark256 @@ -112,17 +123,6 @@ func (r *LightRenderer) defaultTheme() *ColorTheme { return Default16 } -func (r *LightRenderer) stty(cmd string) string { - proc := exec.Command("stty", cmd) - proc.Stdin = r.ttyin - out, err := proc.Output() - if err != nil { - // Not sure how to handle this - panic("stty " + cmd + ": " + err.Error()) - } - return strings.TrimSpace(string(out)) -} - func (r *LightRenderer) findOffset() (row int, col int) { r.csi("6n") r.flush() @@ -167,8 +167,13 @@ func (r *LightRenderer) Init() { } r.escDelay = delay - r.ostty = r.stty("-g") - r.stty("raw") + fd := r.fd() + origState, err := terminal.GetState(fd) + if err != nil { + errorExit(err.Error()) + } + r.origState = origState + terminal.MakeRaw(fd) r.updateTerminalSize() initTheme(r.theme, r.defaultTheme(), r.forceBlack) @@ -212,14 +217,22 @@ func (r *LightRenderer) origin() { r.move(0, 0) } +func getEnv(name string, defaultValue int) int { + env := os.Getenv(name) + if len(env) == 0 { + return defaultValue + } + return atoi(env, defaultValue) +} + func (r *LightRenderer) updateTerminalSize() { - sizes := strings.Split(r.stty("size"), " ") - if len(sizes) < 2 { - r.width = defaultWidth - r.height = r.maxHeightFunc(defaultHeight) + width, height, err := terminal.GetSize(r.fd()) + if err == nil { + r.width = width + r.height = r.maxHeightFunc(height) } else { - r.width = atoi(sizes[1], defaultWidth) - r.height = r.maxHeightFunc(atoi(sizes[0], defaultHeight)) + r.width = getEnv("COLUMNS", defaultWidth) + r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight)) } } @@ -241,7 +254,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte) []byte { c, ok := r.getch(false) if !ok { r.Close() - errorExit("Failed to read /dev/tty") + errorExit("Failed to read " + consoleDevice) } retries := 0 @@ -486,13 +499,13 @@ func (r *LightRenderer) mouseSequence(sz *int) Event { } func (r *LightRenderer) Pause() { - r.stty(r.ostty) + terminal.Restore(r.fd(), r.origState) r.csi("?1049h") r.flush() } func (r *LightRenderer) Resume() bool { - r.stty("raw") + terminal.MakeRaw(r.fd()) r.csi("?1049l") r.flush() // Should redraw @@ -525,7 +538,7 @@ func (r *LightRenderer) Close() { r.csi("A") } r.flush() - r.stty(r.ostty) + terminal.Restore(r.fd(), r.origState) } func (r *LightRenderer) MaxX() int { From 4ecb7f3a162186fd7758f3bdedb3a0b4bb32f9d0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Jan 2017 13:22:09 +0900 Subject: [PATCH 371/658] Replace --normalize with --literal and enable normalization by default Ref #790 --- CHANGELOG.md | 5 +++-- man/man1/fzf.1 | 5 ++--- shell/completion.bash | 1 + src/options.go | 10 +++++----- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbc1e41..22a5b75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,9 @@ CHANGELOG - Added `--height HEIGHT[%]` option - Preview window will truncate long lines by default. Line wrap can be enabled by `:wrap` flag in `--preview-window`. -- Added `--normalize` option to normalize latin script letters before - matching. e.g. `sodanco` can match `Só Danço Samba`. +- Latin script letters will be normalized before matching so that it's easier + to match against accented letters. e.g. `sodanco` can match `Só Danço Samba`. + - Normalization can be disabled via `--literal` 0.15.9 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 2ef3cdd..8980316 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -48,9 +48,8 @@ Case-insensitive match (default: smart-case match) .B "+i" Case-sensitive match .TP -.B "--normalize" -Normalize latin script letters before matching. This is not enabled by default -to avoid performance overhead. +.B "--literal" +Do not normalize latin script letters for matching. .TP .BI "--algo=" TYPE Fuzzy matching algorithm (default: v2) diff --git a/shell/completion.bash b/shell/completion.bash index 9c4cdb1..d17bb19 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -67,6 +67,7 @@ _fzf_opts_completion() { --no-hscroll --jump-labels --height + --literal --reverse --margin --inline-info diff --git a/src/options.go b/src/options.go index bcd2458..a0653d4 100644 --- a/src/options.go +++ b/src/options.go @@ -24,7 +24,7 @@ const usage = `usage: fzf [options] --algo=TYPE Fuzzy matching algorithm: [v1|v2] (default: v2) -i Case-insensitive match (default: smart-case match) +i Case-sensitive match - --normalize Normalize latin script letters before matching + --literal Do not normalize latin script letters before matching -n, --nth=N[,..] Comma-separated list of field index expressions for limiting search scope. Each can be a non-zero integer or a range expression ([BEGIN]..[END]). @@ -190,7 +190,7 @@ func defaultOptions() *Options { FuzzyAlgo: algo.FuzzyMatchV2, Extended: true, Case: CaseSmart, - Normalize: false, + Normalize: true, Nth: make([]Range, 0), WithNth: make([]Range, 0), Delimiter: Delimiter{}, @@ -901,10 +901,10 @@ func parseOptions(opts *Options, allArgs []string) { case "-f", "--filter": filter := nextString(allArgs, &i, "query string required") opts.Filter = &filter - case "--normalize": - opts.Normalize = true - case "--no-normalize": + case "--literal": opts.Normalize = false + case "--no-literal": + opts.Normalize = true case "--algo": opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)")) case "--expect": From dc3957ce79cdcbcd4314700db11ef0fc32d3363c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Jan 2017 15:06:37 +0900 Subject: [PATCH 372/658] [completion] Add preview window to kill completion --- shell/completion.bash | 2 +- shell/completion.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index d17bb19..86f8e15 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -216,7 +216,7 @@ _fzf_complete_kill() { local selected fzf fzf="$(__fzfcmd_complete)" - selected=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ') + selected=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap --min-height 15 $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ') printf '\e[5n' if [ -n "$selected" ]; then diff --git a/shell/completion.zsh b/shell/completion.zsh index d32eff1..9a33a14 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -164,7 +164,7 @@ fzf-completion() { # Kill completion (do not require trigger sequence) if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then fzf="$(__fzfcmd_complete)" - matches=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ') + matches=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap --min-height 15 $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$LBUFFER$matches" fi From 208d4f2173c1630408b836312271b3c675ddc5eb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Jan 2017 16:15:51 +0900 Subject: [PATCH 373/658] [shell] Make layout configurable via $FZF_DEFAULT_OPTS and $FZF_{KEY}_OPTS --- shell/completion.bash | 7 +++---- shell/completion.zsh | 7 +++---- shell/key-bindings.bash | 11 +++++------ shell/key-bindings.fish | 24 ++++++++++++++++++------ shell/key-bindings.zsh | 10 +++++----- 5 files changed, 34 insertions(+), 25 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 86f8e15..ad778d1 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -35,8 +35,7 @@ bind '"\e[0n": redraw-current-line' __fzfcmd_complete() { [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] && - echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || - echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse" + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" } _fzf_orig_completion_filter() { @@ -149,7 +148,7 @@ __fzf_generic_path_completion() { leftover=${leftover/#\/} [ -z "$dir" ] && dir='.' [ "$dir" != "/" ] && dir="${dir/%\//}" - matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $2 -q "$leftover" | while read -r item; do + matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $2 -q "$leftover" | while read -r item; do printf "%q$3 " "$item" done) matches=${matches% } @@ -184,7 +183,7 @@ _fzf_complete() { if [[ "$cur" == *"$trigger" ]]; then cur=${cur:0:${#cur}-${#trigger}} - selected=$(cat | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ') + selected=$(cat | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ') selected=${selected% } # Strip trailing space not to repeat "-o nospace" printf '\e[5n' diff --git a/shell/completion.zsh b/shell/completion.zsh index 9a33a14..850aab6 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -32,8 +32,7 @@ fi __fzfcmd_complete() { [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] && - echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || - echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse" + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" } __fzf_generic_path_completion() { @@ -56,7 +55,7 @@ __fzf_generic_path_completion() { [ -z "$dir" ] && dir='.' [ "$dir" != "/" ] && dir="${dir/%\//}" dir=${~dir} - matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "$leftover" | while read item; do + matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "$leftover" | while read item; do echo -n "${(q)item}$suffix " done) matches=${matches% } @@ -99,7 +98,7 @@ _fzf_complete() { fzf="$(__fzfcmd_complete)" _fzf_feed_fifo "$fifo" - matches=$(cat "$fifo" | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') + matches=$(cat "$fifo" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$lbuf$matches" fi diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 382302d..06e9656 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -5,7 +5,7 @@ __fzf_select__() { -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | cut -b3-"}" - eval "$cmd | fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse -m $@ $FZF_CTRL_T_OPTS" | while read -r item; do + eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" fzf -m "$@" | while read -r item; do printf '%q ' "$item" done echo @@ -19,8 +19,7 @@ __fzf_use_tmux__() { __fzfcmd() { __fzf_use_tmux__ && - echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || - echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse" + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" } __fzf_select_tmux__() { @@ -39,7 +38,7 @@ fzf-file-widget() { if __fzf_use_tmux__; then __fzf_select_tmux__ else - local selected="$(__fzf_select__ --height ${FZF_TMUX_HEIGHT:-40%} --reverse)" + local selected="$(__fzf_select__)" READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}" READLINE_POINT=$(( READLINE_POINT + ${#selected} )) fi @@ -49,7 +48,7 @@ __fzf_cd__() { local cmd dir cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" - dir=$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS") && printf 'cd %q' "$dir" + dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir" } __fzf_history__() ( @@ -57,7 +56,7 @@ __fzf_history__() ( shopt -u nocaseglob nocasematch line=$( HISTTIMEFORMAT= history | - eval "$(__fzfcmd) +s --tac --no-reverse +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS" | + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m" $(__fzfcmd) | command grep '^ *[0-9]') && if [[ $- =~ H ]]; then sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index fc61844..3fb7da8 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -21,7 +21,11 @@ function fzf_key_bindings -o -type d -print \ -o -type l -print 2> /dev/null | sed 's#^\./##'" - eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m $FZF_CTRL_T_OPTS" | while read -l r; set result $result $r; end + set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% + begin + set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" + eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m" | while read -l r; set result $result $r; end + end if [ -z "$result" ] commandline -f repaint return @@ -39,8 +43,12 @@ function fzf_key_bindings end function fzf-history-widget -d "Show command history" - history | eval (__fzfcmd) +s +m --no-reverse --tiebreak=index $FZF_CTRL_R_OPTS -q '(commandline)' | read -l result - and commandline -- $result + set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% + begin + set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --no-reverse --tiebreak=index $FZF_CTRL_R_OPTS +m" + history | eval (__fzfcmd) -q '(commandline)' | read -l result + and commandline -- $result + end commandline -f repaint end @@ -48,8 +56,12 @@ function fzf_key_bindings set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND " command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-" - eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m $FZF_ALT_C_OPTS" | read -l result - [ "$result" ]; and cd $result + set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% + begin + set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" + eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m" | read -l result + [ "$result" ]; and cd $result + end commandline -f repaint end @@ -59,7 +71,7 @@ function fzf_key_bindings if [ $FZF_TMUX -eq 1 ] echo "fzf-tmux -d$FZF_TMUX_HEIGHT" else - echo "fzf --height $FZF_TMUX_HEIGHT --reverse" + echo "fzf" end end diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 7e24d92..c8705ad 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -9,7 +9,7 @@ __fsel() { -o -type d -print \ -o -type l -print 2> /dev/null | cut -b3-"}" setopt localoptions pipefail 2> /dev/null - eval "$cmd | $(__fzfcmd) -m $FZF_CTRL_T_OPTS" | while read item; do + eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do echo -n "${(q)item} " done local ret=$? @@ -23,8 +23,7 @@ __fzf_use_tmux__() { __fzfcmd() { __fzf_use_tmux__ && - echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || - echo "fzf --height ${FZF_TMUX_HEIGHT:-40%} --reverse" + echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf" } fzf-file-widget() { @@ -42,7 +41,7 @@ fzf-cd-widget() { local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" setopt localoptions pipefail 2> /dev/null - cd "${$(eval "$cmd | $(__fzfcmd) +m $FZF_ALT_C_OPTS"):-.}" + cd "${$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m):-.}" local ret=$? zle reset-prompt typeset -f zle-line-init >/dev/null && zle zle-line-init @@ -55,7 +54,8 @@ bindkey '\ec' fzf-cd-widget fzf-history-widget() { local selected num setopt localoptions noglobsubst pipefail 2> /dev/null - selected=( $(fc -l 1 | eval "$(__fzfcmd) +s --tac --no-reverse +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS -q ${(q)LBUFFER}") ) + selected=( $(fc -l 1 | + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m --query=${(q)LBUFFER}" $(__fzfcmd)) ) local ret=$? if [ -n "$selected" ]; then num=$selected[1] From e0036b5ad208f71d02447c233a621e67185b0fff Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Jan 2017 19:42:28 +0900 Subject: [PATCH 374/658] Add --filepath-word option Close #802 --- CHANGELOG.md | 2 ++ man/man1/fzf.1 | 12 ++++++++++++ src/options.go | 7 +++++++ src/terminal.go | 19 +++++++++++++++---- test/test_go.rb | 13 +++++++++++++ 5 files changed, 49 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a5b75..d5802c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ CHANGELOG - Latin script letters will be normalized before matching so that it's easier to match against accented letters. e.g. `sodanco` can match `Só Danço Samba`. - Normalization can be disabled via `--literal` +- Added `--filepath-word` to make word-wise movements/actions (`alt-b`, + `alt-f`, `alt-bs`, `alt-d`) respect path separators 0.15.9 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 8980316..accf3f2 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -129,6 +129,18 @@ Number of screen columns to keep to the right of the highlighted substring (default: 10). Setting it to a large value will cause the text to be positioned on the center of the screen. .TP +.B "--filepath-word" +Make word-wise movements and actions respect path separators. The following +actions are affected: + +\fBbackward-kill-word\fR +.br +\fBbackward-word\fR +.br +\fBforward-word\fR +.br +\fBkill-word\fR +.TP .BI "--jump-labels=" "CHARS" Label characters for \fBjump\fR and \fBjump-accept\fR .SS Layout diff --git a/src/options.go b/src/options.go index a0653d4..7885325 100644 --- a/src/options.go +++ b/src/options.go @@ -45,6 +45,7 @@ const usage = `usage: fzf [options] --no-hscroll Disable horizontal scroll --hscroll-off=COL Number of screen columns to keep to the right of the highlighted substring (default: 10) + --filepath-word Make word-wise movements respect path separators --jump-labels=CHARS Label characters for jump and jump-accept Layout @@ -160,6 +161,7 @@ type Options struct { Cycle bool Hscroll bool HscrollOff int + FileWord bool InlineInfo bool JumpLabels string Prompt string @@ -208,6 +210,7 @@ func defaultOptions() *Options { Cycle: false, Hscroll: true, HscrollOff: 10, + FileWord: false, InlineInfo: false, JumpLabels: defaultJumpLabels, Prompt: "> ", @@ -976,6 +979,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Hscroll = false case "--hscroll-off": opts.HscrollOff = nextInt(allArgs, &i, "hscroll offset required") + case "--filepath-word": + opts.FileWord = true + case "--no-filepath-word": + opts.FileWord = false case "--inline-info": opts.InlineInfo = true case "--no-inline-info": diff --git a/src/terminal.go b/src/terminal.go index 3a89bb4..69735d9 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -59,6 +59,8 @@ type Terminal struct { reverse bool hscroll bool hscrollOff int + wordRubout string + wordNext string cx int cy int offset int @@ -291,6 +293,13 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { } else { renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) } + wordRubout := "[^[:alnum:]][[:alnum:]]" + wordNext := "[[:alnum:]][^[:alnum:]]|(.$)" + if opts.FileWord { + sep := regexp.QuoteMeta(string(os.PathSeparator)) + wordRubout = fmt.Sprintf("%s[^%s]", sep, sep) + wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep) + } return &Terminal{ initDelay: delay, inlineInfo: opts.InlineInfo, @@ -298,6 +307,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { reverse: opts.Reverse, hscroll: opts.Hscroll, hscrollOff: opts.HscrollOff, + wordRubout: wordRubout, + wordNext: wordNext, cx: len(input), cy: 0, offset: 0, @@ -1448,7 +1459,7 @@ func (t *Terminal) Loop() { } case actBackwardKillWord: if t.cx > 0 { - t.rubout("[^[:alnum:]][[:alnum:]]") + t.rubout(t.wordRubout) } case actYank: suffix := copySlice(t.input[t.cx:]) @@ -1467,12 +1478,12 @@ func (t *Terminal) Loop() { t.jumping = jumpAcceptEnabled req(reqJump) case actBackwardWord: - t.cx = findLastMatch("[^[:alnum:]][[:alnum:]]", string(t.input[:t.cx])) + 1 + t.cx = findLastMatch(t.wordRubout, string(t.input[:t.cx])) + 1 case actForwardWord: - t.cx += findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 + t.cx += findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1 case actKillWord: ncx := t.cx + - findFirstMatch("[[:alnum:]][^[:alnum:]]|(.$)", string(t.input[t.cx:])) + 1 + findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1 if ncx > t.cx { t.yanked = copySlice(t.input[t.cx:ncx]) t.input = append(t.input[:t.cx], t.input[ncx:]...) diff --git a/test/test_go.rb b/test/test_go.rb index e41201f..6a8e3bf 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -322,6 +322,19 @@ class TestGoFZF < TestBase tmux.until { |lines| lines.last !~ /^>/ } end + def test_file_word + tmux.send_keys "#{FZF} -q '--/foo bar/foo-bar/baz' --filepath-word", :Enter + tmux.until { |lines| lines.last =~ /^>/ } + + tmux.send_keys :Escape, :b + tmux.send_keys :Escape, :b + tmux.send_keys :Escape, :b + tmux.send_keys :Escape, :d + tmux.send_keys :Escape, :f + tmux.send_keys :Escape, :BSpace + tmux.until { |lines| lines.last == '> --///baz' } + end + def test_multi_order tmux.send_keys "seq 1 10 | #{fzf :multi}", :Enter tmux.until { |lines| lines.last =~ /^>/ } From 44d3faa048df85355ab59ea8b9b9d52dd288532a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Jan 2017 22:02:04 +0900 Subject: [PATCH 375/658] [completion] Restore --height option for kill completion --- shell/completion.bash | 2 +- shell/completion.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index ad778d1..6ef0386 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -215,7 +215,7 @@ _fzf_complete_kill() { local selected fzf fzf="$(__fzfcmd_complete)" - selected=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap --min-height 15 $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ') + selected=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ') printf '\e[5n' if [ -n "$selected" ]; then diff --git a/shell/completion.zsh b/shell/completion.zsh index 850aab6..9b9dba3 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -163,7 +163,7 @@ fzf-completion() { # Kill completion (do not require trigger sequence) if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then fzf="$(__fzfcmd_complete)" - matches=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="$FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap --min-height 15 $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ') + matches=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$LBUFFER$matches" fi From ede7bfb90105b7df5bf115a837e307ac3f2540fe Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 16 Jan 2017 02:26:36 +0900 Subject: [PATCH 376/658] Optimize LightRenderer for slow terminals --- src/terminal.go | 39 +++++++++++++++++++++++---------------- src/tui/light.go | 23 ++++++++++++++++++----- src/tui/ncurses.go | 4 ++++ src/tui/tcell.go | 4 ++++ src/tui/tui.go | 1 + 5 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 69735d9..02c8f1f 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -46,6 +46,7 @@ type itemLine struct { current bool selected bool label string + width int result Result } @@ -678,13 +679,17 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) { } // Avoid unnecessary redraw - newLine := itemLine{current, selected, label, *result} - if t.prevLines[i] == newLine { + newLine := itemLine{current: current, selected: selected, label: label, result: *result, width: 0} + prevLine := t.prevLines[i] + if prevLine.current == newLine.current && + prevLine.selected == newLine.selected && + prevLine.label == newLine.label && + prevLine.result == newLine.result { return } - t.prevLines[i] = newLine - t.move(line, 0, true) + // Optimized renderer can simply erase to the end of the window + t.move(line, 0, t.tui.IsOptimized()) t.window.CPrint(tui.ColCursor, t.strong, label) if current { if selected { @@ -692,15 +697,22 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) { } else { t.window.CPrint(tui.ColCurrent, t.strong, " ") } - t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true) + newLine.width = t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true) } else { if selected { t.window.CPrint(tui.ColSelected, t.strong, ">") } else { t.window.Print(" ") } - t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false, true) + newLine.width = t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false, true) } + if !t.tui.IsOptimized() { + fillSpaces := prevLine.width - newLine.width + if fillSpaces > 0 { + t.window.Print(strings.Repeat(" ", fillSpaces)) + } + } + t.prevLines[i] = newLine } func (t *Terminal) trimRight(runes []rune, width int) ([]rune, int) { @@ -745,17 +757,10 @@ func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) { } func (t *Terminal) overflow(runes []rune, max int) bool { - l := 0 - for _, r := range runes { - l += util.RuneWidth(r, l, t.tabstop) - if l > max { - return true - } - } - return false + return t.displayWidthWithLimit(runes, 0, max) > max } -func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) { +func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) int { item := result.item // Overflow @@ -783,7 +788,8 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo offsets := result.colorOffsets(charOffsets, t.theme, col2, attr, current) maxWidth := t.window.Width() - 3 maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) - if t.overflow(text, maxWidth) { + displayWidth := t.displayWidthWithLimit(text, 0, maxWidth) + if displayWidth > maxWidth { if t.hscroll { // Stri.. if !t.overflow(text[:maxe], maxWidth-2) { @@ -845,6 +851,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo substr, _ = t.processTabs(text[index:], prefixWidth) t.window.CPrint(col1, attr, substr) } + return displayWidth } func numLinesMax(str string, max int) int { diff --git a/src/tui/light.go b/src/tui/light.go index 2e99aa3..075fd6b 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -32,13 +32,18 @@ func openTtyIn() *os.File { return in } -// FIXME: Need better handling of non-displayable characters func (r *LightRenderer) stderr(str string) { + r.stderrInternal(str, true) +} + +// FIXME: Need better handling of non-displayable characters +func (r *LightRenderer) stderrInternal(str string, allowNLCR bool) { bytes := []byte(str) runes := []rune{} for len(bytes) > 0 { r, sz := utf8.DecodeRune(bytes) - if r == utf8.RuneError || r != '\x1b' && r != '\n' && r != '\r' && r < 32 { + if r == utf8.RuneError || r < 32 && + r != '\x1b' && (!allowNLCR || r != '\n' && r != '\r') { runes = append(runes, '?') } else { runes = append(runes, r) @@ -553,6 +558,10 @@ func (r *LightRenderer) DoesAutoWrap() bool { return true } +func (r *LightRenderer) IsOptimized() bool { + return false +} + func (r *LightRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { w := &LightWindow{ renderer: r, @@ -594,6 +603,10 @@ func (w *LightWindow) stderr(str string) { w.renderer.stderr(str) } +func (w *LightWindow) stderrInternal(str string, allowNLCR bool) { + w.renderer.stderrInternal(str, allowNLCR) +} + func (w *LightWindow) Top() int { return w.top } @@ -703,7 +716,7 @@ func (w *LightWindow) CPrint(pair ColorPair, attr Attr, text string) { } else { w.csiColor(pair.Fg(), pair.Bg(), attr) } - w.stderr(text) + w.stderrInternal(text, false) w.csi("m") } @@ -711,7 +724,7 @@ func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) { if w.csiColor(fg, bg, attr) { defer w.csi("m") } - w.stderr(text) + w.stderrInternal(text, false) } type wrappedLine struct { @@ -754,7 +767,7 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn { } return FillNextLine } - w.stderr(wl.text) + w.stderrInternal(wl.text, false) w.posx += wl.displayWidth if j < len(lines)-1 || i < len(allLines)-1 { if w.posy+1 >= w.height { diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index db7cd83..0978ea8 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -279,6 +279,10 @@ func (r *FullscreenRenderer) DoesAutoWrap() bool { return true } +func (r *FullscreenRenderer) IsOptimized() bool { + return true +} + func (w *CursesWindow) Fill(str string) FillReturn { if C.waddstr(w.impl, C.CString(str)) == C.OK { return FillContinue diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 8de6fe3..c898a38 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -158,6 +158,10 @@ func (r *FullscreenRenderer) DoesAutoWrap() bool { return false } +func (r *FullscreenRenderer) IsOptimized() bool { + return false +} + func (r *FullscreenRenderer) Clear() { _screen.Sync() _screen.Clear() diff --git a/src/tui/tui.go b/src/tui/tui.go index 4760a38..fd4a21e 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -204,6 +204,7 @@ type Renderer interface { MaxX() int MaxY() int DoesAutoWrap() bool + IsOptimized() bool NewWindow(top int, left int, width int, height int, border bool) Window } From 4bece042077dbf2a37e4f6a9e03e34e2cd20014c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 16 Jan 2017 02:39:37 +0900 Subject: [PATCH 377/658] 0.16.0 --- CHANGELOG.md | 3 ++- install | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5802c8..d46df32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ CHANGELOG 0.16.0 ------ -- Added `--height HEIGHT[%]` option +- *Added `--height HEIGHT[%]` option* + - fzf can now display finder without occupying the full screen - Preview window will truncate long lines by default. Line wrap can be enabled by `:wrap` flag in `--preview-window`. - Latin script letters will be normalized before matching so that it's easier diff --git a/install b/install index 7a2481f..3c338fb 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.16.0-alpha +version=0.16.0 auto_completion= key_bindings= update_config=2 diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index accf3f2..a7abf84 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -147,7 +147,7 @@ Label characters for \fBjump\fR and \fBjump-accept\fR .TP .BI "--height=" "HEIGHT[%]" Display fzf window below the cursor with the given height instead of using -fullscreen. +the full screen. .TP .BI "--min-height=" "HEIGHT" Minimum height when \fB--height\fR is given in percent (default: 10). diff --git a/src/constants.go b/src/constants.go index 9640dbe..1abc9b9 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.16.0-alpha" + version = "0.16.0" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 11015df52f19a7eb551c460783c2f8ffb6c8afaf Mon Sep 17 00:00:00 2001 From: Amos Bird Date: Mon, 16 Jan 2017 10:58:13 +0800 Subject: [PATCH 378/658] Add half-page-{up,down} actions (#784) --- man/man1/fzf.1 | 2 ++ src/options.go | 4 ++++ src/terminal.go | 8 ++++++++ 3 files changed, 14 insertions(+) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index a7abf84..0225c8e 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -470,6 +470,8 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBnext-history\fR (\fIctrl-n\fR on \fB--history\fR) \fBpage-down\fR \fIpgdn\fR \fBpage-up\fR \fIpgup\fR + \fBhalf-page-down\fR + \fBhalf-page-up\fR \fBpreview-down\fR \fBpreview-up\fR \fBpreview-page-down\fR diff --git a/src/options.go b/src/options.go index 7885325..16020dd 100644 --- a/src/options.go +++ b/src/options.go @@ -688,6 +688,10 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) keymap[key] = actPageUp case "page-down": keymap[key] = actPageDown + case "half-page-up": + keymap[key] = actHalfPageUp + case "half-page-down": + keymap[key] = actHalfPageDown case "previous-history": keymap[key] = actPreviousHistory case "next-history": diff --git a/src/terminal.go b/src/terminal.go index 02c8f1f..4d207b4 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -186,6 +186,8 @@ const ( actUp actPageUp actPageDown + actHalfPageUp + actHalfPageDown actJump actJumpAccept actPrintQuery @@ -1478,6 +1480,12 @@ func (t *Terminal) Loop() { case actPageDown: t.vmove(-(t.maxItems() - 1)) req(reqList) + case actHalfPageUp: + t.vmove(t.maxItems() / 2) + req(reqList) + case actHalfPageDown: + t.vmove(-(t.maxItems() / 2)) + req(reqList) case actJump: t.jumping = jumpEnabled req(reqJump) From f96173cbe4129124e6cbf66804e06dbabc9066c3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 16 Jan 2017 12:01:58 +0900 Subject: [PATCH 379/658] Add -L flag to the default find command Close #781 --- src/constants_unix.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants_unix.go b/src/constants_unix.go index 52677e6..05f66d0 100644 --- a/src/constants_unix.go +++ b/src/constants_unix.go @@ -4,5 +4,5 @@ package fzf const ( // Reader - defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//` + defaultCommand = `find -L . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//` ) From 8e2e63f9b9e7fb2a9f4c58ebc18c5d8a0b4924fe Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 16 Jan 2017 12:06:54 +0900 Subject: [PATCH 380/658] Propertly fill window with background color Close #805 --- src/terminal.go | 5 +++++ src/tui/tui.go | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/terminal.go b/src/terminal.go index 4d207b4..2378984 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -558,6 +558,11 @@ func (t *Terminal) resizeWindows() { width, height, false) } + if !t.tui.IsOptimized() && t.theme != nil && t.theme.HasBg() { + for i := 0; i < t.window.Height(); i++ { + t.window.MoveAndClear(i, 0) + } + } } func (t *Terminal) move(y int, x int, clear bool) { diff --git a/src/tui/tui.go b/src/tui/tui.go index fd4a21e..c8bd5fb 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -175,6 +175,10 @@ type ColorTheme struct { Border Color } +func (t *ColorTheme) HasBg() bool { + return t.Bg != colDefault +} + type Event struct { Type int Char rune From 62ab8ece5edb5e7e2d36ffd24d0e32071c863cdc Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 16 Jan 2017 12:12:59 +0900 Subject: [PATCH 381/658] 0.16.1 --- CHANGELOG.md | 7 +++++++ install | 2 +- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d46df32..473f899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +0.16.1 +------ +- Fixed `--height` option to properly fill the window with the background + color +- Added `half-page-up` and `half-page-down` actions +- Added `-L` flag to the default find command + 0.16.0 ------ - *Added `--height HEIGHT[%]` option* diff --git a/install b/install index 3c338fb..baa7870 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.16.0 +version=0.16.1 auto_completion= key_bindings= update_config=2 diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 6ccdb66..dd5c096 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Jan 2017" "fzf 0.16.0" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Jan 2017" "fzf 0.16.1" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 0225c8e..5160d6b 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Jan 2017" "fzf 0.16.0" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jan 2017" "fzf 0.16.1" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 1abc9b9..0faa7da 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.16.0" + version = "0.16.1" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 7ef0e50507e3896edc1b624d5ea5e73b7fac3506 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 17 Jan 2017 11:58:25 +0900 Subject: [PATCH 382/658] [bash/zsh] Remove unused --reverse in CTRL-R binding Related #807 --- shell/key-bindings.bash | 2 +- shell/key-bindings.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 06e9656..f8e47f5 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -56,7 +56,7 @@ __fzf_history__() ( shopt -u nocaseglob nocasematch line=$( HISTTIMEFORMAT= history | - FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m" $(__fzfcmd) | + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m" $(__fzfcmd) | command grep '^ *[0-9]') && if [[ $- =~ H ]]; then sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index c8705ad..bed639e 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -55,7 +55,7 @@ fzf-history-widget() { local selected num setopt localoptions noglobsubst pipefail 2> /dev/null selected=( $(fc -l 1 | - FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m --query=${(q)LBUFFER}" $(__fzfcmd)) ) + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m --query=${(q)LBUFFER}" $(__fzfcmd)) ) local ret=$? if [ -n "$selected" ]; then num=$selected[1] From 8ac37d59278209a7aec536f319c26109ef6704eb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 17 Jan 2017 18:09:29 +0900 Subject: [PATCH 383/658] [shell] Do not override --reverse in CTRL-R Close #807 --- shell/key-bindings.bash | 2 +- shell/key-bindings.fish | 2 +- shell/key-bindings.zsh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index f8e47f5..cbc1c48 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -56,7 +56,7 @@ __fzf_history__() ( shopt -u nocaseglob nocasematch line=$( HISTTIMEFORMAT= history | - FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m" $(__fzfcmd) | + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m" $(__fzfcmd) | command grep '^ *[0-9]') && if [[ $- =~ H ]]; then sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index 3fb7da8..d20a850 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -45,7 +45,7 @@ function fzf_key_bindings function fzf-history-widget -d "Show command history" set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% begin - set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --no-reverse --tiebreak=index $FZF_CTRL_R_OPTS +m" + set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --tiebreak=index $FZF_CTRL_R_OPTS +m" history | eval (__fzfcmd) -q '(commandline)' | read -l result and commandline -- $result end diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index bed639e..1671e89 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -55,7 +55,7 @@ fzf-history-widget() { local selected num setopt localoptions noglobsubst pipefail 2> /dev/null selected=( $(fc -l 1 | - FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m --query=${(q)LBUFFER}" $(__fzfcmd)) ) + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m --query=${(q)LBUFFER}" $(__fzfcmd)) ) local ret=$? if [ -n "$selected" ]; then num=$selected[1] From d09ad13208118e8ff6232137d75e63eadca51cb3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 21 Jan 2017 03:59:36 +0900 Subject: [PATCH 384/658] [zsh] Workaround trailing esacped space bug in go-shellwords https://github.com/mattn/go-shellwords/issues/3 Close #812 --- shell/key-bindings.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 1671e89..7b8227e 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -55,7 +55,7 @@ fzf-history-widget() { local selected num setopt localoptions noglobsubst pipefail 2> /dev/null selected=( $(fc -l 1 | - FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m --query=${(q)LBUFFER}" $(__fzfcmd)) ) + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) ) local ret=$? if [ -n "$selected" ]; then num=$selected[1] From a06ccc928f46e9c095680bc67b9f65c61f323e23 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 21 Jan 2017 04:17:51 +0900 Subject: [PATCH 385/658] Fix flakies --- test/test_go.rb | 53 +++++++++++++++---------------------------------- 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 6a8e3bf..a352320 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -111,7 +111,7 @@ class Tmux File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse end - def until pane = 0 + def until refresh = false, pane = 0 lines = nil begin wait do @@ -141,7 +141,9 @@ class Tmux self.select { |line| line.send method, val }.first end end - yield lines + yield(lines).tap do |ok| + send_keys 'C-l' if refresh && !ok + end end rescue Exception puts $!.backtrace @@ -1067,7 +1069,7 @@ class TestGoFZF < TestBase }.each do |ts, exp| tmux.prepare tmux.send_keys %[cat #{tempname} | fzf --tabstop=#{ts}], :Enter - tmux.until { |lines| exp.start_with? lines[-3].to_s.strip.sub(/\.\.$/, '') } + tmux.until(true) { |lines| exp.start_with? lines[-3].to_s.strip.sub(/\.\.$/, '') } tmux.send_keys :Enter end end @@ -1375,8 +1377,7 @@ module CompletionTest tmux.send_keys :Tab, :Tab tmux.until { |lines| lines.select_count == 2 } tmux.send_keys :Enter - tmux.until do |lines| - tmux.send_keys 'C-L' + tmux.until(true) do |lines| lines[-1].include?('/tmp/fzf-test/10') && lines[-1].include?('/tmp/fzf-test/100') end @@ -1388,20 +1389,16 @@ module CompletionTest tmux.send_keys "'.fzf-home" tmux.until { |lines| lines.select { |l| l.include? '.fzf-home' }.count > 1 } tmux.send_keys :Enter - tmux.until do |lines| - tmux.send_keys 'C-L' + tmux.until(true) do |lines| lines[-1].end_with?('.fzf-home') end # ~INVALID_USERNAME** tmux.send_keys 'C-u' tmux.send_keys "cat ~such**", :Tab - tmux.until { |lines| lines.any_include? 'no~such~user' } + tmux.until(true) { |lines| lines.any_include? 'no~such~user' } tmux.send_keys :Enter - tmux.until do |lines| - tmux.send_keys 'C-L' - lines[-1].end_with?('no~such~user') - end + tmux.until(true) { |lines| lines[-1].end_with?('no~such~user') } # /tmp/fzf\ test** tmux.send_keys 'C-u' @@ -1410,19 +1407,13 @@ module CompletionTest tmux.send_keys 'foobar$' tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Enter - tmux.until do |lines| - tmux.send_keys 'C-L' - lines[-1].end_with?('/tmp/fzf\ test/foobar') - end + tmux.until(true) { |lines| lines[-1].end_with?('/tmp/fzf\ test/foobar') } # Should include hidden files (1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" } tmux.send_keys 'C-u' tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab - tmux.until do |lines| - tmux.send_keys 'C-L' - lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-') - end + tmux.until(true) { |lines| lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-') } tmux.send_keys :Enter ensure ['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f| @@ -1448,10 +1439,7 @@ module CompletionTest tmux.send_keys 55 tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Enter - tmux.until do |lines| - tmux.send_keys 'C-L' - lines[-1] == 'cd /tmp/fzf-test/d55/' - end + tmux.until(true) { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' } tmux.send_keys :xx tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' } @@ -1479,10 +1467,7 @@ module CompletionTest tmux.send_keys 'sleep12345' tmux.until { |lines| lines.any_include? 'sleep 12345' } tmux.send_keys :Enter - tmux.until do |lines| - tmux.send_keys 'C-L' - lines[-1].include? "kill #{pid}" - end + tmux.until(true) { |lines| lines[-1].include? "kill #{pid}" } ensure Process.kill 'KILL', pid.to_i rescue nil if pid end @@ -1495,10 +1480,7 @@ module CompletionTest tmux.send_keys :Tab, :Tab, :Tab tmux.until { |lines| lines.select_count == 3 } tmux.send_keys :Enter - tmux.until do |lines| - tmux.send_keys 'C-L' - lines[-1] == "ls /tmp 1 2" - end + tmux.until(true) { |lines| lines[-1] == "ls /tmp 1 2" } end def test_unset_completion @@ -1515,7 +1497,7 @@ module CompletionTest # FZF_TMUX=1 new_shell tmux.send_keys 'unset FZFFO**', :Tab, pane: 0 - tmux.until(1) { |lines| lines.match_count == 1 } + tmux.until(false, 1) { |lines| lines.match_count == 1 } tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' } end @@ -1541,10 +1523,7 @@ module CompletionTest tmux.until { |lines| lines.select_count == 2 } tmux.send_keys :Enter - tmux.until do |lines| - tmux.send_keys 'C-l' - lines.any_include? 'cat' - end + tmux.until(true) { |lines| lines.any_include? 'cat' } tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'test3test4' } end From 131aa5dd15788875e796bbd2f2bfdf644584284a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Jan 2017 02:32:49 +0900 Subject: [PATCH 386/658] Composable actions in --bind Close #816 --- CHANGELOG.md | 6 + man/man1/fzf.1 | 12 +- src/options.go | 294 ++++++++++++++++++++++++-------------------- src/options_test.go | 80 ++++++------ src/terminal.go | 141 ++++++++++++--------- 5 files changed, 294 insertions(+), 239 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 473f899..fac6f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +0.16.2 +------ +- Added support for composite actions in `--bind`. Multiple actions can be + chained using `+` separator. + - e.g. `fzf --bind 'ctrl-y:execute(echo -n {} | pbcopy)+abort'` + 0.16.1 ------ - Fixed `--height` option to properly fill the window with the background diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 5160d6b..b139dcc 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -481,17 +481,21 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBselect-all\fR \fBtoggle\fR \fBtoggle-all\fR - \fBtoggle-down\fR \fIctrl-i (tab)\fR - \fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle-up\fR : \fBtoggle-down\fR) - \fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle-down\fR : \fBtoggle-up\fR) + \fBtoggle+down\fR \fIctrl-i (tab)\fR + \fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR) + \fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR) \fBtoggle-preview\fR \fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR) - \fBtoggle-up\fR \fIbtab (shift-tab)\fR + \fBtoggle+up\fR \fIbtab (shift-tab)\fR \fBunix-line-discard\fR \fIctrl-u\fR \fBunix-word-rubout\fR \fIctrl-w\fR \fBup\fR \fIctrl-k ctrl-p up\fR \fByank\fR \fIctrl-y\fR +Multiple actions can be chained using \fB+\fR separator. + + \fBfzf --bind 'ctrl-a:select-all+accept'\fR + With \fBexecute(...)\fR action, you can execute arbitrary commands without leaving fzf. For example, you can turn fzf into a simple file browser by binding \fBenter\fR key to \fBless\fR command like follows. diff --git a/src/options.go b/src/options.go index 16020dd..eb21ee7 100644 --- a/src/options.go +++ b/src/options.go @@ -171,8 +171,7 @@ type Options struct { Filter *string ToggleSort bool Expect map[int]string - Keymap map[int]actionType - Execmap map[int]string + Keymap map[int][]action Preview previewOpts PrintQuery bool ReadZero bool @@ -220,8 +219,7 @@ func defaultOptions() *Options { Filter: nil, ToggleSort: false, Expect: make(map[int]string), - Keymap: make(map[int]actionType), - Execmap: make(map[int]string), + Keymap: make(map[int][]action), Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false}, PrintQuery: false, ReadZero: false, @@ -578,23 +576,25 @@ func firstKey(keymap map[int]string) int { const ( escapedColon = 0 escapedComma = 1 + escapedPlus = 2 ) -func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) { +func parseKeymap(keymap map[int][]action, str string) { if executeRegexp == nil { // Backreferences are not supported. // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') executeRegexp = regexp.MustCompile( - "(?s):execute(-multi)?:.*|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") + "(?si):execute(-multi)?:.+|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") } masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { - if strings.HasPrefix(src, ":execute-multi") { + if src[len(":execute")] == '-' { return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")" } return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")" }) masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) + masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1) idx := 0 for _, pairStr := range strings.Split(masked, ",") { @@ -610,151 +610,173 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) key = ':' + tui.AltZ } else if len(pair[0]) == 1 && pair[0][0] == escapedComma { key = ',' + tui.AltZ + } else if len(pair[0]) == 1 && pair[0][0] == escapedPlus { + key = '+' + tui.AltZ } else { keys := parseKeyChords(pair[0], "key name required") key = firstKey(keys) } - act := origPairStr[len(pair[0])+1 : len(origPairStr)] - actLower := strings.ToLower(act) - switch actLower { - case "ignore": - keymap[key] = actIgnore - case "beginning-of-line": - keymap[key] = actBeginningOfLine - case "abort": - keymap[key] = actAbort - case "accept": - keymap[key] = actAccept - case "print-query": - keymap[key] = actPrintQuery - case "backward-char": - keymap[key] = actBackwardChar - case "backward-delete-char": - keymap[key] = actBackwardDeleteChar - case "backward-word": - keymap[key] = actBackwardWord - case "clear-screen": - keymap[key] = actClearScreen - case "delete-char": - keymap[key] = actDeleteChar - case "delete-char/eof": - keymap[key] = actDeleteCharEOF - case "end-of-line": - keymap[key] = actEndOfLine - case "cancel": - keymap[key] = actCancel - case "forward-char": - keymap[key] = actForwardChar - case "forward-word": - keymap[key] = actForwardWord - case "jump": - keymap[key] = actJump - case "jump-accept": - keymap[key] = actJumpAccept - case "kill-line": - keymap[key] = actKillLine - case "kill-word": - keymap[key] = actKillWord - case "unix-line-discard", "line-discard": - keymap[key] = actUnixLineDiscard - case "unix-word-rubout", "word-rubout": - keymap[key] = actUnixWordRubout - case "yank": - keymap[key] = actYank - case "backward-kill-word": - keymap[key] = actBackwardKillWord - case "toggle-down": - keymap[key] = actToggleDown - case "toggle-up": - keymap[key] = actToggleUp - case "toggle-in": - keymap[key] = actToggleIn - case "toggle-out": - keymap[key] = actToggleOut - case "toggle-all": - keymap[key] = actToggleAll - case "select-all": - keymap[key] = actSelectAll - case "deselect-all": - keymap[key] = actDeselectAll - case "toggle": - keymap[key] = actToggle - case "down": - keymap[key] = actDown - case "up": - keymap[key] = actUp - case "page-up": - keymap[key] = actPageUp - case "page-down": - keymap[key] = actPageDown - case "half-page-up": - keymap[key] = actHalfPageUp - case "half-page-down": - keymap[key] = actHalfPageDown - case "previous-history": - keymap[key] = actPreviousHistory - case "next-history": - keymap[key] = actNextHistory - case "toggle-preview": - keymap[key] = actTogglePreview - case "toggle-sort": - keymap[key] = actToggleSort - case "preview-up": - keymap[key] = actPreviewUp - case "preview-down": - keymap[key] = actPreviewDown - case "preview-page-up": - keymap[key] = actPreviewPageUp - case "preview-page-down": - keymap[key] = actPreviewPageDown - default: - if isExecuteAction(actLower) { - var offset int - if strings.HasPrefix(actLower, "execute-multi") { - keymap[key] = actExecuteMulti - offset = len("execute-multi") - } else { - keymap[key] = actExecute - offset = len("execute") - } - if act[offset] == ':' { - execmap[key] = act[offset+1:] - } else { - execmap[key] = act[offset+1 : len(act)-1] - } - } else { - errorExit("unknown action: " + act) - } + idx2 := len(pair[0]) + 1 + specs := strings.Split(pair[1], "+") + actions := make([]action, 0, len(specs)) + appendAction := func(types ...actionType) { + actions = append(actions, toActions(types...)...) } + prevSpec := "" + for specIndex, maskedSpec := range specs { + spec := origPairStr[idx2 : idx2+len(maskedSpec)] + idx2 += len(maskedSpec) + 1 + spec = prevSpec + spec + specLower := strings.ToLower(spec) + switch specLower { + case "ignore": + appendAction(actIgnore) + case "beginning-of-line": + appendAction(actBeginningOfLine) + case "abort": + appendAction(actAbort) + case "accept": + appendAction(actAccept) + case "print-query": + appendAction(actPrintQuery) + case "backward-char": + appendAction(actBackwardChar) + case "backward-delete-char": + appendAction(actBackwardDeleteChar) + case "backward-word": + appendAction(actBackwardWord) + case "clear-screen": + appendAction(actClearScreen) + case "delete-char": + appendAction(actDeleteChar) + case "delete-char/eof": + appendAction(actDeleteCharEOF) + case "end-of-line": + appendAction(actEndOfLine) + case "cancel": + appendAction(actCancel) + case "forward-char": + appendAction(actForwardChar) + case "forward-word": + appendAction(actForwardWord) + case "jump": + appendAction(actJump) + case "jump-accept": + appendAction(actJumpAccept) + case "kill-line": + appendAction(actKillLine) + case "kill-word": + appendAction(actKillWord) + case "unix-line-discard", "line-discard": + appendAction(actUnixLineDiscard) + case "unix-word-rubout", "word-rubout": + appendAction(actUnixWordRubout) + case "yank": + appendAction(actYank) + case "backward-kill-word": + appendAction(actBackwardKillWord) + case "toggle-down": + appendAction(actToggle, actDown) + case "toggle-up": + appendAction(actToggle, actUp) + case "toggle-in": + appendAction(actToggleIn) + case "toggle-out": + appendAction(actToggleOut) + case "toggle-all": + appendAction(actToggleAll) + case "select-all": + appendAction(actSelectAll) + case "deselect-all": + appendAction(actDeselectAll) + case "toggle": + appendAction(actToggle) + case "down": + appendAction(actDown) + case "up": + appendAction(actUp) + case "page-up": + appendAction(actPageUp) + case "page-down": + appendAction(actPageDown) + case "half-page-up": + appendAction(actHalfPageUp) + case "half-page-down": + appendAction(actHalfPageDown) + case "previous-history": + appendAction(actPreviousHistory) + case "next-history": + appendAction(actNextHistory) + case "toggle-preview": + appendAction(actTogglePreview) + case "toggle-sort": + appendAction(actToggleSort) + case "preview-up": + appendAction(actPreviewUp) + case "preview-down": + appendAction(actPreviewDown) + case "preview-page-up": + appendAction(actPreviewPageUp) + case "preview-page-down": + appendAction(actPreviewPageDown) + default: + t := isExecuteAction(specLower) + if t == actIgnore { + errorExit("unknown action: " + spec) + } else { + var offset int + if t == actExecuteMulti { + offset = len("execute-multi") + } else { + offset = len("execute") + } + if spec[offset] == ':' { + if specIndex == len(specs)-1 { + actions = append(actions, action{t: t, a: spec[offset+1:]}) + } else { + prevSpec = spec + "+" + continue + } + } else { + actions = append(actions, action{t: t, a: spec[offset+1 : len(spec)-1]}) + } + } + } + prevSpec = "" + } + keymap[key] = actions } } -func isExecuteAction(str string) bool { - if !strings.HasPrefix(str, "execute") || len(str) < len("execute()") { - return false +func isExecuteAction(str string) actionType { + t := actExecute + if !strings.HasPrefix(str, "execute") || len(str) < len("execute(") { + return actIgnore } + b := str[len("execute")] if strings.HasPrefix(str, "execute-multi") { - if len(str) < len("execute-multi()") { - return false + if len(str) < len("execute-multi(") { + return actIgnore } + t = actExecuteMulti b = str[len("execute-multi")] } e := str[len(str)-1] if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' || b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") { - return true + return t } - return false + return actIgnore } -func parseToggleSort(keymap map[int]actionType, str string) { +func parseToggleSort(keymap map[int][]action, str string) { keys := parseKeyChords(str, "key name required") if len(keys) != 1 { errorExit("multiple keys specified") } - keymap[firstKey(keys)] = actToggleSort + keymap[firstKey(keys)] = toActions(actToggleSort) } func strLines(str string) []string { @@ -919,7 +941,7 @@ func parseOptions(opts *Options, allArgs []string) { case "--tiebreak": opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) case "--bind": - parseKeymap(opts.Keymap, opts.Execmap, nextString(allArgs, &i, "bind expression required")) + parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required")) case "--color": spec := optionalNextString(allArgs, &i) if len(spec) == 0 { @@ -1089,7 +1111,7 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, value := optString(arg, "--color="); match { opts.Theme = parseTheme(opts.Theme, value) } else if match, value := optString(arg, "--bind="); match { - parseKeymap(opts.Keymap, opts.Execmap, value) + parseKeymap(opts.Keymap, value) } else if match, value := optString(arg, "--history="); match { setHistory(value) } else if match, value := optString(arg, "--history-size="); match { @@ -1145,20 +1167,22 @@ func postProcessOptions(opts *Options) { // Default actions for CTRL-N / CTRL-P when --history is set if opts.History != nil { if _, prs := opts.Keymap[tui.CtrlP]; !prs { - opts.Keymap[tui.CtrlP] = actPreviousHistory + opts.Keymap[tui.CtrlP] = toActions(actPreviousHistory) } if _, prs := opts.Keymap[tui.CtrlN]; !prs { - opts.Keymap[tui.CtrlN] = actNextHistory + opts.Keymap[tui.CtrlN] = toActions(actNextHistory) } } // Extend the default key map keymap := defaultKeymap() - for key, act := range opts.Keymap { - if act == actToggleSort { - opts.ToggleSort = true + for key, actions := range opts.Keymap { + for _, act := range actions { + if act.t == actToggleSort { + opts.ToggleSort = true + } } - keymap[key] = act + keymap[key] = actions } opts.Keymap = keymap diff --git a/src/options_test.go b/src/options_test.go index 07616fc..29d9842 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -225,49 +225,51 @@ func TestParseKeysWithComma(t *testing.T) { } func TestBind(t *testing.T) { - check := func(action actionType, expected actionType) { - if action != expected { - t.Errorf("%d != %d", action, expected) - } - } - checkString := func(action string, expected string) { - if action != expected { - t.Errorf("%d != %d", action, expected) - } - } keymap := defaultKeymap() - execmap := make(map[int]string) - check(actBeginningOfLine, keymap[tui.CtrlA]) - parseKeymap(keymap, execmap, - "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 {};,"+ - "alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ - ",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)") - check(actKillLine, keymap[tui.CtrlA]) - check(actToggleSort, keymap[tui.CtrlB]) - check(actPageUp, keymap[tui.AltZ+'c']) - check(actAbort, keymap[tui.AltZ+',']) - check(actAccept, keymap[tui.AltZ+':']) - check(actPageDown, keymap[tui.AltZ]) - check(actExecute, keymap[tui.F1]) - check(actExecute, keymap[tui.F2]) - check(actExecute, keymap[tui.F3]) - check(actExecute, keymap[tui.F4]) - checkString("ls {}", execmap[tui.F1]) - checkString("echo {}, {}, {}", execmap[tui.F2]) - checkString("echo '({})'", execmap[tui.F3]) - checkString("less {}", execmap[tui.F4]) - checkString("echo (,),[,],/,:,;,%,{}", execmap[tui.AltA]) - checkString("echo (,),[,],/,:,@,%,{}", execmap[tui.AltB]) - checkString("\nfoobar,Y:execute(baz)", execmap[tui.AltZ+'X']) + check := func(keyName int, arg1 string, types ...actionType) { + if len(keymap[keyName]) != len(types) { + t.Errorf("invalid number of actions (%d != %d)", len(types), len(keymap[keyName])) + return + } + for idx, action := range keymap[keyName] { + if types[idx] != action.t { + t.Errorf("invalid action type (%d != %d)", types[idx], action.t) + } + } + if len(arg1) > 0 && keymap[keyName][0].a != arg1 { + t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[keyName][0].a) + } + } + check(tui.CtrlA, "", actBeginningOfLine) + parseKeymap(keymap, + "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+ + "f1:execute(ls {})+abort,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ + "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+ + "x:Execute(foo+bar),X:execute/bar+baz/"+ + ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up") + check(tui.CtrlA, "", actKillLine) + check(tui.CtrlB, "", actToggleSort, actUp, actDown) + check(tui.AltZ+'c', "", actPageUp) + check(tui.AltZ+',', "", actAbort) + check(tui.AltZ+':', "", actAccept) + check(tui.AltZ, "", actPageDown) + check(tui.F1, "ls {}", actExecute, actAbort) + check(tui.F2, "echo {}, {}, {}", actExecute) + check(tui.F3, "echo '({})'", actExecute) + check(tui.F4, "less {}", actExecute) + check(tui.AltZ+'x', "foo+bar", actExecute) + check(tui.AltZ+'X', "bar+baz", actExecute) + check(tui.AltA, "echo (,),[,],/,:,;,%,{}", actExecuteMulti) + check(tui.AltB, "echo (,),[,],/,:,@,%,{}", actExecute) + check(tui.AltZ+'+', "++\nfoobar,Y:execute(baz)+up", actExecute) for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { - parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) - checkString("foobar", execmap[tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) + parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) + check(tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute) } - parseKeymap(keymap, execmap, "f1:abort") - check(actAbort, keymap[tui.F1]) + parseKeymap(keymap, "f1:abort") + check(tui.F1, "", actAbort) } func TestColorSpec(t *testing.T) { @@ -327,7 +329,7 @@ func TestDefaultCtrlNP(t *testing.T) { opts := defaultOptions() parseOptions(opts, words) postProcessOptions(opts) - if opts.Keymap[key] != expected { + if opts.Keymap[key][0].t != expected { t.Error() } } diff --git a/src/terminal.go b/src/terminal.go index 2378984..9e30f30 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -72,8 +72,7 @@ type Terminal struct { toggleSort bool delimiter Delimiter expect map[int]string - keymap map[int]actionType - execmap map[int]string + keymap map[int][]action pressed string printQuery bool history *History @@ -148,6 +147,11 @@ const ( reqQuit ) +type action struct { + t actionType + a string +} + type actionType int const ( @@ -203,54 +207,62 @@ const ( actExecuteMulti ) -func defaultKeymap() map[int]actionType { - keymap := make(map[int]actionType) - keymap[tui.Invalid] = actInvalid - keymap[tui.Resize] = actClearScreen - keymap[tui.CtrlA] = actBeginningOfLine - keymap[tui.CtrlB] = actBackwardChar - keymap[tui.CtrlC] = actAbort - keymap[tui.CtrlG] = actAbort - keymap[tui.CtrlQ] = actAbort - keymap[tui.ESC] = actAbort - keymap[tui.CtrlD] = actDeleteCharEOF - keymap[tui.CtrlE] = actEndOfLine - keymap[tui.CtrlF] = actForwardChar - keymap[tui.CtrlH] = actBackwardDeleteChar - keymap[tui.BSpace] = actBackwardDeleteChar - keymap[tui.Tab] = actToggleDown - keymap[tui.BTab] = actToggleUp - keymap[tui.CtrlJ] = actDown - keymap[tui.CtrlK] = actUp - keymap[tui.CtrlL] = actClearScreen - keymap[tui.CtrlM] = actAccept - keymap[tui.CtrlN] = actDown - keymap[tui.CtrlP] = actUp - keymap[tui.CtrlU] = actUnixLineDiscard - keymap[tui.CtrlW] = actUnixWordRubout - keymap[tui.CtrlY] = actYank +func toActions(types ...actionType) []action { + actions := make([]action, len(types)) + for idx, t := range types { + actions[idx] = action{t: t, a: ""} + } + return actions +} - keymap[tui.AltB] = actBackwardWord - keymap[tui.SLeft] = actBackwardWord - keymap[tui.AltF] = actForwardWord - keymap[tui.SRight] = actForwardWord - keymap[tui.AltD] = actKillWord - keymap[tui.AltBS] = actBackwardKillWord +func defaultKeymap() map[int][]action { + keymap := make(map[int][]action) + keymap[tui.Invalid] = toActions(actInvalid) + keymap[tui.Resize] = toActions(actClearScreen) + keymap[tui.CtrlA] = toActions(actBeginningOfLine) + keymap[tui.CtrlB] = toActions(actBackwardChar) + keymap[tui.CtrlC] = toActions(actAbort) + keymap[tui.CtrlG] = toActions(actAbort) + keymap[tui.CtrlQ] = toActions(actAbort) + keymap[tui.ESC] = toActions(actAbort) + keymap[tui.CtrlD] = toActions(actDeleteCharEOF) + keymap[tui.CtrlE] = toActions(actEndOfLine) + keymap[tui.CtrlF] = toActions(actForwardChar) + keymap[tui.CtrlH] = toActions(actBackwardDeleteChar) + keymap[tui.BSpace] = toActions(actBackwardDeleteChar) + keymap[tui.Tab] = toActions(actToggleDown) + keymap[tui.BTab] = toActions(actToggleUp) + keymap[tui.CtrlJ] = toActions(actDown) + keymap[tui.CtrlK] = toActions(actUp) + keymap[tui.CtrlL] = toActions(actClearScreen) + keymap[tui.CtrlM] = toActions(actAccept) + keymap[tui.CtrlN] = toActions(actDown) + keymap[tui.CtrlP] = toActions(actUp) + keymap[tui.CtrlU] = toActions(actUnixLineDiscard) + keymap[tui.CtrlW] = toActions(actUnixWordRubout) + keymap[tui.CtrlY] = toActions(actYank) - keymap[tui.Up] = actUp - keymap[tui.Down] = actDown - keymap[tui.Left] = actBackwardChar - keymap[tui.Right] = actForwardChar + keymap[tui.AltB] = toActions(actBackwardWord) + keymap[tui.SLeft] = toActions(actBackwardWord) + keymap[tui.AltF] = toActions(actForwardWord) + keymap[tui.SRight] = toActions(actForwardWord) + keymap[tui.AltD] = toActions(actKillWord) + keymap[tui.AltBS] = toActions(actBackwardKillWord) - keymap[tui.Home] = actBeginningOfLine - keymap[tui.End] = actEndOfLine - keymap[tui.Del] = actDeleteChar - keymap[tui.PgUp] = actPageUp - keymap[tui.PgDn] = actPageDown + keymap[tui.Up] = toActions(actUp) + keymap[tui.Down] = toActions(actDown) + keymap[tui.Left] = toActions(actBackwardChar) + keymap[tui.Right] = toActions(actForwardChar) - keymap[tui.Rune] = actRune - keymap[tui.Mouse] = actMouse - keymap[tui.DoubleClick] = actAccept + keymap[tui.Home] = toActions(actBeginningOfLine) + keymap[tui.End] = toActions(actEndOfLine) + keymap[tui.Del] = toActions(actDeleteChar) + keymap[tui.PgUp] = toActions(actPageUp) + keymap[tui.PgDn] = toActions(actPageDown) + + keymap[tui.Rune] = toActions(actRune) + keymap[tui.Mouse] = toActions(actMouse) + keymap[tui.DoubleClick] = toActions(actAccept) return keymap } @@ -323,7 +335,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { delimiter: opts.Delimiter, expect: opts.Expect, keymap: opts.Keymap, - execmap: opts.Execmap, pressed: "", printQuery: opts.PrintQuery, history: opts.History, @@ -1314,13 +1325,21 @@ func (t *Terminal) Loop() { } } - var doAction func(actionType, int) bool - doAction = func(action actionType, mapkey int) bool { - switch action { + var doAction func(action, int) bool + doActions := func(actions []action, mapkey int) bool { + for _, action := range actions { + if !doAction(action, mapkey) { + return false + } + } + return true + } + doAction = func(a action, mapkey int) bool { + switch a.t { case actIgnore: case actExecute: if t.cy >= 0 && t.cy < t.merger.Length() { - t.executeCommand(t.execmap[mapkey], []*Item{t.currentItem()}) + t.executeCommand(a.a, []*Item{t.currentItem()}) } case actExecuteMulti: if len(t.selected) > 0 { @@ -1328,9 +1347,9 @@ func (t *Terminal) Loop() { for i, sel := range t.sortSelected() { sels[i] = sel.item } - t.executeCommand(t.execmap[mapkey], sels) + t.executeCommand(a.a, sels) } else { - return doAction(actExecute, mapkey) + return doAction(action{t: actExecute, a: a.a}, mapkey) } case actInvalid: t.mutex.Unlock() @@ -1431,14 +1450,14 @@ func (t *Terminal) Loop() { } case actToggleIn: if t.reverse { - return doAction(actToggleUp, mapkey) + return doAction(action{t: actToggleUp}, mapkey) } - return doAction(actToggleDown, mapkey) + return doAction(action{t: actToggleDown}, mapkey) case actToggleOut: if t.reverse { - return doAction(actToggleDown, mapkey) + return doAction(action{t: actToggleDown}, mapkey) } - return doAction(actToggleUp, mapkey) + return doAction(action{t: actToggleUp}, mapkey) case actToggleDown: if t.multi && t.merger.Length() > 0 { toggle() @@ -1558,7 +1577,7 @@ func (t *Terminal) Loop() { // Double-click if my >= min { if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { - return doAction(t.keymap[tui.DoubleClick], tui.DoubleClick) + return doActions(t.keymap[tui.DoubleClick], tui.DoubleClick) } } } else if me.Down { @@ -1580,14 +1599,14 @@ func (t *Terminal) Loop() { changed := false mapkey := event.Type if t.jumping == jumpDisabled { - action := t.keymap[mapkey] + actions := t.keymap[mapkey] if mapkey == tui.Rune { mapkey = int(event.Char) + int(tui.AltZ) if act, prs := t.keymap[mapkey]; prs { - action = act + actions = act } } - if !doAction(action, mapkey) { + if !doActions(actions, mapkey) { continue } // Truncate the query if it's too long From 24fa183297865aadab0fb91e1ea0bb4fb1491db4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Jan 2017 02:54:19 +0900 Subject: [PATCH 387/658] make deps --- src/Makefile | 1 + src/deps | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100755 src/deps diff --git a/src/Makefile b/src/Makefile index 00df49d..77a4094 100644 --- a/src/Makefile +++ b/src/Makefile @@ -51,6 +51,7 @@ $(SRCDIR): deps: $(SRCDIR) $(SOURCES) cd $(SRCDIR) && go get -tags "$(TAGS)" + ./deps android-build: $(SRCDIR) cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get diff --git a/src/deps b/src/deps new file mode 100755 index 0000000..f384dcf --- /dev/null +++ b/src/deps @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +if [ -z "$GOPATH" ]; then + echo '$GOPATH not defined' + exit 1 +fi + +reset() ( + cd "$GOPATH/src/$1" + [ "$(git rev-parse HEAD)" = "$2" ] || + (git fetch && git reset --hard "$2") +) + +reset github.com/junegunn/go-isatty 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8 +reset github.com/junegunn/go-runewidth 63c378b851290989b19ca955468386485f118c65 +reset github.com/junegunn/go-shellwords 33bd8f1ebe16d6e5eb688cc885749a63059e9167 +reset golang.org/x/crypto abc5fa7ad02123a41f02bf1391c9760f7586e608 From fa7c8977a85b08eee660f42f8bf52628166e422f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Jan 2017 03:03:26 +0900 Subject: [PATCH 388/658] [vim] tput el to clear the last line Close #814 Not grouping commands to avoid errors on non-standard shells. --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 4064a55..8778c78 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -430,7 +430,7 @@ function! s:execute(dict, command, use_height, temps) abort endif if a:use_height let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty' - call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin)) + call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; tput el > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin)) else execute 'silent !'.command endif From 2a669e9a1754c71fb88c6a316378ee708d710efd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Jan 2017 03:19:50 +0900 Subject: [PATCH 389/658] Clear lines even when background color is not set Also revert the workaround in Vim plugin introduced in fa7c897. Related: #814 --- plugin/fzf.vim | 2 +- src/tui/light.go | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 8778c78..4064a55 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -430,7 +430,7 @@ function! s:execute(dict, command, use_height, temps) abort endif if a:use_height let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty' - call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; tput el > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin)) + call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin)) else execute 'silent !'.command endif diff --git a/src/tui/light.go b/src/tui/light.go index 075fd6b..17cc6ae 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -185,11 +185,10 @@ func (r *LightRenderer) Init() { _, x := r.findOffset() if x > 0 { r.upOneLine = true - r.stderr("\n") + r.makeSpace() } for i := 1; i < r.MaxY(); i++ { - r.stderr("\n") - r.csi("G") + r.makeSpace() } if r.mouse { @@ -197,12 +196,19 @@ func (r *LightRenderer) Init() { } r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi("G") + r.csi("K") // r.csi("s") if r.mouse { r.yoffset, _ = r.findOffset() } } +func (r *LightRenderer) makeSpace() { + r.stderr("\n") + r.csi("G") + r.csi("K") +} + func (r *LightRenderer) move(y int, x int) { // w.csi("u") if r.y < y { From 6ccc12c332f97f6be9487d8289c46964e2d41bd3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Jan 2017 05:26:38 +0900 Subject: [PATCH 390/658] Use alternate screen if --height needs the entire screen - Remove unnecessary scrolling - Allow us to use `--height 100%` under Neovim terminal for 24-bit colors Related: - #789 - https://github.com/neovim/neovim/issues/4151 --- src/tui/light.go | 75 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index 17cc6ae..bc71ee5 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -79,6 +79,7 @@ type LightRenderer struct { yoffset int tabstop int escDelay int + fullscreen bool upOneLine bool queued string y int @@ -106,8 +107,9 @@ func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop in forceBlack: forceBlack, mouse: mouse, ttyin: openTtyIn(), - yoffset: -1, + yoffset: 0, tabstop: tabstop, + fullscreen: false, upOneLine: false, maxHeightFunc: maxHeightFunc} return &r @@ -179,16 +181,24 @@ func (r *LightRenderer) Init() { } r.origState = origState terminal.MakeRaw(fd) - r.updateTerminalSize() + terminalHeight, capHeight := r.updateTerminalSize() + if capHeight == terminalHeight { + r.fullscreen = true + r.height = terminalHeight + } initTheme(r.theme, r.defaultTheme(), r.forceBlack) - _, x := r.findOffset() - if x > 0 { - r.upOneLine = true - r.makeSpace() - } - for i := 1; i < r.MaxY(); i++ { - r.makeSpace() + if r.fullscreen { + r.smcup() + } else { + _, x := r.findOffset() + if x > 0 { + r.upOneLine = true + r.makeSpace() + } + for i := 1; i < r.MaxY(); i++ { + r.makeSpace() + } } if r.mouse { @@ -198,7 +208,7 @@ func (r *LightRenderer) Init() { r.csi("G") r.csi("K") // r.csi("s") - if r.mouse { + if !r.fullscreen && r.mouse { r.yoffset, _ = r.findOffset() } } @@ -236,15 +246,20 @@ func getEnv(name string, defaultValue int) int { return atoi(env, defaultValue) } -func (r *LightRenderer) updateTerminalSize() { +func (r *LightRenderer) updateTerminalSize() (int, int) { width, height, err := terminal.GetSize(r.fd()) if err == nil { r.width = width - r.height = r.maxHeightFunc(height) + if r.fullscreen { + r.height = height + } else { + r.height = r.maxHeightFunc(height) + } } else { r.width = getEnv("COLUMNS", defaultWidth) r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight)) } + return height, r.height } func (r *LightRenderer) getch(nonblock bool) (int, bool) { @@ -470,7 +485,7 @@ func (r *LightRenderer) escSequence(sz *int) Event { } func (r *LightRenderer) mouseSequence(sz *int) Event { - if len(r.buffer) < 6 || r.yoffset < 0 { + if len(r.buffer) < 6 || !r.mouse { return Event{Invalid, 0, nil} } *sz = 6 @@ -509,15 +524,31 @@ func (r *LightRenderer) mouseSequence(sz *int) Event { return Event{Invalid, 0, nil} } +func (r *LightRenderer) smcup() { + r.csi("?1049h") +} + +func (r *LightRenderer) rmcup() { + r.csi("?1049l") +} + func (r *LightRenderer) Pause() { terminal.Restore(r.fd(), r.origState) - r.csi("?1049h") + if r.fullscreen { + r.rmcup() + } else { + r.smcup() + } r.flush() } func (r *LightRenderer) Resume() bool { terminal.MakeRaw(r.fd()) - r.csi("?1049l") + if r.fullscreen { + r.smcup() + } else { + r.rmcup() + } r.flush() // Should redraw return true @@ -540,14 +571,18 @@ func (r *LightRenderer) Refresh() { func (r *LightRenderer) Close() { // r.csi("u") - r.origin() - r.csi("J") + if r.fullscreen { + r.rmcup() + } else { + r.origin() + r.csi("J") + if r.upOneLine { + r.csi("A") + } + } if r.mouse { r.csi("?1000l") } - if r.upOneLine { - r.csi("A") - } r.flush() terminal.Restore(r.fd(), r.origState) } From ff248d566d424beb1eda8f7984f385658353b244 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Jan 2017 14:13:37 +0900 Subject: [PATCH 391/658] Drop ncurses dependency Close #818 --- CHANGELOG.md | 2 ++ install | 18 ++++++++++----- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/Makefile | 55 ++++++++++++++++++++++++++++++++++++--------- src/README.md | 17 +++++++++++--- src/constants.go | 2 +- src/terminal.go | 5 ++++- src/tui/dummy.go | 45 +++++++++++++++++++++++++++++++++++++ src/tui/ncurses.go | 5 +++++ src/tui/tcell.go | 4 ++++ 11 files changed, 135 insertions(+), 22 deletions(-) create mode 100644 src/tui/dummy.go diff --git a/CHANGELOG.md b/CHANGELOG.md index fac6f34..e6df93e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ CHANGELOG 0.16.2 ------ +- Dropped ncurses dependency +- Binaries for freebsd, openbsd, arm5, arm6, arm7, and arm8 - Added support for composite actions in `--bind`. Multiple actions can be chained using `+` separator. - e.g. `fzf --bind 'ctrl-y:execute(echo -n {} | pbcopy)+abort'` diff --git a/install b/install index baa7870..0ff0310 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.16.1 +version=0.16.2-alpha auto_completion= key_bindings= update_config=2 @@ -160,10 +160,18 @@ archi=$(uname -sm) binary_available=1 binary_error="" case "$archi" in - Darwin\ x86_64) download fzf-$version-darwin_${binary_arch:-amd64} ;; - Darwin\ i*86) download fzf-$version-darwin_${binary_arch:-386} ;; - Linux\ x86_64) download fzf-$version-linux_${binary_arch:-amd64} ;; - Linux\ i*86) download fzf-$version-linux_${binary_arch:-386} ;; + Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64} ;; + Darwin\ *86) download fzf-$version-darwin_${binary_arch:-386} ;; + Linux\ *64) download fzf-$version-linux_${binary_arch:-amd64} ;; + Linux\ *86) download fzf-$version-linux_${binary_arch:-386} ;; + Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5} ;; + Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6} ;; + Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7} ;; + Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8} ;; + FreeBSD\ *64) download fzf-$version-freebsd_${binary_arch:-amd64} ;; + FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386} ;; + OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64} ;; + OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386} ;; *) binary_available=0 binary_error=1 ;; esac diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index dd5c096..bd7c756 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Jan 2017" "fzf 0.16.1" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Jan 2017" "fzf 0.16.2" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index b139dcc..098ae6a 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Jan 2017" "fzf 0.16.1" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jan 2017" "fzf 0.16.2" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/Makefile b/src/Makefile index 77a4094..e702548 100644 --- a/src/Makefile +++ b/src/Makefile @@ -15,11 +15,17 @@ SRCDIR := $(GOPATH)/src/github.com/junegunn/fzf/src DOCKEROPTS := -i -t -v $(ROOTDIR):/fzf/src BINARY32 := fzf-$(GOOS)_386 BINARY64 := fzf-$(GOOS)_amd64 +BINARYARM5 := fzf-$(GOOS)_arm5 +BINARYARM6 := fzf-$(GOOS)_arm6 BINARYARM7 := fzf-$(GOOS)_arm7 +BINARYARM8 := fzf-$(GOOS)_arm8 VERSION := $(shell awk -F= '/version =/ {print $$2}' constants.go | tr -d "\" ") RELEASE32 := fzf-$(VERSION)-$(GOOS)_386 RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64 +RELEASEARM5 := fzf-$(VERSION)-$(GOOS)_arm5 +RELEASEARM6 := fzf-$(VERSION)-$(GOOS)_arm6 RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7 +RELEASEARM8 := fzf-$(VERSION)-$(GOOS)_arm8 export GOPATH UNAME_M := $(shell uname -m) @@ -35,16 +41,32 @@ all: fzf/$(BINARY) ifeq ($(GOOS),windows) release: fzf/$(BINARY32) fzf/$(BINARY64) - -cd fzf && cp $(BINARY32) $(RELEASE32).exe && zip $(RELEASE32).zip $(RELEASE32).exe - cd fzf && cp $(BINARY64) $(RELEASE64).exe && zip $(RELEASE64).zip $(RELEASE64).exe && \ - rm -f $(RELEASE32).exe $(RELEASE64).exe + cd fzf && cp $(BINARY32) $(RELEASE32).exe && zip $(RELEASE32).zip $(RELEASE32).exe + cd fzf && cp $(BINARY64) $(RELEASE64).exe && zip $(RELEASE64).zip $(RELEASE64).exe + cd fzf && rm -f $(RELEASE32).exe $(RELEASE64).exe +else ifeq ($(GOOS),linux) +release: fzf/$(BINARY32) fzf/$(BINARY64) fzf/$(BINARYARM5) fzf/$(BINARYARM6) fzf/$(BINARYARM7) fzf/$(BINARYARM8) + cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) + cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) + cd fzf && cp $(BINARYARM5) $(RELEASEARM5) && tar -czf $(RELEASEARM5).tgz $(RELEASEARM5) + cd fzf && cp $(BINARYARM6) $(RELEASEARM6) && tar -czf $(RELEASEARM6).tgz $(RELEASEARM6) + cd fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) + cd fzf && cp $(BINARYARM8) $(RELEASEARM8) && tar -czf $(RELEASEARM8).tgz $(RELEASEARM8) + cd fzf && rm -f $(RELEASE32) $(RELEASE64) $(RELEASEARM5) $(RELEASEARM6) $(RELEASEARM7) $(RELEASEARM8) else -release: test fzf/$(BINARY32) fzf/$(BINARY64) - -cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) - cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \ - rm -f $(RELEASE32) $(RELEASE64) +release: fzf/$(BINARY32) fzf/$(BINARY64) + cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) + cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) + cd fzf && rm -f $(RELEASE32) $(RELEASE64) endif +release-all: clean test + GOOS=darwin make release + GOOS=linux make release + GOOS=freebsd make release + GOOS=openbsd make release + GOOS=windows make release + $(SRCDIR): mkdir -p $(shell dirname $(SRCDIR)) ln -s $(ROOTDIR) $(SRCDIR) @@ -60,7 +82,7 @@ android-build: $(SRCDIR) rm -f $(RELEASEARM7) test: deps - SHELL=/bin/sh GOOS=$(GOOS) go test -v -tags "$(TAGS)" ./... + SHELL=/bin/sh GOOS= go test -v -tags "$(TAGS)" ./... install: $(BINDIR)/fzf @@ -71,10 +93,23 @@ clean: cd fzf && rm -f fzf-* fzf/$(BINARY32): deps - cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY32) + cd fzf && GOARCH=386 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY32) fzf/$(BINARY64): deps - cd fzf && go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY64) + cd fzf && GOARCH=amd64 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY64) + +# https://github.com/golang/go/wiki/GoArm +fzf/$(BINARYARM5): deps + cd fzf && GOARCH=arm GOARM=5 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM5) + +fzf/$(BINARYARM6): deps + cd fzf && GOARCH=arm GOARM=6 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM6) + +fzf/$(BINARYARM7): deps + cd fzf && GOARCH=arm GOARM=7 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM7) + +fzf/$(BINARYARM8): deps + cd fzf && GOARCH=arm64 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM8) $(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR) cp -f fzf/$(BINARY) $(BINDIR) diff --git a/src/README.md b/src/README.md index 2d065a9..ec88ca2 100644 --- a/src/README.md +++ b/src/README.md @@ -59,20 +59,31 @@ Unit tests can be run with `make test`. Integration tests are written in Ruby script that should be run on tmux. ```sh +cd src + # Unit tests make test +# Integration tests +ruby ../test/test_go.rb + +# Build binary for the platform +make + # Install the executable to ../bin directory make install -# Integration tests -ruby ../test/test_go.rb +# Make release archives +make release + +# Make release archives for all supported platforms +make release-all ``` Third-party libraries used -------------------------- -- [ncurses][ncurses] +- ~[ncurses][ncurses]~ - [mattn/go-runewidth](https://github.com/mattn/go-runewidth) - Licensed under [MIT](http://mattn.mit-license.org) - [mattn/go-shellwords](https://github.com/mattn/go-shellwords) diff --git a/src/constants.go b/src/constants.go index 0faa7da..082909b 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.16.1" + version = "0.16.2-alpha" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond diff --git a/src/terminal.go b/src/terminal.go index 9e30f30..7fcef60 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -305,8 +305,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { return util.Max(maxHeight, minHeight) } renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, maxHeightFunc) - } else { + } else if tui.HasFullscreenRenderer() { renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) + } else { + renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, + func(h int) int { return h }) } wordRubout := "[^[:alnum:]][[:alnum:]]" wordNext := "[[:alnum:]][^[:alnum:]]|(.$)" diff --git a/src/tui/dummy.go b/src/tui/dummy.go new file mode 100644 index 0000000..01179c8 --- /dev/null +++ b/src/tui/dummy.go @@ -0,0 +1,45 @@ +// +build !ncurses +// +build !tcell +// +build !windows + +package tui + +type Attr int + +func HasFullscreenRenderer() bool { + return false +} + +func (a Attr) Merge(b Attr) Attr { + return a | b +} + +const ( + AttrRegular Attr = Attr(0) + Bold = Attr(1) + Dim = Attr(1 << 1) + Italic = Attr(1 << 2) + Underline = Attr(1 << 3) + Blink = Attr(1 << 4) + Blink2 = Attr(1 << 5) + Reverse = Attr(1 << 6) +) + +func (r *FullscreenRenderer) Init() {} +func (r *FullscreenRenderer) Pause() {} +func (r *FullscreenRenderer) Clear() {} +func (r *FullscreenRenderer) Refresh() {} +func (r *FullscreenRenderer) Close() {} + +func (r *FullscreenRenderer) Resume() bool { return false } +func (r *FullscreenRenderer) DoesAutoWrap() bool { return false } +func (r *FullscreenRenderer) IsOptimized() bool { return false } +func (r *FullscreenRenderer) GetChar() Event { return Event{} } +func (r *FullscreenRenderer) MaxX() int { return 0 } +func (r *FullscreenRenderer) MaxY() int { return 0 } + +func (r *FullscreenRenderer) RefreshWindows(windows []Window) {} + +func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { + return nil +} diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 0978ea8..2706c91 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -1,3 +1,4 @@ +// +build ncurses // +build !windows // +build !tcell @@ -32,6 +33,10 @@ import ( "unicode/utf8" ) +func HasFullscreenRenderer() bool { + return true +} + type Attr C.uint type CursesWindow struct { diff --git a/src/tui/tcell.go b/src/tui/tcell.go index c898a38..4836bf3 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -15,6 +15,10 @@ import ( "github.com/junegunn/go-runewidth" ) +func HasFullscreenRenderer() bool { + return true +} + func (p ColorPair) style() tcell.Style { style := tcell.StyleDefault return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg())) From cbb5134874ec0bf2a7b6a701f4e1a844f5303067 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Jan 2017 14:40:30 +0900 Subject: [PATCH 392/658] [vim] Use 24-bit colors if termguicolors is set --- plugin/fzf.vim | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 4064a55..75597ba 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -167,9 +167,12 @@ function! s:common_sink(action, lines) abort endfunction function! s:get_color(attr, ...) + let gui = has('termguicolors') && &termguicolors + let fam = gui ? 'gui' : 'cterm' + let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$' for group in a:000 - let code = synIDattr(synIDtrans(hlID(group)), a:attr, 'cterm') - if code =~ '^[0-9]\+$' + let code = synIDattr(synIDtrans(hlID(group)), a:attr, fam) + if code =~? pat return code endif endfor From e5017c0431b1dbecff8ba8a170f7bb74f1760108 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Jan 2017 17:41:47 +0900 Subject: [PATCH 393/658] Remove unnecesasry test case --- test/test_go.rb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index a352320..6f2deb1 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1095,12 +1095,6 @@ class TestGoFZF < TestBase assert_equal 1, $?.exitstatus end - def test_invalid_term - lines = `TERM=xxx #{FZF} 2>&1` - assert_equal 2, $?.exitstatus - assert lines.include?('Invalid $TERM: xxx') || lines.include?('terminal entry not found') - end - def test_invalid_option lines = `#{FZF} --foobar 2>&1` assert_equal 2, $?.exitstatus From 71dec3dc5efde5296fdc5a099573d23d008f3826 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Jan 2017 17:43:27 +0900 Subject: [PATCH 394/658] Fix bug where screen is not properly cleared on toggle-preview --- src/terminal.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/terminal.go b/src/terminal.go index 7fcef60..0786b95 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1360,6 +1360,7 @@ func (t *Terminal) Loop() { case actTogglePreview: if t.hasPreviewWindow() { t.previewer.enabled = !t.previewer.enabled + t.tui.Clear() t.resizeWindows() cnt := t.merger.Length() if t.previewer.enabled && cnt > 0 && cnt > t.cy { From c4baa6a10c5bc7d0b2d530fdb3b1bfeeb262974c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Jan 2017 18:33:03 +0900 Subject: [PATCH 395/658] Update man page: 24-bit color --- man/man1/fzf.1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 098ae6a..ff5be5e 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -209,8 +209,7 @@ Number of spaces for a tab character (default: 8) Color configuration. The name of the base color scheme is followed by custom color mappings. Ansi color code of -1 denotes terminal default foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR -format, but the support for 24-bit colors is experimental and only works when -\fB--height\fR option is used. +format. .RS e.g. \fBfzf --color=bg+:24\fR From 4d928001b85ddc864ac9f6cb4afd5c8a52b99fe5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Jan 2017 18:33:30 +0900 Subject: [PATCH 396/658] Update release script to upload assets in parallel --- src/update_assets.rb | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/update_assets.rb b/src/update_assets.rb index 2fd9edc..b4aaba5 100755 --- a/src/update_assets.rb +++ b/src/update_assets.rb @@ -23,21 +23,23 @@ end # List assets assets = Hash[rel['assets'].map { |a| a.values_at *%w[name id] }] -files.select { |f| File.exists? f }.each do |file| - name = File.basename file +files.select { |f| File.exists? f }.map do |file| + Thread.new do + name = File.basename file - if asset_id = assets[name] - puts "#{name} found. Deleting asset id #{asset_id}." - RestClient.delete "#{base}/assets/#{asset_id}", - :authorization => "token #{token}" - else - puts "#{name} not found" + if asset_id = assets[name] + puts "#{name} found. Deleting asset id #{asset_id}." + RestClient.delete "#{base}/assets/#{asset_id}", + :authorization => "token #{token}" + else + puts "#{name} not found" + end + + puts "Uploading #{name}" + RestClient.post( + "#{base.sub 'api', 'uploads'}/#{rel['id']}/assets?name=#{name}", + File.read(file), + :authorization => "token #{token}", + :content_type => "application/octet-stream") end - - puts "Uploading #{name}" - RestClient.post( - "#{base.sub 'api', 'uploads'}/#{rel['id']}/assets?name=#{name}", - File.read(file), - :authorization => "token #{token}", - :content_type => "application/octet-stream") -end +end.each(&:join) From bb26f32ac7353e8fe0e9624326beda082faa5374 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Jan 2017 18:51:04 +0900 Subject: [PATCH 397/658] Allow build on OpenBSD/FreeBSD/Android Close #497 --- src/Makefile | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Makefile b/src/Makefile index e702548..d1d0c14 100644 --- a/src/Makefile +++ b/src/Makefile @@ -28,11 +28,22 @@ RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7 RELEASEARM8 := fzf-$(VERSION)-$(GOOS)_arm8 export GOPATH +# https://en.wikipedia.org/wiki/Uname UNAME_M := $(shell uname -m) ifeq ($(UNAME_M),x86_64) BINARY := $(BINARY64) +else ifeq ($(UNAME_M),amd64) + BINARY := $(BINARY64) else ifeq ($(UNAME_M),i686) BINARY := $(BINARY32) +else ifeq ($(UNAME_M),i386) + BINARY := $(BINARY32) +else ifeq ($(UNAME_M),armv5l) + BINARY := $(BINARYARM5) +else ifeq ($(UNAME_M),armv6l) + BINARY := $(BINARYARM6) +else ifeq ($(UNAME_M),armv7l) + BINARY := $(BINARYARM7) else $(error "Build on $(UNAME_M) is not supported, yet.") endif From e1291aa6d2c49e563cf40b0bce2e34d30931cbb3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 23 Jan 2017 12:10:43 +0900 Subject: [PATCH 398/658] Fix make deps to see the right git dir --- src/deps | 1 + 1 file changed, 1 insertion(+) diff --git a/src/deps b/src/deps index f384dcf..1d8cab7 100755 --- a/src/deps +++ b/src/deps @@ -7,6 +7,7 @@ fi reset() ( cd "$GOPATH/src/$1" + export GIT_DIR="$(pwd)/.git" [ "$(git rev-parse HEAD)" = "$2" ] || (git fetch && git reset --hard "$2") ) From 12a43b5e62ca71eb25368b926d032c2666b43e4d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 23 Jan 2017 12:15:31 +0900 Subject: [PATCH 399/658] Disable mouse if failed to query cursor position --- src/tui/light.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tui/light.go b/src/tui/light.go index bc71ee5..7bc2ec2 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -191,7 +191,8 @@ func (r *LightRenderer) Init() { if r.fullscreen { r.smcup() } else { - _, x := r.findOffset() + y, x := r.findOffset() + r.mouse = r.mouse && y >= 0 if x > 0 { r.upOneLine = true r.makeSpace() From deccdb1ec5079d490bd390c14b3f9a25c8d574db Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 23 Jan 2017 12:47:55 +0900 Subject: [PATCH 400/658] Cursor postition response can be preceded by user key strokes --- src/tui/light.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index 7bc2ec2..cfccc4b 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "regexp" "strconv" "strings" "syscall" @@ -20,10 +21,13 @@ const ( defaultHeight = 24 escPollInterval = 5 + offsetPollTries = 10 ) const consoleDevice string = "/dev/tty" +var offsetRegexp *regexp.Regexp = regexp.MustCompile("\x1b\\[([0-9]+);([0-9]+)R") + func openTtyIn() *os.File { in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) if err != nil { @@ -133,18 +137,14 @@ func (r *LightRenderer) defaultTheme() *ColorTheme { func (r *LightRenderer) findOffset() (row int, col int) { r.csi("6n") r.flush() - bytes := r.getBytesInternal([]byte{}) - - // ^[[*;*R - if len(bytes) > 5 && bytes[0] == 27 && bytes[1] == 91 && bytes[len(bytes)-1] == 'R' { - nums := strings.Split(string(bytes[2:len(bytes)-1]), ";") - if len(nums) == 2 { - return atoi(nums[0], 0) - 1, atoi(nums[1], 0) - 1 + bytes := []byte{} + for tries := 0; tries < offsetPollTries; tries++ { + bytes = r.getBytesInternal(bytes, tries > 0) + offsets := offsetRegexp.FindSubmatch(bytes) + if len(offsets) > 2 { + return atoi(string(offsets[1]), 0) - 1, atoi(string(offsets[2]), 0) - 1 } - return -1, -1 } - - // No idea return -1, -1 } @@ -274,18 +274,18 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) { } func (r *LightRenderer) getBytes() []byte { - return r.getBytesInternal(r.buffer) + return r.getBytesInternal(r.buffer, false) } -func (r *LightRenderer) getBytesInternal(buffer []byte) []byte { - c, ok := r.getch(false) - if !ok { +func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte { + c, ok := r.getch(nonblock) + if !nonblock && !ok { r.Close() errorExit("Failed to read " + consoleDevice) } retries := 0 - if c == ESC { + if c == ESC || nonblock { retries = r.escDelay / escPollInterval } buffer = append(buffer, byte(c)) From a5ad8fd3bd66278a1a17629a1f0d69a9461b0b80 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 23 Jan 2017 12:51:31 +0900 Subject: [PATCH 401/658] Minor refactoring --- src/tui/light.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index cfccc4b..6fcb4d5 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -20,6 +20,7 @@ const ( defaultWidth = 80 defaultHeight = 24 + defaultEscDelay = 100 escPollInterval = 5 offsetPollTries = 10 ) @@ -164,15 +165,7 @@ func atoi(s string, defaultValue int) int { } func (r *LightRenderer) Init() { - delay := 100 - delayEnv := os.Getenv("ESCDELAY") - if len(delayEnv) > 0 { - num, err := strconv.Atoi(delayEnv) - if err == nil && num >= 0 { - delay = num - } - } - r.escDelay = delay + r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay) fd := r.fd() origState, err := terminal.GetState(fd) From a3244c4892fdf637696f260d39f15ed4c20518e1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 23 Jan 2017 22:07:18 +0900 Subject: [PATCH 402/658] Delete every line below the cursor --- src/tui/light.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index 6fcb4d5..b141368 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -184,6 +184,7 @@ func (r *LightRenderer) Init() { if r.fullscreen { r.smcup() } else { + r.csi("J") y, x := r.findOffset() r.mouse = r.mouse && y >= 0 if x > 0 { @@ -210,7 +211,6 @@ func (r *LightRenderer) Init() { func (r *LightRenderer) makeSpace() { r.stderr("\n") r.csi("G") - r.csi("K") } func (r *LightRenderer) move(y int, x int) { @@ -569,10 +569,10 @@ func (r *LightRenderer) Close() { r.rmcup() } else { r.origin() - r.csi("J") if r.upOneLine { r.csi("A") } + r.csi("J") } if r.mouse { r.csi("?1000l") From 6a75e30941af50a7197ef5e96c07a8605aec9a53 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 24 Jan 2017 00:23:16 +0900 Subject: [PATCH 403/658] Allow invisible preview window (--preview-window 0) Close #820 --- CHANGELOG.md | 2 ++ man/man1/fzf.1 | 3 +++ src/options.go | 2 +- src/terminal.go | 32 +++++++++++++++++++------------- test/test_go.rb | 13 +++++++++++++ 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6df93e..db5182b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ CHANGELOG - Added support for composite actions in `--bind`. Multiple actions can be chained using `+` separator. - e.g. `fzf --bind 'ctrl-y:execute(echo -n {} | pbcopy)+abort'` +- `--preview-window` with size 0 is allowed. This is used to make fzf execute + preview command in the background without displaying the result. 0.16.1 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index ff5be5e..3ac07a4 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -278,6 +278,9 @@ Determine the layout of the preview window. If the argument ends with \fBtoggle-preview\fR action is triggered. Long lines are truncated by default. Line wrap can be enabled with \fB:wrap\fR flag. +If size is given as 0, preview window will not be visible, but fzf will still +execute the command in the background. + .RS .B POSITION: (default: right) \fBup diff --git a/src/options.go b/src/options.go index eb21ee7..2e96c2c 100644 --- a/src/options.go +++ b/src/options.go @@ -823,7 +823,7 @@ func parsePreviewWindow(opts *previewOpts, input string) { opts.wrap = false tokens := strings.Split(input, ":") - sizeRegex := regexp.MustCompile("^[1-9][0-9]*%?$") + sizeRegex := regexp.MustCompile("^[0-9]+%?$") for _, token := range tokens { switch token { case "hidden": diff --git a/src/terminal.go b/src/terminal.go index 0786b95..d06f752 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -509,9 +509,11 @@ func (t *Terminal) resizeWindows() { } } } + + previewVisible := t.isPreviewEnabled() && t.preview.size.size > 0 minAreaWidth := minWidth minAreaHeight := minHeight - if t.isPreviewEnabled() { + if previewVisible { switch t.preview.position { case posUp, posDown: minAreaHeight *= 2 @@ -531,7 +533,7 @@ func (t *Terminal) resizeWindows() { width := screenWidth - marginInt[1] - marginInt[3] height := screenHeight - marginInt[0] - marginInt[2] - if t.isPreviewEnabled() { + if previewVisible { createPreviewWindow := func(y int, x int, w int, h int) { t.bwindow = t.tui.NewWindow(y, x, w, h, true) pwidth := w - 4 @@ -889,7 +891,7 @@ func numLinesMax(str string, max int) int { } func (t *Terminal) printPreview() { - if !t.isPreviewEnabled() { + if !t.hasPreviewWindow() { return } t.pwindow.Erase() @@ -974,7 +976,7 @@ func (t *Terminal) printAll() { func (t *Terminal) refresh() { if !t.suppress { - if t.isPreviewEnabled() { + if t.hasPreviewWindow() { t.tui.RefreshWindows([]tui.Window{t.bwindow, t.pwindow, t.window}) } else { t.tui.RefreshWindows([]tui.Window{t.window}) @@ -1107,12 +1109,16 @@ func (t *Terminal) executeCommand(template string, items []*Item) { t.refresh() } -func (t *Terminal) hasPreviewWindow() bool { +func (t *Terminal) hasPreviewer() bool { return t.previewBox != nil } func (t *Terminal) isPreviewEnabled() bool { - return t.previewBox != nil && t.previewer.enabled + return t.hasPreviewer() && t.previewer.enabled +} + +func (t *Terminal) hasPreviewWindow() bool { + return t.pwindow != nil && t.isPreviewEnabled() } func (t *Terminal) currentItem() *Item { @@ -1174,7 +1180,7 @@ func (t *Terminal) Loop() { }() } - if t.hasPreviewWindow() { + if t.hasPreviewer() { go func() { for { var request *Item @@ -1358,7 +1364,7 @@ func (t *Terminal) Loop() { t.mutex.Unlock() return false case actTogglePreview: - if t.hasPreviewWindow() { + if t.hasPreviewer() { t.previewer.enabled = !t.previewer.enabled t.tui.Clear() t.resizeWindows() @@ -1374,19 +1380,19 @@ func (t *Terminal) Loop() { t.mutex.Unlock() return false case actPreviewUp: - if t.isPreviewEnabled() { + if t.hasPreviewWindow() { scrollPreview(-1) } case actPreviewDown: - if t.isPreviewEnabled() { + if t.hasPreviewWindow() { scrollPreview(1) } case actPreviewPageUp: - if t.isPreviewEnabled() { + if t.hasPreviewWindow() { scrollPreview(-t.pwindow.Height()) } case actPreviewPageDown: - if t.isPreviewEnabled() { + if t.hasPreviewWindow() { scrollPreview(t.pwindow.Height()) } case actBeginningOfLine: @@ -1563,7 +1569,7 @@ func (t *Terminal) Loop() { } t.vmove(me.S) req(reqList) - } else if t.isPreviewEnabled() && t.pwindow.Enclose(my, mx) { + } else if t.hasPreviewWindow() && t.pwindow.Enclose(my, mx) { scrollPreview(-me.S) } } else if t.window.Enclose(my, mx) { diff --git a/test/test_go.rb b/test/test_go.rb index 6f2deb1..cdd96d1 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1224,6 +1224,19 @@ class TestGoFZF < TestBase tmux.send_keys '?' tmux.until { |lines| lines[-1] == '> 555' } end + + def test_preview_size_0 + File.unlink tempname rescue nil + tmux.send_keys %[seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0], :Enter + tmux.until { |lines| lines.item_count == 100 && lines[1] == ' 100/100' && lines[2] == '> 1' } + tmux.until { |_| %w[1] == File.readlines(tempname).map(&:chomp) } + tmux.send_keys :Down + tmux.until { |lines| lines[3] == '> 2' } + tmux.until { |_| %w[1 2] == File.readlines(tempname).map(&:chomp) } + tmux.send_keys :Down + tmux.until { |lines| lines[4] == '> 3' } + tmux.until { |_| %w[1 2 3] == File.readlines(tempname).map(&:chomp) } + end end module TestShell From f2ce233a6dfe5d8c30a6abe527db9187790f4245 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 24 Jan 2017 00:37:47 +0900 Subject: [PATCH 404/658] 0.16.2 --- CHANGELOG.md | 2 ++ install | 2 +- src/constants.go | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db5182b..e435822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,13 @@ CHANGELOG ------ - Dropped ncurses dependency - Binaries for freebsd, openbsd, arm5, arm6, arm7, and arm8 +- Official 24-bit color support - Added support for composite actions in `--bind`. Multiple actions can be chained using `+` separator. - e.g. `fzf --bind 'ctrl-y:execute(echo -n {} | pbcopy)+abort'` - `--preview-window` with size 0 is allowed. This is used to make fzf execute preview command in the background without displaying the result. +- Minor bug fixes and improvements 0.16.1 ------ diff --git a/install b/install index 0ff0310..01c14ab 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.16.2-alpha +version=0.16.2 auto_completion= key_bindings= update_config=2 diff --git a/src/constants.go b/src/constants.go index 082909b..5013e03 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.16.2-alpha" + version = "0.16.2" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 8731d756072a1180feb0b88a4c1ef0637536c27d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 25 Jan 2017 02:39:49 +0900 Subject: [PATCH 405/658] Recalculate the width of trimmed line Close #821 --- src/terminal.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/terminal.go b/src/terminal.go index d06f752..081f715 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -847,6 +847,7 @@ func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.Colo offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth)) } } + displayWidth = t.displayWidthWithLimit(text, 0, displayWidth) } var index int32 From dbddee9de9c7db52f6c4cbbadabc05790f6e31de Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 25 Jan 2017 10:21:14 +0900 Subject: [PATCH 406/658] [fish] Add toggle-sort back to CTRL-R (#759) --- shell/key-bindings.fish | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index d20a850..0b219f2 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -45,7 +45,7 @@ function fzf_key_bindings function fzf-history-widget -d "Show command history" set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% begin - set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --tiebreak=index $FZF_CTRL_R_OPTS +m" + set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m" history | eval (__fzfcmd) -q '(commandline)' | read -l result and commandline -- $result end From da2c28d5c225d7ba8647dbf50825a83d475ed83c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 26 Jan 2017 11:41:20 +0900 Subject: [PATCH 407/658] Add --read0 and --print0 to --help output Close #822 --- man/man1/fzf.1 | 4 ++-- src/options.go | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 3ac07a4..f6bab4c 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -323,10 +323,10 @@ e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR .RE .TP .B "--read0" -Read input delimited by ASCII NUL character instead of newline character +Read input delimited by ASCII NUL characters instead of newline characters .TP .B "--print0" -Print output delimited by ASCII NUL character instead of newline character +Print output delimited by ASCII NUL characters instead of newline characters .TP .B "--sync" Synchronous search for multi-staged filtering. If specified, fzf will launch diff --git a/src/options.go b/src/options.go index 2e96c2c..9d8bb89 100644 --- a/src/options.go +++ b/src/options.go @@ -82,6 +82,8 @@ const usage = `usage: fzf [options] -f, --filter=STR Filter mode. Do not start interactive finder. --print-query Print query as the first line --expect=KEYS Comma-separated list of keys to complete fzf + --read0 Read input delimited by ASCII NUL characters + --print0 Print output delimited by ASCII NUL characters --sync Synchronous search for multi-staged filtering Environment variables From 95c77bfb98eff07c63599bb02a58f73d6c143e62 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 26 Jan 2017 11:54:08 +0900 Subject: [PATCH 408/658] Use --bind instead of --toggle-sort Related #822 --- man/man1/fzf.1 | 2 +- shell/key-bindings.bash | 2 +- shell/key-bindings.fish | 2 +- shell/key-bindings.zsh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index f6bab4c..9a374d5 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -487,7 +487,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR) \fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR) \fBtoggle-preview\fR - \fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR) + \fBtoggle-sort\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR \fBunix-line-discard\fR \fIctrl-u\fR \fBunix-word-rubout\fR \fIctrl-w\fR diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index cbc1c48..69dd0ce 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -56,7 +56,7 @@ __fzf_history__() ( shopt -u nocaseglob nocasematch line=$( HISTTIMEFORMAT= history | - FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m" $(__fzfcmd) | + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" $(__fzfcmd) | command grep '^ *[0-9]') && if [[ $- =~ H ]]; then sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index 0b219f2..ce2d4e0 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -45,7 +45,7 @@ function fzf_key_bindings function fzf-history-widget -d "Show command history" set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% begin - set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m" + set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" history | eval (__fzfcmd) -q '(commandline)' | read -l result and commandline -- $result end diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 7b8227e..abf7559 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -55,7 +55,7 @@ fzf-history-widget() { local selected num setopt localoptions noglobsubst pipefail 2> /dev/null selected=( $(fc -l 1 | - FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) ) + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) ) local ret=$? if [ -n "$selected" ]; then num=$selected[1] From ed57dcb924112192636260fb31d0db54352c517a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 27 Jan 2017 16:38:42 +0900 Subject: [PATCH 409/658] Extend placeholder expression for multiple selections Close #788 --- CHANGELOG.md | 8 ++++ man/man1/fzf.1 | 14 +++++-- src/terminal.go | 109 ++++++++++++++++++++++++++++++++---------------- test/test_go.rb | 52 ++++++++++++++++++++++- 4 files changed, 141 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e435822..4dd6e9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +0.16.3 +------ +- Fixed a bug where fzf incorrectly display the lines when straddling tab + characters are trimmed +- Placeholder expression used in `--preview` and `execute` action can + optionally take `+` flag to be used with multiple selections + - e.g. `git log --oneline | fzf --multi --preview 'git show {+1}'` + 0.16.2 ------ - Dropped ncurses dependency diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 9a374d5..e4321ba 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -262,13 +262,21 @@ Execute the given command for the current line and display the result on the preview window. \fB{}\fR in the command is the placeholder that is replaced to the single-quoted string of the current line. To transform the replacement string, specify field index expressions between the braces (See \fBFIELD INDEX -EXPRESSION\fR for the details). Also, \fB{q}\fR is replaced to the current -query string. +EXPRESSION\fR for the details). .RS e.g. \fBfzf --preview="head -$LINES {}"\fR \fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR +A placeholder expression starting with \fB+\fR flag will be replaced to the +space-separated list of the selected lines (or the current line if no selection +was made) individually quoted. + +e.g. \fBfzf --multi --preview="head -10 {+}"\fR + \fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR + +Also, \fB{q}\fR is replaced to the current query string. + Note that you can escape a placeholder pattern by prepending a backslash. .RE .TP @@ -461,7 +469,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBdown\fR \fIctrl-j ctrl-n down\fR \fBend-of-line\fR \fIctrl-e end\fR \fBexecute(...)\fR (see below for the details) - \fBexecute-multi(...)\fR (see below for the details) + \fRexecute-multi(...)\fR (deprecated in favor of \fB{+}\fR expression) \fBforward-char\fR \fIctrl-f right\fR \fBforward-word\fR \fIalt-f shift-right\fR \fBignore\fR diff --git a/src/terminal.go b/src/terminal.go index 081f715..43d21d8 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -24,7 +24,7 @@ import ( var placeholder *regexp.Regexp func init() { - placeholder = regexp.MustCompile("\\\\?(?:{[0-9,-.]*}|{q})") + placeholder = regexp.MustCompile("\\\\?(?:{\\+?[0-9,-.]*}|{q})") } type jumpMode int @@ -436,9 +436,9 @@ func (t *Terminal) output() bool { } found := len(t.selected) > 0 if !found { - cnt := t.merger.Length() - if cnt > 0 && cnt > t.cy { - t.printer(t.current()) + current := t.currentItem() + if current != nil { + t.printer(current.AsString(t.ansi)) found = true } } else { @@ -1044,7 +1044,27 @@ func quoteEntry(entry string) string { return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" } -func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, query string, items []*Item) string { +func hasPlusFlag(template string) bool { + for _, match := range placeholder.FindAllString(template, -1) { + if match[0] == '\\' { + continue + } + if match[1] == '+' { + return true + } + } + return false +} + +func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, forcePlus bool, query string, allItems []*Item) string { + current := allItems[:1] + selected := allItems[1:] + if current[0] == nil { + current = []*Item{} + } + if selected[0] == nil { + selected = []*Item{} + } return placeholder.ReplaceAllStringFunc(template, func(match string) string { // Escaped pattern if match[0] == '\\' { @@ -1056,6 +1076,16 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, qu return quoteEntry(query) } + plusFlag := forcePlus + if match[1] == '+' { + match = "{" + match[2:] + plusFlag = true + } + items := current + if plusFlag { + items = selected + } + replacements := make([]string, len(items)) if match == "{}" { @@ -1096,8 +1126,12 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, qu }) } -func (t *Terminal) executeCommand(template string, items []*Item) { - command := replacePlaceholder(template, t.ansi, t.delimiter, string(t.input), items) +func (t *Terminal) executeCommand(template string, forcePlus bool) { + valid, list := t.buildPlusList(template, forcePlus) + if !valid { + return + } + command := replacePlaceholder(template, t.ansi, t.delimiter, forcePlus, string(t.input), list) cmd := util.ExecCommand(command) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -1123,11 +1157,24 @@ func (t *Terminal) hasPreviewWindow() bool { } func (t *Terminal) currentItem() *Item { - return t.merger.Get(t.cy).item + cnt := t.merger.Length() + if cnt > 0 && cnt > t.cy { + return t.merger.Get(t.cy).item + } + return nil } -func (t *Terminal) current() string { - return t.currentItem().AsString(t.ansi) +func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) { + current := t.currentItem() + if !forcePlus && !hasPlusFlag(template) || len(t.selected) == 0 { + return current != nil, []*Item{current, current} + } + sels := make([]*Item, len(t.selected)+1) + sels[0] = current + for i, sel := range t.sortSelected() { + sels[i+1] = sel.item + } + return true, sels } // Loop is called to start Terminal I/O @@ -1184,19 +1231,20 @@ func (t *Terminal) Loop() { if t.hasPreviewer() { go func() { for { - var request *Item + var request []*Item t.previewBox.Wait(func(events *util.Events) { for req, value := range *events { switch req { case reqPreviewEnqueue: - request = value.(*Item) + request = value.([]*Item) } } events.Clear() }) - if request != nil { + // We don't display preview window if no match + if request[0] != nil { command := replacePlaceholder(t.preview.command, - t.ansi, t.delimiter, string(t.input), []*Item{request}) + t.ansi, t.delimiter, false, string(t.input), request) cmd := util.ExecCommand(command) out, _ := cmd.CombinedOutput() t.reqBox.Set(reqPreviewDisplay, string(out)) @@ -1232,17 +1280,12 @@ func (t *Terminal) Loop() { t.printInfo() case reqList: t.printList() - cnt := t.merger.Length() - var currentFocus *Item - if cnt > 0 && cnt > t.cy { - currentFocus = t.currentItem() - } else { - currentFocus = nil - } + currentFocus := t.currentItem() if currentFocus != focused { focused = currentFocus if t.isPreviewEnabled() { - t.previewBox.Set(reqPreviewEnqueue, focused) + _, list := t.buildPlusList(t.preview.command, false) + t.previewBox.Set(reqPreviewEnqueue, list) } } case reqJump: @@ -1348,19 +1391,9 @@ func (t *Terminal) Loop() { switch a.t { case actIgnore: case actExecute: - if t.cy >= 0 && t.cy < t.merger.Length() { - t.executeCommand(a.a, []*Item{t.currentItem()}) - } + t.executeCommand(a.a, false) case actExecuteMulti: - if len(t.selected) > 0 { - sels := make([]*Item, len(t.selected)) - for i, sel := range t.sortSelected() { - sels[i] = sel.item - } - t.executeCommand(a.a, sels) - } else { - return doAction(action{t: actExecute, a: a.a}, mapkey) - } + t.executeCommand(a.a, true) case actInvalid: t.mutex.Unlock() return false @@ -1369,9 +1402,11 @@ func (t *Terminal) Loop() { t.previewer.enabled = !t.previewer.enabled t.tui.Clear() t.resizeWindows() - cnt := t.merger.Length() - if t.previewer.enabled && cnt > 0 && cnt > t.cy { - t.previewBox.Set(reqPreviewEnqueue, t.currentItem()) + if t.previewer.enabled { + valid, list := t.buildPlusList(t.preview.command, false) + if valid { + t.previewBox.Set(reqPreviewEnqueue, list) + } } req(reqList, reqInfo, reqHeader) } diff --git a/test/test_go.rb b/test/test_go.rb index cdd96d1..f730f25 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -879,7 +879,7 @@ class TestGoFZF < TestBase def test_execute_multi output = '/tmp/fzf-test-execute-multi' - opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{} >> #{output}; sync)\\"] + opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{+} >> #{output}; sync)\\"] writelines tempname, %w[foo'bar foo"bar foo$bar foobar] tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter tmux.until { |lines| lines[-2].include? '4/4' } @@ -902,6 +902,43 @@ class TestGoFZF < TestBase File.unlink output rescue nil end + def test_execute_plus_flag + output = tempname + ".tmp" + File.unlink output rescue nil + writelines tempname, ["foo bar", "123 456"] + + tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter + + execute = lambda do + tmux.send_keys 'x', 'y' + tmux.until { |lines| lines[-2].include? '0/2' } + tmux.send_keys :BSpace + tmux.until { |lines| lines[-2].include? '2/2' } + end + + tmux.until { |lines| lines[-2].include? '2/2' } + execute.call + + tmux.send_keys :Up + tmux.send_keys :Tab + execute.call + + tmux.send_keys :Tab + execute.call + + tmux.send_keys :Enter + tmux.prepare + readonce + + assert_equal [ + %[foo bar/foo bar/bar/bar], + %[123 456/foo bar/456/bar], + %[123 456 foo bar/foo bar/456 bar/bar] + ], File.readlines(output).map(&:chomp) + rescue + File.unlink output rescue nil + end + def test_execute_shell # Custom script to use as $SHELL output = tempname + '.out' @@ -1198,7 +1235,7 @@ class TestGoFZF < TestBase end def test_preview - tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} --preview 'sleep 0.2; echo {{}-{}}' --bind ?:toggle-preview], :Enter + tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview], :Enter tmux.until { |lines| lines[1].include?(' {1-1}') } tmux.send_keys :Up tmux.until { |lines| lines[1].include?(' {-}') } @@ -1212,6 +1249,17 @@ class TestGoFZF < TestBase tmux.until { |lines| lines[-2].start_with? ' 28/1000' } tmux.send_keys 'foobar' tmux.until { |lines| !lines[1].include?('{') } + tmux.send_keys 'C-u' + tmux.until { |lines| lines.match_count == 1000 } + tmux.until { |lines| lines[1].include?(' {1-1}') } + tmux.send_keys :BTab + tmux.until { |lines| lines[1].include?(' {-1}') } + tmux.send_keys :BTab + tmux.until { |lines| lines[1].include?(' {3-1 }') } + tmux.send_keys :BTab + tmux.until { |lines| lines[1].include?(' {4-1 3}') } + tmux.send_keys :BTab + tmux.until { |lines| lines[1].include?(' {5-1 3 4}') } end def test_preview_hidden From 421b9b271ac263057c81eb58f98d5a96c04ce941 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 27 Jan 2017 17:46:56 +0900 Subject: [PATCH 410/658] Add execute-silent action Close #823 --- CHANGELOG.md | 4 +++ man/man1/fzf.1 | 9 ++++--- src/options.go | 58 +++++++++++++++++++++++++------------------- src/terminal.go | 31 +++++++++++++---------- src/terminal_test.go | 46 +++++++++++++++++++++++++---------- 5 files changed, 93 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd6e9c..f8a5bcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ CHANGELOG - Placeholder expression used in `--preview` and `execute` action can optionally take `+` flag to be used with multiple selections - e.g. `git log --oneline | fzf --multi --preview 'git show {+1}'` +- Added `execute-silent` action for executing a command silently without + switching to the alternate screen. This is useful when the process is + short-lived and you're not interested in its output. + - e.g. `fzf --bind 'ctrl-y:execute!(echo -n {} | pbcopy)'` 0.16.2 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index e4321ba..95d0380 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -469,6 +469,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBdown\fR \fIctrl-j ctrl-n down\fR \fBend-of-line\fR \fIctrl-e end\fR \fBexecute(...)\fR (see below for the details) + \fBexecute-silent(...)\fR (see below for the details) \fRexecute-multi(...)\fR (deprecated in favor of \fB{+}\fR expression) \fBforward-char\fR \fIctrl-f right\fR \fBforward-word\fR \fIalt-f shift-right\fR @@ -538,10 +539,10 @@ the closing character. The catch is that it should be the last one in the comma-separated list of key-action pairs. .RE -\fBexecute-multi(...)\fR is an alternative action that executes the command -with the selected entries when multi-select is enabled (\fB--multi\fR). With -this action, \fB{}\fR is replaced with the quoted strings of the selected -entries separated by spaces. +fzf switches to the alternate screen when executing a command. However, if the +process is expected to complete quickly, and you are not interested in its +output, you might want to use \fBexecute-silent\fR instead, which silently +executes the command without switching. .SH AUTHOR Junegunn Choi (\fIjunegunn.c@gmail.com\fR) diff --git a/src/options.go b/src/options.go index 9d8bb89..d29f8bf 100644 --- a/src/options.go +++ b/src/options.go @@ -581,18 +581,25 @@ const ( escapedPlus = 2 ) +func init() { + // Backreferences are not supported. + // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') + executeRegexp = regexp.MustCompile( + "(?si):(execute(?:-multi|-silent)?):.+|:(execute(?:-multi|-silent)?)(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") +} + func parseKeymap(keymap map[int][]action, str string) { - if executeRegexp == nil { - // Backreferences are not supported. - // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') - executeRegexp = regexp.MustCompile( - "(?si):execute(-multi)?:.+|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") - } masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { - if src[len(":execute")] == '-' { - return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")" + prefix := ":execute" + if src[len(prefix)] == '-' { + c := src[len(prefix)+1] + if c == 's' || c == 'S' { + prefix += "-silent" + } else { + prefix += "-multi" + } } - return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")" + return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")" }) masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) @@ -728,9 +735,12 @@ func parseKeymap(keymap map[int][]action, str string) { errorExit("unknown action: " + spec) } else { var offset int - if t == actExecuteMulti { + switch t { + case actExecuteSilent: + offset = len("execute-silent") + case actExecuteMulti: offset = len("execute-multi") - } else { + default: offset = len("execute") } if spec[offset] == ':' { @@ -752,23 +762,21 @@ func parseKeymap(keymap map[int][]action, str string) { } func isExecuteAction(str string) actionType { - t := actExecute - if !strings.HasPrefix(str, "execute") || len(str) < len("execute(") { + matches := executeRegexp.FindAllStringSubmatch(":"+str, -1) + if matches == nil || len(matches) != 1 { return actIgnore } - - b := str[len("execute")] - if strings.HasPrefix(str, "execute-multi") { - if len(str) < len("execute-multi(") { - return actIgnore - } - t = actExecuteMulti - b = str[len("execute-multi")] + prefix := matches[0][1] + if len(prefix) == 0 { + prefix = matches[0][2] } - e := str[len(str)-1] - if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' || - b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") { - return t + switch prefix { + case "execute": + return actExecute + case "execute-silent": + return actExecuteSilent + case "execute-multi": + return actExecuteMulti } return actIgnore } diff --git a/src/terminal.go b/src/terminal.go index 43d21d8..ee678f5 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -204,7 +204,8 @@ const ( actPreviousHistory actNextHistory actExecute - actExecuteMulti + actExecuteSilent + actExecuteMulti // Deprecated ) func toActions(types ...actionType) []action { @@ -1126,22 +1127,26 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo }) } -func (t *Terminal) executeCommand(template string, forcePlus bool) { +func (t *Terminal) executeCommand(template string, forcePlus bool, background bool) { valid, list := t.buildPlusList(template, forcePlus) if !valid { return } command := replacePlaceholder(template, t.ansi, t.delimiter, forcePlus, string(t.input), list) cmd := util.ExecCommand(command) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - t.tui.Pause() - cmd.Run() - if t.tui.Resume() { - t.printAll() + if !background { + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + t.tui.Pause() + cmd.Run() + if t.tui.Resume() { + t.printAll() + } + t.refresh() + } else { + cmd.Run() } - t.refresh() } func (t *Terminal) hasPreviewer() bool { @@ -1390,10 +1395,10 @@ func (t *Terminal) Loop() { doAction = func(a action, mapkey int) bool { switch a.t { case actIgnore: - case actExecute: - t.executeCommand(a.a, false) + case actExecute, actExecuteSilent: + t.executeCommand(a.a, false, a.t == actExecuteSilent) case actExecuteMulti: - t.executeCommand(a.a, true) + t.executeCommand(a.a, true, false) case actInvalid: t.mutex.Unlock() return false diff --git a/src/terminal_test.go b/src/terminal_test.go index 5afafaa..41941ee 100644 --- a/src/terminal_test.go +++ b/src/terminal_test.go @@ -14,8 +14,10 @@ func newItem(str string) *Item { } func TestReplacePlaceholder(t *testing.T) { - items1 := []*Item{newItem(" foo'bar \x1b[31mbaz\x1b[m")} + item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m") + items1 := []*Item{item1, item1} items2 := []*Item{ + newItem("foo'bar \x1b[31mbaz\x1b[m"), newItem("foo'bar \x1b[31mbaz\x1b[m"), newItem("FOO'BAR \x1b[31mBAZ\x1b[m")} @@ -27,47 +29,65 @@ func TestReplacePlaceholder(t *testing.T) { } // {}, preserve ansi - result = replacePlaceholder("echo {}", false, Delimiter{}, "query", items1) + result = replacePlaceholder("echo {}", false, Delimiter{}, false, "query", items1) check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'") // {}, strip ansi - result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items1) + result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items1) check("echo ' foo'\\''bar baz'") // {}, with multiple items - result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items2) - check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ'") + result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items2) + check("echo 'foo'\\''bar baz'") // {..}, strip leading whitespaces, preserve ansi - result = replacePlaceholder("echo {..}", false, Delimiter{}, "query", items1) + result = replacePlaceholder("echo {..}", false, Delimiter{}, false, "query", items1) check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'") // {..}, strip leading whitespaces, strip ansi - result = replacePlaceholder("echo {..}", true, Delimiter{}, "query", items1) + result = replacePlaceholder("echo {..}", true, Delimiter{}, false, "query", items1) check("echo 'foo'\\''bar baz'") // {q} - result = replacePlaceholder("echo {} {q}", true, Delimiter{}, "query", items1) + result = replacePlaceholder("echo {} {q}", true, Delimiter{}, false, "query", items1) check("echo ' foo'\\''bar baz' 'query'") // {q}, multiple items - result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, "query 'string'", items2) + result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, false, "query 'string'", items2) check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'") - result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "query", items1) + result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, false, "query 'string'", items2) + check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'") + + result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items1) check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") - result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "query", items2) + result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items2) + check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''") + + result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, false, "query", items2) check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''") + // forcePlus + result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, true, "query", items2) + check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''") + + // No match + result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, nil}) + check("echo /") + + // No match, but with selections + result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, item1}) + check("echo /' foo'\\''bar baz'") + // String delimiter delim := "'" - result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, "query", items1) + result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, false, "query", items1) check("echo ' foo'\\''bar baz'/'foo'/'bar baz'") // Regex delimiter regex := regexp.MustCompile("[oa]+") // foo'bar baz - result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, "query", items1) + result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1) check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'") } From 36dceecd58872777d870c8bf9636bd886365f8e9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 28 Jan 2017 02:54:47 +0900 Subject: [PATCH 411/658] Add support for ctrl-space key Close #825 --- CHANGELOG.md | 1 + man/man1/fzf.1 | 1 + src/options.go | 2 ++ src/tui/light.go | 2 ++ src/tui/ncurses.go | 2 ++ src/tui/tcell.go | 2 ++ src/tui/tui.go | 1 + 7 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8a5bcd..37ede60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ CHANGELOG switching to the alternate screen. This is useful when the process is short-lived and you're not interested in its output. - e.g. `fzf --bind 'ctrl-y:execute!(echo -n {} | pbcopy)'` +- `ctrl-space` is allowed in `--bind` 0.16.2 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 95d0380..f747273 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -426,6 +426,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR .B AVAILABLE KEYS: (SYNONYMS) \fIctrl-[a-z]\fR + \fIctrl-space\fR \fIalt-[a-z]\fR \fIalt-[0-9]\fR \fIf[1-12]\fR diff --git a/src/options.go b/src/options.go index d29f8bf..3fadb4e 100644 --- a/src/options.go +++ b/src/options.go @@ -393,6 +393,8 @@ func parseKeyChords(str string, message string) map[int]string { chord = tui.AltZ + int(' ') case "bspace", "bs": chord = tui.BSpace + case "ctrl-space": + chord = tui.CtrlSpace case "alt-enter", "alt-return": chord = tui.AltEnter case "alt-space": diff --git a/src/tui/light.go b/src/tui/light.go index b141368..90b3bae 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -322,6 +322,8 @@ func (r *LightRenderer) GetChar() Event { return Event{CtrlQ, 0, nil} case 127: return Event{BSpace, 0, nil} + case 0: + return Event{CtrlSpace, 0, nil} case ESC: ev := r.escSequence(&sz) // Second chance diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 2706c91..978b2e7 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -475,6 +475,8 @@ func (r *FullscreenRenderer) GetChar() Event { return escSequence() case 127: return Event{BSpace, 0, nil} + case 0: + return Event{CtrlSpace, 0, nil} } // CTRL-A ~ CTRL-Z if c >= CtrlA && c <= CtrlZ { diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 4836bf3..aa67ae1 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -270,6 +270,8 @@ func (r *FullscreenRenderer) GetChar() Event { return Event{CtrlY, 0, nil} case tcell.KeyCtrlZ: return Event{CtrlZ, 0, nil} + case tcell.KeyCtrlSpace: + return Event{CtrlSpace, 0, nil} case tcell.KeyBackspace, tcell.KeyBackspace2: if alt { return Event{AltBS, 0, nil} diff --git a/src/tui/tui.go b/src/tui/tui.go index c8bd5fb..2508aa6 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -38,6 +38,7 @@ const ( CtrlY CtrlZ ESC + CtrlSpace Invalid Resize From dd156b59fc5f36a339e6bf632a72eda51b939eee Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 30 Jan 2017 01:08:07 +0900 Subject: [PATCH 412/658] Fix display issues with execute action - Move cursor to the top-left corner when starting a command in alternate screen - Fix cursor position when returning to alternate screen when fzf is running in full screen mode --- src/terminal.go | 1 + src/tui/light.go | 1 + 2 files changed, 2 insertions(+) diff --git a/src/terminal.go b/src/terminal.go index ee678f5..134462e 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1141,6 +1141,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo t.tui.Pause() cmd.Run() if t.tui.Resume() { + t.tui.Clear() t.printAll() } t.refresh() diff --git a/src/tui/light.go b/src/tui/light.go index 90b3bae..d5631ec 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -534,6 +534,7 @@ func (r *LightRenderer) Pause() { r.rmcup() } else { r.smcup() + r.csi("H") } r.flush() } From 7763fdf6ba51190b4c374aa663379fdad31e81eb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 30 Jan 2017 01:27:12 +0900 Subject: [PATCH 413/658] Update man pages --- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index bd7c756..b243dbd 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Jan 2017" "fzf 0.16.2" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Jan 2017" "fzf 0.16.3" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index f747273..f7d3a44 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Jan 2017" "fzf 0.16.2" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jan 2017" "fzf 0.16.3" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -541,9 +541,11 @@ comma-separated list of key-action pairs. .RE fzf switches to the alternate screen when executing a command. However, if the -process is expected to complete quickly, and you are not interested in its +command is expected to complete quickly, and you are not interested in its output, you might want to use \fBexecute-silent\fR instead, which silently -executes the command without switching. +executes the command without the switching. Note that fzf will not be +responsible until the command is complete. For asynchronous execution, start +your command as a background process (i.e. appending \fB&\fR). .SH AUTHOR Junegunn Choi (\fIjunegunn.c@gmail.com\fR) From a425e96fb28624b5add9e1536badfb620a150125 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 30 Jan 2017 01:47:30 +0900 Subject: [PATCH 414/658] [vim] g:fzf_prefer_tmux for choosing fzf-tmux over --height https://github.com/junegunn/fzf.vim/issues/296 --- plugin/fzf.vim | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 75597ba..1cd6ff7 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -1,4 +1,4 @@ -" Copyright (c) 2016 Junegunn Choi +" Copyright (c) 2017 Junegunn Choi " " MIT License " @@ -301,23 +301,28 @@ try let prefix = '' endif + let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) let use_height = has_key(dict, 'down') && \ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right')) && \ executable('tput') && filereadable('/dev/tty') - let tmux = !use_height && (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict) - let term = has('nvim') && !tmux + let use_term = has('nvim') + let use_tmux = (!use_height && !use_term || prefer_tmux) && s:tmux_enabled() && s:splittable(dict) + if prefer_tmux && use_tmux + let use_height = 0 + let use_term = 0 + endif if use_height let optstr .= ' --height='.s:calc_size(&lines, dict.down, dict) - elseif term + elseif use_term let optstr .= ' --no-height' endif - let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result + let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result - if term + if use_term return s:execute_term(dict, command, temps) endif - let lines = tmux ? s:execute_tmux(dict, command, temps) + let lines = use_tmux ? s:execute_tmux(dict, command, temps) \ : s:execute(dict, command, use_height, temps) call s:callback(dict, lines) return lines From d85a69a7092ac88bf0609643d9a2ad77d4d69ca9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 30 Jan 2017 01:53:17 +0900 Subject: [PATCH 415/658] 0.16.3 --- install | 2 +- src/LICENSE | 2 +- src/constants.go | 2 +- src/core.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/install b/install index 01c14ab..9c8059d 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.16.2 +version=0.16.3 auto_completion= key_bindings= update_config=2 diff --git a/src/LICENSE b/src/LICENSE index ea43326..d222c9b 100644 --- a/src/LICENSE +++ b/src/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Junegunn Choi +Copyright (c) 2017 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/constants.go b/src/constants.go index 5013e03..4936693 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.16.2" + version = "0.16.3" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond diff --git a/src/core.go b/src/core.go index 3e27ed7..9b0109b 100644 --- a/src/core.go +++ b/src/core.go @@ -3,7 +3,7 @@ Package fzf implements fzf, a command-line fuzzy finder. The MIT License (MIT) -Copyright (c) 2016 Junegunn Choi +Copyright (c) 2017 Junegunn Choi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 5b749e2d5c469f85ec7f77923829b5981c2a4ca6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 31 Jan 2017 21:43:41 +0900 Subject: [PATCH 416/658] Update documentation --- BUILD.md | 56 +++---------------------------------------------------- README.md | 37 ++++++++++++------------------------ 2 files changed, 15 insertions(+), 78 deletions(-) diff --git a/BUILD.md b/BUILD.md index ca97e09..5b04f67 100644 --- a/BUILD.md +++ b/BUILD.md @@ -25,65 +25,22 @@ make install # Build 32-bit and 64-bit executables and tarballs make release -# Build executables and tarballs for Linux using Docker -make linux +# Make release archives for all supported platforms +make release-all ``` ### Using `go get` Alternatively, you can build fzf directly with `go get` command without -cloning the repository. +manually cloning the repository. ```sh go get -u github.com/junegunn/fzf/src/fzf ``` -Build options -------------- - -### With ncurses 6 - -The official binaries of fzf are built with ncurses 5 because it's widely -supported by different platforms. However ncurses 5 is old and has a number of -limitations. - -1. Does not support more than 256 color pairs (See [357][357]) -2. Does not support italics -3. Does not support 24-bit color - -[357]: https://github.com/junegunn/fzf/issues/357 - -But you can manually build fzf with ncurses 6 to overcome some of these -limitations. ncurses 6 supports up to 32767 color pairs (1), and supports -italics (2). To build fzf with ncurses 6, you have to install it first. On -macOS, you can use Homebrew to install it. - -```sh -brew install homebrew/dupes/ncurses -LDFLAGS="-L/usr/local/opt/ncurses/lib" make install -``` - -### With tcell - -[tcell][tcell] is a portable alternative to ncurses and we currently use it to -build Windows binaries. tcell has many benefits but most importantly, it -supports 24-bit colors. To build fzf with tcell: - -```sh -TAGS=tcell make install -``` - -However, note that tcell has its own issues. - -- Poor rendering performance compared to ncurses -- Does not support bracketed-paste mode -- Does not support italics unlike ncurses 6 -- Some wide characters are not correctly displayed - Third-party libraries used -------------------------- -- [ncurses][ncurses] - [mattn/go-runewidth](https://github.com/mattn/go-runewidth) - Licensed under [MIT](http://mattn.mit-license.org) - [mattn/go-shellwords](https://github.com/mattn/go-shellwords) @@ -97,10 +54,3 @@ License ------- [MIT](LICENSE) - -[install]: https://github.com/junegunn/fzf#installation -[go]: https://golang.org/ -[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock -[ncurses]: https://www.gnu.org/software/ncurses/ -[req]: http://golang.org/doc/install -[tcell]: https://github.com/gdamore/tcell diff --git a/README.md b/README.md index de69d01..303ca4c 100644 --- a/README.md +++ b/README.md @@ -72,16 +72,6 @@ But it's recommended that you use a plugin manager like Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' } ``` -### Upgrading fzf - -fzf is being actively developed and you might want to upgrade it once in a -while. Please follow the instruction below depending on the installation -method used. - -- git: `cd ~/.fzf && git pull && ./install` -- brew: `brew update; brew reinstall fzf` -- vim-plug: `:PlugUpdate fzf` - ### Windows Pre-built binaries for Windows can be downloaded [here][bin]. However, other @@ -91,6 +81,17 @@ flawlessly. [wsl]: https://blogs.msdn.microsoft.com/wsl/ +Upgrading fzf +------------- + +fzf is being actively developed and you might want to upgrade it once in a +while. Please follow the instruction below depending on the installation +method used. + +- git: `cd ~/.fzf && git pull && ./install` +- brew: `brew update; brew reinstall fzf` +- vim-plug: `:PlugUpdate fzf` + Building fzf ------------ @@ -357,7 +358,7 @@ If you have set up fzf for Vim, `:FZF` command will be added. " With options :FZF --no-sort --reverse --inline-info /tmp -" Bang version starts in fullscreen instead of using tmux pane or Neovim split +" Bang version starts fzf in fullscreen mode :FZF! ``` @@ -408,20 +409,6 @@ command! -bang MyStuff Tips ---- -#### Rendering issues - -If you have any rendering issues, check the following: - -1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it - contains `256` (e.g. `xterm-256color`) -2. If you're on screen or tmux, `$TERM` should be either `screen` or - `screen-256color` -3. Some terminal emulators (e.g. mintty) have problem displaying default - background color and make some text unable to read. In that case, try - `--black` option. And if it solves your problem, I recommend including it - in `FZF_DEFAULT_OPTS` for further convenience. -4. If you still have problem, try `--no-256` option or even `--no-color`. - #### Respecting `.gitignore`, `.hgignore`, and `svn:ignore` [ag](https://github.com/ggreer/the_silver_searcher) or From 712b7b218815fe2368884515852327401311a215 Mon Sep 17 00:00:00 2001 From: Kassio Borges Date: Tue, 31 Jan 2017 16:06:52 +0000 Subject: [PATCH 417/658] [vim] Expose buffer variable with the current fzf setup (#828) Exposing the `b:fzf` variable will be useful to get information about which command is being executed on the current fzf window. With that, now, it's possible to use the current command name on the statusline: ```viml au User FzfStatusLine call fzf_statusline() function! s:fzf_statusline() let fzf_cmd_name = get(b:fzf, 'name', 'FZF') let &l:statusline = '> '.fzf_cmd_name endfunction ``` --- plugin/fzf.vim | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 1cd6ff7..9473c44 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -201,6 +201,10 @@ function! fzf#wrap(...) endfor let [name, opts, bang] = args + if len(name) + let opts.name = name + end + " Layout: g:fzf_layout (and deprecated g:fzf_height) if bang for key in s:layout_keys @@ -521,6 +525,7 @@ function! s:execute_term(dict, command, temps) abort let winrest = winrestcmd() let pbuf = bufnr('') let [ppos, winopts] = s:split(a:dict) + let b:fzf = a:dict let fzf = { 'buf': bufnr(''), 'pbuf': pbuf, 'ppos': ppos, 'dict': a:dict, 'temps': a:temps, \ 'winopts': winopts, 'winrest': winrest, 'lines': &lines, \ 'columns': &columns, 'command': a:command } From dd1f26522c965612cb4f55b44e700b8b85868b27 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 1 Feb 2017 02:05:58 +0900 Subject: [PATCH 418/658] Fix caching scheme when --exact is set and '-prefix is used --- src/pattern.go | 2 +- src/pattern_test.go | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/pattern.go b/src/pattern.go index 731104f..4f64660 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -101,7 +101,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, for idx, term := range termSet { // If the query contains inverse search terms or OR operators, // we cannot cache the search scope - if !cacheable || idx > 0 || term.inv { + if !cacheable || idx > 0 || term.inv || !fuzzy && term.typ != termExact { cacheable = false break Loop } diff --git a/src/pattern_test.go b/src/pattern_test.go index ea0082f..9d66c79 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -186,3 +186,21 @@ func TestCacheKey(t *testing.T) { test(true, "foo | bar !baz", "", false) test(true, "| | | foo", "foo", true) } + +func TestCacheable(t *testing.T) { + test := func(fuzzy bool, str string, cacheable bool) { + clearPatternCache() + pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, true, []Range{}, Delimiter{}, []rune(str)) + if cacheable != pat.cacheable { + t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable) + } + } + test(true, "foo bar", true) + test(true, "foo 'bar", true) + test(true, "foo !bar", false) + + test(false, "foo bar", true) + test(false, "foo '", true) + test(false, "foo 'bar", false) + test(false, "foo !bar", false) +} From 736aeaa1d3716c02b161ac70b27285f15279359d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 2 Feb 2017 10:08:56 +0900 Subject: [PATCH 419/658] Update go-runewidth https://github.com/junegunn/go-runewidth/pull/1 /cc @joshuarubin --- src/deps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/deps b/src/deps index 1d8cab7..17e50f2 100755 --- a/src/deps +++ b/src/deps @@ -13,6 +13,6 @@ reset() ( ) reset github.com/junegunn/go-isatty 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8 -reset github.com/junegunn/go-runewidth 63c378b851290989b19ca955468386485f118c65 +reset github.com/junegunn/go-runewidth 14207d285c6c197daabb5c9793d63e7af9ab2d50 reset github.com/junegunn/go-shellwords 33bd8f1ebe16d6e5eb688cc885749a63059e9167 reset golang.org/x/crypto abc5fa7ad02123a41f02bf1391c9760f7586e608 From bd9c46ee348811f5e42f13ff57d835d5f9301df7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 2 Feb 2017 13:00:41 +0900 Subject: [PATCH 420/658] Update ANSI processor to strip ^H along with its preceding character --- src/ansi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansi.go b/src/ansi.go index f550670..3636e02 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -44,7 +44,7 @@ func init() { */ // 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.|[\x08\x0e\x0f]") + ansiRegex = regexp.MustCompile("\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x0e\x0f]|.\x08") } func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) { From c95bb109c83f8a1b7b4a7c316ab7f0c1ef845cd9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 2 Feb 2017 13:14:27 +0900 Subject: [PATCH 421/658] Suppress CSI codes in the output --- src/tui/light.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index d5631ec..2ce0d5f 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -748,13 +748,17 @@ func (w *LightWindow) Print(text string) { w.cprint2(colDefault, w.bg, AttrRegular, text) } +func cleanse(str string) string { + return strings.Replace(str, "\x1b", "?", -1) +} + func (w *LightWindow) CPrint(pair ColorPair, attr Attr, text string) { if !w.colored { w.csiColor(colDefault, colDefault, attrFor(pair, attr)) } else { w.csiColor(pair.Fg(), pair.Bg(), attr) } - w.stderrInternal(text, false) + w.stderrInternal(cleanse(text), false) w.csi("m") } @@ -762,7 +766,7 @@ func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) { if w.csiColor(fg, bg, attr) { defer w.csi("m") } - w.stderrInternal(text, false) + w.stderrInternal(cleanse(text), false) } type wrappedLine struct { From fcf63c74f1de52e4b0038a50154ee1732ef84af5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 2 Feb 2017 13:46:46 +0900 Subject: [PATCH 422/658] Fix --tiebreak=begin with algo v2 Due to performance consideration, FuzzyMatchV2 does not return the exact positions of the matching characters by default. However, the ommission caused `--tiebreak=begin` to produce inaccurate result in some cases. (echo baz foo bar; echo foo bar baz) | fzf --tiebreak=begin -fbar | head -1 # Expected: foo bar baz # Actual: baz foo bar This commit fixes the problem by using the end offset which is guaranteed to be correct. --- src/result.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/result.go b/src/result.go index 3d79176..e071a9e 100644 --- a/src/result.go +++ b/src/result.go @@ -37,12 +37,14 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result { result := Result{item: item, rank: rank{index: item.index}} numChars := item.text.Length() minBegin := math.MaxUint16 + minEnd := math.MaxUint16 maxEnd := 0 validOffsetFound := false for _, offset := range offsets { b, e := int(offset[0]), int(offset[1]) if b < e { minBegin = util.Min(b, minBegin) + minEnd = util.Min(e, minEnd) maxEnd = util.Max(e, maxEnd) validOffsetFound = true } @@ -68,7 +70,7 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result { } } if criterion == byBegin { - val = util.AsUint16(minBegin - whitePrefixLen) + val = util.AsUint16(minEnd - whitePrefixLen) } else { val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/trimLen) } From fe83589ade42e1d6c24b792ec0fa303f07e953e1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 3 Feb 2017 02:14:14 +0900 Subject: [PATCH 423/658] Add test case for --tiebreak=begin --- test/test_go.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/test_go.rb b/test/test_go.rb index f730f25..702aecc 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -617,6 +617,17 @@ class TestGoFZF < TestBase ], `#{FZF} -foo --tiebreak=begin,length < #{tempname}`.split($/) end + def test_tiebreak_begin_algo_v2 + writelines tempname, [ + 'baz foo bar', + 'foo bar baz', + ] + assert_equal [ + 'foo bar baz', + 'baz foo bar', + ], `#{FZF} -fbar --tiebreak=begin --algo=v2 < #{tempname}`.split($/) + end + def test_tiebreak_end writelines tempname, [ 'xoxxxxxxxx', From 4b700192c1f07aa6ba8d0ed7cb9c1b1dcb6f7ddb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 4 Feb 2017 21:51:22 +0900 Subject: [PATCH 424/658] Add --border option to draw horizontal lines above and below the finder Goes well with --height --- CHANGELOG.md | 5 ++++ README.md | 4 +-- man/man1/fzf.1 | 3 +++ src/options.go | 6 +++++ src/terminal.go | 65 ++++++++++++++++++++++++++++++++-------------- src/tui/dummy.go | 2 +- src/tui/light.go | 30 ++++++++++++++------- src/tui/ncurses.go | 5 ++-- src/tui/tcell.go | 59 ++++++++++++++++++++++------------------- src/tui/tui.go | 10 ++++++- 10 files changed, 128 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37ede60..0ea9ee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +0.16.4 +------ +- Added `--border` option to draw border above and below the finder +- Bug fixes and improvements + 0.16.3 ------ - Fixed a bug where fzf incorrectly display the lines when straddling tab diff --git a/README.md b/README.md index 303ca4c..c016de4 100644 --- a/README.md +++ b/README.md @@ -141,10 +141,10 @@ vim $(fzf --height 40% --reverse) ``` You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by -default. +default. For example, ```sh -export FZF_DEFAULT_OPTS='--height 40% --reverse' +export FZF_DEFAULT_OPTS='--height 40% --reverse --border' ``` #### Search syntax diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index f7d3a44..74ac2a4 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -156,6 +156,9 @@ Ignored when \fB--height\fR is not specified. .B "--reverse" Reverse orientation .TP +.B "--border" +Draw border above and below the finder +.TP .BI "--margin=" MARGIN Comma-separated expression for margins around the finder. .br diff --git a/src/options.go b/src/options.go index 3fadb4e..30cc751 100644 --- a/src/options.go +++ b/src/options.go @@ -54,6 +54,7 @@ const usage = `usage: fzf [options] --min-height=HEIGHT Minimum height when --height is given in percent (default: 10) --reverse Reverse orientation + --border Draw border above and below the finder --margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L) --inline-info Display finder info inline with the query --prompt=STR Input prompt (default: '> ') @@ -183,6 +184,7 @@ type Options struct { Header []string HeaderLines int Margin [4]sizeSpec + Bordered bool Tabstop int Version bool } @@ -1086,6 +1088,10 @@ func parseOptions(opts *Options, allArgs []string) { opts.Height = sizeSpec{} case "--no-margin": opts.Margin = defaultMargin() + case "--no-border": + opts.Bordered = false + case "--border": + opts.Bordered = true case "--margin": opts.Margin = parseMargin( nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) diff --git a/src/terminal.go b/src/terminal.go index 134462e..5853022 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -83,8 +83,10 @@ type Terminal struct { tabstop int margin [4]sizeSpec strong tui.Attr + bordered bool + border tui.Window window tui.Window - bwindow tui.Window + pborder tui.Window pwindow tui.Window count int progress int @@ -295,15 +297,22 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { maxHeightFunc := func(termHeight int) int { var maxHeight int if opts.Height.percent { - maxHeight = util.Min(termHeight, - util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight)) + maxHeight = util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight) } else { - maxHeight = util.Min(termHeight, int(opts.Height.size)) + maxHeight = int(opts.Height.size) + } + + effectiveMinHeight := minHeight + if previewBox != nil && (opts.Preview.position == posUp || opts.Preview.position == posDown) { + effectiveMinHeight *= 2 } if opts.InlineInfo { - return util.Max(maxHeight, minHeight-1) + effectiveMinHeight -= 1 } - return util.Max(maxHeight, minHeight) + if opts.Bordered { + effectiveMinHeight += 2 + } + return util.Min(termHeight, util.Max(maxHeight, effectiveMinHeight)) } renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, maxHeightFunc) } else if tui.HasFullscreenRenderer() { @@ -343,6 +352,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { printQuery: opts.PrintQuery, history: opts.History, margin: opts.Margin, + bordered: opts.Bordered, strong: strongAttr, cycle: opts.Cycle, header: header, @@ -499,6 +509,9 @@ func (t *Terminal) resizeWindows() { } else { marginInt[idx] = int(sizeSpec.size) } + if t.bordered && idx%2 == 0 { + marginInt[idx] += 1 + } } adjust := func(idx1 int, idx2 int, max int, min int) { if max >= min { @@ -524,19 +537,29 @@ func (t *Terminal) resizeWindows() { } adjust(1, 3, screenWidth, minAreaWidth) adjust(0, 2, screenHeight, minAreaHeight) + if t.border != nil { + t.border.Close() + } if t.window != nil { t.window.Close() } - if t.bwindow != nil { - t.bwindow.Close() + if t.pborder != nil { + t.pborder.Close() t.pwindow.Close() } width := screenWidth - marginInt[1] - marginInt[3] height := screenHeight - marginInt[0] - marginInt[2] + if t.bordered { + t.border = t.tui.NewWindow( + marginInt[0]-1, + marginInt[3], + width, + height+2, tui.BorderHorizontal) + } if previewVisible { createPreviewWindow := func(y int, x int, w int, h int) { - t.bwindow = t.tui.NewWindow(y, x, w, h, true) + t.pborder = t.tui.NewWindow(y, x, w, h, tui.BorderAround) pwidth := w - 4 // ncurses auto-wraps the line when the cursor reaches the right-end of // the window. To prevent unintended line-wraps, we use the width one @@ -544,28 +567,28 @@ func (t *Terminal) resizeWindows() { if !t.preview.wrap && t.tui.DoesAutoWrap() { pwidth += 1 } - t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, false) + t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, tui.BorderNone) } switch t.preview.position { case posUp: pheight := calculateSize(height, t.preview.size, minHeight, 3) t.window = t.tui.NewWindow( - marginInt[0]+pheight, marginInt[3], width, height-pheight, false) + marginInt[0]+pheight, marginInt[3], width, height-pheight, tui.BorderNone) createPreviewWindow(marginInt[0], marginInt[3], width, pheight) case posDown: pheight := calculateSize(height, t.preview.size, minHeight, 3) t.window = t.tui.NewWindow( - marginInt[0], marginInt[3], width, height-pheight, false) + marginInt[0], marginInt[3], width, height-pheight, tui.BorderNone) createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) case posLeft: pwidth := calculateSize(width, t.preview.size, minWidth, 5) t.window = t.tui.NewWindow( - marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false) + marginInt[0], marginInt[3]+pwidth, width-pwidth, height, tui.BorderNone) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) case posRight: pwidth := calculateSize(width, t.preview.size, minWidth, 5) t.window = t.tui.NewWindow( - marginInt[0], marginInt[3], width-pwidth, height, false) + marginInt[0], marginInt[3], width-pwidth, height, tui.BorderNone) createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) } } else { @@ -573,7 +596,7 @@ func (t *Terminal) resizeWindows() { marginInt[0], marginInt[3], width, - height, false) + height, tui.BorderNone) } if !t.tui.IsOptimized() && t.theme != nil && t.theme.HasBg() { for i := 0; i < t.window.Height(); i++ { @@ -978,11 +1001,15 @@ func (t *Terminal) printAll() { func (t *Terminal) refresh() { if !t.suppress { - if t.hasPreviewWindow() { - t.tui.RefreshWindows([]tui.Window{t.bwindow, t.pwindow, t.window}) - } else { - t.tui.RefreshWindows([]tui.Window{t.window}) + windows := make([]tui.Window, 0, 4) + if t.bordered { + windows = append(windows, t.border) } + if t.hasPreviewWindow() { + windows = append(windows, t.pborder, t.pwindow) + } + windows = append(windows, t.window) + t.tui.RefreshWindows(windows) } } diff --git a/src/tui/dummy.go b/src/tui/dummy.go index 01179c8..60a23fb 100644 --- a/src/tui/dummy.go +++ b/src/tui/dummy.go @@ -40,6 +40,6 @@ func (r *FullscreenRenderer) MaxY() int { return 0 } func (r *FullscreenRenderer) RefreshWindows(windows []Window) {} -func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { +func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window { return nil } diff --git a/src/tui/light.go b/src/tui/light.go index 2ce0d5f..9465c49 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -95,7 +95,7 @@ type LightRenderer struct { type LightWindow struct { renderer *LightRenderer colored bool - border bool + border BorderStyle top int left int width int @@ -600,11 +600,11 @@ func (r *LightRenderer) IsOptimized() bool { return false } -func (r *LightRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { +func (r *LightRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window { w := &LightWindow{ renderer: r, colored: r.theme != nil, - border: border, + border: borderStyle, top: top, left: left, width: width, @@ -614,13 +614,27 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, bord if r.theme != nil { w.bg = r.theme.Bg } - if w.border { - w.drawBorder() - } + w.drawBorder() return w } func (w *LightWindow) drawBorder() { + switch w.border { + case BorderAround: + w.drawBorderAround() + case BorderHorizontal: + w.drawBorderHorizontal() + } +} + +func (w *LightWindow) drawBorderHorizontal() { + w.Move(0, 0) + w.CPrint(ColBorder, AttrRegular, repeat("─", w.width)) + w.Move(w.height-1, 0) + w.CPrint(ColBorder, AttrRegular, repeat("─", w.width)) +} + +func (w *LightWindow) drawBorderAround() { w.Move(0, 0) w.CPrint(ColBorder, AttrRegular, "┌"+repeat("─", w.width-2)+"┐") for y := 1; y < w.height-1; y++ { @@ -854,9 +868,7 @@ func (w *LightWindow) FinishFill() { } func (w *LightWindow) Erase() { - if w.border { - w.drawBorder() - } + w.drawBorder() // We don't erase the window here to avoid flickering during scroll w.Move(0, 0) } diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 978b2e7..4b88b44 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -189,12 +189,13 @@ func (r *FullscreenRenderer) Close() { C.delscreen(_screen) } -func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { +func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window { win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left)) if r.theme != nil { C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index())))) } - if border { + // FIXME Does not implement BorderHorizontal + if borderStyle != BorderNone { pair, attr := _colorFn(ColBorder, 0) C.wcolor_set(win, pair, nil) C.wattron(win, attr) diff --git a/src/tui/tcell.go b/src/tui/tcell.go index aa67ae1..964c19e 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -27,15 +27,15 @@ func (p ColorPair) style() tcell.Style { type Attr tcell.Style type TcellWindow struct { - color bool - top int - left int - width int - height int - lastX int - lastY int - moveCursor bool - border bool + color bool + top int + left int + width int + height int + lastX int + lastY int + moveCursor bool + borderStyle BorderStyle } func (w *TcellWindow) Top() int { @@ -61,8 +61,11 @@ func (w *TcellWindow) Refresh() { } w.lastX = 0 w.lastY = 0 - if w.border { - w.drawBorder() + switch w.borderStyle { + case BorderAround: + w.drawBorder(true) + case BorderHorizontal: + w.drawBorder(false) } } @@ -377,15 +380,15 @@ func (r *FullscreenRenderer) RefreshWindows(windows []Window) { _screen.Show() } -func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window { +func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window { // TODO return &TcellWindow{ - color: r.theme != nil, - top: top, - left: left, - width: width, - height: height, - border: border} + color: r.theme != nil, + top: top, + left: left, + width: width, + height: height, + borderStyle: borderStyle} } func (w *TcellWindow) Close() { @@ -536,7 +539,7 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn { return w.fillString(str, ColorPair{fg, bg, -1}, a) } -func (w *TcellWindow) drawBorder() { +func (w *TcellWindow) drawBorder(around bool) { left := w.left right := left + w.width top := w.top @@ -554,13 +557,15 @@ func (w *TcellWindow) drawBorder() { _screen.SetContent(x, bot-1, tcell.RuneHLine, nil, style) } - for y := top; y < bot; y++ { - _screen.SetContent(left, y, tcell.RuneVLine, nil, style) - _screen.SetContent(right-1, y, tcell.RuneVLine, nil, style) - } + if around { + for y := top; y < bot; y++ { + _screen.SetContent(left, y, tcell.RuneVLine, nil, style) + _screen.SetContent(right-1, y, tcell.RuneVLine, nil, style) + } - _screen.SetContent(left, top, tcell.RuneULCorner, nil, style) - _screen.SetContent(right-1, top, tcell.RuneURCorner, nil, style) - _screen.SetContent(left, bot-1, tcell.RuneLLCorner, nil, style) - _screen.SetContent(right-1, bot-1, tcell.RuneLRCorner, nil, style) + _screen.SetContent(left, top, tcell.RuneULCorner, nil, style) + _screen.SetContent(right-1, top, tcell.RuneURCorner, nil, style) + _screen.SetContent(left, bot-1, tcell.RuneLLCorner, nil, style) + _screen.SetContent(right-1, bot-1, tcell.RuneLRCorner, nil, style) + } } diff --git a/src/tui/tui.go b/src/tui/tui.go index 2508aa6..f8d905a 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -195,6 +195,14 @@ type MouseEvent struct { Mod bool } +type BorderStyle int + +const ( + BorderNone BorderStyle = iota + BorderAround + BorderHorizontal +) + type Renderer interface { Init() Pause() @@ -211,7 +219,7 @@ type Renderer interface { DoesAutoWrap() bool IsOptimized() bool - NewWindow(top int, left int, width int, height int, border bool) Window + NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window } type Window interface { From 72265298f95d184c8e7d118e335f836899831042 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 4 Feb 2017 21:52:05 +0900 Subject: [PATCH 425/658] [vim] Apply --no-height when running fzf in full screen mode To override --height option in FZF_DEFAULT_OPTS --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 9473c44..552b2e3 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -317,7 +317,7 @@ try endif if use_height let optstr .= ' --height='.s:calc_size(&lines, dict.down, dict) - elseif use_term + else let optstr .= ' --no-height' endif let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result From e5147392809df7ea4033a7d1f602ad1cfb0673a3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 4 Feb 2017 22:49:17 +0900 Subject: [PATCH 426/658] Fix failing test case --- test/test_go.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 702aecc..582b10b 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1554,7 +1554,7 @@ module CompletionTest tmux.prepare # Using tmux - tmux.send_keys 'unset FZFFOO**', :Tab + tmux.send_keys 'unset FZFFOOBR**', :Tab tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' } @@ -1562,7 +1562,7 @@ module CompletionTest # FZF_TMUX=1 new_shell - tmux.send_keys 'unset FZFFO**', :Tab, pane: 0 + tmux.send_keys 'unset FZFFOOBR**', :Tab, pane: 0 tmux.until(false, 1) { |lines| lines.match_count == 1 } tmux.send_keys :Enter tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' } From 02ceae15a235de6b5cd2aca9de070fb3fff78e5b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 5 Feb 2017 02:07:54 +0900 Subject: [PATCH 427/658] [vim] Download instruction for Windows --- plugin/fzf.vim | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 552b2e3..5d1ed2f 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -28,10 +28,12 @@ let g:loaded_fzf = 1 let s:default_layout = { 'down': '~40%' } let s:layout_keys = ['window', 'up', 'down', 'left', 'right'] -let s:fzf_go = expand(':h:h').'/bin/fzf' -let s:install = expand(':h:h').'/install' +let s:is_win = has('win32') || has('win64') +let s:base_dir = expand(':h:h') +let s:fzf_go = s:base_dir.'/bin/fzf' +let s:fzf_tmux = s:base_dir.'/bin/fzf-tmux' +let s:install = s:base_dir.'/install' let s:installed = 0 -let s:fzf_tmux = expand(':h:h').'/bin/fzf-tmux' let s:cpo_save = &cpo set cpo&vim @@ -42,6 +44,11 @@ function! s:fzf_exec() let s:exec = s:fzf_go elseif executable('fzf') let s:exec = 'fzf' + elseif s:is_win + call s:warn('fzf executable not found.') + call s:warn('Download fzf binary for Windows from https://github.com/junegunn/fzf-bin/releases/') + call s:warn('and place it as '.s:base_dir.'\bin\fzf.exe') + throw 'fzf executable not found' elseif !s:installed && executable(s:install) && \ input('fzf executable not found. Download binary? (y/n) ') =~? '^y' redraw @@ -246,7 +253,7 @@ function! fzf#wrap(...) endfunction function! fzf#shellescape(path) - if has('win32') || has('win64') + if s:is_win let shellslash = &shellslash try set noshellslash @@ -263,7 +270,7 @@ try let oshell = &shell let useshellslash = &shellslash - if has('win32') || has('win64') + if s:is_win set shell=cmd.exe set noshellslash else @@ -307,7 +314,7 @@ try let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) let use_height = has_key(dict, 'down') && - \ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right')) && + \ !(has('nvim') || s:is_win || s:present(dict, 'up', 'left', 'right')) && \ executable('tput') && filereadable('/dev/tty') let use_term = has('nvim') let use_tmux = (!use_height && !use_term || prefer_tmux) && s:tmux_enabled() && s:splittable(dict) @@ -317,7 +324,7 @@ try endif if use_height let optstr .= ' --height='.s:calc_size(&lines, dict.down, dict) - else + elseif use_term let optstr .= ' --no-height' endif let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result @@ -405,7 +412,7 @@ function! s:xterm_launcher() \ &columns, &lines/2, getwinposx(), getwinposy()) endfunction unlet! s:launcher -if has('win32') || has('win64') +if s:is_win let s:launcher = '%s' else let s:launcher = function('s:xterm_launcher') From ee5aeb80a49fbf7bb958e09aa641ef17fb885884 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 5 Feb 2017 16:17:54 +0900 Subject: [PATCH 428/658] 0.16.4 --- install | 20 +++----------------- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/Makefile | 26 +++++++++++++------------- src/constants.go | 2 +- 5 files changed, 19 insertions(+), 33 deletions(-) diff --git a/install b/install index 9c8059d..3d07565 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.16.3 +version=0.16.4 auto_completion= key_bindings= update_config=2 @@ -88,17 +88,6 @@ check_binary() { return 1 } -symlink() { - echo " - Creating symlink: bin/$1 -> bin/fzf" - (cd "$fzf_base"/bin && - rm -f fzf && - ln -sf $1 fzf) - if [ $? -ne 0 ]; then - binary_error="Failed to create symlink" - return 1 - fi -} - link_fzf_in_path() { if which_fzf="$(command -v fzf)"; then echo " - Found in \$PATH" @@ -124,9 +113,6 @@ download() { echo " - Already exists" check_binary && return fi - if [ -x "$fzf_base"/bin/$1 ]; then - symlink $1 && check_binary && return - fi link_fzf_in_path && return fi mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin @@ -147,12 +133,12 @@ download() { fi set +o pipefail - if [ ! -f $1 ]; then + if [ ! -f fzf ]; then binary_error="Failed to download ${1}" return fi - chmod +x $1 && symlink $1 && check_binary + chmod +x fzf && check_binary } # Try to download binary executable diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index b243dbd..055dab3 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Jan 2017" "fzf 0.16.3" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Feb 2017" "fzf 0.16.4" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 74ac2a4..3d33d43 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Jan 2017" "fzf 0.16.3" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Feb 2017" "fzf 0.16.4" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/Makefile b/src/Makefile index d1d0c14..102229b 100644 --- a/src/Makefile +++ b/src/Makefile @@ -52,23 +52,23 @@ all: fzf/$(BINARY) ifeq ($(GOOS),windows) release: fzf/$(BINARY32) fzf/$(BINARY64) - cd fzf && cp $(BINARY32) $(RELEASE32).exe && zip $(RELEASE32).zip $(RELEASE32).exe - cd fzf && cp $(BINARY64) $(RELEASE64).exe && zip $(RELEASE64).zip $(RELEASE64).exe - cd fzf && rm -f $(RELEASE32).exe $(RELEASE64).exe + cd fzf && cp -f $(BINARY32) fzf.exe && zip $(RELEASE32).zip fzf.exe + cd fzf && cp -f $(BINARY64) fzf.exe && zip $(RELEASE64).zip fzf.exe + cd fzf && rm -f fzf.exe else ifeq ($(GOOS),linux) release: fzf/$(BINARY32) fzf/$(BINARY64) fzf/$(BINARYARM5) fzf/$(BINARYARM6) fzf/$(BINARYARM7) fzf/$(BINARYARM8) - cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) - cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) - cd fzf && cp $(BINARYARM5) $(RELEASEARM5) && tar -czf $(RELEASEARM5).tgz $(RELEASEARM5) - cd fzf && cp $(BINARYARM6) $(RELEASEARM6) && tar -czf $(RELEASEARM6).tgz $(RELEASEARM6) - cd fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) - cd fzf && cp $(BINARYARM8) $(RELEASEARM8) && tar -czf $(RELEASEARM8).tgz $(RELEASEARM8) - cd fzf && rm -f $(RELEASE32) $(RELEASE64) $(RELEASEARM5) $(RELEASEARM6) $(RELEASEARM7) $(RELEASEARM8) + cd fzf && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf + cd fzf && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf + cd fzf && cp -f $(BINARYARM5) fzf && tar -czf $(RELEASEARM5).tgz fzf + cd fzf && cp -f $(BINARYARM6) fzf && tar -czf $(RELEASEARM6).tgz fzf + cd fzf && cp -f $(BINARYARM7) fzf && tar -czf $(RELEASEARM7).tgz fzf + cd fzf && cp -f $(BINARYARM8) fzf && tar -czf $(RELEASEARM8).tgz fzf + cd fzf && rm -f fzf else release: fzf/$(BINARY32) fzf/$(BINARY64) - cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) - cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) - cd fzf && rm -f $(RELEASE32) $(RELEASE64) + cd fzf && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf + cd fzf && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf + cd fzf && rm -f fzf endif release-all: clean test diff --git a/src/constants.go b/src/constants.go index 4936693..d5246db 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.16.3" + version = "0.16.4" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 876c233a2641507d67b470cabbdedfb5dcd4696d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 6 Feb 2017 21:06:12 +0900 Subject: [PATCH 429/658] Remove Ruby version Related #832 --- fzf | 1348 ------------------------------------------------------- install | 81 +--- 2 files changed, 5 insertions(+), 1424 deletions(-) delete mode 100755 fzf diff --git a/fzf b/fzf deleted file mode 100755 index 62893af..0000000 --- a/fzf +++ /dev/null @@ -1,1348 +0,0 @@ -#!/usr/bin/env ruby -# encoding: utf-8 -# -# ____ ____ -# / __/___ / __/ -# / /_/_ / / /_ -# / __/ / /_/ __/ -# /_/ /___/_/ Fuzzy finder for your shell -# -# Version: 0.8.9 (Dec 24, 2014) -# Deprecation alert: -# This script is no longer maintained. Use the new Go version. -# -# Author: Junegunn Choi -# URL: https://github.com/junegunn/fzf -# License: MIT -# -# Copyright (c) 2014 Junegunn Choi -# -# MIT License -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -begin - require 'curses' -rescue LoadError - $stderr.puts 'curses gem is not installed. Try `gem install curses`.' - sleep 1 - exit 1 -end -require 'thread' -require 'set' - -unless String.method_defined? :force_encoding - class String - def force_encoding *arg - self - end - end -end - -class String - attr_accessor :orig - - def tokenize delim, nth - unless delim - # AWK default - prefix_length = (index(/\S/) || 0) rescue 0 - tokens = scan(/\S+\s*/) rescue [] - else - prefix_length = 0 - tokens = scan(delim) rescue [] - end - nth.map { |n| - if n.begin == 0 && n.end == -1 - [prefix_length, tokens.join] - elsif part = tokens[n] - [prefix_length + (tokens[0...(n.begin)] || []).join.length, - part.join] - end - }.compact - end -end - -class FZF - C = Curses - attr_reader :rxflag, :sort, :nth, :color, :black, :ansi256, :reverse, :prompt, - :mouse, :multi, :query, :select1, :exit0, :filter, :extended, - :print_query, :with_nth - - def sync - @shr_mtx.synchronize { yield } - end - - def get name - sync { instance_variable_get name } - end - - def geta(*names) - sync { names.map { |name| instance_variable_get name } } - end - - def call(name, method, *args) - sync { instance_variable_get(name).send(method, *args) } - end - - def set name, value = nil - sync do - instance_variable_set name, - (block_given? ? yield(instance_variable_get(name)) : value) - end - end - - def initialize argv, source = $stdin - @rxflag = nil - @sort = ENV.fetch('FZF_DEFAULT_SORT', 1000).to_i - @color = true - @ansi256 = true - @black = false - @multi = false - @mouse = true - @extended = nil - @select1 = false - @exit0 = false - @filter = nil - @nth = nil - @with_nth = nil - @delim = nil - @reverse = false - @prompt = '> ' - @shr_mtx = Mutex.new - @expect = false - @print_query = false - - argv = - if opts = ENV['FZF_DEFAULT_OPTS'] - require 'shellwords' - Shellwords.shellwords(opts) + argv - else - argv.dup - end - while o = argv.shift - case o - when '--version' then FZF.version - when '-h', '--help' then usage 0 - when '-m', '--multi' then @multi = true - when '+m', '--no-multi' then @multi = false - when '-x', '--extended' then @extended = :fuzzy - when '+x', '--no-extended' then @extended = nil - when '-i' then @rxflag = Regexp::IGNORECASE - when '+i' then @rxflag = 0 - when '-c', '--color' then @color = true - when '+c', '--no-color' then @color = false - when '-2', '--256' then @ansi256 = true - when '+2', '--no-256' then @ansi256 = false - when '--black' then @black = true - when '--no-black' then @black = false - when '--mouse' then @mouse = true - when '--no-mouse' then @mouse = false - when '--reverse' then @reverse = true - when '--no-reverse' then @reverse = false - when '+s', '--no-sort' then @sort = nil - when '-1', '--select-1' then @select1 = true - when '+1', '--no-select-1' then @select1 = false - when '-0', '--exit-0' then @exit0 = true - when '+0', '--no-exit-0' then @exit0 = false - when '-q', '--query' - usage 1, 'query string required' unless query = argv.shift - @query = query - when /^-q(.*)$/, /^--query=(.*)$/ - @query = $1 - when '-f', '--filter' - usage 1, 'query string required' unless query = argv.shift - @filter = query - when /^-f(.*)$/, /^--filter=(.*)$/ - @filter = $1 - when '-n', '--nth' - usage 1, 'field expression required' unless nth = argv.shift - @nth = parse_nth nth - when /^-n([0-9,-\.]+)$/, /^--nth=([0-9,-\.]+)$/ - @nth = parse_nth $1 - when '--with-nth' - usage 1, 'field expression required' unless nth = argv.shift - @with_nth = parse_nth nth - when /^--with-nth=([0-9,-\.]+)$/ - @with_nth = parse_nth $1 - when '-d', '--delimiter' - usage 1, 'delimiter required' unless delim = argv.shift - @delim = FZF.build_delim_regex delim - when /^-d(.+)$/, /^--delimiter=(.+)$/ - @delim = FZF.build_delim_regex $1 - when '-s', '--sort' - usage 1, 'sort size required' unless sort = argv.shift - usage 1, 'invalid sort size' unless sort =~ /^[0-9]+$/ - @sort = sort.to_i - when /^-s([0-9]+)$/, /^--sort=([0-9]+)$/ - @sort = $1.to_i - when '--prompt' - usage 1, 'prompt string required' unless prompt = argv.shift - @prompt = prompt - when /^--prompt=(.*)$/ - @prompt = $1 - when '--print-query' then @print_query = true - when '--no-print-query' then @print_query = false - when '-e', '--extended-exact' then @extended = :exact - when '+e', '--no-extended-exact' then @extended = nil - when '--expect' - argv.shift - @expect = true - when /^--expect=(.*)$/ - @expect = true - when '--toggle-sort', '--tiebreak', '--color', '--bind', '--history', '--history-size' - argv.shift - when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll', - '--inline-info', '--no-inline-info', '--read0', '--cycle', /^--bind=(.*)$/, - /^--color=(.*)$/, /^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/, /^--history(-max)?=(.*)$/ - # XXX - else - usage 1, "illegal option: #{o}" - end - end - - @source = source.clone - @evt_mtx = Mutex.new - @cv = ConditionVariable.new - @events = {} - @new = [] - @queue = Queue.new - @pending = nil - @rev_dir = @reverse ? -1 : 1 - @stdout = $stdout.clone - - unless @filter - # Shared variables: needs protection - @query ||= '' - @matches = [] - @count = 0 - @xcur = @query.length - @ycur = 0 - @yoff = 0 - @dirty = Set.new - @spinner = '-\|/-\|/'.split(//) - @selects = {} # ordered >= 1.9 - - @main = Thread.current - @plcount = 0 - end - end - - def parse_nth nth - ranges = nth.split(',').map { |expr| - x = proc { usage 1, "invalid field expression: #{expr}" } - first, second = expr.split('..', 2) - x.call if !first.empty? && first.to_i == 0 || - second && !second.empty? && (second.to_i == 0 || second.include?('.')) - - first = first.empty? ? 1 : first.to_i - second = case second - when nil then first - when '' then -1 - else second.to_i - end - - Range.new(*[first, second].map { |e| e > 0 ? e - 1 : e }) - } - ranges == [0..-1] ? nil : ranges - end - - def FZF.build_delim_regex delim - Regexp.compile(delim) rescue (delim = Regexp.escape(delim)) - Regexp.compile "(?:.*?#{delim})|(?:.+?$)" - end - - def burp string, orig = nil - @stdout.puts(orig || string.orig || string) - end - - def start - if @filter - start_reader.join - filter_list @new - else - start_reader - query = get(:@query) - emit(:key) { [query, query.length] } unless empty = query.empty? - if @select1 || @exit0 - start_search do |loaded, matches| - len = empty ? get(:@count) : matches.length - if loaded - if @select1 && len == 1 - puts @query if @print_query - puts if @expect - burp(empty ? matches.first : matches.first.first) - exit 0 - elsif @exit0 && len == 0 - puts @query if @print_query - puts if @expect - exit 0 - end - end - - if loaded || len > 1 - start_renderer - Thread.new { start_loop } - end - end - - sleep - else - start_search - start_renderer - start_loop - end - end - end - - def filter_list list - puts @filter if @print_query - matches = matcher.match(list, @filter, '', '') - if @sort && matches.length <= @sort - matches = FZF.sort(matches) - end - matches.each { |m| puts m.first } - end - - def matcher - @matcher ||= - if @extended - ExtendedFuzzyMatcher.new @rxflag, @extended, @nth, @delim - else - FuzzyMatcher.new @rxflag, @nth, @delim - end - end - - class << self - def version - File.open(__FILE__, 'r') do |f| - f.each_line do |line| - if line =~ /Version: (.*)/ - $stdout.puts 'fzf ' << $1 - exit - end - end - end - end - - def sort list - list.sort_by { |tuple| rank tuple } - end - - def rank tuple - line, offsets = tuple - matchlen = 0 - pe = 0 - offsets.sort.each do |pair| - b, e = pair - b = pe if pe > b - pe = e if e > pe - matchlen += e - b if e > b - end - [matchlen, line.length, line] - end - end - - def usage x, message = nil - $stderr.puts message if message - $stderr.puts %[usage: fzf [options] - - Search - -x, --extended Extended-search mode - -e, --extended-exact Extended-search mode (exact match) - -i Case-insensitive match (default: smart-case match) - +i Case-sensitive match - -n, --nth=N[,..] Comma-separated list of field index expressions - for limiting search scope. Each can be a non-zero - integer or a range expression ([BEGIN]..[END]). - --with-nth=N[,..] Transform the item using index expressions for search - -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) - - Search result - -s, --sort=MAX Maximum number of matched items to sort (default: 1000) - +s, --no-sort Do not sort the result. Keep the sequence unchanged. - - Interface - -m, --multi Enable multi-select with tab/shift-tab - --no-mouse Disable mouse - +c, --no-color Disable colors - +2, --no-256 Disable 256-color - --black Use black background - --reverse Reverse orientation - --prompt=STR Input prompt (default: '> ') - - Scripting - -q, --query=STR Start the finder with the given query - -1, --select-1 Automatically select the only match - -0, --exit-0 Exit immediately when there's no match - -f, --filter=STR Filter mode. Do not start interactive finder. - --print-query Print query as the first line - - Environment variables - FZF_DEFAULT_COMMAND Default command to use when input is tty - FZF_DEFAULT_OPTS Default options (e.g. "-x -m --sort 10000")] + $/ + $/ - exit x - end - - def emit event - @evt_mtx.synchronize do - @events[event] = yield - @cv.broadcast - end - end - - def max_items; C.lines - 2; end - - def cursor_y offset = 0 - @reverse ? (offset) : (C.lines - 1 - offset) - end - - def cprint str, col - C.attron(col) do - addstr_safe str - end if str - end - def addstr_safe str - str = str.gsub("\0", '') rescue str - C.addstr str - end - - def print_input - C.setpos cursor_y, 0 - C.clrtoeol - cprint @prompt, color(:prompt, true) - C.attron(C::A_BOLD) do - C.addstr get(:@query) - end - end - - def print_info msg = nil - C.setpos cursor_y(1), 0 - C.clrtoeol - - prefix = - if spin_char = call(:@spinner, :first) - cprint spin_char, color(:spinner, true) - ' ' - else - ' ' - end - C.attron color(:info, false) do - sync do - C.addstr "#{prefix}#{@matches.length}/#{@count}" - if (selected = @selects.length) > 0 - C.addstr " (#{selected})" - end - end - C.addstr msg if msg - end - end - - def refresh - query, xcur = geta(:@query, :@xcur) - C.setpos cursor_y, @prompt.length + width(query[0, xcur]) - C.refresh - end - - def ctrl char - char.to_s.ord - 'a'.ord + 1 - end - - def format line, limit, offsets - offsets ||= [] - maxe = offsets.map { |e| e.last }.max || 0 - - # Overflow - if width(line) > limit - ewidth = width(line[0...maxe]) - # Stri.. - if ewidth <= limit - 2 - line, _ = trim line, limit - 2, false - line << '..' - # ..ring - else - # ..ri.. - line = line[0...maxe] + '..' if ewidth < width(line) - 2 - line, diff = trim line, limit - 2, true - offsets = offsets.map { |pair| - b, e = pair - b += 2 - diff - e += 2 - diff - b = [2, b].max - [b, e] - } - line = '..' + line - end - end - - tokens = [] - index = 0 - offsets.select { |pair| pair.first < pair.last }. - sort_by { |pair| pair }.each do |pair| - b, e = pair.map { |x| [index, x].max } - tokens << [line[index...b], false] - tokens << [line[b...e], true] - index = e - end - tokens << [line[index..-1], false] if index < line.length - tokens.reject { |pair| pair.first.empty? } - end - - def print_item row, tokens, chosen, selected - # Cursor - C.setpos row, 0 - C.clrtoeol - cprint chosen ? '>' : ' ', color(:cursor, true) - cprint selected ? '>' : ' ', - chosen ? color(:chosen) : (selected ? color(:selected, true) : 0) - - # Highlighted item - C.attron color(:chosen, true) if chosen - tokens.each do |pair| - token, highlighted = pair - - if highlighted - cprint token, color(chosen ? :match! : :match, chosen) - C.attron color(:chosen, true) if chosen - else - addstr_safe token - end - end - C.attroff color(:chosen, true) if chosen - end - - AFTER_1_9 = RUBY_VERSION.split('.').map { |e| e.rjust(3, '0') }.join >= '001009' - - if AFTER_1_9 - @@wrx = Regexp.new '\p{Han}|\p{Katakana}|\p{Hiragana}|\p{Hangul}' - def width str - str.gsub(@@wrx, ' ').length rescue str.length - end - - def trim str, len, left - width = width str - diff = 0 - while width > len - width -= ((left ? str[0, 1] : str[-1, 1]) =~ @@wrx ? 2 : 1) rescue 1 - str = left ? str[1..-1] : str[0...-1] - diff += 1 - end - [str, diff] - end - else - def width str - str.length - end - - def trim str, len, left - diff = str.length - len - if diff > 0 - [left ? str[diff..-1] : str[0...-diff], diff] - else - [str, 0] - end - end - - class ::String - def ord - self.unpack('c').first - end - end - - class ::Fixnum - def ord - self - end - end - end - - def init_screen - $stdout.reopen($stderr) - - C.init_screen - C.mousemask C::ALL_MOUSE_EVENTS if @mouse - C.start_color - dbg = - if !@black && C.respond_to?(:use_default_colors) - C.use_default_colors - -1 - else - C::COLOR_BLACK - end - C.raw - C.noecho - - if @color - if @ansi256 && ENV['TERM'].to_s =~ /256/ - C.init_pair 1, 110, dbg - C.init_pair 2, 108, dbg - C.init_pair 3, 254, 236 - C.init_pair 4, 151, 236 - C.init_pair 5, 148, dbg - C.init_pair 6, 144, dbg - C.init_pair 7, 161, 236 - C.init_pair 8, 168, 236 - else - C.init_pair 1, C::COLOR_BLUE, dbg - C.init_pair 2, C::COLOR_GREEN, dbg - C.init_pair 3, C::COLOR_YELLOW, C::COLOR_BLACK - C.init_pair 4, C::COLOR_GREEN, C::COLOR_BLACK - C.init_pair 5, C::COLOR_GREEN, dbg - C.init_pair 6, C::COLOR_WHITE, dbg - C.init_pair 7, C::COLOR_RED, C::COLOR_BLACK - C.init_pair 8, C::COLOR_MAGENTA, C::COLOR_BLACK - end - - def self.color sym, bold = false - C.color_pair([:prompt, :match, :chosen, :match!, - :spinner, :info, :cursor, :selected].index(sym) + 1) | - (bold ? C::A_BOLD : 0) - end - else - def self.color sym, bold = false - case sym - when :chosen - bold ? C::A_REVERSE : 0 - when :match - C::A_UNDERLINE - when :match! - C::A_REVERSE | C::A_UNDERLINE - else - 0 - end | (bold ? C::A_BOLD : 0) - end - end - - C.refresh - end - - def start_reader - stream = - if @source.tty? - default_command = ENV['FZF_DEFAULT_COMMAND'] - if default_command && !default_command.empty? - IO.popen(default_command) - elsif !`which find`.empty? - IO.popen("find * -path '*/\\.*' -prune -o -type f -print -o -type l -print 2> /dev/null") - else - exit 1 - end - else - @source - end - - Thread.new do - if @with_nth - while line = stream.gets - emit(:new) { @new << transform(line) } - end - else - while line = stream.gets - emit(:new) { @new << line.chomp } - end - end - emit(:loaded) { true } - @spinner.clear if @spinner - end - end - - def transform line - line = line.chomp - mut = (line =~ / $/ ? line : line + ' '). - tokenize(@delim, @with_nth).map { |e| e.last }.join('').sub(/ *$/, '') - mut.orig = line - mut - end - - def start_search &callback - Thread.new do - lists = [] - events = {} - fcache = {} - q = '' - delay = -5 - - begin - while true - @evt_mtx.synchronize do - while true - events.merge! @events - - if @events.empty? # No new events - @cv.wait @evt_mtx - next - end - @events.clear - break - end - - if events[:new] - lists << @new - set(:@count) { |c| c + @new.length } - set(:@spinner) { |spinner| - if e = spinner.shift - spinner.push e - end; spinner - } - @new = [] - fcache.clear - end - end#mtx - - new_search = events[:key] || events.delete(:new) - user_input = events[:key] - progress = 0 - started_at = Time.now - - if updated = new_search && !lists.empty? - q, cx = events.delete(:key) || [q, 0] - empty = matcher.empty?(q) - unless matches = fcache[q] - found = [] - skip = false - cnt = 0 - lists.each do |list| - cnt += list.length - skip = @evt_mtx.synchronize { @events[:key] } - break if skip - - if !empty && (progress = 100 * cnt / get(:@count)) < 100 && Time.now - started_at > 0.5 - render { print_info " (#{progress}%)" } - end - - found.concat(q.empty? ? list : - matcher.match(list, q, q[0, cx], q[cx..-1])) - end - if skip - sleep 0.1 - next - end - matches = @sort ? found : found.reverse - if !empty && @sort && matches.length <= @sort - matches = FZF.sort(matches) - end - fcache[q] = matches - end - - # Atomic update - set(:@matches, matches) - end#new_search - - callback = nil if callback && - (updated || events[:loaded]) && - callback.call(events[:loaded], matches) - - # This small delay reduces the number of partial lists - sleep((delay = [20, delay + 5].min) * 0.01) unless user_input - - update_list new_search - end#while - rescue Exception => e - @main.raise e - end - end - end - - def pick - sync do - item = @matches[@ycur] - item.is_a?(Array) ? item[0] : item - end - end - - def constrain offset, cursor, count, height - original = [offset, cursor] - diffpos = cursor - offset - - # Constrain cursor - cursor = [0, [cursor, count - 1].min].max - - # Ceil - if cursor > offset + (height - 1) - offset = cursor - (height - 1) - # Floor - elsif offset > cursor - offset = cursor - end - - # Adjustment - if count - offset < height - offset = [0, count - height].max - cursor = [0, [offset + diffpos, count - 1].min].max - end - - [[offset, cursor] != original, offset, cursor] - end - - def update_list wipe - render do - pos, items = sync { - changed, @yoff, @ycur = - constrain(@yoff, @ycur, @matches.length, max_items) - wipe ||= changed - - [@ycur - @yoff, @matches[@yoff, max_items]] - } - - # Wipe - if items.length < @plcount - @plcount.downto(items.length) do |idx| - C.setpos cursor_y(idx + 2), 0 - C.clrtoeol - end - end - @plcount = items.length - - dirty = Set[pos] - set(:@dirty) do |vs| - dirty.merge vs - Set.new - end - items.each_with_index do |item, idx| - next unless wipe || dirty.include?(idx) - row = cursor_y(idx + 2) - chosen = idx == pos - selected = @selects.include?([*item][0]) - line, offsets = item - tokens = format line, C.cols - 3, offsets - print_item row, tokens, chosen, selected - end - print_info - print_input - end - end - - def start_renderer - init_screen - - Thread.new do - begin - while blk = @queue.shift - blk.call - refresh - end - rescue Exception => e - @main.raise e - end - end - end - - def render &blk - @queue.push blk - nil - end - - def vselect &prc - sync do - @dirty << @ycur - @yoff - @ycur = prc.call @ycur - end - update_list false - end - - def num_unicode_bytes chr - # http://en.wikipedia.org/wiki/UTF-8 - if chr & 0b10000000 > 0 - bytes = 0 - 7.downto(2) do |shift| - break if (chr >> shift) & 0x1 == 0 - bytes += 1 - end - bytes - else - 1 - end - end - - def read_nb chars = 1, default = nil, tries = 10 - tries.times do |_| - begin - return @tty.read_nonblock(chars).ord - rescue Exception - sleep 0.01 - end - end - default - end - - def read_nbs - ords = [] - while ord = read_nb - ords << ord - end - ords - end - - def get_mouse - case ord = read_nb - when 32, 36, 40, 48, # mouse-down / shift / cmd / ctrl - 35, 39, 43, 51 # mouse-up / shift / cmd / ctrl - x = read_nb - 33 - y = read_nb - 33 - { :event => (ord % 2 == 0 ? :click : :release), - :x => x, :y => y, :shift => ord >= 36 } - when 96, 100, 104, 112, # scroll-up / shift / cmd / ctrl - 97, 101, 105, 113 # scroll-down / shift / cmd / ctrl - read_nb(2) - { :event => :scroll, :diff => (ord % 2 == 0 ? -1 : 1), :shift => ord >= 100 } - else - # e.g. 40, 43, 104, 105 - read_nb(2) - nil - end - end - - def get_input actions - @tty ||= - begin - require 'io/console' - IO.console - rescue LoadError - IO.open(IO.sysopen('/dev/tty'), 'r') - end - - if pending = @pending - @pending = nil - return pending - end - - str = '' - while true - ord = - if str.empty? - @tty.getc.ord - else - begin - ord = @tty.read_nonblock(1).ord - if (nb = num_unicode_bytes(ord)) > 1 - ords = [ord] - (nb - 1).times do |_| - ords << @tty.read_nonblock(1).ord - end - # UTF-8 TODO Ruby 1.8 - ords.pack('C*').force_encoding('UTF-8') - else - ord - end - rescue Exception - return str - end - end - - ord = - case read_nb(1, :esc) - when 91, 79 - case read_nb(1, nil) - when 68 then ctrl(:b) - when 67 then ctrl(:f) - when 66 then ctrl(:j) - when 65 then ctrl(:k) - when 90 then :stab - when 50 then read_nb; :ins - when 51 then read_nb; :del - when 53 then read_nb; :pgup - when 54 then read_nb; :pgdn - when 49 - case read_nbs - when [59, 50, 68] then ctrl(:a) - when [59, 50, 67] then ctrl(:e) - when [59, 53, 68] then :alt_b - when [59, 53, 67] then :alt_f - when [126] then ctrl(:a) - end - when 52 then read_nb; ctrl(:e) - when 72 then ctrl(:a) - when 70 then ctrl(:e) - when 77 - get_mouse - end - when 'b', 98 then :alt_b - when 'd', 100 then :alt_d - when 'f', 102 then :alt_f - when :esc then :esc - when 127 then :alt_bs - else next - end if ord == 27 - - return ord if ord.nil? || ord.is_a?(Hash) - - if actions.has_key?(ord) - if str.empty? - return ord - else - @pending = ord - return str - end - else - unless ord.is_a? String - ord = [ord].pack('U*') - end - str << ord if ord =~ /[[:print:]]/ - end - end - end - - class MouseEvent - DOUBLE_CLICK_INTERVAL = 0.5 - - attr_reader :v - - def initialize v = nil - @c = 0 - @v = v - @t = Time.at 0 - end - - def v= v - @c = (@v == v && within?) ? @c + 1 : 0 - @v = v - @t = Time.now - end - - def double? v - @c == 1 && @v == v && within? - end - - def within? - (Time.now - @t) < DOUBLE_CLICK_INTERVAL - end - end - - def start_loop - got = nil - begin - input = call(:@query, :dup) - cursor = input.length - yanked = '' - mouse_event = MouseEvent.new - backword = proc { - cursor = (input[0, cursor].rindex(/[^[:alnum:]][[:alnum:]]/) || -1) + 1 - nil - } - forward = proc { - cursor += (input[cursor..-1].index(/([[:alnum:]][^[:alnum:]])|(.$)/) || -1) + 1 - nil - } - rubout = proc { |regex| - pcursor = cursor - cursor = (input[0, cursor].rindex(regex) || -1) + 1 - if pcursor > cursor - yanked = input[cursor...pcursor] - input = input[0...cursor] + input[pcursor..-1] - end - } - actions = { - :esc => proc { exit 1 }, - ctrl(:d) => proc { - if input.empty? - exit 1 - elsif cursor < input.length - input = input[0...cursor] + input[(cursor + 1)..-1] - end - }, - ctrl(:m) => proc { - got = pick - exit 0 - }, - ctrl(:u) => proc { - yanked = input[0...cursor] if cursor > 0 - input = input[cursor..-1] - cursor = 0 - }, - ctrl(:a) => proc { cursor = 0; nil }, - ctrl(:e) => proc { cursor = input.length; nil }, - ctrl(:j) => proc { vselect { |v| v - @rev_dir } }, - ctrl(:k) => proc { vselect { |v| v + @rev_dir } }, - ctrl(:w) => proc { rubout.call /\s\S/ }, - ctrl(:y) => proc { actions[:default].call yanked }, - ctrl(:h) => proc { input[cursor -= 1] = '' if cursor > 0 }, - ctrl(:i) => proc { |o| - if @multi && sel = pick - sync do - if @selects.has_key? sel - @selects.delete sel - else - @selects[sel] = sel.orig - end - end - vselect { |v| v + case o - when :stab then 1 - when :sclick then 0 - else -1 - end * @rev_dir } - end - }, - ctrl(:b) => proc { cursor = [0, cursor - 1].max; nil }, - ctrl(:f) => proc { cursor = [input.length, cursor + 1].min; nil }, - ctrl(:l) => proc { render { C.clear; C.refresh }; update_list true }, - :del => proc { input[cursor] = '' if input.length > cursor }, - :pgup => proc { vselect { |v| v + @rev_dir * (max_items - 1) } }, - :pgdn => proc { vselect { |v| v - @rev_dir * (max_items - 1) } }, - :alt_bs => proc { rubout.call /[^[:alnum:]][[:alnum:]]/ }, - :alt_b => proc { backword.call }, - :alt_d => proc { - pcursor = cursor - forward.call - if cursor > pcursor - yanked = input[pcursor...cursor] - input = input[0...pcursor] + input[cursor..-1] - cursor = pcursor - end - }, - :alt_f => proc { - forward.call - }, - :default => proc { |val| - case val - when String - input.insert cursor, val - cursor += val.length - when Hash - event = val[:event] - case event - when :click, :release - x, y, shift = val.values_at :x, :y, :shift - y = @reverse ? (C.lines - 1 - y) : y - if y == C.lines - 1 - cursor = [0, [input.length, x - @prompt.length].min].max - elsif x > 1 && y <= max_items - tv = get(:@yoff) + max_items - y - 1 - - case event - when :click - vselect { |_| tv } - actions[ctrl(:i)].call(:sclick) if shift - mouse_event.v = tv - when :release - if !shift && mouse_event.double?(tv) - actions[ctrl(:m)].call - end - end - end - when :scroll - diff, shift = val.values_at :diff, :shift - actions[ctrl(:i)].call(:sclick) if shift - actions[ctrl(diff > 0 ? :j : :k)].call - end - nil - end - } - } - actions[ctrl(:p)] = actions[ctrl(:k)] - actions[ctrl(:n)] = actions[ctrl(:j)] - actions[:stab] = actions[ctrl(:i)] - actions[127] = actions[ctrl(:h)] - actions[ctrl(:q)] = actions[ctrl(:g)] = actions[ctrl(:c)] = actions[:esc] - - while true - set(:@xcur, cursor) - render { print_input } - - if key = get_input(actions) - upd = actions.fetch(key, actions[:default]).call(key) - - # Dispatch key event - emit(:key) { [set(:@query, input.dup), cursor] } if upd - end - end - ensure - C.close_screen - q, selects = geta(:@query, :@selects) - @stdout.puts q if @print_query - @stdout.puts if @expect - if got - if selects.empty? - burp got - else - selects.each do |sel, orig| - burp sel, orig - end - end - end - end - end - - class Matcher - class MatchData - def initialize n - @n = n - end - - def offset _ - @n - end - end - - def initialize nth, delim - @nth = nth - @delim = delim - @tokens_cache = {} - end - - def tokenize str - @tokens_cache[str] ||= str.tokenize(@delim, @nth) - end - - def do_match str, pat - if @nth - tokenize(str).each do |pair| - prefix_length, token = pair - if md = token.match(pat) rescue nil - return MatchData.new(md.offset(0).map { |o| o + prefix_length }) - end - end - nil - else - str.match(pat) rescue nil - end - end - end - - class FuzzyMatcher < Matcher - attr_reader :caches, :rxflag - - def initialize rxflag, nth = nil, delim = nil - super nth, delim - @caches = Hash.new { |h, k| h[k] = {} } - @regexp = {} - @rxflag = rxflag - end - - def empty? q - q.empty? - end - - def rxflag_for q - @rxflag || (q =~ /[A-Z]/ ? 0 : Regexp::IGNORECASE) - end - - def fuzzy_regex q - @regexp[q] ||= begin - q = q.downcase if @rxflag == Regexp::IGNORECASE - Regexp.new(q.split(//).inject('') { |sum, e| - e = Regexp.escape e - sum << (e.length > 1 ? "(?:#{e}).*?" : # FIXME: not equivalent - "#{e}[^#{e}]*?") - }, rxflag_for(q)) - end - end - - def match list, q, prefix, suffix - regexp = fuzzy_regex q - - cache = @caches[list.object_id] - prefix_cache = nil - (prefix.length - 1).downto(1) do |len| - break if prefix_cache = cache[prefix[0, len]] - end - - suffix_cache = nil - 0.upto(suffix.length - 1) do |idx| - break if suffix_cache = cache[suffix[idx..-1]] - end unless suffix.empty? - - partial_cache = [prefix_cache, - suffix_cache].compact.sort_by { |e| e.length }.first - cache[q] ||= (partial_cache ? - partial_cache.map { |e| e.first } : list).map { |line| - # Ignore errors: e.g. invalid byte sequence in UTF-8 - md = do_match(line, regexp) - md && [line, [md.offset(0)]] - }.compact - end - end - - class ExtendedFuzzyMatcher < FuzzyMatcher - def initialize rxflag, mode = :fuzzy, nth = nil, delim = nil - super rxflag, nth, delim - @regexps = {} - @mode = mode - end - - def empty? q - parse(q).empty? - end - - def parse q - q = q.strip - @regexps[q] ||= q.split(/\s+/).map { |w| - invert = - if w =~ /^!/ - w = w[1..-1] - true - end - - [ @regexp[w] ||= - case w - when '' - nil - when /^\^(.*)\$$/ - Regexp.new('^' << Regexp.escape($1) << '$', rxflag_for(w)) - when /^'/ - if @mode == :fuzzy && w.length > 1 - exact_regex w[1..-1] - elsif @mode == :exact - exact_regex w - end - when /^\^/ - w.length > 1 ? - Regexp.new('^' << Regexp.escape(w[1..-1]), rxflag_for(w)) : nil - when /\$$/ - w.length > 1 ? - Regexp.new(Regexp.escape(w[0..-2]) << '$', rxflag_for(w)) : nil - else - @mode == :fuzzy ? fuzzy_regex(w) : exact_regex(w) - end, invert ] - }.select { |pair| pair.first } - end - - def exact_regex w - Regexp.new(Regexp.escape(w), rxflag_for(w)) - end - - def match list, q, prefix, suffix - regexps = parse q - # Look for prefix cache - cache = @caches[list.object_id] - prefix = prefix.strip.sub(/\$\S*$/, '').sub(/(^|\s)!\S*$/, '') - prefix_cache = nil - (prefix.length - 1).downto(1) do |len| - break if prefix_cache = cache[Set[@regexps[prefix[0, len]]]] - end - - cache[Set[regexps]] ||= (prefix_cache ? - prefix_cache.map { |e| e.first } : - list).map { |line| - offsets = [] - regexps.all? { |pair| - regexp, invert = pair - md = do_match(line, regexp) - if md && !invert - offsets << md.offset(0) - elsif !md && invert - true - end - } && [line, offsets] - }.select { |e| e } - end - end -end#FZF - -FZF.new(ARGV, $stdin).start if ENV.fetch('FZF_EXECUTABLE', '1') == '1' - diff --git a/install b/install index 3d07565..96c94d2 100755 --- a/install +++ b/install @@ -158,80 +158,9 @@ case "$archi" in FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386} ;; OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64} ;; OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386} ;; - *) binary_available=0 binary_error=1 ;; + *) binary_available=0 binary_error=1 ;; esac -install_ruby_fzf() { - if [ -z "$allow_legacy" ]; then - ask "Do you want to install legacy Ruby version instead?" && exit 1 - fi - echo "Installing legacy Ruby version ..." - - # ruby executable - echo -n "Checking Ruby executable ... " - ruby=$(command -v ruby) - if [ $? -ne 0 ]; then - echo "ruby executable not found !!!" - exit 1 - fi - - # System ruby is preferred - system_ruby=/usr/bin/ruby - if [ -x $system_ruby ] && [ $system_ruby != "$ruby" ]; then - $system_ruby --disable-gems -rcurses -e0 2> /dev/null - [ $? -eq 0 ] && ruby=$system_ruby - fi - - echo "OK ($ruby)" - - # Curses-support - echo -n "Checking Curses support ... " - "$ruby" -rcurses -e0 2> /dev/null - if [ $? -eq 0 ]; then - echo "OK" - else - echo "Not found" - echo "Installing 'curses' gem ... " - if (( EUID )); then - /usr/bin/env gem install curses --user-install - else - /usr/bin/env gem install curses - fi - if [ $? -ne 0 ]; then - echo - echo "Failed to install 'curses' gem." - if [[ $(uname -r) =~ 'ARCH' ]]; then - echo "Make sure that base-devel package group is installed." - fi - exit 1 - fi - fi - - # Ruby version - echo -n "Checking Ruby version ... " - "$ruby" -e 'exit RUBY_VERSION >= "1.9"' - if [ $? -eq 0 ]; then - echo ">= 1.9" - "$ruby" --disable-gems -rcurses -e0 2> /dev/null - if [ $? -eq 0 ]; then - fzf_cmd="$ruby --disable-gems $fzf_base/fzf" - else - fzf_cmd="$ruby $fzf_base/fzf" - fi - else - echo "< 1.9" - fzf_cmd="$ruby $fzf_base/fzf" - fi - - # Create fzf script - echo -n "Creating wrapper script for fzf ... " - rm -f "$fzf_base"/bin/fzf - echo "#!/bin/sh" > "$fzf_base"/bin/fzf - echo "$fzf_cmd \"\$@\"" >> "$fzf_base"/bin/fzf - chmod +x "$fzf_base"/bin/fzf - echo "OK" -} - cd "$fzf_base" if [ -n "$binary_error" ]; then if [ $binary_available -eq 0 ]; then @@ -249,12 +178,12 @@ if [ -n "$binary_error" ]; then echo "OK" cp "$GOPATH/bin/fzf" "$fzf_base/bin/" else - echo "Failed to build binary ..." - install_ruby_fzf + echo "Failed to build binary. Installation failed." + exit 1 fi else - echo "go executable not found. Cannot build binary ..." - install_ruby_fzf + echo "go executable not found. Installation failed." + exit 1 fi fi From 5bb18b644178c4075d81eb6b85af2ecc5104d55a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 6 Feb 2017 21:15:29 +0900 Subject: [PATCH 430/658] Remove Dockerfiles and clean up Makefile Due to the recent removal of ncurses dependency, we can cross-compile binaries for different platforms without virtual machines. --- src/Dockerfile.android | 40 ---------------------------------- src/Dockerfile.arch | 24 --------------------- src/Dockerfile.centos | 32 --------------------------- src/Dockerfile.ubuntu | 22 ------------------- src/Makefile | 49 +++--------------------------------------- 5 files changed, 3 insertions(+), 164 deletions(-) delete mode 100644 src/Dockerfile.android delete mode 100644 src/Dockerfile.arch delete mode 100644 src/Dockerfile.centos delete mode 100644 src/Dockerfile.ubuntu diff --git a/src/Dockerfile.android b/src/Dockerfile.android deleted file mode 100644 index d6a0ba4..0000000 --- a/src/Dockerfile.android +++ /dev/null @@ -1,40 +0,0 @@ -FROM ubuntu:14.04 -MAINTAINER Junegunn Choi - -# apt-get -RUN apt-get update && apt-get -y upgrade && \ - apt-get install -y --force-yes git curl build-essential - -# Install Go 1.4 -RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ - tar -xz && mv go go1.4 && \ - sed -i 's@#define PTHREAD_KEYS_MAX 128@@' /go1.4/src/runtime/cgo/gcc_android_arm.c - -ENV GOROOT /go1.4 -ENV PATH /go1.4/bin:$PATH - -RUN cd / && \ - curl -O http://dl.google.com/android/ndk/android-ndk-r10e-linux-x86_64.bin && \ - chmod 755 /android-ndk* && /android-ndk-r10e-linux-x86_64.bin && \ - mv android-ndk-r10e /android-ndk - -RUN cd /android-ndk && bash ./build/tools/make-standalone-toolchain.sh --platform=android-21 --install-dir=/ndk --arch=arm - -ENV NDK_CC /ndk/bin/arm-linux-androideabi-gcc - -RUN cd $GOROOT/src && \ - CC_FOR_TARGET=$NDK_CC GOOS=android GOARCH=arm GOARM=7 ./make.bash - -RUN cd / && curl \ - http://ftp.gnu.org/gnu/ncurses/ncurses-5.9.tar.gz | \ - tar -xz && cd /ncurses-5.9 && \ - ./configure CC=$NDK_CC CFLAGS="-fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch" LDFLAGS="-march=armv7-a -Wl,--no-warn-mismatch" --host=arm-linux --enable-overwrite --enable-const --without-cxx-binding --without-shared --without-debug --enable-widec --enable-ext-colors --enable-ext-mouse --enable-pc-files --with-pkg-config-libdir=$PKG_CONFIG_LIBDIR --without-manpages --without-ada --disable-shared --without-tests --prefix=/ndk/sysroot/usr --with-default-terminfo-dirs=/usr/share/terminfo --with-terminfo-dirs=/usr/share/terminfo ac_cv_header_locale_h=n ac_cv_func_getpwent=no ac_cv_func_getpwnam=no ac_cv_func_getpwuid=no && \ - sed -i 's@#define HAVE_LOCALE_H 1@/* #undef HAVE_LOCALE_H */@' include/ncurses_cfg.h && \ - make && \ - sed -i '0,/echo.*/{s/echo.*/exit 0/}' misc/run_tic.sh && \ - make install && \ - mv /ndk/sysroot/usr/lib/libncursesw.a /ndk/sysroot/usr/lib/libncurses.a - -# Default CMD -CMD cd /fzf/src && /bin/bash diff --git a/src/Dockerfile.arch b/src/Dockerfile.arch deleted file mode 100644 index 77df9e4..0000000 --- a/src/Dockerfile.arch +++ /dev/null @@ -1,24 +0,0 @@ -FROM base/archlinux:2014.07.03 -MAINTAINER Junegunn Choi - -# apt-get -RUN pacman-key --populate archlinux && pacman-key --refresh-keys -RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git - -# Install Go 1.4 -RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ - tar -xz && mv go go1.4 - -ENV GOROOT /go1.4 -ENV PATH /go1.4/bin:$PATH - -# For i386 build -RUN echo '[multilib]' >> /etc/pacman.conf && \ - echo 'Include = /etc/pacman.d/mirrorlist' >> /etc/pacman.conf && \ - pacman-db-upgrade && yes | pacman -Sy gcc-multilib lib32-ncurses && \ - cd $GOROOT/src && GOARCH=386 ./make.bash - -# Default CMD -CMD cd /fzf/src && /bin/bash - diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos deleted file mode 100644 index 4ae27b7..0000000 --- a/src/Dockerfile.centos +++ /dev/null @@ -1,32 +0,0 @@ -FROM centos:centos6 -MAINTAINER Junegunn Choi - -# yum -RUN yum install -y git gcc make tar glibc-devel glibc-devel.i686 \ - ncurses-devel ncurses-static ncurses-devel.i686 \ - gpm-devel gpm-static libgcc.i686 - -# Install Go 1.4 -RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ - tar -xz && mv go go1.4 - -# Install Go 1.7 -RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.7.linux-amd64.tar.gz | \ - tar -xz && mv go go1.7 - -# Install RPMs for building static 32-bit binary -RUN curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.8/os/i386/Packages/ncurses-static-5.7-4.20090207.el6.i686.rpm -o rpm && rpm -i rpm && \ - curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.8/os/i386/Packages/gpm-static-1.20.6-12.el6.i686.rpm -o rpm && rpm -i rpm - -ENV GOROOT_BOOTSTRAP /go1.4 -ENV GOROOT /go1.7 -ENV PATH /go1.7/bin:$PATH - -# For i386 build -RUN cd $GOROOT/src && GOARCH=386 ./make.bash - -# Default CMD -CMD cd /fzf/src && /bin/bash - diff --git a/src/Dockerfile.ubuntu b/src/Dockerfile.ubuntu deleted file mode 100644 index b0ae875..0000000 --- a/src/Dockerfile.ubuntu +++ /dev/null @@ -1,22 +0,0 @@ -FROM ubuntu:14.04 -MAINTAINER Junegunn Choi - -# apt-get -RUN apt-get update && apt-get -y upgrade && \ - apt-get install -y --force-yes git curl build-essential libncurses-dev libgpm-dev - -# Install Go 1.4 -RUN cd / && curl \ - https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \ - tar -xz && mv go go1.4 - -ENV GOROOT /go1.4 -ENV PATH /go1.4/bin:$PATH - -# For i386 build -RUN apt-get install -y lib32ncurses5-dev && \ - cd $GOROOT/src && GOARCH=386 ./make.bash - -# Default CMD -CMD cd /fzf/src && /bin/bash - diff --git a/src/Makefile b/src/Makefile index 102229b..b8c3090 100644 --- a/src/Makefile +++ b/src/Makefile @@ -4,6 +4,8 @@ ifeq ($(UNAME_S),Darwin) GOOS := darwin else ifeq ($(UNAME_S),Linux) GOOS := linux +else +$(error "$$GOOS is not defined.") endif endif @@ -12,7 +14,6 @@ ROOTDIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) BINDIR := $(shell dirname $(ROOTDIR))/bin GOPATH := $(shell dirname $(ROOTDIR))/gopath SRCDIR := $(GOPATH)/src/github.com/junegunn/fzf/src -DOCKEROPTS := -i -t -v $(ROOTDIR):/fzf/src BINARY32 := fzf-$(GOOS)_386 BINARY64 := fzf-$(GOOS)_amd64 BINARYARM5 := fzf-$(GOOS)_arm5 @@ -86,12 +87,6 @@ deps: $(SRCDIR) $(SOURCES) cd $(SRCDIR) && go get -tags "$(TAGS)" ./deps -android-build: $(SRCDIR) - cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get - cd $(SRCDIR)/fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-w -extldflags=-pie" -o $(BINARYARM7) - cd $(SRCDIR)/fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) && \ - rm -f $(RELEASEARM7) - test: deps SHELL=/bin/sh GOOS= go test -v -tags "$(TAGS)" ./... @@ -129,42 +124,4 @@ $(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR) $(BINDIR): mkdir -p $@ -docker-arch: - docker build -t junegunn/arch-sandbox - < Dockerfile.arch - -docker-ubuntu: - docker build -t junegunn/ubuntu-sandbox - < Dockerfile.ubuntu - -docker-centos: - docker build -t junegunn/centos-sandbox - < Dockerfile.centos - -docker-android: - docker build -t junegunn/android-sandbox - < Dockerfile.android - -arch: docker-arch - docker run $(DOCKEROPTS) junegunn/$@-sandbox \ - sh -c 'cd /fzf/src; /bin/bash' - -ubuntu: docker-ubuntu - docker run $(DOCKEROPTS) junegunn/$@-sandbox \ - sh -c 'cd /fzf/src; /bin/bash' - -centos: docker-centos - docker run $(DOCKEROPTS) junegunn/$@-sandbox \ - sh -c 'cd /fzf/src; /bin/bash' - -linux: docker-centos - docker run $(DOCKEROPTS) junegunn/centos-sandbox \ - /bin/bash -ci 'cd /fzf/src; make TAGS=static release' - -ubuntu-android: docker-android - docker run $(DOCKEROPTS) junegunn/android-sandbox \ - sh -c 'cd /fzf/src; /bin/bash' - -android: docker-android - docker run $(DOCKEROPTS) junegunn/android-sandbox \ - /bin/bash -ci 'cd /fzf/src; GOOS=android make android-build' - -.PHONY: all deps release test install uninstall clean \ - linux arch ubuntu centos docker-arch docker-ubuntu docker-centos \ - android-build docker-android ubuntu-android android +.PHONY: all deps release release-all test install uninstall clean From a847fe875413ac0d35815d67fea3e03a6c0cb2ff Mon Sep 17 00:00:00 2001 From: Prabir Shrestha Date: Mon, 6 Feb 2017 21:42:08 -0800 Subject: [PATCH 431/658] Use "type" instead of "cat" on windows (#836) --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 5d1ed2f..1225cec 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -304,7 +304,7 @@ try elseif type == 3 let temps.input = tempname() call writefile(source, temps.input) - let prefix = 'cat '.s:shellesc(temps.input).'|' + let prefix = (s:is_win ? 'type ' : 'cat ').s:shellesc(temps.input).'|' else throw 'invalid source type' endif From 1ba7acf4bd4314f299b57cea73cbd376bde5aba3 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 8 Feb 2017 14:55:17 +0900 Subject: [PATCH 432/658] [fzf-tmux] Fix race condition when using -l/-u on zoomed panes Using a dummy command that works as the barrier. --- bin/fzf-tmux | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index f201199..9561484 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -138,6 +138,7 @@ cleanup() { # Remove temp window if we were zoomed if [[ -n "$zoomed" ]]; then + tmux display-message -p "#{window_id}" > /dev/null tmux swap-pane -t $original_window \; \ select-window -t $original_window \; \ kill-window -t $tmp_window \; \ From afa25d8c57f8ffc1ba54623360456baa9321abd7 Mon Sep 17 00:00:00 2001 From: Daniel Gray Date: Wed, 8 Feb 2017 23:05:02 -0600 Subject: [PATCH 433/658] [zsh] Do not cd when cancelling alt+c keybind (#840) --- shell/key-bindings.zsh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index abf7559..6ac38d2 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -41,7 +41,12 @@ fzf-cd-widget() { local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" setopt localoptions pipefail 2> /dev/null - cd "${$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m):-.}" + local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)" + if [[ -z "$dir" ]]; then + zle redisplay + return 0 + fi + cd "$dir" local ret=$? zle reset-prompt typeset -f zle-line-init >/dev/null && zle zle-line-init From 19569bd5c5e189d9fbc87ab3655f005baf73e4d7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 14 Feb 2017 22:28:04 +0900 Subject: [PATCH 434/658] Move cursor to the top-left when returning to alternate screen Fix broken preview border. Reported by Thomas Sattler. fzf --bind 'enter:execute(date)' --preview=date --reverse --- src/tui/light.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tui/light.go b/src/tui/light.go index 9465c49..a53e287 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -552,6 +552,9 @@ func (r *LightRenderer) Resume() bool { } func (r *LightRenderer) Clear() { + if r.fullscreen { + r.csi("H") + } // r.csi("u") r.origin() r.csi("J") From 21da02fac2a6f8fd04052165efa52f8c11399bb7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 14 Feb 2017 22:30:09 +0900 Subject: [PATCH 435/658] Fix indentation --- src/tui/light.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index a53e287..37fcc12 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -552,9 +552,9 @@ func (r *LightRenderer) Resume() bool { } func (r *LightRenderer) Clear() { - if r.fullscreen { - r.csi("H") - } + if r.fullscreen { + r.csi("H") + } // r.csi("u") r.origin() r.csi("J") From ce43ea9f4273ef2b4c67cb02dc55ed883f6c0fd9 Mon Sep 17 00:00:00 2001 From: Pierre Neidhardt Date: Thu, 16 Feb 2017 09:18:01 +0100 Subject: [PATCH 436/658] [shell] Replace `sed` with `-mindepth 1` and `cut` (#844) --- shell/key-bindings.bash | 4 ++-- shell/key-bindings.fish | 8 ++++---- shell/key-bindings.zsh | 5 ++--- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 69dd0ce..b150533 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -46,8 +46,8 @@ fzf-file-widget() { __fzf_cd__() { local cmd dir - cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ - -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" + cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ + -o -type d -print 2> /dev/null | cut -b3-"}" dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir" } diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index ce2d4e0..6b239ba 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -2,7 +2,7 @@ # ------------ function fzf_key_bindings - # Store last token in $dir as root for the 'find' command + # Store current token in $dir as root for the 'find' command function fzf-file-widget -d "List files and folders" set -l dir (commandline -t) # The commandline token might be escaped, we need to unescape it. @@ -19,7 +19,7 @@ function fzf_key_bindings command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ -o -type f -print \ -o -type d -print \ - -o -type l -print 2> /dev/null | sed 's#^\./##'" + -o -type l -print 2> /dev/null | cut -b3-" set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% begin @@ -54,8 +54,8 @@ function fzf_key_bindings function fzf-cd-widget -d "Change directory" set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND " - command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ - -o -type d -print 2> /dev/null | sed 1d | cut -b3-" + command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ + -o -type d -print 2> /dev/null | cut -b3-" set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% begin set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 6ac38d2..a04e1d8 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -38,8 +38,8 @@ bindkey '^T' fzf-file-widget # ALT-C - cd into the selected directory fzf-cd-widget() { - local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ - -o -type d -print 2> /dev/null | sed 1d | cut -b3-"}" + local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ + -o -type d -print 2> /dev/null | cut -b3-"}" setopt localoptions pipefail 2> /dev/null local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)" if [[ -z "$dir" ]]; then @@ -76,4 +76,3 @@ zle -N fzf-history-widget bindkey '^R' fzf-history-widget fi - From fc274c2ba4682198aca68d0334a5445f269aba92 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 17 Feb 2017 10:20:39 +0900 Subject: [PATCH 437/658] [vim] Do not escape % when using system() instead of ! Close https://github.com/junegunn/fzf.vim/issues/309 --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 1225cec..9c14f7f 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -445,7 +445,7 @@ function! s:execute(dict, command, use_height, temps) abort endif let command = printf(fmt, escaped) else - let command = escaped + let command = a:use_height ? a:command : escaped endif if a:use_height let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty' From 3d74d277aa0446264240308e52df6f223cc2a8ed Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 17 Feb 2017 13:07:45 +0900 Subject: [PATCH 438/658] Use cut instead of sed in the default command --- src/constants_unix.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants_unix.go b/src/constants_unix.go index 05f66d0..2dad4d4 100644 --- a/src/constants_unix.go +++ b/src/constants_unix.go @@ -4,5 +4,5 @@ package fzf const ( // Reader - defaultCommand = `find -L . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//` + defaultCommand = `find -L . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-` ) From 9526594905916310299bd50f6d910eecf85cede5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 18 Feb 2017 18:16:34 +0900 Subject: [PATCH 439/658] [vim] Fix FZF_DEFAULT_COMMAND on Windows Close #847. Patch submitted by @wontoncc. --- plugin/fzf.vim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 9c14f7f..81c54bc 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -291,9 +291,9 @@ try endtry if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) - let temps.source = tempname() + let temps.source = tempname().(s:is_win ? '.bat' : '') call writefile(split($FZF_DEFAULT_COMMAND, "\n"), temps.source) - let dict.source = (empty($SHELL) ? &shell : $SHELL) . ' ' . s:shellesc(temps.source) + let dict.source = (empty($SHELL) ? &shell : $SHELL) . (s:is_win ? ' /c ' : ' ') . s:shellesc(temps.source) endif if has_key(dict, 'source') From c2db67c1c086f0bbebf4802abb3fb9335ee46662 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 18 Feb 2017 21:58:03 +0900 Subject: [PATCH 440/658] [vim] Prepend @echo off to $FZF_DEFAULT_COMMAND on Windows (#847) --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 81c54bc..ba2414e 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -292,7 +292,7 @@ try if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) let temps.source = tempname().(s:is_win ? '.bat' : '') - call writefile(split($FZF_DEFAULT_COMMAND, "\n"), temps.source) + call writefile((s:is_win ? ['@echo off'] : []) + split($FZF_DEFAULT_COMMAND, "\n"), temps.source) let dict.source = (empty($SHELL) ? &shell : $SHELL) . (s:is_win ? ' /c ' : ' ') . s:shellesc(temps.source) endif From 309e1d8619f5a31eec797ab93c89702bb2c71f78 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 18 Feb 2017 23:17:29 +0900 Subject: [PATCH 441/658] Properly truncate long query string --- src/constants.go | 7 +++---- src/terminal.go | 26 +++++++++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/constants.go b/src/constants.go index d5246db..cf91ce5 100644 --- a/src/constants.go +++ b/src/constants.go @@ -18,10 +18,9 @@ const ( readerBufferSize = 64 * 1024 // Terminal - initialDelay = 20 * time.Millisecond - initialDelayTac = 100 * time.Millisecond - spinnerDuration = 200 * time.Millisecond - maxPatternLength = 100 + initialDelay = 20 * time.Millisecond + initialDelayTac = 100 * time.Millisecond + spinnerDuration = 200 * time.Millisecond // Matcher numPartitionsMultiplier = 8 diff --git a/src/terminal.go b/src/terminal.go index 5853022..f75480e 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -603,6 +603,7 @@ func (t *Terminal) resizeWindows() { t.window.MoveAndClear(i, 0) } } + t.truncateQuery() } func (t *Terminal) move(y int, x int, clear bool) { @@ -628,13 +629,19 @@ func (t *Terminal) printPrompt() { } func (t *Terminal) printInfo() { + pos := 0 if t.inlineInfo { - t.move(0, t.displayWidth([]rune(t.prompt))+t.displayWidth(t.input)+1, true) + pos = t.displayWidth([]rune(t.prompt)) + t.displayWidth(t.input) + 1 + if pos+len(" < ") > t.window.Width() { + return + } + t.move(0, pos, true) if t.reading { t.window.CPrint(tui.ColSpinner, t.strong, " < ") } else { t.window.CPrint(tui.ColPrompt, t.strong, " < ") } + pos += len(" < ") } else { t.move(1, 0, true) if t.reading { @@ -643,6 +650,7 @@ func (t *Terminal) printInfo() { t.window.CPrint(tui.ColSpinner, t.strong, _spinner[idx]) } t.move(1, 2, false) + pos = 2 } output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count) @@ -659,7 +667,9 @@ func (t *Terminal) printInfo() { if t.progress > 0 && t.progress < 100 { output += fmt.Sprintf(" (%d%%)", t.progress) } - t.window.CPrint(tui.ColInfo, 0, output) + if pos+len(output) <= t.window.Width() { + t.window.CPrint(tui.ColInfo, 0, output) + } } func (t *Terminal) printHeader() { @@ -1210,6 +1220,12 @@ func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item return true, sels } +func (t *Terminal) truncateQuery() { + maxPatternLength := util.Max(1, t.window.Width()-t.displayWidth([]rune(t.prompt))-1) + t.input, _ = t.trimRight(t.input, maxPatternLength) + t.cx = util.Constrain(t.cx, 0, len(t.input)) +} + // Loop is called to start Terminal I/O func (t *Terminal) Loop() { // prof := profile.Start(profile.ProfilePath("/tmp/")) @@ -1688,11 +1704,7 @@ func (t *Terminal) Loop() { if !doActions(actions, mapkey) { continue } - // Truncate the query if it's too long - if len(t.input) > maxPatternLength { - t.input = t.input[:maxPatternLength] - t.cx = util.Constrain(t.cx, 0, maxPatternLength) - } + t.truncateQuery() changed = string(previousInput) != string(t.input) } else { if mapkey == tui.Rune { From c82fb3c9b907043a515dae7fdcc0e30b4c8b7cd1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 18 Feb 2017 23:49:00 +0900 Subject: [PATCH 442/658] Add toggle-preview-wrap action --- CHANGELOG.md | 5 +++++ man/man1/fzf.1 | 1 + src/options.go | 2 ++ src/terminal.go | 6 ++++++ src/tui/light.go | 2 +- 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ea9ee2..6ed3a70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +0.16.5 +------ +- Minor bug fixes +- Added `toggle-preview-wrap` action + 0.16.4 ------ - Added `--border` option to draw border above and below the finder diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 3d33d43..03136c1 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -500,6 +500,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR) \fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR) \fBtoggle-preview\fR + \fBtoggle-preview-wrap\fR \fBtoggle-sort\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR \fBunix-line-discard\fR \fIctrl-u\fR diff --git a/src/options.go b/src/options.go index 30cc751..c78d2bf 100644 --- a/src/options.go +++ b/src/options.go @@ -723,6 +723,8 @@ func parseKeymap(keymap map[int][]action, str string) { appendAction(actNextHistory) case "toggle-preview": appendAction(actTogglePreview) + case "toggle-preview-wrap": + appendAction(actTogglePreviewWrap) case "toggle-sort": appendAction(actToggleSort) case "preview-up": diff --git a/src/terminal.go b/src/terminal.go index f75480e..d056c1a 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -199,6 +199,7 @@ const ( actPrintQuery actToggleSort actTogglePreview + actTogglePreviewWrap actPreviewUp actPreviewDown actPreviewPageUp @@ -1459,6 +1460,11 @@ func (t *Terminal) Loop() { } req(reqList, reqInfo, reqHeader) } + case actTogglePreviewWrap: + if t.hasPreviewWindow() { + t.preview.wrap = !t.preview.wrap + req(reqPreviewRefresh) + } case actToggleSort: t.sort = !t.sort t.eventBox.Set(EvtSearchNew, t.sort) diff --git a/src/tui/light.go b/src/tui/light.go index 37fcc12..fb8f4f0 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -596,7 +596,7 @@ func (r *LightRenderer) MaxY() int { } func (r *LightRenderer) DoesAutoWrap() bool { - return true + return false } func (r *LightRenderer) IsOptimized() bool { From 11407bf65650ef5bd97453e16ab3424b90ef750e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 19 Feb 2017 01:33:13 +0900 Subject: [PATCH 443/658] Exclude sysfs in find commands --- shell/key-bindings.bash | 4 ++-- shell/key-bindings.fish | 4 ++-- shell/key-bindings.zsh | 4 ++-- src/constants_unix.go | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index b150533..5a68bc1 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -1,7 +1,7 @@ # Key bindings # ------------ __fzf_select__() { - local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ + local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | cut -b3-"}" @@ -46,7 +46,7 @@ fzf-file-widget() { __fzf_cd__() { local cmd dir - cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ + cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | cut -b3-"}" dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir" } diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index 6b239ba..c35dac7 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -16,7 +16,7 @@ function fzf_key_bindings # "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not # $dir itself, even if hidden. set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND " - command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ + command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | cut -b3-" @@ -54,7 +54,7 @@ function fzf_key_bindings function fzf-cd-widget -d "Change directory" set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND " - command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ + command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ -o -type d -print 2> /dev/null | cut -b3-" set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% begin diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index a04e1d8..9b2353a 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -4,7 +4,7 @@ if [[ $- == *i* ]]; then # CTRL-T - Paste the selected file path(s) into the command line __fsel() { - local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ + local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type f -print \ -o -type d -print \ -o -type l -print 2> /dev/null | cut -b3-"}" @@ -38,7 +38,7 @@ bindkey '^T' fzf-file-widget # ALT-C - cd into the selected directory fzf-cd-widget() { - local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ + local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | cut -b3-"}" setopt localoptions pipefail 2> /dev/null local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)" diff --git a/src/constants_unix.go b/src/constants_unix.go index 2dad4d4..94371e6 100644 --- a/src/constants_unix.go +++ b/src/constants_unix.go @@ -4,5 +4,5 @@ package fzf const ( // Reader - defaultCommand = `find -L . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-` + defaultCommand = `command find -L . -mindepth 1 \( -path '*/\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-` ) From e87a85a1791877b6850298d7def7a253540a891c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 19 Feb 2017 01:40:25 +0900 Subject: [PATCH 444/658] 0.16.5 --- CHANGELOG.md | 1 + install | 2 +- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ed3a70..f9de7fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ------ - Minor bug fixes - Added `toggle-preview-wrap` action +- Built with Go 1.8 0.16.4 ------ diff --git a/install b/install index 96c94d2..de16b18 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.16.4 +version=0.16.5 auto_completion= key_bindings= update_config=2 diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 055dab3..7463aa3 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 2017" "fzf 0.16.4" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Feb 2017" "fzf 0.16.5" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 03136c1..59710d5 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 2017" "fzf 0.16.4" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Feb 2017" "fzf 0.16.5" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index cf91ce5..38756e9 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.16.4" + version = "0.16.5" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 76d3f6d2489951929b06fcab81b961c7e92425bb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 19 Feb 2017 20:47:44 +0900 Subject: [PATCH 445/658] [vim] Escape ! when using :! to execute command - call fzf#run({'source': "echo '!'"}) - call fzf#run({'source': "echo '!'", 'down': '40%'}) Close https://github.com/junegunn/fzf.vim/issues/315 --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index ba2414e..526c6e9 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -451,7 +451,7 @@ function! s:execute(dict, command, use_height, temps) abort let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty' call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin)) else - execute 'silent !'.command + execute 'silent !'.escape(command, '!') endif let exit_status = v:shell_error redraw! From b29375c844744fb45183bdbbb6d1ff854cf23b7a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 19 Feb 2017 20:53:12 +0900 Subject: [PATCH 446/658] [vim] Minor refactoring --- plugin/fzf.vim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 526c6e9..d4e4d26 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -436,7 +436,7 @@ function! s:execute(dict, command, use_height, temps) abort if has('unix') && !a:use_height silent! !clear 2> /dev/null endif - let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#') + let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#!') if has('gui_running') let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher))) let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher @@ -451,7 +451,7 @@ function! s:execute(dict, command, use_height, temps) abort let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty' call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin)) else - execute 'silent !'.escape(command, '!') + execute 'silent !'.command endif let exit_status = v:shell_error redraw! From 0b940e4b2ba2eb2b00541a870f9b1a23c15c9bce Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 24 Feb 2017 02:30:11 +0900 Subject: [PATCH 447/658] Redraw item if query string has changed --- src/terminal.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index d056c1a..4bff1b1 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -46,6 +46,7 @@ type itemLine struct { current bool selected bool label string + queryLen int width int result Result } @@ -737,11 +738,13 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) { } // Avoid unnecessary redraw - newLine := itemLine{current: current, selected: selected, label: label, result: *result, width: 0} + newLine := itemLine{current: current, selected: selected, label: label, + result: *result, queryLen: len(t.input), width: 0} prevLine := t.prevLines[i] if prevLine.current == newLine.current && prevLine.selected == newLine.selected && prevLine.label == newLine.label && + prevLine.queryLen == newLine.queryLen && prevLine.result == newLine.result { return } From cd59e5d07b3a69a75f7c0d02efd66b6f04bb13a1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 25 Feb 2017 23:52:56 +0900 Subject: [PATCH 448/658] [neovim] Set 'dir' to the current direcotry Close https://github.com/junegunn/fzf.vim/issues/308 --- plugin/fzf.vim | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index d4e4d26..932efde 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -290,6 +290,10 @@ try throw v:exception endtry + if has('nvim') && !has_key(dict, 'dir') + let dict.dir = getcwd() + endif + if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) let temps.source = tempname().(s:is_win ? '.bat' : '') call writefile((s:is_win ? ['@echo off'] : []) + split($FZF_DEFAULT_COMMAND, "\n"), temps.source) From c89ac341e4d9199ff0ca6ca7af29d9b14a1b7fbd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 1 Mar 2017 16:00:08 +0900 Subject: [PATCH 449/658] Clear background even if background color is not set This is needed when fzf is started from inside a program (e.g. Vim) and it uses a different background color than the terminal. - https://github.com/junegunn/fzf.vim/issues/325 - https://github.com/junegunn/fzf.vim/issues/300 --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 4bff1b1..29f2c9d 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -600,7 +600,7 @@ func (t *Terminal) resizeWindows() { width, height, tui.BorderNone) } - if !t.tui.IsOptimized() && t.theme != nil && t.theme.HasBg() { + if !t.tui.IsOptimized() { for i := 0; i < t.window.Height(); i++ { t.window.MoveAndClear(i, 0) } From 3a6af27586c65bb9243c5af7c43a40619503db52 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 1 Mar 2017 16:07:04 +0900 Subject: [PATCH 450/658] [zsh] emulate -L zsh to avoid issues with incompatible options Close #858 --- shell/completion.zsh | 4 ++-- shell/key-bindings.zsh | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 9b9dba3..ccd18ce 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -46,7 +46,7 @@ __fzf_generic_path_completion() { tail=$6 fzf="$(__fzfcmd_complete)" - setopt localoptions nonomatch + setopt nonomatch dir="$base" while [ 1 ]; do if [[ -z "$dir" || -d ${~dir} ]]; then @@ -142,8 +142,8 @@ _fzf_complete_unalias() { } fzf-completion() { + emulate -L zsh local tokens cmd prefix trigger tail fzf matches lbuf d_cmds - setopt localoptions noshwordsplit noksh_arrays # http://zsh.sourceforge.net/FAQ/zshfaq03.html # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 9b2353a..dbafafb 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -27,6 +27,7 @@ __fzfcmd() { } fzf-file-widget() { + emulate -L zsh LBUFFER="${LBUFFER}$(__fsel)" local ret=$? zle redisplay @@ -38,9 +39,10 @@ bindkey '^T' fzf-file-widget # ALT-C - cd into the selected directory fzf-cd-widget() { + emulate -L zsh + setopt pipefail 2> /dev/null local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | cut -b3-"}" - setopt localoptions pipefail 2> /dev/null local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)" if [[ -z "$dir" ]]; then zle redisplay @@ -57,8 +59,9 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { + emulate -L zsh + setopt pipefail 2> /dev/null local selected num - setopt localoptions noglobsubst pipefail 2> /dev/null selected=( $(fc -l 1 | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) ) local ret=$? From 972fb1a29ddd484c97342cf0e7399bd904ded58d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 2 Mar 2017 12:49:51 +0900 Subject: [PATCH 451/658] Suppress ANSI colors in preview window if --no-color is set --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 29f2c9d..c7c21c4 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -958,7 +958,7 @@ func (t *Terminal) printPreview() { trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X()) } str, _ = t.processTabs(trimmed, 0) - if ansi != nil && ansi.colored() { + if t.theme != nil && ansi != nil && ansi.colored() { fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str) } else { fillRet = t.pwindow.Fill(str) From 111d1934c49ac22cf84abf0ada9c24f091fc38cd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 2 Mar 2017 14:14:57 +0900 Subject: [PATCH 452/658] [vim] Throw error if g:fzf_layout is incorrectly used https://github.com/junegunn/fzf.vim/issues/327 https://github.com/junegunn/fzf.vim/issues/317 --- plugin/fzf.vim | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 932efde..840d1fe 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -192,6 +192,16 @@ function! s:defaults() return empty(colors) ? '' : ('--color='.colors) endfunction +function! s:validate_layout(layout) + for key in keys(a:layout) + if index(s:layout_keys, key) < 0 + throw printf('invalid entry in g:fzf_layout: %s (allowed: %s)%s', + \ key, join(s:layout_keys, ', '), key == 'options' ? '. Use $FZF_DEFAULT_OPTS.' : '') + endif + endfor + return a:layout +endfunction + " [name string,] [opts dict,] [fullscreen boolean] function! fzf#wrap(...) let args = ['', {}, 0] @@ -223,7 +233,7 @@ function! fzf#wrap(...) if !exists('g:fzf_layout') && exists('g:fzf_height') let opts.down = g:fzf_height else - let opts = extend(opts, get(g:, 'fzf_layout', s:default_layout)) + let opts = extend(opts, s:validate_layout(get(g:, 'fzf_layout', s:default_layout))) endif endif From a484811f78c9462b2480e33b9f44ad2fae2f4955 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 2 Mar 2017 14:17:59 +0900 Subject: [PATCH 453/658] [vim] Capitalize exception messages --- plugin/fzf.vim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 840d1fe..8e7c04a 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -195,7 +195,7 @@ endfunction function! s:validate_layout(layout) for key in keys(a:layout) if index(s:layout_keys, key) < 0 - throw printf('invalid entry in g:fzf_layout: %s (allowed: %s)%s', + throw printf('Invalid entry in g:fzf_layout: %s (allowed: %s)%s', \ key, join(s:layout_keys, ', '), key == 'options' ? '. Use $FZF_DEFAULT_OPTS.' : '') endif endfor @@ -210,7 +210,7 @@ function! fzf#wrap(...) for arg in copy(a:000) let tidx = index(expects, type(arg), tidx) if tidx < 0 - throw 'invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])' + throw 'Invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])' endif let args[tidx] = arg let tidx += 1 @@ -320,7 +320,7 @@ try call writefile(source, temps.input) let prefix = (s:is_win ? 'type ' : 'cat ').s:shellesc(temps.input).'|' else - throw 'invalid source type' + throw 'Invalid source type' endif else let prefix = '' From cb9238dc4ee6ee8c9d25fa0c1f6d731b379de9b1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 3 Mar 2017 02:26:30 +0900 Subject: [PATCH 454/658] Display -S if sort is disabled and toggle-sort is used This is to address a common confusion that one does not realize that sorting is intentionally turned off by default and can be enabled by a bind key. --- src/terminal.go | 4 ++-- test/test_go.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index c7c21c4..f556bad 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -658,9 +658,9 @@ func (t *Terminal) printInfo() { output := fmt.Sprintf("%d/%d", t.merger.Length(), t.count) if t.toggleSort { if t.sort { - output += "/S" + output += " +S" } else { - output += " " + output += " -S" } } if t.multi && len(t.selected) > 0 { diff --git a/test/test_go.rb b/test/test_go.rb index 582b10b..0470375 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -513,11 +513,11 @@ class TestGoFZF < TestBase tmux.send_keys "seq 1 111 | #{fzf "-m +s --tac #{opt} -q11"}", :Enter tmux.until { |lines| lines[-3].include? '> 111' } tmux.send_keys :Tab - tmux.until { |lines| lines[-2].include? '4/111 (1)' } + tmux.until { |lines| lines[-2].include? '4/111 -S (1)' } tmux.send_keys 'C-R' tmux.until { |lines| lines[-3].include? '> 11' } tmux.send_keys :Tab - tmux.until { |lines| lines[-2].include? '4/111/S (2)' } + tmux.until { |lines| lines[-2].include? '4/111 +S (2)' } tmux.send_keys :Enter assert_equal ['111', '11'], readonce.split($/) end From c387689d1cd45f0d8eb122fe95ee72ccc61d3bff Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 3 Mar 2017 12:20:01 +0900 Subject: [PATCH 455/658] [shell] Enable sorting by default in CTRL-R CTRL-R binding used to start with --no-sort to list the matched commands in chronological order. However, it has been a constant source of confusion. Let's enable it by default from now on. The sorted result shouldn't be too confusing as we use --tiebreak=index. --- README.md | 4 ++-- shell/key-bindings.bash | 2 +- shell/key-bindings.fish | 2 +- shell/key-bindings.zsh | 2 +- test/test_go.rb | 1 + 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c016de4..b9f9ce3 100644 --- a/README.md +++ b/README.md @@ -230,8 +230,8 @@ fish. - Set `FZF_CTRL_T_COMMAND` to override the default command - Set `FZF_CTRL_T_OPTS` to pass additional options - `CTRL-R` - Paste the selected command from history onto the command line - - Sort is disabled by default to respect chronological ordering - - Press `CTRL-R` again to toggle sort + - If you want to see the commands in chronological order, press `CTRL-R` + again which toggles sorting by relevance - Set `FZF_CTRL_R_OPTS` to pass additional options - `ALT-C` - cd into the selected directory - Set `FZF_ALT_C_COMMAND` to override the default command diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash index 5a68bc1..4266a40 100644 --- a/shell/key-bindings.bash +++ b/shell/key-bindings.bash @@ -56,7 +56,7 @@ __fzf_history__() ( shopt -u nocaseglob nocasematch line=$( HISTTIMEFORMAT= history | - FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" $(__fzfcmd) | + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" $(__fzfcmd) | command grep '^ *[0-9]') && if [[ $- =~ H ]]; then sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line" diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index c35dac7..e424f1a 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -45,7 +45,7 @@ function fzf_key_bindings function fzf-history-widget -d "Show command history" set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% begin - set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" + set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" history | eval (__fzfcmd) -q '(commandline)' | read -l result and commandline -- $result end diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index dbafafb..e67e3a7 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -63,7 +63,7 @@ fzf-history-widget() { setopt pipefail 2> /dev/null local selected num selected=( $(fc -l 1 | - FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS +s --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) ) + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) ) local ret=$? if [ -n "$selected" ]; then num=$selected[1] diff --git a/test/test_go.rb b/test/test_go.rb index 0470375..fef328b 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1408,6 +1408,7 @@ module TestShell tmux.send_keys 'C-r' tmux.until { |lines| lines.item_count > 0 } end + tmux.send_keys 'C-r' tmux.send_keys '3d' tmux.until { |lines| lines[-3].end_with? 'echo 3rd' } tmux.send_keys :Enter From 05fdf91fc5645018b2100917fa0be8d486e2ce39 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 3 Mar 2017 18:57:22 +0900 Subject: [PATCH 456/658] Revert "[zsh] emulate -L zsh to avoid issues with incompatible options" This reverts commit 3a6af27586c65bb9243c5af7c43a40619503db52. --- shell/completion.zsh | 4 ++-- shell/key-bindings.zsh | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index ccd18ce..9b9dba3 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -46,7 +46,7 @@ __fzf_generic_path_completion() { tail=$6 fzf="$(__fzfcmd_complete)" - setopt nonomatch + setopt localoptions nonomatch dir="$base" while [ 1 ]; do if [[ -z "$dir" || -d ${~dir} ]]; then @@ -142,8 +142,8 @@ _fzf_complete_unalias() { } fzf-completion() { - emulate -L zsh local tokens cmd prefix trigger tail fzf matches lbuf d_cmds + setopt localoptions noshwordsplit noksh_arrays # http://zsh.sourceforge.net/FAQ/zshfaq03.html # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index e67e3a7..230c812 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -27,7 +27,6 @@ __fzfcmd() { } fzf-file-widget() { - emulate -L zsh LBUFFER="${LBUFFER}$(__fsel)" local ret=$? zle redisplay @@ -39,10 +38,9 @@ bindkey '^T' fzf-file-widget # ALT-C - cd into the selected directory fzf-cd-widget() { - emulate -L zsh - setopt pipefail 2> /dev/null local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ -o -type d -print 2> /dev/null | cut -b3-"}" + setopt localoptions pipefail 2> /dev/null local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)" if [[ -z "$dir" ]]; then zle redisplay @@ -59,9 +57,8 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { - emulate -L zsh - setopt pipefail 2> /dev/null local selected num + setopt localoptions noglobsubst pipefail 2> /dev/null selected=( $(fc -l 1 | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) ) local ret=$? From 337cdbb37c1efc49b09b4cacc6e9ee1369c7d76d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 3 Mar 2017 19:09:29 +0900 Subject: [PATCH 457/658] [zsh] Use setopt noposixbuiltins instead of emulate -L zsh Close #858 https://github.com/junegunn/fzf/commit/3a6af27586c65bb9243c5af7c43a40619503db52#commitcomment-21135641 --- shell/completion.zsh | 2 +- shell/key-bindings.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/completion.zsh b/shell/completion.zsh index 9b9dba3..6a37476 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -143,7 +143,7 @@ _fzf_complete_unalias() { fzf-completion() { local tokens cmd prefix trigger tail fzf matches lbuf d_cmds - setopt localoptions noshwordsplit noksh_arrays + setopt localoptions noshwordsplit noksh_arrays noposixbuiltins # http://zsh.sourceforge.net/FAQ/zshfaq03.html # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 230c812..caa6c38 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -58,7 +58,7 @@ bindkey '\ec' fzf-cd-widget # CTRL-R - Paste the selected command from history into the command line fzf-history-widget() { local selected num - setopt localoptions noglobsubst pipefail 2> /dev/null + setopt localoptions noglobsubst noposixbuiltins pipefail 2> /dev/null selected=( $(fc -l 1 | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) ) local ret=$? From 53348feb8959754497453232ffacd4bd3f1154ea Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 4 Mar 2017 11:29:31 +0900 Subject: [PATCH 458/658] Add --no-clear option --- man/man1/fzf.1 | 6 ++++++ src/options.go | 6 ++++++ src/terminal.go | 4 ++-- src/tui/light.go | 24 ++++++++++++++++-------- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 59710d5..dde187d 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -339,6 +339,12 @@ Read input delimited by ASCII NUL characters instead of newline characters .B "--print0" Print output delimited by ASCII NUL characters instead of newline characters .TP +.B "--no-clear" +Do not clear finder interface on exit. If fzf was started in full screen mode, +it will not switch back to the original screen, so you'll have to manually run +\fBtput rmcup\fR to return. This option can be used to avoid flickering of the +screen when your application needs to start fzf multiple times in order. +.TP .B "--sync" Synchronous search for multi-staged filtering. If specified, fzf will launch ncurses finder only after the input stream is complete. diff --git a/src/options.go b/src/options.go index c78d2bf..7a3d60c 100644 --- a/src/options.go +++ b/src/options.go @@ -186,6 +186,7 @@ type Options struct { Margin [4]sizeSpec Bordered bool Tabstop int + ClearOnExit bool Version bool } @@ -234,6 +235,7 @@ func defaultOptions() *Options { HeaderLines: 0, Margin: defaultMargin(), Tabstop: 8, + ClearOnExit: true, Version: false} } @@ -1099,6 +1101,10 @@ func parseOptions(opts *Options, allArgs []string) { nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) case "--tabstop": opts.Tabstop = nextInt(allArgs, &i, "tab stop required") + case "--clear": + opts.ClearOnExit = true + case "--no-clear": + opts.ClearOnExit = false case "--version": opts.Version = true default: diff --git a/src/terminal.go b/src/terminal.go index f556bad..3a83bad 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -316,11 +316,11 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { } return util.Min(termHeight, util.Max(maxHeight, effectiveMinHeight)) } - renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, maxHeightFunc) + renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, maxHeightFunc) } else if tui.HasFullscreenRenderer() { renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) } else { - renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, + renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, func(h int) int { return h }) } wordRubout := "[^[:alnum:]][[:alnum:]]" diff --git a/src/tui/light.go b/src/tui/light.go index fb8f4f0..2af31c4 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -74,6 +74,7 @@ type LightRenderer struct { theme *ColorTheme mouse bool forceBlack bool + clearOnExit bool prevDownTime time.Time clickY []int ttyin *os.File @@ -106,11 +107,12 @@ type LightWindow struct { bg Color } -func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, maxHeightFunc func(int) int) Renderer { +func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, maxHeightFunc func(int) int) Renderer { r := LightRenderer{ theme: theme, forceBlack: forceBlack, mouse: mouse, + clearOnExit: clearOnExit, ttyin: openTtyIn(), yoffset: 0, tabstop: tabstop, @@ -571,14 +573,20 @@ func (r *LightRenderer) Refresh() { func (r *LightRenderer) Close() { // r.csi("u") - if r.fullscreen { - r.rmcup() - } else { - r.origin() - if r.upOneLine { - r.csi("A") + if r.clearOnExit { + if r.fullscreen { + r.rmcup() + } else { + r.origin() + if r.upOneLine { + r.csi("A") + } + r.csi("J") } - r.csi("J") + } else if r.fullscreen { + r.csi("G") + } else { + r.move(r.height, 0) } if r.mouse { r.csi("?1000l") From 6fd4be580b65913299f2306998fc3f133c259fb2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 4 Mar 2017 14:09:36 +0900 Subject: [PATCH 459/658] Use alternate screen only when the value of height is 100% Do not automatically decide to use alternate screen when the value of height exceeds the height of the terminal. # Use alternate screen fzf fzf --height 100% fzf --no-height # Still use current screen fzf --height 10000 --- src/terminal.go | 16 +++++++++------- src/tui/light.go | 19 +++++-------------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 3a83bad..197ab6b 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -295,7 +295,14 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { strongAttr = tui.AttrRegular } var renderer tui.Renderer - if opts.Height.size > 0 { + if opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100 { + if tui.HasFullscreenRenderer() { + renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) + } else { + renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, + true, func(h int) int { return h }) + } + } else { maxHeightFunc := func(termHeight int) int { var maxHeight int if opts.Height.percent { @@ -316,12 +323,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { } return util.Min(termHeight, util.Max(maxHeight, effectiveMinHeight)) } - renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, maxHeightFunc) - } else if tui.HasFullscreenRenderer() { - renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) - } else { - renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, - func(h int) int { return h }) + renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc) } wordRubout := "[^[:alnum:]][[:alnum:]]" wordNext := "[[:alnum:]][^[:alnum:]]|(.$)" diff --git a/src/tui/light.go b/src/tui/light.go index 2af31c4..3f6985f 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -107,7 +107,7 @@ type LightWindow struct { bg Color } -func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, maxHeightFunc func(int) int) Renderer { +func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer { r := LightRenderer{ theme: theme, forceBlack: forceBlack, @@ -116,7 +116,7 @@ func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop in ttyin: openTtyIn(), yoffset: 0, tabstop: tabstop, - fullscreen: false, + fullscreen: fullscreen, upOneLine: false, maxHeightFunc: maxHeightFunc} return &r @@ -176,11 +176,7 @@ func (r *LightRenderer) Init() { } r.origState = origState terminal.MakeRaw(fd) - terminalHeight, capHeight := r.updateTerminalSize() - if capHeight == terminalHeight { - r.fullscreen = true - r.height = terminalHeight - } + r.updateTerminalSize() initTheme(r.theme, r.defaultTheme(), r.forceBlack) if r.fullscreen { @@ -242,20 +238,15 @@ func getEnv(name string, defaultValue int) int { return atoi(env, defaultValue) } -func (r *LightRenderer) updateTerminalSize() (int, int) { +func (r *LightRenderer) updateTerminalSize() { width, height, err := terminal.GetSize(r.fd()) if err == nil { r.width = width - if r.fullscreen { - r.height = height - } else { - r.height = r.maxHeightFunc(height) - } + r.height = r.maxHeightFunc(height) } else { r.width = getEnv("COLUMNS", defaultWidth) r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight)) } - return height, r.height } func (r *LightRenderer) getch(nonblock bool) (int, bool) { From 88ac3971587ed93060df4c9e5eb06206519e0aad Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 4 Mar 2017 14:26:47 +0900 Subject: [PATCH 460/658] Add test case for --no-clear --- test/test_go.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/test_go.rb b/test/test_go.rb index fef328b..b827bf1 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1296,6 +1296,14 @@ class TestGoFZF < TestBase tmux.until { |lines| lines[4] == '> 3' } tmux.until { |_| %w[1 2 3] == File.readlines(tempname).map(&:chomp) } end + + def test_no_clear + tmux.send_keys 'seq 100 | fzf --no-clear --inline-info --height 5', :Enter + prompt = '> < 100/100' + tmux.until { |lines| lines[-1] == prompt } + tmux.send_keys :Enter + tmux.until { |lines| lines[-2] == prompt && lines[-1] == '1' } + end end module TestShell From e03e91477b768ff5f798708f70e7236be3ff8e26 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 5 Mar 2017 03:05:06 +0900 Subject: [PATCH 461/658] 0.16.6 --- CHANGELOG.md | 5 +++++ install | 2 +- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9de7fc..3d1c8be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +0.16.6 +------ +- Minor bug fixes and improvements +- Added `--no-clear` option for scripting purposes + 0.16.5 ------ - Minor bug fixes diff --git a/install b/install index de16b18..71497c8 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.16.5 +version=0.16.6 auto_completion= key_bindings= update_config=2 diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 7463aa3..10e948b 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 2017" "fzf 0.16.5" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Mar 2017" "fzf 0.16.6" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index dde187d..d139f3b 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 2017" "fzf 0.16.5" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Mar 2017" "fzf 0.16.6" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 38756e9..4228984 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.16.5" + version = "0.16.6" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 54a4ab0f26042c912a2e5524813eb9cbe2a3f814 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 7 Mar 2017 23:03:14 +0900 Subject: [PATCH 462/658] Add Chocolatey instruction Thanks to @majkinetor. Close #869. --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b9f9ce3..4062a45 100644 --- a/README.md +++ b/README.md @@ -74,10 +74,18 @@ Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' } ### Windows -Pre-built binaries for Windows can be downloaded [here][bin]. However, other -components of the project may not work on Windows. You might want to consider -installing fzf on [Windows Subsystem for Linux][wsl] where everything runs -flawlessly. +Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also +available as a [Chocolatey package][choco]. + +[choco]: https://chocolatey.org/packages/fzf + +```sh +choco install fzf +``` + +However, other components of the project may not work on Windows. You might +want to consider installing fzf on [Windows Subsystem for Linux][wsl] where +everything runs flawlessly. [wsl]: https://blogs.msdn.microsoft.com/wsl/ From 505dc0491b727657569359b3318fe76278530265 Mon Sep 17 00:00:00 2001 From: Christian Sturm Date: Fri, 10 Mar 2017 15:22:37 +0100 Subject: [PATCH 463/658] Make install script to work with non GNU tar (#871) --- install | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install b/install index 71497c8..93e975a 100755 --- a/install +++ b/install @@ -99,11 +99,11 @@ link_fzf_in_path() { } try_curl() { - command -v curl > /dev/null && curl -fL $1 | tar -xz + command -v curl > /dev/null && curl -fL $1 | tar -xzf - } try_wget() { - command -v wget > /dev/null && wget -O - $1 | tar -xz + command -v wget > /dev/null && wget -O - $1 | tar -xzf - } download() { From 2ba10071c96334f3241c39cff5682bc2256badda Mon Sep 17 00:00:00 2001 From: Sam Van Den Berge Date: Mon, 20 Mar 2017 17:06:13 +0100 Subject: [PATCH 464/658] Add support for IPv6 addresses in ssh completion (#877) Signed-off-by: Sam Van Den Berge --- shell/completion.bash | 2 +- shell/completion.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 6ef0386..96078a1 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -234,7 +234,7 @@ _fzf_complete_telnet() { _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \ - <(command grep -oE '^[a-z0-9.,-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ + <(command grep -oE '^[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u ) diff --git a/shell/completion.zsh b/shell/completion.zsh index 6a37476..a50b751 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -117,7 +117,7 @@ _fzf_complete_telnet() { _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \ - <(command grep -oE '^[a-z0-9.,-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ + <(command grep -oE '^[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u ) From d295d20dc4f0374d08ab251732c4052e6de43ce8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 27 Mar 2017 06:41:39 +0200 Subject: [PATCH 465/658] fzf#run: improve "is already running" message (#885) This displays the buffer(s) in this case, which is useful when FZF got stuck, and you have to manually remove the buffer. --- plugin/fzf.vim | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 8e7c04a..820ca56 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -287,9 +287,12 @@ try set shell=sh endif - if has('nvim') && len(filter(range(1, bufnr('$')), 'bufname(v:val) =~# ";#FZF"')) - call s:warn('FZF is already running!') - return [] + if has('nvim') + let running = filter(range(1, bufnr('$')), "bufname(v:val) =~# ';#FZF'") + if len(running) + call s:warn('FZF is already running (in buffer '.join(running, ', ').')!') + return [] + endif endif let dict = exists('a:1') ? s:upgrade(a:1) : {} let temps = { 'result': tempname() } From 5d12f523a30ee70979b2967d375f62400301b13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miodrag=20Mili=C4=87?= Date: Wed, 29 Mar 2017 18:59:41 +0200 Subject: [PATCH 466/658] Add chocolatey upgrade instruction to Readme (#890) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4062a45..304447d 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ method used. - git: `cd ~/.fzf && git pull && ./install` - brew: `brew update; brew reinstall fzf` +- chocolatey: `choco upgrade fzf` - vim-plug: `:PlugUpdate fzf` Building fzf From bbe696e92522239fe714ed8001887e0125d88f36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=94=E6=89=80=E5=92=8C=E5=93=89?= Date: Sat, 1 Apr 2017 11:09:46 +0900 Subject: [PATCH 467/658] [fzf-tmux] Fix issue with zoomed pane on fish (#891) --- bin/fzf-tmux | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 9561484..d7185a2 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -121,7 +121,7 @@ args+=("--no-height") if tmux list-panes -F '#F' | grep -q Z; then zoomed=1 original_window=$(tmux display-message -p "#{window_id}") - tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - \\\\; do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'") + tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'") tmux swap-pane -t $tmp_window \; select-window -t $tmp_window fi From fa1fc3d855dfccb4390ae0b91323a8d8fd660669 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 1 Apr 2017 12:00:30 +0900 Subject: [PATCH 468/658] Add vim doc Close #893 --- .gitignore | 1 + README-VIM.md | 148 +++++++++++++++++++++++++++++++++++++++++ README.md | 73 ++------------------- doc/fzf.txt | 177 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 330 insertions(+), 69 deletions(-) create mode 100644 README-VIM.md create mode 100644 doc/fzf.txt diff --git a/.gitignore b/.gitignore index d2372a9..e3e9b3b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ gopath pkg Gemfile.lock .DS_Store +doc/tags diff --git a/README-VIM.md b/README-VIM.md new file mode 100644 index 0000000..06db5c8 --- /dev/null +++ b/README-VIM.md @@ -0,0 +1,148 @@ +FZF Vim integration +=================== + +This repository only enables basic integration with Vim. If you're looking for +more, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project. + +(Note: To use fzf in GVim, an external terminal emulator is required.) + +`:FZF[!]` +--------- + +If you have set up fzf for Vim, `:FZF` command will be added. + +```vim +" Look for files under current directory +:FZF + +" Look for files under your home directory +:FZF ~ + +" With options +:FZF --no-sort --reverse --inline-info /tmp + +" Bang version starts fzf in fullscreen mode +:FZF! +``` + +Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key, +`CTRL-T`, `CTRL-X` or `CTRL-V` to open selected files in the current window, +in new tabs, in horizontal splits, or in vertical splits respectively. + +Note that the environment variables `FZF_DEFAULT_COMMAND` and +`FZF_DEFAULT_OPTS` also apply here. + +### Configuration + +- `g:fzf_action` + - Customizable extra key bindings for opening selected files in different ways +- `g:fzf_layout` + - Determines the size and position of fzf window (tmux pane or Neovim split) +- `g:fzf_colors` + - Customizes fzf colors to match the current color scheme +- `g:fzf_history_dir` + - Enables history feature +- `g:fzf_launcher` + - (Only in GVim) Terminal emulator to open fzf with + - `g:Fzf_launcher` for function reference + +#### Examples + +```vim +" This is the default extra key bindings +let g:fzf_action = { + \ 'ctrl-t': 'tab split', + \ 'ctrl-x': 'split', + \ 'ctrl-v': 'vsplit' } + +" Default fzf layout +" - down / up / left / right +let g:fzf_layout = { 'down': '~40%' } + +" In Neovim, you can set up fzf window using a Vim command +let g:fzf_layout = { 'window': 'enew' } +let g:fzf_layout = { 'window': '-tabnew' } + +" This will split only under the current window. +let g:fzf_layout = { 'window': '10 split | enew'} + +" Customize fzf colors to match your color scheme +let g:fzf_colors = +\ { 'fg': ['fg', 'Normal'], + \ 'bg': ['bg', 'Normal'], + \ 'hl': ['fg', 'Comment'], + \ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'], + \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], + \ 'hl+': ['fg', 'Statement'], + \ 'info': ['fg', 'PreProc'], + \ 'prompt': ['fg', 'Conditional'], + \ 'pointer': ['fg', 'Exception'], + \ 'marker': ['fg', 'Keyword'], + \ 'spinner': ['fg', 'Label'], + \ 'header': ['fg', 'Comment'] } + +" Enable per-command history. +" CTRL-N and CTRL-P will be automatically bound to next-history and +" previous-history instead of down and up. If you don't like the change, +" explicitly bind the keys to down and up in your $FZF_DEFAULT_OPTS. +let g:fzf_history_dir = '~/.local/share/fzf-history' +``` + +`fzf#run` +--------- + +For more advanced uses, you can use `fzf#run([options])` function with the +following options. + +| Option name | Type | Description | +| -------------------------- | ------------- | ---------------------------------------------------------------- | +| `source` | string | External command to generate input to fzf (e.g. `find .`) | +| `source` | list | Vim list as input to fzf | +| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) | +| `sink` | funcref | Reference to function to process each selected item | +| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once | +| `options` | string | Options to fzf | +| `dir` | string | Working directory | +| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) | +| `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) | +| `launcher` | string | External terminal emulator to start fzf with (GVim only) | +| `launcher` | funcref | Function for generating `launcher` string (GVim only) | + +`fzf#wrap` +---------- + +`fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper +function that decorates the options dictionary so that it understands +`g:fzf_layout`, `g:fzf_action`, `g:fzf_colors`, and `g:fzf_history_dir` like +`:FZF`. + +```vim +command! -bang MyStuff + \ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, 0)) +``` + +GVim +---- + +In GVim, you need an external terminal emulator to start fzf with. `xterm` +command is used by default, but you can customize it with `g:fzf_launcher`. + +```vim +" This is the default. %s is replaced with fzf command +let g:fzf_launcher = 'xterm -e bash -ic %s' + +" Use urxvt instead +let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s' +``` + +If you're running MacVim on OSX, I recommend you to use iTerm2 as the +launcher. Refer to the [this wiki page][macvim-iterm2] to see how to set up. + +[macvim-iterm2]: https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2 + +[License](LICENSE) +------------------ + +The MIT License (MIT) + +Copyright (c) 2017 Junegunn Choi diff --git a/README.md b/README.md index 304447d..16e7aef 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ brew install fzf /usr/local/opt/fzf/install ``` -### Vim plugin +### As Vim plugin You can manually add the directory to `&runtimepath` as follows, @@ -345,75 +345,10 @@ commands as well like follows. complete -F _fzf_file_completion -o default -o bashdefault doge ``` -Usage as Vim plugin -------------------- +Vim plugin +---------- -This repository only enables basic integration with Vim. If you're looking for -more, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project. - -(Note: To use fzf in GVim, an external terminal emulator is required.) - -#### `:FZF[!]` - -If you have set up fzf for Vim, `:FZF` command will be added. - -```vim -" Look for files under current directory -:FZF - -" Look for files under your home directory -:FZF ~ - -" With options -:FZF --no-sort --reverse --inline-info /tmp - -" Bang version starts fzf in fullscreen mode -:FZF! -``` - -Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key, -`CTRL-T`, `CTRL-X` or `CTRL-V` to open selected files in the current window, -in new tabs, in horizontal splits, or in vertical splits respectively. - -Note that the environment variables `FZF_DEFAULT_COMMAND` and -`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for -customization. - -[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-Vim-plugin - -#### `fzf#run` - -For more advanced uses, you can use `fzf#run([options])` function with the -following options. - -| Option name | Type | Description | -| -------------------------- | ------------- | ---------------------------------------------------------------- | -| `source` | string | External command to generate input to fzf (e.g. `find .`) | -| `source` | list | Vim list as input to fzf | -| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) | -| `sink` | funcref | Reference to function to process each selected item | -| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once | -| `options` | string | Options to fzf | -| `dir` | string | Working directory | -| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) | -| `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) | -| `launcher` | string | External terminal emulator to start fzf with (GVim only) | -| `launcher` | funcref | Function for generating `launcher` string (GVim only) | - -Examples can be found on [the wiki -page](https://github.com/junegunn/fzf/wiki/Examples-(vim)). - -#### `fzf#wrap` - -`fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper -function that decorates the options dictionary so that it understands -`g:fzf_layout`, `g:fzf_action`, `g:fzf_colors`, and `g:fzf_history_dir` like -`:FZF`. - -```vim -command! -bang MyStuff - \ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, 0)) -``` +See [README-VIM.md](README-VIM.md). Tips ---- diff --git a/doc/fzf.txt b/doc/fzf.txt new file mode 100644 index 0000000..447e1fa --- /dev/null +++ b/doc/fzf.txt @@ -0,0 +1,177 @@ +fzf.txt fzf Last change: April 1 2017 +FZF - TABLE OF CONTENTS *fzf* *fzf-toc* +============================================================================== + + FZF Vim integration + :FZF[!] + Configuration + Examples + fzf#run + fzf#wrap + GVim + License + +FZF VIM INTEGRATION *fzf-vim-integration* +============================================================================== + +This repository only enables basic integration with Vim. If you're looking for +more, check out {fzf.vim}{1} project. + +(Note: To use fzf in GVim, an external terminal emulator is required.) + + {1} https://github.com/junegunn/fzf.vim + + +:FZF[!] +============================================================================== + + *:FZF* + +If you have set up fzf for Vim, `:FZF` command will be added. +> + " Look for files under current directory + :FZF + + " Look for files under your home directory + :FZF ~ + + " With options + :FZF --no-sort --reverse --inline-info /tmp + + " Bang version starts fzf in fullscreen mode + :FZF! +< +Similarly to {ctrlp.vim}{2}, use enter key, CTRL-T, CTRL-X or CTRL-V to open +selected files in the current window, in new tabs, in horizontal splits, or in +vertical splits respectively. + +Note that the environment variables `FZF_DEFAULT_COMMAND` and +`FZF_DEFAULT_OPTS` also apply here. + + {2} https://github.com/kien/ctrlp.vim + + +< Configuration >_____________________________________________________________~ + *fzf-configuration* + + *g:fzf_action* *g:fzf_layout* *g:fzf_colors* *g:fzf_history_dir* *g:fzf_launcher* + *g:Fzf_launcher* + + - `g:fzf_action` + - Customizable extra key bindings for opening selected files in different + ways + - `g:fzf_layout` + - Determines the size and position of fzf window (tmux pane or Neovim split) + - `g:fzf_colors` + - Customizes fzf colors to match the current color scheme + - `g:fzf_history_dir` + - Enables history feature + - `g:fzf_launcher` + - (Only in GVim) Terminal emulator to open fzf with + - `g:Fzf_launcher` for function reference + + +Examples~ + *fzf-examples* +> + " This is the default extra key bindings + let g:fzf_action = { + \ 'ctrl-t': 'tab split', + \ 'ctrl-x': 'split', + \ 'ctrl-v': 'vsplit' } + + " Default fzf layout + " - down / up / left / right + let g:fzf_layout = { 'down': '~40%' } + + " In Neovim, you can set up fzf window using a Vim command + let g:fzf_layout = { 'window': 'enew' } + let g:fzf_layout = { 'window': '-tabnew' } + + " This will split only under the current window. + let g:fzf_layout = { 'window': '10 split | enew'} + + " Customize fzf colors to match your color scheme + let g:fzf_colors = + \ { 'fg': ['fg', 'Normal'], + \ 'bg': ['bg', 'Normal'], + \ 'hl': ['fg', 'Comment'], + \ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'], + \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], + \ 'hl+': ['fg', 'Statement'], + \ 'info': ['fg', 'PreProc'], + \ 'prompt': ['fg', 'Conditional'], + \ 'pointer': ['fg', 'Exception'], + \ 'marker': ['fg', 'Keyword'], + \ 'spinner': ['fg', 'Label'], + \ 'header': ['fg', 'Comment'] } + + " Enable per-command history. + " CTRL-N and CTRL-P will be automatically bound to next-history and + " previous-history instead of down and up. If you don't like the change, + " explicitly bind the keys to down and up in your $FZF_DEFAULT_OPTS. + let g:fzf_history_dir = '~/.local/share/fzf-history' +< + +FZF#RUN *fzf#run* +============================================================================== + +For more advanced uses, you can use `fzf#run([options])` function with the +following options. + + ---------------------------+---------------+-------------------------------------------------------------- + Option name | Type | Description ~ + ---------------------------+---------------+-------------------------------------------------------------- + `source` | string | External command to generate input to fzf (e.g. `find .` ) + `source` | list | Vim list as input to fzf + `sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` ) + `sink` | funcref | Reference to function to process each selected item + `sink*` | funcref | Similar to `sink` , but takes the list of output lines at once + `options` | string | Options to fzf + `dir` | string | Working directory + `up` / `down` / `left` / `right` | number/string | Use tmux pane with the given size (e.g. `20` , `50%` ) + `window` (Neovim only) | string | Command to open fzf window (e.g. `vertical aboveleft 30new` ) + `launcher` | string | External terminal emulator to start fzf with (GVim only) + `launcher` | funcref | Function for generating `launcher` string (GVim only) + ---------------------------+---------------+-------------------------------------------------------------- + + +FZF#WRAP *fzf#wrap* +============================================================================== + +`fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper +function that decorates the options dictionary so that it understands +`g:fzf_layout`, `g:fzf_action`, `g:fzf_colors`, and `g:fzf_history_dir` like +`:FZF`. +> + command! -bang MyStuff + \ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, 0)) +< + +GVIM *fzf-gvim* +============================================================================== + +In GVim, you need an external terminal emulator to start fzf with. `xterm` +command is used by default, but you can customize it with `g:fzf_launcher`. +> + " This is the default. %s is replaced with fzf command + let g:fzf_launcher = 'xterm -e bash -ic %s' + + " Use urxvt instead + let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s' +< +If you're running MacVim on OSX, I recommend you to use iTerm2 as the +launcher. Refer to the {this wiki page}{3} to see how to set up. + + {3} https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2 + + +LICENSE *fzf-license* +============================================================================== + +The MIT License (MIT) + +Copyright (c) 2017 Junegunn Choi + +============================================================================== +vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap: From 50e989ca85ccaca7c80ab3c541020697a8dfcd77 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 1 Apr 2017 12:06:25 +0900 Subject: [PATCH 469/658] Update example in README-VIM --- README-VIM.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README-VIM.md b/README-VIM.md index 06db5c8..0cee379 100644 --- a/README-VIM.md +++ b/README-VIM.md @@ -62,9 +62,7 @@ let g:fzf_layout = { 'down': '~40%' } " In Neovim, you can set up fzf window using a Vim command let g:fzf_layout = { 'window': 'enew' } let g:fzf_layout = { 'window': '-tabnew' } - -" This will split only under the current window. -let g:fzf_layout = { 'window': '10 split | enew'} +let g:fzf_layout = { 'window': '10split enew' } " Customize fzf colors to match your color scheme let g:fzf_colors = From 2216169ca19f3ecd9c7577d1a9f23e39265c7085 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 1 Apr 2017 12:19:39 +0900 Subject: [PATCH 470/658] Update doc/fzf.txt accordingly --- doc/fzf.txt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/fzf.txt b/doc/fzf.txt index 447e1fa..1039094 100644 --- a/doc/fzf.txt +++ b/doc/fzf.txt @@ -87,9 +87,7 @@ Examples~ " In Neovim, you can set up fzf window using a Vim command let g:fzf_layout = { 'window': 'enew' } let g:fzf_layout = { 'window': '-tabnew' } - - " This will split only under the current window. - let g:fzf_layout = { 'window': '10 split | enew'} + let g:fzf_layout = { 'window': '10split enew' } " Customize fzf colors to match your color scheme let g:fzf_colors = From 0fe07cf9fe92c86580976b918062d7ed6f6d4dba Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 2 Apr 2017 10:47:06 +0900 Subject: [PATCH 471/658] Update README.md Add PayPal donation button --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16e7aef..abe0abd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -fzf - a command-line fuzzy finder [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) +fzf - a command-line fuzzy finder [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf) [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=EKYAW9PGKPD2N) === fzf is a general-purpose command-line fuzzy finder. From 90781974467048a133113af1dc03830ab093f573 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 2 Apr 2017 11:30:22 +0900 Subject: [PATCH 472/658] Add --version to --help output and man page Close #888 Close #894 --- man/man1/fzf.1 | 3 +++ src/options.go | 1 + 2 files changed, 4 insertions(+) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index d139f3b..6649f02 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -352,6 +352,9 @@ ncurses finder only after the input stream is complete. .RS e.g. \fBfzf --multi | fzf --sync\fR .RE +.TP +.B "--version" +Display version information and exit .SH ENVIRONMENT VARIABLES .TP diff --git a/src/options.go b/src/options.go index 7a3d60c..0c84a3a 100644 --- a/src/options.go +++ b/src/options.go @@ -86,6 +86,7 @@ const usage = `usage: fzf [options] --read0 Read input delimited by ASCII NUL characters --print0 Print output delimited by ASCII NUL characters --sync Synchronous search for multi-staged filtering + --version Display version information and exit Environment variables FZF_DEFAULT_COMMAND Default command to use when input is tty From 7c8f7d3f201b89c2e1fb1aca4b7289964469cba8 Mon Sep 17 00:00:00 2001 From: Kouki Higashikawa Date: Mon, 3 Apr 2017 11:49:54 +0900 Subject: [PATCH 473/658] [fzf-tmux] Close with exit code 130 when tmux pane is killed Fix #796 --- bin/fzf-tmux | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index d7185a2..b01d995 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -144,8 +144,12 @@ cleanup() { kill-window -t $tmp_window \; \ resize-pane -Z fi + + if [ $# -gt 0 ]; then + exit 130 + fi } -trap cleanup EXIT SIGINT SIGTERM +trap 'cleanup 1' SIGUSR1 envs="env TERM=$TERM " [[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" @@ -164,18 +168,22 @@ for arg in "${args[@]}"; do opts="$opts \"$arg\"" done +pppid=$$ +trap_set="trap 'kill -SIGUSR1 $pppid' EXIT SIGINT SIGTERM" +trap_unset="trap - EXIT SIGINT SIGTERM" + if [[ -n "$term" ]] || [[ -t 0 ]]; then cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ - split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \ + split-window $opt "$trap_set;cd $(printf %q "$PWD");$envs bash $argsf;$trap_unset" $swap \ > /dev/null 2>&1 else mkfifo $fifo1 cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ - split-window $opt "$envs bash $argsf" $swap \ + split-window $opt "$trap_set;$envs bash $argsf;$trap_unset" $swap \ > /dev/null 2>&1 cat <&0 > $fifo1 & fi From a50909e8067f5ef5a0aa2bb4637f033fe91df8b2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 6 Apr 2017 02:08:49 +0900 Subject: [PATCH 474/658] Correction: fzf no longer depends on ncurses --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index abe0abd..56fcd17 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ See [BUILD.md](BUILD.md). Usage ----- -fzf will launch curses-based finder, read the list from STDIN, and write the +fzf will launch interactive finder, read the list from STDIN, and write the selected item to STDOUT. ```sh From a5862d4b9cc5f57ef83174c0d714bfb485094a4f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 11 Apr 2017 22:21:16 +0900 Subject: [PATCH 475/658] [bash-completion] Use -o dirnames instead of -o plusdirs Close #903 Related #135 --- shell/completion.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/completion.bash b/shell/completion.bash index 96078a1..e8c34e5 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -304,7 +304,7 @@ done # Directory for cmd in $d_cmds; do - _fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o plusdirs" + _fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames" done unset _fzf_defc From 7a11a06cbd03353e7c0c7ddc25cd6e9fe58cef6d Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Fri, 21 Apr 2017 22:30:51 -0400 Subject: [PATCH 476/658] [vim] Use backslash for Windows filepaths (#896) - Fix shellescaping issues for filepaths - Supports both forward slashes or backslashes - Paths with spaces - Use jobstart for neovim in s:execute (Windows) - https://github.com/neovim/neovim/pull/6497 - Make 2 s:fzf_shellescape functions - (Windows) Substitute \" with \\" to escape the last backslash - (Default) Regular shellescape - Support list 'options' - Add "@echo off" to the batchfile used to execute fzf --- plugin/fzf.vim | 133 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 97 insertions(+), 36 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 820ca56..3eb1d0e 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -26,10 +26,56 @@ if exists('g:loaded_fzf') endif let g:loaded_fzf = 1 +let s:is_win = has('win32') || has('win64') +if s:is_win && &shellslash + set noshellslash + let s:base_dir = expand(':h:h') + set shellslash +else + let s:base_dir = expand(':h:h') +endif +if s:is_win + function! s:fzf_call(fn, ...) + let shellslash = &shellslash + try + set noshellslash + return call(a:fn, a:000) + finally + let &shellslash = shellslash + endtry + endfunction + + function! s:fzf_shellescape(path) + return substitute(s:fzf_call('shellescape', a:path), '[^\\]\zs\\"$', '\\\\"', '') + endfunction +else + function! s:fzf_call(fn, ...) + return call(a:fn, a:000) + endfunction + + function! s:fzf_shellescape(path) + return shellescape(a:path) + endfunction +endif + +function! s:fzf_getcwd() + return s:fzf_call('getcwd') +endfunction + +function! s:fzf_fnamemodify(fname, mods) + return s:fzf_call('fnamemodify', a:fname, a:mods) +endfunction + +function! s:fzf_expand(fmt) + return s:fzf_call('expand', a:fmt) +endfunction + +function! s:fzf_tempname() + return s:fzf_call('tempname') +endfunction + let s:default_layout = { 'down': '~40%' } let s:layout_keys = ['window', 'up', 'down', 'left', 'right'] -let s:is_win = has('win32') || has('win64') -let s:base_dir = expand(':h:h') let s:fzf_go = s:base_dir.'/bin/fzf' let s:fzf_tmux = s:base_dir.'/bin/fzf-tmux' let s:install = s:base_dir.'/install' @@ -41,7 +87,7 @@ set cpo&vim function! s:fzf_exec() if !exists('s:exec') if executable(s:fzf_go) - let s:exec = s:fzf_go + let s:exec = s:fzf_expand(s:fzf_go) elseif executable('fzf') let s:exec = 'fzf' elseif s:is_win @@ -62,7 +108,7 @@ function! s:fzf_exec() throw 'fzf executable not found' endif endif - return s:shellesc(s:exec) + return s:is_win ? s:exec : s:shellesc(s:exec) endfunction function! s:tmux_enabled() @@ -133,7 +179,7 @@ function! s:has_any(dict, keys) endfunction function! s:open(cmd, target) - if stridx('edit', a:cmd) == 0 && fnamemodify(a:target, ':p') ==# expand('%:p') + if stridx('edit', a:cmd) == 0 && s:fzf_fnamemodify(a:target, ':p') ==# s:fzf_expand('%:p') return endif execute a:cmd s:escape(a:target) @@ -148,11 +194,11 @@ function! s:common_sink(action, lines) abort if len(a:lines) > 1 augroup fzf_swap autocmd SwapExists * let v:swapchoice='o' - \| call s:warn('fzf: E325: swap file exists: '.expand('')) + \| call s:warn('fzf: E325: swap file exists: '.s:fzf_expand('')) augroup END endif try - let empty = empty(expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified + let empty = empty(s:fzf_expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified let autochdir = &autochdir set noautochdir for item in a:lines @@ -202,6 +248,11 @@ function! s:validate_layout(layout) return a:layout endfunction +function! s:evaluate_opts(options) + return type(a:options) == type([]) ? + \ join(map(copy(a:options), 's:fzf_shellescape(v:val)')) : a:options +endfunction + " [name string,] [opts dict,] [fullscreen boolean] function! fzf#wrap(...) let args = ['', {}, 0] @@ -238,15 +289,16 @@ function! fzf#wrap(...) endif " Colors: g:fzf_colors - let opts.options = s:defaults() .' '. get(opts, 'options', '') + let opts.options = s:defaults() .' '. s:evaluate_opts(get(opts, 'options', '')) " History: g:fzf_history_dir if len(name) && len(get(g:, 'fzf_history_dir', '')) - let dir = expand(g:fzf_history_dir) + let dir = s:fzf_expand(g:fzf_history_dir) if !isdirectory(dir) call mkdir(dir, 'p') endif - let opts.options = join(['--history', s:escape(dir.'/'.name), opts.options]) + let history = s:is_win ? s:fzf_shellescape(dir.'\'.name) : s:escape(dir.'/'.name) + let opts.options = join(['--history', history, opts.options]) endif " Action: g:fzf_action @@ -262,19 +314,6 @@ function! fzf#wrap(...) return opts endfunction -function! fzf#shellescape(path) - if s:is_win - let shellslash = &shellslash - try - set noshellslash - return shellescape(a:path) - finally - let &shellslash = shellslash - endtry - endif - return shellescape(a:path) -endfunction - function! fzf#run(...) abort try let oshell = &shell @@ -295,8 +334,8 @@ try endif endif let dict = exists('a:1') ? s:upgrade(a:1) : {} - let temps = { 'result': tempname() } - let optstr = get(dict, 'options', '') + let temps = { 'result': s:fzf_tempname() } + let optstr = s:evaluate_opts(get(dict, 'options', '')) try let fzf_exec = s:fzf_exec() catch @@ -304,11 +343,11 @@ try endtry if has('nvim') && !has_key(dict, 'dir') - let dict.dir = getcwd() + let dict.dir = s:fzf_getcwd() endif if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) - let temps.source = tempname().(s:is_win ? '.bat' : '') + let temps.source = s:fzf_tempname().(s:is_win ? '.bat' : '') call writefile((s:is_win ? ['@echo off'] : []) + split($FZF_DEFAULT_COMMAND, "\n"), temps.source) let dict.source = (empty($SHELL) ? &shell : $SHELL) . (s:is_win ? ' /c ' : ' ') . s:shellesc(temps.source) endif @@ -319,7 +358,7 @@ try if type == 1 let prefix = source.'|' elseif type == 3 - let temps.input = tempname() + let temps.input = s:fzf_tempname() call writefile(source, temps.input) let prefix = (s:is_win ? 'type ' : 'cat ').s:shellesc(temps.input).'|' else @@ -333,7 +372,7 @@ try let use_height = has_key(dict, 'down') && \ !(has('nvim') || s:is_win || s:present(dict, 'up', 'left', 'right')) && \ executable('tput') && filereadable('/dev/tty') - let use_term = has('nvim') + let use_term = has('nvim') && !s:is_win let use_tmux = (!use_height && !use_term || prefer_tmux) && s:tmux_enabled() && s:splittable(dict) if prefer_tmux && use_tmux let use_height = 0 @@ -394,13 +433,13 @@ endfunction function! s:pushd(dict) if s:present(a:dict, 'dir') - let cwd = getcwd() + let cwd = s:fzf_getcwd() if get(a:dict, 'prev_dir', '') ==# cwd return 1 endif let a:dict.prev_dir = cwd execute 'lcd' s:escape(a:dict.dir) - let a:dict.dir = getcwd() + let a:dict.dir = s:fzf_getcwd() return 1 endif return 0 @@ -464,6 +503,23 @@ function! s:execute(dict, command, use_height, temps) abort else let command = a:use_height ? a:command : escaped endif + if s:is_win + let batchfile = s:fzf_tempname().'.bat' + call writefile(['@echo off', command], batchfile) + let command = batchfile + if has('nvim') + let s:dict = a:dict + let s:temps = a:temps + let fzf = {} + function! fzf.on_exit(job_id, exit_status, event) dict + let lines = s:collect(s:temps) + call s:callback(s:dict, lines) + endfunction + let cmd = 'start /wait cmd /c '.command + call jobstart(cmd, fzf) + return [] + endif + endif if a:use_height let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty' call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin)) @@ -675,19 +731,24 @@ let s:default_action = { function! s:shortpath() let short = pathshorten(fnamemodify(getcwd(), ':~:.')) - return empty(short) ? '~/' : short . (short =~ '/$' ? '' : '/') + let slash = (s:is_win && !&shellslash) ? '\' : '/' + return empty(short) ? '~'.slash : short . (short =~ slash.'$' ? '' : slash) endfunction function! s:cmd(bang, ...) abort let args = copy(a:000) - let opts = { 'options': '--multi ' } + let opts = { 'options': ['--multi'] } if len(args) && isdirectory(expand(args[-1])) let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '') - let opts.options .= ' --prompt '.fzf#shellescape(opts.dir) + if s:is_win && !&shellslash + let opts.dir = substitute(opts.dir, '/', '\\', 'g') + endif + let prompt = opts.dir else - let opts.options .= ' --prompt '.fzf#shellescape(s:shortpath()) + let prompt = s:shortpath() endif - let opts.options .= ' '.join(args) + call extend(opts.options, ['--prompt', prompt]) + call extend(opts.options, args) call fzf#run(fzf#wrap('FZF', opts, a:bang)) endfunction From 5677e5e1338a07d00d0a1c74ce918710a9ce8fdc Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 28 Apr 2017 01:57:38 +0900 Subject: [PATCH 477/658] [fish] Fix ~/.config/fish/functions/fish_user_key_bindings.fish Install script will create the file with the proper function body only if the file doesn't exist. If it already exists, it will try to append `fzf_key_bindings` as before. Close #851 --- install | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/install b/install index 93e975a..5eb40cc 100755 --- a/install +++ b/install @@ -297,6 +297,17 @@ append_line() { set +e } +create_file() { + local file="$1" + shift + echo "Create $file:" + for line in "$@"; do + echo " $line" + echo "$line" >> "$file" + done + echo +} + if [ $update_config -eq 2 ]; then echo ask "Do you want to update your shell configuration files?" @@ -310,7 +321,14 @@ done if [ $key_bindings -eq 1 ] && [ $has_fish -eq 1 ]; then bind_file=~/.config/fish/functions/fish_user_key_bindings.fish - append_line $update_config "fzf_key_bindings" "$bind_file" + if [ ! -e "$bind_file" ]; then + create_file "$bind_file" \ + 'function fish_user_key_bindings' \ + ' fzf_key_bindings' \ + 'end' + else + append_line $update_config "fzf_key_bindings" "$bind_file" + fi fi if [ $update_config -eq 1 ]; then From d5e72bf55d0bb317a3b9515885e92d94097b38fe Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 28 Apr 2017 02:09:55 +0900 Subject: [PATCH 478/658] Update README-VIM: options as list (#896) --- README-VIM.md | 11 ++++++++++- doc/fzf.txt | 11 +++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/README-VIM.md b/README-VIM.md index 0cee379..804400d 100644 --- a/README-VIM.md +++ b/README-VIM.md @@ -99,13 +99,22 @@ following options. | `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) | | `sink` | funcref | Reference to function to process each selected item | | `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once | -| `options` | string | Options to fzf | +| `options` | string/list | Options to fzf | | `dir` | string | Working directory | | `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) | | `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) | | `launcher` | string | External terminal emulator to start fzf with (GVim only) | | `launcher` | funcref | Function for generating `launcher` string (GVim only) | +`options` entry can be either a string or a list. For simple cases, string +should suffice, but prefer to use list type if you're concerned about escaping +issues on different platforms. + +```vim +call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'}) +call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']}) +``` + `fzf#wrap` ---------- diff --git a/doc/fzf.txt b/doc/fzf.txt index 1039094..52080ef 100644 --- a/doc/fzf.txt +++ b/doc/fzf.txt @@ -1,4 +1,4 @@ -fzf.txt fzf Last change: April 1 2017 +fzf.txt fzf Last change: April 28 2017 FZF - TABLE OF CONTENTS *fzf* *fzf-toc* ============================================================================== @@ -125,7 +125,7 @@ following options. `sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` ) `sink` | funcref | Reference to function to process each selected item `sink*` | funcref | Similar to `sink` , but takes the list of output lines at once - `options` | string | Options to fzf + `options` | string/list | Options to fzf `dir` | string | Working directory `up` / `down` / `left` / `right` | number/string | Use tmux pane with the given size (e.g. `20` , `50%` ) `window` (Neovim only) | string | Command to open fzf window (e.g. `vertical aboveleft 30new` ) @@ -133,6 +133,13 @@ following options. `launcher` | funcref | Function for generating `launcher` string (GVim only) ---------------------------+---------------+-------------------------------------------------------------- +`options` entry can be either a string or a list. For simple cases, string +should suffice, but prefer to use list type if you're concerned about escaping +issues on different platforms. +> + call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'}) + call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']}) +< FZF#WRAP *fzf#wrap* ============================================================================== From 6b592137b966299a8a1324ccb1ddf3d6aaa7bbc9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 28 Apr 2017 02:36:36 +0900 Subject: [PATCH 479/658] Add support for ctrl-alt-[a-z] key chords Close #906 --- man/man1/fzf.1 | 3 ++- src/options.go | 6 +++-- src/options_test.go | 9 ++++--- src/tui/light.go | 5 ++-- src/tui/ncurses.go | 2 +- src/tui/tcell.go | 61 +++++++++++++++++++++++++-------------------- src/tui/tui.go | 5 ++-- 7 files changed, 52 insertions(+), 39 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 6649f02..0e71808 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Mar 2017" "fzf 0.16.6" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Apr 2017" "fzf 0.16.7" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -439,6 +439,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR .B AVAILABLE KEYS: (SYNONYMS) \fIctrl-[a-z]\fR \fIctrl-space\fR + \fIctrl-alt-[a-z]\fR \fIalt-[a-z]\fR \fIalt-[0-9]\fR \fIf[1-12]\fR diff --git a/src/options.go b/src/options.go index 0c84a3a..8dd58f0 100644 --- a/src/options.go +++ b/src/options.go @@ -401,7 +401,7 @@ func parseKeyChords(str string, message string) map[int]string { case "ctrl-space": chord = tui.CtrlSpace case "alt-enter", "alt-return": - chord = tui.AltEnter + chord = tui.CtrlAltM case "alt-space": chord = tui.AltSpace case "alt-/": @@ -437,7 +437,9 @@ func parseKeyChords(str string, message string) map[int]string { case "f12": chord = tui.F12 default: - if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { + if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) { + chord = tui.CtrlAltA + int(lkey[9]) - 'a' + } else if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { chord = tui.CtrlA + int(lkey[5]) - 'a' } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { chord = tui.AltA + int(lkey[4]) - 'a' diff --git a/src/options_test.go b/src/options_test.go index 29d9842..907faf0 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -125,14 +125,14 @@ func TestIrrelevantNth(t *testing.T) { } func TestParseKeys(t *testing.T) { - pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ALT-enter,alt-SPACE", "") + pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "") check := func(i int, s string) { if pairs[i] != s { t.Errorf("%s != %s", pairs[i], s) } } - if len(pairs) != 11 { - t.Error(11) + if len(pairs) != 12 { + t.Error(12) } check(tui.CtrlZ, "ctrl-z") check(tui.AltZ, "alt-z") @@ -143,7 +143,8 @@ func TestParseKeys(t *testing.T) { check(tui.CtrlA+'g'-'a', "ctrl-G") check(tui.AltZ+'J', "J") check(tui.AltZ+'g', "g") - check(tui.AltEnter, "ALT-enter") + check(tui.CtrlAltA, "ctrl-alt-a") + check(tui.CtrlAltM, "ALT-enter") check(tui.AltSpace, "alt-SPACE") // Synonyms diff --git a/src/tui/light.go b/src/tui/light.go index 3f6985f..7819050 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -344,9 +344,10 @@ func (r *LightRenderer) escSequence(sz *int) Event { return Event{ESC, 0, nil} } *sz = 2 + if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 { + return Event{int(CtrlAltA + r.buffer[1] - 1), 0, nil} + } switch r.buffer[1] { - case 13: - return Event{AltEnter, 0, nil} case 32: return Event{AltSpace, 0, nil} case 47: diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index 4b88b44..ba3a1cf 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -353,7 +353,7 @@ func escSequence() Event { case C.ERR: return Event{ESC, 0, nil} case CtrlM: - return Event{AltEnter, 0, nil} + return Event{CtrlAltM, 0, nil} case '/': return Event{AltSlash, 0, nil} case ' ': diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 964c19e..bda5d08 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -221,58 +221,65 @@ func (r *FullscreenRenderer) GetChar() Event { // process keyboard: case *tcell.EventKey: alt := (ev.Modifiers() & tcell.ModAlt) > 0 + keyfn := func(r rune) int { + if alt { + return CtrlAltA - 'a' + int(r) + } + return CtrlA - 'a' + int(r) + } switch ev.Key() { case tcell.KeyCtrlA: - return Event{CtrlA, 0, nil} + return Event{keyfn('a'), 0, nil} case tcell.KeyCtrlB: - return Event{CtrlB, 0, nil} + return Event{keyfn('b'), 0, nil} case tcell.KeyCtrlC: - return Event{CtrlC, 0, nil} + return Event{keyfn('c'), 0, nil} case tcell.KeyCtrlD: - return Event{CtrlD, 0, nil} + return Event{keyfn('d'), 0, nil} case tcell.KeyCtrlE: - return Event{CtrlE, 0, nil} + return Event{keyfn('e'), 0, nil} case tcell.KeyCtrlF: - return Event{CtrlF, 0, nil} + return Event{keyfn('f'), 0, nil} case tcell.KeyCtrlG: - return Event{CtrlG, 0, nil} + return Event{keyfn('g'), 0, nil} + case tcell.KeyCtrlH: + return Event{keyfn('h'), 0, nil} + case tcell.KeyCtrlI: + return Event{keyfn('i'), 0, nil} case tcell.KeyCtrlJ: - return Event{CtrlJ, 0, nil} + return Event{keyfn('j'), 0, nil} case tcell.KeyCtrlK: - return Event{CtrlK, 0, nil} + return Event{keyfn('k'), 0, nil} case tcell.KeyCtrlL: - return Event{CtrlL, 0, nil} + return Event{keyfn('l'), 0, nil} case tcell.KeyCtrlM: - if alt { - return Event{AltEnter, 0, nil} - } - return Event{CtrlM, 0, nil} + return Event{keyfn('m'), 0, nil} case tcell.KeyCtrlN: - return Event{CtrlN, 0, nil} + return Event{keyfn('n'), 0, nil} case tcell.KeyCtrlO: - return Event{CtrlO, 0, nil} + return Event{keyfn('o'), 0, nil} case tcell.KeyCtrlP: - return Event{CtrlP, 0, nil} + return Event{keyfn('p'), 0, nil} case tcell.KeyCtrlQ: - return Event{CtrlQ, 0, nil} + return Event{keyfn('q'), 0, nil} case tcell.KeyCtrlR: - return Event{CtrlR, 0, nil} + return Event{keyfn('r'), 0, nil} case tcell.KeyCtrlS: - return Event{CtrlS, 0, nil} + return Event{keyfn('s'), 0, nil} case tcell.KeyCtrlT: - return Event{CtrlT, 0, nil} + return Event{keyfn('t'), 0, nil} case tcell.KeyCtrlU: - return Event{CtrlU, 0, nil} + return Event{keyfn('u'), 0, nil} case tcell.KeyCtrlV: - return Event{CtrlV, 0, nil} + return Event{keyfn('v'), 0, nil} case tcell.KeyCtrlW: - return Event{CtrlW, 0, nil} + return Event{keyfn('w'), 0, nil} case tcell.KeyCtrlX: - return Event{CtrlX, 0, nil} + return Event{keyfn('x'), 0, nil} case tcell.KeyCtrlY: - return Event{CtrlY, 0, nil} + return Event{keyfn('y'), 0, nil} case tcell.KeyCtrlZ: - return Event{CtrlZ, 0, nil} + return Event{keyfn('z'), 0, nil} case tcell.KeyCtrlSpace: return Event{CtrlSpace, 0, nil} case tcell.KeyBackspace, tcell.KeyBackspace2: diff --git a/src/tui/tui.go b/src/tui/tui.go index f8d905a..d360c4e 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -75,7 +75,6 @@ const ( F11 F12 - AltEnter AltSpace AltSlash AltBS @@ -90,7 +89,9 @@ const ( // Reset iota AltD AltE AltF - AltZ = AltA + 'z' - 'a' + AltZ = AltA + 'z' - 'a' + CtrlAltA = AltZ + 1 + CtrlAltM = CtrlAltA + 'm' - 'a' ) const ( From d34e4cf6984a139c12646d21771526e1e2a6f4f7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 28 Apr 2017 22:58:08 +0900 Subject: [PATCH 480/658] Support CTRL-Z (SIGSTOP) --- src/terminal.go | 49 +++++++++++++++++++++++++++++++++-------- src/terminal_unix.go | 8 +++++++ src/terminal_windows.go | 8 +++++++ src/tui/dummy.go | 12 +++++----- src/tui/light.go | 38 +++++++++++++++++++------------- src/tui/ncurses.go | 5 ++--- src/tui/tcell.go | 9 +++----- src/tui/tui.go | 4 ++-- 8 files changed, 92 insertions(+), 41 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 197ab6b..2802edd 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -59,6 +59,7 @@ type Terminal struct { inlineInfo bool prompt string reverse bool + fullscreen bool hscroll bool hscrollOff int wordRubout string @@ -141,6 +142,7 @@ const ( reqList reqJump reqRefresh + reqReinit reqRedraw reqClose reqPrintQuery @@ -210,6 +212,7 @@ const ( actExecute actExecuteSilent actExecuteMulti // Deprecated + actSigStop ) func toActions(types ...actionType) []action { @@ -246,6 +249,9 @@ func defaultKeymap() map[int][]action { keymap[tui.CtrlU] = toActions(actUnixLineDiscard) keymap[tui.CtrlW] = toActions(actUnixWordRubout) keymap[tui.CtrlY] = toActions(actYank) + if !util.IsWindows() { + keymap[tui.CtrlZ] = toActions(actSigStop) + } keymap[tui.AltB] = toActions(actBackwardWord) keymap[tui.SLeft] = toActions(actBackwardWord) @@ -295,7 +301,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { strongAttr = tui.AttrRegular } var renderer tui.Renderer - if opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100 { + fullscreen := opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100 + if fullscreen { if tui.HasFullscreenRenderer() { renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) } else { @@ -337,6 +344,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { inlineInfo: opts.InlineInfo, prompt: opts.Prompt, reverse: opts.Reverse, + fullscreen: fullscreen, hscroll: opts.Hscroll, hscrollOff: opts.HscrollOff, wordRubout: wordRubout, @@ -1170,6 +1178,12 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo }) } +func (t *Terminal) redraw() { + t.tui.Clear() + t.tui.Refresh() + t.printAll() +} + func (t *Terminal) executeCommand(template string, forcePlus bool, background bool) { valid, list := t.buildPlusList(template, forcePlus) if !valid { @@ -1181,12 +1195,10 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - t.tui.Pause() + t.tui.Pause(true) cmd.Run() - if t.tui.Resume() { - t.tui.Clear() - t.printAll() - } + t.tui.Resume(true) + t.redraw() t.refresh() } else { cmd.Run() @@ -1244,6 +1256,15 @@ func (t *Terminal) Loop() { t.reqBox.Set(reqQuit, nil) }() + contChan := make(chan os.Signal, 1) + notifyOnCont(contChan) + go func() { + for { + <-contChan + t.reqBox.Set(reqReinit, nil) + } + }() + resizeChan := make(chan os.Signal, 1) notifyOnResize(resizeChan) // Non-portable go func() { @@ -1352,10 +1373,11 @@ func (t *Terminal) Loop() { t.printHeader() case reqRefresh: t.suppress = false + case reqReinit: + t.tui.Resume(t.fullscreen) + t.redraw() case reqRedraw: - t.tui.Clear() - t.tui.Refresh() - t.printAll() + t.redraw() case reqClose: t.tui.Close() if t.output() { @@ -1654,6 +1676,15 @@ func (t *Terminal) Loop() { t.input = []rune(t.history.next()) t.cx = len(t.input) } + case actSigStop: + p, err := os.FindProcess(os.Getpid()) + if err == nil { + t.tui.Clear() + t.tui.Pause(t.fullscreen) + notifyStop(p) + t.mutex.Unlock() + return false + } case actMouse: me := event.MouseEvent mx, my := me.X, me.Y diff --git a/src/terminal_unix.go b/src/terminal_unix.go index 6284c22..2ae8175 100644 --- a/src/terminal_unix.go +++ b/src/terminal_unix.go @@ -11,3 +11,11 @@ import ( func notifyOnResize(resizeChan chan<- os.Signal) { signal.Notify(resizeChan, syscall.SIGWINCH) } + +func notifyStop(p *os.Process) { + p.Signal(syscall.SIGSTOP) +} + +func notifyOnCont(resizeChan chan<- os.Signal) { + signal.Notify(resizeChan, syscall.SIGCONT) +} diff --git a/src/terminal_windows.go b/src/terminal_windows.go index 5512bba..9de7ae4 100644 --- a/src/terminal_windows.go +++ b/src/terminal_windows.go @@ -9,3 +9,11 @@ import ( func notifyOnResize(resizeChan chan<- os.Signal) { // TODO } + +func notifyStop(p *os.Process) { + // NOOP +} + +func notifyOnCont(resizeChan chan<- os.Signal) { + // NOOP +} diff --git a/src/tui/dummy.go b/src/tui/dummy.go index 60a23fb..c96ce8e 100644 --- a/src/tui/dummy.go +++ b/src/tui/dummy.go @@ -25,13 +25,13 @@ const ( Reverse = Attr(1 << 6) ) -func (r *FullscreenRenderer) Init() {} -func (r *FullscreenRenderer) Pause() {} -func (r *FullscreenRenderer) Clear() {} -func (r *FullscreenRenderer) Refresh() {} -func (r *FullscreenRenderer) Close() {} +func (r *FullscreenRenderer) Init() {} +func (r *FullscreenRenderer) Pause(bool) {} +func (r *FullscreenRenderer) Resume(bool) {} +func (r *FullscreenRenderer) Clear() {} +func (r *FullscreenRenderer) Refresh() {} +func (r *FullscreenRenderer) Close() {} -func (r *FullscreenRenderer) Resume() bool { return false } func (r *FullscreenRenderer) DoesAutoWrap() bool { return false } func (r *FullscreenRenderer) IsOptimized() bool { return false } func (r *FullscreenRenderer) GetChar() Event { return Event{} } diff --git a/src/tui/light.go b/src/tui/light.go index 7819050..be6950c 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -522,27 +522,35 @@ func (r *LightRenderer) rmcup() { r.csi("?1049l") } -func (r *LightRenderer) Pause() { +func (r *LightRenderer) Pause(clear bool) { terminal.Restore(r.fd(), r.origState) - if r.fullscreen { - r.rmcup() - } else { - r.smcup() - r.csi("H") + if clear { + if r.fullscreen { + r.rmcup() + } else { + r.smcup() + r.csi("H") + } + r.flush() } - r.flush() } -func (r *LightRenderer) Resume() bool { +func (r *LightRenderer) Resume(clear bool) { terminal.MakeRaw(r.fd()) - if r.fullscreen { - r.smcup() - } else { - r.rmcup() + if clear { + if r.fullscreen { + r.smcup() + } else { + r.rmcup() + } + r.flush() + } else if !r.fullscreen && r.mouse { + // NOTE: Resume(false) is only called on SIGCONT after SIGSTOP. + // And It's highly likely that the offset we obtained at the beginning will + // no longer be correct, so we simply disable mouse input. + r.csi("?1000l") + r.mouse = false } - r.flush() - // Should redraw - return true } func (r *LightRenderer) Clear() { diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go index ba3a1cf..3263c39 100644 --- a/src/tui/ncurses.go +++ b/src/tui/ncurses.go @@ -176,12 +176,11 @@ func initPairs(theme *ColorTheme) { } } -func (r *FullscreenRenderer) Pause() { +func (r *FullscreenRenderer) Pause(bool) { C.endwin() } -func (r *FullscreenRenderer) Resume() bool { - return false +func (r *FullscreenRenderer) Resume(bool) { } func (r *FullscreenRenderer) Close() { diff --git a/src/tui/tcell.go b/src/tui/tcell.go index bda5d08..3a8c765 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -282,7 +282,7 @@ func (r *FullscreenRenderer) GetChar() Event { return Event{keyfn('z'), 0, nil} case tcell.KeyCtrlSpace: return Event{CtrlSpace, 0, nil} - case tcell.KeyBackspace, tcell.KeyBackspace2: + case tcell.KeyBackspace2: if alt { return Event{AltBS, 0, nil} } @@ -308,8 +308,6 @@ func (r *FullscreenRenderer) GetChar() Event { case tcell.KeyPgDn: return Event{PgDn, 0, nil} - case tcell.KeyTab: - return Event{Tab, 0, nil} case tcell.KeyBacktab: return Event{BTab, 0, nil} @@ -366,13 +364,12 @@ func (r *FullscreenRenderer) GetChar() Event { return Event{Invalid, 0, nil} } -func (r *FullscreenRenderer) Pause() { +func (r *FullscreenRenderer) Pause(bool) { _screen.Fini() } -func (r *FullscreenRenderer) Resume() bool { +func (r *FullscreenRenderer) Resume(bool) { r.initScreen() - return true } func (r *FullscreenRenderer) Close() { diff --git a/src/tui/tui.go b/src/tui/tui.go index d360c4e..c638b35 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -206,8 +206,8 @@ const ( type Renderer interface { Init() - Pause() - Resume() bool + Pause(clear bool) + Resume(clear bool) Clear() RefreshWindows(windows []Window) Refresh() From 7f17a9d1b4508306ee6898ae2a0c578d8da41f3e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 30 Apr 2017 00:47:44 +0900 Subject: [PATCH 481/658] Update mattn/go-shellwords --- src/deps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/deps b/src/deps index 17e50f2..cdb3914 100755 --- a/src/deps +++ b/src/deps @@ -14,5 +14,5 @@ reset() ( reset github.com/junegunn/go-isatty 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8 reset github.com/junegunn/go-runewidth 14207d285c6c197daabb5c9793d63e7af9ab2d50 -reset github.com/junegunn/go-shellwords 33bd8f1ebe16d6e5eb688cc885749a63059e9167 +reset github.com/junegunn/go-shellwords 02e3cf038dcea8290e44424da473dd12be796a8a reset golang.org/x/crypto abc5fa7ad02123a41f02bf1391c9760f7586e608 From 93b8f61551724498a3dea9b084f89666185afb46 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 30 Apr 2017 11:18:56 +0900 Subject: [PATCH 482/658] [vim] Export $FZF_HEIGHT for previewer scripts Preview script cannot properly determine the height of fzf finder if `--height` option is used. https://github.com/junegunn/fzf.vim/issues/361 --- plugin/fzf.vim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 3eb1d0e..126e16b 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -379,7 +379,9 @@ try let use_term = 0 endif if use_height - let optstr .= ' --height='.s:calc_size(&lines, dict.down, dict) + let height = s:calc_size(&lines, dict.down, dict) + let $FZF_HEIGHT = height + let optstr .= ' --height='.height elseif use_term let optstr .= ' --no-height' endif From eba9e04e2ef2b0c17c0f80bde92469409bec0713 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 30 Apr 2017 11:36:23 +0900 Subject: [PATCH 483/658] Export FZF_PREVIEW_HEIGHT instead of FZF_HEIGHT https://github.com/junegunn/fzf.vim/issues/361 --- plugin/fzf.vim | 1 - src/terminal.go | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 126e16b..fd5874c 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -380,7 +380,6 @@ try endif if use_height let height = s:calc_size(&lines, dict.down, dict) - let $FZF_HEIGHT = height let optstr .= ' --height='.height elseif use_term let optstr .= ' --no-height' diff --git a/src/terminal.go b/src/terminal.go index 2802edd..5aab6cd 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -580,6 +580,7 @@ func (t *Terminal) resizeWindows() { pwidth += 1 } t.pwindow = t.tui.NewWindow(y+1, x+2, pwidth, h-2, tui.BorderNone) + os.Setenv("FZF_PREVIEW_HEIGHT", strconv.Itoa(h-2)) } switch t.preview.position { case posUp: From 2d61691bb2641f53f3503c4b5d8ea57b0736da4d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 30 Apr 2017 11:54:40 +0900 Subject: [PATCH 484/658] 0.16.7 --- CHANGELOG.md | 7 +++++++ install | 2 +- man/man1/fzf-tmux.1 | 2 +- src/constants.go | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d1c8be..e2cdc30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +0.16.7 +------ +- Added support for `ctrl-alt-[a-z]` key chords +- CTRL-Z (SIGSTOP) now works with fzf +- fzf will export `$FZF_PREVIEW_WINDOW` so that the scripts can use it +- Bug fixes and improvements in Vim plugin and shell extensions + 0.16.6 ------ - Minor bug fixes and improvements diff --git a/install b/install index 5eb40cc..9a51207 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.16.6 +version=0.16.7 auto_completion= key_bindings= update_config=2 diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 10e948b..71b4e35 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Mar 2017" "fzf 0.16.6" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Apr 2017" "fzf 0.16.7" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/src/constants.go b/src/constants.go index 4228984..941fdcc 100644 --- a/src/constants.go +++ b/src/constants.go @@ -8,7 +8,7 @@ import ( const ( // Current version - version = "0.16.6" + version = "0.16.7" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 6898849e3e7908c8c5d7fff243ff3d8b630ebc75 Mon Sep 17 00:00:00 2001 From: Theodore Dubois Date: Thu, 4 May 2017 18:48:28 -0700 Subject: [PATCH 485/658] Mention that the fish bug has been fixed (#912) --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 56fcd17..069cd2f 100644 --- a/README.md +++ b/README.md @@ -394,9 +394,9 @@ export FZF_DEFAULT_COMMAND=' #### Fish shell It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362) -that it doesn't allow reading from STDIN in command substitution, which means -simple `vim (fzf)` won't work as expected. The workaround is to use the `read` -fish command: +(will be fixed in 2.6.0) that it doesn't allow reading from STDIN in command +substitution, which means simple `vim (fzf)` won't work as expected. The +workaround is to use the `read` fish command: ```sh fzf | read -l result; and vim $result From e65f14cbed2ebb6a572188d8e373d1279e7edc03 Mon Sep 17 00:00:00 2001 From: Aurelien Rainone Date: Sat, 20 May 2017 12:08:56 +0200 Subject: [PATCH 486/658] Update README: Add table of contents (#927) --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index 069cd2f..7b7c1fa 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,39 @@ Pros - Batteries included - Vim/Neovim plugin, key bindings and fuzzy auto-completion +Table of Contents +----------------- + + * [Installation](#installation) + * [Using git](#using-git) + * [Using Homebrew](#using-homebrew) + * [As Vim plugin](#as-vim-plugin) + * [Windows](#windows) + * [Upgrading fzf](#upgrading-fzf) + * [Building fzf](#building-fzf) + * [Usage](#usage) + * [Using the finder](#using-the-finder) + * [Layout](#layout) + * [Search syntax](#search-syntax) + * [Environment variables](#environment-variables) + * [Options](#options) + * [Examples](#examples) + * [fzf-tmux script](#fzf-tmux-script) + * [Key bindings for command line](#key-bindings-for-command-line) + * [Fuzzy completion for bash and zsh](#fuzzy-completion-for-bash-and-zsh) + * [Files and directories](#files-and-directories) + * [Process IDs](#process-ids) + * [Host names](#host-names) + * [Environment variables / Aliases](#environment-variables--aliases) + * [Settings](#settings) + * [Supported commands](#supported-commands) + * [Vim plugin](#vim-plugin) + * [Tips](#tips) + * [Respecting .gitignore, .hgignore, and svn:ignore](#respecting-gitignore-hgignore-and-svnignore) + * [git ls-tree for fast traversal](#git-ls-tree-for-fast-traversal) + * [Fish shell](#fish-shell) + * [License](#license) + Installation ------------ From 11300913a45ea7c4fa5290767291c55bbc9276d7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 22 May 2017 01:04:04 +0900 Subject: [PATCH 487/658] [vim] Do not expand s:fzf_go expand() may return an empty string depending on the value of &wildignore. Since expand('') always returns an absolute path, we can remove expand() call here. Close #917. --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index fd5874c..b761121 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -87,7 +87,7 @@ set cpo&vim function! s:fzf_exec() if !exists('s:exec') if executable(s:fzf_go) - let s:exec = s:fzf_expand(s:fzf_go) + let s:exec = s:fzf_go elseif executable('fzf') let s:exec = 'fzf' elseif s:is_win From 3a5086796d797f6e4402e9cd73e13687facfcb7f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 22 May 2017 01:23:59 +0900 Subject: [PATCH 488/658] [vim] Prevent 'wildignore' from affecting expand() (#917) --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index b761121..e82ef11 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -67,7 +67,7 @@ function! s:fzf_fnamemodify(fname, mods) endfunction function! s:fzf_expand(fmt) - return s:fzf_call('expand', a:fmt) + return s:fzf_call('expand', a:fmt, 1) endfunction function! s:fzf_tempname() From e1e33397700573da6d6b4fbaff9ac71f10fa36ed Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 22 May 2017 17:07:05 +0900 Subject: [PATCH 489/658] Implement bindable "change" event and "top" action # Move cursor to the top result whenever the query string is changed fzf --bind change:top Close #925 --- man/man1/fzf.1 | 6 ++++++ src/options.go | 4 ++++ src/terminal.go | 9 +++++++++ src/tui/tui.go | 2 ++ test/test_go.rb | 14 ++++++++++++++ 5 files changed, 35 insertions(+) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 0e71808..c55c900 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -467,6 +467,11 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fIdouble-click\fR or any single character +Additionally, a special event named \fIchange\fR is available which is +triggered whenever the query string is changed. + + e.g. \fBfzf --bind change:top\fR + \fBACTION: DEFAULT BINDINGS (NOTES): \fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR \fBaccept\fR \fIenter double-click\fR @@ -513,6 +518,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBtoggle-preview-wrap\fR \fBtoggle-sort\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR + \fBtop\fR (move to the top result) \fBunix-line-discard\fR \fIctrl-u\fR \fBunix-word-rubout\fR \fIctrl-w\fR \fBup\fR \fIctrl-k ctrl-p up\fR diff --git a/src/options.go b/src/options.go index 8dd58f0..e9e2f28 100644 --- a/src/options.go +++ b/src/options.go @@ -400,6 +400,8 @@ func parseKeyChords(str string, message string) map[int]string { chord = tui.BSpace case "ctrl-space": chord = tui.CtrlSpace + case "change": + chord = tui.Change case "alt-enter", "alt-return": chord = tui.CtrlAltM case "alt-space": @@ -714,6 +716,8 @@ func parseKeymap(keymap map[int][]action, str string) { appendAction(actDown) case "up": appendAction(actUp) + case "top": + appendAction(actTop) case "page-up": appendAction(actPageUp) case "page-down": diff --git a/src/terminal.go b/src/terminal.go index 5aab6cd..3d9ef6c 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -213,6 +213,7 @@ const ( actExecuteSilent actExecuteMulti // Deprecated actSigStop + actTop ) func toActions(types ...actionType) []action { @@ -1609,6 +1610,9 @@ func (t *Terminal) Loop() { req(reqClose) case actClearScreen: req(reqRedraw) + case actTop: + t.vset(0) + req(reqList) case actUnixLineDiscard: if t.cx > 0 { t.yanked = copySlice(t.input[:t.cx]) @@ -1749,6 +1753,11 @@ func (t *Terminal) Loop() { } t.truncateQuery() changed = string(previousInput) != string(t.input) + if onChanges, prs := t.keymap[tui.Change]; changed && prs { + if !doActions(onChanges, tui.Change) { + continue + } + } } else { if mapkey == tui.Rune { if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() { diff --git a/src/tui/tui.go b/src/tui/tui.go index c638b35..1ba8ca7 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -75,6 +75,8 @@ const ( F11 F12 + Change + AltSpace AltSlash AltBS diff --git a/test/test_go.rb b/test/test_go.rb index b827bf1..1106bab 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1304,6 +1304,20 @@ class TestGoFZF < TestBase tmux.send_keys :Enter tmux.until { |lines| lines[-2] == prompt && lines[-1] == '1' } end + + def test_change_top + tmux.send_keys %[seq 1000 | #{FZF} --bind change:top], :Enter + tmux.until { |lines| lines.match_count == 1000 } + tmux.send_keys :Up + tmux.until { |lines| lines[-4] == '> 2' } + tmux.send_keys 1 + tmux.until { |lines| lines[-3] == '> 1' } + tmux.send_keys :Up + tmux.until { |lines| lines[-4] == '> 10' } + tmux.send_keys 1 + tmux.until { |lines| lines[-3] == '> 11' } + tmux.send_keys :Enter + end end module TestShell From f5746002fd6b870bdeac6b8c164d5f39efbf5b8d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 24 May 2017 02:43:39 +0900 Subject: [PATCH 490/658] Do not "--cycle" on page-up/page-down Close #928 --- src/terminal.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 3d9ef6c..817ce4f 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1591,20 +1591,20 @@ func (t *Terminal) Loop() { case actToggleDown: if t.multi && t.merger.Length() > 0 { toggle() - t.vmove(-1) + t.vmove(-1, true) req(reqList) } case actToggleUp: if t.multi && t.merger.Length() > 0 { toggle() - t.vmove(1) + t.vmove(1, true) req(reqList) } case actDown: - t.vmove(-1) + t.vmove(-1, true) req(reqList) case actUp: - t.vmove(1) + t.vmove(1, true) req(reqList) case actAccept: req(reqClose) @@ -1632,16 +1632,16 @@ func (t *Terminal) Loop() { t.input = append(append(t.input[:t.cx], t.yanked...), suffix...) t.cx += len(t.yanked) case actPageUp: - t.vmove(t.maxItems() - 1) + t.vmove(t.maxItems()-1, false) req(reqList) case actPageDown: - t.vmove(-(t.maxItems() - 1)) + t.vmove(-(t.maxItems() - 1), false) req(reqList) case actHalfPageUp: - t.vmove(t.maxItems() / 2) + t.vmove(t.maxItems()/2, false) req(reqList) case actHalfPageDown: - t.vmove(-(t.maxItems() / 2)) + t.vmove(-(t.maxItems() / 2), false) req(reqList) case actJump: t.jumping = jumpEnabled @@ -1699,7 +1699,7 @@ func (t *Terminal) Loop() { if t.multi && me.Mod { toggle() } - t.vmove(me.S) + t.vmove(me.S, true) req(reqList) } else if t.hasPreviewWindow() && t.pwindow.Enclose(my, mx) { scrollPreview(-me.S) @@ -1796,12 +1796,12 @@ func (t *Terminal) constrain() { t.offset = util.Max(0, t.offset) } -func (t *Terminal) vmove(o int) { +func (t *Terminal) vmove(o int, allowCycle bool) { if t.reverse { o *= -1 } dest := t.cy + o - if t.cycle { + if t.cycle && allowCycle { max := t.merger.Length() - 1 if dest > max { if t.cy == max { From 96a3250152749641edce8e91385ecf29dc2789a4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 24 May 2017 13:20:13 +0900 Subject: [PATCH 491/658] Update test case for --cycle --- test/test_go.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_go.rb b/test/test_go.rb index 1106bab..85b8f37 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -975,15 +975,15 @@ class TestGoFZF < TestBase tmux.until { |lines| lines[-10].start_with? '>' } tmux.send_keys :Down tmux.until { |lines| lines[-9].start_with? '>' } - tmux.send_keys :PgUp + tmux.send_keys :Up tmux.until { |lines| lines[-10].start_with? '>' } tmux.send_keys :PgUp - tmux.until { |lines| lines[-3].start_with? '>' } + tmux.until { |lines| lines[-10].start_with? '>' } tmux.send_keys :Up - tmux.until { |lines| lines[-4].start_with? '>' } - tmux.send_keys :PgDn tmux.until { |lines| lines[-3].start_with? '>' } tmux.send_keys :PgDn + tmux.until { |lines| lines[-3].start_with? '>' } + tmux.send_keys :Down tmux.until { |lines| lines[-10].start_with? '>' } end From ab182e276badc8222962f4d28a052dfa7f016d88 Mon Sep 17 00:00:00 2001 From: Tw Date: Wed, 24 May 2017 11:36:59 -0500 Subject: [PATCH 492/658] Use read syscall directly to get character (#931) Due to go std lib uses poller for os.File introducing in this commit: https://github.com/golang/go/commit/c05b06a12d005f50e4776095a60d6bd9c2c91fac There are two changes to watch out: 1. os.File.Fd will always return a blocking fd except on bsd. 2. os.File.Read won't return EAGAIN error for nonblocking fd. So For 1, we just get tty's fd in advance and then set its block mode. For 2, we use read syscall directly to get what we wanted error(EAGAIN). Fix issue #910. Signed-off-by: Tw --- src/tui/light.go | 3 ++- src/util/util_unix.go | 5 +++++ src/util/util_windows.go | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/tui/light.go b/src/tui/light.go index be6950c..e690ef9 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -251,8 +251,9 @@ func (r *LightRenderer) updateTerminalSize() { func (r *LightRenderer) getch(nonblock bool) (int, bool) { b := make([]byte, 1) + fd := r.fd() util.SetNonblock(r.ttyin, nonblock) - _, err := r.ttyin.Read(b) + _, err := util.Read(fd, b) if err != nil { return 0, false } diff --git a/src/util/util_unix.go b/src/util/util_unix.go index bc1b7b5..d538ee0 100644 --- a/src/util/util_unix.go +++ b/src/util/util_unix.go @@ -26,3 +26,8 @@ func IsWindows() bool { func SetNonblock(file *os.File, nonblock bool) { syscall.SetNonblock(int(file.Fd()), nonblock) } + +// Read executes syscall.Read on file descriptor +func Read(fd int, b []byte) (int, error) { + return syscall.Read(int(fd), b) +} diff --git a/src/util/util_windows.go b/src/util/util_windows.go index 9ba4f79..0664416 100644 --- a/src/util/util_windows.go +++ b/src/util/util_windows.go @@ -32,3 +32,8 @@ func IsWindows() bool { func SetNonblock(file *os.File, nonblock bool) { syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock) } + +// Read executes syscall.Read on file descriptor +func Read(fd int, b []byte) (int, error) { + return syscall.Read(syscall.Handle(fd), b) +} From 21d664d670182a7937f3fc783cac11fa1c2afab4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 25 May 2017 19:09:04 +0900 Subject: [PATCH 493/658] Update extra bash completion example --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7b7c1fa..93c7628 100644 --- a/README.md +++ b/README.md @@ -374,8 +374,8 @@ On bash, fuzzy completion is enabled only for a predefined set of commands commands as well like follows. ```sh -# There are also _fzf_path_completion and _fzf_dir_completion -complete -F _fzf_file_completion -o default -o bashdefault doge +complete -F _fzf_path_completion -o default -o bashdefault ag +complete -F _fzf_dir_completion -o default -o bashdefault tree ``` Vim plugin From cf4711d8786f95193423cfc68dd44c1393569c73 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 26 May 2017 19:02:49 +0900 Subject: [PATCH 494/658] Fix display of tab characters in --prompt --- src/terminal.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 817ce4f..4e7da73 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -58,6 +58,7 @@ type Terminal struct { initDelay time.Duration inlineInfo bool prompt string + promptLen int reverse bool fullscreen bool hscroll bool @@ -133,7 +134,6 @@ func (a byTimeOrder) Less(i, j int) bool { } var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} -var _tabStop int const ( reqPrompt util.EventType = iota @@ -340,10 +340,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { wordRubout = fmt.Sprintf("%s[^%s]", sep, sep) wordNext = fmt.Sprintf("[^%s]%s|(.$)", sep, sep) } - return &Terminal{ + t := Terminal{ initDelay: delay, inlineInfo: opts.InlineInfo, - prompt: opts.Prompt, reverse: opts.Reverse, fullscreen: fullscreen, hscroll: opts.Hscroll, @@ -390,6 +389,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { startChan: make(chan bool, 1), tui: renderer, initFunc: func() { renderer.Init() }} + t.prompt, t.promptLen = t.processTabs([]rune(opts.Prompt), 0) + return &t } // Input returns current query string @@ -633,7 +634,7 @@ func (t *Terminal) move(y int, x int, clear bool) { } func (t *Terminal) placeCursor() { - t.move(0, t.displayWidth([]rune(t.prompt))+t.displayWidth(t.input[:t.cx]), false) + t.move(0, t.promptLen+t.displayWidth(t.input[:t.cx]), false) } func (t *Terminal) printPrompt() { @@ -645,7 +646,7 @@ func (t *Terminal) printPrompt() { func (t *Terminal) printInfo() { pos := 0 if t.inlineInfo { - pos = t.displayWidth([]rune(t.prompt)) + t.displayWidth(t.input) + 1 + pos = t.promptLen + t.displayWidth(t.input) + 1 if pos+len(" < ") > t.window.Width() { return } @@ -1241,7 +1242,7 @@ func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item } func (t *Terminal) truncateQuery() { - maxPatternLength := util.Max(1, t.window.Width()-t.displayWidth([]rune(t.prompt))-1) + maxPatternLength := util.Max(1, t.window.Width()-t.promptLen-1) t.input, _ = t.trimRight(t.input, maxPatternLength) t.cx = util.Constrain(t.cx, 0, len(t.input)) } @@ -1707,7 +1708,7 @@ func (t *Terminal) Loop() { } else if t.window.Enclose(my, mx) { mx -= t.window.Left() my -= t.window.Top() - mx = util.Constrain(mx-t.displayWidth([]rune(t.prompt)), 0, len(t.input)) + mx = util.Constrain(mx-t.promptLen, 0, len(t.input)) if !t.reverse { my = t.window.Height() - my - 1 } From 5d6eb5bfd64c6d5d773c71159d819b651dd5f7f1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 28 May 2017 02:26:42 +0900 Subject: [PATCH 495/658] Respect ANSI color state from the previous line in preview output --- src/terminal.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 4e7da73..fdd3caa 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -954,6 +954,7 @@ func (t *Terminal) printPreview() { } reader := bufio.NewReader(strings.NewReader(t.previewer.text)) lineNo := -t.previewer.offset + var ansi *ansiState for { line, err := reader.ReadString('\n') eof := err == io.EOF @@ -965,7 +966,7 @@ func (t *Terminal) printPreview() { break } else if lineNo > 0 { var fillRet tui.FillReturn - extractColor(line, nil, func(str string, ansi *ansiState) bool { + _, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool { trimmed := []rune(str) if !t.preview.wrap { trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X()) From 8aab0fc1894facf9ac51132ff8a832b18bc115ac Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Sun, 28 May 2017 21:06:06 -0400 Subject: [PATCH 496/658] [vim] Replace s:fzf_shellescape and s:shellesc with fzf#shellescape (#916) --- plugin/fzf.vim | 44 ++++++++++++++++++++++++-------------------- test/fzf.vader | 18 ++++++++++++++++++ 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index e82ef11..fbfcd1d 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -44,20 +44,28 @@ if s:is_win let &shellslash = shellslash endtry endfunction - - function! s:fzf_shellescape(path) - return substitute(s:fzf_call('shellescape', a:path), '[^\\]\zs\\"$', '\\\\"', '') - endfunction else function! s:fzf_call(fn, ...) return call(a:fn, a:000) endfunction - - function! s:fzf_shellescape(path) - return shellescape(a:path) - endfunction endif +function! s:shellesc_cmd(arg) + let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g') + let escaped = substitute(escaped, '%', '%%', 'g') + let escaped = substitute(escaped, '"', '\\^&', 'g') + let escaped = substitute(escaped, '\\\+\(\\^\)', '\\\\\1', 'g') + return '^"'.substitute(escaped, '[^\\]\zs\\$', '\\\\', '').'^"' +endfunction + +function! fzf#shellescape(arg, ...) + let shell = get(a:000, 0, &shell) + if shell =~# 'cmd.exe$' + return s:shellesc_cmd(a:arg) + endif + return s:fzf_call('shellescape', a:arg) +endfunction + function! s:fzf_getcwd() return s:fzf_call('getcwd') endfunction @@ -108,7 +116,7 @@ function! s:fzf_exec() throw 'fzf executable not found' endif endif - return s:is_win ? s:exec : s:shellesc(s:exec) + return s:is_win ? s:exec : fzf#shellescape(s:exec) endfunction function! s:tmux_enabled() @@ -128,10 +136,6 @@ function! s:tmux_enabled() return s:tmux endfunction -function! s:shellesc(arg) - return '"'.substitute(a:arg, '"', '\\"', 'g').'"' -endfunction - function! s:escape(path) let escaped_chars = '$%#''"' @@ -250,7 +254,7 @@ endfunction function! s:evaluate_opts(options) return type(a:options) == type([]) ? - \ join(map(copy(a:options), 's:fzf_shellescape(v:val)')) : a:options + \ join(map(copy(a:options), 'fzf#shellescape(v:val)')) : a:options endfunction " [name string,] [opts dict,] [fullscreen boolean] @@ -297,7 +301,7 @@ function! fzf#wrap(...) if !isdirectory(dir) call mkdir(dir, 'p') endif - let history = s:is_win ? s:fzf_shellescape(dir.'\'.name) : s:escape(dir.'/'.name) + let history = s:is_win ? fzf#shellescape(dir.'\'.name) : s:escape(dir.'/'.name) let opts.options = join(['--history', history, opts.options]) endif @@ -349,7 +353,7 @@ try if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) let temps.source = s:fzf_tempname().(s:is_win ? '.bat' : '') call writefile((s:is_win ? ['@echo off'] : []) + split($FZF_DEFAULT_COMMAND, "\n"), temps.source) - let dict.source = (empty($SHELL) ? &shell : $SHELL) . (s:is_win ? ' /c ' : ' ') . s:shellesc(temps.source) + let dict.source = (empty($SHELL) ? &shell : $SHELL) . (s:is_win ? ' /c ' : ' ') . fzf#shellescape(temps.source) endif if has_key(dict, 'source') @@ -360,7 +364,7 @@ try elseif type == 3 let temps.input = s:fzf_tempname() call writefile(source, temps.input) - let prefix = (s:is_win ? 'type ' : 'cat ').s:shellesc(temps.input).'|' + let prefix = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input).'|' else throw 'Invalid source type' endif @@ -424,7 +428,7 @@ function! s:fzf_tmux(dict) endif endfor return printf('LINES=%d COLUMNS=%d %s %s %s --', - \ &lines, &columns, s:shellesc(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-')) + \ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-')) endfunction function! s:splittable(dict) @@ -493,7 +497,7 @@ function! s:execute(dict, command, use_height, temps) abort if has('unix') && !a:use_height silent! !clear 2> /dev/null endif - let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#!') + let escaped = (a:use_height || s:is_win) ? a:command : escape(substitute(a:command, '\n', '\\n', 'g'), '%#!') if has('gui_running') let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher))) let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher @@ -502,7 +506,7 @@ function! s:execute(dict, command, use_height, temps) abort endif let command = printf(fmt, escaped) else - let command = a:use_height ? a:command : escaped + let command = escaped endif if s:is_win let batchfile = s:fzf_tempname().'.bat' diff --git a/test/fzf.vader b/test/fzf.vader index c6f899f..64a5c7b 100644 --- a/test/fzf.vader +++ b/test/fzf.vader @@ -147,6 +147,24 @@ Execute (fzf#wrap): let opts = fzf#wrap({}) Assert opts.options =~ '^--color=fg:' +Execute (fzf#shellescape with sh): + AssertEqual '''''', fzf#shellescape('', 'sh') + AssertEqual '''""''', fzf#shellescape('""', 'sh') + AssertEqual '''foobar>''', fzf#shellescape('foobar>', 'sh') + AssertEqual '''\"''', fzf#shellescape('\"', 'sh') + AssertEqual '''echo ''\''''a''\'''' && echo ''\''''b''\''''''', fzf#shellescape('echo ''a'' && echo ''b''', 'sh') + +Execute (fzf#shellescape with cmd.exe): + AssertEqual '^"^"', fzf#shellescape('', 'cmd.exe') + AssertEqual '^"\^"\^"^"', fzf#shellescape('""', 'cmd.exe') + AssertEqual '^"foobar^>^"', fzf#shellescape('foobar>', 'cmd.exe') + AssertEqual '^"\\\^"\\^"', fzf#shellescape('\\\\\\\\"\', 'cmd.exe') + AssertEqual '^"echo ''a'' ^&^& echo ''b''^"', fzf#shellescape('echo ''a'' && echo ''b''', 'cmd.exe') + + AssertEqual '^"C:\Program Files ^(x86^)\\^"', fzf#shellescape('C:\Program Files (x86)\', 'cmd.exe') + AssertEqual '^"C:/Program Files ^(x86^)/^"', fzf#shellescape('C:/Program Files (x86)/', 'cmd.exe') + " AssertEqual '^"%%USERPROFILE%%^", fzf#shellescape('%USERPROFILE%', 'cmd.exe') + Execute (Cleanup): unlet g:dir Restore From 669a6fee40a2d946ab35561c47e76d6a2216948c Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Tue, 30 May 2017 20:56:01 -0400 Subject: [PATCH 497/658] [vim] Use utf-8 for cmd.exe (#929) --- plugin/fzf.vim | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index fbfcd1d..81aff37 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -44,10 +44,22 @@ if s:is_win let &shellslash = shellslash endtry endfunction + + " Use utf-8 for fzf.vim commands + " Return array of shell commands for cmd.exe + function! s:wrap_cmds(cmds) + return ['@echo off', 'for /f "tokens=4" %%a in (''chcp'') do set origchcp=%%a', 'chcp 65001 > nul'] + + \ (type(a:cmds) == type([]) ? a:cmds : [a:cmds]) + + \ ['chcp %origchcp% > nul'] + endfunction else function! s:fzf_call(fn, ...) return call(a:fn, a:000) endfunction + + function! s:wrap_cmds(cmds) + return a:cmds + endfunction endif function! s:shellesc_cmd(arg) @@ -352,7 +364,7 @@ try if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) let temps.source = s:fzf_tempname().(s:is_win ? '.bat' : '') - call writefile((s:is_win ? ['@echo off'] : []) + split($FZF_DEFAULT_COMMAND, "\n"), temps.source) + call writefile(s:wrap_cmds(split($FZF_DEFAULT_COMMAND, "\n")), temps.source) let dict.source = (empty($SHELL) ? &shell : $SHELL) . (s:is_win ? ' /c ' : ' ') . fzf#shellescape(temps.source) endif @@ -510,7 +522,7 @@ function! s:execute(dict, command, use_height, temps) abort endif if s:is_win let batchfile = s:fzf_tempname().'.bat' - call writefile(['@echo off', command], batchfile) + call writefile(s:wrap_cmds(command), batchfile) let command = batchfile if has('nvim') let s:dict = a:dict From 0665fe04138866fcdd659dcae221672bf1f5f325 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 31 May 2017 10:02:04 +0900 Subject: [PATCH 498/658] [vim] Remove unnecessary ternary expression Related: https://github.com/junegunn/fzf.vim/issues/378 --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 81aff37..2917568 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -128,7 +128,7 @@ function! s:fzf_exec() throw 'fzf executable not found' endif endif - return s:is_win ? s:exec : fzf#shellescape(s:exec) + return fzf#shellescape(s:exec) endfunction function! s:tmux_enabled() From 076f49d447093944b0ba91c4c60b57a9126869ea Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 31 May 2017 10:03:23 +0900 Subject: [PATCH 499/658] [vim] Make sure to delete temporary batchfile on Windows --- plugin/fzf.vim | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 2917568..8c8ff85 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -524,6 +524,7 @@ function! s:execute(dict, command, use_height, temps) abort let batchfile = s:fzf_tempname().'.bat' call writefile(s:wrap_cmds(command), batchfile) let command = batchfile + let a:temps.batchfile = batchfile if has('nvim') let s:dict = a:dict let s:temps = a:temps From 35d407021c468f6faf0d99358d3fca1720d4edc9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 31 May 2017 23:59:11 +0900 Subject: [PATCH 500/658] [vim] Replace invalid s:escape calls with fzf#shellescape --- plugin/fzf.vim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 8c8ff85..8bdeb3b 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -313,7 +313,7 @@ function! fzf#wrap(...) if !isdirectory(dir) call mkdir(dir, 'p') endif - let history = s:is_win ? fzf#shellescape(dir.'\'.name) : s:escape(dir.'/'.name) + let history = fzf#shellescape(dir.'/'.name) let opts.options = join(['--history', history, opts.options]) endif @@ -553,7 +553,7 @@ function! s:execute_tmux(dict, command, temps) abort let command = a:command if s:pushd(a:dict) " -c '#{pane_current_path}' is only available on tmux 1.9 or above - let command = 'cd '.s:escape(a:dict.dir).' && '.command + let command = join(['cd', fzf#shellescape(a:dict.dir), '&&', command]) endif call system(command) From 7d3575b3624c39f26c5f7a1646ebe2f78920f04a Mon Sep 17 00:00:00 2001 From: Edgar Lee Date: Thu, 1 Jun 2017 16:27:17 -0700 Subject: [PATCH 501/658] Use glide to handle go dependencies --- .gitignore | 2 +- BUILD.md | 2 -- glide.lock | 38 ++++++++++++++++++++++++++++++++++++++ glide.yaml | 16 ++++++++++++++++ src/Makefile | 12 ++---------- src/deps | 18 ------------------ src/options.go | 2 +- src/tui/tcell.go | 7 +++---- src/util/util.go | 4 ++-- src/util/util_windows.go | 2 +- 10 files changed, 64 insertions(+), 39 deletions(-) create mode 100644 glide.lock create mode 100644 glide.yaml delete mode 100755 src/deps diff --git a/.gitignore b/.gitignore index e3e9b3b..4e88fdf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ bin src/fzf/fzf-* -gopath pkg Gemfile.lock .DS_Store doc/tags +vendor diff --git a/BUILD.md b/BUILD.md index 5b04f67..f52f395 100644 --- a/BUILD.md +++ b/BUILD.md @@ -10,8 +10,6 @@ Build instructions ### Using Makefile -Makefile will set up and use its own `$GOPATH` under the project root. - ```sh # Source files are located in src directory cd src diff --git a/glide.lock b/glide.lock new file mode 100644 index 0000000..dbe594f --- /dev/null +++ b/glide.lock @@ -0,0 +1,38 @@ +hash: d68dd0bd779ac4ffca1e0c49ca38d85f90d5d68fa8e2d5d7db70a8ce8c662ec1 +updated: 2017-06-01T15:48:41.653745249-07:00 +imports: +- name: github.com/gdamore/encoding + version: b23993cbb6353f0e6aa98d0ee318a34728f628b9 +- name: github.com/gdamore/tcell + version: 44772c121bb7838819d3ba4a7e84c0c2d617328e + subpackages: + - encoding +- name: github.com/lucasb-eyer/go-colorful + version: c900de9dbbc73129068f5af6a823068fc5f2308c +- name: github.com/mattn/go-isatty + version: 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8 +- name: github.com/mattn/go-runewidth + version: 14207d285c6c197daabb5c9793d63e7af9ab2d50 +- name: github.com/mattn/go-shellwords + version: 02e3cf038dcea8290e44424da473dd12be796a8a +- name: golang.org/x/crypto + version: e1a4589e7d3ea14a3352255d04b6f1a418845e5e + subpackages: + - ssh/terminal +- name: golang.org/x/sys + version: b90f89a1e7a9c1f6b918820b3daa7f08488c8594 + subpackages: + - unix +- name: golang.org/x/text + version: 4ee4af566555f5fbe026368b75596286a312663a + subpackages: + - encoding + - encoding/charmap + - encoding/internal + - encoding/internal/identifier + - encoding/japanese + - encoding/korean + - encoding/simplifiedchinese + - encoding/traditionalchinese + - transform +testImports: [] diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 0000000..7103c09 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,16 @@ +package: github.com/junegunn/fzf +import: +- package: github.com/mattn/go-isatty + version: 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8 +- package: github.com/mattn/go-runewidth + version: 14207d285c6c197daabb5c9793d63e7af9ab2d50 +- package: github.com/mattn/go-shellwords + version: 02e3cf038dcea8290e44424da473dd12be796a8a +- package: github.com/gdamore/tcell + version: 44772c121bb7838819d3ba4a7e84c0c2d617328e + subpackages: + - encoding +- package: golang.org/x/crypto + version: e1a4589e7d3ea14a3352255d04b6f1a418845e5e + subpackages: + - ssh/terminal diff --git a/src/Makefile b/src/Makefile index b8c3090..5205b1c 100644 --- a/src/Makefile +++ b/src/Makefile @@ -12,8 +12,6 @@ endif SOURCES := $(wildcard *.go */*.go) ROOTDIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) BINDIR := $(shell dirname $(ROOTDIR))/bin -GOPATH := $(shell dirname $(ROOTDIR))/gopath -SRCDIR := $(GOPATH)/src/github.com/junegunn/fzf/src BINARY32 := fzf-$(GOOS)_386 BINARY64 := fzf-$(GOOS)_amd64 BINARYARM5 := fzf-$(GOOS)_arm5 @@ -27,7 +25,6 @@ RELEASEARM5 := fzf-$(VERSION)-$(GOOS)_arm5 RELEASEARM6 := fzf-$(VERSION)-$(GOOS)_arm6 RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7 RELEASEARM8 := fzf-$(VERSION)-$(GOOS)_arm8 -export GOPATH # https://en.wikipedia.org/wiki/Uname UNAME_M := $(shell uname -m) @@ -79,13 +76,8 @@ release-all: clean test GOOS=openbsd make release GOOS=windows make release -$(SRCDIR): - mkdir -p $(shell dirname $(SRCDIR)) - ln -s $(ROOTDIR) $(SRCDIR) - -deps: $(SRCDIR) $(SOURCES) - cd $(SRCDIR) && go get -tags "$(TAGS)" - ./deps +deps: $(SOURCES) + cd .. && go get -u github.com/Masterminds/glide && $(GOPATH)/bin/glide install test: deps SHELL=/bin/sh GOOS= go test -v -tags "$(TAGS)" ./... diff --git a/src/deps b/src/deps deleted file mode 100755 index cdb3914..0000000 --- a/src/deps +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -if [ -z "$GOPATH" ]; then - echo '$GOPATH not defined' - exit 1 -fi - -reset() ( - cd "$GOPATH/src/$1" - export GIT_DIR="$(pwd)/.git" - [ "$(git rev-parse HEAD)" = "$2" ] || - (git fetch && git reset --hard "$2") -) - -reset github.com/junegunn/go-isatty 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8 -reset github.com/junegunn/go-runewidth 14207d285c6c197daabb5c9793d63e7af9ab2d50 -reset github.com/junegunn/go-shellwords 02e3cf038dcea8290e44424da473dd12be796a8a -reset golang.org/x/crypto abc5fa7ad02123a41f02bf1391c9760f7586e608 diff --git a/src/options.go b/src/options.go index e9e2f28..a0b28d4 100644 --- a/src/options.go +++ b/src/options.go @@ -12,7 +12,7 @@ import ( "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" - "github.com/junegunn/go-shellwords" + "github.com/mattn/go-shellwords" ) const usage = `usage: fzf [options] diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 3a8c765..e66ed2f 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -8,11 +8,10 @@ import ( "runtime" - // https://github.com/gdamore/tcell/pull/135 - "github.com/junegunn/tcell" - "github.com/junegunn/tcell/encoding" + "github.com/gdamore/tcell" + "github.com/gdamore/tcell/encoding" - "github.com/junegunn/go-runewidth" + "github.com/mattn/go-runewidth" ) func HasFullscreenRenderer() bool { diff --git a/src/util/util.go b/src/util/util.go index 15ec0ad..867935a 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -5,8 +5,8 @@ import ( "os" "time" - "github.com/junegunn/go-isatty" - "github.com/junegunn/go-runewidth" + "github.com/mattn/go-isatty" + "github.com/mattn/go-runewidth" ) var _runeWidths = make(map[rune]int) diff --git a/src/util/util_windows.go b/src/util/util_windows.go index 0664416..1bf8f7a 100644 --- a/src/util/util_windows.go +++ b/src/util/util_windows.go @@ -7,7 +7,7 @@ import ( "os/exec" "syscall" - "github.com/junegunn/go-shellwords" + "github.com/mattn/go-shellwords" ) // ExecCommand executes the given command with $SHELL From 2e3dc75425d23b4d9e8e05a901395914cf8d3120 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 2 Jun 2017 13:25:35 +0900 Subject: [PATCH 502/658] Fix inconsistent tiebreak scores when --nth is used Make sure to consistently calculate tiebreak scores based on the original line. This change may not be preferable if you filter aligned tabular input on a subset of columns using --nth. However, if we calculate length tiebreak only on the matched components instead of the entire line, the result can be very confusing when multiple --nth components are specified, so let's keep it simple and consistent. Close #926 --- src/core.go | 14 ++++++----- src/item.go | 9 ++++++++ src/pattern.go | 31 +++++++++++-------------- src/result.go | 7 +++--- src/result_test.go | 12 +++++----- src/tokenizer.go | 5 ++-- src/tokenizer_test.go | 12 +++++----- test/test_go.rb | 54 +------------------------------------------ 8 files changed, 49 insertions(+), 95 deletions(-) diff --git a/src/core.go b/src/core.go index 9b0109b..2b91ab6 100644 --- a/src/core.go +++ b/src/core.go @@ -95,9 +95,10 @@ func Run(opts *Options) { } chars, colors := ansiProcessor(data) return &Item{ - index: int32(index), - text: chars, - colors: colors} + index: int32(index), + trimLength: -1, + text: chars, + colors: colors} }) } else { chunkList = NewChunkList(func(data []byte, index int) *Item { @@ -110,9 +111,10 @@ func Run(opts *Options) { } textRunes := joinTokens(trans) item := Item{ - index: int32(index), - origText: &data, - colors: nil} + index: int32(index), + trimLength: -1, + origText: &data, + colors: nil} trimmed, colors := ansiProcessorRunes(textRunes) item.text = trimmed diff --git a/src/item.go b/src/item.go index 4e60faf..c67ac7a 100644 --- a/src/item.go +++ b/src/item.go @@ -7,6 +7,7 @@ import ( // Item represents each input line type Item struct { index int32 + trimLength int32 text util.Chars origText *[]byte colors *[]ansiOffset @@ -18,6 +19,14 @@ func (item *Item) Index() int32 { return item.index } +func (item *Item) TrimLength() int32 { + if item.trimLength >= 0 { + return item.trimLength + } + item.trimLength = int32(item.text.TrimLength()) + return item.trimLength +} + // Colors returns ansiOffsets of the Item func (item *Item) Colors() []ansiOffset { if item.colors == nil { diff --git a/src/pattern.go b/src/pattern.go index 4f64660..4614e07 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -299,20 +299,20 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []* // MatchItem returns true if the Item is a match func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) { if p.extended { - if offsets, bonus, trimLen, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) { - return buildResult(item, offsets, bonus, trimLen), offsets, pos + if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) { + return buildResult(item, offsets, bonus), offsets, pos } return nil, nil, nil } - offset, bonus, trimLen, pos := p.basicMatch(item, withPos, slab) + offset, bonus, pos := p.basicMatch(item, withPos, slab) if sidx := offset[0]; sidx >= 0 { offsets := []Offset{offset} - return buildResult(item, offsets, bonus, trimLen), offsets, pos + return buildResult(item, offsets, bonus), offsets, pos } return nil, nil, nil } -func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) { +func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) { input := p.prepareInput(item) if p.fuzzy { return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab) @@ -320,11 +320,10 @@ func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab) } -func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, int, *[]int) { +func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) { input := p.prepareInput(item) offsets := []Offset{} var totalScore int - var totalTrimLen int var allPos *[]int if withPos { allPos = &[]int{} @@ -332,16 +331,15 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of for _, termSet := range p.termSets { var offset Offset var currentScore int - var trimLen int matched := false for _, term := range termSet { pfun := p.procFun[term.typ] - off, score, tLen, pos := p.iter(pfun, input, term.caseSensitive, p.normalize, p.forward, term.text, withPos, slab) + off, score, pos := p.iter(pfun, input, term.caseSensitive, p.normalize, p.forward, term.text, withPos, slab) if sidx := off[0]; sidx >= 0 { if term.inv { continue } - offset, currentScore, trimLen = off, score, tLen + offset, currentScore = off, score matched = true if withPos { if pos != nil { @@ -354,7 +352,7 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of } break } else if term.inv { - offset, currentScore, trimLen = Offset{0, 0}, 0, 0 + offset, currentScore = Offset{0, 0}, 0 matched = true continue } @@ -362,10 +360,9 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of if matched { offsets = append(offsets, offset) totalScore += currentScore - totalTrimLen += trimLen } } - return offsets, totalScore, totalTrimLen, allPos + return offsets, totalScore, allPos } func (p *Pattern) prepareInput(item *Item) []Token { @@ -375,7 +372,7 @@ func (p *Pattern) prepareInput(item *Item) []Token { var ret []Token if len(p.nth) == 0 { - ret = []Token{Token{text: &item.text, prefixLength: 0, trimLength: int32(item.text.TrimLength())}} + ret = []Token{Token{text: &item.text, prefixLength: 0}} } else { tokens := Tokenize(item.text, p.delimiter) ret = Transform(tokens, p.nth) @@ -384,7 +381,7 @@ func (p *Pattern) prepareInput(item *Item) []Token { return ret } -func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) { +func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, *[]int) { for _, part := range tokens { if res, pos := pfun(caseSensitive, normalize, forward, *part.text, pattern, withPos, slab); res.Start >= 0 { sidx := int32(res.Start) + part.prefixLength @@ -394,8 +391,8 @@ func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, norma (*pos)[idx] += int(part.prefixLength) } } - return Offset{sidx, eidx}, res.Score, int(part.trimLength), pos + return Offset{sidx, eidx}, res.Score, pos } } - return Offset{-1, -1}, 0, -1, nil + return Offset{-1, -1}, 0, nil } diff --git a/src/result.go b/src/result.go index e071a9e..0b1fbf0 100644 --- a/src/result.go +++ b/src/result.go @@ -29,7 +29,7 @@ type Result struct { rank rank } -func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result { +func buildResult(item *Item, offsets []Offset, score int) *Result { if len(offsets) > 1 { sort.Sort(ByOrder(offsets)) } @@ -57,8 +57,7 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result { // Higher is better val = math.MaxUint16 - util.AsUint16(score) case byLength: - // If offsets is empty, trimLen will be 0, but we don't care - val = util.AsUint16(trimLen) + val = util.AsUint16(int(item.TrimLength())) case byBegin, byEnd: if validOffsetFound { whitePrefixLen := 0 @@ -72,7 +71,7 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result { if criterion == byBegin { val = util.AsUint16(minEnd - whitePrefixLen) } else { - val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/trimLen) + val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength())) } } } diff --git a/src/result_test.go b/src/result_test.go index 0e91fc8..ad510c2 100644 --- a/src/result_test.go +++ b/src/result_test.go @@ -52,7 +52,7 @@ func TestResultRank(t *testing.T) { sortCriteria = []criterion{byScore, byLength} strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} - item1 := buildResult(&Item{text: util.RunesToChars(strs[0]), index: 1}, []Offset{}, 2, 3) + item1 := buildResult(&Item{text: util.RunesToChars(strs[0]), index: 1, trimLength: -1}, []Offset{}, 2) if item1.rank.points[0] != math.MaxUint16-2 || // Bonus item1.rank.points[1] != 3 || // Length item1.rank.points[2] != 0 || // Unused @@ -61,7 +61,7 @@ func TestResultRank(t *testing.T) { t.Error(item1.rank) } // Only differ in index - item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2, 3) + item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2) items := []*Result{item1, item2} sort.Sort(ByRelevance(items)) @@ -77,10 +77,10 @@ func TestResultRank(t *testing.T) { } // Sort by relevance - item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 3, 0) - item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 4, 0) - item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 5, 0) - item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 6, 0) + item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 3) + item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 4) + item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 5) + item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 6) items = []*Result{item1, item2, item3, item4, item5, item6} sort.Sort(ByRelevance(items)) if !(items[0] == item6 && items[1] == item5 && diff --git a/src/tokenizer.go b/src/tokenizer.go index ed87362..0e216ac 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -20,7 +20,6 @@ type Range struct { type Token struct { text *util.Chars prefixLength int32 - trimLength int32 } // Delimiter for tokenizing the input @@ -81,7 +80,7 @@ func withPrefixLengths(tokens []util.Chars, begin int) []Token { prefixLength := begin for idx, token := range tokens { // NOTE: &tokens[idx] instead of &tokens - ret[idx] = Token{&tokens[idx], int32(prefixLength), int32(token.TrimLength())} + ret[idx] = Token{&tokens[idx], int32(prefixLength)} prefixLength += token.Length() } return ret @@ -242,7 +241,7 @@ func Transform(tokens []Token, withNth []Range) []Token { } else { prefixLength = 0 } - transTokens[idx] = Token{&merged, prefixLength, int32(merged.TrimLength())} + transTokens[idx] = Token{&merged, prefixLength} } return transTokens } diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go index 1dd4414..5925090 100644 --- a/src/tokenizer_test.go +++ b/src/tokenizer_test.go @@ -48,22 +48,22 @@ func TestTokenize(t *testing.T) { // AWK-style input := " abc: def: ghi " tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{}) - if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 { + if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 { t.Errorf("%s", tokens) } // With delimiter tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":")) - if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 { + if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 { t.Errorf("%s", tokens) } // With delimiter regex tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp("\\s+")) - if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 || - tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 || - tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 || - tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 { + if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 || + tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 || + tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 || + tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 14 { t.Errorf("%s", tokens) } } diff --git a/test/test_go.rb b/test/test_go.rb index 85b8f37..3c7000b 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -683,62 +683,10 @@ class TestGoFZF < TestBase ] assert_equal output, `#{FZF} -fh < #{tempname}`.split($/) - output = %w[ - 1234567:h - 12345:he - 1:hell - 123:hello - ] + # Since 0.16.8, --nth doesn't affect --tiebreak assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.split($/) end - def test_tiebreak_length_with_nth_trim_length - input = [ - "apple juice bottle 1", - "apple ui bottle 2", - "app ice bottle 3", - "app ic bottle 4", - ] - writelines tempname, input - - # len(1) - output = [ - "app ice bottle 3", - "app ic bottle 4", - "apple juice bottle 1", - "apple ui bottle 2", - ] - assert_equal output, `#{FZF} -fa -n1 < #{tempname}`.split($/) - - # len(1 ~ 2) - output = [ - "app ic bottle 4", - "app ice bottle 3", - "apple ui bottle 2", - "apple juice bottle 1", - ] - assert_equal output, `#{FZF} -fai -n1..2 < #{tempname}`.split($/) - - # len(1) + len(2) - output = [ - "app ic bottle 4", - "app ice bottle 3", - "apple ui bottle 2", - "apple juice bottle 1", - ] - assert_equal output, `#{FZF} -x -f"a i" -n1,2 < #{tempname}`.split($/) - - # len(2) - output = [ - "app ic bottle 4", - "app ice bottle 3", - "apple ui bottle 2", - "apple juice bottle 1", - ] - assert_equal output, `#{FZF} -fi -n2 < #{tempname}`.split($/) - assert_equal output, `#{FZF} -fi -n2,1..2 < #{tempname}`.split($/) - end - def test_invalid_cache tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter tmux.until { |lines| lines[-2].include? '2/3' } From 8bbf9335e13f4d56fd7008757d4a9ead34e6381c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 2 Jun 2017 16:32:15 +0900 Subject: [PATCH 503/658] Restructuring: main package in project root --- .gitignore | 4 +- .travis.yml | 11 +-- BUILD.md | 13 ++-- Makefile | 134 +++++++++++++++++++++++++++++++++++++ src/fzf/main.go => main.go | 0 src/Makefile | 119 -------------------------------- src/README.md | 106 ----------------------------- 7 files changed, 144 insertions(+), 243 deletions(-) create mode 100644 Makefile rename src/fzf/main.go => main.go (100%) delete mode 100644 src/Makefile delete mode 100644 src/README.md diff --git a/.gitignore b/.gitignore index 4e88fdf..61ef433 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -bin -src/fzf/fzf-* +bin/fzf +target pkg Gemfile.lock .DS_Store diff --git a/.travis.yml b/.travis.yml index 3f2a67f..915e871 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,13 +16,6 @@ install: - sudo apt-get install -y zsh fish script: | - export GOPATH=~/go - export FZF_BASE=$GOPATH/src/github.com/junegunn/fzf - - mkdir -p $GOPATH/src/github.com/junegunn - ln -s $(pwd) $FZF_BASE - - cd $FZF_BASE/src && make test fzf/fzf-linux_amd64 install && - cd $FZF_BASE/bin && ln -sf fzf-linux_amd64 fzf-$(./fzf --version)-linux_amd64 && - cd $FZF_BASE && yes | ./install && rm -f fzf && + make test install && + ./install --all && tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ] diff --git a/BUILD.md b/BUILD.md index f52f395..390d8c2 100644 --- a/BUILD.md +++ b/BUILD.md @@ -10,20 +10,19 @@ Build instructions ### Using Makefile -```sh -# Source files are located in src directory -cd src +Makefile will set up and use its own `$GOPATH` under the project root. -# Build fzf binary for your platform in src/fzf +```sh +# Build fzf binary for your platform in target make # Build fzf binary and copy it to bin directory make install -# Build 32-bit and 64-bit executables and tarballs +# Build 32-bit and 64-bit executables and tarballs in target make release -# Make release archives for all supported platforms +# Make release archives for all supported platforms in target make release-all ``` @@ -33,7 +32,7 @@ Alternatively, you can build fzf directly with `go get` command without manually cloning the repository. ```sh -go get -u github.com/junegunn/fzf/src/fzf +go get -u github.com/junegunn/fzf ``` Third-party libraries used diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..660af46 --- /dev/null +++ b/Makefile @@ -0,0 +1,134 @@ +ifndef GOOS +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) + GOOS := darwin +else ifeq ($(UNAME_S),Linux) + GOOS := linux +else +$(error "$$GOOS is not defined.") +endif +endif + +ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +GOPATH := $(ROOT_DIR)/gopath +SRC_LINK := $(GOPATH)/src/github.com/junegunn/fzf/src +VENDOR_LINK := $(GOPATH)/src/github.com/junegunn/fzf/vendor + +GLIDE_YAML := glide.yaml +GLIDE_LOCK := glide.lock +SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(SRC_LINK) $(VENDOR_LINK) $(GLIDE_LOCK) + +BINARY32 := fzf-$(GOOS)_386 +BINARY64 := fzf-$(GOOS)_amd64 +BINARYARM5 := fzf-$(GOOS)_arm5 +BINARYARM6 := fzf-$(GOOS)_arm6 +BINARYARM7 := fzf-$(GOOS)_arm7 +BINARYARM8 := fzf-$(GOOS)_arm8 +VERSION := $(shell awk -F= '/version =/ {print $$2}' src/constants.go | tr -d "\" ") +RELEASE32 := fzf-$(VERSION)-$(GOOS)_386 +RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64 +RELEASEARM5 := fzf-$(VERSION)-$(GOOS)_arm5 +RELEASEARM6 := fzf-$(VERSION)-$(GOOS)_arm6 +RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7 +RELEASEARM8 := fzf-$(VERSION)-$(GOOS)_arm8 +export GOPATH + +# https://en.wikipedia.org/wiki/Uname +UNAME_M := $(shell uname -m) +ifeq ($(UNAME_M),x86_64) + BINARY := $(BINARY64) +else ifeq ($(UNAME_M),amd64) + BINARY := $(BINARY64) +else ifeq ($(UNAME_M),i686) + BINARY := $(BINARY32) +else ifeq ($(UNAME_M),i386) + BINARY := $(BINARY32) +else ifeq ($(UNAME_M),armv5l) + BINARY := $(BINARYARM5) +else ifeq ($(UNAME_M),armv6l) + BINARY := $(BINARYARM6) +else ifeq ($(UNAME_M),armv7l) + BINARY := $(BINARYARM7) +else +$(error "Build on $(UNAME_M) is not supported, yet.") +endif + +all: target/$(BINARY) + +target: + mkdir -p $@ + +ifeq ($(GOOS),windows) +release: target/$(BINARY32) target/$(BINARY64) + cd target && cp -f $(BINARY32) fzf.exe && zip $(RELEASE32).zip bin/fzf.exe + cd target && cp -f $(BINARY64) fzf.exe && zip $(RELEASE64).zip bin/fzf.exe + cd target && rm -f fzf.exe +else ifeq ($(GOOS),linux) +release: target/$(BINARY32) target/$(BINARY64) target/$(BINARYARM5) target/$(BINARYARM6) target/$(BINARYARM7) target/$(BINARYARM8) + cd target && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf + cd target && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf + cd target && cp -f $(BINARYARM5) fzf && tar -czf $(RELEASEARM5).tgz fzf + cd target && cp -f $(BINARYARM6) fzf && tar -czf $(RELEASEARM6).tgz fzf + cd target && cp -f $(BINARYARM7) fzf && tar -czf $(RELEASEARM7).tgz fzf + cd target && cp -f $(BINARYARM8) fzf && tar -czf $(RELEASEARM8).tgz fzf + cd target && rm -f fzf +else +release: target/$(BINARY32) target/$(BINARY64) + cd target && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf + cd target && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf + cd target && rm -f fzf +endif + +release-all: clean test + GOOS=darwin make release + GOOS=linux make release + GOOS=freebsd make release + GOOS=openbsd make release + GOOS=windows make release + +$(SRC_LINK): + mkdir -p $(shell dirname $(SRC_LINK)) + ln -s $(ROOT_DIR)/src $(SRC_LINK) + +$(VENDOR_LINK): + mkdir -p $(shell dirname $(VENDOR_LINK)) + ln -s $(ROOT_DIR)/vendor $(VENDOR_LINK) + +$(GLIDE_LOCK): $(GLIDE_YAML) + go get -u github.com/Masterminds/glide && $(GOPATH)/bin/glide install && touch $@ + +test: $(SOURCES) + 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 + +install: bin/fzf + +clean: + rm -rf target + +target/$(BINARY32): $(SOURCES) + GOARCH=386 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $@ + +target/$(BINARY64): $(SOURCES) + GOARCH=amd64 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $@ + +# https://github.com/golang/go/wiki/GoArm +target/$(BINARYARM5): $(SOURCES) + GOARCH=arm GOARM=5 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $@ + +target/$(BINARYARM6): $(SOURCES) + GOARCH=arm GOARM=6 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $@ + +target/$(BINARYARM7): $(SOURCES) + GOARCH=arm GOARM=7 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $@ + +target/$(BINARYARM8): $(SOURCES) + GOARCH=arm64 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $@ + +bin/fzf: target/$(BINARY) | bin + cp -f target/$(BINARY) bin/fzf + +.PHONY: all release release-all test install clean diff --git a/src/fzf/main.go b/main.go similarity index 100% rename from src/fzf/main.go rename to main.go diff --git a/src/Makefile b/src/Makefile deleted file mode 100644 index 5205b1c..0000000 --- a/src/Makefile +++ /dev/null @@ -1,119 +0,0 @@ -ifndef GOOS -UNAME_S := $(shell uname -s) -ifeq ($(UNAME_S),Darwin) - GOOS := darwin -else ifeq ($(UNAME_S),Linux) - GOOS := linux -else -$(error "$$GOOS is not defined.") -endif -endif - -SOURCES := $(wildcard *.go */*.go) -ROOTDIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) -BINDIR := $(shell dirname $(ROOTDIR))/bin -BINARY32 := fzf-$(GOOS)_386 -BINARY64 := fzf-$(GOOS)_amd64 -BINARYARM5 := fzf-$(GOOS)_arm5 -BINARYARM6 := fzf-$(GOOS)_arm6 -BINARYARM7 := fzf-$(GOOS)_arm7 -BINARYARM8 := fzf-$(GOOS)_arm8 -VERSION := $(shell awk -F= '/version =/ {print $$2}' constants.go | tr -d "\" ") -RELEASE32 := fzf-$(VERSION)-$(GOOS)_386 -RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64 -RELEASEARM5 := fzf-$(VERSION)-$(GOOS)_arm5 -RELEASEARM6 := fzf-$(VERSION)-$(GOOS)_arm6 -RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7 -RELEASEARM8 := fzf-$(VERSION)-$(GOOS)_arm8 - -# https://en.wikipedia.org/wiki/Uname -UNAME_M := $(shell uname -m) -ifeq ($(UNAME_M),x86_64) - BINARY := $(BINARY64) -else ifeq ($(UNAME_M),amd64) - BINARY := $(BINARY64) -else ifeq ($(UNAME_M),i686) - BINARY := $(BINARY32) -else ifeq ($(UNAME_M),i386) - BINARY := $(BINARY32) -else ifeq ($(UNAME_M),armv5l) - BINARY := $(BINARYARM5) -else ifeq ($(UNAME_M),armv6l) - BINARY := $(BINARYARM6) -else ifeq ($(UNAME_M),armv7l) - BINARY := $(BINARYARM7) -else -$(error "Build on $(UNAME_M) is not supported, yet.") -endif - -all: fzf/$(BINARY) - -ifeq ($(GOOS),windows) -release: fzf/$(BINARY32) fzf/$(BINARY64) - cd fzf && cp -f $(BINARY32) fzf.exe && zip $(RELEASE32).zip fzf.exe - cd fzf && cp -f $(BINARY64) fzf.exe && zip $(RELEASE64).zip fzf.exe - cd fzf && rm -f fzf.exe -else ifeq ($(GOOS),linux) -release: fzf/$(BINARY32) fzf/$(BINARY64) fzf/$(BINARYARM5) fzf/$(BINARYARM6) fzf/$(BINARYARM7) fzf/$(BINARYARM8) - cd fzf && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf - cd fzf && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf - cd fzf && cp -f $(BINARYARM5) fzf && tar -czf $(RELEASEARM5).tgz fzf - cd fzf && cp -f $(BINARYARM6) fzf && tar -czf $(RELEASEARM6).tgz fzf - cd fzf && cp -f $(BINARYARM7) fzf && tar -czf $(RELEASEARM7).tgz fzf - cd fzf && cp -f $(BINARYARM8) fzf && tar -czf $(RELEASEARM8).tgz fzf - cd fzf && rm -f fzf -else -release: fzf/$(BINARY32) fzf/$(BINARY64) - cd fzf && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf - cd fzf && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf - cd fzf && rm -f fzf -endif - -release-all: clean test - GOOS=darwin make release - GOOS=linux make release - GOOS=freebsd make release - GOOS=openbsd make release - GOOS=windows make release - -deps: $(SOURCES) - cd .. && go get -u github.com/Masterminds/glide && $(GOPATH)/bin/glide install - -test: deps - SHELL=/bin/sh GOOS= go test -v -tags "$(TAGS)" ./... - -install: $(BINDIR)/fzf - -uninstall: - rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY) - -clean: - cd fzf && rm -f fzf-* - -fzf/$(BINARY32): deps - cd fzf && GOARCH=386 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY32) - -fzf/$(BINARY64): deps - cd fzf && GOARCH=amd64 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY64) - -# https://github.com/golang/go/wiki/GoArm -fzf/$(BINARYARM5): deps - cd fzf && GOARCH=arm GOARM=5 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM5) - -fzf/$(BINARYARM6): deps - cd fzf && GOARCH=arm GOARM=6 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM6) - -fzf/$(BINARYARM7): deps - cd fzf && GOARCH=arm GOARM=7 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM7) - -fzf/$(BINARYARM8): deps - cd fzf && GOARCH=arm64 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARYARM8) - -$(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR) - cp -f fzf/$(BINARY) $(BINDIR) - cd $(BINDIR) && ln -sf $(BINARY) fzf - -$(BINDIR): - mkdir -p $@ - -.PHONY: all deps release release-all test install uninstall clean diff --git a/src/README.md b/src/README.md deleted file mode 100644 index ec88ca2..0000000 --- a/src/README.md +++ /dev/null @@ -1,106 +0,0 @@ -fzf in Go -========= - -fzf in go - -This directory contains the source code for the new fzf implementation in -[Go][go]. - -Upgrade from Ruby version -------------------------- - -The install script has been updated to download the right binary for your -system. If you already have installed fzf, simply git-pull the repository and -rerun the install script. - -```sh -cd ~/.fzf -git pull -./install -``` - -Otherwise, follow [the instruction][install] as before. You can also install -fzf using Homebrew if you prefer that way. - -Motivations ------------ - -### No Ruby dependency - -There have always been complaints about fzf being a Ruby script. To make -matters worse, Ruby 2.1 removed ncurses binding from its standard libary. -Because of the change, users running Ruby 2.1 or above are forced to build C -extensions of curses gem to meet the requirement of fzf. The new Go version -will be distributed as an executable binary so it will be much more accessible -and should be easier to setup. - -### Performance - -Many people have been surprised to see how fast fzf is even when it was -written in Ruby. It stays quite responsive even for 100k+ lines, which is -well above the size of the usual input. - -The new Go version, of course, is significantly faster than that. It has all -the performance optimization techniques used in Ruby implementation and more. -It also doesn't suffer from [GIL][gil], so the search performance scales -proportional to the number of CPU cores. On my MacBook Pro (Mid 2012), the new -version was shown to be an order of magnitude faster on certain cases. It also -starts much faster though the difference may not be noticeable. - -Build ------ - -See [BUILD.md](../BUILD.md) - -Test ----- - -Unit tests can be run with `make test`. Integration tests are written in Ruby -script that should be run on tmux. - -```sh -cd src - -# Unit tests -make test - -# Integration tests -ruby ../test/test_go.rb - -# Build binary for the platform -make - -# Install the executable to ../bin directory -make install - -# Make release archives -make release - -# Make release archives for all supported platforms -make release-all -``` - -Third-party libraries used --------------------------- - -- ~[ncurses][ncurses]~ -- [mattn/go-runewidth](https://github.com/mattn/go-runewidth) - - Licensed under [MIT](http://mattn.mit-license.org) -- [mattn/go-shellwords](https://github.com/mattn/go-shellwords) - - Licensed under [MIT](http://mattn.mit-license.org) -- [mattn/go-isatty](https://github.com/mattn/go-isatty) - - Licensed under [MIT](http://mattn.mit-license.org) -- [tcell](https://github.com/gdamore/tcell) - - Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE) - -License -------- - -[MIT](LICENSE) - -[install]: https://github.com/junegunn/fzf#installation -[go]: https://golang.org/ -[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock -[ncurses]: https://www.gnu.org/software/ncurses/ -[req]: http://golang.org/doc/install -[tcell]: https://github.com/gdamore/tcell From 83e9af6601f2a7b7fc0fff06fc6767b7054a5467 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 2 Jun 2017 17:57:28 +0900 Subject: [PATCH 504/658] Add git revision to --version output --- Makefile | 26 +++++++++++++++----------- install | 2 +- main.go | 4 +++- src/core.go | 8 ++++++-- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index 660af46..f2426e8 100644 --- a/Makefile +++ b/Makefile @@ -9,14 +9,19 @@ $(error "$$GOOS is not defined.") endif endif -ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST))) +ROOT_DIR := $(shell dirname $(MAKEFILE)) GOPATH := $(ROOT_DIR)/gopath SRC_LINK := $(GOPATH)/src/github.com/junegunn/fzf/src VENDOR_LINK := $(GOPATH)/src/github.com/junegunn/fzf/vendor +export GOPATH GLIDE_YAML := glide.yaml GLIDE_LOCK := glide.lock -SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(SRC_LINK) $(VENDOR_LINK) $(GLIDE_LOCK) +SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(SRC_LINK) $(VENDOR_LINK) $(GLIDE_LOCK) $(MAKEFILE) + +REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES)) +BUILD_FLAGS := -a -ldflags "-X main.revision=$(REVISION) -w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" BINARY32 := fzf-$(GOOS)_386 BINARY64 := fzf-$(GOOS)_amd64 @@ -31,7 +36,6 @@ RELEASEARM5 := fzf-$(VERSION)-$(GOOS)_arm5 RELEASEARM6 := fzf-$(VERSION)-$(GOOS)_arm6 RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7 RELEASEARM8 := fzf-$(VERSION)-$(GOOS)_arm8 -export GOPATH # https://en.wikipedia.org/wiki/Uname UNAME_M := $(shell uname -m) @@ -60,8 +64,8 @@ target: ifeq ($(GOOS),windows) release: target/$(BINARY32) target/$(BINARY64) - cd target && cp -f $(BINARY32) fzf.exe && zip $(RELEASE32).zip bin/fzf.exe - cd target && cp -f $(BINARY64) fzf.exe && zip $(RELEASE64).zip bin/fzf.exe + cd target && cp -f $(BINARY32) fzf.exe && zip $(RELEASE32).zip fzf.exe + cd target && cp -f $(BINARY64) fzf.exe && zip $(RELEASE64).zip fzf.exe cd target && rm -f fzf.exe else ifeq ($(GOOS),linux) release: target/$(BINARY32) target/$(BINARY64) target/$(BINARYARM5) target/$(BINARYARM6) target/$(BINARYARM7) target/$(BINARYARM8) @@ -110,23 +114,23 @@ clean: rm -rf target target/$(BINARY32): $(SOURCES) - GOARCH=386 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $@ + GOARCH=386 go build $(BUILD_FLAGS) -o $@ target/$(BINARY64): $(SOURCES) - GOARCH=amd64 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $@ + GOARCH=amd64 go build $(BUILD_FLAGS) -o $@ # https://github.com/golang/go/wiki/GoArm target/$(BINARYARM5): $(SOURCES) - GOARCH=arm GOARM=5 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $@ + GOARCH=arm GOARM=5 go build $(BUILD_FLAGS) -o $@ target/$(BINARYARM6): $(SOURCES) - GOARCH=arm GOARM=6 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $@ + GOARCH=arm GOARM=6 go build $(BUILD_FLAGS) -o $@ target/$(BINARYARM7): $(SOURCES) - GOARCH=arm GOARM=7 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $@ + GOARCH=arm GOARM=7 go build $(BUILD_FLAGS) -o $@ target/$(BINARYARM8): $(SOURCES) - GOARCH=arm64 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $@ + GOARCH=arm64 go build $(BUILD_FLAGS) -o $@ bin/fzf: target/$(BINARY) | bin cp -f target/$(BINARY) bin/fzf diff --git a/install b/install index 9a51207..516c4cb 100755 --- a/install +++ b/install @@ -72,7 +72,7 @@ ask() { check_binary() { echo -n " - Checking fzf executable ... " local output - output=$("$fzf_base"/bin/fzf --version 2>&1) + output=$("$fzf_base"/bin/fzf --version 2>&1 | awk '{print $1}') if [ $? -ne 0 ]; then echo "Error: $output" binary_error="Invalid binary" diff --git a/main.go b/main.go index 29d4767..3b59244 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,8 @@ package main import "github.com/junegunn/fzf/src" +var revision string + func main() { - fzf.Run(fzf.ParseOptions()) + fzf.Run(fzf.ParseOptions(), revision) } diff --git a/src/core.go b/src/core.go index 2b91ab6..6c3b321 100644 --- a/src/core.go +++ b/src/core.go @@ -43,12 +43,16 @@ Matcher -> EvtHeader -> Terminal (update header) */ // Run starts fzf -func Run(opts *Options) { +func Run(opts *Options, revision string) { sort := opts.Sort > 0 sortCriteria = opts.Criteria if opts.Version { - fmt.Println(version) + if len(revision) > 0 { + fmt.Printf("%s (%s)\n", version, revision) + } else { + fmt.Println(version) + } os.Exit(exitOk) } From 34f16e5b7ddf18cae15ba646583c492ed8437e73 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 2 Jun 2017 18:19:21 +0900 Subject: [PATCH 505/658] Fix Makefile and install script for the new project layout --- Makefile | 20 ++++++++++---------- install | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index f2426e8..5c15f69 100644 --- a/Makefile +++ b/Makefile @@ -92,16 +92,16 @@ release-all: clean test $(SRC_LINK): mkdir -p $(shell dirname $(SRC_LINK)) - ln -s $(ROOT_DIR)/src $(SRC_LINK) + ln -sf $(ROOT_DIR)/src $(SRC_LINK) $(VENDOR_LINK): mkdir -p $(shell dirname $(VENDOR_LINK)) - ln -s $(ROOT_DIR)/vendor $(VENDOR_LINK) + ln -sf $(ROOT_DIR)/vendor $(VENDOR_LINK) -$(GLIDE_LOCK): $(GLIDE_YAML) +vendor: $(GLIDE_YAML) go get -u github.com/Masterminds/glide && $(GOPATH)/bin/glide install && touch $@ -test: $(SOURCES) +test: $(SOURCES) vendor SHELL=/bin/sh GOOS= go test -v -tags "$(TAGS)" \ github.com/junegunn/fzf/src \ github.com/junegunn/fzf/src/algo \ @@ -113,23 +113,23 @@ install: bin/fzf clean: rm -rf target -target/$(BINARY32): $(SOURCES) +target/$(BINARY32): $(SOURCES) vendor GOARCH=386 go build $(BUILD_FLAGS) -o $@ -target/$(BINARY64): $(SOURCES) +target/$(BINARY64): $(SOURCES) vendor GOARCH=amd64 go build $(BUILD_FLAGS) -o $@ # https://github.com/golang/go/wiki/GoArm -target/$(BINARYARM5): $(SOURCES) +target/$(BINARYARM5): $(SOURCES) vendor GOARCH=arm GOARM=5 go build $(BUILD_FLAGS) -o $@ -target/$(BINARYARM6): $(SOURCES) +target/$(BINARYARM6): $(SOURCES) vendor GOARCH=arm GOARM=6 go build $(BUILD_FLAGS) -o $@ -target/$(BINARYARM7): $(SOURCES) +target/$(BINARYARM7): $(SOURCES) vendor GOARCH=arm GOARM=7 go build $(BUILD_FLAGS) -o $@ -target/$(BINARYARM8): $(SOURCES) +target/$(BINARYARM8): $(SOURCES) vendor GOARCH=arm64 go build $(BUILD_FLAGS) -o $@ bin/fzf: target/$(BINARY) | bin diff --git a/install b/install index 516c4cb..7c3b1f3 100755 --- a/install +++ b/install @@ -169,12 +169,12 @@ if [ -n "$binary_error" ]; then echo " - $binary_error !!!" fi if command -v go > /dev/null; then - echo -n "Building binary (go get -u github.com/junegunn/fzf/src/fzf) ... " + echo -n "Building binary (go get -u github.com/junegunn/fzf) ... " if [ -z "${GOPATH-}" ]; then export GOPATH="${TMPDIR:-/tmp}/fzf-gopath" mkdir -p "$GOPATH" fi - if go get -u github.com/junegunn/fzf/src/fzf; then + if go get -u github.com/junegunn/fzf; then echo "OK" cp "$GOPATH/bin/fzf" "$fzf_base/bin/" else From ca0b3b6fd7517aaedcb7517804e2040a8a7aad95 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 3 Jun 2017 19:47:53 +0900 Subject: [PATCH 506/658] Fixes for Cygwin - Update install script to download Windows binary if $TERM == cygwin - Unset TERM if $TERM == cygwin (#933) - Always use cmd.exe instead of $SHELL when running commands --- install | 46 +++++++++++++++++++++++++--------------- src/tui/tcell.go | 4 ++++ src/util/util_windows.go | 6 +----- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/install b/install index 7c3b1f3..150cea6 100755 --- a/install +++ b/install @@ -99,11 +99,23 @@ link_fzf_in_path() { } try_curl() { - command -v curl > /dev/null && curl -fL $1 | tar -xzf - + command -v curl > /dev/null && + if [[ $1 =~ tgz$ ]]; then + curl -fL $1 | tar -xzf - + else + local temp=${TMPDIR:-/tmp}/fzf.zip + curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp" + fi } try_wget() { - command -v wget > /dev/null && wget -O - $1 | tar -xzf - + command -v wget > /dev/null && + if [[ $1 =~ tgz$ ]]; then + wget -O - $1 | tar -xzf - + else + local temp=${TMPDIR:-/tmp}/fzf.zip + wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp" + fi } download() { @@ -123,8 +135,8 @@ download() { local url [[ "$version" =~ alpha ]] && - url=https://github.com/junegunn/fzf-bin/releases/download/alpha/${1}.tgz || - url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz + url=https://github.com/junegunn/fzf-bin/releases/download/alpha/${1} || + url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1} set -o pipefail if ! (try_curl $url || try_wget $url); then set +o pipefail @@ -146,18 +158,19 @@ archi=$(uname -sm) binary_available=1 binary_error="" case "$archi" in - Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64} ;; - Darwin\ *86) download fzf-$version-darwin_${binary_arch:-386} ;; - Linux\ *64) download fzf-$version-linux_${binary_arch:-amd64} ;; - Linux\ *86) download fzf-$version-linux_${binary_arch:-386} ;; - Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5} ;; - Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6} ;; - Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7} ;; - Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8} ;; - FreeBSD\ *64) download fzf-$version-freebsd_${binary_arch:-amd64} ;; - FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386} ;; - OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64} ;; - OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386} ;; + Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64}.tgz ;; + Darwin\ *86) download fzf-$version-darwin_${binary_arch:-386}.tgz ;; + Linux\ *64) download fzf-$version-linux_${binary_arch:-amd64}.tgz ;; + Linux\ *86) download fzf-$version-linux_${binary_arch:-386}.tgz ;; + Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5}.tgz ;; + Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6}.tgz ;; + Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7}.tgz ;; + Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;; + FreeBSD\ *64) download fzf-$version-freebsd_${binary_arch:-amd64}.tgz ;; + FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386}.tgz ;; + OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64}.tgz ;; + OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386}.tgz ;; + CYGWIN*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;; *) binary_available=0 binary_error=1 ;; esac @@ -341,4 +354,3 @@ if [ $update_config -eq 1 ]; then echo fi echo 'For more information, see: https://github.com/junegunn/fzf' - diff --git a/src/tui/tcell.go b/src/tui/tcell.go index e66ed2f..0c80de2 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -3,6 +3,7 @@ package tui import ( + "os" "time" "unicode/utf8" @@ -140,6 +141,9 @@ func (r *FullscreenRenderer) initScreen() { } func (r *FullscreenRenderer) Init() { + if os.Getenv("TERM") == "cygwin" { + os.Setenv("TERM", "") + } encoding.Register() r.initScreen() diff --git a/src/util/util_windows.go b/src/util/util_windows.go index 1bf8f7a..efd19a2 100644 --- a/src/util/util_windows.go +++ b/src/util/util_windows.go @@ -12,15 +12,11 @@ import ( // ExecCommand executes the given command with $SHELL func ExecCommand(command string) *exec.Cmd { - shell := os.Getenv("SHELL") - if len(shell) == 0 { - shell = "cmd" - } args, _ := shellwords.Parse(command) allArgs := make([]string, len(args)+1) allArgs[0] = "/c" copy(allArgs[1:], args) - return exec.Command(shell, allArgs...) + return exec.Command("cmd", allArgs...) } // IsWindows returns true on Windows From 773133c4ce80b55ebc2bc9a79d3ec1c6b5b03d17 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jun 2017 15:15:46 +0900 Subject: [PATCH 507/658] [vim] Allow running install --bin on Cygwin --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 8bdeb3b..d3dcadf 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -110,7 +110,7 @@ function! s:fzf_exec() let s:exec = s:fzf_go elseif executable('fzf') let s:exec = 'fzf' - elseif s:is_win + elseif s:is_win && !has('win32unix') call s:warn('fzf executable not found.') call s:warn('Download fzf binary for Windows from https://github.com/junegunn/fzf-bin/releases/') call s:warn('and place it as '.s:base_dir.'\bin\fzf.exe') From bf0cb4bfe208cecb99ac27e6770c6d034d1bff3c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jun 2017 16:23:47 +0900 Subject: [PATCH 508/658] Use find as the default command on Cygwin environment --- src/constants.go | 13 +++++++++++++ src/constants_unix.go | 8 -------- src/constants_windows.go | 8 -------- 3 files changed, 13 insertions(+), 16 deletions(-) delete mode 100644 src/constants_unix.go delete mode 100644 src/constants_windows.go diff --git a/src/constants.go b/src/constants.go index 941fdcc..0688990 100644 --- a/src/constants.go +++ b/src/constants.go @@ -1,6 +1,7 @@ package fzf import ( + "os" "time" "github.com/junegunn/fzf/src/util" @@ -47,6 +48,18 @@ const ( defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+" ) +var defaultCommand string + +func init() { + if !util.IsWindows() { + defaultCommand = `command find -L . -mindepth 1 \( -path '*/\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-` + } else if os.Getenv("TERM") == "cygwin" { + defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"` + } else { + defaultCommand = `dir /s/b` + } +} + // fzf events const ( EvtReadNew util.EventType = iota diff --git a/src/constants_unix.go b/src/constants_unix.go deleted file mode 100644 index 94371e6..0000000 --- a/src/constants_unix.go +++ /dev/null @@ -1,8 +0,0 @@ -// +build !windows - -package fzf - -const ( - // Reader - defaultCommand = `command find -L . -mindepth 1 \( -path '*/\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-` -) diff --git a/src/constants_windows.go b/src/constants_windows.go deleted file mode 100644 index efd3f11..0000000 --- a/src/constants_windows.go +++ /dev/null @@ -1,8 +0,0 @@ -// +build windows - -package fzf - -const ( - // Reader - defaultCommand = `dir /s/b` -) From 3cf9ae04c7d8ed13906baea2d12258e44a94b306 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 4 Jun 2017 23:24:57 +0900 Subject: [PATCH 509/658] [fzf-tmux] Fix cleanup of temporary files Close #935 --- bin/fzf-tmux | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index b01d995..5ec0dc4 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -134,7 +134,7 @@ fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id" fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id" fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id" cleanup() { - rm -f $argsf $fifo1 $fifo2 $fifo3 + \rm -f $argsf $fifo1 $fifo2 $fifo3 # Remove temp window if we were zoomed if [[ -n "$zoomed" ]]; then @@ -150,6 +150,7 @@ cleanup() { fi } trap 'cleanup 1' SIGUSR1 +trap 'cleanup' EXIT envs="env TERM=$TERM " [[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")" @@ -169,7 +170,7 @@ for arg in "${args[@]}"; do done pppid=$$ -trap_set="trap 'kill -SIGUSR1 $pppid' EXIT SIGINT SIGTERM" +trap_set="trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM" trap_unset="trap - EXIT SIGINT SIGTERM" if [[ -n "$term" ]] || [[ -t 0 ]]; then From 7e483b0c25224ae218bcc2fb67896d6b4d1c8ea0 Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Mon, 5 Jun 2017 00:54:47 -0400 Subject: [PATCH 510/658] [vim] Add support for Cygwin (#933) --- plugin/fzf.vim | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index d3dcadf..99fda1f 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -386,10 +386,10 @@ try let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) let use_height = has_key(dict, 'down') && - \ !(has('nvim') || s:is_win || s:present(dict, 'up', 'left', 'right')) && + \ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right')) && \ executable('tput') && filereadable('/dev/tty') let use_term = has('nvim') && !s:is_win - let use_tmux = (!use_height && !use_term || prefer_tmux) && s:tmux_enabled() && s:splittable(dict) + let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict) if prefer_tmux && use_tmux let use_height = 0 let use_term = 0 @@ -485,7 +485,7 @@ function! s:xterm_launcher() \ &columns, &lines/2, getwinposx(), getwinposy()) endfunction unlet! s:launcher -if s:is_win +if s:is_win || has('win32unix') let s:launcher = '%s' else let s:launcher = function('s:xterm_launcher') @@ -537,6 +537,11 @@ function! s:execute(dict, command, use_height, temps) abort call jobstart(cmd, fzf) return [] endif + elseif has('win32unix') && $TERM !=# 'cygwin' + let shellscript = s:fzf_tempname() + call writefile([command], shellscript) + let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript) + let a:temps.shellscript = shellscript endif if a:use_height let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty' @@ -760,6 +765,8 @@ function! s:cmd(bang, ...) abort let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '') if s:is_win && !&shellslash let opts.dir = substitute(opts.dir, '/', '\\', 'g') + elseif has('win32unix') + let opts.dir = fnamemodify(opts.dir, ':p') endif let prompt = opts.dir else From b49f22cdf9f2ca021382ddeb026c739a22120781 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 5 Jun 2017 23:21:50 +0900 Subject: [PATCH 511/658] 0.16.8 --- CHANGELOG.md | 14 ++++++++++++++ install | 2 +- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2cdc30..fb8557c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ CHANGELOG ========= +0.16.8 +------ +- New `change` event and `top` action for `--bind` + - `fzf --bind change:top` + - Move cursor to the top result whenever the query string is changed + - `fzf --bind ctrl-u:unix-word-rubout+top` + - `top` combined with `unix-word-rubout` +- Fixed inconsistent tiebreak scores when `--nth` is used +- Proper display of tab characters in `--prompt` +- Fixed not to `--cycle` on page-up/page-down to prevent overshoot +- Git revision in `--version` output +- Basic support for Cygwin environment +- Many fixes in Vim plugin on Windows/Cygwin (thanks to @janlazo) + 0.16.7 ------ - Added support for `ctrl-alt-[a-z]` key chords diff --git a/install b/install index 150cea6..40f5ec4 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.16.7 +version=0.16.8 auto_completion= key_bindings= update_config=2 diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 71b4e35..a9db142 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Apr 2017" "fzf 0.16.7" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Jun 2017" "fzf 0.16.8" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index c55c900..291a184 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Apr 2017" "fzf 0.16.7" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jun 2017" "fzf 0.16.8" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 0688990..56097ff 100644 --- a/src/constants.go +++ b/src/constants.go @@ -9,7 +9,7 @@ import ( const ( // Current version - version = "0.16.7" + version = "0.16.8" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From a4d78e2200d9954eddb412c26d0934e791148221 Mon Sep 17 00:00:00 2001 From: Uri Gorelik Date: Wed, 7 Jun 2017 21:02:34 -0400 Subject: [PATCH 512/658] Update CHANGELOG with unix-line-discard+top (#940) Also change the example binding for `unix-word-rubout` to *ctrl-w* instead of *ctrl-u* --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb8557c..ffb10fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,8 @@ CHANGELOG - New `change` event and `top` action for `--bind` - `fzf --bind change:top` - Move cursor to the top result whenever the query string is changed - - `fzf --bind ctrl-u:unix-word-rubout+top` - - `top` combined with `unix-word-rubout` + - `fzf --bind 'ctrl-w:unix-word-rubout+top,ctrl-u:unix-line-discard+top'` + - `top` combined with `unix-word-rubout` and `unix-line-discard` - Fixed inconsistent tiebreak scores when `--nth` is used - Proper display of tab characters in `--prompt` - Fixed not to `--cycle` on page-up/page-down to prevent overshoot From 050777b8c4ad94a9d1821cdd6868d1b59bcd7ec3 Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Wed, 7 Jun 2017 21:25:35 -0400 Subject: [PATCH 513/658] [vim] Uncomment test case to escape % in cmd.exe (#941) --- test/fzf.vader | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fzf.vader b/test/fzf.vader index 64a5c7b..c2d8d53 100644 --- a/test/fzf.vader +++ b/test/fzf.vader @@ -163,7 +163,7 @@ Execute (fzf#shellescape with cmd.exe): AssertEqual '^"C:\Program Files ^(x86^)\\^"', fzf#shellescape('C:\Program Files (x86)\', 'cmd.exe') AssertEqual '^"C:/Program Files ^(x86^)/^"', fzf#shellescape('C:/Program Files (x86)/', 'cmd.exe') - " AssertEqual '^"%%USERPROFILE%%^", fzf#shellescape('%USERPROFILE%', 'cmd.exe') + AssertEqual '^"%%USERPROFILE%%^"', fzf#shellescape('%USERPROFILE%', 'cmd.exe') Execute (Cleanup): unlet g:dir From 5efdeccdbbf9dba6296f16de65efc6c4ad3a22a9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 9 Jun 2017 12:00:59 +0900 Subject: [PATCH 514/658] [vim] Expand 'dir' on Cygwin to handle Windows-style paths See https://github.com/junegunn/fzf/pull/933#discussion_r120011934 Close https://github.com/junegunn/fzf.vim/pull/386 --- plugin/fzf.vim | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 99fda1f..bad7449 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -361,6 +361,9 @@ try if has('nvim') && !has_key(dict, 'dir') let dict.dir = s:fzf_getcwd() endif + if has('win32unix') && has_key(dict, 'dir') + let dict.dir = fnamemodify(dict.dir, ':p') + endif if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) let temps.source = s:fzf_tempname().(s:is_win ? '.bat' : '') @@ -765,8 +768,6 @@ function! s:cmd(bang, ...) abort let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '') if s:is_win && !&shellslash let opts.dir = substitute(opts.dir, '/', '\\', 'g') - elseif has('win32unix') - let opts.dir = fnamemodify(opts.dir, ':p') endif let prompt = opts.dir else From 842a73357c9c1933601e4f0c4fd6607e1647ec70 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 10 Jun 2017 13:34:49 +0900 Subject: [PATCH 515/658] [fish] Fix CTRL-T with paths that don't start with ./ Close #943 --- shell/key-bindings.fish | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index e424f1a..0cff4da 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -19,7 +19,7 @@ function fzf_key_bindings command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ -o -type f -print \ -o -type d -print \ - -o -type l -print 2> /dev/null | cut -b3-" + -o -type l -print 2> /dev/null | sed 's@^\./@@'" set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% begin From 1a68698d76d3a07b5c2c41621ca4feb51ec9afdc Mon Sep 17 00:00:00 2001 From: John Nguyen Date: Mon, 12 Jun 2017 02:24:45 -0700 Subject: [PATCH 516/658] [fish] Fix completion for current dir search (#946) If "." is given as the argument to begin completion, the leading "." is not correctly removed. In general, if user selects a fzf completion, the current token should be "consumed". --- shell/key-bindings.fish | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index 0cff4da..238975f 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -29,9 +29,7 @@ function fzf_key_bindings if [ -z "$result" ] commandline -f repaint return - end - - if [ "$dir" != . ] + else # Remove last token from commandline. commandline -t "" end From bf928624594f7a35be55c2450488da66753785bc Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 20 Jun 2017 14:15:11 +0900 Subject: [PATCH 517/658] Update man page: missing name "border" for --color --- man/man1/fzf.1 | 1 + 1 file changed, 1 insertion(+) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 291a184..dbd65bb 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -236,6 +236,7 @@ e.g. \fBfzf --color=bg+:24\fR \fBbg+ \fRBackground (current line) \fBhl+ \fRHighlighted substrings (current line) \fBinfo \fRInfo + \fBborder \fRBorder of the preview window and horizontal separators (\fB--border\fR) \fBprompt \fRPrompt \fBpointer \fRPointer to the current line \fBmarker \fRMulti-select marker From 0c66521b231556c1d27b8d6574361003870be3de Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 22 Jun 2017 02:35:57 +0900 Subject: [PATCH 518/658] Fix handling of bracketed paste mode fzf should immediately continue consuming the buffer after discarding bracketed paste mode sequence. Close #951 --- src/tui/light.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/tui/light.go b/src/tui/light.go index e690ef9..c19c8dc 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -411,10 +411,12 @@ func (r *LightRenderer) escSequence(sz *int) Event { return Event{F12, 0, nil} } } - // Bracketed paste mode \e[200~ / \e[201 - if r.buffer[3] == 48 && (r.buffer[4] == 48 || r.buffer[4] == 49) && r.buffer[5] == 126 { - *sz = 6 - return Event{Invalid, 0, nil} + // Bracketed paste mode: \e[200~ ... \e[201~ + if r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' { + // Immediately discard the sequence from the buffer and reread input + r.buffer = r.buffer[6:] + *sz = 0 + return r.GetChar() } return Event{Invalid, 0, nil} // INS case 51: From faedae708e4c288ffd40e7fad3d6a796b48e804a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 23 Jun 2017 01:50:45 +0900 Subject: [PATCH 519/658] Fix FZF_CTRL_T_COMMAND example for fish See #944 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 93c7628..da4a0cc 100644 --- a/README.md +++ b/README.md @@ -457,7 +457,7 @@ make use of this feature. `$dir` defaults to `.` when the last token is not a valid directory. Example: ```sh -set -l FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'" +set -g FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'" ``` [License](LICENSE) From dbcaec59ae0ebf1489860968d8fb186972f00ffb Mon Sep 17 00:00:00 2001 From: Tom Fitzhenry Date: Sun, 25 Jun 2017 22:09:51 +1000 Subject: [PATCH 520/658] [fish] Support multiline commands (#954) Fix found by @amosbird at https://github.com/junegunn/fzf/issues/953#issuecomment-310309055 closes #440 --- shell/key-bindings.fish | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index 238975f..03b543e 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -44,7 +44,7 @@ function fzf_key_bindings set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% begin set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" - history | eval (__fzfcmd) -q '(commandline)' | read -l result + history -z | eval (__fzfcmd) --read0 -q '(commandline)' | perl -pe 'chomp if eof' | read -lz result and commandline -- $result end commandline -f repaint From 70cfa6af13e56bf50c70c46e62085f9d9b52f909 Mon Sep 17 00:00:00 2001 From: John Nguyen Date: Sun, 25 Jun 2017 05:16:15 -0700 Subject: [PATCH 521/658] [fish] Accept starting dir for key binding (#944) This also modifies behaviour. The longest file path in the input is used as root directory for `find` command. The remainder of the input is passed to fzf's --query as a initial search parameters. --- shell/key-bindings.fish | 75 ++++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 13 deletions(-) diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index 03b543e..803d0a9 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -4,14 +4,9 @@ function fzf_key_bindings # Store current token in $dir as root for the 'find' command function fzf-file-widget -d "List files and folders" - set -l dir (commandline -t) - # The commandline token might be escaped, we need to unescape it. - set dir (eval "printf '%s' $dir") - if [ ! -d "$dir" ] - set dir . - end - # Some 'find' versions print undesired duplicated slashes if the path ends with slashes. - set dir (string replace --regex '(.)/+$' '$1' "$dir") + set -l commandline (__fzf_parse_commandline) + set -l dir $commandline[1] + set -l fzf_query $commandline[2] # "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not # $dir itself, even if hidden. @@ -24,7 +19,7 @@ function fzf_key_bindings set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% begin set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" - eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m" | while read -l r; set result $result $r; end + eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end end if [ -z "$result" ] commandline -f repaint @@ -51,15 +46,26 @@ function fzf_key_bindings end function fzf-cd-widget -d "Change directory" + set -l commandline (__fzf_parse_commandline) + set -l dir $commandline[1] + set -l fzf_query $commandline[2] + set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND " - command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ - -o -type d -print 2> /dev/null | cut -b3-" + command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \ + -o -type d -print 2> /dev/null | sed 's@^\./@@'" set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% begin set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" - eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m" | read -l result - [ "$result" ]; and cd $result + eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result + + if [ -n "$result" ] + cd $result + + # Remove last token from commandline. + commandline -t "" + end end + commandline -f repaint end @@ -82,4 +88,47 @@ function fzf_key_bindings bind -M insert \cr fzf-history-widget 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' + # eval is used to do shell expansion on paths + set -l commandline (eval "printf '%s' "(commandline -t)) + + if [ -z $commandline ] + # Default to current directory with no --query + set dir '.' + set fzf_query '' + else + set dir (__fzf_get_dir $commandline) + + if [ "$dir" = "." -a (string sub -l 1 $commandline) != '.' ] + # if $dir is "." but commandline is not a relative path, this means no file path found + set fzf_query $commandline + else + # Also remove trailing slash after dir, to "split" input properly + set fzf_query (string replace -r "^$dir/?" '' "$commandline") + end + end + + echo $dir + echo $fzf_query + end + + function __fzf_get_dir -d 'Find the longest existing filepath from input string' + set dir $argv + + # Strip all trailing slashes. Ignore if $dir is root dir (/) + if [ (string length $dir) -gt 1 ] + set dir (string replace -r '/*$' '' $dir) + end + + # Iteratively check if dir exists and strip tail end of path + while [ ! -d "$dir" ] + # If path is absolute, this can keep going until ends up at / + # If path is relative, this can keep going until entire input is consumed, dirname returns "." + set dir (dirname "$dir") + end + + echo $dir + end + end From 3fc795340d7787d1c64bfe755965d86370ca687b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 1 Jul 2017 01:05:47 +0900 Subject: [PATCH 522/658] Fix test failulre with non-zero pane-base-index --- test/test_go.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_go.rb b/test/test_go.rb index 3c7000b..e2265c3 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -73,6 +73,7 @@ class Tmux else raise "Unknown shell: #{shell}" end + go("set-window-option -t #{@win} pane-base-index 0") @lines = `tput lines`.chomp.to_i if shell == :fish From 07ef2b051ccdc6917b9b65d157df3ebc8f3ff0de Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 1 Jul 2017 01:13:15 +0900 Subject: [PATCH 523/658] Print [ERROR] on info line when the default command failed With zero result. Related: https://github.com/junegunn/fzf.vim/issues/22#issuecomment-311869805 --- src/core.go | 2 +- src/reader.go | 20 +++++++++++--------- src/terminal.go | 8 +++++++- test/test_go.rb | 6 ++++++ 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/core.go b/src/core.go index 6c3b321..a528dbb 100644 --- a/src/core.go +++ b/src/core.go @@ -228,7 +228,7 @@ func Run(opts *Options, revision string) { case EvtReadNew, EvtReadFin: reading = reading && evt == EvtReadNew snapshot, count := chunkList.Snapshot() - terminal.UpdateCount(count, !reading) + terminal.UpdateCount(count, !reading, value.(bool)) matcher.Reset(snapshot, terminal.Input(), false, !reading, sort) case EvtSearchNew: diff --git a/src/reader.go b/src/reader.go index 7e8e2e0..1572e5d 100644 --- a/src/reader.go +++ b/src/reader.go @@ -17,16 +17,17 @@ type Reader struct { // ReadSource reads data from the default command or from standard input func (r *Reader) ReadSource() { + var success bool if util.IsTty() { cmd := os.Getenv("FZF_DEFAULT_COMMAND") if len(cmd) == 0 { cmd = defaultCommand } - r.readFromCommand(cmd) + success = r.readFromCommand(cmd) } else { - r.readFromStdin() + success = r.readFromStdin() } - r.eventBox.Set(EvtReadFin, nil) + r.eventBox.Set(EvtReadFin, success) } func (r *Reader) feed(src io.Reader) { @@ -50,7 +51,7 @@ func (r *Reader) feed(src io.Reader) { } } if r.pusher(bytea) { - r.eventBox.Set(EvtReadNew, nil) + r.eventBox.Set(EvtReadNew, true) } } if err != nil { @@ -59,20 +60,21 @@ func (r *Reader) feed(src io.Reader) { } } -func (r *Reader) readFromStdin() { +func (r *Reader) readFromStdin() bool { r.feed(os.Stdin) + return true } -func (r *Reader) readFromCommand(cmd string) { +func (r *Reader) readFromCommand(cmd string) bool { listCommand := util.ExecCommand(cmd) out, err := listCommand.StdoutPipe() if err != nil { - return + return false } err = listCommand.Start() if err != nil { - return + return false } - defer listCommand.Wait() r.feed(out) + return listCommand.Wait() == nil } diff --git a/src/terminal.go b/src/terminal.go index fdd3caa..d3c808d 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -94,6 +94,7 @@ type Terminal struct { count int progress int reading bool + success bool jumping jumpMode jumpLabels string printer func(string) @@ -372,6 +373,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { ansi: opts.Ansi, tabstop: opts.Tabstop, reading: true, + success: true, jumping: jumpDisabled, jumpLabels: opts.JumpLabels, printer: opts.Printer, @@ -401,10 +403,11 @@ func (t *Terminal) Input() []rune { } // UpdateCount updates the count information -func (t *Terminal) UpdateCount(cnt int, final bool) { +func (t *Terminal) UpdateCount(cnt int, final bool, success bool) { t.mutex.Lock() t.count = cnt t.reading = !final + t.success = success t.mutex.Unlock() t.reqBox.Set(reqInfo, nil) if final { @@ -682,6 +685,9 @@ func (t *Terminal) printInfo() { if t.progress > 0 && t.progress < 100 { output += fmt.Sprintf(" (%d%%)", t.progress) } + if !t.success && t.count == 0 { + output += " [ERROR]" + } if pos+len(output) <= t.window.Width() { t.window.CPrint(tui.ColInfo, 0, output) } diff --git a/test/test_go.rb b/test/test_go.rb index e2265c3..f88d96f 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -260,6 +260,12 @@ class TestGoFZF < TestBase assert_equal 'hello', readonce.chomp end + def test_fzf_default_command_failure + tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', 'FZF_DEFAULT_COMMAND=false'), :Enter + tmux.until { |lines| lines[-2].include?('ERROR') } + tmux.send_keys :Enter + end + def test_key_bindings tmux.send_keys "#{FZF} -q 'foo bar foo-bar'", :Enter tmux.until { |lines| lines.last =~ /^>/ } From b13fcfd831313a60c624dc48cb1b41a2d68bdc5c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 4 Jul 2017 23:02:08 +0900 Subject: [PATCH 524/658] Add missing --no-expect flag --- src/options.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/options.go b/src/options.go index a0b28d4..afd46b9 100644 --- a/src/options.go +++ b/src/options.go @@ -963,6 +963,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)")) case "--expect": opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") + case "--no-expect": + opts.Expect = make(map[int]string) case "--tiebreak": opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) case "--bind": From 68bd410159500f67e069a5d909f2d3062115cffc Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Sun, 9 Jul 2017 00:08:16 -0400 Subject: [PATCH 525/658] [vim] Don't pipe FZF_DEFAULT_COMMAND in Windows (#969) Related #960, #552 --- plugin/fzf.vim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index bad7449..c067512 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -365,10 +365,10 @@ try let dict.dir = fnamemodify(dict.dir, ':p') endif - if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) - let temps.source = s:fzf_tempname().(s:is_win ? '.bat' : '') + if !has_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND) && !s:is_win + let temps.source = s:fzf_tempname() call writefile(s:wrap_cmds(split($FZF_DEFAULT_COMMAND, "\n")), temps.source) - let dict.source = (empty($SHELL) ? &shell : $SHELL) . (s:is_win ? ' /c ' : ' ') . fzf#shellescape(temps.source) + let dict.source = (empty($SHELL) ? &shell : $SHELL).' '.fzf#shellescape(temps.source) endif if has_key(dict, 'source') From 940214a1a2cbc64f32539e9a65beafc767f259e2 Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Sun, 9 Jul 2017 13:06:13 -0400 Subject: [PATCH 526/658] [neovim] Fix lcd when fzf job exits on Windows (#970) Related: #960 (relative filepaths) --- plugin/fzf.vim | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index c067512..24427be 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -529,12 +529,15 @@ function! s:execute(dict, command, use_height, temps) abort let command = batchfile let a:temps.batchfile = batchfile if has('nvim') - let s:dict = a:dict - let s:temps = a:temps let fzf = {} + let fzf.dict = a:dict + let fzf.temps = a:temps function! fzf.on_exit(job_id, exit_status, event) dict - let lines = s:collect(s:temps) - call s:callback(s:dict, lines) + if s:present(self.dict, 'dir') + execute 'lcd' s:escape(self.dict.dir) + endif + let lines = s:collect(self.temps) + call s:callback(self.dict, lines) endfunction let cmd = 'start /wait cmd /c '.command call jobstart(cmd, fzf) From 7b5ccc45bc76f289fe2c48cf91e895b9ca99b1e2 Mon Sep 17 00:00:00 2001 From: Tom Fitzhenry Date: Sat, 15 Jul 2017 19:50:23 +1000 Subject: [PATCH 527/658] [fish] Fix ctrl-r regression in versions <2.4 (#972) Close #966 --- shell/key-bindings.fish | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish index 803d0a9..97a3ec5 100644 --- a/shell/key-bindings.fish +++ b/shell/key-bindings.fish @@ -39,8 +39,20 @@ function fzf_key_bindings set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40% begin set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" - history -z | eval (__fzfcmd) --read0 -q '(commandline)' | perl -pe 'chomp if eof' | read -lz result - and commandline -- $result + + set -l FISH_MAJOR (echo $FISH_VERSION | cut -f1 -d.) + set -l FISH_MINOR (echo $FISH_VERSION | cut -f2 -d.) + + # history's -z flag is needed for multi-line support. + # history's -z flag was added in fish 2.4.0, so don't use it for versions + # before 2.4.0. + if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ]; + history -z | eval (__fzfcmd) --read0 -q '(commandline)' | perl -pe 'chomp if eof' | read -lz result + and commandline -- $result + else + history | eval (__fzfcmd) -q '(commandline)' | read -l result + and commandline -- $result + end end commandline -f repaint end From d4f3d5a16423fbf039644f04516c052d1654326c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 15 Jul 2017 12:28:29 +0900 Subject: [PATCH 528/658] Remove pointer indirection by changing Chunk definition --- src/chunklist.go | 16 ++++++++-------- src/chunklist_test.go | 4 ++-- src/core.go | 16 ++++++++-------- src/item.go | 6 ++++++ src/merger.go | 2 +- src/pattern.go | 4 ++-- src/pattern_test.go | 4 ++-- 7 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/chunklist.go b/src/chunklist.go index a953fae..1144274 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -2,12 +2,12 @@ package fzf import "sync" -// Chunk is a list of Item pointers whose size has the upper limit of chunkSize -type Chunk []*Item // >>> []Item +// Chunk is a list of Items whose size has the upper limit of chunkSize +type Chunk []Item // ItemBuilder is a closure type that builds Item object from a pointer to a // string and an integer -type ItemBuilder func([]byte, int) *Item +type ItemBuilder func([]byte, int) Item // ChunkList is a list of Chunks type ChunkList struct { @@ -28,11 +28,11 @@ func NewChunkList(trans ItemBuilder) *ChunkList { func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool { item := trans(data, index) - if item != nil { - *c = append(*c, item) - return true + if item.Nil() { + return false } - return false + *c = append(*c, item) + return true } // IsFull returns true if the Chunk is full @@ -58,7 +58,7 @@ func (cl *ChunkList) Push(data []byte) bool { defer cl.mutex.Unlock() if len(cl.chunks) == 0 || cl.lastChunk().IsFull() { - newChunk := Chunk(make([]*Item, 0, chunkSize)) + newChunk := Chunk(make([]Item, 0, chunkSize)) cl.chunks = append(cl.chunks, &newChunk) } diff --git a/src/chunklist_test.go b/src/chunklist_test.go index 56559d8..983a7ed 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -11,8 +11,8 @@ func TestChunkList(t *testing.T) { // FIXME global sortCriteria = []criterion{byScore, byLength} - cl := NewChunkList(func(s []byte, i int) *Item { - return &Item{text: util.ToChars(s), index: int32(i * 2)} + cl := NewChunkList(func(s []byte, i int) Item { + return Item{text: util.ToChars(s), index: int32(i * 2)} }) // Snapshot diff --git a/src/core.go b/src/core.go index a528dbb..7e16dc3 100644 --- a/src/core.go +++ b/src/core.go @@ -91,27 +91,27 @@ func Run(opts *Options, revision string) { var chunkList *ChunkList header := make([]string, 0, opts.HeaderLines) if len(opts.WithNth) == 0 { - chunkList = NewChunkList(func(data []byte, index int) *Item { + chunkList = NewChunkList(func(data []byte, index int) Item { if len(header) < opts.HeaderLines { header = append(header, string(data)) eventBox.Set(EvtHeader, header) - return nil + return nilItem } chars, colors := ansiProcessor(data) - return &Item{ + return Item{ index: int32(index), trimLength: -1, text: chars, colors: colors} }) } else { - chunkList = NewChunkList(func(data []byte, index int) *Item { + chunkList = NewChunkList(func(data []byte, index int) Item { tokens := Tokenize(util.ToChars(data), opts.Delimiter) trans := Transform(tokens, opts.WithNth) if len(header) < opts.HeaderLines { header = append(header, string(joinTokens(trans))) eventBox.Set(EvtHeader, header) - return nil + return nilItem } textRunes := joinTokens(trans) item := Item{ @@ -123,7 +123,7 @@ func Run(opts *Options, revision string) { trimmed, colors := ansiProcessorRunes(textRunes) item.text = trimmed item.colors = colors - return &item + return item }) } @@ -168,8 +168,8 @@ func Run(opts *Options, revision string) { reader := Reader{ func(runes []byte) bool { item := chunkList.trans(runes, 0) - if item != nil { - if result, _, _ := pattern.MatchItem(item, false, slab); result != nil { + if !item.Nil() { + if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil { opts.Printer(item.text.ToString()) found = true } diff --git a/src/item.go b/src/item.go index c67ac7a..955c31d 100644 --- a/src/item.go +++ b/src/item.go @@ -19,6 +19,12 @@ func (item *Item) Index() int32 { return item.index } +var nilItem = Item{index: -1} + +func (item *Item) Nil() bool { + return item.index < 0 +} + func (item *Item) TrimLength() int32 { if item.trimLength >= 0 { return item.trimLength diff --git a/src/merger.go b/src/merger.go index 2c7675d..950ba04 100644 --- a/src/merger.go +++ b/src/merger.go @@ -65,7 +65,7 @@ func (mg *Merger) Get(idx int) *Result { idx = mg.count - idx - 1 } chunk := (*mg.chunks)[idx/chunkSize] - return &Result{item: (*chunk)[idx%chunkSize]} + return &Result{item: &(*chunk)[idx%chunkSize]} } if mg.sorted { diff --git a/src/pattern.go b/src/pattern.go index 4614e07..05b03b9 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -281,8 +281,8 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []* matches := []*Result{} if space == nil { - for _, item := range *chunk { - if match, _, _ := p.MatchItem(item, false, slab); match != nil { + for idx := range *chunk { + if match, _, _ := p.MatchItem(&(*chunk)[idx], false, slab); match != nil { matches = append(matches, match) } } diff --git a/src/pattern_test.go b/src/pattern_test.go index 9d66c79..5722be4 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -139,7 +139,7 @@ func TestOrigTextAndTransformed(t *testing.T) { origBytes := []byte("junegunn.choi") for _, extended := range []bool{false, true} { chunk := Chunk{ - &Item{ + Item{ text: util.RunesToChars([]rune("junegunn")), origText: &origBytes, transformed: trans}, @@ -152,7 +152,7 @@ func TestOrigTextAndTransformed(t *testing.T) { t.Error("Invalid match result", matches) } - match, offsets, pos := pattern.MatchItem(chunk[0], true, slab) + match, offsets, pos := pattern.MatchItem(&chunk[0], true, slab) if !(match.item.text.ToString() == "junegunn" && string(*match.item.origText) == "junegunn.choi" && offsets[0][0] == 0 && offsets[0][1] == 5 && From 6725151a994da0493aa2925c7fc141c1618bc3f9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 15 Jul 2017 16:58:21 +0900 Subject: [PATCH 529/658] Remove unnecessary copy of Chunk slice --- src/chunklist.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/chunklist.go b/src/chunklist.go index 1144274..f6bedcc 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -79,15 +79,8 @@ func (cl *ChunkList) Snapshot() ([]*Chunk, int) { // Duplicate the last chunk if cnt := len(ret); cnt > 0 { - ret[cnt-1] = ret[cnt-1].dupe() + newChunk := *ret[cnt-1] + ret[cnt-1] = &newChunk } return ret, cl.count } - -func (c *Chunk) dupe() *Chunk { - newChunk := make(Chunk, len(*c)) - for idx, ptr := range *c { - newChunk[idx] = ptr - } - return &newChunk -} From 8dbdd557302a282ff01dc1a89c4e5c28676bf72e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 15 Jul 2017 19:35:27 +0900 Subject: [PATCH 530/658] Refactor cache lookup - Remove multiple mutex locks in partial cache lookup - Simplify return values --- src/cache.go | 36 ++++++++++++++++++++++++++++++++---- src/pattern.go | 18 ++---------------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/cache.go b/src/cache.go index baf88dd..272a78b 100644 --- a/src/cache.go +++ b/src/cache.go @@ -34,9 +34,9 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) { } // Find is called to lookup ChunkCache -func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Result, bool) { +func (cc *ChunkCache) Find(chunk *Chunk, key string) []*Result { if len(key) == 0 || !chunk.IsFull() { - return nil, false + return nil } cc.mutex.Lock() @@ -46,8 +46,36 @@ func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Result, bool) { if ok { list, ok := (*qc)[key] if ok { - return list, true + return list } } - return nil, false + return nil +} + +func (cc *ChunkCache) Search(chunk *Chunk, key string) []*Result { + if len(key) == 0 || !chunk.IsFull() { + return nil + } + + cc.mutex.Lock() + defer cc.mutex.Unlock() + + qc, ok := cc.cache[chunk] + if !ok { + return nil + } + + for idx := 1; idx < len(key); idx++ { + // [---------| ] | [ |---------] + // [--------| ] | [ |--------] + // [-------| ] | [ |-------] + prefix := key[:len(key)-idx] + suffix := key[idx:] + for _, substr := range [2]string{prefix, suffix} { + if cached, found := (*qc)[substr]; found { + return cached + } + } + } + return nil } diff --git a/src/pattern.go b/src/pattern.go index 05b03b9..f1caeba 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -247,27 +247,13 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []*Result { // ChunkCache: Exact match cacheKey := p.CacheKey() if p.cacheable { - if cached, found := _cache.Find(chunk, cacheKey); found { + if cached := _cache.Find(chunk, cacheKey); cached != nil { return cached } } // Prefix/suffix cache - var space []*Result -Loop: - for idx := 1; idx < len(cacheKey); idx++ { - // [---------| ] | [ |---------] - // [--------| ] | [ |--------] - // [-------| ] | [ |-------] - prefix := cacheKey[:len(cacheKey)-idx] - suffix := cacheKey[idx:] - for _, substr := range [2]*string{&prefix, &suffix} { - if cached, found := _cache.Find(chunk, *substr); found { - space = cached - break Loop - } - } - } + space := _cache.Search(chunk, cacheKey) matches := p.matchChunk(chunk, space, slab) From 4b59ced08f1d417530a25af8fe13aa5d40579220 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 16 Jul 2017 22:12:33 +0900 Subject: [PATCH 531/658] Add gopath to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 61ef433..d4cc4a9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ Gemfile.lock .DS_Store doc/tags vendor +gopath From 9e85cba0d06025983a1a747bfc06c9955388d9c0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 16 Jul 2017 23:31:19 +0900 Subject: [PATCH 532/658] Reduce memory footprint of Item struct --- src/algo/algo.go | 5 +- src/cache.go | 4 +- src/cache_test.go | 24 +++++----- src/chunklist_test.go | 8 ++-- src/core.go | 18 ++------ src/item.go | 26 ++++------- src/pattern.go | 19 ++++---- src/pattern_test.go | 6 +-- src/result.go | 6 +-- src/result_test.go | 24 +++++++--- src/util/chars.go | 101 +++++++++++++++++++++++++++-------------- src/util/chars_test.go | 20 ++------ 12 files changed, 139 insertions(+), 122 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index ac6c66d..c4930c1 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -283,8 +283,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C // Phase 1. Check if there's a match and calculate bonus for each point pidx, lastIdx, prevClass := 0, 0, charNonWord + input.CopyRunes(T) for idx := 0; idx < N; idx++ { - char := input.Get(idx) + char := T[idx] var class charClass if char <= unicode.MaxASCII { class = charClassOfAscii(char) @@ -389,7 +390,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C if i == 0 { fmt.Print(" ") for j := int(F[i]); j <= lastIdx; j++ { - fmt.Printf(" " + string(input.Get(j)) + " ") + fmt.Printf(" " + string(T[j]) + " ") } fmt.Println() } diff --git a/src/cache.go b/src/cache.go index 272a78b..0540bdc 100644 --- a/src/cache.go +++ b/src/cache.go @@ -33,8 +33,8 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) { (*qc)[key] = list } -// Find is called to lookup ChunkCache -func (cc *ChunkCache) Find(chunk *Chunk, key string) []*Result { +// Lookup is called to lookup ChunkCache +func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []*Result { if len(key) == 0 || !chunk.IsFull() { return nil } diff --git a/src/cache_test.go b/src/cache_test.go index 8703fc4..54f3fb0 100644 --- a/src/cache_test.go +++ b/src/cache_test.go @@ -14,27 +14,27 @@ func TestChunkCache(t *testing.T) { cache.Add(chunk2p, "bar", items2) { // chunk1 is not full - cached, found := cache.Find(chunk1p, "foo") - if found { - t.Error("Cached disabled for non-empty chunks", found, cached) + cached := cache.Lookup(chunk1p, "foo") + if cached != nil { + t.Error("Cached disabled for non-empty chunks", cached) } } { - cached, found := cache.Find(chunk2p, "foo") - if !found || len(cached) != 1 { - t.Error("Expected 1 item cached", found, cached) + cached := cache.Lookup(chunk2p, "foo") + if cached == nil || len(cached) != 1 { + t.Error("Expected 1 item cached", cached) } } { - cached, found := cache.Find(chunk2p, "bar") - if !found || len(cached) != 2 { - t.Error("Expected 2 items cached", found, cached) + cached := cache.Lookup(chunk2p, "bar") + if cached == nil || len(cached) != 2 { + t.Error("Expected 2 items cached", cached) } } { - cached, found := cache.Find(chunk1p, "foobar") - if found { - t.Error("Expected 0 item cached", found, cached) + cached := cache.Lookup(chunk1p, "foobar") + if cached != nil { + t.Error("Expected 0 item cached", cached) } } } diff --git a/src/chunklist_test.go b/src/chunklist_test.go index 983a7ed..78468e3 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -12,7 +12,9 @@ func TestChunkList(t *testing.T) { sortCriteria = []criterion{byScore, byLength} cl := NewChunkList(func(s []byte, i int) Item { - return Item{text: util.ToChars(s), index: int32(i * 2)} + chars := util.ToChars(s) + chars.Index = int32(i * 2) + return Item{text: chars} }) // Snapshot @@ -41,8 +43,8 @@ func TestChunkList(t *testing.T) { if len(*chunk1) != 2 { t.Error("Snapshot should contain only two items") } - if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].index != 0 || - (*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].index != 2 { + if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].Index() != 0 || + (*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].Index() != 2 { t.Error("Invalid data") } if chunk1.IsFull() { diff --git a/src/core.go b/src/core.go index 7e16dc3..aa42510 100644 --- a/src/core.go +++ b/src/core.go @@ -98,11 +98,8 @@ func Run(opts *Options, revision string) { return nilItem } chars, colors := ansiProcessor(data) - return Item{ - index: int32(index), - trimLength: -1, - text: chars, - colors: colors} + chars.Index = int32(index) + return Item{text: chars, colors: colors} }) } else { chunkList = NewChunkList(func(data []byte, index int) Item { @@ -114,16 +111,9 @@ func Run(opts *Options, revision string) { return nilItem } textRunes := joinTokens(trans) - item := Item{ - index: int32(index), - trimLength: -1, - origText: &data, - colors: nil} - trimmed, colors := ansiProcessorRunes(textRunes) - item.text = trimmed - item.colors = colors - return item + trimmed.Index = int32(index) + return Item{text: trimmed, colors: colors, origText: &data} }) } diff --git a/src/item.go b/src/item.go index 955c31d..b3879cb 100644 --- a/src/item.go +++ b/src/item.go @@ -4,33 +4,27 @@ import ( "github.com/junegunn/fzf/src/util" ) -// Item represents each input line +// Item represents each input line. 56 bytes. type Item struct { - index int32 - trimLength int32 - text util.Chars - origText *[]byte - colors *[]ansiOffset - transformed []Token + text util.Chars // 32 = 24 + 1 + 1 + 2 + 4 + transformed *[]Token // 8 + origText *[]byte // 8 + colors *[]ansiOffset // 8 } // Index returns ordinal index of the Item func (item *Item) Index() int32 { - return item.index + return item.text.Index } -var nilItem = Item{index: -1} +var nilItem = Item{text: util.Chars{Index: -1}} func (item *Item) Nil() bool { - return item.index < 0 + return item.Index() < 0 } -func (item *Item) TrimLength() int32 { - if item.trimLength >= 0 { - return item.trimLength - } - item.trimLength = int32(item.text.TrimLength()) - return item.trimLength +func (item *Item) TrimLength() uint16 { + return item.text.TrimLength() } // Colors returns ansiOffsets of the Item diff --git a/src/pattern.go b/src/pattern.go index f1caeba..07ed9cd 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -247,7 +247,7 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []*Result { // ChunkCache: Exact match cacheKey := p.CacheKey() if p.cacheable { - if cached := _cache.Find(chunk, cacheKey); cached != nil { + if cached := _cache.Lookup(chunk, cacheKey); cached != nil { return cached } } @@ -352,18 +352,17 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of } func (p *Pattern) prepareInput(item *Item) []Token { - if item.transformed != nil { - return item.transformed + if len(p.nth) == 0 { + return []Token{Token{text: &item.text, prefixLength: 0}} } - var ret []Token - if len(p.nth) == 0 { - ret = []Token{Token{text: &item.text, prefixLength: 0}} - } else { - tokens := Tokenize(item.text, p.delimiter) - ret = Transform(tokens, p.nth) + if item.transformed != nil { + return *item.transformed } - item.transformed = ret + + tokens := Tokenize(item.text, p.delimiter) + ret := Transform(tokens, p.nth) + item.transformed = &ret return ret } diff --git a/src/pattern_test.go b/src/pattern_test.go index 5722be4..31a127e 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -142,13 +142,13 @@ func TestOrigTextAndTransformed(t *testing.T) { Item{ text: util.RunesToChars([]rune("junegunn")), origText: &origBytes, - transformed: trans}, + transformed: &trans}, } pattern.extended = extended matches := pattern.matchChunk(&chunk, nil, slab) // No cache if !(matches[0].item.text.ToString() == "junegunn" && string(*matches[0].item.origText) == "junegunn.choi" && - reflect.DeepEqual(matches[0].item.transformed, trans)) { + reflect.DeepEqual(*matches[0].item.transformed, trans)) { t.Error("Invalid match result", matches) } @@ -156,7 +156,7 @@ func TestOrigTextAndTransformed(t *testing.T) { if !(match.item.text.ToString() == "junegunn" && string(*match.item.origText) == "junegunn.choi" && offsets[0][0] == 0 && offsets[0][1] == 5 && - reflect.DeepEqual(match.item.transformed, trans)) { + reflect.DeepEqual(*match.item.transformed, trans)) { t.Error("Invalid match result", match, offsets, extended) } if !((*pos)[0] == 4 && (*pos)[1] == 0) { diff --git a/src/result.go b/src/result.go index 0b1fbf0..fd4d1a9 100644 --- a/src/result.go +++ b/src/result.go @@ -34,7 +34,7 @@ func buildResult(item *Item, offsets []Offset, score int) *Result { sort.Sort(ByOrder(offsets)) } - result := Result{item: item, rank: rank{index: item.index}} + result := Result{item: item, rank: rank{index: item.Index()}} numChars := item.text.Length() minBegin := math.MaxUint16 minEnd := math.MaxUint16 @@ -57,7 +57,7 @@ func buildResult(item *Item, offsets []Offset, score int) *Result { // Higher is better val = math.MaxUint16 - util.AsUint16(score) case byLength: - val = util.AsUint16(int(item.TrimLength())) + val = item.TrimLength() case byBegin, byEnd: if validOffsetFound { whitePrefixLen := 0 @@ -86,7 +86,7 @@ var sortCriteria []criterion // Index returns ordinal index of the Item func (result *Result) Index() int32 { - return result.item.index + return result.item.Index() } func minRank() rank { diff --git a/src/result_test.go b/src/result_test.go index ad510c2..8c74691 100644 --- a/src/result_test.go +++ b/src/result_test.go @@ -11,6 +11,11 @@ import ( "github.com/junegunn/fzf/src/util" ) +func withIndex(i *Item, index int) *Item { + (*i).text.Index = int32(index) + return i +} + func TestOffsetSort(t *testing.T) { offsets := []Offset{ Offset{3, 5}, Offset{2, 7}, @@ -52,12 +57,13 @@ func TestResultRank(t *testing.T) { sortCriteria = []criterion{byScore, byLength} strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} - item1 := buildResult(&Item{text: util.RunesToChars(strs[0]), index: 1, trimLength: -1}, []Offset{}, 2) + item1 := buildResult( + withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2) if item1.rank.points[0] != math.MaxUint16-2 || // Bonus item1.rank.points[1] != 3 || // Length item1.rank.points[2] != 0 || // Unused item1.rank.points[3] != 0 || // Unused - item1.item.index != 1 { + item1.item.Index() != 1 { t.Error(item1.rank) } // Only differ in index @@ -73,14 +79,18 @@ func TestResultRank(t *testing.T) { sort.Sort(ByRelevance(items)) if items[0] != item2 || items[1] != item2 || items[2] != item1 || items[3] != item1 { - t.Error(items, item1, item1.item.index, item2, item2.item.index) + t.Error(items, item1, item1.item.Index(), item2, item2.item.Index()) } // Sort by relevance - item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 3) - item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 4) - item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 5) - item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 6) + item3 := buildResult( + withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 3) + item4 := buildResult( + withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 4) + item5 := buildResult( + withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 5) + item6 := buildResult( + withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 6) items = []*Result{item1, item2, item3, item4, item5, item6} sort.Sort(ByRelevance(items)) if !(items[0] == item6 && items[1] == item5 && diff --git a/src/util/chars.go b/src/util/chars.go index 061120e..8325cf4 100644 --- a/src/util/chars.go +++ b/src/util/chars.go @@ -3,63 +3,81 @@ package util import ( "unicode" "unicode/utf8" + "unsafe" ) type Chars struct { - runes []rune - bytes []byte + slice []byte // or []rune + inBytes bool + trimLengthKnown bool + trimLength uint16 + + // XXX Piggybacking item index here is a horrible idea. But I'm trying to + // minimize the memory footprint by not wasting padded spaces. + Index int32 } // ToChars converts byte array into rune array -func ToChars(bytea []byte) Chars { +func ToChars(bytes []byte) Chars { var runes []rune - ascii := true - numBytes := len(bytea) + inBytes := true + numBytes := len(bytes) for i := 0; i < numBytes; { - if bytea[i] < utf8.RuneSelf { - if !ascii { - runes = append(runes, rune(bytea[i])) + if bytes[i] < utf8.RuneSelf { + if !inBytes { + runes = append(runes, rune(bytes[i])) } i++ } else { - if ascii { - ascii = false + if inBytes { + inBytes = false runes = make([]rune, i, numBytes) for j := 0; j < i; j++ { - runes[j] = rune(bytea[j]) + runes[j] = rune(bytes[j]) } } - r, sz := utf8.DecodeRune(bytea[i:]) + r, sz := utf8.DecodeRune(bytes[i:]) i += sz runes = append(runes, r) } } - if ascii { - return Chars{bytes: bytea} + if inBytes { + return Chars{slice: bytes, inBytes: inBytes} } - return Chars{runes: runes} + return RunesToChars(runes) } func RunesToChars(runes []rune) Chars { - return Chars{runes: runes} + return Chars{slice: *(*[]byte)(unsafe.Pointer(&runes)), inBytes: false} +} + +func (chars *Chars) optionalRunes() []rune { + if chars.inBytes { + return nil + } + return *(*[]rune)(unsafe.Pointer(&chars.slice)) } func (chars *Chars) Get(i int) rune { - if chars.runes != nil { - return chars.runes[i] + if runes := chars.optionalRunes(); runes != nil { + return runes[i] } - return rune(chars.bytes[i]) + return rune(chars.slice[i]) } func (chars *Chars) Length() int { - if chars.runes != nil { - return len(chars.runes) + if runes := chars.optionalRunes(); runes != nil { + return len(runes) } - return len(chars.bytes) + return len(chars.slice) } // TrimLength returns the length after trimming leading and trailing whitespaces -func (chars *Chars) TrimLength() int { +func (chars *Chars) TrimLength() uint16 { + if chars.trimLengthKnown { + return chars.trimLength + } + chars.trimLengthKnown = true var i int len := chars.Length() for i = len - 1; i >= 0; i-- { @@ -80,7 +98,8 @@ func (chars *Chars) TrimLength() int { break } } - return i - j + 1 + chars.trimLength = AsUint16(i - j + 1) + return chars.trimLength } func (chars *Chars) TrailingWhitespaces() int { @@ -96,28 +115,40 @@ func (chars *Chars) TrailingWhitespaces() int { } func (chars *Chars) ToString() string { - if chars.runes != nil { - return string(chars.runes) + if runes := chars.optionalRunes(); runes != nil { + return string(runes) } - return string(chars.bytes) + return string(chars.slice) } func (chars *Chars) ToRunes() []rune { - if chars.runes != nil { - return chars.runes + if runes := chars.optionalRunes(); runes != nil { + return runes } - runes := make([]rune, len(chars.bytes)) - for idx, b := range chars.bytes { + bytes := chars.slice + runes := make([]rune, len(bytes)) + for idx, b := range bytes { runes[idx] = rune(b) } return runes } -func (chars *Chars) Slice(b int, e int) Chars { - if chars.runes != nil { - return Chars{runes: chars.runes[b:e]} +func (chars *Chars) CopyRunes(dest []rune) { + if runes := chars.optionalRunes(); runes != nil { + copy(dest, runes) + return } - return Chars{bytes: chars.bytes[b:e]} + for idx, b := range chars.slice { + dest[idx] = rune(b) + } + return +} + +func (chars *Chars) Slice(b int, e int) Chars { + if runes := chars.optionalRunes(); runes != nil { + return RunesToChars(runes[b:e]) + } + return Chars{slice: chars.slice[b:e], inBytes: true} } func (chars *Chars) Split(delimiter string) []Chars { diff --git a/src/util/chars_test.go b/src/util/chars_test.go index 12c629d..07b8dea 100644 --- a/src/util/chars_test.go +++ b/src/util/chars_test.go @@ -2,27 +2,16 @@ package util import "testing" -func TestToCharsNil(t *testing.T) { - bs := Chars{bytes: []byte{}} - if bs.bytes == nil || bs.runes != nil { - t.Error() - } - rs := RunesToChars([]rune{}) - if rs.bytes != nil || rs.runes == nil { - t.Error() - } -} - func TestToCharsAscii(t *testing.T) { chars := ToChars([]byte("foobar")) - if chars.ToString() != "foobar" || chars.runes != nil { + if !chars.inBytes || chars.ToString() != "foobar" || !chars.inBytes { t.Error() } } func TestCharsLength(t *testing.T) { chars := ToChars([]byte("\tabc한글 ")) - if chars.Length() != 8 || chars.TrimLength() != 5 { + if chars.inBytes || chars.Length() != 8 || chars.TrimLength() != 5 { t.Error() } } @@ -36,7 +25,7 @@ func TestCharsToString(t *testing.T) { } func TestTrimLength(t *testing.T) { - check := func(str string, exp int) { + check := func(str string, exp uint16) { chars := ToChars([]byte(str)) trimmed := chars.TrimLength() if trimmed != exp { @@ -61,7 +50,8 @@ func TestSplit(t *testing.T) { input := ToChars([]byte(str)) result := input.Split(delim) if len(result) != len(tokens) { - t.Errorf("Invalid Split result for '%s': %d tokens found (expected %d): %s", + t.Errorf( + "Invalid Split result for '%s': %d tokens found (expected %d): %s", str, len(result), len(tokens), result) } for idx, token := range tokens { From 5e72709613b816531c1e0aed6a710257e08bb5d8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 18 Jul 2017 02:17:05 +0900 Subject: [PATCH 533/658] Speed up initial scanning with bitwise AND operation --- src/util/chars.go | 58 +++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/src/util/chars.go b/src/util/chars.go index 8325cf4..5e70200 100644 --- a/src/util/chars.go +++ b/src/util/chars.go @@ -6,6 +6,11 @@ import ( "unsafe" ) +const ( + overflow64 uint64 = 0x8080808080808080 + overflow32 uint32 = 0x80808080 +) + type Chars struct { slice []byte // or []rune inBytes bool @@ -17,33 +22,42 @@ type Chars struct { Index int32 } -// ToChars converts byte array into rune array -func ToChars(bytes []byte) Chars { - var runes []rune - inBytes := true - numBytes := len(bytes) - for i := 0; i < numBytes; { - if bytes[i] < utf8.RuneSelf { - if !inBytes { - runes = append(runes, rune(bytes[i])) - } - i++ - } else { - if inBytes { - inBytes = false - runes = make([]rune, i, numBytes) - for j := 0; j < i; j++ { - runes[j] = rune(bytes[j]) - } - } - r, sz := utf8.DecodeRune(bytes[i:]) - i += sz - runes = append(runes, r) +func checkAscii(bytes []byte) (bool, int) { + i := 0 + for ; i < len(bytes)-8; i += 8 { + if (overflow64 & *(*uint64)(unsafe.Pointer(&bytes[i]))) > 0 { + return false, i } } + for ; i < len(bytes)-4; i += 4 { + if (overflow32 & *(*uint32)(unsafe.Pointer(&bytes[i]))) > 0 { + return false, i + } + } + for ; i < len(bytes); i++ { + if bytes[i] >= utf8.RuneSelf { + return false, i + } + } + return true, 0 +} + +// ToChars converts byte array into rune array +func ToChars(bytes []byte) Chars { + inBytes, bytesUntil := checkAscii(bytes) if inBytes { return Chars{slice: bytes, inBytes: inBytes} } + + runes := make([]rune, bytesUntil, len(bytes)) + for i := 0; i < bytesUntil; i++ { + runes[i] = rune(bytes[i]) + } + for i := bytesUntil; i < len(bytes); { + r, sz := utf8.DecodeRune(bytes[i:]) + i += sz + runes = append(runes, r) + } return RunesToChars(runes) } From bbe10f4f7745000c121b629ff68e81bba5a497f6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 18 Jul 2017 03:10:49 +0900 Subject: [PATCH 534/658] Consolidate Result and rank structs By not storing item index twice, we can cut down the size of Result struct and now it makes more sense to store and pass Results by values. Benchmarks show no degradation of performance by additional pointer indirection for looking up index. --- src/cache.go | 8 ++++---- src/cache_test.go | 4 ++-- src/matcher.go | 8 ++++---- src/merger.go | 18 +++++++++--------- src/merger_test.go | 16 ++++++++-------- src/pattern.go | 16 +++++++++------- src/result.go | 33 ++++++++++++++------------------- src/result_test.go | 22 +++++++++++----------- src/terminal.go | 8 ++++---- 9 files changed, 65 insertions(+), 68 deletions(-) diff --git a/src/cache.go b/src/cache.go index 0540bdc..df1a6ab 100644 --- a/src/cache.go +++ b/src/cache.go @@ -3,7 +3,7 @@ package fzf import "sync" // queryCache associates strings to lists of items -type queryCache map[string][]*Result +type queryCache map[string][]Result // ChunkCache associates Chunk and query string to lists of items type ChunkCache struct { @@ -17,7 +17,7 @@ func NewChunkCache() ChunkCache { } // Add adds the list to the cache -func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) { +func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) { if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax { return } @@ -34,7 +34,7 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) { } // Lookup is called to lookup ChunkCache -func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []*Result { +func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []Result { if len(key) == 0 || !chunk.IsFull() { return nil } @@ -52,7 +52,7 @@ func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []*Result { return nil } -func (cc *ChunkCache) Search(chunk *Chunk, key string) []*Result { +func (cc *ChunkCache) Search(chunk *Chunk, key string) []Result { if len(key) == 0 || !chunk.IsFull() { return nil } diff --git a/src/cache_test.go b/src/cache_test.go index 54f3fb0..8a2d2cf 100644 --- a/src/cache_test.go +++ b/src/cache_test.go @@ -7,8 +7,8 @@ func TestChunkCache(t *testing.T) { chunk2 := make(Chunk, chunkSize) chunk1p := &Chunk{} chunk2p := &chunk2 - items1 := []*Result{&Result{}} - items2 := []*Result{&Result{}, &Result{}} + items1 := []Result{Result{}} + items2 := []Result{Result{}, Result{}} cache.Add(chunk1p, "foo", items1) cache.Add(chunk2p, "foo", items1) cache.Add(chunk2p, "bar", items2) diff --git a/src/matcher.go b/src/matcher.go index 57c263a..c29f2b6 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -131,7 +131,7 @@ func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk { type partialResult struct { index int - matches []*Result + matches []Result } func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { @@ -162,7 +162,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { go func(idx int, slab *util.Slab, chunks []*Chunk) { defer func() { waitGroup.Done() }() count := 0 - allMatches := make([][]*Result, len(chunks)) + allMatches := make([][]Result, len(chunks)) for idx, chunk := range chunks { matches := request.pattern.Match(chunk, slab) allMatches[idx] = matches @@ -172,7 +172,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { } countChan <- len(matches) } - sliceMatches := make([]*Result, 0, count) + sliceMatches := make([]Result, 0, count) for _, matches := range allMatches { sliceMatches = append(sliceMatches, matches...) } @@ -212,7 +212,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { } } - partialResults := make([][]*Result, numSlices) + partialResults := make([][]Result, numSlices) for _ = range slices { partialResult := <-resultChan partialResults[partialResult.index] = partialResult.matches diff --git a/src/merger.go b/src/merger.go index 950ba04..7d30a76 100644 --- a/src/merger.go +++ b/src/merger.go @@ -3,14 +3,14 @@ package fzf import "fmt" // EmptyMerger is a Merger with no data -var EmptyMerger = NewMerger(nil, [][]*Result{}, false, false) +var EmptyMerger = NewMerger(nil, [][]Result{}, false, false) // Merger holds a set of locally sorted lists of items and provides the view of // a single, globally-sorted list type Merger struct { pattern *Pattern - lists [][]*Result - merged []*Result + lists [][]Result + merged []Result chunks *[]*Chunk cursors []int sorted bool @@ -35,11 +35,11 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger { } // NewMerger returns a new Merger -func NewMerger(pattern *Pattern, lists [][]*Result, sorted bool, tac bool) *Merger { +func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger { mg := Merger{ pattern: pattern, lists: lists, - merged: []*Result{}, + merged: []Result{}, chunks: nil, cursors: make([]int, len(lists)), sorted: sorted, @@ -59,13 +59,13 @@ func (mg *Merger) Length() int { } // Get returns the pointer to the Result object indexed by the given integer -func (mg *Merger) Get(idx int) *Result { +func (mg *Merger) Get(idx int) Result { if mg.chunks != nil { if mg.tac { idx = mg.count - idx - 1 } chunk := (*mg.chunks)[idx/chunkSize] - return &Result{item: &(*chunk)[idx%chunkSize]} + return Result{item: &(*chunk)[idx%chunkSize]} } if mg.sorted { @@ -89,7 +89,7 @@ func (mg *Merger) cacheable() bool { return mg.count < mergerCacheMax } -func (mg *Merger) mergedGet(idx int) *Result { +func (mg *Merger) mergedGet(idx int) Result { for i := len(mg.merged); i <= idx; i++ { minRank := minRank() minIdx := -1 @@ -100,7 +100,7 @@ func (mg *Merger) mergedGet(idx int) *Result { continue } if cursor >= 0 { - rank := list[cursor].rank + rank := list[cursor] if minIdx < 0 || compareRanks(rank, minRank, mg.tac) { minRank = rank minIdx = listIdx diff --git a/src/merger_test.go b/src/merger_test.go index a4adee1..b98aca8 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -15,11 +15,11 @@ func assert(t *testing.T, cond bool, msg ...string) { } } -func randResult() *Result { +func randResult() Result { str := fmt.Sprintf("%d", rand.Uint32()) - return &Result{ - item: &Item{text: util.RunesToChars([]rune(str))}, - rank: rank{index: rand.Int31()}} + chars := util.RunesToChars([]rune(str)) + chars.Index = rand.Int31() + return Result{item: &Item{text: chars}} } func TestEmptyMerger(t *testing.T) { @@ -29,14 +29,14 @@ func TestEmptyMerger(t *testing.T) { assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list") } -func buildLists(partiallySorted bool) ([][]*Result, []*Result) { +func buildLists(partiallySorted bool) ([][]Result, []Result) { numLists := 4 - lists := make([][]*Result, numLists) + lists := make([][]Result, numLists) cnt := 0 for i := 0; i < numLists; i++ { numResults := rand.Int() % 20 cnt += numResults - lists[i] = make([]*Result, numResults) + lists[i] = make([]Result, numResults) for j := 0; j < numResults; j++ { item := randResult() lists[i][j] = item @@ -45,7 +45,7 @@ func buildLists(partiallySorted bool) ([][]*Result, []*Result) { sort.Sort(ByRelevance(lists[i])) } } - items := []*Result{} + items := []Result{} for _, list := range lists { items = append(items, list...) } diff --git a/src/pattern.go b/src/pattern.go index 07ed9cd..97ee8fd 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -243,7 +243,7 @@ func (p *Pattern) CacheKey() string { } // Match returns the list of matches Items in the given Chunk -func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []*Result { +func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result { // ChunkCache: Exact match cacheKey := p.CacheKey() if p.cacheable { @@ -263,19 +263,19 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []*Result { return matches } -func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []*Result { - matches := []*Result{} +func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result { + matches := []Result{} if space == nil { for idx := range *chunk { if match, _, _ := p.MatchItem(&(*chunk)[idx], false, slab); match != nil { - matches = append(matches, match) + matches = append(matches, *match) } } } else { for _, result := range space { if match, _, _ := p.MatchItem(result.item, false, slab); match != nil { - matches = append(matches, match) + matches = append(matches, *match) } } } @@ -286,14 +286,16 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []* func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) { if p.extended { if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) { - return buildResult(item, offsets, bonus), offsets, pos + result := buildResult(item, offsets, bonus) + return &result, offsets, pos } return nil, nil, nil } offset, bonus, pos := p.basicMatch(item, withPos, slab) if sidx := offset[0]; sidx >= 0 { offsets := []Offset{offset} - return buildResult(item, offsets, bonus), offsets, pos + result := buildResult(item, offsets, bonus) + return &result, offsets, pos } return nil, nil, nil } diff --git a/src/result.go b/src/result.go index fd4d1a9..2df101b 100644 --- a/src/result.go +++ b/src/result.go @@ -19,22 +19,17 @@ type colorOffset struct { index int32 } -type rank struct { - points [4]uint16 - index int32 -} - type Result struct { - item *Item - rank rank + item *Item + points [4]uint16 } -func buildResult(item *Item, offsets []Offset, score int) *Result { +func buildResult(item *Item, offsets []Offset, score int) Result { if len(offsets) > 1 { sort.Sort(ByOrder(offsets)) } - result := Result{item: item, rank: rank{index: item.Index()}} + result := Result{item: item} numChars := item.text.Length() minBegin := math.MaxUint16 minEnd := math.MaxUint16 @@ -75,10 +70,10 @@ func buildResult(item *Item, offsets []Offset, score int) *Result { } } } - result.rank.points[idx] = val + result.points[idx] = val } - return &result + return result } // Sort criteria to use. Never changes once fzf is started. @@ -89,8 +84,8 @@ func (result *Result) Index() int32 { return result.item.Index() } -func minRank() rank { - return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}} +func minRank() Result { + return Result{item: &nilItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}} } func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset { @@ -201,7 +196,7 @@ func (a ByOrder) Less(i, j int) bool { } // ByRelevance is for sorting Items -type ByRelevance []*Result +type ByRelevance []Result func (a ByRelevance) Len() int { return len(a) @@ -212,11 +207,11 @@ func (a ByRelevance) Swap(i, j int) { } func (a ByRelevance) Less(i, j int) bool { - return compareRanks((*a[i]).rank, (*a[j]).rank, false) + return compareRanks(a[i], a[j], false) } // ByRelevanceTac is for sorting Items -type ByRelevanceTac []*Result +type ByRelevanceTac []Result func (a ByRelevanceTac) Len() int { return len(a) @@ -227,10 +222,10 @@ func (a ByRelevanceTac) Swap(i, j int) { } func (a ByRelevanceTac) Less(i, j int) bool { - return compareRanks((*a[i]).rank, (*a[j]).rank, true) + return compareRanks(a[i], a[j], true) } -func compareRanks(irank rank, jrank rank, tac bool) bool { +func compareRanks(irank Result, jrank Result, tac bool) bool { for idx := 0; idx < 4; idx++ { left := irank.points[idx] right := jrank.points[idx] @@ -240,5 +235,5 @@ func compareRanks(irank rank, jrank rank, tac bool) bool { return false } } - return (irank.index <= jrank.index) != tac + return (irank.item.Index() <= jrank.item.Index()) != tac } diff --git a/src/result_test.go b/src/result_test.go index 8c74691..1d86b1d 100644 --- a/src/result_test.go +++ b/src/result_test.go @@ -31,10 +31,10 @@ func TestOffsetSort(t *testing.T) { } func TestRankComparison(t *testing.T) { - rank := func(vals ...uint16) rank { - return rank{ + rank := func(vals ...uint16) Result { + return Result{ points: [4]uint16{vals[0], vals[1], vals[2], vals[3]}, - index: int32(vals[4])} + item: &Item{text: util.Chars{Index: int32(vals[4])}}} } if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) || !compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) || @@ -59,23 +59,23 @@ func TestResultRank(t *testing.T) { strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} item1 := buildResult( withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2) - if item1.rank.points[0] != math.MaxUint16-2 || // Bonus - item1.rank.points[1] != 3 || // Length - item1.rank.points[2] != 0 || // Unused - item1.rank.points[3] != 0 || // Unused + if item1.points[0] != math.MaxUint16-2 || // Bonus + item1.points[1] != 3 || // Length + item1.points[2] != 0 || // Unused + item1.points[3] != 0 || // Unused item1.item.Index() != 1 { - t.Error(item1.rank) + t.Error(item1) } // Only differ in index item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2) - items := []*Result{item1, item2} + items := []Result{item1, item2} sort.Sort(ByRelevance(items)) if items[0] != item2 || items[1] != item1 { t.Error(items) } - items = []*Result{item2, item1, item1, item2} + items = []Result{item2, item1, item1, item2} sort.Sort(ByRelevance(items)) if items[0] != item2 || items[1] != item2 || items[2] != item1 || items[3] != item1 { @@ -91,7 +91,7 @@ func TestResultRank(t *testing.T) { withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 5) item6 := buildResult( withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 6) - items = []*Result{item1, item2, item3, item4, item5, item6} + items = []Result{item1, item2, item3, item4, item5, item6} sort.Sort(ByRelevance(items)) if !(items[0] == item6 && items[1] == item5 && items[2] == item4 && items[3] == item3 && diff --git a/src/terminal.go b/src/terminal.go index d3c808d..d4f4c85 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -714,7 +714,7 @@ func (t *Terminal) printHeader() { colors: colors} t.move(line, 2, true) - t.printHighlighted(&Result{item: item}, + t.printHighlighted(Result{item: item}, tui.AttrRegular, tui.ColHeader, tui.ColDefault, false, false) } } @@ -742,7 +742,7 @@ func (t *Terminal) printList() { } } -func (t *Terminal) printItem(result *Result, line int, i int, current bool) { +func (t *Terminal) printItem(result Result, line int, i int, current bool) { item := result.item _, selected := t.selected[item.Index()] label := " " @@ -758,7 +758,7 @@ func (t *Terminal) printItem(result *Result, line int, i int, current bool) { // Avoid unnecessary redraw newLine := itemLine{current: current, selected: selected, label: label, - result: *result, queryLen: len(t.input), width: 0} + result: result, queryLen: len(t.input), width: 0} prevLine := t.prevLines[i] if prevLine.current == newLine.current && prevLine.selected == newLine.selected && @@ -840,7 +840,7 @@ func (t *Terminal) overflow(runes []rune, max int) bool { return t.displayWidthWithLimit(runes, 0, max) > max } -func (t *Terminal) printHighlighted(result *Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) int { +func (t *Terminal) printHighlighted(result Result, attr tui.Attr, col1 tui.ColorPair, col2 tui.ColorPair, current bool, match bool) int { item := result.item // Overflow From 7727ad43afb57cb325439590a9a6bad94601a720 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 18 Jul 2017 16:33:58 +0900 Subject: [PATCH 535/658] [vim] Use fnameescape to escape command line arguments Fix https://github.com/junegunn/fzf.vim/issues/404 Thanks to @janlazo. --- plugin/fzf.vim | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 24427be..3beb5be 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -149,13 +149,8 @@ function! s:tmux_enabled() endfunction function! s:escape(path) - let escaped_chars = '$%#''"' - - if has('unix') - let escaped_chars .= ' \' - endif - - return escape(a:path, escaped_chars) + let path = fnameescape(a:path) + return s:is_win ? escape(path, '$') : path endfunction " Upgrade legacy options From 6b5886c034800c46d25a02c8b91c2797ec5fb6d6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 18 Jul 2017 20:50:38 +0900 Subject: [PATCH 536/658] Adjust --no-clear option for repetitive relaunching Related: https://gist.github.com/junegunn/4963bab6ace453f7f529d2d0e01b1d85 Close #974 --- src/terminal.go | 24 +++++++++++++++--------- src/tui/light.go | 20 +++++++++++++------- test/test_go.rb | 7 ++++--- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index d4f4c85..2863f1a 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -87,6 +87,7 @@ type Terminal struct { margin [4]sizeSpec strong tui.Attr bordered bool + cleanExit bool border tui.Window window tui.Window pborder tui.Window @@ -366,6 +367,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { history: opts.History, margin: opts.Margin, bordered: opts.Bordered, + cleanExit: opts.ClearOnExit, strong: strongAttr, cycle: opts.Cycle, header: header, @@ -1341,7 +1343,14 @@ func (t *Terminal) Loop() { }() } - exit := func(code int) { + exit := func(code int, printQuery bool) { + if !t.cleanExit && t.fullscreen && t.inlineInfo { + t.placeCursor() + } + t.tui.Close() + if printQuery { + t.printer(string(t.input)) + } if code <= exitNoMatch && t.history != nil { t.history.append(string(t.input)) } @@ -1389,11 +1398,11 @@ func (t *Terminal) Loop() { case reqRedraw: t.redraw() case reqClose: - t.tui.Close() if t.output() { - exit(exitOk) + exit(exitOk, false) + } else { + exit(exitNoMatch, false) } - exit(exitNoMatch) case reqPreviewDisplay: t.previewer.text = value.(string) t.previewer.lines = strings.Count(t.previewer.text, "\n") @@ -1402,12 +1411,9 @@ func (t *Terminal) Loop() { case reqPreviewRefresh: t.printPreview() case reqPrintQuery: - t.tui.Close() - t.printer(string(t.input)) - exit(exitOk) + exit(exitOk, true) case reqQuit: - t.tui.Close() - exit(exitInterrupt) + exit(exitInterrupt, false) } } t.placeCursor() diff --git a/src/tui/light.go b/src/tui/light.go index c19c8dc..5159aaf 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -182,10 +182,18 @@ func (r *LightRenderer) Init() { if r.fullscreen { r.smcup() } else { - r.csi("J") + // We assume that --no-clear is used for repetitive relaunching of fzf. + // So we do not clear the lower bottom of the screen. + if r.clearOnExit { + r.csi("J") + } y, x := r.findOffset() r.mouse = r.mouse && y >= 0 - if x > 0 { + // When --no-clear is used for repetitive relaunching, there is a small + // time frame between fzf processes where the user keystrokes are not + // captured by either of fzf process which can cause x offset to be + // increased and we're left with unwanted extra new line. + if x > 0 && r.clearOnExit { r.upOneLine = true r.makeSpace() } @@ -200,7 +208,7 @@ func (r *LightRenderer) Init() { r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi("G") r.csi("K") - // r.csi("s") + r.csi("s") if !r.fullscreen && r.mouse { r.yoffset, _ = r.findOffset() } @@ -586,10 +594,8 @@ func (r *LightRenderer) Close() { } r.csi("J") } - } else if r.fullscreen { - r.csi("G") - } else { - r.move(r.height, 0) + } else if !r.fullscreen { + r.csi("u") } if r.mouse { r.csi("?1000l") diff --git a/test/test_go.rb b/test/test_go.rb index f88d96f..6c9a90d 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1253,11 +1253,12 @@ class TestGoFZF < TestBase end def test_no_clear - tmux.send_keys 'seq 100 | fzf --no-clear --inline-info --height 5', :Enter - prompt = '> < 100/100' + tmux.send_keys "seq 10 | fzf --no-clear --inline-info --height 5 > #{tempname}", :Enter + prompt = '> < 10/10' tmux.until { |lines| lines[-1] == prompt } tmux.send_keys :Enter - tmux.until { |lines| lines[-2] == prompt && lines[-1] == '1' } + tmux.until { |_| %w[1] == File.readlines(tempname).map(&:chomp) } + tmux.until { |lines| lines[-1] == prompt } end def test_change_top From a9e64efe450b2bddedbea852662b0b58656e949b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 19 Jul 2017 13:17:06 +0900 Subject: [PATCH 537/658] Fix regression: output printed on alternate screen --- src/terminal.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 2863f1a..0338136 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1343,14 +1343,12 @@ func (t *Terminal) Loop() { }() } - exit := func(code int, printQuery bool) { + exit := func(getCode func() int) { if !t.cleanExit && t.fullscreen && t.inlineInfo { t.placeCursor() } t.tui.Close() - if printQuery { - t.printer(string(t.input)) - } + code := getCode() if code <= exitNoMatch && t.history != nil { t.history.append(string(t.input)) } @@ -1398,11 +1396,12 @@ func (t *Terminal) Loop() { case reqRedraw: t.redraw() case reqClose: - if t.output() { - exit(exitOk, false) - } else { - exit(exitNoMatch, false) - } + exit(func() int { + if t.output() { + return exitOk + } + return exitNoMatch + }) case reqPreviewDisplay: t.previewer.text = value.(string) t.previewer.lines = strings.Count(t.previewer.text, "\n") @@ -1411,9 +1410,12 @@ func (t *Terminal) Loop() { case reqPreviewRefresh: t.printPreview() case reqPrintQuery: - exit(exitOk, true) + exit(func() int { + t.printer(string(t.input)) + return exitOk + }) case reqQuit: - exit(exitInterrupt, false) + exit(func() int { return exitInterrupt }) } } t.placeCursor() From 28810c178f9697b0a6ca32b938f4d5e556f54444 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 19 Jul 2017 21:49:41 +0900 Subject: [PATCH 538/658] Optimize ANSI code scanner This change gives 5x speed improvement --- src/ansi.go | 60 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/src/ansi.go b/src/ansi.go index 3636e02..5856bd5 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -44,7 +44,21 @@ func init() { */ // 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.|[\x0e\x0f]|.\x08") + ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x0e\x0f]|.\x08)") +} + +func findAnsiStart(str string) int { + idx := 0 + for ; idx < len(str); idx++ { + b := str[idx] + if b == 0x1b || b == 0x0e || b == 0x0f { + return idx + } + if b == 0x08 && idx > 0 { + return idx - 1 + } + } + return idx } func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) { @@ -55,41 +69,61 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state}) } - idx := 0 - for _, offset := range ansiRegex.FindAllStringIndex(str, -1) { - prev := str[idx:offset[0]] - output.WriteString(prev) + prevIdx := 0 + runeCount := 0 + for idx := 0; idx < len(str); { + idx += findAnsiStart(str[idx:]) + + // No sign of ANSI code + if idx == len(str) { + break + } + + // Make sure that we found an ANSI code + offset := ansiRegex.FindStringIndex(str[idx:]) + if offset == nil { + idx++ + continue + } + offset[0] += idx + offset[1] += idx + idx = offset[1] + + // Check if we should continue + prev := str[prevIdx:offset[0]] if proc != nil && !proc(prev, state) { return "", nil, nil } - newState := interpretCode(str[offset[0]:offset[1]], state) + prevIdx = offset[1] + runeCount += utf8.RuneCountInString(prev) + output.WriteString(prev) + + newState := interpretCode(str[offset[0]:offset[1]], state) if !newState.equals(state) { if state != nil { // Update last offset - (&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes())) + (&offsets[len(offsets)-1]).offset[1] = int32(runeCount) } if newState.colored() { // Append new offset state = newState - newLen := int32(utf8.RuneCount(output.Bytes())) - offsets = append(offsets, ansiOffset{[2]int32{newLen, newLen}, *state}) + offsets = append(offsets, ansiOffset{[2]int32{int32(runeCount), int32(runeCount)}, *state}) } else { // Discard state state = nil } } - - idx = offset[1] } - rest := str[idx:] + rest := str[prevIdx:] if len(rest) > 0 { output.WriteString(rest) if state != nil { // Update last offset - (&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes())) + runeCount += utf8.RuneCountInString(rest) + (&offsets[len(offsets)-1]).offset[1] = int32(runeCount) } } if proc != nil { From bc9d2abdb67639e06f7002b278341fb498b79456 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 19 Jul 2017 22:46:16 +0900 Subject: [PATCH 539/658] Improve preview window rendering - Fix incorrect display of the last line when more than a line is wrapped above - Avoid unnecessary flickering of the window --- src/terminal.go | 6 ++++-- src/tui/light.go | 16 ++++++++++++---- src/tui/tcell.go | 4 ++++ src/tui/tui.go | 1 + 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 0338136..81fb880 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -962,6 +962,7 @@ func (t *Terminal) printPreview() { } reader := bufio.NewReader(strings.NewReader(t.previewer.text)) lineNo := -t.previewer.offset + height := t.pwindow.Height() var ansi *ansiState for { line, err := reader.ReadString('\n') @@ -970,7 +971,8 @@ func (t *Terminal) printPreview() { line = line[:len(line)-1] } lineNo++ - if lineNo > t.pwindow.Height() { + if lineNo > height || + t.pwindow.Y() == height-1 && t.pwindow.X() > 0 { break } else if lineNo > 0 { var fillRet tui.FillReturn @@ -1000,7 +1002,7 @@ func (t *Terminal) printPreview() { } } t.pwindow.FinishFill() - if t.previewer.lines > t.pwindow.Height() { + if t.previewer.lines > height { offset := fmt.Sprintf("%d/%d", t.previewer.offset+1, t.previewer.lines) pos := t.pwindow.Width() - len(offset) if t.tui.DoesAutoWrap() { diff --git a/src/tui/light.go b/src/tui/light.go index 5159aaf..7fc231f 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -705,6 +705,10 @@ func (w *LightWindow) X() int { return w.posx } +func (w *LightWindow) Y() int { + return w.posy +} + func (w *LightWindow) Enclose(y int, x int) bool { return x >= w.left && x < (w.left+w.width) && y >= w.top && y < (w.top+w.height) @@ -839,17 +843,20 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn { for j, wl := range lines { if w.posx >= w.Width()-1 && wl.displayWidth == 0 { if w.posy < w.height-1 { - w.MoveAndClear(w.posy+1, 0) + w.Move(w.posy+1, 0) } return FillNextLine } w.stderrInternal(wl.text, false) w.posx += wl.displayWidth + + // Wrap line if j < len(lines)-1 || i < len(allLines)-1 { if w.posy+1 >= w.height { return FillSuspend } - w.MoveAndClear(w.posy+1, 0) + w.MoveAndClear(w.posy, w.posx) + w.Move(w.posy+1, 0) onMove() } } @@ -864,13 +871,13 @@ func (w *LightWindow) setBg() { } func (w *LightWindow) Fill(text string) FillReturn { - w.MoveAndClear(w.posy, w.posx) + w.Move(w.posy, w.posx) w.setBg() return w.fill(text, w.setBg) } func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn { - w.MoveAndClear(w.posy, w.posx) + w.Move(w.posy, w.posx) if bg == colDefault { bg = w.bg } @@ -882,6 +889,7 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu } func (w *LightWindow) FinishFill() { + w.MoveAndClear(w.posy, w.posx) for y := w.posy + 1; y < w.height; y++ { w.MoveAndClear(y, 0) } diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 0c80de2..bed5280 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -164,6 +164,10 @@ func (w *TcellWindow) X() int { return w.lastX } +func (w *TcellWindow) Y() int { + return w.lastY +} + func (r *FullscreenRenderer) DoesAutoWrap() bool { return false } diff --git a/src/tui/tui.go b/src/tui/tui.go index 1ba8ca7..2420616 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -236,6 +236,7 @@ type Window interface { Close() X() int + Y() int Enclose(y int, x int) bool Move(y int, x int) From c9f16b6430f3b9c9d12ee078e2218e8467c13340 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 20 Jul 2017 02:44:30 +0900 Subject: [PATCH 540/658] Avoid unconditionally storsing input as runes When --with-nth is used, fzf used to preprocess each line and store the result as rune array, which was wasteful if the line only contains ascii characters. --- src/core.go | 14 +++------- src/options_test.go | 5 ++-- src/pattern.go | 2 +- src/pattern_test.go | 2 +- src/terminal.go | 2 +- src/tokenizer.go | 63 +++++++++++++++++++----------------------- src/tokenizer_test.go | 14 ++++------ src/util/chars.go | 43 ---------------------------- src/util/chars_test.go | 26 ----------------- 9 files changed, 44 insertions(+), 127 deletions(-) diff --git a/src/core.go b/src/core.go index aa42510..3e60934 100644 --- a/src/core.go +++ b/src/core.go @@ -63,9 +63,6 @@ func Run(opts *Options, revision string) { ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) { return util.ToChars(data), nil } - ansiProcessorRunes := func(data []rune) (util.Chars, *[]ansiOffset) { - return util.RunesToChars(data), nil - } if opts.Ansi { if opts.Theme != nil { var state *ansiState @@ -82,9 +79,6 @@ func Run(opts *Options, revision string) { return util.RunesToChars([]rune(trimmed)), nil } } - ansiProcessorRunes = func(data []rune) (util.Chars, *[]ansiOffset) { - return ansiProcessor([]byte(string(data))) - } } // Chunk list @@ -103,15 +97,15 @@ func Run(opts *Options, revision string) { }) } else { chunkList = NewChunkList(func(data []byte, index int) Item { - tokens := Tokenize(util.ToChars(data), opts.Delimiter) + tokens := Tokenize(string(data), opts.Delimiter) trans := Transform(tokens, opts.WithNth) + transformed := joinTokens(trans) if len(header) < opts.HeaderLines { - header = append(header, string(joinTokens(trans))) + header = append(header, transformed) eventBox.Set(EvtHeader, header) return nilItem } - textRunes := joinTokens(trans) - trimmed, colors := ansiProcessorRunes(textRunes) + trimmed, colors := ansiProcessor([]byte(transformed)) trimmed.Index = int32(index) return Item{text: trimmed, colors: colors, origText: &data} }) diff --git a/src/options_test.go b/src/options_test.go index 907faf0..d3c9345 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/junegunn/fzf/src/tui" - "github.com/junegunn/fzf/src/util" ) func TestDelimiterRegex(t *testing.T) { @@ -44,7 +43,7 @@ func TestDelimiterRegex(t *testing.T) { func TestDelimiterRegexString(t *testing.T) { delim := delimiterRegexp("*") - tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim) + tokens := Tokenize("-*--*---**---", delim) if delim.regex != nil || tokens[0].text.ToString() != "-*" || tokens[1].text.ToString() != "--*" || @@ -57,7 +56,7 @@ func TestDelimiterRegexString(t *testing.T) { func TestDelimiterRegexRegex(t *testing.T) { delim := delimiterRegexp("--\\*") - tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim) + tokens := Tokenize("-*--*---**---", delim) if delim.str != nil || tokens[0].text.ToString() != "-*--*" || tokens[1].text.ToString() != "---*" || diff --git a/src/pattern.go b/src/pattern.go index 97ee8fd..64296d7 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -362,7 +362,7 @@ func (p *Pattern) prepareInput(item *Item) []Token { return *item.transformed } - tokens := Tokenize(item.text, p.delimiter) + tokens := Tokenize(item.text.ToString(), p.delimiter) ret := Transform(tokens, p.nth) item.transformed = &ret return ret diff --git a/src/pattern_test.go b/src/pattern_test.go index 31a127e..85c174c 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -133,7 +133,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(util.RunesToChars([]rune("junegunn")), Delimiter{}) + tokens := Tokenize("junegunn", Delimiter{}) trans := Transform(tokens, []Range{Range{1, 1}}) origBytes := []byte("junegunn.choi") diff --git a/src/terminal.go b/src/terminal.go index 81fb880..8d0b6bf 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1174,7 +1174,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo for idx, item := range items { chars := util.RunesToChars([]rune(item.AsString(stripAnsi))) - tokens := Tokenize(chars, delimiter) + tokens := Tokenize(chars.ToString(), delimiter) trans := Transform(tokens, ranges) str := string(joinTokens(trans)) if delimiter.str != nil { diff --git a/src/tokenizer.go b/src/tokenizer.go index 0e216ac..5b7a8b6 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -1,6 +1,7 @@ package fzf import ( + "bytes" "regexp" "strconv" "strings" @@ -74,14 +75,14 @@ func ParseRange(str *string) (Range, bool) { return newRange(n, n), true } -func withPrefixLengths(tokens []util.Chars, begin int) []Token { +func withPrefixLengths(tokens []string, begin int) []Token { ret := make([]Token, len(tokens)) prefixLength := begin - for idx, token := range tokens { - // NOTE: &tokens[idx] instead of &tokens - ret[idx] = Token{&tokens[idx], int32(prefixLength)} - prefixLength += token.Length() + for idx := range tokens { + chars := util.ToChars([]byte(tokens[idx])) + ret[idx] = Token{&chars, int32(prefixLength)} + prefixLength += chars.Length() } return ret } @@ -92,16 +93,15 @@ const ( awkWhite ) -func awkTokenizer(input util.Chars) ([]util.Chars, int) { +func awkTokenizer(input string) ([]string, int) { // 9, 32 - ret := []util.Chars{} + ret := []string{} prefixLength := 0 state := awkNil - numChars := input.Length() begin := 0 end := 0 - for idx := 0; idx < numChars; idx++ { - r := input.Get(idx) + for idx := 0; idx < len(input); idx++ { + r := input[idx] white := r == 9 || r == 32 switch state { case awkNil: @@ -119,19 +119,19 @@ func awkTokenizer(input util.Chars) ([]util.Chars, int) { if white { end = idx + 1 } else { - ret = append(ret, input.Slice(begin, end)) + ret = append(ret, input[begin:end]) state, begin, end = awkBlack, idx, idx+1 } } } if begin < end { - ret = append(ret, input.Slice(begin, end)) + ret = append(ret, input[begin:end]) } return ret, prefixLength } // Tokenize tokenizes the given string with the delimiter -func Tokenize(text util.Chars, delimiter Delimiter) []Token { +func Tokenize(text string, delimiter Delimiter) []Token { if delimiter.str == nil && delimiter.regex == nil { // AWK-style (\S+\s*) tokens, prefixLength := awkTokenizer(text) @@ -139,36 +139,31 @@ func Tokenize(text util.Chars, delimiter Delimiter) []Token { } if delimiter.str != nil { - return withPrefixLengths(text.Split(*delimiter.str), 0) + return withPrefixLengths(strings.SplitAfter(text, *delimiter.str), 0) } // FIXME performance var tokens []string if delimiter.regex != nil { - str := text.ToString() - for len(str) > 0 { - loc := delimiter.regex.FindStringIndex(str) + for len(text) > 0 { + loc := delimiter.regex.FindStringIndex(text) if loc == nil { - loc = []int{0, len(str)} + loc = []int{0, len(text)} } last := util.Max(loc[1], 1) - tokens = append(tokens, str[:last]) - str = str[last:] + tokens = append(tokens, text[:last]) + text = text[last:] } } - asRunes := make([]util.Chars, len(tokens)) - for i, token := range tokens { - asRunes[i] = util.RunesToChars([]rune(token)) - } - return withPrefixLengths(asRunes, 0) + return withPrefixLengths(tokens, 0) } -func joinTokens(tokens []Token) []rune { - ret := []rune{} +func joinTokens(tokens []Token) string { + var output bytes.Buffer for _, token := range tokens { - ret = append(ret, token.text.ToRunes()...) + output.WriteString(token.text.ToString()) } - return ret + return output.String() } // Transform is used to transform the input when --with-nth option is given @@ -181,7 +176,7 @@ func Transform(tokens []Token, withNth []Range) []Token { if r.begin == r.end { idx := r.begin if idx == rangeEllipsis { - chars := util.RunesToChars(joinTokens(tokens)) + chars := util.ToChars([]byte(joinTokens(tokens))) parts = append(parts, &chars) } else { if idx < 0 { @@ -224,15 +219,15 @@ func Transform(tokens []Token, withNth []Range) []Token { var merged util.Chars switch len(parts) { case 0: - merged = util.RunesToChars([]rune{}) + merged = util.ToChars([]byte{}) case 1: merged = *parts[0] default: - runes := []rune{} + var output bytes.Buffer for _, part := range parts { - runes = append(runes, part.ToRunes()...) + output.WriteString(part.ToString()) } - merged = util.RunesToChars(runes) + merged = util.ToChars([]byte(output.String())) } var prefixLength int32 diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go index 5925090..110fd06 100644 --- a/src/tokenizer_test.go +++ b/src/tokenizer_test.go @@ -2,8 +2,6 @@ package fzf import ( "testing" - - "github.com/junegunn/fzf/src/util" ) func TestParseRange(t *testing.T) { @@ -47,19 +45,19 @@ func TestParseRange(t *testing.T) { func TestTokenize(t *testing.T) { // AWK-style input := " abc: def: ghi " - tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{}) + tokens := Tokenize(input, Delimiter{}) if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 { t.Errorf("%s", tokens) } // With delimiter - tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":")) + tokens = Tokenize(input, delimiterRegexp(":")) if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 { - t.Errorf("%s", tokens) + t.Error(tokens[0].text.ToString(), tokens[0].prefixLength) } // With delimiter regex - tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp("\\s+")) + tokens = Tokenize(input, delimiterRegexp("\\s+")) if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 || tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 || tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 || @@ -71,7 +69,7 @@ func TestTokenize(t *testing.T) { func TestTransform(t *testing.T) { input := " abc: def: ghi: jkl" { - tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{}) + tokens := Tokenize(input, Delimiter{}) { ranges := splitNth("1,2,3") tx := Transform(tokens, ranges) @@ -93,7 +91,7 @@ func TestTransform(t *testing.T) { } } { - tokens := Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":")) + tokens := Tokenize(input, delimiterRegexp(":")) { ranges := splitNth("1..2,3,2..,1") tx := Transform(tokens, ranges) diff --git a/src/util/chars.go b/src/util/chars.go index 5e70200..61e9341 100644 --- a/src/util/chars.go +++ b/src/util/chars.go @@ -157,46 +157,3 @@ func (chars *Chars) CopyRunes(dest []rune) { } return } - -func (chars *Chars) Slice(b int, e int) Chars { - if runes := chars.optionalRunes(); runes != nil { - return RunesToChars(runes[b:e]) - } - return Chars{slice: chars.slice[b:e], inBytes: true} -} - -func (chars *Chars) Split(delimiter string) []Chars { - delim := []rune(delimiter) - numChars := chars.Length() - numDelim := len(delim) - begin := 0 - ret := make([]Chars, 0, 1) - - for index := 0; index < numChars; { - if index+numDelim <= numChars { - match := true - for off, d := range delim { - if chars.Get(index+off) != d { - match = false - break - } - } - // Found the delimiter - if match { - incr := Max(numDelim, 1) - ret = append(ret, chars.Slice(begin, index+incr)) - index += incr - begin = index - continue - } - } else { - // Impossible to find the delimiter in the remaining substring - break - } - index++ - } - if begin < numChars || len(ret) == 0 { - ret = append(ret, chars.Slice(begin, numChars)) - } - return ret -} diff --git a/src/util/chars_test.go b/src/util/chars_test.go index 07b8dea..b7983f3 100644 --- a/src/util/chars_test.go +++ b/src/util/chars_test.go @@ -44,29 +44,3 @@ func TestTrimLength(t *testing.T) { check(" h o ", 5) check(" ", 0) } - -func TestSplit(t *testing.T) { - check := func(str string, delim string, tokens ...string) { - input := ToChars([]byte(str)) - result := input.Split(delim) - if len(result) != len(tokens) { - t.Errorf( - "Invalid Split result for '%s': %d tokens found (expected %d): %s", - str, len(result), len(tokens), result) - } - for idx, token := range tokens { - if result[idx].ToString() != token { - t.Errorf("Invalid Split result for '%s': %s (expected %s)", - str, result[idx].ToString(), token) - } - } - } - check("abc:def::", ":", "abc:", "def:", ":") - check("abc:def::", "-", "abc:def::") - check("abc", "", "a", "b", "c") - check("abc", "a", "a", "bc") - check("abc", "ab", "ab", "c") - check("abc", "abc", "abc") - check("abc", "abcd", "abc") - check("", "abcd", "") -} From 18a1aeaa911700488258999a9f05806b6975ff3c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 21 Jul 2017 00:08:55 +0900 Subject: [PATCH 541/658] 0.16.9 --- CHANGELOG.md | 12 ++++++++++++ install | 2 +- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffb10fe..737064c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ CHANGELOG ========= +0.16.9 +------ +- Memory and performance optimization + - Around 20% performance improvement for general use cases + - Up to 5x faster processing of `--ansi` + - Up to 50% reduction of memory usage +- Bug fixes and usability improvements + - Fixed handling of bracketed paste mode + - [ERROR] on info line when the default command failed + - More efficient rendering of preview window + - `--no-clear` updated for repetitive relaunching scenarios + 0.16.8 ------ - New `change` event and `top` action for `--bind` diff --git a/install b/install index 40f5ec4..8ae2722 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.16.8 +version=0.16.9 auto_completion= key_bindings= update_config=2 diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index a9db142..b78f54c 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Jun 2017" "fzf 0.16.8" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Jul 2017" "fzf 0.16.9" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index dbd65bb..815c144 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. -.TH fzf 1 "Jun 2017" "fzf 0.16.8" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jul 2017" "fzf 0.16.9" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 56097ff..30d1896 100644 --- a/src/constants.go +++ b/src/constants.go @@ -9,7 +9,7 @@ import ( const ( // Current version - version = "0.16.8" + version = "0.16.9" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 06301c7847c69915fe8214ab87480f0eba59c5bf Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 21 Jul 2017 16:43:00 +0900 Subject: [PATCH 542/658] Fix regression: ANSI color in preview window not cleared --- src/tui/light.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tui/light.go b/src/tui/light.go index 7fc231f..a7d12f3 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -882,8 +882,8 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu bg = w.bg } if w.csiColor(fg, bg, attr) { - return w.fill(text, func() { w.csiColor(fg, bg, attr) }) defer w.csi("m") + return w.fill(text, func() { w.csiColor(fg, bg, attr) }) } return w.fill(text, w.setBg) } From bd98f988f0502251d13650afddde1916ad87d28d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 21 Jul 2017 17:29:14 +0900 Subject: [PATCH 543/658] Further reduce unnecessary rune array conversion I was too quick to release 0.16.9, this commit makes --ansi processing even faster. --- src/algo/algo_test.go | 2 +- src/core.go | 4 ++-- src/merger_test.go | 2 +- src/pattern_test.go | 6 +++--- src/terminal.go | 5 ++--- src/terminal_test.go | 2 +- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index df8b227..2da0b3c 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -17,7 +17,7 @@ func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool if !caseSensitive { pattern = strings.ToLower(pattern) } - res, pos := fun(caseSensitive, normalize, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil) + res, pos := fun(caseSensitive, normalize, forward, util.ToChars([]byte(input)), []rune(pattern), true, nil) var start, end int if pos == nil || len(*pos) == 0 { start = res.Start diff --git a/src/core.go b/src/core.go index 3e60934..74ff9e7 100644 --- a/src/core.go +++ b/src/core.go @@ -69,14 +69,14 @@ func Run(opts *Options, revision string) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { trimmed, offsets, newState := extractColor(string(data), state, nil) state = newState - return util.RunesToChars([]rune(trimmed)), offsets + return util.ToChars([]byte(trimmed)), offsets } } else { // When color is disabled but ansi option is given, // we simply strip out ANSI codes from the input ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) { trimmed, _, _ := extractColor(string(data), nil, nil) - return util.RunesToChars([]rune(trimmed)), nil + return util.ToChars([]byte(trimmed)), nil } } } diff --git a/src/merger_test.go b/src/merger_test.go index b98aca8..c6af4f6 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -17,7 +17,7 @@ func assert(t *testing.T, cond bool, msg ...string) { func randResult() Result { str := fmt.Sprintf("%d", rand.Uint32()) - chars := util.RunesToChars([]rune(str)) + chars := util.ToChars([]byte(str)) chars.Index = rand.Int31() return Result{item: &Item{text: chars}} } diff --git a/src/pattern_test.go b/src/pattern_test.go index 85c174c..81d158a 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -78,7 +78,7 @@ func TestExact(t *testing.T) { pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("'abc")) res, pos := algo.ExactMatchNaive( - pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text, true, nil) + pattern.caseSensitive, pattern.normalize, pattern.forward, util.ToChars([]byte("aabbcc abc")), pattern.termSets[0][0].text, true, nil) if res.Start != 7 || res.End != 10 { t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) } @@ -94,7 +94,7 @@ func TestEqual(t *testing.T) { match := func(str string, sidxExpected int, eidxExpected int) { res, pos := algo.EqualMatch( - pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text, true, nil) + pattern.caseSensitive, pattern.normalize, pattern.forward, util.ToChars([]byte(str)), pattern.termSets[0][0].text, true, nil) if res.Start != sidxExpected || res.End != eidxExpected { t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) } @@ -140,7 +140,7 @@ func TestOrigTextAndTransformed(t *testing.T) { for _, extended := range []bool{false, true} { chunk := Chunk{ Item{ - text: util.RunesToChars([]rune("junegunn")), + text: util.ToChars([]byte("junegunn")), origText: &origBytes, transformed: &trans}, } diff --git a/src/terminal.go b/src/terminal.go index 8d0b6bf..d6d2155 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -712,7 +712,7 @@ func (t *Terminal) printHeader() { trimmed, colors, newState := extractColor(lineStr, state, nil) state = newState item := &Item{ - text: util.RunesToChars([]rune(trimmed)), + text: util.ToChars([]byte(trimmed)), colors: colors} t.move(line, 2, true) @@ -1173,8 +1173,7 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, fo } for idx, item := range items { - chars := util.RunesToChars([]rune(item.AsString(stripAnsi))) - tokens := Tokenize(chars.ToString(), delimiter) + tokens := Tokenize(item.AsString(stripAnsi), delimiter) trans := Transform(tokens, ranges) str := string(joinTokens(trans)) if delimiter.str != nil { diff --git a/src/terminal_test.go b/src/terminal_test.go index 41941ee..d42d2b8 100644 --- a/src/terminal_test.go +++ b/src/terminal_test.go @@ -10,7 +10,7 @@ import ( func newItem(str string) *Item { bytes := []byte(str) trimmed, _, _ := extractColor(str, nil, nil) - return &Item{origText: &bytes, text: util.RunesToChars([]rune(trimmed))} + return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))} } func TestReplacePlaceholder(t *testing.T) { From 6dbc108da2770426eda79551368ac2582d9df9b4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 21 Jul 2017 18:41:11 +0900 Subject: [PATCH 544/658] 0.16.10 --- CHANGELOG.md | 5 +++++ install | 2 +- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 737064c..90357cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +0.16.10 +------- +- Fixed invalid handling of ANSI colors in preview window +- Further improved `--ansi` performance + 0.16.9 ------ - Memory and performance optimization diff --git a/install b/install index 8ae2722..429dd68 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.16.9 +version=0.16.10 auto_completion= key_bindings= update_config=2 diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index b78f54c..2c12cdf 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Jul 2017" "fzf 0.16.9" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Jul 2017" "fzf 0.16.10" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 815c144..fe69843 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Jul 2017" "fzf 0.16.9" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Jul 2017" "fzf 0.16.10" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 30d1896..9b71c52 100644 --- a/src/constants.go +++ b/src/constants.go @@ -9,7 +9,7 @@ import ( const ( // Current version - version = "0.16.9" + version = "0.16.10" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From a2604c0963caf458fdd46fe0f83fcd2cc6388582 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 23 Jul 2017 13:12:15 +0900 Subject: [PATCH 545/658] [nvim] Disable number in fzf buffer https://github.com/junegunn/fzf.vim/issues/396#issuecomment-317214036 One can override the setting on FileType fzf autocmd. --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 3beb5be..a70a3b3 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -688,7 +688,7 @@ function! s:execute_term(dict, command, temps) abort lcd - endif endtry - setlocal nospell bufhidden=wipe nobuflisted + setlocal nospell bufhidden=wipe nobuflisted nonumber setf fzf startinsert return [] From 980b58ef5adad8d5d556ace537c68cb6f2189c7a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 23 Jul 2017 22:07:24 +0900 Subject: [PATCH 546/658] Update README Removed outdated animated GIF. --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index da4a0cc..41aaf53 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,19 @@ fzf is a general-purpose command-line fuzzy finder. -![](https://raw.github.com/junegunn/i/master/fzf.gif) + + +It's an interactive Unix filter for command-line that can be used with any +list; files, command history, processes, hostnames, bookmarks, git commits, +etc. Pros ---- -- No dependencies +- Portable, no dependencies - Blazingly fast - The most comprehensive feature set -- Flexible layout using tmux panes +- Flexible layout - Batteries included - Vim/Neovim plugin, key bindings and fuzzy auto-completion From bc3983181d992c564e069791b5c6549bde9bfeb3 Mon Sep 17 00:00:00 2001 From: Alexey Shamrin Date: Tue, 25 Jul 2017 13:10:34 +0300 Subject: [PATCH 547/658] Update fish comments, because 2.6.0 was released (#991) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 41aaf53..ac0ba3a 100644 --- a/README.md +++ b/README.md @@ -430,10 +430,10 @@ export FZF_DEFAULT_COMMAND=' #### Fish shell -It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362) -(will be fixed in 2.6.0) that it doesn't allow reading from STDIN in command -substitution, which means simple `vim (fzf)` won't work as expected. The -workaround is to use the `read` fish command: +Fish shell before version 2.6.0 [doesn't allow](https://github.com/fish-shell/fish-shell/issues/1362) +reading from STDIN in command substitution, which means simple `vim (fzf)` +doesn't work as expected. The workaround for fish 2.5.0 and earlier is to use +the `read` fish command: ```sh fzf | read -l result; and vim $result From a0a3c349c90d6210edec28d4d528a82f764268f1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 28 Jul 2017 01:39:25 +0900 Subject: [PATCH 548/658] Update preview window when selection has changed Close #995 --- src/terminal.go | 46 ++++++++++++++++++++++++++-------------------- test/test_go.rb | 10 ++++++++++ 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index d6d2155..36cc7c2 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -101,6 +101,7 @@ type Terminal struct { printer func(string) merger *Merger selected map[int32]selectedItem + version int64 reqBox *util.EventBox preview previewOpts previewer previewer @@ -1257,6 +1258,24 @@ func (t *Terminal) truncateQuery() { t.cx = util.Constrain(t.cx, 0, len(t.input)) } +func (t *Terminal) selectItem(item *Item) { + t.selected[item.Index()] = selectedItem{time.Now(), item} + t.version++ +} + +func (t *Terminal) deselectItem(item *Item) { + delete(t.selected, item.Index()) + t.version++ +} + +func (t *Terminal) toggleItem(item *Item) { + if _, found := t.selected[item.Index()]; !found { + t.selectItem(item) + } else { + t.deselectItem(item) + } +} + // Loop is called to start Terminal I/O func (t *Terminal) Loop() { // prof := profile.Start(profile.ProfilePath("/tmp/")) @@ -1359,6 +1378,7 @@ func (t *Terminal) Loop() { go func() { var focused *Item + var version int64 for { t.reqBox.Wait(func(events *util.Events) { defer events.Clear() @@ -1375,7 +1395,8 @@ func (t *Terminal) Loop() { case reqList: t.printList() currentFocus := t.currentItem() - if currentFocus != focused { + if currentFocus != focused || version != t.version { + version = t.version focused = currentFocus if t.isPreviewEnabled() { _, list := t.buildPlusList(t.preview.command, false) @@ -1441,22 +1462,9 @@ func (t *Terminal) Loop() { } } } - selectItem := func(item *Item) bool { - if _, found := t.selected[item.Index()]; !found { - t.selected[item.Index()] = selectedItem{time.Now(), item} - return true - } - return false - } - toggleY := func(y int) { - item := t.merger.Get(y).item - if !selectItem(item) { - delete(t.selected, item.Index()) - } - } toggle := func() { if t.cy < t.merger.Length() { - toggleY(t.cy) + t.toggleItem(t.merger.Get(t.cy).item) req(reqInfo) } } @@ -1570,16 +1578,14 @@ func (t *Terminal) Loop() { case actSelectAll: if t.multi { for i := 0; i < t.merger.Length(); i++ { - item := t.merger.Get(i).item - selectItem(item) + t.selectItem(t.merger.Get(i).item) } req(reqList, reqInfo) } case actDeselectAll: if t.multi { for i := 0; i < t.merger.Length(); i++ { - item := t.merger.Get(i) - delete(t.selected, item.Index()) + t.deselectItem(t.merger.Get(i).item) } req(reqList, reqInfo) } @@ -1591,7 +1597,7 @@ func (t *Terminal) Loop() { case actToggleAll: if t.multi { for i := 0; i < t.merger.Length(); i++ { - toggleY(i) + t.toggleItem(t.merger.Get(i).item) } req(reqList, reqInfo) } diff --git a/test/test_go.rb b/test/test_go.rb index 6c9a90d..fa4915a 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1274,6 +1274,16 @@ class TestGoFZF < TestBase tmux.until { |lines| lines[-3] == '> 11' } tmux.send_keys :Enter end + + def test_preview_update_on_select + tmux.send_keys(%(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all), + :Enter) + tmux.until { |lines| lines.item_count == 10 } + tmux.send_keys 'a' + tmux.until { |lines| lines.any? { |line| line.include? '1 2 3 4 5' } } + tmux.send_keys 'a' + tmux.until { |lines| !lines.any? { |line| line.include? '1 2 3 4 5' } } + end end module TestShell From 41f0b2c354dc1cea0b84bf420d9e33c652c4a91b Mon Sep 17 00:00:00 2001 From: Andrew Halberstadt Date: Thu, 27 Jul 2017 23:22:33 -0400 Subject: [PATCH 549/658] Add MinGW on Windows to install script (#998) Running uname -sm yields: MINGW32_NT-6.2 i686 --- install | 1 + 1 file changed, 1 insertion(+) diff --git a/install b/install index 429dd68..79a1888 100755 --- a/install +++ b/install @@ -171,6 +171,7 @@ case "$archi" in OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64}.tgz ;; OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386}.tgz ;; CYGWIN*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;; + MINGW*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;; *) binary_available=0 binary_error=1 ;; esac From 4c5a679066972cefb831adeb179e45eb7068bbba Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 28 Jul 2017 13:13:03 +0900 Subject: [PATCH 550/658] Make deselect-all instantaneous --- src/terminal.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 36cc7c2..8c7c1a8 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1584,9 +1584,8 @@ func (t *Terminal) Loop() { } case actDeselectAll: if t.multi { - for i := 0; i < t.merger.Length(); i++ { - t.deselectItem(t.merger.Get(i).item) - } + t.selected = make(map[int32]selectedItem) + t.version++ req(reqList, reqInfo) } case actToggle: From 5e302c70e9b42af7ca446d4e1cfddc55c7c1f575 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 29 Jul 2017 15:41:55 +0900 Subject: [PATCH 551/658] Update README: rg intead of pt --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac0ba3a..caf49d0 100644 --- a/README.md +++ b/README.md @@ -393,7 +393,7 @@ Tips #### Respecting `.gitignore`, `.hgignore`, and `svn:ignore` [ag](https://github.com/ggreer/the_silver_searcher) or -[pt](https://github.com/monochromegane/the_platinum_searcher) will do the +[rg](https://github.com/BurntSushi/ripgrep) will do the filtering: ```sh From e1c8f19e8f5820fb325ef41897315f257099470f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 29 Jul 2017 17:08:51 +0900 Subject: [PATCH 552/658] Update README: Advanced topics --- README.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/README.md b/README.md index caf49d0..a8096e5 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ Table of Contents * [Settings](#settings) * [Supported commands](#supported-commands) * [Vim plugin](#vim-plugin) + * [Advanced topics](#advanced-topics) + * [Performance](#performance) + * [Preview window](#preview-window) * [Tips](#tips) * [Respecting .gitignore, .hgignore, and svn:ignore](#respecting-gitignore-hgignore-and-svnignore) * [git ls-tree for fast traversal](#git-ls-tree-for-fast-traversal) @@ -387,6 +390,87 @@ Vim plugin See [README-VIM.md](README-VIM.md). +Advanced topics +--------------- + +### Performance + +fzf is fast, and is [getting even faster][perf]. Performance should not be +a problem in most use cases. However, you might want to be aware of the +options that affect the performance. + +- `--ansi` tells fzf to extract and parse ANSI color codes in the input and it + makes the initial scanning slower. So it's not recommended that you add it + to your `$FZF_DEFAULT_OPTS`. +- `--nth` makes fzf slower as fzf has to tokenize each line. +- `--with-nth` makes fzf slower as fzf has to tokenize and reassemble each + line. +- If you absolutely need better performance, you can consider using + `--algo=v1` (the default being `v2`) to make fzf use faster greedy + algorithm. However, this algorithm is not guaranteed to find the optimal + ordering of the matches and is not recommended. + +[perf]: https://junegunn.kr/images/fzf-0.16.10.png + +### Executing external programs + +You can set up key bindings for starting external process in the foreground +(`execute`) or in the background (`execute-silent`). + +```sh +# Press F1 to open the file with less without exiting fzf +# Press CTRL-Y to copy the line to clipboard and aborts fzf (requires pbcopy) +fzf --bind 'f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort' +``` + +See *KEY BINDINGS* section of the man page for details. + +### Preview window + +When `--preview` option is set, fzf automatically starts external process with +the current line as the argument and shows the result in the split window. + +```sh +# {} is replaced to the single-quoted string of the focused line +fzf --preview 'cat {}' +``` + +Since preview window is updated only after the process is complete, it's +important that the command finishes quickly. + +```sh +# Use head instead of cat so that the command doesn't take too long to finish +fzf --preview 'head -100 {}' +``` + +Preview window supports ANSI colors, so you can use programs that +syntax-highlights the content of a file. + +- Highlight: http://www.andre-simon.de/doku/highlight/en/highlight.php +- CodeRay: http://coderay.rubychan.de/ +- Rouge: https://github.com/jneen/rouge + +```sh +# Try highlight, coderay, rougify in turn, then fall back to cat +fzf --preview '[[ $(file --mime {}) =~ binary ]] && + echo {} is a binary file || + (highlight -O ansi -l {} || + coderay {} || + rougify {} || + cat {}) 2> /dev/null | head -500' +``` + +You can customize the size and position of the preview window using +`--preview-window` option. For example, + +```sh +fzf --height 40% --reverse --preview 'file {}' --preview-window down:1 +``` + +For more advanced examples, see [Key bindings for git with fzf][fzf-git]. + +[fzf-git]: https://junegunn.kr/2016/07/fzf-git/ + Tips ---- From f1f31baae177e802e72dcd1c61491b0e07b466fe Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 29 Jul 2017 17:10:00 +0900 Subject: [PATCH 553/658] Update README: Missing TOC --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a8096e5..c264ede 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Table of Contents * [Vim plugin](#vim-plugin) * [Advanced topics](#advanced-topics) * [Performance](#performance) + * [Executing external programs](#executing-external-programs) * [Preview window](#preview-window) * [Tips](#tips) * [Respecting .gitignore, .hgignore, and svn:ignore](#respecting-gitignore-hgignore-and-svnignore) From 298749bfcd0190745aba83addd9f504363d36924 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 29 Jul 2017 17:12:46 +0900 Subject: [PATCH 554/658] Update README --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c264ede..3160644 100644 --- a/README.md +++ b/README.md @@ -415,11 +415,11 @@ options that affect the performance. ### Executing external programs -You can set up key bindings for starting external process in the foreground -(`execute`) or in the background (`execute-silent`). +You can set up key bindings for starting external processes without leaving +fzf (`execute`, `execute-silent`). -```sh -# Press F1 to open the file with less without exiting fzf +```bash +# Press F1 to open the file with less without leaving fzf # Press CTRL-Y to copy the line to clipboard and aborts fzf (requires pbcopy) fzf --bind 'f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort' ``` @@ -431,7 +431,7 @@ See *KEY BINDINGS* section of the man page for details. When `--preview` option is set, fzf automatically starts external process with the current line as the argument and shows the result in the split window. -```sh +```bash # {} is replaced to the single-quoted string of the focused line fzf --preview 'cat {}' ``` @@ -439,7 +439,7 @@ fzf --preview 'cat {}' Since preview window is updated only after the process is complete, it's important that the command finishes quickly. -```sh +```bash # Use head instead of cat so that the command doesn't take too long to finish fzf --preview 'head -100 {}' ``` @@ -451,7 +451,7 @@ syntax-highlights the content of a file. - CodeRay: http://coderay.rubychan.de/ - Rouge: https://github.com/jneen/rouge -```sh +```bash # Try highlight, coderay, rougify in turn, then fall back to cat fzf --preview '[[ $(file --mime {}) =~ binary ]] && echo {} is a binary file || @@ -464,7 +464,7 @@ fzf --preview '[[ $(file --mime {}) =~ binary ]] && You can customize the size and position of the preview window using `--preview-window` option. For example, -```sh +```bash fzf --height 40% --reverse --preview 'file {}' --preview-window down:1 ``` From 69aa2fea686b6e26418fa352abebd81e0a1ecc7b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 30 Jul 2017 17:31:50 +0900 Subject: [PATCH 555/658] Optimize fuzzy search performance for ASCII strings --- src/algo/algo.go | 68 ++++++++++++++++++++++++++++++++++++++++------- src/util/chars.go | 8 ++++++ 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index c4930c1..a2f0f2b 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -78,9 +78,11 @@ Scoring criteria */ import ( + "bytes" "fmt" "strings" "unicode" + "unicode/utf8" "github.com/junegunn/fzf/src/util" ) @@ -251,19 +253,37 @@ func normalizeRune(r rune) rune { // 2. "pattern" is already normalized if "normalize" is true type Algo func(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) +func trySkip(input *util.Chars, caseSensitive bool, b byte, from int) int { + byteArray := input.Bytes()[from:] + idx := bytes.IndexByte(byteArray, b) + if idx == 0 { + // Can't skip any further + return from + } + // We may need to search for the uppercase letter again. We don't have to + // consider normalization as we can be sure that this is an ASCII string. + if !caseSensitive && b >= 'a' && b <= 'z' { + uidx := bytes.IndexByte(byteArray, b-32) + if idx < 0 || uidx >= 0 && uidx < idx { + idx = uidx + } + } + if idx < 0 { + return -1 + } + return from + idx +} + func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { // Assume that pattern is given in lowercase if case-insensitive. // First check if there's a match and calculate bonus for each position. // If the input string is too long, consider finding the matching chars in // this phase as well (non-optimal alignment). - N := input.Length() M := len(pattern) - switch M { - case 0: + if M == 0 { return Result{0, 0, 0}, posArray(withPos, M) - case 1: - return ExactMatchNaive(caseSensitive, normalize, forward, input, pattern[0:1], withPos, slab) } + N := input.Length() // Since O(nm) algorithm can be prohibitively expensive for large input, // we fall back to the greedy algorithm. @@ -281,10 +301,31 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C // Rune array offset32, T := alloc32(offset32, slab, N, false) - // Phase 1. Check if there's a match and calculate bonus for each point + // Phase 1. Optimized search for ASCII string + firstIdx := 0 + if input.IsBytes() { + idx := 0 + for pidx := 0; pidx < M; pidx++ { + // Not possible + if pattern[pidx] >= utf8.RuneSelf { + return Result{-1, -1, 0}, nil + } + idx = trySkip(&input, caseSensitive, byte(pattern[pidx]), idx) + if idx < 0 { + return Result{-1, -1, 0}, nil + } + if pidx == 0 && idx > 0 { + // Step back to find the right bonus point + firstIdx = idx - 1 + } + idx++ + } + } + + // Phase 2. Calculate bonus for each point pidx, lastIdx, prevClass := 0, 0, charNonWord input.CopyRunes(T) - for idx := 0; idx < N; idx++ { + for idx := firstIdx; idx < N; idx++ { char := T[idx] var class charClass if char <= unicode.MaxASCII { @@ -324,8 +365,17 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C if pidx != M { return Result{-1, -1, 0}, nil } + if M == 1 && B[F[0]] == bonusBoundary { + p := int(F[0]) + result := Result{p, p + 1, scoreMatch + bonusBoundary*bonusFirstCharMultiplier} + if !withPos { + return result, nil + } + pos := []int{p} + return result, &pos + } - // Phase 2. Fill in score matrix (H) + // Phase 3. Fill in score matrix (H) // Unlike the original algorithm, we do not allow omission. width := lastIdx - int(F[0]) + 1 offset16, H := alloc16(offset16, slab, width*M, false) @@ -414,7 +464,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C } } - // Phase 3. (Optional) Backtrace to find character positions + // Phase 4. (Optional) Backtrace to find character positions pos := posArray(withPos, M) j := int(F[0]) if withPos { diff --git a/src/util/chars.go b/src/util/chars.go index 61e9341..0127bcb 100644 --- a/src/util/chars.go +++ b/src/util/chars.go @@ -65,6 +65,14 @@ func RunesToChars(runes []rune) Chars { return Chars{slice: *(*[]byte)(unsafe.Pointer(&runes)), inBytes: false} } +func (chars *Chars) IsBytes() bool { + return chars.inBytes +} + +func (chars *Chars) Bytes() []byte { + return chars.slice +} + func (chars *Chars) optionalRunes() []rune { if chars.inBytes { return nil From 8db3345c2fe5551a7cbc838ddef009813cdeec41 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 30 Jul 2017 18:15:02 +0900 Subject: [PATCH 556/658] Optimize exact match by applying the same trick for fuzzy match --- src/algo/algo.go | 62 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index a2f0f2b..7a6dad3 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -274,6 +274,41 @@ func trySkip(input *util.Chars, caseSensitive bool, b byte, from int) int { return from + idx } +func isAscii(runes []rune) bool { + for _, r := range runes { + if r >= utf8.RuneSelf { + return false + } + } + return true +} + +func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int { + // Can't determine + if !input.IsBytes() { + return 0 + } + + // Not possible + if !isAscii(pattern) { + return -1 + } + + firstIdx, idx := 0, 0 + for pidx := 0; pidx < len(pattern); pidx++ { + idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx) + if idx < 0 { + return -1 + } + if pidx == 0 && idx > 0 { + // Step back to find the right bonus point + firstIdx = idx - 1 + } + idx++ + } + return firstIdx +} + func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { // Assume that pattern is given in lowercase if case-insensitive. // First check if there's a match and calculate bonus for each position. @@ -302,30 +337,15 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C offset32, T := alloc32(offset32, slab, N, false) // Phase 1. Optimized search for ASCII string - firstIdx := 0 - if input.IsBytes() { - idx := 0 - for pidx := 0; pidx < M; pidx++ { - // Not possible - if pattern[pidx] >= utf8.RuneSelf { - return Result{-1, -1, 0}, nil - } - idx = trySkip(&input, caseSensitive, byte(pattern[pidx]), idx) - if idx < 0 { - return Result{-1, -1, 0}, nil - } - if pidx == 0 && idx > 0 { - // Step back to find the right bonus point - firstIdx = idx - 1 - } - idx++ - } + idx := asciiFuzzyIndex(&input, pattern, caseSensitive) + if idx < 0 { + return Result{-1, -1, 0}, nil } // Phase 2. Calculate bonus for each point pidx, lastIdx, prevClass := 0, 0, charNonWord input.CopyRunes(T) - for idx := firstIdx; idx < N; idx++ { + for ; idx < N; idx++ { char := T[idx] var class charClass if char <= unicode.MaxASCII { @@ -657,6 +677,10 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util return Result{-1, -1, 0}, nil } + if asciiFuzzyIndex(&text, pattern, caseSensitive) < 0 { + return Result{-1, -1, 0}, nil + } + // For simplicity, only look at the bonus at the first character position pidx := 0 bestPos, bonus, bestBonus := -1, int16(0), int16(-1) From 1fcc07e54ed2098be9f8b8b21e4b4292f2c5ea0c Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Sat, 29 Jul 2017 20:38:58 -0400 Subject: [PATCH 557/658] [vim] Fix escape of backslash in s:shortpath Close #1000 --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index a70a3b3..7e19f25 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -756,7 +756,7 @@ let s:default_action = { function! s:shortpath() let short = pathshorten(fnamemodify(getcwd(), ':~:.')) let slash = (s:is_win && !&shellslash) ? '\' : '/' - return empty(short) ? '~'.slash : short . (short =~ slash.'$' ? '' : slash) + return empty(short) ? '~'.slash : short . (short =~ escape(slash, '\').'$' ? '' : slash) endfunction function! s:cmd(bang, ...) abort From 376a76d1d3c39a1686b6ac7a6118c1f80af3178e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 30 Jul 2017 21:51:44 +0900 Subject: [PATCH 558/658] [bash] Do not append space when path completion is cancelled Close #990 --- shell/completion.bash | 9 +++++---- test/test_go.rb | 13 +++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index e8c34e5..eeb5a5d 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -151,7 +151,7 @@ __fzf_generic_path_completion() { matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $2 -q "$leftover" | while read -r item; do printf "%q$3 " "$item" done) - matches=${matches% } + [ $4 = 1 ] && matches=${matches% } if [ -n "$matches" ]; then COMPREPLY=( "$matches" ) else @@ -167,6 +167,7 @@ __fzf_generic_path_completion() { shift shift shift + shift _fzf_handle_dynamic_completion "$cmd" "$@" fi } @@ -198,7 +199,7 @@ _fzf_complete() { } _fzf_path_completion() { - __fzf_generic_path_completion _fzf_compgen_path "-m" "" "$@" + __fzf_generic_path_completion _fzf_compgen_path "-m" "" 0 "$@" } # Deprecated. No file only completion. @@ -207,7 +208,7 @@ _fzf_file_completion() { } _fzf_dir_completion() { - __fzf_generic_path_completion _fzf_compgen_dir "" "/" "$@" + __fzf_generic_path_completion _fzf_compgen_dir "" "/" 1 "$@" } _fzf_complete_kill() { @@ -299,7 +300,7 @@ _fzf_defc() { # Anything for cmd in $a_cmds; do - _fzf_defc "$cmd" _fzf_path_completion "-o default -o bashdefault" + _fzf_defc "$cmd" _fzf_path_completion "-o nospace -o default -o bashdefault" done # Directory diff --git a/test/test_go.rb b/test/test_go.rb index fa4915a..fc3b908 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1482,6 +1482,19 @@ module CompletionTest tmux.send_keys :Enter end + def test_completion_cancel + %w[ls cd].each do |cmd| + tmux.prepare + tmux.send_keys "#{cmd} **", :Tab + tmux.until { |lines| lines.item_count > 0 } + tmux.send_keys 'C-c' + tmux.until { |lines| lines[-1].include?('**') } + tmux.send_keys 'xoxo' + tmux.until { |lines| lines[-1].include?('**xoxo') } + tmux.send_keys 'C-u' + end + end + def test_dir_completion (1..100).each do |idx| FileUtils.mkdir_p "/tmp/fzf-test/d#{idx}" From 9d2c6a95f4259e9bccc114f48b3b3d21a357b6d1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 31 Jul 2017 14:08:17 +0900 Subject: [PATCH 559/658] Revert "[bash] Do not append space when path completion is cancelled" This reverts commit 376a76d1d3c39a1686b6ac7a6118c1f80af3178e as it affects normal completion --- shell/completion.bash | 9 ++++----- test/test_go.rb | 13 ------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index eeb5a5d..e8c34e5 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -151,7 +151,7 @@ __fzf_generic_path_completion() { matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $2 -q "$leftover" | while read -r item; do printf "%q$3 " "$item" done) - [ $4 = 1 ] && matches=${matches% } + matches=${matches% } if [ -n "$matches" ]; then COMPREPLY=( "$matches" ) else @@ -167,7 +167,6 @@ __fzf_generic_path_completion() { shift shift shift - shift _fzf_handle_dynamic_completion "$cmd" "$@" fi } @@ -199,7 +198,7 @@ _fzf_complete() { } _fzf_path_completion() { - __fzf_generic_path_completion _fzf_compgen_path "-m" "" 0 "$@" + __fzf_generic_path_completion _fzf_compgen_path "-m" "" "$@" } # Deprecated. No file only completion. @@ -208,7 +207,7 @@ _fzf_file_completion() { } _fzf_dir_completion() { - __fzf_generic_path_completion _fzf_compgen_dir "" "/" 1 "$@" + __fzf_generic_path_completion _fzf_compgen_dir "" "/" "$@" } _fzf_complete_kill() { @@ -300,7 +299,7 @@ _fzf_defc() { # Anything for cmd in $a_cmds; do - _fzf_defc "$cmd" _fzf_path_completion "-o nospace -o default -o bashdefault" + _fzf_defc "$cmd" _fzf_path_completion "-o default -o bashdefault" done # Directory diff --git a/test/test_go.rb b/test/test_go.rb index fc3b908..fa4915a 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1482,19 +1482,6 @@ module CompletionTest tmux.send_keys :Enter end - def test_completion_cancel - %w[ls cd].each do |cmd| - tmux.prepare - tmux.send_keys "#{cmd} **", :Tab - tmux.until { |lines| lines.item_count > 0 } - tmux.send_keys 'C-c' - tmux.until { |lines| lines[-1].include?('**') } - tmux.send_keys 'xoxo' - tmux.until { |lines| lines[-1].include?('**xoxo') } - tmux.send_keys 'C-u' - end - end - def test_dir_completion (1..100).each do |idx| FileUtils.mkdir_p "/tmp/fzf-test/d#{idx}" From f4b46fad27b4d4f7f3f5649b5f2948c298c3a5ae Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 31 Jul 2017 03:21:35 +0900 Subject: [PATCH 560/658] Inline function calls in a tight loop Manually inline function calls in a tight loop as Go compiler does not inline non-leaf functions. It is observed that this unpleasant code change resulted up to 10% performance improvement. --- src/pattern.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index 64296d7..8cb2098 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -301,7 +301,12 @@ 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) { - input := p.prepareInput(item) + var input []Token + if len(p.nth) == 0 { + input = []Token{Token{text: &item.text, prefixLength: 0}} + } else { + input = p.transformInput(item) + } if p.fuzzy { return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab) } @@ -309,7 +314,12 @@ 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) { - input := p.prepareInput(item) + var input []Token + if len(p.nth) == 0 { + input = []Token{Token{text: &item.text, prefixLength: 0}} + } else { + input = p.transformInput(item) + } offsets := []Offset{} var totalScore int var allPos *[]int @@ -353,11 +363,7 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of return offsets, totalScore, allPos } -func (p *Pattern) prepareInput(item *Item) []Token { - if len(p.nth) == 0 { - return []Token{Token{text: &item.text, prefixLength: 0}} - } - +func (p *Pattern) transformInput(item *Item) []Token { if item.transformed != nil { return *item.transformed } From 37370f057f5f39a54316bc7a048ab12b35004b7c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 1 Aug 2017 03:39:57 +0900 Subject: [PATCH 561/658] Do not use defer in performance-sensitive contexts --- src/chunklist.go | 6 ++++-- src/core.go | 2 +- src/util/eventbox.go | 10 +++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/chunklist.go b/src/chunklist.go index f6bedcc..63c6078 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -55,7 +55,6 @@ func CountItems(cs []*Chunk) int { // Push adds the item to the list func (cl *ChunkList) Push(data []byte) bool { cl.mutex.Lock() - defer cl.mutex.Unlock() if len(cl.chunks) == 0 || cl.lastChunk().IsFull() { newChunk := Chunk(make([]Item, 0, chunkSize)) @@ -64,15 +63,16 @@ func (cl *ChunkList) Push(data []byte) bool { if cl.lastChunk().push(cl.trans, data, cl.count) { cl.count++ + cl.mutex.Unlock() return true } + cl.mutex.Unlock() return false } // Snapshot returns immutable snapshot of the ChunkList func (cl *ChunkList) Snapshot() ([]*Chunk, int) { cl.mutex.Lock() - defer cl.mutex.Unlock() ret := make([]*Chunk, len(cl.chunks)) copy(ret, cl.chunks) @@ -82,5 +82,7 @@ func (cl *ChunkList) Snapshot() ([]*Chunk, int) { newChunk := *ret[cnt-1] ret[cnt-1] = &newChunk } + + cl.mutex.Unlock() return ret, cl.count } diff --git a/src/core.go b/src/core.go index 74ff9e7..0b90a51 100644 --- a/src/core.go +++ b/src/core.go @@ -205,7 +205,6 @@ func Run(opts *Options, revision string) { delay := true ticks++ eventBox.Wait(func(events *util.Events) { - defer events.Clear() for evt, value := range *events { switch evt { @@ -265,6 +264,7 @@ func Run(opts *Options, revision string) { } } } + events.Clear() }) if delay && reading { dur := util.DurWithin( diff --git a/src/util/eventbox.go b/src/util/eventbox.go index ccdbb99..b710cf1 100644 --- a/src/util/eventbox.go +++ b/src/util/eventbox.go @@ -26,23 +26,23 @@ func NewEventBox() *EventBox { // Wait blocks the goroutine until signaled func (b *EventBox) Wait(callback func(*Events)) { b.cond.L.Lock() - defer b.cond.L.Unlock() if len(b.events) == 0 { b.cond.Wait() } callback(&b.events) + b.cond.L.Unlock() } // Set turns on the event type on the box func (b *EventBox) Set(event EventType, value interface{}) { b.cond.L.Lock() - defer b.cond.L.Unlock() b.events[event] = value if _, found := b.ignore[event]; !found { b.cond.Broadcast() } + b.cond.L.Unlock() } // Clear clears the events @@ -56,27 +56,27 @@ func (events *Events) Clear() { // Peek peeks at the event box if the given event is set func (b *EventBox) Peek(event EventType) bool { b.cond.L.Lock() - defer b.cond.L.Unlock() _, ok := b.events[event] + b.cond.L.Unlock() return ok } // Watch deletes the events from the ignore list func (b *EventBox) Watch(events ...EventType) { b.cond.L.Lock() - defer b.cond.L.Unlock() for _, event := range events { delete(b.ignore, event) } + b.cond.L.Unlock() } // Unwatch adds the events to the ignore list func (b *EventBox) Unwatch(events ...EventType) { b.cond.L.Lock() - defer b.cond.L.Unlock() for _, event := range events { b.ignore[event] = true } + b.cond.L.Unlock() } // WaitFor blocks the execution until the event is received From 3e28403978a09082aaf3269da59e2c47666c75ba Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 1 Aug 2017 21:34:44 +0900 Subject: [PATCH 562/658] [man] Add note on `--no-` convention Close #1003 --- man/man1/fzf.1 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index fe69843..514a00c 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -111,6 +111,9 @@ Comma-separated list of sort criteria to apply when the scores are tied. .B "-m, --multi" Enable multi-select with tab/shift-tab .TP +.B "+m, --no-multi" +Disable multi-select +.TP .B "--no-mouse" Disable mouse .TP @@ -357,6 +360,9 @@ e.g. \fBfzf --multi | fzf --sync\fR .B "--version" Display version information and exit +.TP +Note that most options have the opposite versions with \fB--no-\fR prefix. + .SH ENVIRONMENT VARIABLES .TP .B FZF_DEFAULT_COMMAND From 99927c70718350dfd4e70e7b408ba1bcbc80d81d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 1 Aug 2017 22:04:42 +0900 Subject: [PATCH 563/658] Modify loop conditions in checkAscii function --- src/util/chars.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/chars.go b/src/util/chars.go index 0127bcb..b06a682 100644 --- a/src/util/chars.go +++ b/src/util/chars.go @@ -24,12 +24,12 @@ type Chars struct { func checkAscii(bytes []byte) (bool, int) { i := 0 - for ; i < len(bytes)-8; i += 8 { + for ; i <= len(bytes)-8; i += 8 { if (overflow64 & *(*uint64)(unsafe.Pointer(&bytes[i]))) > 0 { return false, i } } - for ; i < len(bytes)-4; i += 4 { + for ; i <= len(bytes)-4; i += 4 { if (overflow32 & *(*uint32)(unsafe.Pointer(&bytes[i]))) > 0 { return false, i } From a56489bc7f6b2f0ff8925b69089d7b3fea1ef235 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 2 Aug 2017 00:09:00 +0900 Subject: [PATCH 564/658] Remove non-exclusive access to ChunkList field --- src/chunklist.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/chunklist.go b/src/chunklist.go index 63c6078..4a4458a 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -75,6 +75,7 @@ func (cl *ChunkList) Snapshot() ([]*Chunk, int) { cl.mutex.Lock() ret := make([]*Chunk, len(cl.chunks)) + count := cl.count copy(ret, cl.chunks) // Duplicate the last chunk @@ -84,5 +85,5 @@ func (cl *ChunkList) Snapshot() ([]*Chunk, int) { } cl.mutex.Unlock() - return ret, cl.count + return ret, count } From 39dbc8acdbb41a1010bc38474e0a308cd0fc25e9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 2 Aug 2017 02:50:12 +0900 Subject: [PATCH 565/658] Exit 2 instead of panic when failed to open /dev/tty --- src/tui/light.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tui/light.go b/src/tui/light.go index a7d12f3..f905918 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -32,7 +32,8 @@ var offsetRegexp *regexp.Regexp = regexp.MustCompile("\x1b\\[([0-9]+);([0-9]+)R" func openTtyIn() *os.File { in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) if err != nil { - panic("Failed to open " + consoleDevice) + fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice) + os.Exit(2) } return in } From ecb6b234cc130c760f792f95ce59b413be4cb5c4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 2 Aug 2017 02:44:06 +0900 Subject: [PATCH 566/658] 0.16.11 --- CHANGELOG.md | 5 +++++ install | 2 +- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90357cc..219123f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +0.16.11 +------- +- Performance optimization +- Fixed missing preview update + 0.16.10 ------- - Fixed invalid handling of ANSI colors in preview window diff --git a/install b/install index 79a1888..b7929e4 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.16.10 +version=0.16.11 auto_completion= key_bindings= update_config=2 diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 2c12cdf..3213c2a 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Jul 2017" "fzf 0.16.10" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Aug 2017" "fzf 0.16.11" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 514a00c..2b32d38 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Jul 2017" "fzf 0.16.10" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Aug 2017" "fzf 0.16.11" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index 9b71c52..10df1e7 100644 --- a/src/constants.go +++ b/src/constants.go @@ -9,7 +9,7 @@ import ( const ( // Current version - version = "0.16.10" + version = "0.16.11" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 4a4fef2daf1541b4f98c944058bdc325cfbc6c23 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 4 Aug 2017 09:28:29 +0900 Subject: [PATCH 567/658] Update performance comparison chart --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3160644..43ffa71 100644 --- a/README.md +++ b/README.md @@ -411,7 +411,7 @@ options that affect the performance. algorithm. However, this algorithm is not guaranteed to find the optimal ordering of the matches and is not recommended. -[perf]: https://junegunn.kr/images/fzf-0.16.10.png +[perf]: https://junegunn.kr/images/fzf-0.16.11.png ### Executing external programs From e5e75efebcb45a24e78b362143edb768e64f0419 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 4 Aug 2017 19:25:06 +0900 Subject: [PATCH 568/658] [vim] Fix vader test cases --- test/fzf.vader | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/fzf.vader b/test/fzf.vader index c2d8d53..a1a6a2e 100644 --- a/test/fzf.vader +++ b/test/fzf.vader @@ -6,11 +6,11 @@ Execute (Setup): Execute (fzf#run with dir option): let cwd = getcwd() - let result = fzf#run({ 'options': '--filter=vdr', 'dir': g:dir }) + let result = fzf#run({ 'source': 'git ls-files', 'options': '--filter=vdr', 'dir': g:dir }) AssertEqual ['fzf.vader'], result AssertEqual getcwd(), cwd - let result = sort(fzf#run({ 'options': '--filter e', 'dir': g:dir })) + let result = sort(fzf#run({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir })) AssertEqual ['fzf.vader', 'test_go.rb'], result AssertEqual getcwd(), cwd @@ -19,7 +19,7 @@ Execute (fzf#run with Funcref command): function! g:FzfTest(e) call add(g:ret, a:e) endfunction - let result = sort(fzf#run({ 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir })) + let result = sort(fzf#run({ 'source': 'git ls-files', 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir })) AssertEqual ['fzf.vader', 'test_go.rb'], result AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret) @@ -140,7 +140,7 @@ Execute (fzf#wrap): let g:fzf_history_dir = '/tmp' let opts = fzf#wrap('foobar', {'options': '--color light'}) Log opts - Assert opts.options =~ '--history /tmp/foobar' + Assert opts.options =~ "--history '/tmp/foobar'" Assert opts.options =~ '--color light' let g:fzf_colors = { 'fg': ['fg', 'Error'] } From 2b98fee13602a90bcfaf76a0d75388b76692a95f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 4 Aug 2017 23:29:02 +0900 Subject: [PATCH 569/658] Fix Travis CI build tcell build is commented out as it doesn't reliably respond to tmux send-keys. --- .travis.yml | 7 +- test/test_go.rb | 477 +++++++++++++++++++++++++++--------------------- 2 files changed, 274 insertions(+), 210 deletions(-) diff --git a/.travis.yml b/.travis.yml index 915e871..21ac3f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,16 +4,13 @@ matrix: - env: TAGS= rvm: 2.3.3 # - env: TAGS=tcell -# rvm: 2.2.0 +# rvm: 2.3.3 install: -- sudo apt-get update -- sudo apt-get install -y libncurses-dev lib32ncurses5-dev libgpm-dev - sudo add-apt-repository -y ppa:pi-rho/dev - sudo apt-add-repository -y ppa:fish-shell/release-2 - sudo apt-get update -- sudo apt-get install -y tmux=1.9a-1~ppa1~p -- sudo apt-get install -y zsh fish +- sudo apt-get install -y tmux zsh fish script: | make test install && diff --git a/test/test_go.rb b/test/test_go.rb index fa4915a..b75b150 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1,8 +1,14 @@ #!/usr/bin/env ruby # encoding: utf-8 +# frozen_string_literal: true + +# rubocop:disable Metrics/LineLength +# rubocop:disable Metrics/MethodLength require 'minitest/autorun' require 'fileutils' +require 'English' +require 'shellwords' DEFAULT_TIMEOUT = 20 @@ -12,15 +18,15 @@ Dir.chdir base FZF = "FZF_DEFAULT_OPTS= FZF_DEFAULT_COMMAND= #{base}/bin/fzf" class NilClass - def include? str + def include?(_str) false end - def start_with? str + def start_with?(_str) false end - def end_with? str + def end_with?(_str) false end end @@ -61,7 +67,7 @@ class Tmux attr_reader :win - def initialize shell = :bash + def initialize(shell = :bash) @win = case shell when :bash @@ -76,17 +82,16 @@ class Tmux go("set-window-option -t #{@win} pane-base-index 0") @lines = `tput lines`.chomp.to_i - if shell == :fish - send_keys('function fish_prompt; end; clear', :Enter) - self.until { |lines| lines.empty? } - end + return unless shell == :fish + send_keys('function fish_prompt; end; clear', :Enter) + self.until(&:empty?) end def kill go("kill-window -t #{win} 2> /dev/null") end - def send_keys *args + def send_keys(*args) target = if args.last.is_a?(Hash) hash = args.pop @@ -95,32 +100,42 @@ class Tmux else win end - args = args.map { |a| %{"#{a}"} }.join ' ' - go("send-keys -t #{target} #{args}") + enum = (args + [nil]).each_cons(2) + loop do + pair = enum.next + if pair.first == :Escape + arg = pair.compact.map { |key| %("#{key}") }.join(' ') + go(%(send-keys -t #{target} #{arg})) + enum.next if pair.last + else + go(%(send-keys -t #{target} "#{pair.first}")) + end + break unless pair.last + end end - def paste str - %x[tmux setb '#{str.gsub("'", "'\\''")}' \\; pasteb -t #{win} \\; send-keys -t #{win} Enter] + def paste(str) + `tmux setb '#{str.gsub("'", "'\\''")}' \\; pasteb -t #{win} \\; send-keys -t #{win} Enter` end - def capture pane = 0 - File.unlink TEMPNAME while File.exists? TEMPNAME + def capture(pane = 0) + File.unlink TEMPNAME while File.exist? TEMPNAME wait do go("capture-pane -t #{win}.#{pane} \\; save-buffer #{TEMPNAME} 2> /dev/null") - $?.exitstatus == 0 + $CHILD_STATUS.exitstatus.zero? end - File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse + File.read(TEMPNAME).split($INPUT_RECORD_SEPARATOR)[0, @lines].reverse.drop_while(&:empty?).reverse end - def until refresh = false, pane = 0 + def until(refresh = false, pane = 0) lines = nil begin wait do lines = capture(pane) class << lines def counts - self.lazy - .map { |l| l.scan /^. ([0-9]+)\/([0-9]+)( \(([0-9]+)\))?/ } + lazy + .map { |l| l.scan %r{^. ([0-9]+)\/([0-9]+)( \(([0-9]+)\))?} } .reject(&:empty?) .first&.first&.map(&:to_i)&.values_at(0, 1, 3) || [0, 0, 0] end @@ -137,17 +152,17 @@ class Tmux counts[2] end - def any_include? val + def any_include?(val) method = val.is_a?(Regexp) ? :match : :include? - self.select { |line| line.send method, val }.first + select { |line| line.send method, val }.first end end yield(lines).tap do |ok| send_keys 'C-l' if refresh && !ok end end - rescue Exception - puts $!.backtrace + rescue StandardError + puts $ERROR_INFO.backtrace puts '>' * 80 puts lines puts '<' * 80 @@ -160,17 +175,19 @@ class Tmux tries = 0 begin self.until do |lines| - self.send_keys 'C-u', 'hello' + send_keys 'C-u', 'hello' lines[-1].end_with?('hello') end - rescue Exception + rescue StandardError (tries += 1) < 5 ? retry : raise end - self.send_keys 'C-u' + send_keys 'C-u' end -private - def go *args - %x[tmux #{args.join ' '}].split($/) + + private + + def go(*args) + `tmux #{args.join ' '}`.split($INPUT_RECORD_SEPARATOR) end end @@ -186,16 +203,16 @@ class TestBase < Minitest::Test @temp_suffix].join '-' end - def writelines path, lines - File.unlink path while File.exists? path - File.open(path, 'w') { |f| f << lines.join($/) + $/ } + def writelines(path, lines) + File.unlink path while File.exist? path + File.open(path, 'w') { |f| f << lines.join($INPUT_RECORD_SEPARATOR) + $INPUT_RECORD_SEPARATOR } end def readonce - wait { File.exists?(tempname) } + wait { File.exist?(tempname) } File.read(tempname) ensure - File.unlink tempname while File.exists?(tempname) + File.unlink tempname while File.exist?(tempname) @temp_suffix += 1 tmux.prepare end @@ -205,17 +222,15 @@ class TestBase < Minitest::Test end def fzf!(*opts) - opts = opts.map { |o| + opts = opts.map do |o| case o when Symbol o = o.to_s - o.length > 1 ? "--#{o.gsub('_', '-')}" : "-#{o}" + o.length > 1 ? "--#{o.tr('_', '-')}" : "-#{o}" when String, Numeric o.to_s - else - nil end - }.compact + end.compact "#{FZF} #{opts.join ' '}" end end @@ -271,19 +286,19 @@ class TestGoFZF < TestBase tmux.until { |lines| lines.last =~ /^>/ } # CTRL-A - tmux.send_keys "C-A", "(" + tmux.send_keys 'C-A', '(' tmux.until { |lines| lines.last == '> (foo bar foo-bar' } # META-F - tmux.send_keys :Escape, :f, ")" + tmux.send_keys :Escape, :f, ')' tmux.until { |lines| lines.last == '> (foo) bar foo-bar' } # CTRL-B - tmux.send_keys "C-B", "var" + tmux.send_keys 'C-B', 'var' tmux.until { |lines| lines.last == '> (foovar) bar foo-bar' } # Left, CTRL-D - tmux.send_keys :Left, :Left, "C-D" + tmux.send_keys :Left, :Left, 'C-D' tmux.until { |lines| lines.last == '> (foovr) bar foo-bar' } # META-BS @@ -291,7 +306,7 @@ class TestGoFZF < TestBase tmux.until { |lines| lines.last == '> (r) bar foo-bar' } # CTRL-Y - tmux.send_keys "C-Y", "C-Y" + tmux.send_keys 'C-Y', 'C-Y' tmux.until { |lines| lines.last == '> (foovfoovr) bar foo-bar' } # META-B @@ -307,27 +322,27 @@ class TestGoFZF < TestBase tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-bar' } # CTRL-E - tmux.send_keys "C-E", 'baz' + tmux.send_keys 'C-E', 'baz' tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-barbaz' } # CTRL-U - tmux.send_keys "C-U" + tmux.send_keys 'C-U' tmux.until { |lines| lines.last == '>' } # CTRL-Y - tmux.send_keys "C-Y" + tmux.send_keys 'C-Y' tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-barbaz' } # CTRL-W - tmux.send_keys "C-W", "bar-foo" + tmux.send_keys 'C-W', 'bar-foo' tmux.until { |lines| lines.last == '> ( fovfoovr) bar bar-foo' } # META-D - tmux.send_keys :Escape, :b, :Escape, :b, :Escape, :d, "C-A", "C-Y" + tmux.send_keys :Escape, :b, :Escape, :b, :Escape, :d, 'C-A', 'C-Y' tmux.until { |lines| lines.last == '> bar( fovfoovr) bar -foo' } # CTRL-M - tmux.send_keys "C-M" + tmux.send_keys 'C-M' tmux.until { |lines| lines.last !~ /^>/ } end @@ -352,8 +367,8 @@ class TestGoFZF < TestBase 'C-K', 'C-K', 'C-K', 'C-K', :BTab, :BTab, # 5, 6 :PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7 tmux.until { |lines| lines[-2].include? '(6)' } - tmux.send_keys "C-M" - assert_equal %w[3 2 5 6 8 7], readonce.split($/) + tmux.send_keys 'C-M' + assert_equal %w[3 2 5 6 8 7], readonce.split($INPUT_RECORD_SEPARATOR) end def test_with_nth @@ -361,7 +376,7 @@ class TestGoFZF < TestBase tmux.send_keys "(echo ' 1st 2nd 3rd/'; echo ' first second third/') | #{fzf multi && :multi, :x, :nth, 2, :with_nth, '2,-1,1'}", - :Enter + :Enter tmux.until { |lines| lines[-2].include?('2/2') } # Transformed list @@ -374,12 +389,12 @@ class TestGoFZF < TestBase 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($INPUT_RECORD_SEPARATOR) else tmux.send_keys '^', '3' tmux.until { |lines| lines[-2].include?('1/2') } tmux.send_keys :Enter - assert_equal [' 1st 2nd 3rd/'], readonce.split($/) + assert_equal [' 1st 2nd 3rd/'], readonce.split($INPUT_RECORD_SEPARATOR) end end end @@ -388,7 +403,7 @@ class TestGoFZF < TestBase [true, false].each do |rev| tmux.send_keys "seq 1 100 | #{fzf rev && :reverse}", :Enter tmux.until { |lines| lines.include? ' 100/100' } - tmux.send_keys *110.times.map { rev ? :Down : :Up } + tmux.send_keys(*Array.new(110) { rev ? :Down : :Up }) tmux.until { |lines| lines.include? '> 100' } tmux.send_keys :Enter assert_equal '100', readonce.chomp @@ -397,22 +412,22 @@ class TestGoFZF < TestBase def test_select_1 tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 5555, :'1'}", :Enter - assert_equal ['5555', '55'], readonce.split($/) + assert_equal %w[5555 55], readonce.split($INPUT_RECORD_SEPARATOR) end def test_exit_0 - tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 555555, :'0'}", :Enter - assert_equal ['555555'], readonce.split($/) + tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 555_555, :'0'}", :Enter + assert_equal ['555555'], readonce.split($INPUT_RECORD_SEPARATOR) end def test_select_1_exit_0_fail - [:'0', :'1', [:'1', :'0']].each do |opt| + [:'0', :'1', %i[1 0]].each do |opt| tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter tmux.until { |lines| lines.last =~ /^> 5/ } tmux.send_keys :BTab, :BTab, :BTab tmux.until { |lines| lines[-2].include?('(3)') } tmux.send_keys :Enter - assert_equal ['5', '5', '50', '51'], readonce.split($/) + assert_equal %w[5 5 50 51], readonce.split($INPUT_RECORD_SEPARATOR) end end @@ -420,7 +435,7 @@ class TestGoFZF < TestBase tmux.paste "(echo abc; echo 가나다) | #{fzf :query, '가다'}" tmux.until { |lines| lines[-2].include? '1/2' } tmux.send_keys :Enter - assert_equal ['가나다'], readonce.split($/) + assert_equal ['가나다'], readonce.split($INPUT_RECORD_SEPARATOR) end def test_sync @@ -433,7 +448,7 @@ class TestGoFZF < TestBase tmux.send_keys :Enter tmux.until { |lines| lines[-1] == '>' } tmux.send_keys 'C-K', :Enter - assert_equal ['9090'], readonce.split($/) + assert_equal ['9090'], readonce.split($INPUT_RECORD_SEPARATOR) end def test_tac @@ -442,7 +457,7 @@ class TestGoFZF < TestBase 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($INPUT_RECORD_SEPARATOR) end def test_tac_sort @@ -453,7 +468,7 @@ class TestGoFZF < TestBase 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($INPUT_RECORD_SEPARATOR) end def test_tac_nosort @@ -464,7 +479,7 @@ class TestGoFZF < TestBase 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($INPUT_RECORD_SEPARATOR) end def test_expect @@ -473,19 +488,19 @@ class TestGoFZF < TestBase tmux.until { |lines| lines[-2].include? '100/100' } tmux.send_keys '55' tmux.until { |lines| lines[-2].include? '1/100' } - tmux.send_keys *feed + tmux.send_keys(*feed) tmux.prepare - assert_equal [expected, '55'], readonce.split($/) + assert_equal [expected, '55'], readonce.split($INPUT_RECORD_SEPARATOR) end test.call 'ctrl-t', 'C-T' test.call 'ctrl-t', 'Enter', '' - test.call 'alt-c', [:Escape, :c] + test.call 'alt-c', %i[Escape c] test.call 'f1', 'f1' test.call 'f2', 'f2' test.call 'f3', 'f3' test.call 'f2,f4', 'f2', 'f2' test.call 'f2,f4', 'f4', 'f4' - test.call 'alt-/', [:Escape, :/] + test.call 'alt-/', %i[Escape /] %w[f5 f6 f7 f8 f9 f10].each do |key| test.call 'f5,f6,f7,f8,f9,f10', key, key end @@ -498,7 +513,7 @@ class TestGoFZF < TestBase tmux.send_keys '55' tmux.until { |lines| lines[-2].include? '1/100' } tmux.send_keys :Escape, :z - assert_equal ['55', 'alt-z', '55'], readonce.split($/) + assert_equal ['55', 'alt-z', '55'], readonce.split($INPUT_RECORD_SEPARATOR) end def test_expect_printable_character_print_query @@ -507,12 +522,12 @@ class TestGoFZF < TestBase tmux.send_keys '55' tmux.until { |lines| lines[-2].include? '1/100' } tmux.send_keys 'z' - assert_equal ['55', 'z', '55'], readonce.split($/) + assert_equal %w[55 z 55], readonce.split($INPUT_RECORD_SEPARATOR) end def test_expect_print_query_select_1 tmux.send_keys "seq 1 100 | #{fzf '-q55 -1 --expect=alt-z --print-query'}", :Enter - assert_equal ['55', '', '55'], readonce.split($/) + assert_equal ['55', '', '55'], readonce.split($INPUT_RECORD_SEPARATOR) end def test_toggle_sort @@ -526,14 +541,14 @@ class TestGoFZF < TestBase tmux.send_keys :Tab tmux.until { |lines| lines[-2].include? '4/111 +S (2)' } tmux.send_keys :Enter - assert_equal ['111', '11'], readonce.split($/) + assert_equal %w[111 11], readonce.split($INPUT_RECORD_SEPARATOR) end end def test_unicode_case writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4] - assert_equal %w[СТРОКА2 Строка4], `#{FZF} -fС < #{tempname}`.split($/) - assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `#{FZF} -fс < #{tempname}`.split($/) + assert_equal %w[СТРОКА2 Строка4], `#{FZF} -fС < #{tempname}`.split($INPUT_RECORD_SEPARATOR) + assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `#{FZF} -fс < #{tempname}`.split($INPUT_RECORD_SEPARATOR) end def test_tiebreak @@ -545,7 +560,7 @@ class TestGoFZF < TestBase ] writelines tempname, input - assert_equal input, `#{FZF} -ffoobar --tiebreak=index < #{tempname}`.split($/) + assert_equal input, `#{FZF} -ffoobar --tiebreak=index < #{tempname}`.split($INPUT_RECORD_SEPARATOR) by_length = %w[ ----foobar-- @@ -553,8 +568,8 @@ class TestGoFZF < TestBase -------foobar- --foobar-------- ] - assert_equal by_length, `#{FZF} -ffoobar < #{tempname}`.split($/) - assert_equal by_length, `#{FZF} -ffoobar --tiebreak=length < #{tempname}`.split($/) + assert_equal by_length, `#{FZF} -ffoobar < #{tempname}`.split($INPUT_RECORD_SEPARATOR) + assert_equal by_length, `#{FZF} -ffoobar --tiebreak=length < #{tempname}`.split($INPUT_RECORD_SEPARATOR) by_begin = %w[ --foobar-------- @@ -562,17 +577,17 @@ class TestGoFZF < TestBase -----foobar--- -------foobar- ] - assert_equal by_begin, `#{FZF} -ffoobar --tiebreak=begin < #{tempname}`.split($/) - assert_equal by_begin, `#{FZF} -f"!z foobar" -x --tiebreak begin < #{tempname}`.split($/) + assert_equal by_begin, `#{FZF} -ffoobar --tiebreak=begin < #{tempname}`.split($INPUT_RECORD_SEPARATOR) + assert_equal by_begin, `#{FZF} -f"!z foobar" -x --tiebreak begin < #{tempname}`.split($INPUT_RECORD_SEPARATOR) assert_equal %w[ -------foobar- ----foobar-- -----foobar--- --foobar-------- - ], `#{FZF} -ffoobar --tiebreak end < #{tempname}`.split($/) + ], `#{FZF} -ffoobar --tiebreak end < #{tempname}`.split($INPUT_RECORD_SEPARATOR) - assert_equal input, `#{FZF} -f"!z" -x --tiebreak end < #{tempname}`.split($/) + assert_equal input, `#{FZF} -f"!z" -x --tiebreak end < #{tempname}`.split($INPUT_RECORD_SEPARATOR) end def test_tiebreak_index_begin @@ -582,7 +597,7 @@ class TestGoFZF < TestBase 'xxoxxxoxx', 'xxxoxoxxx', 'xxxxoxox', - ' xxoxoxxx', + ' xxoxoxxx' ] assert_equal [ @@ -591,8 +606,8 @@ class TestGoFZF < TestBase 'xxxoxoxxx', 'xxoxxxoxx', 'xoxxxxxox', - 'xoxxxxxoxx', - ], `#{FZF} -foo < #{tempname}`.split($/) + 'xoxxxxxoxx' + ], `#{FZF} -foo < #{tempname}`.split($INPUT_RECORD_SEPARATOR) assert_equal [ 'xxxoxoxxx', @@ -600,8 +615,8 @@ class TestGoFZF < TestBase ' xxoxoxxx', 'xxoxxxoxx', 'xoxxxxxoxx', - 'xoxxxxxox', - ], `#{FZF} -foo --tiebreak=index < #{tempname}`.split($/) + 'xoxxxxxox' + ], `#{FZF} -foo --tiebreak=index < #{tempname}`.split($INPUT_RECORD_SEPARATOR) # Note that --tiebreak=begin is now based on the first occurrence of the # first character on the pattern @@ -611,8 +626,8 @@ class TestGoFZF < TestBase 'xxxxoxox', 'xxoxxxoxx', 'xoxxxxxoxx', - 'xoxxxxxox', - ], `#{FZF} -foo --tiebreak=begin < #{tempname}`.split($/) + 'xoxxxxxox' + ], `#{FZF} -foo --tiebreak=begin < #{tempname}`.split($INPUT_RECORD_SEPARATOR) assert_equal [ ' xxoxoxxx', @@ -620,19 +635,19 @@ class TestGoFZF < TestBase 'xxxxoxox', 'xxoxxxoxx', 'xoxxxxxox', - 'xoxxxxxoxx', - ], `#{FZF} -foo --tiebreak=begin,length < #{tempname}`.split($/) + 'xoxxxxxoxx' + ], `#{FZF} -foo --tiebreak=begin,length < #{tempname}`.split($INPUT_RECORD_SEPARATOR) end def test_tiebreak_begin_algo_v2 writelines tempname, [ 'baz foo bar', - 'foo bar baz', + 'foo bar baz' ] assert_equal [ 'foo bar baz', - 'baz foo bar', - ], `#{FZF} -fbar --tiebreak=begin --algo=v2 < #{tempname}`.split($/) + 'baz foo bar' + ], `#{FZF} -fbar --tiebreak=begin --algo=v2 < #{tempname}`.split($INPUT_RECORD_SEPARATOR) end def test_tiebreak_end @@ -642,7 +657,7 @@ class TestGoFZF < TestBase 'xxxoxxxxxx', 'xxxxoxxxx', 'xxxxxoxxx', - ' xxxxoxxx', + ' xxxxoxxx' ] assert_equal [ @@ -651,8 +666,8 @@ class TestGoFZF < TestBase 'xxxxxoxxx', 'xoxxxxxxxx', 'xxoxxxxxxx', - 'xxxoxxxxxx', - ], `#{FZF} -fo < #{tempname}`.split($/) + 'xxxoxxxxxx' + ], `#{FZF} -fo < #{tempname}`.split($INPUT_RECORD_SEPARATOR) assert_equal [ 'xxxxxoxxx', @@ -660,8 +675,8 @@ class TestGoFZF < TestBase 'xxxxoxxxx', 'xxxoxxxxxx', 'xxoxxxxxxx', - 'xoxxxxxxxx', - ], `#{FZF} -fo --tiebreak=end < #{tempname}`.split($/) + 'xoxxxxxxxx' + ], `#{FZF} -fo --tiebreak=end < #{tempname}`.split($INPUT_RECORD_SEPARATOR) assert_equal [ 'xxxxxoxxx', @@ -669,8 +684,8 @@ class TestGoFZF < TestBase 'xxxxoxxxx', 'xxxoxxxxxx', 'xxoxxxxxxx', - 'xoxxxxxxxx', - ], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.split($/) + 'xoxxxxxxxx' + ], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.split($INPUT_RECORD_SEPARATOR) end def test_tiebreak_length_with_nth @@ -688,10 +703,10 @@ class TestGoFZF < TestBase 123:hello 1234567:h ] - assert_equal output, `#{FZF} -fh < #{tempname}`.split($/) + assert_equal output, `#{FZF} -fh < #{tempname}`.split($INPUT_RECORD_SEPARATOR) # Since 0.16.8, --nth doesn't affect --tiebreak - assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.split($/) + assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.split($INPUT_RECORD_SEPARATOR) end def test_invalid_cache @@ -709,18 +724,17 @@ class TestGoFZF < TestBase end def test_bind - tmux.send_keys "seq 1 1000 | #{ - fzf '-m --bind=ctrl-j:accept,u:up,T:toggle-up,t:toggle'}", :Enter + tmux.send_keys "seq 1 1000 | #{fzf '-m --bind=ctrl-j:accept,u:up,T:toggle-up,t:toggle'}", :Enter tmux.until { |lines| lines[-2].end_with? '/1000' } tmux.send_keys 'uuu', 'TTT', 'tt', 'uu', 'ttt', 'C-j' - assert_equal %w[4 5 6 9], readonce.split($/) + assert_equal %w[4 5 6 9], readonce.split($INPUT_RECORD_SEPARATOR) end def test_bind_print_query tmux.send_keys "seq 1 1000 | #{fzf '-m --bind=ctrl-j:print-query'}", :Enter tmux.until { |lines| lines[-2].end_with? '/1000' } tmux.send_keys 'print-my-query', 'C-j' - assert_equal %w[print-my-query], readonce.split($/) + assert_equal %w[print-my-query], readonce.split($INPUT_RECORD_SEPARATOR) end def test_long_line @@ -732,9 +746,12 @@ class TestGoFZF < TestBase end def test_read0 - lines = `find .`.split($/) + lines = `find .`.split($INPUT_RECORD_SEPARATOR) assert_equal lines.last, `find . | #{FZF} -e -f "^#{lines.last}$"`.chomp - assert_equal lines.last, `find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp + assert_equal( + lines.last, + `find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp + ) end def test_select_all_deselect_all_toggle_all @@ -757,16 +774,21 @@ class TestGoFZF < TestBase tmux.send_keys 'C-a' tmux.until { |lines| lines[-2].include? '(12)' } tmux.send_keys :Enter - assert_equal %w[2 1 10 20 30 40 50 60 70 80 90 100], readonce.split($/) + assert_equal %w[2 1 10 20 30 40 50 60 70 80 90 100], + readonce.split($INPUT_RECORD_SEPARATOR) end def test_history history_file = '/tmp/fzf-test-history' # History with limited number of entries - File.unlink history_file rescue nil + begin + File.unlink history_file + rescue + nil + end opts = "--history=#{history_file} --history-size=4" - input = %w[00 11 22 33 44].map { |e| e + $/ } + input = %w[00 11 22 33 44].map { |e| e + $INPUT_RECORD_SEPARATOR } input.each do |keys| tmux.send_keys "seq 100 | #{fzf opts}", :Enter tmux.until { |lines| lines[-2].include? '100/100' } @@ -796,7 +818,7 @@ class TestGoFZF < TestBase tmux.until { |lines| lines[-1].end_with? '> 310' } tmux.send_keys :Enter readonce - assert_equal %w[22 33 44 310].map { |e| e + $/ }, File.readlines(history_file) + assert_equal %w[22 33 44 310].map { |e| e + $INPUT_RECORD_SEPARATOR }, File.readlines(history_file) # Respect --bind option tmux.send_keys "seq 100 | #{fzf opts + ' --bind ctrl-p:next-history,ctrl-n:previous-history'}", :Enter @@ -811,7 +833,7 @@ class TestGoFZF < TestBase def test_execute output = '/tmp/fzf-test-execute' opts = %[--bind \\"alt-a:execute(echo [{}] >> #{output}),alt-b:execute[echo /{}{}/ >> #{output}],C:execute:echo /{}{}{}/ >> #{output}\\"] - wait = lambda { |exp| tmux.until { |lines| lines[-2].include? exp } } + wait = ->(exp) { tmux.until { |lines| lines[-2].include? exp } } writelines tempname, %w[foo'bar foo"bar foo$bar] tmux.send_keys "cat #{tempname} | #{fzf opts}; sync", :Enter wait['3/3'] @@ -838,9 +860,13 @@ class TestGoFZF < TestBase assert_equal %w[[foo'bar] [foo'bar] /foo"barfoo"bar/ /foo"barfoo"bar/ /foo$barfoo$barfoo$bar/], - File.readlines(output).map(&:chomp) + File.readlines(output).map(&:chomp) ensure - File.unlink output rescue nil + begin + File.unlink output + rescue + nil + end end def test_execute_multi @@ -860,20 +886,28 @@ class TestGoFZF < TestBase tmux.send_keys :Enter tmux.prepare readonce - assert_equal [%[foo'bar/foo'bar], - %[foo'bar foo"bar foo$bar/foo'bar foo"bar foo$bar], - %[foo'bar foo"bar foobar/foo'bar foo"bar foobar]], - File.readlines(output).map(&:chomp) + assert_equal [%(foo'bar/foo'bar), + %(foo'bar foo"bar foo$bar/foo'bar foo"bar foo$bar), + %(foo'bar foo"bar foobar/foo'bar foo"bar foobar)], + File.readlines(output).map(&:chomp) ensure - File.unlink output rescue nil + begin + File.unlink output + rescue + nil + end end def test_execute_plus_flag - output = tempname + ".tmp" - File.unlink output rescue nil - writelines tempname, ["foo bar", "123 456"] + output = tempname + '.tmp' + begin + File.unlink output + rescue + nil + end + writelines tempname, ['foo bar', '123 456'] - tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter + tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter execute = lambda do tmux.send_keys 'x', 'y' @@ -897,19 +931,28 @@ class TestGoFZF < TestBase readonce assert_equal [ - %[foo bar/foo bar/bar/bar], - %[123 456/foo bar/456/bar], - %[123 456 foo bar/foo bar/456 bar/bar] + %(foo bar/foo bar/bar/bar), + %(123 456/foo bar/456/bar), + %(123 456 foo bar/foo bar/456 bar/bar) ], File.readlines(output).map(&:chomp) rescue - File.unlink output rescue nil + begin + File.unlink output + rescue + nil + end end def test_execute_shell # Custom script to use as $SHELL output = tempname + '.out' - File.unlink output rescue nil - writelines tempname, ['#!/usr/bin/env bash', "echo $1 / $2 > #{output}", "sync"] + begin + File.unlink output + rescue + nil + end + writelines tempname, + ['#!/usr/bin/env bash', "echo $1 / $2 > #{output}", 'sync'] system "chmod +x #{tempname}" tmux.send_keys "echo foo | SHELL=#{tempname} fzf --bind 'enter:execute:{}bar'", :Enter @@ -920,7 +963,11 @@ class TestGoFZF < TestBase tmux.prepare assert_equal ["-c / 'foo'bar"], File.readlines(output).map(&:chomp) ensure - File.unlink output rescue nil + begin + File.unlink output + rescue + nil + end end def test_cycle @@ -947,9 +994,9 @@ class TestGoFZF < TestBase 2.times do tmux.until do |lines| lines[-2].include?('/90') && - lines[-3] == ' 1' && - lines[-4] == ' 2' && - lines[-13] == '> 50' + lines[-3] == ' 1' && + lines[-4] == ' 2' && + lines[-13] == '> 50' end tmux.send_keys :Down end @@ -962,9 +1009,9 @@ class TestGoFZF < TestBase 2.times do tmux.until do |lines| lines[1].include?('/90') && - lines[2] == ' 1' && - lines[3] == ' 2' && - lines[12] == '> 50' + lines[2] == ' 1' && + lines[3] == ' 2' && + lines[12] == '> 50' end tmux.send_keys :Up end @@ -976,19 +1023,19 @@ class TestGoFZF < TestBase tmux.send_keys "seq 100 | #{fzf '--header-lines=200'}", :Enter tmux.until do |lines| lines[-2].include?('0/0') && - lines[-3].include?(' 1') + 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.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' + lines[-3] == ' 11111' && + lines[-7] == ' 55555' && + lines[-8] == '> 66666' end tmux.send_keys :Enter assert_equal '6', readonce.chomp @@ -999,7 +1046,7 @@ class TestGoFZF < TestBase header = File.readlines(FILE).take(5).map(&:strip) tmux.until do |lines| lines[-2].include?('100/100') && - lines[-7..-3].map(&:strip) == header + lines[-7..-3].map(&:strip) == header end end @@ -1008,7 +1055,7 @@ class TestGoFZF < TestBase header = File.readlines(FILE).take(5).map(&:strip) tmux.until do |lines| lines[1].include?('100/100') && - lines[2..6].map(&:strip) == header + lines[2..6].map(&:strip) == header end end @@ -1017,8 +1064,8 @@ class TestGoFZF < TestBase header = File.readlines(FILE).take(5).map(&:strip) tmux.until do |lines| lines[-2].include?('90/90') && - lines[-7...-2].map(&:strip) == header && - lines[-17...-7].map(&:strip) == (1..10).map(&:to_s).reverse + lines[-7...-2].map(&:strip) == header && + lines[-17...-7].map(&:strip) == (1..10).map(&:to_s).reverse end end @@ -1027,13 +1074,13 @@ class TestGoFZF < TestBase header = File.readlines(FILE).take(5).map(&:strip) tmux.until do |lines| lines[1].include?('90/90') && - lines[2...7].map(&:strip) == header && - lines[7...17].map(&:strip) == (1..10).map(&:to_s) + lines[2...7].map(&:strip) == header && + lines[7...17].map(&:strip) == (1..10).map(&:to_s) end end def test_cancel - tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter + 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') } @@ -1046,13 +1093,13 @@ class TestGoFZF < TestBase end def test_margin - tmux.send_keys "yes | head -1000 | #{fzf "--margin 5,3"}", :Enter + 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.send_keys "seq 1000 | #{fzf '--margin 7,5 --reverse'}", :Enter tmux.until { |lines| lines[1 + 7] == ' 1000/1000' } tmux.send_keys :Enter end @@ -1068,50 +1115,61 @@ class TestGoFZF < TestBase 6 => '> f oo ba r baz barfooq ux', 7 => '> f oo ba r baz barfooq ux', 8 => '> f oo ba r baz barfooq ux', - 9 => '> f oo ba r baz barfooq ux', + 9 => '> f oo ba r baz barfooq ux' }.each do |ts, exp| tmux.prepare - tmux.send_keys %[cat #{tempname} | fzf --tabstop=#{ts}], :Enter - tmux.until(true) { |lines| exp.start_with? lines[-3].to_s.strip.sub(/\.\.$/, '') } + tmux.send_keys %(cat #{tempname} | fzf --tabstop=#{ts}), :Enter + tmux.until(true) do |lines| + exp.start_with? lines[-3].to_s.strip.sub(/\.\.$/, '') + end tmux.send_keys :Enter end end - def test_with_nth + def test_with_nth_basic writelines tempname, ['hello world ', 'byebye'] - assert_equal 'hello world ', `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp + assert_equal( + 'hello world ', + `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp + ) end def test_with_nth_ansi writelines tempname, ["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'] - assert_equal 'hello world ', `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi < #{tempname}`.chomp + assert_equal( + 'hello world ', + `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi < #{tempname}`.chomp + ) end def test_with_nth_no_ansi src = "\x1b[33mhello \x1b[34;1mworld\x1b[m " writelines tempname, [src, 'byebye'] - assert_equal src, `#{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi < #{tempname}`.chomp + assert_equal( + src, + `#{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi < #{tempname}`.chomp + ) end def test_exit_0_exit_code `echo foo | #{FZF} -q bar -0` - assert_equal 1, $?.exitstatus + assert_equal 1, $CHILD_STATUS.exitstatus end def test_invalid_option lines = `#{FZF} --foobar 2>&1` - assert_equal 2, $?.exitstatus + assert_equal 2, $CHILD_STATUS.exitstatus assert lines.include?('unknown option: --foobar'), lines end def test_filter_exitstatus # filter / streaming filter - ["", "--no-sort"].each do |opts| + ['', '--no-sort'].each do |opts| assert `echo foo | #{FZF} -f foo #{opts}`.include?('foo') - assert_equal 0, $?.exitstatus + assert_equal 0, $CHILD_STATUS.exitstatus assert `echo foo | #{FZF} -f bar #{opts}`.empty? - assert_equal 1, $?.exitstatus + assert_equal 1, $CHILD_STATUS.exitstatus end end @@ -1138,11 +1196,11 @@ class TestGoFZF < TestBase def test_or_operator assert_equal %w[1 5 10], `seq 10 | #{FZF} -f "1 | 5"`.lines.map(&:chomp) assert_equal %w[1 10 2 3 4 5 6 7 8 9], - `seq 10 | #{FZF} -f '1 | !1'`.lines.map(&:chomp) + `seq 10 | #{FZF} -f '1 | !1'`.lines.map(&:chomp) end def test_hscroll_off - writelines tempname, ['=' * 10000 + '0123456789'] + writelines tempname, ['=' * 10_000 + '0123456789'] [0, 3, 6].each do |off| tmux.prepare tmux.send_keys "#{FZF} --hscroll-off=#{off} -q 0 < #{tempname}", :Enter @@ -1188,7 +1246,7 @@ class TestGoFZF < TestBase tmux.send_keys :Tab tmux.until { |lines| lines[-3] == '>>1' } tmux.send_keys :Enter - assert_equal %w[5 2 1], readonce.split($/) + assert_equal %w[5 2 1], readonce.split($INPUT_RECORD_SEPARATOR) end def test_jump_accept @@ -1201,7 +1259,7 @@ class TestGoFZF < TestBase end def test_preview - tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview], :Enter + tmux.send_keys %(seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview), :Enter tmux.until { |lines| lines[1].include?(' {1-1}') } tmux.send_keys :Up tmux.until { |lines| lines[1].include?(' {-}') } @@ -1229,7 +1287,7 @@ class TestGoFZF < TestBase end def test_preview_hidden - tmux.send_keys %[seq 1000 | #{FZF} --preview 'echo {{}-{}}' --preview-window down:1:hidden --bind ?:toggle-preview], :Enter + tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter tmux.until { |lines| lines[-1] == '>' } tmux.send_keys '?' tmux.until { |lines| lines[-2].include?(' {1-1}') } @@ -1240,16 +1298,20 @@ class TestGoFZF < TestBase end def test_preview_size_0 - File.unlink tempname rescue nil - tmux.send_keys %[seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0], :Enter + begin + File.unlink tempname + rescue + nil + end + tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0), :Enter tmux.until { |lines| lines.item_count == 100 && lines[1] == ' 100/100' && lines[2] == '> 1' } - tmux.until { |_| %w[1] == File.readlines(tempname).map(&:chomp) } + tmux.until { |_| %w[1] == File.readlines(tempname).map(&:chomp) } tmux.send_keys :Down tmux.until { |lines| lines[3] == '> 2' } - tmux.until { |_| %w[1 2] == File.readlines(tempname).map(&:chomp) } + tmux.until { |_| %w[1 2] == File.readlines(tempname).map(&:chomp) } tmux.send_keys :Down tmux.until { |lines| lines[4] == '> 3' } - tmux.until { |_| %w[1 2 3] == File.readlines(tempname).map(&:chomp) } + tmux.until { |_| %w[1 2 3] == File.readlines(tempname).map(&:chomp) } end def test_no_clear @@ -1262,7 +1324,7 @@ class TestGoFZF < TestBase end def test_change_top - tmux.send_keys %[seq 1000 | #{FZF} --bind change:top], :Enter + tmux.send_keys %(seq 1000 | #{FZF} --bind change:top), :Enter tmux.until { |lines| lines.match_count == 1000 } tmux.send_keys :Up tmux.until { |lines| lines[-4] == '> 2' } @@ -1282,7 +1344,7 @@ class TestGoFZF < TestBase tmux.send_keys 'a' tmux.until { |lines| lines.any? { |line| line.include? '1 2 3 4 5' } } tmux.send_keys 'a' - tmux.until { |lines| !lines.any? { |line| line.include? '1 2 3 4 5' } } + tmux.until { |lines| lines.none? { |line| line.include? '1 2 3 4 5' } } end end @@ -1295,22 +1357,22 @@ module TestShell @tmux.kill end - def set_var name, val + def set_var(name, val) tmux.prepare tmux.send_keys "export #{name}='#{val}'", :Enter tmux.prepare end - def unset_var name + def unset_var(name) tmux.prepare tmux.send_keys "unset #{name}", :Enter tmux.prepare end def test_ctrl_t - set_var "FZF_CTRL_T_COMMAND", "seq 100" + set_var 'FZF_CTRL_T_COMMAND', 'seq 100' - lines = retries do + retries do tmux.prepare tmux.send_keys 'C-t' tmux.until { |lines| lines.item_count == 100 } @@ -1324,7 +1386,7 @@ module TestShell def test_ctrl_t_unicode writelines tempname, ['fzf-unicode 테스트1', 'fzf-unicode 테스트2'] - set_var "FZF_CTRL_T_COMMAND", "cat #{tempname}" + set_var 'FZF_CTRL_T_COMMAND', "cat #{tempname}" retries do tmux.prepare @@ -1348,16 +1410,16 @@ module TestShell tmux.until { |lines| lines.select_count == 2 } tmux.send_keys :Enter - tmux.until { |lines| lines.any_include? /echo.*fzf-unicode.*1.*fzf-unicode.*2/ } + tmux.until { |lines| lines.any_include?(/echo.*fzf-unicode.*1.*fzf-unicode.*2/) } tmux.send_keys :Enter - tmux.until { |lines| lines.any_include? /^fzf-unicode.*1.*fzf-unicode.*2/ } + tmux.until { |lines| lines.any_include?(/^fzf-unicode.*1.*fzf-unicode.*2/) } end def test_alt_c lines = retries do tmux.prepare tmux.send_keys :Escape, :c - tmux.until { |lines| lines.item_count > 0 } + tmux.until { |lines| lines.item_count.positive? } end expected = lines.reverse.select { |l| l.start_with? '>' }.first[2..-1] tmux.send_keys :Enter @@ -1375,7 +1437,7 @@ module TestShell retries do tmux.prepare tmux.send_keys :Escape, :c - lines = tmux.until { |lines| lines.item_count == 1 } + tmux.until { |lines| lines.item_count == 1 } end tmux.send_keys :Enter @@ -1394,7 +1456,7 @@ module TestShell retries do tmux.prepare tmux.send_keys 'C-r' - tmux.until { |lines| lines.item_count > 0 } + tmux.until { |lines| lines.item_count.positive? } end tmux.send_keys 'C-r' tmux.send_keys '3d' @@ -1405,14 +1467,14 @@ module TestShell tmux.until { |lines| lines[-1] == '3rd' } end - def retries times = 3, &block - (times - 1).times do |t| + def retries(times = 3) + (times - 1).times do begin - return block.call + return yield rescue RuntimeError end end - block.call + yield end end @@ -1426,7 +1488,7 @@ module CompletionTest end tmux.prepare tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab - tmux.until { |lines| lines.item_count > 0 } + tmux.until { |lines| lines.item_count.positive? } tmux.send_keys ' !d' tmux.until { |lines| lines.match_count == 2 } tmux.send_keys :Tab, :Tab @@ -1434,13 +1496,13 @@ module CompletionTest tmux.send_keys :Enter tmux.until(true) do |lines| lines[-1].include?('/tmp/fzf-test/10') && - lines[-1].include?('/tmp/fzf-test/100') + lines[-1].include?('/tmp/fzf-test/100') end # ~USERNAME** tmux.send_keys 'C-u' tmux.send_keys "cat ~#{ENV['USER']}**", :Tab - tmux.until { |lines| lines.item_count > 0 } + tmux.until { |lines| lines.item_count.positive? } tmux.send_keys "'.fzf-home" tmux.until { |lines| lines.select { |l| l.include? '.fzf-home' }.count > 1 } tmux.send_keys :Enter @@ -1450,7 +1512,7 @@ module CompletionTest # ~INVALID_USERNAME** tmux.send_keys 'C-u' - tmux.send_keys "cat ~such**", :Tab + tmux.send_keys 'cat ~such**', :Tab tmux.until(true) { |lines| lines.any_include? 'no~such~user' } tmux.send_keys :Enter tmux.until(true) { |lines| lines[-1].end_with?('no~such~user') } @@ -1458,7 +1520,7 @@ module CompletionTest # /tmp/fzf\ test** tmux.send_keys 'C-u' tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab - tmux.until { |lines| lines.item_count > 0 } + tmux.until { |lines| lines.item_count.positive? } tmux.send_keys 'foobar$' tmux.until { |lines| lines.match_count == 1 } tmux.send_keys :Enter @@ -1478,7 +1540,7 @@ module CompletionTest def test_file_completion_root tmux.send_keys 'ls /**', :Tab - tmux.until { |lines| lines.item_count > 0 } + tmux.until { |lines| lines.item_count.positive? } tmux.send_keys :Enter end @@ -1489,7 +1551,7 @@ module CompletionTest FileUtils.touch '/tmp/fzf-test/d55/xxx' tmux.prepare tmux.send_keys 'cd /tmp/fzf-test/**', :Tab - tmux.until { |lines| lines.item_count > 0 } + tmux.until { |lines| lines.item_count.positive? } tmux.send_keys :Tab, :Tab # Tab does not work here tmux.send_keys 55 tmux.until { |lines| lines.match_count == 1 } @@ -1518,13 +1580,19 @@ module CompletionTest tmux.prepare tmux.send_keys 'C-L' tmux.send_keys 'kill ', :Tab - tmux.until { |lines| lines.item_count > 0 } + tmux.until { |lines| lines.item_count.positive? } tmux.send_keys 'sleep12345' tmux.until { |lines| lines.any_include? 'sleep 12345' } tmux.send_keys :Enter tmux.until(true) { |lines| lines[-1].include? "kill #{pid}" } ensure - Process.kill 'KILL', pid.to_i rescue nil if pid + if pid + begin + Process.kill 'KILL', pid.to_i + rescue + nil + end + end end def test_custom_completion @@ -1535,7 +1603,7 @@ module CompletionTest tmux.send_keys :Tab, :Tab, :Tab tmux.until { |lines| lines.select_count == 3 } tmux.send_keys :Enter - tmux.until(true) { |lines| lines[-1] == "ls /tmp 1 2" } + tmux.until(true) { |lines| lines[-1] == 'ls /tmp 1 2' } end def test_unset_completion @@ -1621,10 +1689,10 @@ class TestFish < TestBase def new_shell tmux.send_keys 'env FZF_TMUX=1 fish', :Enter tmux.send_keys 'function fish_prompt; end; clear', :Enter - tmux.until { |lines| lines.empty? } + tmux.until(&:empty?) end - def set_var name, val + def set_var(name, val) tmux.prepare tmux.send_keys "set -g #{name} '#{val}'", :Enter tmux.prepare @@ -1635,4 +1703,3 @@ class TestFish < TestBase @tmux = Tmux.new :fish end end - From b208aa675ec1745af4fa2c2b461ddb2c7367b2e7 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 5 Aug 2017 04:28:43 +0900 Subject: [PATCH 570/658] Update Travis build to run on Trusty --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 21ac3f4..0a368fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,6 @@ language: ruby +dist: trusty +sudo: required matrix: include: - env: TAGS= From 999d374f0ce609e1a1a747b5d931bcd9250586d5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 8 Aug 2017 13:22:30 +0900 Subject: [PATCH 571/658] Fix invalid cache lookups --- src/pattern.go | 21 +++++++++------------ src/pattern_test.go | 32 +++++++++++++++----------------- test/test_go.rb | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index 8cb2098..47cabf7 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -10,12 +10,12 @@ import ( // fuzzy // 'exact -// ^exact-prefix -// exact-suffix$ -// !not-fuzzy -// !'not-exact -// !^not-exact-prefix -// !not-exact-suffix$ +// ^prefix-exact +// suffix-exact$ +// !inverse-exact +// !'inverse-fuzzy +// !^inverse-prefix-exact +// !inverse-suffix-exact$ type termType int @@ -32,7 +32,6 @@ type term struct { inv bool text []rune caseSensitive bool - origText []rune } type termSet []term @@ -101,7 +100,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, for idx, term := range termSet { // If the query contains inverse search terms or OR operators, // we cannot cache the search scope - if !cacheable || idx > 0 || term.inv || !fuzzy && term.typ != termExact { + if !cacheable || idx > 0 || term.inv || fuzzy && term.typ != termFuzzy || !fuzzy && term.typ != termExact { cacheable = false break Loop } @@ -153,7 +152,6 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet if !caseSensitive { text = lowerText } - origText := []rune(text) if !fuzzy { typ = termExact } @@ -204,8 +202,7 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet typ: typ, inv: inv, text: textRunes, - caseSensitive: caseSensitive, - origText: origText}) + caseSensitive: caseSensitive}) switchSet = true } } @@ -236,7 +233,7 @@ func (p *Pattern) CacheKey() string { cacheableTerms := []string{} for _, termSet := range p.termSets { if len(termSet) == 1 && !termSet[0].inv && (p.fuzzy || termSet[0].typ == termExact) { - cacheableTerms = append(cacheableTerms, string(termSet[0].origText)) + cacheableTerms = append(cacheableTerms, string(termSet[0].text)) } } return strings.Join(cacheableTerms, " ") diff --git a/src/pattern_test.go b/src/pattern_test.go index 81d158a..9d56ff9 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -33,19 +33,11 @@ func TestParseTermsExtended(t *testing.T) { terms[8][3].typ != termExact || !terms[8][3].inv { t.Errorf("%s", terms) } - for idx, termSet := range terms[:8] { + for _, termSet := range terms[:8] { term := termSet[0] if len(term.text) != 3 { t.Errorf("%s", term) } - if idx > 0 && len(term.origText) != 4+idx/5 { - t.Errorf("%s", term) - } - } - for _, term := range terms[8] { - if len(term.origText) != 4 { - t.Errorf("%s", term) - } } } @@ -167,6 +159,7 @@ func TestOrigTextAndTransformed(t *testing.T) { func TestCacheKey(t *testing.T) { test := func(extended bool, patStr string, expected string, cacheable bool) { + clearPatternCache() pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune(patStr)) if pat.CacheKey() != expected { t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) @@ -188,19 +181,24 @@ func TestCacheKey(t *testing.T) { } func TestCacheable(t *testing.T) { - test := func(fuzzy bool, str string, cacheable bool) { + test := func(fuzzy bool, str string, expected string, cacheable bool) { clearPatternCache() pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, true, []Range{}, Delimiter{}, []rune(str)) + if pat.CacheKey() != expected { + t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) + } if cacheable != pat.cacheable { t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable) } + clearPatternCache() } - test(true, "foo bar", true) - test(true, "foo 'bar", true) - test(true, "foo !bar", false) + test(true, "foo bar", "foo bar", true) + test(true, "foo 'bar", "foo bar", false) + test(true, "foo !bar", "foo", false) - test(false, "foo bar", true) - test(false, "foo '", true) - test(false, "foo 'bar", false) - test(false, "foo !bar", false) + test(false, "foo bar", "foo bar", true) + test(false, "foo 'bar", "foo", false) + test(false, "foo '", "foo", true) + test(false, "foo 'bar", "foo", false) + test(false, "foo !bar", "foo", false) } diff --git a/test/test_go.rb b/test/test_go.rb index b75b150..f001ec6 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -719,6 +719,38 @@ class TestGoFZF < TestBase tmux.send_keys :Enter end + def test_invalid_cache_query_type + command = %[(echo 'foo\\$bar'; echo 'barfoo'; echo 'foo^bar'; echo \\"foo'1-2\\"; seq 100) | #{fzf}] + + # Suffix match + tmux.send_keys command, :Enter + tmux.until { |lines| lines.match_count == 104 } + tmux.send_keys 'foo$' + tmux.until { |lines| lines.match_count == 1 } + tmux.send_keys 'bar' + tmux.until { |lines| lines.match_count == 1 } + tmux.send_keys :Enter + + # Prefix match + tmux.prepare + tmux.send_keys command, :Enter + tmux.until { |lines| lines.match_count == 104 } + tmux.send_keys '^bar' + tmux.until { |lines| lines.match_count == 1 } + tmux.send_keys 'C-a', 'foo' + tmux.until { |lines| lines.match_count == 1 } + tmux.send_keys :Enter + + # Exact match + tmux.prepare + tmux.send_keys command, :Enter + tmux.until { |lines| lines.match_count == 104 } + tmux.send_keys "'12" + tmux.until { |lines| lines.match_count == 1 } + tmux.send_keys 'C-a', 'foo' + tmux.until { |lines| lines.match_count == 1 } + end + def test_smart_case_for_each_term assert_equal 1, `echo Foo bar | #{FZF} -x -f "foo Fbar" | wc -l`.to_i end From 462c68b625e30964d24ef0d26b5e82169f506f0c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 9 Aug 2017 01:54:01 +0900 Subject: [PATCH 572/658] [vim] Fix issues with other plugins changing working directory Close #1005 --- plugin/fzf.vim | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 7e19f25..f895537 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -466,11 +466,11 @@ augroup fzf_popd augroup END function! s:dopopd() - if !exists('w:fzf_prev_dir') || exists('*haslocaldir') && !haslocaldir() + if !exists('w:fzf_dir') || s:fzf_getcwd() != w:fzf_dir[1] return endif - execute 'lcd' s:escape(w:fzf_prev_dir) - unlet w:fzf_prev_dir + execute 'lcd' s:escape(w:fzf_dir[0]) + unlet w:fzf_dir endfunction function! s:xterm_launcher() @@ -719,7 +719,7 @@ function! s:callback(dict, lines) abort let popd = has_key(a:dict, 'prev_dir') && \ (!&autochdir || (empty(a:lines) || len(a:lines) == 1 && empty(a:lines[0]))) if popd - let w:fzf_prev_dir = a:dict.prev_dir + let w:fzf_dir = [a:dict.prev_dir, a:dict.dir] endif try @@ -743,7 +743,7 @@ function! s:callback(dict, lines) abort " We may have opened a new window or tab if popd - let w:fzf_prev_dir = a:dict.prev_dir + let w:fzf_dir = [a:dict.prev_dir, a:dict.dir] call s:dopopd() endif endfunction From dc55e68524a84544417c1ce290df447c4f0b1d60 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 9 Aug 2017 01:58:09 +0900 Subject: [PATCH 573/658] Remove unnecessary SCP (Save Cursor Position) It is reported that it can have an unwanted side effect of clearing the screen on terminal emulators that do not properly support it. Patch suggested by @arya. Close #1011 --- src/tui/light.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tui/light.go b/src/tui/light.go index f905918..15f4e01 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -209,7 +209,9 @@ func (r *LightRenderer) Init() { r.csi(fmt.Sprintf("%dA", r.MaxY()-1)) r.csi("G") r.csi("K") - r.csi("s") + if !r.clearOnExit && !r.fullscreen { + r.csi("s") + } if !r.fullscreen && r.mouse { r.yoffset, _ = r.findOffset() } From e85a8a68d0248a6edfb6ef63c5edb4bcbe18f954 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 9 Aug 2017 23:25:32 +0900 Subject: [PATCH 574/658] Allow escaping meta characters with backslashes One can escape meta characters in extended-search mode with backslashes. Prefixes: \' \! \^ Suffix: \$ Term separator: \ To keep things simple, we are not going to support escaping of escaped sequences (e.g. \\') for matching them literally. Since this is a breaking change, we will bump the minor version. Close #444 --- src/pattern.go | 41 ++++++++++++++++++++++++++++------------- src/pattern_test.go | 12 ++++++------ src/terminal.go | 10 +++++++--- test/test_go.rb | 22 ++++++++++++++++++++++ 4 files changed, 63 insertions(+), 22 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index 47cabf7..3432930 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -53,13 +53,15 @@ type Pattern struct { } var ( - _patternCache map[string]*Pattern - _splitRegex *regexp.Regexp - _cache ChunkCache + _patternCache map[string]*Pattern + _splitRegex *regexp.Regexp + _escapedPrefixRegex *regexp.Regexp + _cache ChunkCache ) func init() { - _splitRegex = regexp.MustCompile("\\s+") + _splitRegex = regexp.MustCompile(" +") + _escapedPrefixRegex = regexp.MustCompile("^\\\\['!^]") clearPatternCache() clearChunkCache() } @@ -80,7 +82,10 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, var asString string if extended { - asString = strings.Trim(string(runes), " ") + asString = strings.TrimLeft(string(runes), " ") + for strings.HasSuffix(asString, " ") && !strings.HasSuffix(asString, "\\ ") { + asString = asString[:len(asString)-1] + } } else { asString = string(runes) } @@ -140,12 +145,13 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, } func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet { + str = strings.Replace(str, "\\ ", "\t", -1) tokens := _splitRegex.Split(str, -1) sets := []termSet{} set := termSet{} switchSet := false for _, token := range tokens { - typ, inv, text := termFuzzy, false, token + typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1) lowerText := strings.ToLower(text) caseSensitive := caseMode == CaseRespect || caseMode == CaseSmart && text != lowerText @@ -167,6 +173,15 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet text = text[1:] } + if strings.HasSuffix(text, "$") { + if strings.HasSuffix(text, "\\$") { + text = text[:len(text)-2] + "$" + } else { + typ = termSuffix + text = text[:len(text)-1] + } + } + if strings.HasPrefix(text, "'") { // Flip exactness if fuzzy && !inv { @@ -177,16 +192,16 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet text = text[1:] } } else if strings.HasPrefix(text, "^") { - if strings.HasSuffix(text, "$") { + if typ == termSuffix { typ = termEqual - text = text[1 : len(text)-1] } else { typ = termPrefix - text = text[1:] } - } else if strings.HasSuffix(text, "$") { - typ = termSuffix - text = text[:len(text)-1] + text = text[1:] + } + + if _escapedPrefixRegex.MatchString(text) { + text = text[1:] } if len(text) > 0 { @@ -236,7 +251,7 @@ func (p *Pattern) CacheKey() string { cacheableTerms = append(cacheableTerms, string(termSet[0].text)) } } - return strings.Join(cacheableTerms, " ") + return strings.Join(cacheableTerms, "\t") } // Match returns the list of matches Items in the given Chunk diff --git a/src/pattern_test.go b/src/pattern_test.go index 9d56ff9..efb1ef2 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -165,15 +165,15 @@ func TestCacheKey(t *testing.T) { t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey()) } if pat.cacheable != cacheable { - t.Errorf("Expected: %s, actual: %s (%s)", cacheable, pat.cacheable, patStr) + t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr) } clearPatternCache() } test(false, "foo !bar", "foo !bar", true) test(false, "foo | bar !baz", "foo | bar !baz", true) - test(true, "foo bar baz", "foo bar baz", true) + test(true, "foo bar baz", "foo\tbar\tbaz", true) test(true, "foo !bar", "foo", false) - test(true, "foo !bar baz", "foo baz", false) + test(true, "foo !bar baz", "foo\tbaz", false) test(true, "foo | bar baz", "baz", false) test(true, "foo | bar | baz", "", false) test(true, "foo | bar !baz", "", false) @@ -192,11 +192,11 @@ func TestCacheable(t *testing.T) { } clearPatternCache() } - test(true, "foo bar", "foo bar", true) - test(true, "foo 'bar", "foo bar", false) + test(true, "foo bar", "foo\tbar", true) + test(true, "foo 'bar", "foo\tbar", false) test(true, "foo !bar", "foo", false) - test(false, "foo bar", "foo bar", true) + test(false, "foo bar", "foo\tbar", true) test(false, "foo 'bar", "foo", false) test(false, "foo '", "foo", true) test(false, "foo 'bar", "foo", false) diff --git a/src/terminal.go b/src/terminal.go index 8c7c1a8..5c66f44 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -281,9 +281,13 @@ func defaultKeymap() map[int][]action { return keymap } +func trimQuery(query string) []rune { + return []rune(strings.Replace(query, "\t", " ", -1)) +} + // NewTerminal returns new Terminal object func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { - input := []rune(opts.Query) + input := trimQuery(opts.Query) var header []string if opts.Reverse { header = opts.Header @@ -1694,13 +1698,13 @@ func (t *Terminal) Loop() { case actPreviousHistory: if t.history != nil { t.history.override(string(t.input)) - t.input = []rune(t.history.previous()) + t.input = trimQuery(t.history.previous()) t.cx = len(t.input) } case actNextHistory: if t.history != nil { t.history.override(string(t.input)) - t.input = []rune(t.history.next()) + t.input = trimQuery(t.history.next()) t.cx = len(t.input) } case actSigStop: diff --git a/test/test_go.rb b/test/test_go.rb index f001ec6..3460f56 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1378,6 +1378,28 @@ class TestGoFZF < TestBase tmux.send_keys 'a' tmux.until { |lines| lines.none? { |line| line.include? '1 2 3 4 5' } } end + + def test_escaped_meta_characters + input = <<~EOF + foo^bar + foo$bar + foo!bar + foo'bar + foo bar + bar foo + EOF + writelines tempname, input.lines.map(&:chomp) + + assert_equal input.lines.count, `#{FZF} -f'foo bar' < #{tempname}`.lines.count + assert_equal ['foo bar'], `#{FZF} -f'foo\\ bar' < #{tempname}`.lines.map(&:chomp) + assert_equal ['bar foo'], `#{FZF} -f'foo$' < #{tempname}`.lines.map(&:chomp) + assert_equal ['foo$bar'], `#{FZF} -f'foo\\$' < #{tempname}`.lines.map(&:chomp) + assert_equal [], `#{FZF} -f'!bar' < #{tempname}`.lines.map(&:chomp) + assert_equal ['foo!bar'], `#{FZF} -f'\\!bar' < #{tempname}`.lines.map(&:chomp) + assert_equal ['foo bar'], `#{FZF} -f'^foo\\ bar$' < #{tempname}`.lines.map(&:chomp) + assert_equal [], `#{FZF} -f"'br" < #{tempname}`.lines.map(&:chomp) + assert_equal ["foo'bar"], `#{FZF} -f"\\'br" < #{tempname}`.lines.map(&:chomp) + end end module TestShell From 6d53089cc1857255be7f7a33c139128c092d6f9b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 9 Aug 2017 23:33:20 +0900 Subject: [PATCH 575/658] Allow escaping term starting with | Close #444 --- src/pattern.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pattern.go b/src/pattern.go index 3432930..c98f8dd 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -61,7 +61,7 @@ var ( func init() { _splitRegex = regexp.MustCompile(" +") - _escapedPrefixRegex = regexp.MustCompile("^\\\\['!^]") + _escapedPrefixRegex = regexp.MustCompile("^\\\\['!^|]") clearPatternCache() clearChunkCache() } From 6b18b144cf0a608433009515cddef74737bfce56 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 10 Aug 2017 12:40:53 +0900 Subject: [PATCH 576/658] Fix escaping of meta characters after ' or ! prefix https://github.com/junegunn/fzf/issues/444#issuecomment-321432803 --- src/pattern.go | 8 +++----- test/test_go.rb | 10 ++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index c98f8dd..00e3a3a 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -182,7 +182,9 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet } } - if strings.HasPrefix(text, "'") { + if _escapedPrefixRegex.MatchString(text) { + text = text[1:] + } else if strings.HasPrefix(text, "'") { // Flip exactness if fuzzy && !inv { typ = termExact @@ -200,10 +202,6 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet text = text[1:] } - if _escapedPrefixRegex.MatchString(text) { - text = text[1:] - } - if len(text) > 0 { if switchSet { sets = append(sets, set) diff --git a/test/test_go.rb b/test/test_go.rb index 3460f56..f8bd6b0 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1400,6 +1400,16 @@ class TestGoFZF < TestBase assert_equal [], `#{FZF} -f"'br" < #{tempname}`.lines.map(&:chomp) assert_equal ["foo'bar"], `#{FZF} -f"\\'br" < #{tempname}`.lines.map(&:chomp) end + + def test_escaped_meta_characters_only_on_relevant_positions + input = <<~EOF + \\^ + ^ + EOF + writelines tempname, input.lines.map(&:chomp) + assert_equal %w[^ \\^], `#{FZF} -f"\\^" < #{tempname}`.lines.map(&:chomp) + assert_equal %w[\\^], `#{FZF} -f"'\\^" < #{tempname}`.lines.map(&:chomp) + end end module TestShell From e55e029ae8002c46e117d55bdfcc725b69bc21ef Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 10 Aug 2017 23:18:52 +0900 Subject: [PATCH 577/658] Build cache key for a pattern only once --- src/pattern.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index 00e3a3a..35a2f33 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -47,6 +47,7 @@ type Pattern struct { text []rune termSets []termSet cacheable bool + cacheKey string delimiter Delimiter nth []Range procFun map[termType]algo.Algo @@ -134,6 +135,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, delimiter: delimiter, procFun: make(map[termType]algo.Algo)} + ptr.cacheKey = ptr.buildCacheKey() ptr.procFun[termFuzzy] = fuzzyAlgo ptr.procFun[termEqual] = algo.EqualMatch ptr.procFun[termExact] = algo.ExactMatchNaive @@ -238,8 +240,7 @@ func (p *Pattern) AsString() string { return string(p.text) } -// CacheKey is used to build string to be used as the key of result cache -func (p *Pattern) CacheKey() string { +func (p *Pattern) buildCacheKey() string { if !p.extended { return p.AsString() } @@ -252,6 +253,11 @@ func (p *Pattern) CacheKey() string { return strings.Join(cacheableTerms, "\t") } +// CacheKey is used to build string to be used as the key of result cache +func (p *Pattern) CacheKey() string { + return p.cacheKey +} + // Match returns the list of matches Items in the given Chunk func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result { // ChunkCache: Exact match From 02a7b96f33036192abb5816b5860e546e6b4e64e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 10 Aug 2017 23:59:40 +0900 Subject: [PATCH 578/658] Treat $ as proper search query When $ is the leading character in a query, it's probably not meant to be an anchor. --- src/pattern.go | 2 +- src/pattern_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index 35a2f33..8029fd0 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -175,7 +175,7 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet text = text[1:] } - if strings.HasSuffix(text, "$") { + if text != "$" && strings.HasSuffix(text, "$") { if strings.HasSuffix(text, "\\$") { text = text[:len(text)-2] + "$" } else { diff --git a/src/pattern_test.go b/src/pattern_test.go index efb1ef2..7adc51d 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -58,7 +58,7 @@ func TestParseTermsExtendedExact(t *testing.T) { } func TestParseTermsEmpty(t *testing.T) { - terms := parseTerms(true, CaseSmart, false, "' $ ^ !' !^ !$") + terms := parseTerms(true, CaseSmart, false, "' ^ !' !^") if len(terms) != 0 { t.Errorf("%s", terms) } From a09e411936244aa515519134dbbd938ae3cadcd4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 11 Aug 2017 00:07:18 +0900 Subject: [PATCH 579/658] Treat | as proper query when it can't be an OR operator --- src/pattern.go | 5 ++++- src/pattern_test.go | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index 8029fd0..dbb3983 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -152,6 +152,7 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet sets := []termSet{} set := termSet{} switchSet := false + afterBar := false for _, token := range tokens { typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1) lowerText := strings.ToLower(text) @@ -164,10 +165,12 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet typ = termExact } - if text == "|" { + if len(set) > 0 && !afterBar && text == "|" { switchSet = false + afterBar = true continue } + afterBar = false if strings.HasPrefix(text, "!") { inv = true diff --git a/src/pattern_test.go b/src/pattern_test.go index 7adc51d..4066eb4 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -16,7 +16,7 @@ func init() { func TestParseTermsExtended(t *testing.T) { terms := parseTerms(true, CaseSmart, false, - "| aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | | zzz$ | !ZZZ |") + "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | zzz$ | !ZZZ |") if len(terms) != 9 || terms[0][0].typ != termFuzzy || terms[0][0].inv || terms[1][0].typ != termExact || terms[1][0].inv || @@ -177,7 +177,8 @@ func TestCacheKey(t *testing.T) { test(true, "foo | bar baz", "baz", false) test(true, "foo | bar | baz", "", false) test(true, "foo | bar !baz", "", false) - test(true, "| | | foo", "foo", true) + test(true, "| | foo", "", false) + test(true, "| | | foo", "foo", false) } func TestCacheable(t *testing.T) { From 6c76d8cd1cf52a277221bc2f9c6e7762a6360129 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 11 Aug 2017 13:09:33 +0900 Subject: [PATCH 580/658] Disallow escaping of meta characters except for spaces https://github.com/junegunn/fzf/issues/444#issuecomment-321719604 --- src/pattern.go | 20 ++++++-------------- test/test_go.rb | 18 ++---------------- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/src/pattern.go b/src/pattern.go index dbb3983..8de15b9 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -54,15 +54,13 @@ type Pattern struct { } var ( - _patternCache map[string]*Pattern - _splitRegex *regexp.Regexp - _escapedPrefixRegex *regexp.Regexp - _cache ChunkCache + _patternCache map[string]*Pattern + _splitRegex *regexp.Regexp + _cache ChunkCache ) func init() { _splitRegex = regexp.MustCompile(" +") - _escapedPrefixRegex = regexp.MustCompile("^\\\\['!^|]") clearPatternCache() clearChunkCache() } @@ -179,17 +177,11 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet } if text != "$" && strings.HasSuffix(text, "$") { - if strings.HasSuffix(text, "\\$") { - text = text[:len(text)-2] + "$" - } else { - typ = termSuffix - text = text[:len(text)-1] - } + typ = termSuffix + text = text[:len(text)-1] } - if _escapedPrefixRegex.MatchString(text) { - text = text[1:] - } else if strings.HasPrefix(text, "'") { + if strings.HasPrefix(text, "'") { // Flip exactness if fuzzy && !inv { typ = termExact diff --git a/test/test_go.rb b/test/test_go.rb index f8bd6b0..27c2a93 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1391,24 +1391,10 @@ class TestGoFZF < TestBase writelines tempname, input.lines.map(&:chomp) assert_equal input.lines.count, `#{FZF} -f'foo bar' < #{tempname}`.lines.count + assert_equal input.lines.count - 1, `#{FZF} -f'^foo bar$' < #{tempname}`.lines.count assert_equal ['foo bar'], `#{FZF} -f'foo\\ bar' < #{tempname}`.lines.map(&:chomp) - assert_equal ['bar foo'], `#{FZF} -f'foo$' < #{tempname}`.lines.map(&:chomp) - assert_equal ['foo$bar'], `#{FZF} -f'foo\\$' < #{tempname}`.lines.map(&:chomp) - assert_equal [], `#{FZF} -f'!bar' < #{tempname}`.lines.map(&:chomp) - assert_equal ['foo!bar'], `#{FZF} -f'\\!bar' < #{tempname}`.lines.map(&:chomp) assert_equal ['foo bar'], `#{FZF} -f'^foo\\ bar$' < #{tempname}`.lines.map(&:chomp) - assert_equal [], `#{FZF} -f"'br" < #{tempname}`.lines.map(&:chomp) - assert_equal ["foo'bar"], `#{FZF} -f"\\'br" < #{tempname}`.lines.map(&:chomp) - end - - def test_escaped_meta_characters_only_on_relevant_positions - input = <<~EOF - \\^ - ^ - EOF - writelines tempname, input.lines.map(&:chomp) - assert_equal %w[^ \\^], `#{FZF} -f"\\^" < #{tempname}`.lines.map(&:chomp) - assert_equal %w[\\^], `#{FZF} -f"'\\^" < #{tempname}`.lines.map(&:chomp) + assert_equal input.lines.count - 1, `#{FZF} -f'!^foo\\ bar$' < #{tempname}`.lines.count end end From 6bc592e6c95e31fa597c4db8f793a6b14966857f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 12 Aug 2017 00:28:30 +0900 Subject: [PATCH 581/658] Update FuzzyMatchV1 to use skip optimization used in V2 --- src/algo/algo.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/algo/algo.go b/src/algo/algo.go index 7a6dad3..919e31a 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -586,6 +586,9 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text util.Ch if len(pattern) == 0 { return Result{0, 0, 0}, nil } + if asciiFuzzyIndex(&text, pattern, caseSensitive) < 0 { + return Result{-1, -1, 0}, nil + } pidx := 0 sidx := -1 From 053d628b532546ac0f96147792b7480686fad6b0 Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Sun, 13 Aug 2017 10:20:06 -0400 Subject: [PATCH 582/658] Add MinGW 64 to install fzf in Windows 64-bit (#1015) --- install | 1 + 1 file changed, 1 insertion(+) diff --git a/install b/install index b7929e4..fe67c6a 100755 --- a/install +++ b/install @@ -172,6 +172,7 @@ case "$archi" in OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386}.tgz ;; CYGWIN*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;; MINGW*\ *86) download fzf-$version-windows_${binary_arch:-386}.zip ;; + MINGW*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;; *) binary_available=0 binary_error=1 ;; esac From 2069bbc8b54fa77384e42274ee15af7b397af884 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 14 Aug 2017 16:22:07 +0900 Subject: [PATCH 583/658] [vim] Allow Funcref in g:fzf_action https://github.com/junegunn/fzf.vim/issues/185 --- README-VIM.md | 13 +++++++++++++ doc/fzf.txt | 15 ++++++++++++++- plugin/fzf.vim | 7 +++++-- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/README-VIM.md b/README-VIM.md index 804400d..956a46b 100644 --- a/README-VIM.md +++ b/README-VIM.md @@ -55,6 +55,19 @@ let g:fzf_action = { \ 'ctrl-x': 'split', \ 'ctrl-v': 'vsplit' } +" An action can be a reference to a function that processes selected lines +function! s:build_quickfix_list(lines) + call setqflist(map(copy(a:lines), '{ "filename": v:val }')) + copen + cc +endfunction + +let g:fzf_action = { + \ 'ctrl-q': function('s:build_quickfix_list'), + \ 'ctrl-t': 'tab split', + \ 'ctrl-x': 'split', + \ 'ctrl-v': 'vsplit' } + " Default fzf layout " - down / up / left / right let g:fzf_layout = { 'down': '~40%' } diff --git a/doc/fzf.txt b/doc/fzf.txt index 52080ef..a523903 100644 --- a/doc/fzf.txt +++ b/doc/fzf.txt @@ -1,4 +1,4 @@ -fzf.txt fzf Last change: April 28 2017 +fzf.txt fzf Last change: August 14 2017 FZF - TABLE OF CONTENTS *fzf* *fzf-toc* ============================================================================== @@ -80,6 +80,19 @@ Examples~ \ 'ctrl-x': 'split', \ 'ctrl-v': 'vsplit' } + " An action can be a reference to a function that processes selected lines + function! s:build_quickfix_list(lines) + call setqflist(map(copy(a:lines), '{ "filename": v:val }')) + copen + cc + endfunction + + let g:fzf_action = { + \ 'ctrl-q': function('s:build_quickfix_list'), + \ 'ctrl-t': 'tab split', + \ 'ctrl-x': 'split', + \ 'ctrl-v': 'vsplit' } + " Default fzf layout " - down / up / left / right let g:fzf_layout = { 'down': '~40%' } diff --git a/plugin/fzf.vim b/plugin/fzf.vim index f895537..9ce03c0 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -201,7 +201,10 @@ function! s:common_sink(action, lines) abort return endif let key = remove(a:lines, 0) - let cmd = get(a:action, key, 'e') + let Cmd = get(a:action, key, 'e') + if type(Cmd) == type(function('call')) + return Cmd(a:lines) + endif if len(a:lines) > 1 augroup fzf_swap autocmd SwapExists * let v:swapchoice='o' @@ -217,7 +220,7 @@ function! s:common_sink(action, lines) abort execute 'e' s:escape(item) let empty = 0 else - call s:open(cmd, item) + call s:open(Cmd, item) endif if !has('patch-8.0.0177') && !has('nvim-0.2') && exists('#BufEnter') \ && isdirectory(item) From 0d171ba1d81886c6f9caf61867129e6daa268cd6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 15 Aug 2017 01:10:41 +0900 Subject: [PATCH 584/658] Remove special nilItem --- src/cache_test.go | 3 +-- src/chunklist.go | 23 ++++++++++++----------- src/chunklist_test.go | 20 +++++++++++--------- src/core.go | 25 +++++++++++++------------ src/item.go | 6 +----- src/merger.go | 4 ++-- src/pattern.go | 4 ++-- src/pattern_test.go | 13 ++++++------- src/result.go | 2 +- 9 files changed, 49 insertions(+), 51 deletions(-) diff --git a/src/cache_test.go b/src/cache_test.go index 8a2d2cf..5d9c5cc 100644 --- a/src/cache_test.go +++ b/src/cache_test.go @@ -4,9 +4,8 @@ import "testing" func TestChunkCache(t *testing.T) { cache := NewChunkCache() - chunk2 := make(Chunk, chunkSize) chunk1p := &Chunk{} - chunk2p := &chunk2 + chunk2p := &Chunk{count: chunkSize} items1 := []Result{Result{}} items2 := []Result{Result{}, Result{}} cache.Add(chunk1p, "foo", items1) diff --git a/src/chunklist.go b/src/chunklist.go index 4a4458a..599a1ad 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -3,11 +3,14 @@ package fzf import "sync" // Chunk is a list of Items whose size has the upper limit of chunkSize -type Chunk []Item +type Chunk struct { + items [chunkSize]Item + count int +} // ItemBuilder is a closure type that builds Item object from a pointer to a // string and an integer -type ItemBuilder func([]byte, int) Item +type ItemBuilder func(*Item, []byte, int) bool // ChunkList is a list of Chunks type ChunkList struct { @@ -27,17 +30,16 @@ func NewChunkList(trans ItemBuilder) *ChunkList { } func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool { - item := trans(data, index) - if item.Nil() { - return false + if trans(&c.items[c.count], data, index) { + c.count++ + return true } - *c = append(*c, item) - return true + return false } // IsFull returns true if the Chunk is full func (c *Chunk) IsFull() bool { - return len(*c) == chunkSize + return c.count == chunkSize } func (cl *ChunkList) lastChunk() *Chunk { @@ -49,7 +51,7 @@ func CountItems(cs []*Chunk) int { if len(cs) == 0 { return 0 } - return chunkSize*(len(cs)-1) + len(*(cs[len(cs)-1])) + return chunkSize*(len(cs)-1) + cs[len(cs)-1].count } // Push adds the item to the list @@ -57,8 +59,7 @@ func (cl *ChunkList) Push(data []byte) bool { cl.mutex.Lock() if len(cl.chunks) == 0 || cl.lastChunk().IsFull() { - newChunk := Chunk(make([]Item, 0, chunkSize)) - cl.chunks = append(cl.chunks, &newChunk) + cl.chunks = append(cl.chunks, &Chunk{}) } if cl.lastChunk().push(cl.trans, data, cl.count) { diff --git a/src/chunklist_test.go b/src/chunklist_test.go index 78468e3..c8d33a6 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -11,10 +11,10 @@ func TestChunkList(t *testing.T) { // FIXME global sortCriteria = []criterion{byScore, byLength} - cl := NewChunkList(func(s []byte, i int) Item { - chars := util.ToChars(s) - chars.Index = int32(i * 2) - return Item{text: chars} + cl := NewChunkList(func(item *Item, s []byte, i int) bool { + item.text = util.ToChars(s) + item.text.Index = int32(i * 2) + return true }) // Snapshot @@ -40,11 +40,13 @@ func TestChunkList(t *testing.T) { // Check the content of the ChunkList chunk1 := snapshot[0] - if len(*chunk1) != 2 { + if chunk1.count != 2 { t.Error("Snapshot should contain only two items") } - if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].Index() != 0 || - (*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].Index() != 2 { + if chunk1.items[0].text.ToString() != "hello" || + chunk1.items[0].Index() != 0 || + chunk1.items[1].text.ToString() != "world" || + chunk1.items[1].Index() != 2 { t.Error("Invalid data") } if chunk1.IsFull() { @@ -67,14 +69,14 @@ func TestChunkList(t *testing.T) { !snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 { t.Error("Expected two full chunks and one more chunk") } - if len(*snapshot[2]) != 2 { + if snapshot[2].count != 2 { t.Error("Unexpected number of items") } cl.Push([]byte("hello")) cl.Push([]byte("world")) - lastChunkCount := len(*snapshot[len(snapshot)-1]) + lastChunkCount := snapshot[len(snapshot)-1].count if lastChunkCount != 2 { t.Error("Unexpected number of items:", lastChunkCount) } diff --git a/src/core.go b/src/core.go index 0b90a51..968d407 100644 --- a/src/core.go +++ b/src/core.go @@ -85,29 +85,30 @@ func Run(opts *Options, revision string) { var chunkList *ChunkList header := make([]string, 0, opts.HeaderLines) if len(opts.WithNth) == 0 { - chunkList = NewChunkList(func(data []byte, index int) Item { + chunkList = NewChunkList(func(item *Item, data []byte, index int) bool { if len(header) < opts.HeaderLines { header = append(header, string(data)) eventBox.Set(EvtHeader, header) - return nilItem + return false } - chars, colors := ansiProcessor(data) - chars.Index = int32(index) - return Item{text: chars, colors: colors} + item.text, item.colors = ansiProcessor(data) + item.text.Index = int32(index) + return true }) } else { - chunkList = NewChunkList(func(data []byte, index int) Item { + chunkList = NewChunkList(func(item *Item, data []byte, index int) bool { tokens := Tokenize(string(data), opts.Delimiter) trans := Transform(tokens, opts.WithNth) transformed := joinTokens(trans) if len(header) < opts.HeaderLines { header = append(header, transformed) eventBox.Set(EvtHeader, header) - return nilItem + return false } - trimmed, colors := ansiProcessor([]byte(transformed)) - trimmed.Index = int32(index) - return Item{text: trimmed, colors: colors, origText: &data} + item.text, item.colors = ansiProcessor([]byte(transformed)) + item.text.Index = int32(index) + item.origText = &data + return true }) } @@ -151,8 +152,8 @@ func Run(opts *Options, revision string) { slab := util.MakeSlab(slab16Size, slab32Size) reader := Reader{ func(runes []byte) bool { - item := chunkList.trans(runes, 0) - if !item.Nil() { + item := Item{} + if chunkList.trans(&item, runes, 0) { if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil { opts.Printer(item.text.ToString()) found = true diff --git a/src/item.go b/src/item.go index b3879cb..cb778cb 100644 --- a/src/item.go +++ b/src/item.go @@ -17,11 +17,7 @@ func (item *Item) Index() int32 { return item.text.Index } -var nilItem = Item{text: util.Chars{Index: -1}} - -func (item *Item) Nil() bool { - return item.Index() < 0 -} +var minItem = Item{text: util.Chars{Index: -1}} func (item *Item) TrimLength() uint16 { return item.text.TrimLength() diff --git a/src/merger.go b/src/merger.go index 7d30a76..d710f2a 100644 --- a/src/merger.go +++ b/src/merger.go @@ -29,7 +29,7 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger { count: 0} for _, chunk := range *mg.chunks { - mg.count += len(*chunk) + mg.count += chunk.count } return &mg } @@ -65,7 +65,7 @@ func (mg *Merger) Get(idx int) Result { idx = mg.count - idx - 1 } chunk := (*mg.chunks)[idx/chunkSize] - return Result{item: &(*chunk)[idx%chunkSize]} + return Result{item: &chunk.items[idx%chunkSize]} } if mg.sorted { diff --git a/src/pattern.go b/src/pattern.go index 8de15b9..94615ad 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -278,8 +278,8 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re matches := []Result{} if space == nil { - for idx := range *chunk { - if match, _, _ := p.MatchItem(&(*chunk)[idx], false, slab); match != nil { + for idx := 0; idx < chunk.count; idx++ { + if match, _, _ := p.MatchItem(&chunk.items[idx], false, slab); match != nil { matches = append(matches, *match) } } diff --git a/src/pattern_test.go b/src/pattern_test.go index 4066eb4..54d6f51 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -130,12 +130,11 @@ func TestOrigTextAndTransformed(t *testing.T) { origBytes := []byte("junegunn.choi") for _, extended := range []bool{false, true} { - chunk := Chunk{ - Item{ - text: util.ToChars([]byte("junegunn")), - origText: &origBytes, - transformed: &trans}, - } + chunk := Chunk{count: 1} + chunk.items[0] = Item{ + text: util.ToChars([]byte("junegunn")), + origText: &origBytes, + transformed: &trans} pattern.extended = extended matches := pattern.matchChunk(&chunk, nil, slab) // No cache if !(matches[0].item.text.ToString() == "junegunn" && @@ -144,7 +143,7 @@ func TestOrigTextAndTransformed(t *testing.T) { t.Error("Invalid match result", matches) } - match, offsets, pos := pattern.MatchItem(&chunk[0], true, slab) + match, offsets, pos := pattern.MatchItem(&chunk.items[0], true, slab) if !(match.item.text.ToString() == "junegunn" && string(*match.item.origText) == "junegunn.choi" && offsets[0][0] == 0 && offsets[0][1] == 5 && diff --git a/src/result.go b/src/result.go index 2df101b..58cbafd 100644 --- a/src/result.go +++ b/src/result.go @@ -85,7 +85,7 @@ func (result *Result) Index() int32 { } func minRank() Result { - return Result{item: &nilItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}} + return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}} } func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset { From 487c8fe88f4cfcc55850b8aef73665b1d09b8fe0 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 16 Aug 2017 03:24:23 +0900 Subject: [PATCH 585/658] Make Reader event notification asynchronous Instead of notifying the event coordinator (EventBox) whenever a new line is arrived, start a background goroutine that periodically does the task. Atomic.StoreInt32 is much cheaper than mutex synchronization that happens during EventBox update. --- src/constants.go | 7 +++++-- src/core.go | 8 ++++---- src/reader.go | 39 +++++++++++++++++++++++++++++++++++++-- src/reader_test.go | 28 ++++++++++++++++++---------- 4 files changed, 64 insertions(+), 18 deletions(-) diff --git a/src/constants.go b/src/constants.go index 10df1e7..0f6a32c 100644 --- a/src/constants.go +++ b/src/constants.go @@ -16,7 +16,10 @@ const ( coordinatorDelayStep time.Duration = 10 * time.Millisecond // Reader - readerBufferSize = 64 * 1024 + readerBufferSize = 64 * 1024 + readerPollIntervalMin = 10 * time.Millisecond + readerPollIntervalStep = 5 * time.Millisecond + readerPollIntervalMax = 50 * time.Millisecond // Terminal initialDelay = 20 * time.Millisecond @@ -68,7 +71,7 @@ const ( EvtSearchProgress EvtSearchFin EvtHeader - EvtClose + EvtReady ) const ( diff --git a/src/core.go b/src/core.go index 968d407..61f14f9 100644 --- a/src/core.go +++ b/src/core.go @@ -115,9 +115,9 @@ func Run(opts *Options, revision string) { // Reader streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync if !streamingFilter { - reader := Reader{func(data []byte) bool { + reader := NewReader(func(data []byte) bool { return chunkList.Push(data) - }, eventBox, opts.ReadZero} + }, eventBox, opts.ReadZero) go reader.ReadSource() } @@ -150,7 +150,7 @@ func Run(opts *Options, revision string) { found := false if streamingFilter { slab := util.MakeSlab(slab16Size, slab32Size) - reader := Reader{ + reader := NewReader( func(runes []byte) bool { item := Item{} if chunkList.trans(&item, runes, 0) { @@ -160,7 +160,7 @@ func Run(opts *Options, revision string) { } } return false - }, eventBox, opts.ReadZero} + }, eventBox, opts.ReadZero) reader.ReadSource() } else { eventBox.Unwatch(EvtReadNew) diff --git a/src/reader.go b/src/reader.go index 1572e5d..401b8f0 100644 --- a/src/reader.go +++ b/src/reader.go @@ -4,6 +4,8 @@ import ( "bufio" "io" "os" + "sync/atomic" + "time" "github.com/junegunn/fzf/src/util" ) @@ -13,10 +15,43 @@ type Reader struct { pusher func([]byte) bool eventBox *util.EventBox delimNil bool + event int32 +} + +// NewReader returns new Reader object +func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool) *Reader { + return &Reader{pusher, eventBox, delimNil, int32(EvtReady)} +} + +func (r *Reader) startEventPoller() { + go func() { + ptr := &r.event + pollInterval := readerPollIntervalMin + for { + if atomic.CompareAndSwapInt32(ptr, int32(EvtReadNew), int32(EvtReady)) { + r.eventBox.Set(EvtReadNew, true) + pollInterval = readerPollIntervalMin + } else if atomic.LoadInt32(ptr) == int32(EvtReadFin) { + return + } else { + pollInterval += readerPollIntervalStep + if pollInterval > readerPollIntervalMax { + pollInterval = readerPollIntervalMax + } + } + time.Sleep(pollInterval) + } + }() +} + +func (r *Reader) fin(success bool) { + atomic.StoreInt32(&r.event, int32(EvtReadFin)) + r.eventBox.Set(EvtReadFin, success) } // ReadSource reads data from the default command or from standard input func (r *Reader) ReadSource() { + r.startEventPoller() var success bool if util.IsTty() { cmd := os.Getenv("FZF_DEFAULT_COMMAND") @@ -27,7 +62,7 @@ func (r *Reader) ReadSource() { } else { success = r.readFromStdin() } - r.eventBox.Set(EvtReadFin, success) + r.fin(success) } func (r *Reader) feed(src io.Reader) { @@ -51,7 +86,7 @@ func (r *Reader) feed(src io.Reader) { } } if r.pusher(bytea) { - r.eventBox.Set(EvtReadNew, true) + atomic.StoreInt32(&r.event, int32(EvtReadNew)) } } if err != nil { diff --git a/src/reader_test.go b/src/reader_test.go index d5c218c..82ca6b7 100644 --- a/src/reader_test.go +++ b/src/reader_test.go @@ -2,6 +2,7 @@ package fzf import ( "testing" + "time" "github.com/junegunn/fzf/src/util" ) @@ -11,7 +12,10 @@ func TestReadFromCommand(t *testing.T) { eb := util.NewEventBox() reader := Reader{ pusher: func(s []byte) bool { strs = append(strs, string(s)); return true }, - eventBox: eb} + eventBox: eb, + event: int32(EvtReady)} + + reader.startEventPoller() // Check EventBox if eb.Peek(EvtReadNew) { @@ -19,21 +23,16 @@ func TestReadFromCommand(t *testing.T) { } // Normal command - reader.readFromCommand(`echo abc && echo def`) + reader.fin(reader.readFromCommand(`echo abc && echo def`)) if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" { t.Errorf("%s", strs) } // Check EventBox again - if !eb.Peek(EvtReadNew) { - t.Error("EvtReadNew should be set yet") - } + eb.WaitFor(EvtReadFin) // Wait should return immediately eb.Wait(func(events *util.Events) { - if _, found := (*events)[EvtReadNew]; !found { - t.Errorf("%s", events) - } events.Clear() }) @@ -42,8 +41,14 @@ func TestReadFromCommand(t *testing.T) { t.Error("EvtReadNew should not be set yet") } + // Make sure that event poller is finished + time.Sleep(readerPollIntervalMax) + + // Restart event poller + reader.startEventPoller() + // Failing command - reader.readFromCommand(`no-such-command`) + reader.fin(reader.readFromCommand(`no-such-command`)) strs = []string{} if len(strs) > 0 { t.Errorf("%s", strs) @@ -51,6 +56,9 @@ func TestReadFromCommand(t *testing.T) { // Check EventBox again if eb.Peek(EvtReadNew) { - t.Error("Command failed. EvtReadNew should be set") + t.Error("Command failed. EvtReadNew should not be set") + } + if !eb.Peek(EvtReadFin) { + t.Error("EvtReadFin should be set") } } From 0558dfee795c297abef27a3abaa232c73d8a042d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 16 Aug 2017 12:26:06 +0900 Subject: [PATCH 586/658] Remove count field from ChunkList --- src/chunklist.go | 22 +++++++--------------- src/chunklist_test.go | 7 ++----- src/core.go | 13 ++++++++----- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/src/chunklist.go b/src/chunklist.go index 599a1ad..510cd73 100644 --- a/src/chunklist.go +++ b/src/chunklist.go @@ -8,14 +8,12 @@ type Chunk struct { count int } -// ItemBuilder is a closure type that builds Item object from a pointer to a -// string and an integer -type ItemBuilder func(*Item, []byte, int) bool +// ItemBuilder is a closure type that builds Item object from byte array +type ItemBuilder func(*Item, []byte) bool // ChunkList is a list of Chunks type ChunkList struct { chunks []*Chunk - count int mutex sync.Mutex trans ItemBuilder } @@ -24,13 +22,12 @@ type ChunkList struct { func NewChunkList(trans ItemBuilder) *ChunkList { return &ChunkList{ chunks: []*Chunk{}, - count: 0, mutex: sync.Mutex{}, trans: trans} } -func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool { - if trans(&c.items[c.count], data, index) { +func (c *Chunk) push(trans ItemBuilder, data []byte) bool { + if trans(&c.items[c.count], data) { c.count++ return true } @@ -62,13 +59,9 @@ func (cl *ChunkList) Push(data []byte) bool { cl.chunks = append(cl.chunks, &Chunk{}) } - if cl.lastChunk().push(cl.trans, data, cl.count) { - cl.count++ - cl.mutex.Unlock() - return true - } + ret := cl.lastChunk().push(cl.trans, data) cl.mutex.Unlock() - return false + return ret } // Snapshot returns immutable snapshot of the ChunkList @@ -76,7 +69,6 @@ func (cl *ChunkList) Snapshot() ([]*Chunk, int) { cl.mutex.Lock() ret := make([]*Chunk, len(cl.chunks)) - count := cl.count copy(ret, cl.chunks) // Duplicate the last chunk @@ -86,5 +78,5 @@ func (cl *ChunkList) Snapshot() ([]*Chunk, int) { } cl.mutex.Unlock() - return ret, count + return ret, CountItems(ret) } diff --git a/src/chunklist_test.go b/src/chunklist_test.go index c8d33a6..6c1d09e 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -11,9 +11,8 @@ func TestChunkList(t *testing.T) { // FIXME global sortCriteria = []criterion{byScore, byLength} - cl := NewChunkList(func(item *Item, s []byte, i int) bool { + cl := NewChunkList(func(item *Item, s []byte) bool { item.text = util.ToChars(s) - item.text.Index = int32(i * 2) return true }) @@ -44,9 +43,7 @@ func TestChunkList(t *testing.T) { t.Error("Snapshot should contain only two items") } if chunk1.items[0].text.ToString() != "hello" || - chunk1.items[0].Index() != 0 || - chunk1.items[1].text.ToString() != "world" || - chunk1.items[1].Index() != 2 { + chunk1.items[1].text.ToString() != "world" { t.Error("Invalid data") } if chunk1.IsFull() { diff --git a/src/core.go b/src/core.go index 61f14f9..6bb4b2e 100644 --- a/src/core.go +++ b/src/core.go @@ -83,20 +83,22 @@ func Run(opts *Options, revision string) { // Chunk list var chunkList *ChunkList + var itemIndex int32 header := make([]string, 0, opts.HeaderLines) if len(opts.WithNth) == 0 { - chunkList = NewChunkList(func(item *Item, data []byte, index int) bool { + chunkList = NewChunkList(func(item *Item, data []byte) bool { if len(header) < opts.HeaderLines { header = append(header, string(data)) eventBox.Set(EvtHeader, header) return false } item.text, item.colors = ansiProcessor(data) - item.text.Index = int32(index) + item.text.Index = itemIndex + itemIndex++ return true }) } else { - chunkList = NewChunkList(func(item *Item, data []byte, index int) bool { + chunkList = NewChunkList(func(item *Item, data []byte) bool { tokens := Tokenize(string(data), opts.Delimiter) trans := Transform(tokens, opts.WithNth) transformed := joinTokens(trans) @@ -106,8 +108,9 @@ func Run(opts *Options, revision string) { return false } item.text, item.colors = ansiProcessor([]byte(transformed)) - item.text.Index = int32(index) + item.text.Index = itemIndex item.origText = &data + itemIndex++ return true }) } @@ -153,7 +156,7 @@ func Run(opts *Options, revision string) { reader := NewReader( func(runes []byte) bool { item := Item{} - if chunkList.trans(&item, runes, 0) { + if chunkList.trans(&item, runes) { if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil { opts.Printer(item.text.ToString()) found = true From 644277faf17de60125ccb095734628b49d962cd6 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 17 Aug 2017 16:57:02 +0900 Subject: [PATCH 587/658] Linuxbrew can install fzf Close #1017 --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 43ffa71..f55a5ae 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Table of Contents * [Installation](#installation) * [Using git](#using-git) - * [Using Homebrew](#using-homebrew) + * [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew) * [As Vim plugin](#as-vim-plugin) * [Windows](#windows) * [Upgrading fzf](#upgrading-fzf) @@ -83,9 +83,10 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf ~/.fzf/install ``` -### Using Homebrew +### Using Homebrew or Linuxbrew -On OS X, you can use [Homebrew](http://brew.sh/) to install fzf. +Alternatively, you can use [Homebrew](http://brew.sh/) or +[Linuxbrew](http://linuxbrew.sh/) to install fzf. ```sh brew install fzf From 656963e01805efccc788e7e2d83a4bcfaa01ee7b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 17 Aug 2017 19:11:56 +0900 Subject: [PATCH 588/658] Short-circuit ANSI processing if no ANSI codes are found --- src/ansi.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ansi.go b/src/ansi.go index 5856bd5..70ec560 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -74,8 +74,11 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo for idx := 0; idx < len(str); { idx += findAnsiStart(str[idx:]) - // No sign of ANSI code if idx == len(str) { + // No sign of ANSI code + if len(offsets) == 0 { + return str, nil, state + } break } From 8d23646fe6a7cc404824a6c88b3fe373fe38fb60 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 17 Aug 2017 19:12:44 +0900 Subject: [PATCH 589/658] Revert "Short-circuit ANSI processing if no ANSI codes are found" This reverts commit 656963e01805efccc788e7e2d83a4bcfaa01ee7b. --- src/ansi.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/ansi.go b/src/ansi.go index 70ec560..5856bd5 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -74,11 +74,8 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo for idx := 0; idx < len(str); { idx += findAnsiStart(str[idx:]) + // No sign of ANSI code if idx == len(str) { - // No sign of ANSI code - if len(offsets) == 0 { - return str, nil, state - } break } From 931c78a70c3ff39f417898c1e97d21a82b47ec8d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 18 Aug 2017 03:04:11 +0900 Subject: [PATCH 590/658] Short-circuit ANSI processing if no ANSI codes are found Rework of 656963e. Makes --ansi processing around 20% faster on plain strings without ANSI codes. --- src/ansi.go | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/ansi.go b/src/ansi.go index 5856bd5..e1c8529 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -73,8 +73,6 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo runeCount := 0 for idx := 0; idx < len(str); { idx += findAnsiStart(str[idx:]) - - // No sign of ANSI code if idx == len(str) { break } @@ -117,22 +115,30 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo } } - rest := str[prevIdx:] - if len(rest) > 0 { + var rest string + var trimmed string + + if prevIdx == 0 { + // No ANSI code found + rest = str + trimmed = str + } else { + rest = str[prevIdx:] output.WriteString(rest) - if state != nil { - // Update last offset - runeCount += utf8.RuneCountInString(rest) - (&offsets[len(offsets)-1]).offset[1] = int32(runeCount) - } + 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 output.String(), nil, state + return trimmed, nil, state } - return output.String(), &offsets, state + return trimmed, &offsets, state } func interpretCode(ansiCode string, prevState *ansiState) *ansiState { From 6977cf268f253f9f0b826b80ade5ced665436cfd Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 18 Aug 2017 05:30:13 +0900 Subject: [PATCH 591/658] Limit search scope of uppercase letter --- src/algo/algo.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 919e31a..e98ff1d 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -263,8 +263,11 @@ func trySkip(input *util.Chars, caseSensitive bool, b byte, from int) int { // We may need to search for the uppercase letter again. We don't have to // consider normalization as we can be sure that this is an ASCII string. if !caseSensitive && b >= 'a' && b <= 'z' { + if idx > 0 { + byteArray = byteArray[:idx] + } uidx := bytes.IndexByte(byteArray, b-32) - if idx < 0 || uidx >= 0 && uidx < idx { + if uidx >= 0 { idx = uidx } } From c304fc43333c95467394ca2d6e5eddbbcdf99e3d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 19 Aug 2017 12:14:48 +0900 Subject: [PATCH 592/658] Delay slab allocation --- src/algo/algo.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index e98ff1d..74c4ad5 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -329,6 +329,12 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab) } + // Phase 1. Optimized search for ASCII string + idx := asciiFuzzyIndex(&input, pattern, caseSensitive) + if idx < 0 { + return Result{-1, -1, 0}, nil + } + // Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages offset16 := 0 offset32 := 0 @@ -339,12 +345,6 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C // Rune array offset32, T := alloc32(offset32, slab, N, false) - // Phase 1. Optimized search for ASCII string - idx := asciiFuzzyIndex(&input, pattern, caseSensitive) - if idx < 0 { - return Result{-1, -1, 0}, nil - } - // Phase 2. Calculate bonus for each point pidx, lastIdx, prevClass := 0, 0, charNonWord input.CopyRunes(T) From 87874bba88c315b11ce5da37c178e6fdedb8708a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 20 Aug 2017 01:58:51 +0900 Subject: [PATCH 593/658] Remove redundant read event when --sync is used --- src/core.go | 4 ++++ src/util/eventbox.go | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/src/core.go b/src/core.go index 6bb4b2e..1c8182e 100644 --- a/src/core.go +++ b/src/core.go @@ -188,6 +188,7 @@ func Run(opts *Options, revision string) { if opts.Sync { eventBox.Unwatch(EvtReadNew) eventBox.WaitFor(EvtReadFin) + eventBox.Unset(EvtReadNew) } // Go interactive @@ -216,6 +217,9 @@ func Run(opts *Options, revision string) { reading = reading && evt == EvtReadNew snapshot, count := chunkList.Snapshot() terminal.UpdateCount(count, !reading, value.(bool)) + if opts.Sync { + terminal.UpdateList(PassMerger(&snapshot, opts.Tac)) + } matcher.Reset(snapshot, terminal.Input(), false, !reading, sort) case EvtSearchNew: diff --git a/src/util/eventbox.go b/src/util/eventbox.go index b710cf1..acd9561 100644 --- a/src/util/eventbox.go +++ b/src/util/eventbox.go @@ -45,6 +45,13 @@ func (b *EventBox) Set(event EventType, value interface{}) { b.cond.L.Unlock() } +// Unset turns off the event type on the box +func (b *EventBox) Unset(event EventType) { + b.cond.L.Lock() + delete(b.events, event) + b.cond.L.Unlock() +} + // Clear clears the events // Unsynchronized; should be called within Wait routine func (events *Events) Clear() { From 3399e39968473b1920db19ef1969b9df34bdd3c1 Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Sat, 19 Aug 2017 23:28:36 -0400 Subject: [PATCH 594/658] [vim] Escape backslashes in fzf#shellescape (#1021) --- plugin/fzf.vim | 4 ++-- test/fzf.vader | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 9ce03c0..b08d0f6 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -66,8 +66,8 @@ function! s:shellesc_cmd(arg) let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g') let escaped = substitute(escaped, '%', '%%', 'g') let escaped = substitute(escaped, '"', '\\^&', 'g') - let escaped = substitute(escaped, '\\\+\(\\^\)', '\\\\\1', 'g') - return '^"'.substitute(escaped, '[^\\]\zs\\$', '\\\\', '').'^"' + let escaped = substitute(escaped, '\(\\\+\)\(\\^\)', '\1\1\2', 'g') + return '^"'.substitute(escaped, '\(\\\+\)$', '\1\1', '').'^"' endfunction function! fzf#shellescape(arg, ...) diff --git a/test/fzf.vader b/test/fzf.vader index a1a6a2e..faa9418 100644 --- a/test/fzf.vader +++ b/test/fzf.vader @@ -149,16 +149,18 @@ Execute (fzf#wrap): Execute (fzf#shellescape with sh): AssertEqual '''''', fzf#shellescape('', 'sh') + AssertEqual '''\''', fzf#shellescape('\', 'sh') AssertEqual '''""''', fzf#shellescape('""', 'sh') AssertEqual '''foobar>''', fzf#shellescape('foobar>', 'sh') - AssertEqual '''\"''', fzf#shellescape('\"', 'sh') + AssertEqual '''\\\"\\\''', fzf#shellescape('\\\"\\\', 'sh') AssertEqual '''echo ''\''''a''\'''' && echo ''\''''b''\''''''', fzf#shellescape('echo ''a'' && echo ''b''', 'sh') Execute (fzf#shellescape with cmd.exe): AssertEqual '^"^"', fzf#shellescape('', 'cmd.exe') + AssertEqual '^"\\^"', fzf#shellescape('\', 'cmd.exe') AssertEqual '^"\^"\^"^"', fzf#shellescape('""', 'cmd.exe') AssertEqual '^"foobar^>^"', fzf#shellescape('foobar>', 'cmd.exe') - AssertEqual '^"\\\^"\\^"', fzf#shellescape('\\\\\\\\"\', 'cmd.exe') + AssertEqual '^"\\\\\\\^"\\\\\\^"', fzf#shellescape('\\\"\\\', 'cmd.exe') AssertEqual '^"echo ''a'' ^&^& echo ''b''^"', fzf#shellescape('echo ''a'' && echo ''b''', 'cmd.exe') AssertEqual '^"C:\Program Files ^(x86^)\\^"', fzf#shellescape('C:\Program Files (x86)\', 'cmd.exe') From a2a4df088646a5f363f9321c52ef9ab9ba4706aa Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 20 Aug 2017 03:33:55 +0900 Subject: [PATCH 595/658] Pass util.Chars by pointer --- src/algo/algo.go | 24 ++++++++++++------------ src/algo/algo_test.go | 3 ++- src/pattern.go | 2 +- src/pattern_test.go | 6 ++++-- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 74c4ad5..925ef28 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -229,7 +229,7 @@ func bonusFor(prevClass charClass, class charClass) int16 { return 0 } -func bonusAt(input util.Chars, idx int) int16 { +func bonusAt(input *util.Chars, idx int) int16 { if idx == 0 { return bonusBoundary } @@ -251,7 +251,7 @@ func normalizeRune(r rune) rune { // Algo functions make two assumptions // 1. "pattern" is given in lowercase if "caseSensitive" is false // 2. "pattern" is already normalized if "normalize" is true -type Algo func(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) +type Algo func(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) func trySkip(input *util.Chars, caseSensitive bool, b byte, from int) int { byteArray := input.Bytes()[from:] @@ -312,7 +312,7 @@ func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int return firstIdx } -func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { // Assume that pattern is given in lowercase if case-insensitive. // First check if there's a match and calculate bonus for each position. // If the input string is too long, consider finding the matching chars in @@ -330,7 +330,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C } // Phase 1. Optimized search for ASCII string - idx := asciiFuzzyIndex(&input, pattern, caseSensitive) + idx := asciiFuzzyIndex(input, pattern, caseSensitive) if idx < 0 { return Result{-1, -1, 0}, nil } @@ -525,7 +525,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C } // Implement the same sorting criteria as V2 -func calculateScore(caseSensitive bool, normalize bool, text util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) { +func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) { pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0) pos := posArray(withPos, len(pattern)) prevClass := charNonWord @@ -585,11 +585,11 @@ func calculateScore(caseSensitive bool, normalize bool, text util.Chars, pattern } // FuzzyMatchV1 performs fuzzy-match -func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { if len(pattern) == 0 { return Result{0, 0, 0}, nil } - if asciiFuzzyIndex(&text, pattern, caseSensitive) < 0 { + if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 { return Result{-1, -1, 0}, nil } @@ -671,7 +671,7 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text util.Ch // bonus point, instead of stopping immediately after finding the first match. // The solution is much cheaper since there is only one possible alignment of // the pattern. -func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { if len(pattern) == 0 { return Result{0, 0, 0}, nil } @@ -683,7 +683,7 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util return Result{-1, -1, 0}, nil } - if asciiFuzzyIndex(&text, pattern, caseSensitive) < 0 { + if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 { return Result{-1, -1, 0}, nil } @@ -741,7 +741,7 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util } // PrefixMatch performs prefix-match -func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { if len(pattern) == 0 { return Result{0, 0, 0}, nil } @@ -768,7 +768,7 @@ func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text util.Cha } // SuffixMatch performs suffix-match -func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { lenRunes := text.Length() trimmedLen := lenRunes - text.TrailingWhitespaces() if len(pattern) == 0 { @@ -799,7 +799,7 @@ func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text util.Cha } // EqualMatch performs equal-match -func EqualMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { +func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { lenPattern := len(pattern) if text.Length() != lenPattern { return Result{-1, -1, 0}, nil diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index 2da0b3c..610c30e 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -17,7 +17,8 @@ func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool if !caseSensitive { pattern = strings.ToLower(pattern) } - res, pos := fun(caseSensitive, normalize, forward, util.ToChars([]byte(input)), []rune(pattern), true, nil) + chars := util.ToChars([]byte(input)) + res, pos := fun(caseSensitive, normalize, forward, &chars, []rune(pattern), true, nil) var start, end int if pos == nil || len(*pos) == 0 { start = res.Start diff --git a/src/pattern.go b/src/pattern.go index 94615ad..636ae1e 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -387,7 +387,7 @@ func (p *Pattern) transformInput(item *Item) []Token { func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, *[]int) { for _, part := range tokens { - if res, pos := pfun(caseSensitive, normalize, forward, *part.text, pattern, withPos, slab); res.Start >= 0 { + if res, pos := pfun(caseSensitive, normalize, forward, part.text, pattern, withPos, slab); res.Start >= 0 { sidx := int32(res.Start) + part.prefixLength eidx := int32(res.End) + part.prefixLength if pos != nil { diff --git a/src/pattern_test.go b/src/pattern_test.go index 54d6f51..1930ddd 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -69,8 +69,9 @@ func TestExact(t *testing.T) { clearPatternCache() pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("'abc")) + chars := util.ToChars([]byte("aabbcc abc")) res, pos := algo.ExactMatchNaive( - pattern.caseSensitive, pattern.normalize, pattern.forward, util.ToChars([]byte("aabbcc abc")), pattern.termSets[0][0].text, true, nil) + pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil) if res.Start != 7 || res.End != 10 { t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) } @@ -85,8 +86,9 @@ func TestEqual(t *testing.T) { pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("^AbC$")) match := func(str string, sidxExpected int, eidxExpected int) { + chars := util.ToChars([]byte(str)) res, pos := algo.EqualMatch( - pattern.caseSensitive, pattern.normalize, pattern.forward, util.ToChars([]byte(str)), pattern.termSets[0][0].text, true, nil) + pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil) if res.Start != sidxExpected || res.End != eidxExpected { t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End) } From 302cc552efd6c48e3abd04c2077df3b8ce7356af Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 20 Aug 2017 03:45:17 +0900 Subject: [PATCH 596/658] Remove unused clear arguments of alloc16 and alloc32 --- src/algo/algo.go | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 925ef28..8701d30 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -158,27 +158,17 @@ func posArray(withPos bool, len int) *[]int { return nil } -func alloc16(offset int, slab *util.Slab, size int, clear bool) (int, []int16) { +func alloc16(offset int, slab *util.Slab, size int) (int, []int16) { if slab != nil && cap(slab.I16) > offset+size { slice := slab.I16[offset : offset+size] - if clear { - for idx := range slice { - slice[idx] = 0 - } - } return offset + size, slice } return offset, make([]int16, size) } -func alloc32(offset int, slab *util.Slab, size int, clear bool) (int, []int32) { +func alloc32(offset int, slab *util.Slab, size int) (int, []int32) { if slab != nil && cap(slab.I32) > offset+size { slice := slab.I32[offset : offset+size] - if clear { - for idx := range slice { - slice[idx] = 0 - } - } return offset + size, slice } return offset, make([]int32, size) @@ -339,11 +329,11 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util. offset16 := 0 offset32 := 0 // Bonus point for each position - offset16, B := alloc16(offset16, slab, N, false) + offset16, B := alloc16(offset16, slab, N) // The first occurrence of each character in the pattern - offset32, F := alloc32(offset32, slab, M, false) + offset32, F := alloc32(offset32, slab, M) // Rune array - offset32, T := alloc32(offset32, slab, N, false) + offset32, T := alloc32(offset32, slab, N) // Phase 2. Calculate bonus for each point pidx, lastIdx, prevClass := 0, 0, charNonWord @@ -401,10 +391,10 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util. // Phase 3. Fill in score matrix (H) // Unlike the original algorithm, we do not allow omission. width := lastIdx - int(F[0]) + 1 - offset16, H := alloc16(offset16, slab, width*M, false) + offset16, H := alloc16(offset16, slab, width*M) // Possible length of consecutive chunk at each position. - offset16, C := alloc16(offset16, slab, width*M, false) + offset16, C := alloc16(offset16, slab, width*M) maxScore, maxScorePos := int16(0), 0 for i := 0; i < M; i++ { From 6aae12288e4a72818d8efb60b85b7d34330b414e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 20 Aug 2017 03:51:53 +0900 Subject: [PATCH 597/658] Extract debug code from FuzzyMatchV2 --- src/algo/algo.go | 61 +++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 8701d30..168dd91 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -302,6 +302,38 @@ func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int return firstIdx } +func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) { + width := lastIdx - int(F[0]) + 1 + + for i, f := range F { + I := i * width + if i == 0 { + fmt.Print(" ") + for j := int(f); j <= lastIdx; j++ { + fmt.Printf(" " + string(T[j]) + " ") + } + fmt.Println() + } + fmt.Print(string(pattern[i]) + " ") + for idx := int(F[0]); idx < int(f); idx++ { + fmt.Print(" 0 ") + } + for idx := int(f); idx <= lastIdx; idx++ { + fmt.Printf("%2d ", H[i*width+idx-int(F[0])]) + } + fmt.Println() + + fmt.Print(" ") + for idx, p := range C[I : I+width] { + if idx+int(F[0]) < int(F[i]) { + p = 0 + } + fmt.Printf("%2d ", p) + } + fmt.Println() + } +} + func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) { // Assume that pattern is given in lowercase if case-insensitive. // First check if there's a match and calculate bonus for each position. @@ -448,33 +480,10 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util. } H[I+j0] = score } + } - if DEBUG { - if i == 0 { - fmt.Print(" ") - for j := int(F[i]); j <= lastIdx; j++ { - fmt.Printf(" " + string(T[j]) + " ") - } - fmt.Println() - } - fmt.Print(string(pattern[i]) + " ") - for idx := int(F[0]); idx < int(F[i]); idx++ { - fmt.Print(" 0 ") - } - for idx := int(F[i]); idx <= lastIdx; idx++ { - fmt.Printf("%2d ", H[i*width+idx-int(F[0])]) - } - fmt.Println() - - fmt.Print(" ") - for idx, p := range C[I : I+width] { - if idx+int(F[0]) < int(F[i]) { - p = 0 - } - fmt.Printf("%2d ", p) - } - fmt.Println() - } + if DEBUG { + debugV2(T, pattern, F, lastIdx, H, C) } // Phase 4. (Optional) Backtrace to find character positions From 941b0a0ff7c15f7cd51f8f6b4e9a64fd48902ef5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 20 Aug 2017 04:06:21 +0900 Subject: [PATCH 598/658] Minor optimization of FuzzyMatchV2 Calculate the first row of the score matrix during phase 2 --- src/algo/algo.go | 91 ++++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 168dd91..6498ae8 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -360,17 +360,20 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util. // Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages offset16 := 0 offset32 := 0 + offset16, H0 := alloc16(offset16, slab, N) + offset16, C0 := alloc16(offset16, slab, N) // Bonus point for each position offset16, B := alloc16(offset16, slab, N) // The first occurrence of each character in the pattern offset32, F := alloc32(offset32, slab, M) // Rune array offset32, T := alloc32(offset32, slab, N) + input.CopyRunes(T) // Phase 2. Calculate bonus for each point - pidx, lastIdx, prevClass := 0, 0, charNonWord - input.CopyRunes(T) - for ; idx < N; idx++ { + maxScore, maxScorePos := int16(0), 0 + pidx, lastIdx := 0, 0 + for pchar0, prevClass, inGap := pattern[0], charNonWord, false; idx < N; idx++ { char := T[idx] var class charClass if char <= unicode.MaxASCII { @@ -392,51 +395,73 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util. } T[idx] = char - B[idx] = bonusFor(prevClass, class) + bonus := bonusFor(prevClass, class) + B[idx] = bonus prevClass = class - if pidx < M { - if char == pattern[pidx] { - lastIdx = idx + if char == pattern[util.Min(pidx, M-1)] { + if pidx < M { F[pidx] = int32(idx) pidx++ } - } else { - if char == pattern[M-1] { - lastIdx = idx + lastIdx = idx + } + + if char == pchar0 { + score := scoreMatch + bonus*bonusFirstCharMultiplier + H0[idx] = score + C0[idx] = 1 + if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) { + maxScore, maxScorePos = score, idx + if forward && bonus == bonusBoundary { + break + } } + inGap = false + } else { + if idx == 0 { + H0[idx] = 0 + } else if inGap { + H0[idx] = util.Max16(H0[idx-1]+scoreGapExtention, 0) + } else { + H0[idx] = util.Max16(H0[idx-1]+scoreGapStart, 0) + } + C0[idx] = 0 + inGap = true } } if pidx != M { return Result{-1, -1, 0}, nil } - if M == 1 && B[F[0]] == bonusBoundary { - p := int(F[0]) - result := Result{p, p + 1, scoreMatch + bonusBoundary*bonusFirstCharMultiplier} + if M == 1 { + result := Result{maxScorePos, maxScorePos + 1, int(maxScore)} if !withPos { return result, nil } - pos := []int{p} + pos := []int{maxScorePos} return result, &pos } // Phase 3. Fill in score matrix (H) // Unlike the original algorithm, we do not allow omission. - width := lastIdx - int(F[0]) + 1 + f0 := int(F[0]) + width := lastIdx - f0 + 1 offset16, H := alloc16(offset16, slab, width*M) + copy(H, H0[f0:lastIdx+1]) // Possible length of consecutive chunk at each position. offset16, C := alloc16(offset16, slab, width*M) + copy(C, C0[f0:lastIdx+1]) - maxScore, maxScorePos := int16(0), 0 - for i := 0; i < M; i++ { + for i := 1; i < M; i++ { I := i * width + f := int(F[i]) inGap := false - for j := int(F[i]); j <= lastIdx; j++ { - j0 := j - int(F[0]) + for j := f; j <= lastIdx; j++ { + j0 := j - f0 var s1, s2, consecutive int16 - if j > int(F[i]) { + if j > f { if inGap { s2 = H[I+j0-1] + scoreGapExtention } else { @@ -445,24 +470,14 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util. } if pattern[i] == T[j] { - var diag int16 - if i > 0 && j0 > 0 { - diag = H[I-width+j0-1] - } - s1 = diag + scoreMatch + s1 = H[I-width+j0-1] + scoreMatch b := B[j] - if i > 0 { - // j > 0 if i > 0 - consecutive = C[I-width+j0-1] + 1 - // Break consecutive chunk - if b == bonusBoundary { - consecutive = 1 - } else if consecutive > 1 { - b = util.Max16(b, util.Max16(bonusConsecutive, B[j-int(consecutive)+1])) - } - } else { + consecutive = C[I-width+j0-1] + 1 + // Break consecutive chunk + if b == bonusBoundary { consecutive = 1 - b *= bonusFirstCharMultiplier + } else if consecutive > 1 { + b = util.Max16(b, util.Max16(bonusConsecutive, B[j-int(consecutive)+1])) } if s1+b < s2 { s1 += B[j] @@ -488,14 +503,14 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util. // Phase 4. (Optional) Backtrace to find character positions pos := posArray(withPos, M) - j := int(F[0]) + j := f0 if withPos { i := M - 1 j = maxScorePos preferMatch := true for { I := i * width - j0 := j - int(F[0]) + j0 := j - f0 s := H[I+j0] var s1, s2 int16 From 55ee4186aa688e524e041971d588a6f002486deb Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 20 Aug 2017 14:29:50 +0900 Subject: [PATCH 599/658] Ignore EvtReadNew if EvtReadFin is already set --- src/core.go | 4 +++- src/util/eventbox.go | 7 ------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/core.go b/src/core.go index 1c8182e..023f7be 100644 --- a/src/core.go +++ b/src/core.go @@ -188,7 +188,6 @@ func Run(opts *Options, revision string) { if opts.Sync { eventBox.Unwatch(EvtReadNew) eventBox.WaitFor(EvtReadFin) - eventBox.Unset(EvtReadNew) } // Go interactive @@ -210,6 +209,9 @@ func Run(opts *Options, revision string) { delay := true ticks++ eventBox.Wait(func(events *util.Events) { + if _, fin := (*events)[EvtReadFin]; fin { + delete(*events, EvtReadNew) + } for evt, value := range *events { switch evt { diff --git a/src/util/eventbox.go b/src/util/eventbox.go index acd9561..b710cf1 100644 --- a/src/util/eventbox.go +++ b/src/util/eventbox.go @@ -45,13 +45,6 @@ func (b *EventBox) Set(event EventType, value interface{}) { b.cond.L.Unlock() } -// Unset turns off the event type on the box -func (b *EventBox) Unset(event EventType) { - b.cond.L.Lock() - delete(b.events, event) - b.cond.L.Unlock() -} - // Clear clears the events // Unsynchronized; should be called within Wait routine func (events *Events) Clear() { From 71fdb99a07cbb05e5cea4050abb56df2013898f9 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 26 Aug 2017 01:28:39 +0900 Subject: [PATCH 600/658] Remove bound checkings in inner loops --- src/algo/algo.go | 92 ++++++++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 38 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 6498ae8..433b8da 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -328,7 +328,11 @@ func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []in if idx+int(F[0]) < int(F[i]) { p = 0 } - fmt.Printf("%2d ", p) + if p > 0 { + fmt.Printf("%2d ", p) + } else { + fmt.Print(" ") + } } fmt.Println() } @@ -373,8 +377,10 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util. // Phase 2. Calculate bonus for each point maxScore, maxScorePos := int16(0), 0 pidx, lastIdx := 0, 0 - for pchar0, prevClass, inGap := pattern[0], charNonWord, false; idx < N; idx++ { - char := T[idx] + pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), charNonWord, false + Tsub := T[idx:] + H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)] + for off, char := range Tsub { var class charClass if char <= unicode.MaxASCII { class = charClassOfAscii(char) @@ -394,41 +400,41 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util. char = normalizeRune(char) } - T[idx] = char + Tsub[off] = char bonus := bonusFor(prevClass, class) - B[idx] = bonus + Bsub[off] = bonus prevClass = class - if char == pattern[util.Min(pidx, M-1)] { + if char == pchar { if pidx < M { - F[pidx] = int32(idx) + F[pidx] = int32(idx + off) pidx++ + pchar = pattern[util.Min(pidx, M-1)] } - lastIdx = idx + lastIdx = idx + off } if char == pchar0 { score := scoreMatch + bonus*bonusFirstCharMultiplier - H0[idx] = score - C0[idx] = 1 + H0sub[off] = score + C0sub[off] = 1 if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) { - maxScore, maxScorePos = score, idx + maxScore, maxScorePos = score, idx+off if forward && bonus == bonusBoundary { break } } inGap = false } else { - if idx == 0 { - H0[idx] = 0 - } else if inGap { - H0[idx] = util.Max16(H0[idx-1]+scoreGapExtention, 0) + if inGap { + H0sub[off] = util.Max16(prevH0+scoreGapExtention, 0) } else { - H0[idx] = util.Max16(H0[idx-1]+scoreGapStart, 0) + H0sub[off] = util.Max16(prevH0+scoreGapStart, 0) } - C0[idx] = 0 + C0sub[off] = 0 inGap = true } + prevH0 = H0sub[off] } if pidx != M { return Result{-1, -1, 0}, nil @@ -453,47 +459,57 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util. offset16, C := alloc16(offset16, slab, width*M) copy(C, C0[f0:lastIdx+1]) - for i := 1; i < M; i++ { - I := i * width - f := int(F[i]) + Fsub := F[1:] + Psub := pattern[1:][:len(Fsub)] + for off, f := range Fsub { + f := int(f) + pchar := Psub[off] + pidx := off + 1 + row := pidx * width inGap := false - for j := f; j <= lastIdx; j++ { - j0 := j - f0 + Tsub := T[f : lastIdx+1] + Bsub := B[f:][:len(Tsub)] + Csub := C[row+f-f0:][:len(Tsub)] + Cdiag := C[row+f-f0-1-width:][:len(Tsub)] + Hsub := H[row+f-f0:][:len(Tsub)] + Hdiag := H[row+f-f0-1-width:][:len(Tsub)] + Hleft := H[row+f-f0-1:][:len(Tsub)] + Hleft[0] = 0 + for off, char := range Tsub { + col := off + f var s1, s2, consecutive int16 - if j > f { - if inGap { - s2 = H[I+j0-1] + scoreGapExtention - } else { - s2 = H[I+j0-1] + scoreGapStart - } + if inGap { + s2 = Hleft[off] + scoreGapExtention + } else { + s2 = Hleft[off] + scoreGapStart } - if pattern[i] == T[j] { - s1 = H[I-width+j0-1] + scoreMatch - b := B[j] - consecutive = C[I-width+j0-1] + 1 + if pchar == char { + s1 = Hdiag[off] + scoreMatch + b := Bsub[off] + consecutive = Cdiag[off] + 1 // Break consecutive chunk if b == bonusBoundary { consecutive = 1 } else if consecutive > 1 { - b = util.Max16(b, util.Max16(bonusConsecutive, B[j-int(consecutive)+1])) + b = util.Max16(b, util.Max16(bonusConsecutive, B[col-int(consecutive)+1])) } if s1+b < s2 { - s1 += B[j] + s1 += Bsub[off] consecutive = 0 } else { s1 += b } } - C[I+j0] = consecutive + Csub[off] = consecutive inGap = s1 < s2 score := util.Max16(util.Max16(s1, s2), 0) - if i == M-1 && (forward && score > maxScore || !forward && score >= maxScore) { - maxScore, maxScorePos = score, j + if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) { + maxScore, maxScorePos = score, col } - H[I+j0] = score + Hsub[off] = score } } From e825b07e85cf2474459faa9dbb898f1f98b1305e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 26 Aug 2017 01:56:49 +0900 Subject: [PATCH 601/658] [neovim] Allow running FZF in multiple windows Close #1023 --- plugin/fzf.vim | 7 ------- 1 file changed, 7 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index b08d0f6..f0cbe79 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -340,13 +340,6 @@ try set shell=sh endif - if has('nvim') - let running = filter(range(1, bufnr('$')), "bufname(v:val) =~# ';#FZF'") - if len(running) - call s:warn('FZF is already running (in buffer '.join(running, ', ').')!') - return [] - endif - endif let dict = exists('a:1') ? s:upgrade(a:1) : {} let temps = { 'result': s:fzf_tempname() } let optstr = s:evaluate_opts(get(dict, 'options', '')) From 329de8f41664ab0e10ad934abe3623a07988e83f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 26 Aug 2017 02:51:19 +0900 Subject: [PATCH 602/658] [fzf-tmux] Execute trap with bash instead of the default shell Close #1007 --- bin/fzf-tmux | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index 5ec0dc4..c3834f7 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -146,6 +146,7 @@ cleanup() { fi if [ $# -gt 0 ]; then + trap - EXIT exit 130 fi } @@ -170,21 +171,22 @@ for arg in "${args[@]}"; do done pppid=$$ -trap_set="trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM" -trap_unset="trap - EXIT SIGINT SIGTERM" +echo -n "trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM;" > $argsf +close="; trap - EXIT SIGINT SIGTERM $close" if [[ -n "$term" ]] || [[ -t 0 ]]; then - cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf + cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf + cat $argsf TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ - split-window $opt "$trap_set;cd $(printf %q "$PWD");$envs bash $argsf;$trap_unset" $swap \ + split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \ > /dev/null 2>&1 else mkfifo $fifo1 - cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf + cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ - split-window $opt "$trap_set;$envs bash $argsf;$trap_unset" $swap \ + split-window $opt "$envs bash $argsf" $swap \ > /dev/null 2>&1 cat <&0 > $fifo1 & fi From af809c966150f4623f9316e8b63e146491134e50 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 26 Aug 2017 03:24:42 +0900 Subject: [PATCH 603/658] Minor refactorings --- src/ansi.go | 2 +- src/reader.go | 2 +- src/tokenizer.go | 2 +- src/util/chars.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ansi.go b/src/ansi.go index e1c8529..d7c81d3 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -79,7 +79,7 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo // Make sure that we found an ANSI code offset := ansiRegex.FindStringIndex(str[idx:]) - if offset == nil { + if len(offset) < 2 { idx++ continue } diff --git a/src/reader.go b/src/reader.go index 401b8f0..22ce4ba 100644 --- a/src/reader.go +++ b/src/reader.go @@ -76,7 +76,7 @@ func (r *Reader) feed(src io.Reader) { // end in delim. bytea, err := reader.ReadBytes(delim) byteaLen := len(bytea) - if len(bytea) > 0 { + if byteaLen > 0 { if err == nil { // get rid of carriage return if under Windows: if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') { diff --git a/src/tokenizer.go b/src/tokenizer.go index 5b7a8b6..6c1d8ca 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -147,7 +147,7 @@ func Tokenize(text string, delimiter Delimiter) []Token { if delimiter.regex != nil { for len(text) > 0 { loc := delimiter.regex.FindStringIndex(text) - if loc == nil { + if len(loc) < 2 { loc = []int{0, len(text)} } last := util.Max(loc[1], 1) diff --git a/src/util/chars.go b/src/util/chars.go index b06a682..9e58313 100644 --- a/src/util/chars.go +++ b/src/util/chars.go @@ -160,7 +160,7 @@ func (chars *Chars) CopyRunes(dest []rune) { copy(dest, runes) return } - for idx, b := range chars.slice { + for idx, b := range chars.slice[:len(dest)] { dest[idx] = rune(b) } return From 159699b5d7505b132d2299690d7bf9f79d5fbda4 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 26 Aug 2017 20:09:46 +0900 Subject: [PATCH 604/658] Remove an unnecessary code branch --- src/algo/algo.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/algo/algo.go b/src/algo/algo.go index 433b8da..2fe5c09 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -384,20 +384,17 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util. var class charClass if char <= unicode.MaxASCII { class = charClassOfAscii(char) + if !caseSensitive && class == charUpper { + char += 32 + } } else { class = charClassOfNonAscii(char) - } - - if !caseSensitive && class == charUpper { - if char <= unicode.MaxASCII { - char += 32 - } else { + if !caseSensitive && class == charUpper { char = unicode.To(unicode.LowerCase, char) } - } - - if normalize { - char = normalizeRune(char) + if normalize { + char = normalizeRune(char) + } } Tsub[off] = char From 6b4805ca1a1b0758363d1bb8c4c996730e19dc5a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 26 Aug 2017 21:58:18 +0900 Subject: [PATCH 605/658] Optimize rank comparison on x86 (little-endian) --- src/result.go | 15 +-------------- src/result_others.go | 16 ++++++++++++++++ src/result_test.go | 8 ++++---- src/result_x86.go | 16 ++++++++++++++++ 4 files changed, 37 insertions(+), 18 deletions(-) create mode 100644 src/result_others.go create mode 100644 src/result_x86.go diff --git a/src/result.go b/src/result.go index 58cbafd..289d83a 100644 --- a/src/result.go +++ b/src/result.go @@ -70,7 +70,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result { } } } - result.points[idx] = val + result.points[3-idx] = val } return result @@ -224,16 +224,3 @@ func (a ByRelevanceTac) Swap(i, j int) { func (a ByRelevanceTac) Less(i, j int) bool { return compareRanks(a[i], a[j], true) } - -func compareRanks(irank Result, jrank Result, tac bool) bool { - for idx := 0; idx < 4; idx++ { - left := irank.points[idx] - right := jrank.points[idx] - if left < right { - return true - } else if left > right { - return false - } - } - return (irank.item.Index() <= jrank.item.Index()) != tac -} diff --git a/src/result_others.go b/src/result_others.go new file mode 100644 index 0000000..e3363a8 --- /dev/null +++ b/src/result_others.go @@ -0,0 +1,16 @@ +// +build !386,!amd64 + +package fzf + +func compareRanks(irank Result, jrank Result, tac bool) bool { + for idx := 3; idx >= 0; idx-- { + left := irank.points[idx] + right := jrank.points[idx] + if left < right { + return true + } else if left > right { + return false + } + } + return (irank.item.Index() <= jrank.item.Index()) != tac +} diff --git a/src/result_test.go b/src/result_test.go index 1d86b1d..afd1730 100644 --- a/src/result_test.go +++ b/src/result_test.go @@ -59,10 +59,10 @@ func TestResultRank(t *testing.T) { strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} item1 := buildResult( withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2) - if item1.points[0] != math.MaxUint16-2 || // Bonus - item1.points[1] != 3 || // Length - item1.points[2] != 0 || // Unused - item1.points[3] != 0 || // Unused + if item1.points[3] != math.MaxUint16-2 || // Bonus + item1.points[2] != 3 || // Length + item1.points[1] != 0 || // Unused + item1.points[0] != 0 || // Unused item1.item.Index() != 1 { t.Error(item1) } diff --git a/src/result_x86.go b/src/result_x86.go new file mode 100644 index 0000000..60e26e9 --- /dev/null +++ b/src/result_x86.go @@ -0,0 +1,16 @@ +// +build 386 amd64 + +package fzf + +import "unsafe" + +func compareRanks(irank Result, jrank Result, tac bool) bool { + left := *(*uint64)(unsafe.Pointer(&irank.points[0])) + right := *(*uint64)(unsafe.Pointer(&jrank.points[0])) + if left < right { + return true + } else if left > right { + return false + } + return (irank.item.Index() <= jrank.item.Index()) != tac +} From fee404399adcebc0c6253e2ef4dc95871f81c806 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 27 Aug 2017 02:19:39 +0900 Subject: [PATCH 606/658] Make --expect additive Similarly to --bind or --color. --expect used to replace the previously specified keys, and fzf#wrap({'options': '--expect=f1'}) wouldn't work as expected. It forced us to come up with some ugly hacks like the following: https://github.com/junegunn/fzf.vim/blob/13b27c45c8bdf6c3a41376bb83e4895edadf8c7e/autoload/fzf/vim.vim#L1086 --- man/man1/fzf.1 | 6 ++++-- src/options.go | 8 ++++++-- src/options_test.go | 7 +++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 2b32d38..ee64375 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -331,10 +331,12 @@ Comma-separated list of keys that can be used to complete fzf in addition to the default enter key. When this option is set, fzf will print the name of the key pressed as the first line of its output (or as the second line if \fB--print-query\fR is also used). The line will be empty if fzf is completed -with the default enter key. +with the default enter key. If \fB--expect\fR option is specified multiple +times, fzf will expect the union of the keys. \fB--no-expect\fR will clear the +list. .RS -e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR +e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s --expect=f1,f2,~,@\fR .RE .TP .B "--read0" diff --git a/src/options.go b/src/options.go index afd46b9..e2f33f8 100644 --- a/src/options.go +++ b/src/options.go @@ -962,7 +962,9 @@ func parseOptions(opts *Options, allArgs []string) { case "--algo": opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)")) case "--expect": - opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") + for k, v := range parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") { + opts.Expect[k] = v + } case "--no-expect": opts.Expect = make(map[int]string) case "--tiebreak": @@ -1140,7 +1142,9 @@ func parseOptions(opts *Options, allArgs []string) { } else if match, value := optString(arg, "--toggle-sort="); match { parseToggleSort(opts.Keymap, value) } else if match, value := optString(arg, "--expect="); match { - opts.Expect = parseKeyChords(value, "key names required") + for k, v := range parseKeyChords(value, "key names required") { + opts.Expect[k] = v + } } else if match, value := optString(arg, "--tiebreak="); match { opts.Criteria = parseTiebreak(value) } else if match, value := optString(arg, "--color="); match { diff --git a/src/options_test.go b/src/options_test.go index d3c9345..22f4e4e 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -414,3 +414,10 @@ func TestPreviewOpts(t *testing.T) { t.Error(opts.Preview) } } + +func TestAdditiveExpect(t *testing.T) { + opts := optsFor("--expect=a", "--expect", "b", "--expect=c") + if len(opts.Expect) != 3 { + t.Error(opts.Expect) + } +} From e89eebb7bae2bb3f080568dff20b341ed669fd3b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 27 Aug 2017 03:32:21 +0900 Subject: [PATCH 607/658] 0.17.0 --- CHANGELOG.md | 7 +++++++ install | 2 +- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 5 ++++- src/constants.go | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 219123f..f869daf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +0.17.0 +------ +- Performance optimization +- One can match literal spaces in extended-search mode with a space prepended + by a backslash. +- `--expect` is now additive and can be specified multiple times. + 0.16.11 ------- - Performance optimization diff --git a/install b/install index fe67c6a..4a41b6b 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.16.11 +version=0.17.0 auto_completion= key_bindings= update_config=2 diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index 3213c2a..e73d83f 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Aug 2017" "fzf 0.16.11" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Aug 2017" "fzf 0.17.0" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index ee64375..88a4a90 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Aug 2017" "fzf 0.16.11" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Aug 2017" "fzf 0.17.0" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder @@ -412,6 +412,9 @@ Unless specified otherwise, fzf will start in "extended-search mode". In this mode, you can specify multiple patterns delimited by spaces, such as: \fB'wild ^music .mp3$ sbtrkt !rmx\fR +You can prepend a backslash to a space (\fB\\ \fR) to match a literal space +character. + .SS Exact-match (quoted) A term that is prefixed by a single-quote character (\fB'\fR) is interpreted as an "exact-match" (or "non-fuzzy") term. fzf will search for the exact diff --git a/src/constants.go b/src/constants.go index 0f6a32c..cfd3a3b 100644 --- a/src/constants.go +++ b/src/constants.go @@ -9,7 +9,7 @@ import ( const ( // Current version - version = "0.16.11" + version = "0.17.0" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From b27dc3eb1745ed29146feca94652bba0f0dd21c2 Mon Sep 17 00:00:00 2001 From: Jack O'Connor Date: Mon, 28 Aug 2017 09:32:13 -0400 Subject: [PATCH 608/658] [vim] Add parens around piped source commands (#1029) Previously a command like `echo a && echo b` would get transformed into `echo a && echo b | fzf`, which only pipes the output of the second command. Adding parentheses around the source command avoids this issue, and works on both Unix and Windows. --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index f0cbe79..0f4fc77 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -366,7 +366,7 @@ try let source = dict.source let type = type(source) if type == 1 - let prefix = source.'|' + let prefix = '( '.source.' )|' elseif type == 3 let temps.input = s:fzf_tempname() call writefile(source, temps.input) From 48adad5454c25a626b5f8398cce97701d63af640 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 30 Aug 2017 18:58:28 +0900 Subject: [PATCH 609/658] [neovim] Set &shell to sh (again) after opening a new window Close #1031 --- plugin/fzf.vim | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 0f4fc77..d3673b3 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -328,17 +328,20 @@ function! fzf#wrap(...) return opts endfunction -function! fzf#run(...) abort -try - let oshell = &shell - let useshellslash = &shellslash - +function! s:use_sh() + let [shell, shellslash] = [&shell, &shellslash] if s:is_win set shell=cmd.exe set noshellslash else set shell=sh endif + return [shell, shellslash] +endfunction + +function! fzf#run(...) abort +try + let [shell, shellslash] = s:use_sh() let dict = exists('a:1') ? s:upgrade(a:1) : {} let temps = { 'result': s:fzf_tempname() } @@ -405,8 +408,7 @@ try call s:callback(dict, lines) return lines finally - let &shell = oshell - let &shellslash = useshellslash + let [&shell, &shellslash] = [shell, shellslash] endtry endfunction @@ -625,6 +627,7 @@ function! s:execute_term(dict, command, temps) abort let winrest = winrestcmd() let pbuf = bufnr('') let [ppos, winopts] = s:split(a:dict) + call s:use_sh() let b:fzf = a:dict let fzf = { 'buf': bufnr(''), 'pbuf': pbuf, 'ppos': ppos, 'dict': a:dict, 'temps': a:temps, \ 'winopts': winopts, 'winrest': winrest, 'lines': &lines, From 5b7457ff08668efef879174fc7f855fc77d6d18d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 1 Sep 2017 02:43:44 +0900 Subject: [PATCH 610/658] [install] Wait for a linefeed when asking for confirmation Close #1035 --- install | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/install b/install index 4a41b6b..f51479f 100755 --- a/install +++ b/install @@ -59,14 +59,14 @@ cd "$(dirname "${BASH_SOURCE[0]}")" fzf_base="$(pwd)" ask() { - # If stdin is a tty, we are "interactive". - # non-interactive shell: wait for a linefeed - # interactive shell: continue after a single keypress - read_n=$([ -t 0 ] && echo "-n 1") - - read -p "$1 ([y]/n) " $read_n -r - echo - [[ $REPLY =~ ^[Nn]$ ]] + while true; do + read -p "$1 ([y]/n) " -r + if [[ $REPLY =~ ^[Yy]$ ]]; then + return 1 + elif [[ $REPLY =~ ^[Nn]$ ]]; then + return 0 + fi + done } check_binary() { From 9b9c67b768ce4986613e4162af1f57b5b21b5fa6 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 31 Aug 2017 22:16:00 -0400 Subject: [PATCH 611/658] [fzf-tmux] Add pane_height/pane_width fallback (#1037) --- bin/fzf-tmux | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index c3834f7..d509f6a 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -16,8 +16,8 @@ skip="" swap="" close="" term="" -[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) -[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) +[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}") +[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}") help() { >&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS] From 655dfb832891bbb3424774e7f32cf2cef5ee8729 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 1 Sep 2017 18:46:00 +0900 Subject: [PATCH 612/658] [fzf-tmux] Remove cat command Close #1039 --- bin/fzf-tmux | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index d509f6a..d7ce7a6 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -176,7 +176,6 @@ close="; trap - EXIT SIGINT SIGTERM $close" if [[ -n "$term" ]] || [[ -t 0 ]]; then cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf - cat $argsf TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \ From e3e7b3360cd83c20a109ccc550bd0508fc5b1cba Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 2 Sep 2017 03:19:50 +0900 Subject: [PATCH 613/658] Delete ncurses implementation --- src/tui/ncurses.go | 505 --------------------------------------------- 1 file changed, 505 deletions(-) delete mode 100644 src/tui/ncurses.go diff --git a/src/tui/ncurses.go b/src/tui/ncurses.go deleted file mode 100644 index 3263c39..0000000 --- a/src/tui/ncurses.go +++ /dev/null @@ -1,505 +0,0 @@ -// +build ncurses -// +build !windows -// +build !tcell - -package tui - -/* -#include -#include -#cgo !static LDFLAGS: -lncurses -#cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl -#cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch - -FILE* c_tty() { - return fopen("/dev/tty", "r"); -} - -SCREEN* c_newterm(FILE* tty) { - return newterm(NULL, stderr, tty); -} - -int c_getcurx(WINDOW* win) { - return getcurx(win); -} -*/ -import "C" - -import ( - "os" - "strconv" - "strings" - "time" - "unicode/utf8" -) - -func HasFullscreenRenderer() bool { - return true -} - -type Attr C.uint - -type CursesWindow struct { - impl *C.WINDOW - top int - left int - width int - height int -} - -func (w *CursesWindow) Top() int { - return w.top -} - -func (w *CursesWindow) Left() int { - return w.left -} - -func (w *CursesWindow) Width() int { - return w.width -} - -func (w *CursesWindow) Height() int { - return w.height -} - -func (w *CursesWindow) Refresh() { - C.wnoutrefresh(w.impl) -} - -func (w *CursesWindow) FinishFill() { - // NO-OP -} - -const ( - Bold Attr = C.A_BOLD - Dim = C.A_DIM - Blink = C.A_BLINK - Reverse = C.A_REVERSE - Underline = C.A_UNDERLINE -) - -var Italic Attr = C.A_VERTICAL << 1 // FIXME - -const ( - AttrRegular Attr = 0 -) - -var ( - _screen *C.SCREEN - _colorMap map[int]int16 - _colorFn func(ColorPair, Attr) (C.short, C.int) -) - -func init() { - _colorMap = make(map[int]int16) - if strings.HasPrefix(C.GoString(C.curses_version()), "ncurses 5") { - Italic = C.A_NORMAL - } -} - -func (a Attr) Merge(b Attr) Attr { - return a | b -} - -func (r *FullscreenRenderer) defaultTheme() *ColorTheme { - if C.tigetnum(C.CString("colors")) >= 256 { - return Dark256 - } - return Default16 -} - -func (r *FullscreenRenderer) Init() { - C.setlocale(C.LC_ALL, C.CString("")) - tty := C.c_tty() - if tty == nil { - errorExit("Failed to open /dev/tty") - } - _screen = C.c_newterm(tty) - if _screen == nil { - errorExit("Invalid $TERM: " + os.Getenv("TERM")) - } - C.set_term(_screen) - if r.mouse { - C.mousemask(C.ALL_MOUSE_EVENTS, nil) - C.mouseinterval(0) - } - C.noecho() - C.raw() // stty dsusp undef - C.nonl() - C.keypad(C.stdscr, true) - - delay := 50 - delayEnv := os.Getenv("ESCDELAY") - if len(delayEnv) > 0 { - num, err := strconv.Atoi(delayEnv) - if err == nil && num >= 0 { - delay = num - } - } - C.set_escdelay(C.int(delay)) - - if r.theme != nil { - C.start_color() - initTheme(r.theme, r.defaultTheme(), r.forceBlack) - initPairs(r.theme) - C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal.index())))) - _colorFn = attrColored - } else { - initTheme(r.theme, nil, r.forceBlack) - _colorFn = attrMono - } - - C.nodelay(C.stdscr, true) - ch := C.getch() - if ch != C.ERR { - C.ungetch(ch) - } - C.nodelay(C.stdscr, false) -} - -func initPairs(theme *ColorTheme) { - C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg)) - for _, pair := range []ColorPair{ - ColNormal, - ColPrompt, - ColMatch, - ColCurrent, - ColCurrentMatch, - ColSpinner, - ColInfo, - ColCursor, - ColSelected, - ColHeader, - ColBorder} { - C.init_pair(C.short(pair.index()), C.short(pair.Fg()), C.short(pair.Bg())) - } -} - -func (r *FullscreenRenderer) Pause(bool) { - C.endwin() -} - -func (r *FullscreenRenderer) Resume(bool) { -} - -func (r *FullscreenRenderer) Close() { - C.endwin() - C.delscreen(_screen) -} - -func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window { - win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left)) - if r.theme != nil { - C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index())))) - } - // FIXME Does not implement BorderHorizontal - if borderStyle != BorderNone { - pair, attr := _colorFn(ColBorder, 0) - C.wcolor_set(win, pair, nil) - C.wattron(win, attr) - C.box(win, 0, 0) - C.wattroff(win, attr) - C.wcolor_set(win, 0, nil) - } - - return &CursesWindow{ - impl: win, - top: top, - left: left, - width: width, - height: height, - } -} - -func attrColored(color ColorPair, a Attr) (C.short, C.int) { - return C.short(color.index()), C.int(a) -} - -func attrMono(color ColorPair, a Attr) (C.short, C.int) { - return 0, C.int(attrFor(color, a)) -} - -func (r *FullscreenRenderer) MaxX() int { - return int(C.COLS) -} - -func (r *FullscreenRenderer) MaxY() int { - return int(C.LINES) -} - -func (w *CursesWindow) Close() { - C.delwin(w.impl) -} - -func (w *CursesWindow) Enclose(y int, x int) bool { - return bool(C.wenclose(w.impl, C.int(y), C.int(x))) -} - -func (w *CursesWindow) Move(y int, x int) { - C.wmove(w.impl, C.int(y), C.int(x)) -} - -func (w *CursesWindow) MoveAndClear(y int, x int) { - w.Move(y, x) - C.wclrtoeol(w.impl) -} - -func (w *CursesWindow) Print(text string) { - C.waddstr(w.impl, C.CString(strings.Map(func(r rune) rune { - if r < 32 { - return -1 - } - return r - }, text))) -} - -func (w *CursesWindow) CPrint(color ColorPair, attr Attr, text string) { - p, a := _colorFn(color, attr) - C.wcolor_set(w.impl, p, nil) - C.wattron(w.impl, a) - w.Print(text) - C.wattroff(w.impl, a) - C.wcolor_set(w.impl, 0, nil) -} - -func (r *FullscreenRenderer) Clear() { - C.clear() - C.endwin() -} - -func (r *FullscreenRenderer) Refresh() { - C.refresh() -} - -func (w *CursesWindow) Erase() { - C.werase(w.impl) -} - -func (w *CursesWindow) X() int { - return int(C.c_getcurx(w.impl)) -} - -func (r *FullscreenRenderer) DoesAutoWrap() bool { - return true -} - -func (r *FullscreenRenderer) IsOptimized() bool { - return true -} - -func (w *CursesWindow) Fill(str string) FillReturn { - if C.waddstr(w.impl, C.CString(str)) == C.OK { - return FillContinue - } - return FillSuspend -} - -func (w *CursesWindow) CFill(fg Color, bg Color, attr Attr, str string) FillReturn { - index := ColorPair{fg, bg, -1}.index() - C.wcolor_set(w.impl, C.short(index), nil) - C.wattron(w.impl, C.int(attr)) - ret := w.Fill(str) - C.wattroff(w.impl, C.int(attr)) - C.wcolor_set(w.impl, 0, nil) - return ret -} - -func (r *FullscreenRenderer) RefreshWindows(windows []Window) { - for _, w := range windows { - w.Refresh() - } - C.doupdate() -} - -func (p ColorPair) index() int16 { - if p.id >= 0 { - return p.id - } - - // ncurses does not support 24-bit colors - if p.is24() { - return ColDefault.index() - } - - key := p.key() - if found, prs := _colorMap[key]; prs { - return found - } - - id := int16(len(_colorMap)) + ColUser.id - C.init_pair(C.short(id), C.short(p.Fg()), C.short(p.Bg())) - _colorMap[key] = id - return id -} - -func consume(expects ...rune) bool { - for _, r := range expects { - if int(C.getch()) != int(r) { - return false - } - } - return true -} - -func escSequence() Event { - C.nodelay(C.stdscr, true) - defer func() { - C.nodelay(C.stdscr, false) - }() - c := C.getch() - switch c { - case C.ERR: - return Event{ESC, 0, nil} - case CtrlM: - return Event{CtrlAltM, 0, nil} - case '/': - return Event{AltSlash, 0, nil} - case ' ': - return Event{AltSpace, 0, nil} - case 127, C.KEY_BACKSPACE: - return Event{AltBS, 0, nil} - case '[': - // Bracketed paste mode (printf "\e[?2004h") - // \e[200~ TEXT \e[201~ - if consume('2', '0', '0', '~') { - return Event{Invalid, 0, nil} - } - } - if c >= 'a' && c <= 'z' { - return Event{AltA + int(c) - 'a', 0, nil} - } - - if c >= '0' && c <= '9' { - return Event{Alt0 + int(c) - '0', 0, nil} - } - - // Don't care. Ignore the rest. - for ; c != C.ERR; c = C.getch() { - } - return Event{Invalid, 0, nil} -} - -func (r *FullscreenRenderer) GetChar() Event { - c := C.getch() - switch c { - case C.ERR: - // Unexpected error from blocking read - r.Close() - errorExit("Failed to read /dev/tty") - case C.KEY_UP: - return Event{Up, 0, nil} - case C.KEY_DOWN: - return Event{Down, 0, nil} - case C.KEY_LEFT: - return Event{Left, 0, nil} - case C.KEY_RIGHT: - return Event{Right, 0, nil} - case C.KEY_HOME: - return Event{Home, 0, nil} - case C.KEY_END: - return Event{End, 0, nil} - case C.KEY_BACKSPACE: - return Event{BSpace, 0, nil} - case C.KEY_F0 + 1: - return Event{F1, 0, nil} - case C.KEY_F0 + 2: - return Event{F2, 0, nil} - case C.KEY_F0 + 3: - return Event{F3, 0, nil} - case C.KEY_F0 + 4: - return Event{F4, 0, nil} - case C.KEY_F0 + 5: - return Event{F5, 0, nil} - case C.KEY_F0 + 6: - return Event{F6, 0, nil} - case C.KEY_F0 + 7: - return Event{F7, 0, nil} - case C.KEY_F0 + 8: - return Event{F8, 0, nil} - case C.KEY_F0 + 9: - return Event{F9, 0, nil} - case C.KEY_F0 + 10: - return Event{F10, 0, nil} - case C.KEY_F0 + 11: - return Event{F11, 0, nil} - case C.KEY_F0 + 12: - return Event{F12, 0, nil} - case C.KEY_DC: - return Event{Del, 0, nil} - case C.KEY_PPAGE: - return Event{PgUp, 0, nil} - case C.KEY_NPAGE: - return Event{PgDn, 0, nil} - case C.KEY_BTAB: - return Event{BTab, 0, nil} - case C.KEY_ENTER: - return Event{CtrlM, 0, nil} - case C.KEY_SLEFT: - return Event{SLeft, 0, nil} - case C.KEY_SRIGHT: - return Event{SRight, 0, nil} - case C.KEY_MOUSE: - var me C.MEVENT - if C.getmouse(&me) != C.ERR { - mod := ((me.bstate & C.BUTTON_SHIFT) | (me.bstate & C.BUTTON_CTRL) | (me.bstate & C.BUTTON_ALT)) > 0 - x := int(me.x) - y := int(me.y) - /* Cannot use BUTTON1_DOUBLE_CLICKED due to mouseinterval(0) */ - if (me.bstate & C.BUTTON1_PRESSED) > 0 { - now := time.Now() - if now.Sub(r.prevDownTime) < doubleClickDuration { - r.clickY = append(r.clickY, y) - } else { - r.clickY = []int{y} - r.prevDownTime = now - } - return Event{Mouse, 0, &MouseEvent{y, x, 0, true, false, mod}} - } else if (me.bstate & C.BUTTON1_RELEASED) > 0 { - double := false - if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] && - time.Now().Sub(r.prevDownTime) < doubleClickDuration { - double = true - } - return Event{Mouse, 0, &MouseEvent{y, x, 0, false, double, mod}} - } else if (me.bstate&0x8000000) > 0 || (me.bstate&0x80) > 0 { - return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}} - } else if (me.bstate & C.BUTTON4_PRESSED) > 0 { - return Event{Mouse, 0, &MouseEvent{y, x, 1, false, false, mod}} - } - } - return Event{Invalid, 0, nil} - case C.KEY_RESIZE: - return Event{Resize, 0, nil} - case ESC: - return escSequence() - case 127: - return Event{BSpace, 0, nil} - case 0: - return Event{CtrlSpace, 0, nil} - } - // CTRL-A ~ CTRL-Z - if c >= CtrlA && c <= CtrlZ { - return Event{int(c), 0, nil} - } - - // Multi-byte character - buffer := []byte{byte(c)} - for { - r, _ := utf8.DecodeRune(buffer) - if r != utf8.RuneError { - return Event{Rune, r, nil} - } - - c := C.getch() - if c == C.ERR { - break - } - if c >= C.KEY_CODE_YES { - C.ungetch(c) - break - } - buffer = append(buffer, byte(c)) - } - return Event{Invalid, 0, nil} -} From 20cdbac8c3d9e658a0bf6cf3051981d0910793f2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 3 Sep 2017 11:38:22 +0900 Subject: [PATCH 614/658] [install] Ignore user-defined grep aliases --- install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install b/install index f51479f..1a7f6c7 100755 --- a/install +++ b/install @@ -257,7 +257,7 @@ has_fish=$(command -v fish > /dev/null && echo 1 || echo 0) if [ $has_fish -eq 1 ]; then echo -n "Update fish_user_paths ... " fish << EOF - echo \$fish_user_paths | grep $fzf_base/bin > /dev/null + echo \$fish_user_paths | \grep $fzf_base/bin > /dev/null or set --universal fish_user_paths \$fish_user_paths $fzf_base/bin EOF [ $? -eq 0 ] && echo "OK" || echo "Failed" From 9516fe332447641c2446503927a2eca640b6ce3f Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 3 Sep 2017 11:45:22 +0900 Subject: [PATCH 615/658] [install] Add --no-{bash,zsh,fish} Close #1040 --- install | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/install b/install index 1a7f6c7..416ec89 100755 --- a/install +++ b/install @@ -8,6 +8,7 @@ key_bindings= update_config=2 binary_arch= allow_legacy= +shells="bash zsh fish" help() { cat << EOF @@ -21,6 +22,10 @@ usage: $0 [OPTIONS] --[no-]completion Enable/disable fuzzy completion (bash & zsh) --[no-]update-rc Whether or not to update shell configuration files + --no-bash Do not set up bash configuration + --no-zsh Do not set up zsh configuration + --no-fish Do not set up fish configuration + --32 Download 32-bit binary --64 Download 64-bit binary EOF @@ -47,6 +52,9 @@ for opt in "$@"; do --32) binary_arch=386 ;; --64) binary_arch=amd64 ;; --bin) ;; + --no-bash) shells=${shells/bash/} ;; + --no-zsh) shells=${shells/zsh/} ;; + --no-fish) shells=${shells/fish/} ;; *) echo "unknown option: $opt" help @@ -204,6 +212,17 @@ fi [[ "$*" =~ "--bin" ]] && exit 0 +for s in $shells; do + if ! command -v "$s" > /dev/null; then + shells=${shells/$s/} + fi +done + +if [[ ${#shells} -lt 3 ]]; then + echo "No shell configuration to be updated." + exit 0 +fi + # Auto-completion if [ -z "$auto_completion" ]; then ask "Do you want to enable fuzzy auto-completion?" @@ -217,9 +236,8 @@ if [ -z "$key_bindings" ]; then fi echo -has_zsh=$(command -v zsh > /dev/null && echo 1 || echo 0) -shells=$([ $has_zsh -eq 1 ] && echo "bash zsh" || echo "bash") for shell in $shells; do + [[ "$shell" = fish ]] && continue echo -n "Generate ~/.fzf.$shell ... " src=~/.fzf.${shell} @@ -253,8 +271,7 @@ EOF done # fish -has_fish=$(command -v fish > /dev/null && echo 1 || echo 0) -if [ $has_fish -eq 1 ]; then +if [[ "$shells" =~ fish ]]; then echo -n "Update fish_user_paths ... " fish << EOF echo \$fish_user_paths | \grep $fzf_base/bin > /dev/null @@ -330,11 +347,12 @@ if [ $update_config -eq 2 ]; then fi echo for shell in $shells; do + [[ "$shell" = fish ]] && continue [ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" "$dest" "~/.fzf.${shell}" done -if [ $key_bindings -eq 1 ] && [ $has_fish -eq 1 ]; then +if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then bind_file=~/.config/fish/functions/fish_user_key_bindings.fish if [ ! -e "$bind_file" ]; then create_file "$bind_file" \ @@ -348,9 +366,9 @@ fi if [ $update_config -eq 1 ]; then echo 'Finished. Restart your shell or reload config file.' - echo ' source ~/.bashrc # bash' - [ $has_zsh -eq 1 ] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh" - [ $has_fish -eq 1 ] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish' + [[ "$shells" =~ bash ]] && echo ' source ~/.bashrc # bash' + [[ "$shells" =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh" + [[ "$shells" =~ fish ]] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish' echo echo 'Use uninstall script to remove fzf.' echo From c7ad97c641aa9b3fa6af13431381e27539a5985d Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Wed, 16 Aug 2017 21:27:10 -0400 Subject: [PATCH 616/658] [neovim] use terminal in Windows for v0.2.1+ --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index d3673b3..1565db8 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -385,7 +385,7 @@ try let use_height = has_key(dict, 'down') && \ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right')) && \ executable('tput') && filereadable('/dev/tty') - let use_term = has('nvim') && !s:is_win + let use_term = has('nvim-0.2.1') || (has('nvim') && !s:is_win) let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict) if prefer_tmux && use_tmux let use_height = 0 From 5097e563df9c066e307b7923283cf1609ede693e Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Sat, 26 Aug 2017 20:10:01 -0400 Subject: [PATCH 617/658] [neovim] Fix terminal buffer marker on Windows Original Patch: https://github.com/junegunn/fzf/commit/a9bf29b65ec36d9caa7298aad0322fba8cb3e130 --- plugin/fzf.vim | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 1565db8..15b2f6d 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -35,6 +35,8 @@ else let s:base_dir = expand(':h:h') endif if s:is_win + let s:term_marker = '&::FZF' + function! s:fzf_call(fn, ...) let shellslash = &shellslash try @@ -53,6 +55,8 @@ if s:is_win \ ['chcp %origchcp% > nul'] endfunction else + let s:term_marker = ";#FZF" + function! s:fzf_call(fn, ...) return call(a:fn, a:000) endfunction @@ -681,7 +685,7 @@ function! s:execute_term(dict, command, temps) abort if s:present(a:dict, 'dir') execute 'lcd' s:escape(a:dict.dir) endif - call termopen(a:command . ';#FZF', fzf) + call termopen(a:command.s:term_marker, fzf) finally if s:present(a:dict, 'dir') lcd - From 446e04469df8ff3e49c18fe49722cac77e35d1a5 Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Mon, 21 Aug 2017 21:54:33 -0400 Subject: [PATCH 618/658] [neovim] use batchfile for s:execute_term in Windows --- plugin/fzf.vim | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 15b2f6d..6b16b10 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -685,7 +685,14 @@ function! s:execute_term(dict, command, temps) abort if s:present(a:dict, 'dir') execute 'lcd' s:escape(a:dict.dir) endif - call termopen(a:command.s:term_marker, fzf) + if s:is_win + let fzf.temps.batchfile = s:fzf_tempname().'.bat' + call writefile(s:wrap_cmds(a:command), fzf.temps.batchfile) + let command = fzf.temps.batchfile + else + let command = a:command + endif + call termopen(command.s:term_marker, fzf) finally if s:present(a:dict, 'dir') lcd - From 6c41c95f282861d1cd49827d4a1ca992944a861f Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Sun, 27 Aug 2017 00:55:14 -0400 Subject: [PATCH 619/658] [vim] s:execute_term works in GVim on Windows Requirements: - compiled with +terminal - has patch-8.0.995 - has('gui_running') returns 1 --- plugin/fzf.vim | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 6b16b10..e18b23c 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -389,7 +389,7 @@ try let use_height = has_key(dict, 'down') && \ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right')) && \ executable('tput') && filereadable('/dev/tty') - let use_term = has('nvim-0.2.1') || (has('nvim') && !s:is_win) + let use_term = has('nvim-0.2.1') || (has('nvim') && !s:is_win) || (has('terminal') && has('gui_running') && has('patch-8.0.995')) let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict) if prefer_tmux && use_tmux let use_height = 0 @@ -647,7 +647,7 @@ function! s:execute_term(dict, command, temps) abort endif endif endfunction - function! fzf.on_exit(id, code, _event) + function! fzf.on_exit(id, code, ...) if s:getpos() == self.ppos " {'window': 'enew'} for [opt, val] in items(self.winopts) execute 'let' opt '=' val @@ -692,7 +692,12 @@ function! s:execute_term(dict, command, temps) abort else let command = a:command endif - call termopen(command.s:term_marker, fzf) + let command .= s:term_marker + if has('nvim') + call termopen(command, fzf) + else + call term_start([&shell, &shellcmdflag, command], {'curwin': fzf.buf, 'exit_cb': function(fzf.on_exit)}) + endif finally if s:present(a:dict, 'dir') lcd - From fd6bc7308f4109a96ff05715434f60574d1e7670 Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Sun, 3 Sep 2017 15:44:01 -0400 Subject: [PATCH 620/658] [vim] Use s:execute_term in Windows IMPORTANT: cmd.exe and powershell are fine in default Windows terminal. cmd.exe prompt is broken on ConEmu because it natively supports ucs-2 only. utf-16 support is exclusive to .Net (ie. powershell). utf-8 supports requires chcp, external program, but does not fix the cmd.exe prompt. Use powershell on ConEmu to avoid corrupted text on display --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index e18b23c..1ffb45b 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -389,7 +389,7 @@ try let use_height = has_key(dict, 'down') && \ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right')) && \ executable('tput') && filereadable('/dev/tty') - let use_term = has('nvim-0.2.1') || (has('nvim') && !s:is_win) || (has('terminal') && has('gui_running') && has('patch-8.0.995')) + let use_term = has('nvim-0.2.1') || (has('nvim') && !s:is_win) || (has('terminal') && has('patch-8.0.995') && (has('gui_running') || s:is_win)) let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict) if prefer_tmux && use_tmux let use_height = 0 From 26d7896877641a229da00c2a33aeae1f8fd1da13 Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Tue, 5 Sep 2017 00:15:30 -0400 Subject: [PATCH 621/658] [vim] Bind Ctrl-J in Vim terminal to fix enter key Temporary workaround for non-Windows environment Reference: https://github.com/vim/vim/issues/1998 https://github.com/junegunn/fzf/pull/1019#issuecomment-327008348 --- plugin/fzf.vim | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 1ffb45b..d90694a 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -400,6 +400,9 @@ try let optstr .= ' --height='.height elseif use_term let optstr .= ' --no-height' + if !has('nvim') && !s:is_win + let optstr .= ' --bind ctrl-j:accept' + endif endif let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result From 58b5be8ab6a99937a872e2ce4713f9ac79ff7d4e Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Tue, 5 Sep 2017 13:40:13 +0900 Subject: [PATCH 622/658] 0.17.0-2 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f869daf..05673b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ CHANGELOG ========= +0.17.0-2 +-------- + +A maintenance release for auxiliary scripts. fzf binaries are not updated. + +- Experimental support for the builtin terminal of Vim 8 + - fzf can now run inside GVim +- Updated Vim plugin to better handle `&shell` issue on fish +- Fixed a bug of fzf-tmux where invalid output is generated +- Fixed fzf-tmux to work even when `tput` does not work + 0.17.0 ------ - Performance optimization From 288976310b44b9176a2d5fb2d2da8c5a8b51423d Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 6 Sep 2017 10:44:25 +0900 Subject: [PATCH 623/658] Update g:fzf_colors example --- README-VIM.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README-VIM.md b/README-VIM.md index 956a46b..385712f 100644 --- a/README-VIM.md +++ b/README-VIM.md @@ -86,6 +86,7 @@ let g:fzf_colors = \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], \ 'hl+': ['fg', 'Statement'], \ 'info': ['fg', 'PreProc'], + \ 'border': ['fg', 'Ignore'], \ 'prompt': ['fg', 'Conditional'], \ 'pointer': ['fg', 'Exception'], \ 'marker': ['fg', 'Keyword'], From 5edc3f755ca5f1e5512679cc8e7f995de7e12c3b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 7 Sep 2017 11:01:40 +0900 Subject: [PATCH 624/658] [vim] Update FZF command not set up lengthy prompt on narrow screen Port of https://github.com/junegunn/fzf.vim/commit/e7928d154aad71e530daf5c17733275da5732680 Since :FZF does not enable preview window, we determine based on full &columns instead of &columns / 2. --- plugin/fzf.vim | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index d90694a..2f44909 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -789,6 +789,7 @@ function! s:cmd(bang, ...) abort else let prompt = s:shortpath() endif + let prompt = strwidth(prompt) < &columns - 20 ? prompt : '> ' call extend(opts.options, ['--prompt', prompt]) call extend(opts.options, args) call fzf#run(fzf#wrap('FZF', opts, a:bang)) From 79d2ef46160fff0a489b8c69b2930b4e1422d74b Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Wed, 6 Sep 2017 22:03:26 -0400 Subject: [PATCH 625/658] [vim] Do not pathshorten prompt in cygwin (#1043) Prevents the following case: before pathshorten - /usr/bin after pathshorten - /u/bin piped to cmd.exe - U:/bin --- plugin/fzf.vim | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index 2f44909..c0bae81 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -772,7 +772,10 @@ let s:default_action = { \ 'ctrl-v': 'vsplit' } function! s:shortpath() - let short = pathshorten(fnamemodify(getcwd(), ':~:.')) + let short = fnamemodify(getcwd(), ':~:.') + if !has('win32unix') + let short = pathshorten(short) + endif let slash = (s:is_win && !&shellslash) ? '\' : '/' return empty(short) ? '~'.slash : short . (short =~ escape(slash, '\').'$' ? '' : slash) endfunction From a78ade1771d2c2c41704547eaa81cbc653175e22 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 7 Sep 2017 12:38:34 +0900 Subject: [PATCH 626/658] Update link to performance chart --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f55a5ae..7c29026 100644 --- a/README.md +++ b/README.md @@ -412,7 +412,7 @@ options that affect the performance. algorithm. However, this algorithm is not guaranteed to find the optimal ordering of the matches and is not recommended. -[perf]: https://junegunn.kr/images/fzf-0.16.11.png +[perf]: https://junegunn.kr/images/fzf-0.17.0.png ### Executing external programs From a8deca2dd92742b7e37783fd5629bef701d236ed Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 7 Sep 2017 12:42:23 +0900 Subject: [PATCH 627/658] [vim] Update README-VIM: fzf can run inside GVim --- README-VIM.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README-VIM.md b/README-VIM.md index 385712f..3c8bab0 100644 --- a/README-VIM.md +++ b/README-VIM.md @@ -145,8 +145,13 @@ command! -bang MyStuff GVim ---- -In GVim, you need an external terminal emulator to start fzf with. `xterm` -command is used by default, but you can customize it with `g:fzf_launcher`. +With the latest version of GVim, fzf will start inside the builtin terminal +emulator of Vim. Please note that this terminal feature of Vim is still young +and unstable and you may run into some issues. + +If you have an older version of GVim, you need an external terminal emulator +to start fzf with. `xterm` command is used by default, but you can customize +it with `g:fzf_launcher`. ```vim " This is the default. %s is replaced with fzf command From e3973c74e7eeb40eede286c88380402ef6996a88 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 8 Sep 2017 18:15:31 +0900 Subject: [PATCH 628/658] Fix custom foreground color inside preview window Close #1046 --- src/terminal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal.go b/src/terminal.go index 5c66f44..92e3425 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -990,7 +990,7 @@ func (t *Terminal) printPreview() { if t.theme != nil && ansi != nil && ansi.colored() { fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str) } else { - fillRet = t.pwindow.Fill(str) + fillRet = t.pwindow.CFill(tui.ColNormal.Fg(), tui.ColNormal.Bg(), tui.AttrRegular, str) } return fillRet == tui.FillContinue }) From 7cfa6f0265a7a786b931dde03b75843124a984b2 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 8 Sep 2017 18:33:17 +0900 Subject: [PATCH 629/658] Fix custom foreground color inside preview window (addendum) This fixes foreground color inside preview window when the text has ANSI attributes except for foreground color. Close #1046 --- src/tui/light.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tui/light.go b/src/tui/light.go index 15f4e01..fe9035e 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -105,6 +105,7 @@ type LightWindow struct { posx int posy int tabstop int + fg Color bg Color } @@ -633,8 +634,10 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, bord width: width, height: height, tabstop: r.tabstop, + fg: colDefault, bg: colDefault} if r.theme != nil { + w.fg = r.theme.Fg w.bg = r.theme.Bg } w.drawBorder() @@ -881,6 +884,9 @@ func (w *LightWindow) Fill(text string) FillReturn { func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn { w.Move(w.posy, w.posx) + if fg == colDefault { + fg = w.fg + } if bg == colDefault { bg = w.bg } From e1582b8323a70785d7ebefce993df7474a28e749 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 9 Sep 2017 13:50:07 +0900 Subject: [PATCH 630/658] Clean up renderer code Remove code that is no longer relevant after the removal of ncurses renderer. This commit also fixes background color issue on tcell-based FullscreenRenderer (Windows). --- src/terminal.go | 19 +++++-------- src/tui/dummy.go | 1 - src/tui/light.go | 4 --- src/tui/tcell.go | 29 ++++++++++---------- src/tui/tui.go | 70 +++++++++++++++++++++--------------------------- 5 files changed, 53 insertions(+), 70 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 92e3425..7813680 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -623,10 +623,8 @@ func (t *Terminal) resizeWindows() { width, height, tui.BorderNone) } - if !t.tui.IsOptimized() { - for i := 0; i < t.window.Height(); i++ { - t.window.MoveAndClear(i, 0) - } + for i := 0; i < t.window.Height(); i++ { + t.window.MoveAndClear(i, 0) } t.truncateQuery() } @@ -722,7 +720,7 @@ func (t *Terminal) printHeader() { t.move(line, 2, true) t.printHighlighted(Result{item: item}, - tui.AttrRegular, tui.ColHeader, tui.ColDefault, false, false) + tui.AttrRegular, tui.ColHeader, tui.ColHeader, false, false) } } @@ -775,8 +773,7 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool) { return } - // Optimized renderer can simply erase to the end of the window - t.move(line, 0, t.tui.IsOptimized()) + t.move(line, 0, false) t.window.CPrint(tui.ColCursor, t.strong, label) if current { if selected { @@ -793,11 +790,9 @@ func (t *Terminal) printItem(result Result, line int, i int, current bool) { } newLine.width = t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false, true) } - if !t.tui.IsOptimized() { - fillSpaces := prevLine.width - newLine.width - if fillSpaces > 0 { - t.window.Print(strings.Repeat(" ", fillSpaces)) - } + fillSpaces := prevLine.width - newLine.width + if fillSpaces > 0 { + t.window.Print(strings.Repeat(" ", fillSpaces)) } t.prevLines[i] = newLine } diff --git a/src/tui/dummy.go b/src/tui/dummy.go index c96ce8e..3965c9e 100644 --- a/src/tui/dummy.go +++ b/src/tui/dummy.go @@ -33,7 +33,6 @@ func (r *FullscreenRenderer) Refresh() {} func (r *FullscreenRenderer) Close() {} func (r *FullscreenRenderer) DoesAutoWrap() bool { return false } -func (r *FullscreenRenderer) IsOptimized() bool { return false } func (r *FullscreenRenderer) GetChar() Event { return Event{} } func (r *FullscreenRenderer) MaxX() int { return 0 } func (r *FullscreenRenderer) MaxY() int { return 0 } diff --git a/src/tui/light.go b/src/tui/light.go index fe9035e..52e26ed 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -620,10 +620,6 @@ func (r *LightRenderer) DoesAutoWrap() bool { return false } -func (r *LightRenderer) IsOptimized() bool { - return false -} - func (r *LightRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window { w := &LightWindow{ renderer: r, diff --git a/src/tui/tcell.go b/src/tui/tcell.go index bed5280..7db37c4 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -172,10 +172,6 @@ func (r *FullscreenRenderer) DoesAutoWrap() bool { return false } -func (r *FullscreenRenderer) IsOptimized() bool { - return false -} - func (r *FullscreenRenderer) Clear() { _screen.Sync() _screen.Clear() @@ -409,14 +405,13 @@ func (w *TcellWindow) Close() { func fill(x, y, w, h int, r rune) { for ly := 0; ly <= h; ly++ { for lx := 0; lx <= w; lx++ { - _screen.SetContent(x+lx, y+ly, r, nil, ColDefault.style()) + _screen.SetContent(x+lx, y+ly, r, nil, ColNormal.style()) } } } func (w *TcellWindow) Erase() { - // TODO - fill(w.left, w.top, w.width, w.height, ' ') + fill(w.left-1, w.top, w.width+1, w.height, ' ') } func (w *TcellWindow) Enclose(y int, x int) bool { @@ -433,13 +428,13 @@ func (w *TcellWindow) Move(y int, x int) { func (w *TcellWindow) MoveAndClear(y int, x int) { w.Move(y, x) for i := w.lastX; i < w.width; i++ { - _screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, ColDefault.style()) + _screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, ColNormal.style()) } w.lastX = x } func (w *TcellWindow) Print(text string) { - w.printString(text, ColDefault, 0) + w.printString(text, ColNormal, 0) } func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) { @@ -452,7 +447,7 @@ func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) { Reverse(a&Attr(tcell.AttrReverse) != 0). Underline(a&Attr(tcell.AttrUnderline) != 0) } else { - style = ColDefault.style(). + style = ColNormal.style(). Reverse(a&Attr(tcell.AttrReverse) != 0 || pair == ColCurrent || pair == ColCurrentMatch). Underline(a&Attr(tcell.AttrUnderline) != 0 || pair == ColMatch || pair == ColCurrentMatch) } @@ -503,7 +498,7 @@ func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn if w.color { style = pair.style() } else { - style = ColDefault.style() + style = ColNormal.style() } style = style. Blink(a&Attr(tcell.AttrBlink) != 0). @@ -543,11 +538,17 @@ func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn } func (w *TcellWindow) Fill(str string) FillReturn { - return w.fillString(str, ColDefault, 0) + return w.fillString(str, ColNormal, 0) } func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn { - return w.fillString(str, ColorPair{fg, bg, -1}, a) + if fg == colDefault { + fg = ColNormal.Fg() + } + if bg == colDefault { + bg = ColNormal.Bg() + } + return w.fillString(str, NewColorPair(fg, bg), a) } func (w *TcellWindow) drawBorder(around bool) { @@ -560,7 +561,7 @@ func (w *TcellWindow) drawBorder(around bool) { if w.color { style = ColBorder.style() } else { - style = ColDefault.style() + style = ColNormal.style() } for x := left; x < right; x++ { diff --git a/src/tui/tui.go b/src/tui/tui.go index 2420616..244e0b0 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -133,7 +133,7 @@ const ( type ColorPair struct { fg Color bg Color - id int16 + id int } func HexToColor(rrggbb string) Color { @@ -155,12 +155,8 @@ func (p ColorPair) Bg() Color { return p.bg } -func (p ColorPair) key() int { - return (int(p.Fg()) << 8) + int(p.Bg()) -} - func (p ColorPair) is24() bool { - return p.Fg().is24() || p.Bg().is24() + return p.fg.is24() || p.bg.is24() } type ColorTheme struct { @@ -179,10 +175,6 @@ type ColorTheme struct { Border Color } -func (t *ColorTheme) HasBg() bool { - return t.Bg != colDefault -} - type Event struct { Type int Char rune @@ -220,7 +212,6 @@ type Renderer interface { MaxX() int MaxY() int DoesAutoWrap() bool - IsOptimized() bool NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window } @@ -271,7 +262,6 @@ var ( Dark256 *ColorTheme Light256 *ColorTheme - ColDefault ColorPair ColNormal ColorPair ColPrompt ColorPair ColMatch ColorPair @@ -283,7 +273,6 @@ var ( ColSelected ColorPair ColHeader ColorPair ColBorder ColorPair - ColUser ColorPair ) func EmptyTheme() *ColorTheme { @@ -387,33 +376,36 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) { } func initPalette(theme *ColorTheme) { - ColDefault = ColorPair{colDefault, colDefault, 0} - if theme != nil { - ColNormal = ColorPair{theme.Fg, theme.Bg, 1} - ColPrompt = ColorPair{theme.Prompt, theme.Bg, 2} - ColMatch = ColorPair{theme.Match, theme.Bg, 3} - ColCurrent = ColorPair{theme.Current, theme.DarkBg, 4} - ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg, 5} - ColSpinner = ColorPair{theme.Spinner, theme.Bg, 6} - ColInfo = ColorPair{theme.Info, theme.Bg, 7} - ColCursor = ColorPair{theme.Cursor, theme.DarkBg, 8} - ColSelected = ColorPair{theme.Selected, theme.DarkBg, 9} - ColHeader = ColorPair{theme.Header, theme.Bg, 10} - ColBorder = ColorPair{theme.Border, theme.Bg, 11} - } else { - ColNormal = ColorPair{colDefault, colDefault, 1} - ColPrompt = ColorPair{colDefault, colDefault, 2} - ColMatch = ColorPair{colDefault, colDefault, 3} - ColCurrent = ColorPair{colDefault, colDefault, 4} - ColCurrentMatch = ColorPair{colDefault, colDefault, 5} - ColSpinner = ColorPair{colDefault, colDefault, 6} - ColInfo = ColorPair{colDefault, colDefault, 7} - ColCursor = ColorPair{colDefault, colDefault, 8} - ColSelected = ColorPair{colDefault, colDefault, 9} - ColHeader = ColorPair{colDefault, colDefault, 10} - ColBorder = ColorPair{colDefault, colDefault, 11} + idx := 0 + pair := func(fg, bg Color) ColorPair { + idx++ + return ColorPair{fg, bg, idx} + } + if theme != nil { + ColNormal = pair(theme.Fg, theme.Bg) + ColPrompt = pair(theme.Prompt, theme.Bg) + ColMatch = pair(theme.Match, theme.Bg) + ColCurrent = pair(theme.Current, theme.DarkBg) + ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg) + ColSpinner = pair(theme.Spinner, theme.Bg) + ColInfo = pair(theme.Info, theme.Bg) + ColCursor = pair(theme.Cursor, theme.DarkBg) + ColSelected = pair(theme.Selected, theme.DarkBg) + ColHeader = pair(theme.Header, theme.Bg) + ColBorder = pair(theme.Border, theme.Bg) + } else { + ColNormal = pair(colDefault, colDefault) + ColPrompt = pair(colDefault, colDefault) + ColMatch = pair(colDefault, colDefault) + ColCurrent = pair(colDefault, colDefault) + ColCurrentMatch = pair(colDefault, colDefault) + ColSpinner = pair(colDefault, colDefault) + ColInfo = pair(colDefault, colDefault) + ColCursor = pair(colDefault, colDefault) + ColSelected = pair(colDefault, colDefault) + ColHeader = pair(colDefault, colDefault) + ColBorder = pair(colDefault, colDefault) } - ColUser = ColorPair{colDefault, colDefault, 12} } func attrFor(color ColorPair, attr Attr) Attr { From 2f8238342b9b2388e4cf235a3de4e717890db0bb Mon Sep 17 00:00:00 2001 From: Lawrence Wu <5601392+llwu@users.noreply.github.com> Date: Sun, 10 Sep 2017 17:18:26 -0700 Subject: [PATCH 631/658] [install] Don't touch dotfiles if not requested (#1048) --- install | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/install b/install index 416ec89..c6b4b8c 100755 --- a/install +++ b/install @@ -305,20 +305,22 @@ append_line() { line="$2" file="$3" pat="${4:-}" + lno="" echo "Update $file:" echo " - $line" - [ -f "$file" ] || touch "$file" - if [ $# -lt 4 ]; then - lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ') - else - lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ') + if [ -f "$file" ]; then + if [ $# -lt 4 ]; then + lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ') + else + lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ') + fi fi if [ -n "$lno" ]; then echo " - Already exists: line #$lno" else if [ $update -eq 1 ]; then - echo >> "$file" + [ -f "$file" ] && echo >> "$file" echo "$line" >> "$file" echo " + Added" else From 41e02083353d277491daaaa9594cdfa062b77737 Mon Sep 17 00:00:00 2001 From: Robert Orzanna Date: Sun, 17 Sep 2017 10:12:20 +0200 Subject: [PATCH 632/658] Update Homebrew/Linuxbrew instructions (#1052) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7c29026..f7ebb73 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,8 @@ Alternatively, you can use [Homebrew](http://brew.sh/) or ```sh brew install fzf -# Install shell extensions -/usr/local/opt/fzf/install +# To install useful key bindings and fuzzy completion: +$(brew --prefix)/opt/fzf/install ``` ### As Vim plugin From 45d4c57d910277bf062aa5979104ab6f2be4249e Mon Sep 17 00:00:00 2001 From: Josh Pencheon Date: Tue, 26 Sep 2017 16:18:01 +0100 Subject: [PATCH 633/658] [completion] Include host aliases in ssh completion (#1062) --- shell/completion.bash | 2 +- shell/completion.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index e8c34e5..9e299ec 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -233,7 +233,7 @@ _fzf_complete_telnet() { _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( - cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \ + cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}') \ <(command grep -oE '^[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u diff --git a/shell/completion.zsh b/shell/completion.zsh index a50b751..d3e64d8 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -116,7 +116,7 @@ _fzf_complete_telnet() { _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( - command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \ + command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}') \ <(command grep -oE '^[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u From 7f5f6efbac677603181434193abbb2a8d550b8b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Gonz=C3=A1lez?= Date: Thu, 28 Sep 2017 15:23:44 +0200 Subject: [PATCH 634/658] [fzf-tmux] Executes fzf from fzf-tmux with a process name (#1056) --- bin/fzf-tmux | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/fzf-tmux b/bin/fzf-tmux index d7ce7a6..79407e6 100755 --- a/bin/fzf-tmux +++ b/bin/fzf-tmux @@ -178,14 +178,14 @@ if [[ -n "$term" ]] || [[ -t 0 ]]; then cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ - split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \ + split-window $opt "$envs bash -c 'cd $(printf %q "$PWD"); exec -a fzf bash $argsf'" $swap \ > /dev/null 2>&1 else mkfifo $fifo1 cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\ set-window-option remain-on-exit off \;\ - split-window $opt "$envs bash $argsf" $swap \ + split-window $opt "$envs bash -c 'exec -a fzf bash $argsf'" $swap \ > /dev/null 2>&1 cat <&0 > $fifo1 & fi From ee40212e9772941b49a4bdb1e95f99913cdea469 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 28 Sep 2017 23:05:02 +0900 Subject: [PATCH 635/658] Update FZF_DEFAULT_COMMAND - Use bash for `set -o pipefail` - Fall back to simpler find command when the original command failed Related: #1061 --- src/constants.go | 2 +- src/reader.go | 10 ++++++---- src/reader_test.go | 4 ++-- src/util/util_unix.go | 5 +++++ src/util/util_windows.go | 8 +++++++- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/constants.go b/src/constants.go index cfd3a3b..d544529 100644 --- a/src/constants.go +++ b/src/constants.go @@ -55,7 +55,7 @@ var defaultCommand string func init() { if !util.IsWindows() { - defaultCommand = `command find -L . -mindepth 1 \( -path '*/\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-` + defaultCommand = `set -o pipefail; (command find -L . -mindepth 1 \( -path '*/\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print || command find -L . -mindepth 1 -path '*/\.*' -prune -o -type f -print -o -type l -print) 2> /dev/null | cut -b3-` } else if os.Getenv("TERM") == "cygwin" { defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"` } else { diff --git a/src/reader.go b/src/reader.go index 22ce4ba..5fd6d87 100644 --- a/src/reader.go +++ b/src/reader.go @@ -56,9 +56,11 @@ func (r *Reader) ReadSource() { if util.IsTty() { cmd := os.Getenv("FZF_DEFAULT_COMMAND") if len(cmd) == 0 { - cmd = defaultCommand + // The default command for *nix requires bash + success = r.readFromCommand("bash", defaultCommand) + } else { + success = r.readFromCommand("sh", cmd) } - success = r.readFromCommand(cmd) } else { success = r.readFromStdin() } @@ -100,8 +102,8 @@ func (r *Reader) readFromStdin() bool { return true } -func (r *Reader) readFromCommand(cmd string) bool { - listCommand := util.ExecCommand(cmd) +func (r *Reader) readFromCommand(shell string, cmd string) bool { + listCommand := util.ExecCommandWith(shell, cmd) out, err := listCommand.StdoutPipe() if err != nil { return false diff --git a/src/reader_test.go b/src/reader_test.go index 82ca6b7..c29936c 100644 --- a/src/reader_test.go +++ b/src/reader_test.go @@ -23,7 +23,7 @@ func TestReadFromCommand(t *testing.T) { } // Normal command - reader.fin(reader.readFromCommand(`echo abc && echo def`)) + reader.fin(reader.readFromCommand("sh", `echo abc && echo def`)) if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" { t.Errorf("%s", strs) } @@ -48,7 +48,7 @@ func TestReadFromCommand(t *testing.T) { reader.startEventPoller() // Failing command - reader.fin(reader.readFromCommand(`no-such-command`)) + reader.fin(reader.readFromCommand("sh", `no-such-command`)) strs = []string{} if len(strs) > 0 { t.Errorf("%s", strs) diff --git a/src/util/util_unix.go b/src/util/util_unix.go index d538ee0..fc63c02 100644 --- a/src/util/util_unix.go +++ b/src/util/util_unix.go @@ -14,6 +14,11 @@ func ExecCommand(command string) *exec.Cmd { if len(shell) == 0 { shell = "sh" } + return ExecCommandWith(shell, command) +} + +// ExecCommandWith executes the given command with the specified shell +func ExecCommandWith(shell string, command string) *exec.Cmd { return exec.Command(shell, "-c", command) } diff --git a/src/util/util_windows.go b/src/util/util_windows.go index efd19a2..493f4d7 100644 --- a/src/util/util_windows.go +++ b/src/util/util_windows.go @@ -10,8 +10,14 @@ import ( "github.com/mattn/go-shellwords" ) -// ExecCommand executes the given command with $SHELL +// ExecCommand executes the given command with cmd func ExecCommand(command string) *exec.Cmd { + return ExecCommandWith("cmd", command) +} + +// ExecCommandWith executes the given command with cmd. _shell parameter is +// ignored on Windows. +func ExecCommandWith(_shell string, command string) *exec.Cmd { args, _ := shellwords.Parse(command) allArgs := make([]string, len(args)+1) allArgs[0] = "/c" From 077ae51f0511260ba2d2c793b776582d41fefd6a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 29 Sep 2017 00:36:09 +0900 Subject: [PATCH 636/658] [vim] Use Vim 8 terminal when appropriate Close #1055 --- README-VIM.md | 6 +++--- doc/fzf.txt | 18 ++++++++++++------ plugin/fzf.vim | 16 +++++++++------- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/README-VIM.md b/README-VIM.md index 3c8bab0..4913fc7 100644 --- a/README-VIM.md +++ b/README-VIM.md @@ -37,7 +37,7 @@ Note that the environment variables `FZF_DEFAULT_COMMAND` and - `g:fzf_action` - Customizable extra key bindings for opening selected files in different ways - `g:fzf_layout` - - Determines the size and position of fzf window (tmux pane or Neovim split) + - Determines the size and position of fzf window - `g:fzf_colors` - Customizes fzf colors to match the current color scheme - `g:fzf_history_dir` @@ -72,7 +72,7 @@ let g:fzf_action = { " - down / up / left / right let g:fzf_layout = { 'down': '~40%' } -" In Neovim, you can set up fzf window using a Vim command +" You can set up fzf window using a Vim command (Neovim or latest Vim 8 required) let g:fzf_layout = { 'window': 'enew' } let g:fzf_layout = { 'window': '-tabnew' } let g:fzf_layout = { 'window': '10split enew' } @@ -116,7 +116,7 @@ following options. | `options` | string/list | Options to fzf | | `dir` | string | Working directory | | `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) | -| `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) | +| `window` (Vim 8 / Neovim) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) | | `launcher` | string | External terminal emulator to start fzf with (GVim only) | | `launcher` | funcref | Function for generating `launcher` string (GVim only) | diff --git a/doc/fzf.txt b/doc/fzf.txt index a523903..d000114 100644 --- a/doc/fzf.txt +++ b/doc/fzf.txt @@ -1,4 +1,4 @@ -fzf.txt fzf Last change: August 14 2017 +fzf.txt fzf Last change: September 29 2017 FZF - TABLE OF CONTENTS *fzf* *fzf-toc* ============================================================================== @@ -61,7 +61,7 @@ Note that the environment variables `FZF_DEFAULT_COMMAND` and - Customizable extra key bindings for opening selected files in different ways - `g:fzf_layout` - - Determines the size and position of fzf window (tmux pane or Neovim split) + - Determines the size and position of fzf window - `g:fzf_colors` - Customizes fzf colors to match the current color scheme - `g:fzf_history_dir` @@ -97,7 +97,7 @@ Examples~ " - down / up / left / right let g:fzf_layout = { 'down': '~40%' } - " In Neovim, you can set up fzf window using a Vim command + " You can set up fzf window using a Vim command (Neovim or latest Vim 8 required) let g:fzf_layout = { 'window': 'enew' } let g:fzf_layout = { 'window': '-tabnew' } let g:fzf_layout = { 'window': '10split enew' } @@ -111,6 +111,7 @@ Examples~ \ 'bg+': ['bg', 'CursorLine', 'CursorColumn'], \ 'hl+': ['fg', 'Statement'], \ 'info': ['fg', 'PreProc'], + \ 'border': ['fg', 'Ignore'], \ 'prompt': ['fg', 'Conditional'], \ 'pointer': ['fg', 'Exception'], \ 'marker': ['fg', 'Keyword'], @@ -141,7 +142,7 @@ following options. `options` | string/list | Options to fzf `dir` | string | Working directory `up` / `down` / `left` / `right` | number/string | Use tmux pane with the given size (e.g. `20` , `50%` ) - `window` (Neovim only) | string | Command to open fzf window (e.g. `vertical aboveleft 30new` ) + `window` (Vim 8 / Neovim) | string | Command to open fzf window (e.g. `vertical aboveleft 30new` ) `launcher` | string | External terminal emulator to start fzf with (GVim only) `launcher` | funcref | Function for generating `launcher` string (GVim only) ---------------------------+---------------+-------------------------------------------------------------- @@ -169,8 +170,13 @@ function that decorates the options dictionary so that it understands GVIM *fzf-gvim* ============================================================================== -In GVim, you need an external terminal emulator to start fzf with. `xterm` -command is used by default, but you can customize it with `g:fzf_launcher`. +With the latest version of GVim, fzf will start inside the builtin terminal +emulator of Vim. Please note that this terminal feature of Vim is still young +and unstable and you may run into some issues. + +If you have an older version of GVim, you need an external terminal emulator +to start fzf with. `xterm` command is used by default, but you can customize +it with `g:fzf_launcher`. > " This is the default. %s is replaced with fzf command let g:fzf_launcher = 'xterm -e bash -ic %s' diff --git a/plugin/fzf.vim b/plugin/fzf.vim index c0bae81..bf80cfb 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -386,10 +386,11 @@ try endif let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) - let use_height = has_key(dict, 'down') && - \ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right')) && + let use_height = has_key(dict, 'down') && !has('gui_running') && + \ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right', 'window')) && \ executable('tput') && filereadable('/dev/tty') - let use_term = has('nvim-0.2.1') || (has('nvim') && !s:is_win) || (has('terminal') && has('patch-8.0.995') && (has('gui_running') || s:is_win)) + let has_term = has('nvim-0.2.1') || has('nvim') && !s:is_win || has('terminal') && has('patch-8.0.995') + let use_term = has_term && (has('gui_running') || s:is_win || !use_height && s:present(dict, 'down', 'up', 'left', 'right', 'window')) let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict) if prefer_tmux && use_tmux let use_height = 0 @@ -400,9 +401,6 @@ try let optstr .= ' --height='.height elseif use_term let optstr .= ' --no-height' - if !has('nvim') && !s:is_win - let optstr .= ' --bind ctrl-j:accept' - endif endif let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result @@ -699,7 +697,11 @@ function! s:execute_term(dict, command, temps) abort if has('nvim') call termopen(command, fzf) else - call term_start([&shell, &shellcmdflag, command], {'curwin': fzf.buf, 'exit_cb': function(fzf.on_exit)}) + let t = term_start([&shell, &shellcmdflag, command], {'curwin': fzf.buf, 'exit_cb': function(fzf.on_exit)}) + " FIXME: https://github.com/vim/vim/issues/1998 + if !has('nvim') && !s:is_win + call term_wait(t, 20) + endif endif finally if s:present(a:dict, 'dir') From c2614467cf79dcfaca58da30df70d4e6dc6b3792 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 30 Sep 2017 22:13:43 +0900 Subject: [PATCH 637/658] [neovim] Fix Neovim plugin to use terminal instead of --height Close #1066 Close #1068 --- plugin/fzf.vim | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index bf80cfb..bad7255 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -389,8 +389,10 @@ try let use_height = has_key(dict, 'down') && !has('gui_running') && \ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right', 'window')) && \ executable('tput') && filereadable('/dev/tty') - let has_term = has('nvim-0.2.1') || has('nvim') && !s:is_win || has('terminal') && has('patch-8.0.995') - let use_term = has_term && (has('gui_running') || s:is_win || !use_height && s:present(dict, 'down', 'up', 'left', 'right', 'window')) + let has_vim8_term = has('terminal') && has('patch-8.0.995') + let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win + let use_term = has_nvim_term || + \ has_vim8_term && (has('gui_running') || s:is_win || !use_height && s:present(dict, 'down', 'up', 'left', 'right', 'window')) let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict) if prefer_tmux && use_tmux let use_height = 0 From 1b1bc9ea36d76a8b0b84ee66c02b59097bb834be Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 8 Oct 2017 03:52:55 +0900 Subject: [PATCH 638/658] [install] Download arm8 binaries on Linux aarch64 Close #1060 --- install | 1 + 1 file changed, 1 insertion(+) diff --git a/install b/install index c6b4b8c..54c66d8 100755 --- a/install +++ b/install @@ -174,6 +174,7 @@ case "$archi" in Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6}.tgz ;; Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7}.tgz ;; Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;; + Linux\ aarch64*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;; FreeBSD\ *64) download fzf-$version-freebsd_${binary_arch:-amd64}.tgz ;; FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386}.tgz ;; OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64}.tgz ;; From 0580fe9046d038c359645f6e7ed99acfa45b2190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionel=20Cristian=20M=C4=83rie=C8=99?= Date: Tue, 10 Oct 2017 06:27:01 +0300 Subject: [PATCH 639/658] Don't do shell quoting for weird chars (#1079) * Don't do shell quoting for weird chars This would prevent tabs from being escaped as `$'\t'` (definitely not what I would want to see as initial value in the search). * Do different escape. --- shell/key-bindings.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index caa6c38..7064611 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -60,7 +60,7 @@ fzf-history-widget() { local selected num setopt localoptions noglobsubst noposixbuiltins pipefail 2> /dev/null selected=( $(fc -l 1 | - FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) ) + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) ) local ret=$? if [ -n "$selected" ]; then num=$selected[1] From c4185e81e86e339ae2c18e5d9b596b7992ec179b Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Mon, 2 Oct 2017 11:36:19 -0400 Subject: [PATCH 640/658] Fix ExecCommandWith for cmd.exe in Windows (#1072) Close #1018 Run the command as is in cmd.exe with no parsing and escaping. Explicity set cmd.SysProcAttr so execCommand does not escape the command. Technically, the command should be escaped with ^ for special characters, including ". This allows cmd.exe commands to be chained together. See https://github.com/neovim/neovim/pull/7343#issuecomment-333350201 This commit also updates quoteEntry to use strings.Replace instead of strconv.Quote which escapes more than \ and ". --- src/terminal.go | 11 ++++++++++- src/terminal_test.go | 19 +++++++++++++++++++ src/util/util_windows.go | 14 +++++++------- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/terminal.go b/src/terminal.go index 7813680..98d837c 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -1103,9 +1103,18 @@ func keyMatch(key int, event tui.Event) bool { event.Type == tui.Mouse && key == tui.DoubleClick && event.MouseEvent.Double } +func quoteEntryCmd(entry string) string { + escaped := strings.Replace(entry, `\`, `\\`, -1) + escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"` + r, _ := regexp.Compile(`[&|<>()@^%!"]`) + return r.ReplaceAllStringFunc(escaped, func(match string) string { + return "^" + match + }) +} + func quoteEntry(entry string) string { if util.IsWindows() { - return strconv.Quote(strings.Replace(entry, "\"", "\\\"", -1)) + return quoteEntryCmd(entry) } return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" } diff --git a/src/terminal_test.go b/src/terminal_test.go index d42d2b8..60f2b1a 100644 --- a/src/terminal_test.go +++ b/src/terminal_test.go @@ -91,3 +91,22 @@ func TestReplacePlaceholder(t *testing.T) { result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1) check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'") } + +func TestQuoteEntryCmd(t *testing.T) { + tests := map[string]string{ + `"`: `^"\^"^"`, + `\`: `^"\\^"`, + `\"`: `^"\\\^"^"`, + `"\\\"`: `^"\^"\\\\\\\^"^"`, + `&|<>()@^%!`: `^"^&^|^<^>^(^)^@^^^%^!^"`, + `%USERPROFILE%`: `^"^%USERPROFILE^%^"`, + `C:\Program Files (x86)\`: `^"C:\\Program Files ^(x86^)\\^"`, + } + + for input, expected := range tests { + escaped := quoteEntryCmd(input) + if escaped != expected { + t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped) + } + } +} diff --git a/src/util/util_windows.go b/src/util/util_windows.go index 493f4d7..86409fd 100644 --- a/src/util/util_windows.go +++ b/src/util/util_windows.go @@ -6,8 +6,6 @@ import ( "os" "os/exec" "syscall" - - "github.com/mattn/go-shellwords" ) // ExecCommand executes the given command with cmd @@ -18,11 +16,13 @@ func ExecCommand(command string) *exec.Cmd { // ExecCommandWith executes the given command with cmd. _shell parameter is // ignored on Windows. func ExecCommandWith(_shell string, command string) *exec.Cmd { - args, _ := shellwords.Parse(command) - allArgs := make([]string, len(args)+1) - allArgs[0] = "/c" - copy(allArgs[1:], args) - return exec.Command("cmd", allArgs...) + cmd := exec.Command("cmd") + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: false, + CmdLine: fmt.Sprintf(` /s /c "%s"`, command), + CreationFlags: 0, + } + return cmd } // IsWindows returns true on Windows From cb8e97274ecb84124e3e9243941b82c0e58fbd96 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 14 Oct 2017 16:18:46 +0900 Subject: [PATCH 641/658] Update README to add an example of _fzf_compgen_dir /cc @chrisjohnson Close #1067 Close #1083 --- README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f7ebb73..a6fe8ad 100644 --- a/README.md +++ b/README.md @@ -367,15 +367,26 @@ export FZF_COMPLETION_TRIGGER='~~' # Options to fzf command export FZF_COMPLETION_OPTS='+c -x' -# Use ag instead of the default find command for listing candidates. +# Use ag instead of the default find command for listing path candidates. # - The first argument to the function is the base path to start traversal -# - Note that ag only lists files not directories # - See the source code (completion.{bash,zsh}) for the details. +# - ag only lists files, so we use with-dir script to augment the output _fzf_compgen_path() { - ag -g "" "$1" + ag -g "" "$1" | with-dir "$1" +} + +# Use ag to generate the list for directory completion +_fzf_compgen_dir() { + ag -g "" "$1" | only-dir "$1" } ``` +`only-dir` and `with-dir` scripts can be found [here][dir-scripts]. They are +written in Ruby, but you should be able to rewrite them in any language you +prefer. + +[dir-scripts]: https://gist.github.com/junegunn/8c3796a965f22e6a803fe53096ad7a75 + #### Supported commands On bash, fuzzy completion is enabled only for a predefined set of commands From 6ae085f9745777dab85a38ff137c0e762bfab083 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Oct 2017 17:41:15 +0900 Subject: [PATCH 642/658] Add link to Windows wiki page Related: #1072 /cc @janlazo --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a6fe8ad..90250a1 100644 --- a/README.md +++ b/README.md @@ -125,10 +125,12 @@ available as a [Chocolatey package][choco]. choco install fzf ``` -However, other components of the project may not work on Windows. You might -want to consider installing fzf on [Windows Subsystem for Linux][wsl] where +However, other components of the project may not work on Windows. Known issues +and limitations can be found on [the wiki page][windows-wiki]. You might want +to consider installing fzf on [Windows Subsystem for Linux][wsl] where everything runs flawlessly. +[windows-wiki]: https://github.com/junegunn/fzf/wiki/Windows [wsl]: https://blogs.msdn.microsoft.com/wsl/ Upgrading fzf From 438f6c96cdc56e1bb3cc12daa16ae08b2ccae18b Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Oct 2017 18:32:59 +0900 Subject: [PATCH 643/658] Fix compilation error of Windows binary --- src/util/util_windows.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/util/util_windows.go b/src/util/util_windows.go index 86409fd..67b7031 100644 --- a/src/util/util_windows.go +++ b/src/util/util_windows.go @@ -3,6 +3,7 @@ package util import ( + "fmt" "os" "os/exec" "syscall" @@ -18,9 +19,9 @@ func ExecCommand(command string) *exec.Cmd { func ExecCommandWith(_shell string, command string) *exec.Cmd { cmd := exec.Command("cmd") cmd.SysProcAttr = &syscall.SysProcAttr{ - HideWindow: false, - CmdLine: fmt.Sprintf(` /s /c "%s"`, command), - CreationFlags: 0, + HideWindow: false, + CmdLine: fmt.Sprintf(` /s /c "%s"`, command), + CreationFlags: 0, } return cmd } From 64a6ced62e626895bc0d47cb124c4a56678a1273 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 15 Oct 2017 19:02:05 +0900 Subject: [PATCH 644/658] Do not immediately check --height option on Windows (#1082) --- src/options.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/options.go b/src/options.go index e2f33f8..00b2afd 100644 --- a/src/options.go +++ b/src/options.go @@ -833,9 +833,6 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec { } func parseHeight(str string) sizeSpec { - if util.IsWindows() { - errorExit("--height options is currently not supported on Windows") - } size := parseSize(str, 100, "height") return size } @@ -1203,6 +1200,9 @@ func parseOptions(opts *Options, allArgs []string) { } func postProcessOptions(opts *Options) { + if util.IsWindows() && opts.Height.size > 0 { + errorExit("--height option is currently not supported on Windows") + } // Default actions for CTRL-N / CTRL-P when --history is set if opts.History != nil { if _, prs := opts.Keymap[tui.CtrlP]; !prs { From 0b33dc6ce19357f7b32857dbb1fecd181fb899e8 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 16 Oct 2017 01:58:57 +0900 Subject: [PATCH 645/658] 0.17.1 --- CHANGELOG.md | 9 +++++++++ install | 2 +- man/man1/fzf-tmux.1 | 2 +- man/man1/fzf.1 | 2 +- src/constants.go | 2 +- 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05673b6..1c16772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ CHANGELOG ========= +0.17.1 +------ + +- Fixed custom background color of preview window (#1046) +- Fixed background color issues of Windows binary +- Fixed Windows binary to execute command using cmd.exe with no parsing and + escaping (#1072) +- Added support for `window` layout on Vim 8 using Vim 8 terminal (#1055) + 0.17.0-2 -------- diff --git a/install b/install index 54c66d8..b7f4107 100755 --- a/install +++ b/install @@ -2,7 +2,7 @@ set -u -version=0.17.0 +version=0.17.1 auto_completion= key_bindings= update_config=2 diff --git a/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1 index e73d83f..8de9f69 100644 --- a/man/man1/fzf-tmux.1 +++ b/man/man1/fzf-tmux.1 @@ -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 "Aug 2017" "fzf 0.17.0" "fzf-tmux - open fzf in tmux split pane" +.TH fzf-tmux 1 "Oct 2017" "fzf 0.17.1" "fzf-tmux - open fzf in tmux split pane" .SH NAME fzf-tmux - open fzf in tmux split pane diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 88a4a90..3cc6c7a 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 "Aug 2017" "fzf 0.17.0" "fzf - a command-line fuzzy finder" +.TH fzf 1 "Oct 2017" "fzf 0.17.1" "fzf - a command-line fuzzy finder" .SH NAME fzf - a command-line fuzzy finder diff --git a/src/constants.go b/src/constants.go index d544529..e6ed170 100644 --- a/src/constants.go +++ b/src/constants.go @@ -9,7 +9,7 @@ import ( const ( // Current version - version = "0.17.0" + version = "0.17.1" // Core coordinatorDelayMax time.Duration = 100 * time.Millisecond From 80a21f7a75d4b95a36c5743624c2ee25d329b2d9 Mon Sep 17 00:00:00 2001 From: Andrey Chernih Date: Thu, 19 Oct 2017 06:04:32 -0700 Subject: [PATCH 646/658] [completion] Fix known_hosts completion for custom port number (#1092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handles records like "[20.20.7.168]:9722 ssh-rsa ..." This is a standard format for servers running on custom port according to http://man.openbsd.org/sshd.8#SSH_KNOWN_HOSTS_FILE_FORMAT A hostname or address may optionally be enclosed within ‘[’ and ‘]’ brackets then followed by ‘:’ and a non-standard port number. --- shell/completion.bash | 2 +- shell/completion.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index 9e299ec..cd2b10b 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -234,7 +234,7 @@ _fzf_complete_telnet() { _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}') \ - <(command grep -oE '^[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ + <(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \ <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u ) diff --git a/shell/completion.zsh b/shell/completion.zsh index d3e64d8..e2ed0b1 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -117,7 +117,7 @@ _fzf_complete_telnet() { _fzf_complete_ssh() { _fzf_complete '+m' "$@" < <( command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}') \ - <(command grep -oE '^[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \ + <(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \ <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | awk '{if (length($2) > 0) {print $2}}' | sort -u ) From 3af63bcf1f203c9716a3f69e1264b8877e528941 Mon Sep 17 00:00:00 2001 From: Daniel Schaffrath Date: Fri, 20 Oct 2017 05:56:02 +0200 Subject: [PATCH 647/658] [zsh] Use fc -r instead of fzf --tac to speed up loadtime (#1097) Reference: http://zsh.sourceforge.net/Doc/Release/Shell-Builtin-Commands.html > The flag -r reverses the order of the events --- shell/key-bindings.zsh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index 7064611..aabe53a 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -59,8 +59,8 @@ bindkey '\ec' fzf-cd-widget fzf-history-widget() { local selected num setopt localoptions noglobsubst noposixbuiltins pipefail 2> /dev/null - selected=( $(fc -l 1 | - FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) ) + selected=( $(fc -rl 1 | + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) ) local ret=$? if [ -n "$selected" ]; then num=$selected[1] From eaf6eb897879becff31ea9b27046c0eff9e1e646 Mon Sep 17 00:00:00 2001 From: Igor Urazov Date: Sat, 21 Oct 2017 04:31:34 +0300 Subject: [PATCH 648/658] [completion] Ensure ps called as command (#1098) When `ps` is aliased for something uncommon, like `alias ps=grc ps` which colorizes ps output, the output of `ps` can be unexpected and/or undesired. This change makes ps to be always executed as command, even if it's aliased. --- shell/completion.bash | 2 +- shell/completion.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/completion.bash b/shell/completion.bash index cd2b10b..445df17 100644 --- a/shell/completion.bash +++ b/shell/completion.bash @@ -215,7 +215,7 @@ _fzf_complete_kill() { local selected fzf fzf="$(__fzfcmd_complete)" - selected=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ') + selected=$(command ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ') printf '\e[5n' if [ -n "$selected" ]; then diff --git a/shell/completion.zsh b/shell/completion.zsh index e2ed0b1..3c083aa 100644 --- a/shell/completion.zsh +++ b/shell/completion.zsh @@ -163,7 +163,7 @@ fzf-completion() { # Kill completion (do not require trigger sequence) if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then fzf="$(__fzfcmd_complete)" - matches=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ') + matches=$(command ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ') if [ -n "$matches" ]; then LBUFFER="$LBUFFER$matches" fi From 5784101beabcacfc86938f17ee3f2051e3b59816 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 23 Oct 2017 13:19:10 +0900 Subject: [PATCH 649/658] Suggest ripgrep instead of the silver searcher Since https://github.com/BurntSushi/ripgrep/issues/200 is fixed in 0.7.1, we can safely suggest ripgrep as the candidate generator as it has a more precise implementation of gitignore filtering than the silver searcher. --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 90250a1..f8f401d 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ or `py`. - `FZF_DEFAULT_COMMAND` - Default command to use when input is tty - - e.g. `export FZF_DEFAULT_COMMAND='ag -g ""'` + - e.g. `export FZF_DEFAULT_COMMAND='rg --files'` - `FZF_DEFAULT_OPTS` - Default options - e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"` @@ -369,17 +369,18 @@ export FZF_COMPLETION_TRIGGER='~~' # Options to fzf command export FZF_COMPLETION_OPTS='+c -x' -# Use ag instead of the default find command for listing path candidates. +# Use rg (https://github.com/BurntSushi/ripgrep) instead of the default find +# command for listing path candidates. # - The first argument to the function is the base path to start traversal # - See the source code (completion.{bash,zsh}) for the details. -# - ag only lists files, so we use with-dir script to augment the output +# - rg only lists files, so we use with-dir script to augment the output _fzf_compgen_path() { - ag -g "" "$1" | with-dir "$1" + rg --files "$1" | with-dir "$1" } -# Use ag to generate the list for directory completion +# Use rg to generate the list for directory completion _fzf_compgen_dir() { - ag -g "" "$1" | only-dir "$1" + rg --files "$1" | only-dir "$1" } ``` @@ -491,18 +492,17 @@ Tips #### Respecting `.gitignore`, `.hgignore`, and `svn:ignore` -[ag](https://github.com/ggreer/the_silver_searcher) or -[rg](https://github.com/BurntSushi/ripgrep) will do the -filtering: +[ripgrep](https://github.com/BurntSushi/ripgrep) or [the silver +searcher](https://github.com/ggreer/the_silver_searcher) can do the filtering: ```sh -# Feed the output of ag into fzf -ag -g "" | fzf +# Feed the output of rg into fzf +rg --files | fzf -# Setting ag as the default source for fzf -export FZF_DEFAULT_COMMAND='ag -g ""' +# Setting rg as the default source for fzf +export FZF_DEFAULT_COMMAND='rg --files' -# Now fzf (w/o pipe) will use ag instead of find +# Now fzf (w/o pipe) will use rg instead of find fzf # To apply the command to CTRL-T as well @@ -512,7 +512,7 @@ export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" If you don't want to exclude hidden files, use the following command: ```sh -export FZF_DEFAULT_COMMAND='ag --hidden --ignore .git -g ""' +export FZF_DEFAULT_COMMAND='rg --files --hidden --glob \!.git' ``` #### `git ls-tree` for fast traversal From fe7b91dfd9831433aca2c5e5e744f6a188b401be Mon Sep 17 00:00:00 2001 From: Adam Dinwoodie Date: Fri, 27 Oct 2017 01:12:12 +0100 Subject: [PATCH 650/658] Add bin/fzf.exe to .gitignore (#1111) On Cygwin and MinGW, the fzf binary will have a .exe extension, so ignore that binary if it exists as well as the bare binary. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d4cc4a9..f796f59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ bin/fzf +bin/fzf.exe target pkg Gemfile.lock From a6d2ab33609ad20ce107e66d9648d908800fa3ad Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 29 Oct 2017 23:34:26 +0900 Subject: [PATCH 651/658] Update README: Examples using fd - https://github.com/sharkdp/fd - https://mike.place/2017/fzf-fd/ /cc @williamsmj --- README.md | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index f8f401d..ba54bc5 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ or `py`. - `FZF_DEFAULT_COMMAND` - Default command to use when input is tty - - e.g. `export FZF_DEFAULT_COMMAND='rg --files'` + - e.g. `export FZF_DEFAULT_COMMAND='fd --type f'` - `FZF_DEFAULT_OPTS` - Default options - e.g. `export FZF_DEFAULT_OPTS="--reverse --inline-info"` @@ -369,27 +369,20 @@ export FZF_COMPLETION_TRIGGER='~~' # Options to fzf command export FZF_COMPLETION_OPTS='+c -x' -# Use rg (https://github.com/BurntSushi/ripgrep) instead of the default find +# Use fd (https://github.com/sharkdp/fd) instead of the default find # command for listing path candidates. -# - The first argument to the function is the base path to start traversal +# - The first argument to the function ($1) is the base path to start traversal # - See the source code (completion.{bash,zsh}) for the details. -# - rg only lists files, so we use with-dir script to augment the output _fzf_compgen_path() { - rg --files "$1" | with-dir "$1" + fd --hidden --follow --exclude ".git" . "$1" } -# Use rg to generate the list for directory completion +# Use fd to generate the list for directory completion _fzf_compgen_dir() { - rg --files "$1" | only-dir "$1" + fd --type d --hidden --follow --exclude ".git" . "$1" } ``` -`only-dir` and `with-dir` scripts can be found [here][dir-scripts]. They are -written in Ruby, but you should be able to rewrite them in any language you -prefer. - -[dir-scripts]: https://gist.github.com/junegunn/8c3796a965f22e6a803fe53096ad7a75 - #### Supported commands On bash, fuzzy completion is enabled only for a predefined set of commands @@ -490,29 +483,33 @@ For more advanced examples, see [Key bindings for git with fzf][fzf-git]. Tips ---- -#### Respecting `.gitignore`, `.hgignore`, and `svn:ignore` +#### Respecting `.gitignore` -[ripgrep](https://github.com/BurntSushi/ripgrep) or [the silver -searcher](https://github.com/ggreer/the_silver_searcher) can do the filtering: +You can use [fd](https://github.com/sharkdp/fd), +[ripgrep](https://github.com/BurntSushi/ripgrep), or [the silver +searcher](https://github.com/ggreer/the_silver_searcher) instead of the +default find command to traverse the file system while respecting +`.gitignore`. ```sh -# Feed the output of rg into fzf -rg --files | fzf +# Feed the output of fd into fzf +fd --type f | fzf -# Setting rg as the default source for fzf -export FZF_DEFAULT_COMMAND='rg --files' +# Setting fd as the default source for fzf +export FZF_DEFAULT_COMMAND='fd --type f' -# Now fzf (w/o pipe) will use rg instead of find +# Now fzf (w/o pipe) will use fd instead of find fzf # To apply the command to CTRL-T as well export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND" ``` -If you don't want to exclude hidden files, use the following command: +If you want the command to follow symbolic links, and don't want it to exclude +hidden files, use the following command: ```sh -export FZF_DEFAULT_COMMAND='rg --files --hidden --glob \!.git' +export FZF_DEFAULT_COMMAND='fd --type f --hidden --follow --exclude .git' ``` #### `git ls-tree` for fast traversal From 8068c975c2ee812ef1762bf2c21b948f03da210d Mon Sep 17 00:00:00 2001 From: Justin Toniazzo Date: Wed, 8 Nov 2017 09:54:46 -0500 Subject: [PATCH 652/658] Fix broken link in readme TOC (#1131) The `Respecting .gitignore` link pointed to a section of the readme which no longer exists. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ba54bc5..a7c193a 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Table of Contents * [Executing external programs](#executing-external-programs) * [Preview window](#preview-window) * [Tips](#tips) - * [Respecting .gitignore, .hgignore, and svn:ignore](#respecting-gitignore-hgignore-and-svnignore) + * [Respecting .gitignore](#respecting-gitignore) * [git ls-tree for fast traversal](#git-ls-tree-for-fast-traversal) * [Fish shell](#fish-shell) * [License](#license) From fd03aabeb2364924ee6bbdf993a3ba8f927855de Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 13 Nov 2017 22:40:17 -0500 Subject: [PATCH 653/658] Add Fedora installation information (#1141) --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index a7c193a..7c256c3 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Table of Contents * [Using git](#using-git) * [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew) * [As Vim plugin](#as-vim-plugin) + * [Fedora](#fedora) * [Windows](#windows) * [Upgrading fzf](#upgrading-fzf) * [Building fzf](#building-fzf) @@ -114,6 +115,20 @@ But it's recommended that you use a plugin manager like Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' } ``` +### Fedora + +fzf is available in Fedora 26 and above, and can be installed using the usual +method: + +```sh +sudo dnf install fzf +``` + +Shell completion and plugins for vim or neovim are enabled by default. Shell +key bindings are installed but not enabled by default. See Fedora's package +documentation for more information. + + ### Windows Pre-built binaries for Windows can be downloaded [here][bin]. fzf is also From 2cd0d4a9f7a0a0a86c1ee70da9fb970ec18eac50 Mon Sep 17 00:00:00 2001 From: Aaron Jensen Date: Mon, 13 Nov 2017 19:43:52 -0800 Subject: [PATCH 654/658] [zsh] Fire zsh precmd functions after cd (#1136) Fixes #915 --- shell/key-bindings.zsh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh index aabe53a..c25368c 100644 --- a/shell/key-bindings.zsh +++ b/shell/key-bindings.zsh @@ -36,6 +36,16 @@ fzf-file-widget() { zle -N fzf-file-widget bindkey '^T' fzf-file-widget +# Ensure precmds are run after cd +fzf-redraw-prompt() { + local precmd + for precmd in $precmd_functions; do + $precmd + done + zle reset-prompt +} +zle -N fzf-redraw-prompt + # ALT-C - cd into the selected directory fzf-cd-widget() { local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ @@ -48,7 +58,7 @@ fzf-cd-widget() { fi cd "$dir" local ret=$? - zle reset-prompt + zle fzf-redraw-prompt typeset -f zle-line-init >/dev/null && zle zle-line-init return $ret } From fc1b119159f24136e64a85a8ea3dd51f19161602 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 19 Nov 2017 12:07:03 +0900 Subject: [PATCH 655/658] [vim] Add instruction to hide statusline of terminal buffer (#1143) --- README-VIM.md | 22 ++++++++++++++++++++++ doc/fzf.txt | 27 ++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/README-VIM.md b/README-VIM.md index 4913fc7..ccb9bf7 100644 --- a/README-VIM.md +++ b/README-VIM.md @@ -142,6 +142,28 @@ command! -bang MyStuff \ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, 0)) ``` +fzf inside terminal buffer +-------------------------- + +The latest versions of Vim and Neovim include builtin terminal emulator +(`:terminal`) and fzf will start in a terminal buffer in the following cases: + +- On Neovim +- On GVim +- On Terminal Vim with the non-default layout + - `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}` + +### Hide statusline + +When fzf starts in a terminal buffer, you may want to hide the statusline of +the containing buffer. + +```vim +autocmd! FileType fzf +autocmd FileType fzf set laststatus=0 noshowmode noruler + \| autocmd BufLeave set laststatus=2 showmode ruler +``` + GVim ---- diff --git a/doc/fzf.txt b/doc/fzf.txt index d000114..387dc04 100644 --- a/doc/fzf.txt +++ b/doc/fzf.txt @@ -1,4 +1,4 @@ -fzf.txt fzf Last change: September 29 2017 +fzf.txt fzf Last change: November 19 2017 FZF - TABLE OF CONTENTS *fzf* *fzf-toc* ============================================================================== @@ -8,6 +8,8 @@ FZF - TABLE OF CONTENTS *fzf* *fzf-to Examples fzf#run fzf#wrap + fzf inside terminal buffer + Hide statusline GVim License @@ -167,6 +169,29 @@ function that decorates the options dictionary so that it understands \ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, 0)) < +FZF INSIDE TERMINAL BUFFER *fzf-inside-terminal-buffer* +============================================================================== + +The latest versions of Vim and Neovim include builtin terminal emulator +(`:terminal`) and fzf will start in a terminal buffer in the following cases: + + - On Neovim + - On GVim + - On Terminal Vim with the non-default layout + - `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}` + + +< Hide statusline >___________________________________________________________~ + *fzf-hide-statusline* + +When fzf starts in a terminal buffer, you may want to hide the statusline of +the containing buffer. +> + autocmd! FileType fzf + autocmd FileType fzf set laststatus=0 noshowmode noruler + \| autocmd BufLeave set laststatus=2 showmode ruler +< + GVIM *fzf-gvim* ============================================================================== From 6b27554cdb4742b3776dc1a7a4b7590c640e7779 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 22 Nov 2017 02:58:54 +0900 Subject: [PATCH 656/658] Clarify installation instructions --- README.md | 62 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 7c256c3..f749049 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ Table of Contents ----------------- * [Installation](#installation) - * [Using git](#using-git) * [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew) + * [Using git](#using-git) * [As Vim plugin](#as-vim-plugin) * [Fedora](#fedora) * [Windows](#windows) @@ -74,20 +74,10 @@ stuff. [bin]: https://github.com/junegunn/fzf-bin/releases -### Using git - -Clone this repository and run -[install](https://github.com/junegunn/fzf/blob/master/install) script. - -```sh -git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf -~/.fzf/install -``` - ### Using Homebrew or Linuxbrew -Alternatively, you can use [Homebrew](http://brew.sh/) or -[Linuxbrew](http://linuxbrew.sh/) to install fzf. +You can use [Homebrew](http://brew.sh/) or [Linuxbrew](http://linuxbrew.sh/) +to install fzf. ```sh brew install fzf @@ -96,23 +86,49 @@ brew install fzf $(brew --prefix)/opt/fzf/install ``` -### As Vim plugin +### Using git -You can manually add the directory to `&runtimepath` as follows, +Alternatively, you can "git clone" this repository to any directory and run +[install](https://github.com/junegunn/fzf/blob/master/install) script. -```vim -" If installed using git -set rtp+=~/.fzf - -" If installed using Homebrew -set rtp+=/usr/local/opt/fzf +```sh +git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf +~/.fzf/install ``` -But it's recommended that you use a plugin manager like -[vim-plug](https://github.com/junegunn/vim-plug). +### As Vim plugin + +Once you have fzf installed, you can enable it inside Vim simply by adding the +directory to `&runtimepath` as follows: ```vim +" If installed using Homebrew +set rtp+=/usr/local/opt/fzf + +" If installed using git +set rtp+=~/.fzf +``` + +If you use [vim-plug](https://github.com/junegunn/vim-plug), the same can be +written as: + +```vim +" If installed using Homebrew +Plug '/usr/local/opt/fzf' + +" If installed using git +Plug '~/.fzf' +``` + +But instead of separately installing fzf on your system (using Homebrew or +"git clone") and enabling it on Vim (adding it to `&runtimepath`), you can use +vim-plug to do both. + +```vim +" PlugInstall and PlugUpdate will clone fzf in ~/.fzf and run install script Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' } + " Both options are optional. You don't have to install fzf in ~/.fzf + " and you don't have to run install script if you use fzf only in Vim. ``` ### Fedora From 37d4015d5611576e5d93af524b6420956d8c7c9a Mon Sep 17 00:00:00 2001 From: Jan Edmund Lazo Date: Tue, 21 Nov 2017 23:34:02 -0500 Subject: [PATCH 657/658] [vim] Don't use :terminal on msys2 or Cygwin (#1155) Close #1152 msys2 terminal Vim assumes that it runs in mintty so `:terminal` uses `TERM=xterm`. fzf doesn't support `TERM=xterm` on Windows. --- plugin/fzf.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/fzf.vim b/plugin/fzf.vim index bad7255..0dbfe90 100644 --- a/plugin/fzf.vim +++ b/plugin/fzf.vim @@ -392,7 +392,7 @@ try let has_vim8_term = has('terminal') && has('patch-8.0.995') let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win let use_term = has_nvim_term || - \ has_vim8_term && (has('gui_running') || s:is_win || !use_height && s:present(dict, 'down', 'up', 'left', 'right', 'window')) + \ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || !use_height && s:present(dict, 'down', 'up', 'left', 'right', 'window')) let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict) if prefer_tmux && use_tmux let use_height = 0 From 1e5bd55672e92088504b0323669e9c120678d3c5 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 27 Nov 2017 15:44:19 +0900 Subject: [PATCH 658/658] [install] Change the order of case patterns for $archi (#1060) /cc @ehandal --- install | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install b/install index b7f4107..d3e21e1 100755 --- a/install +++ b/install @@ -168,13 +168,13 @@ binary_error="" case "$archi" in Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64}.tgz ;; Darwin\ *86) download fzf-$version-darwin_${binary_arch:-386}.tgz ;; - Linux\ *64) download fzf-$version-linux_${binary_arch:-amd64}.tgz ;; - Linux\ *86) download fzf-$version-linux_${binary_arch:-386}.tgz ;; Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5}.tgz ;; Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6}.tgz ;; Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7}.tgz ;; Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;; Linux\ aarch64*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;; + Linux\ *64) download fzf-$version-linux_${binary_arch:-amd64}.tgz ;; + Linux\ *86) download fzf-$version-linux_${binary_arch:-386}.tgz ;; FreeBSD\ *64) download fzf-$version-freebsd_${binary_arch:-amd64}.tgz ;; FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386}.tgz ;; OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64}.tgz ;;