diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..a0c8554
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,30 @@
+
+- Category
+ - [ ] fzf binary
+ - [ ] fzf-tmux script
+ - [ ] Key bindings
+ - [ ] Completion
+ - [ ] Vim
+ - [ ] Neovim
+ - [ ] Etc.
+- OS
+ - [ ] Linux
+ - [ ] Mac OS X
+ - [ ] Windows
+ - [ ] Windows Subsystem for Linux
+ - [ ] Etc.
+- Shell
+ - [ ] bash
+ - [ ] zsh
+ - [ ] fish
+
+
+
diff --git a/.gitignore b/.gitignore
index 0915467..f796f59 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,9 @@
-bin
-src/fzf/fzf_*
+bin/fzf
+bin/fzf.exe
+target
pkg
Gemfile.lock
.DS_Store
+doc/tags
+vendor
+gopath
diff --git a/.travis.yml b/.travis.yml
index cbd5c11..0a368fd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,24 +1,20 @@
language: ruby
-rvm:
-- 2.2.0
+dist: trusty
+sudo: required
+matrix:
+ include:
+ - env: TAGS=
+ rvm: 2.3.3
+# - env: TAGS=tcell
+# rvm: 2.3.3
install:
-- sudo apt-get update
-- sudo apt-get install -y libncurses-dev lib32ncurses5-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: |
- 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
new file mode 100644
index 0000000..390d8c2
--- /dev/null
+++ b/BUILD.md
@@ -0,0 +1,53 @@
+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
+# 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 in target
+make release
+
+# Make release archives for all supported platforms in target
+make release-all
+```
+
+### Using `go get`
+
+Alternatively, you can build fzf directly with `go get` command without
+manually cloning the repository.
+
+```sh
+go get -u github.com/junegunn/fzf
+```
+
+Third-party libraries used
+--------------------------
+
+- [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)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f10cbca..1c16772 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,354 @@
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
+--------
+
+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
+- 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
+- Fixed missing preview update
+
+0.16.10
+-------
+- Fixed invalid handling of ANSI colors in preview window
+- Further improved `--ansi` performance
+
+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`
+ - `fzf --bind change:top`
+ - Move cursor to the top result whenever the query string is changed
+ - `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
+- 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
+- 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
+- Added `--no-clear` option for scripting purposes
+
+0.16.5
+------
+- Minor bug fixes
+- Added `toggle-preview-wrap` action
+- Built with Go 1.8
+
+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
+ 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}'`
+- 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)'`
+- `ctrl-space` is allowed in `--bind`
+
+0.16.2
+------
+- 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
+------
+- 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*
+ - 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
+ 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
+------
+- 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/BUILD.md
+
+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
+
+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
+ - 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
+ - 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
+- Fixed race condition in `toggle-preview`
+
+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
+- Fixed rendering delay when displaying extremely long lines
+
+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
+- `--read0` to read input delimited by ASCII NUL character
+- `--print0` to print output delimited by ASCII NUL character
+
+0.13.5
+------
+- Memory and performance optimization
+ - Up to 2x performance with half the amount of memory
+
+0.13.4
+------
+- Performance optimization
+ - Memory footprint for ascii string is reduced by 60%
+ - 15 to 20% improvement of query performance
+ - Up to 45% better performance of `--nth` with non-regex delimiters
+- Fixed invalid handling of `hidden` property of `--preview-window`
+
+0.13.3
+------
+- Fixed duplicate rendering of the last line in preview window
+
+0.13.2
+------
+- Fixed race condition where preview window is not properly cleared
+
+0.13.1
+------
+- Fixed UI issue with large `--preview` output with many ANSI codes
+
+0.13.0
+------
+- 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
+------
+
+- 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
+------
+
+- 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
+------
+
+- Enhanced ranking algorithm
+- Minor bug fixes
+
+0.11.4
+------
+
+- Added `--hscroll-off=COL` option (default: 10) (#513)
+- Some fixes in Vim plugin and shell extensions
+
+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
+------
+
+- `--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
+- 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
+ - 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
+------
+
+- Added `--tabstop=SPACES` option
+
+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
+------
+
+- 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
+- More robust handling of SIGWINCH
+
+0.10.8
+------
+
+- Fixed panic when trying to set colors after colors are disabled (#370)
+
+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
------
@@ -9,7 +357,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/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/Makefile b/Makefile
new file mode 100644
index 0000000..5c15f69
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,138 @@
+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
+
+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) $(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
+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
+
+# 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 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)
+ 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 -sf $(ROOT_DIR)/src $(SRC_LINK)
+
+$(VENDOR_LINK):
+ mkdir -p $(shell dirname $(VENDOR_LINK))
+ ln -sf $(ROOT_DIR)/vendor $(VENDOR_LINK)
+
+vendor: $(GLIDE_YAML)
+ go get -u github.com/Masterminds/glide && $(GOPATH)/bin/glide install && touch $@
+
+test: $(SOURCES) vendor
+ 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) vendor
+ GOARCH=386 go build $(BUILD_FLAGS) -o $@
+
+target/$(BINARY64): $(SOURCES) vendor
+ GOARCH=amd64 go build $(BUILD_FLAGS) -o $@
+
+# https://github.com/golang/go/wiki/GoArm
+target/$(BINARYARM5): $(SOURCES) vendor
+ GOARCH=arm GOARM=5 go build $(BUILD_FLAGS) -o $@
+
+target/$(BINARYARM6): $(SOURCES) vendor
+ GOARCH=arm GOARM=6 go build $(BUILD_FLAGS) -o $@
+
+target/$(BINARYARM7): $(SOURCES) vendor
+ GOARCH=arm GOARM=7 go build $(BUILD_FLAGS) -o $@
+
+target/$(BINARYARM8): $(SOURCES) vendor
+ GOARCH=arm64 go build $(BUILD_FLAGS) -o $@
+
+bin/fzf: target/$(BINARY) | bin
+ cp -f target/$(BINARY) bin/fzf
+
+.PHONY: all release release-all test install clean
diff --git a/README-VIM.md b/README-VIM.md
new file mode 100644
index 0000000..ccb9bf7
--- /dev/null
+++ b/README-VIM.md
@@ -0,0 +1,196 @@
+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
+- `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' }
+
+" 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%' }
+
+" 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' }
+
+" 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'],
+ \ 'border': ['fg', 'Ignore'],
+ \ '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/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` (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) |
+
+`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`
+----------
+
+`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))
+```
+
+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
+----
+
+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
+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 956dd1e..f749049 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,66 @@
- [![travis-ci](https://travis-ci.org/junegunn/fzf.svg?branch=master)](https://travis-ci.org/junegunn/fzf)
+ [![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.
-![](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
- - 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
- Batteries included
- Vim/Neovim plugin, key bindings and fuzzy auto-completion
+Table of Contents
+-----------------
+
+ * [Installation](#installation)
+ * [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew)
+ * [Using git](#using-git)
+ * [As Vim plugin](#as-vim-plugin)
+ * [Fedora](#fedora)
+ * [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)
+ * [Advanced topics](#advanced-topics)
+ * [Performance](#performance)
+ * [Executing external programs](#executing-external-programs)
+ * [Preview window](#preview-window)
+ * [Tips](#tips)
+ * [Respecting .gitignore](#respecting-gitignore)
+ * [git ls-tree for fast traversal](#git-ls-tree-for-fast-traversal)
+ * [Fish shell](#fish-shell)
+ * [License](#license)
+
Installation
------------
-fzf project consists of the followings:
+fzf project consists of the following components:
- `fzf` executable
- `fzf-tmux` script for launching fzf in a tmux pane
@@ -30,14 +69,26 @@ fzf project consists of the followings:
- 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 Homebrew or Linuxbrew
-Clone this repository and run
+You can use [Homebrew](http://brew.sh/) or [Linuxbrew](http://linuxbrew.sh/)
+to install fzf.
+
+```sh
+brew install fzf
+
+# To install useful key bindings and fuzzy completion:
+$(brew --prefix)/opt/fzf/install
+```
+
+### Using git
+
+Alternatively, you can "git clone" this repository to any directory and run
[install](https://github.com/junegunn/fzf/blob/master/install) script.
```sh
@@ -45,46 +96,95 @@ git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install
```
-#### Using Homebrew
+### As Vim plugin
-On OS X, you can use [Homebrew](http://brew.sh/) to install fzf.
-
-```sh
-brew reinstall --HEAD fzf
-
-# Install shell extensions
-/usr/local/Cellar/fzf/HEAD/install
-```
-
-#### Install as Vim plugin
-
-Once you have cloned the repository, add the following line to your .vimrc.
+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
```
-Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf
-(recommended):
+If you use [vim-plug](https://github.com/junegunn/vim-plug), the same can be
+written as:
```vim
-Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' }
+" If installed using Homebrew
+Plug '/usr/local/opt/fzf'
+
+" If installed using git
+Plug '~/.fzf'
```
-#### Upgrading 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
+
+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
+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. 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
+-------------
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 reinstall --HEAD fzf`
+- brew: `brew update; brew reinstall fzf`
+- chocolatey: `choco upgrade fzf`
- vim-plug: `:PlugUpdate fzf`
+Building fzf
+------------
+
+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
@@ -101,39 +201,75 @@ 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
- Mouse: scroll, click, double-click; shift-click and shift-scroll on
multi-select mode
-#### Extended-search mode
+#### Layout
-With `-x` or `--extended` option, fzf will start in "extended-search mode".
+fzf by default starts in fullscreen mode, but you can make it start below the
+cursor with `--height` option.
-In this mode, you can specify multiple patterns delimited by spaces,
-such as: `^music .mp3$ sbtrkt !rmx`
+```sh
+vim $(fzf --height 40%)
+```
-| 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 |
+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. For example,
+
+```sh
+export FZF_DEFAULT_OPTS='--height 40% --reverse --border'
+```
+
+#### Search syntax
+
+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
+!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 `--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.
+
+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`
- Default command to use when input is tty
+ - e.g. `export FZF_DEFAULT_COMMAND='fd --type f'`
- `FZF_DEFAULT_OPTS`
- - Default options. e.g. `export FZF_DEFAULT_OPTS="--extended --cycle"`
+ - 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
--------
@@ -142,27 +278,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
-----------------
@@ -182,6 +297,40 @@ 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
+-----------------------------
+
+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
+ - Set `FZF_CTRL_T_OPTS` to pass additional options
+- `CTRL-R` - Paste the selected command from history onto the command line
+ - 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
+ - Set `FZF_ALT_C_OPTS` to pass additional options
+
+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
+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
---------------------------------
@@ -250,103 +399,150 @@ export FZF_COMPLETION_TRIGGER='~~'
# Options to fzf command
export FZF_COMPLETION_OPTS='+c -x'
+
+# Use fd (https://github.com/sharkdp/fd) instead of the default find
+# command for listing path candidates.
+# - The first argument to the function ($1) is the base path to start traversal
+# - See the source code (completion.{bash,zsh}) for the details.
+_fzf_compgen_path() {
+ fd --hidden --follow --exclude ".git" . "$1"
+}
+
+# Use fd to generate the list for directory completion
+_fzf_compgen_dir() {
+ fd --type d --hidden --follow --exclude ".git" . "$1"
+}
```
-Usage as Vim plugin
--------------------
+#### Supported commands
-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.
+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.
-(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 -m /tmp
-
-" Bang version starts in fullscreen instead of using tmux pane or Neovim split
-:FZF!
+```sh
+complete -F _fzf_path_completion -o default -o bashdefault ag
+complete -F _fzf_dir_completion -o default -o bashdefault tree
```
-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.
+Vim plugin
+----------
-Note that the environment variables `FZF_DEFAULT_COMMAND` and
-`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for
-customization.
+See [README-VIM.md](README-VIM.md).
-[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-FZF-command-(vim)
+Advanced topics
+---------------
-#### `fzf#run([options])`
+### Performance
-For more advanced uses, you can use `fzf#run()` function with the following
-options.
+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.
-| 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) |
+- `--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.
-Examples can be found on [the wiki
-page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
+[perf]: https://junegunn.kr/images/fzf-0.17.0.png
+
+### Executing external programs
+
+You can set up key bindings for starting external processes without leaving
+fzf (`execute`, `execute-silent`).
+
+```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'
+```
+
+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.
+
+```bash
+# {} 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.
+
+```bash
+# 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
+
+```bash
+# 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,
+
+```bash
+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
----
-#### Rendering issues
+#### Respecting `.gitignore`
-If you have any rendering issues, check the followings:
-
-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
-[pt](https://github.com/monochromegane/the_platinum_searcher) will 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 ag into fzf
-ag -l -g "" | fzf
+# Feed the output of fd into fzf
+fd --type f | fzf
-# Setting ag as the default source for fzf
-export FZF_DEFAULT_COMMAND='ag -l -g ""'
+# Setting fd as the default source for fzf
+export FZF_DEFAULT_COMMAND='fd --type f'
-# Now fzf (w/o pipe) will use ag 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 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='fd --type f --hidden --follow --exclude .git'
+```
+
#### `git ls-tree` for fast traversal
If you're running fzf in a large git repository, `git ls-tree` can boost up the
@@ -355,26 +551,49 @@ 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
-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.
+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 > $TMPDIR/fzf.result; and vim (cat $TMPDIR/fzf.result)
+fzf | read -l result; and vim $result
```
-License
--------
+or, for multiple results:
-[MIT](LICENSE)
+```sh
+fzf -m | while read -l r; set result $result $r; end; and vim $result
+```
-Author
-------
+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
-Junegunn Choi
+```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 -g FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'"
+```
+
+[License](LICENSE)
+------------------
+
+The MIT License (MIT)
+
+Copyright (c) 2017 Junegunn Choi
diff --git a/bin/fzf-tmux b/bin/fzf-tmux
index 239c878..79407e6 100755
--- a/bin/fzf-tmux
+++ b/bin/fzf-tmux
@@ -2,24 +2,52 @@
# fzf-tmux: starts fzf in a tmux pane
# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
+fail() {
+ >&2 echo "$1"
+ exit 2
+}
+
+fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
+[[ -x "$fzf" ]] || fail 'fzf executable not found'
+
args=()
opt=""
skip=""
swap=""
close=""
term=""
-while [ $# -gt 0 ]; do
+[[ -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]
+
+ 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
@@ -35,35 +63,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)
+ max=$columns
else
- [ -n "$LINES" ] && max=$LINES || max=$(tput lines)
+ 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"
@@ -74,63 +100,93 @@ 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_PANE" ] || tmux list-panes -F '#F' | grep -q Z; then
- fzf "${args[@]}"
+if [[ -z "$TMUX" || "$opt" =~ ^-h && "$columns" -le 40 || ! "$opt" =~ ^-h && "$lines" -le 15 ]]; then
+ "$fzf" "${args[@]}"
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
+ 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
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
-}
-trap cleanup EXIT SIGINT SIGTERM
+ \rm -f $argsf $fifo1 $fifo2 $fifo3
-fail() {
- >&2 echo "$1"
- exit 2
+ # 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 \; \
+ resize-pane -Z
+ fi
+
+ if [ $# -gt 0 ]; then
+ trap - EXIT
+ exit 130
+ fi
}
-fzf="$(which fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
-[ -x "$fzf" ] || fail "fzf executable not found"
+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")"
-[ -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 $fifo2
-mkfifo $fifo3
+mkfifo -m o+w $fifo2
+mkfifo -m o+w $fifo3
# Build arguments to fzf
opts=""
for arg in "${args[@]}"; do
+ arg="${arg//\\/\\\\}"
arg="${arg//\"/\\\"}"
arg="${arg//\`/\\\`}"
+ arg="${arg//$/\\$}"
opts="$opts \"$arg\""
done
-if [ -n "$term" -o -t 0 ]; then
- cat <<< "$fzf $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf
- tmux set-window-option -q synchronize-panes off \;\
- split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap
+pppid=$$
+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
+ 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 -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 set-window-option -q synchronize-panes off \;\
- split-window $opt "$envs bash $argsf" $swap
+ 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 -c 'exec -a fzf bash $argsf'" $swap \
+ > /dev/null 2>&1
cat <&0 > $fifo1 &
fi
cat $fifo2
diff --git a/doc/fzf.txt b/doc/fzf.txt
new file mode 100644
index 0000000..387dc04
--- /dev/null
+++ b/doc/fzf.txt
@@ -0,0 +1,226 @@
+fzf.txt fzf Last change: November 19 2017
+FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
+==============================================================================
+
+ FZF Vim integration
+ :FZF[!]
+ Configuration
+ Examples
+ fzf#run
+ fzf#wrap
+ fzf inside terminal buffer
+ Hide statusline
+ 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
+ - `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' }
+
+ " 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%' }
+
+ " 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' }
+
+ " 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'],
+ \ 'border': ['fg', 'Ignore'],
+ \ '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/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` (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)
+ ---------------------------+---------------+--------------------------------------------------------------
+
+`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*
+==============================================================================
+
+`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))
+<
+
+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*
+==============================================================================
+
+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'
+
+ " 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:
diff --git a/fzf b/fzf
deleted file mode 100755
index 17f86b5..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 Defaults 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/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/install b/install
index 9203c98..d3e21e1 100755
--- a/install
+++ b/install
@@ -1,28 +1,86 @@
#!/usr/bin/env bash
-[[ "$@" =~ --pre ]] && version=0.10.5 pre=1 ||
- version=0.10.5 pre=0
+set -u
-cd $(dirname $BASH_SOURCE)
-fzf_base=$(pwd)
+version=0.17.1
+auto_completion=
+key_bindings=
+update_config=2
+binary_arch=
+allow_legacy=
+shells="bash zsh fish"
-# If stdin is a tty, we are "interactive".
-[ -t 0 ] && interactive=yes
+help() {
+ cat << EOF
+usage: $0 [OPTIONS]
+
+ --help Show this message
+ --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
+
+ --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
+}
+
+for opt in "$@"; do
+ case $opt in
+ --help)
+ help
+ exit 0
+ ;;
+ --all)
+ auto_completion=1
+ key_bindings=1
+ update_config=1
+ allow_legacy=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 ;;
+ --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
+ exit 1
+ ;;
+ esac
+done
+
+cd "$(dirname "${BASH_SOURCE[0]}")"
+fzf_base="$(pwd)"
ask() {
- # non-interactive shell: wait for a linefeed
- # interactive shell: continue after a single keypress
- [ -n "$interactive" ] && read_n='-n 1' || read_n=
-
- 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() {
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"
@@ -38,26 +96,44 @@ 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
+link_fzf_in_path() {
+ 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)
+ check_binary && return
+ fi
+ return 1
+}
+
+try_curl() {
+ 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 &&
+ 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() {
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
- elif [ -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
if [ $? -ne 0 ]; then
@@ -65,27 +141,24 @@ download() {
return
fi
- local url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz
- if which curl > /dev/null; then
- curl -fL $url | tar -xz
- elif which wget > /dev/null; then
- wget -O - $url | tar -xz
- else
- binary_error="curl or wget not found"
+ local url
+ [[ "$version" =~ alpha ]] &&
+ 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
+ binary_error="Failed to download with curl and wget"
return
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 || return 1
- if [[ $1 =~ linux_amd64$ ]]; then
- check_binary || download $1-static
- else
- check_binary
- fi
+ chmod +x fzf && check_binary
}
# Try to download binary executable
@@ -93,130 +166,99 @@ 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 ;;
- *) binary_available=0 binary_error=1 ;;
+ Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64}.tgz ;;
+ Darwin\ *86) download fzf-$version-darwin_${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 ;;
+ 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
cd "$fzf_base"
if [ -n "$binary_error" ]; then
if [ $binary_available -eq 0 ]; then
- echo "No prebuilt binary for $archi ... "
+ echo "No prebuilt binary for $archi ..."
else
echo " - $binary_error !!!"
- exit 1
fi
- echo "Installing legacy Ruby version ..."
-
- # ruby executable
- echo -n "Checking Ruby executable ... "
- ruby=`which 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 -a $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
+ if command -v go > /dev/null; then
+ 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 [ $? -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
+ if go get -u github.com/junegunn/fzf; then
+ echo "OK"
+ cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
+ else
+ echo "Failed to build binary. Installation failed."
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"
+ echo "go executable not found. Installation failed."
+ exit 1
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"
fi
-if [[ $1 == "-y" ]] ; then
- # If install -y is called, force auto completion and key_bindings to be enabled
- auto_completion=0
- key_bindings=0
-else
- # Auto-completion
- ask "Do you want to add auto-completion support?"
- auto_completion=$?
+[[ "$*" =~ "--bin" ]] && exit 0
- # Key-bindings
- ask "Do you want to add key bindings?"
+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?"
+ auto_completion=$?
+fi
+
+# 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
+for shell in $shells; do
+ [[ "$shell" = fish ]] && continue
echo -n "Generate ~/.fzf.$shell ... "
src=~/.fzf.${shell}
- fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null"
- if [ $auto_completion -ne 0 ]; then
+ fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null"
+ 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
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
- export MANPATH="\$MANPATH:$fzf_base/man"
-fi
-
# Auto-completion
# ---------------
$fzf_completion
@@ -230,12 +272,10 @@ EOF
done
# fish
-has_fish=0
-if [ -n "$(which fish 2> /dev/null)" ]; then
- has_fish=1
+if [[ "$shells" =~ fish ]]; 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"
@@ -246,51 +286,94 @@ 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() {
- echo "Update $2:"
- echo " - $1"
- [ -f "$2" ] || touch "$2"
- if [ $# -lt 3 ]; then
- line=$(\grep -nF "$1" "$2" | sed 's/:.*//' | tr '\n' ' ')
- else
- line=$(\grep -nF "$3" "$2" | sed 's/:.*//' | tr '\n' ' ')
+ set -e
+
+ local update line file pat lno
+ update="$1"
+ line="$2"
+ file="$3"
+ pat="${4:-}"
+ lno=""
+
+ echo "Update $file:"
+ echo " - $line"
+ 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 "$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 [ $update -eq 1 ]; then
+ [ -f "$file" ] && echo >> "$file"
+ echo "$line" >> "$file"
+ echo " + Added"
+ else
+ echo " ~ Skipped"
+ fi
fi
echo
+ 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?"
+ update_config=$?
+fi
echo
-for shell in bash zsh; do
- append_line "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" ~/.${shell}rc "~/.fzf.${shell}"
+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 0 -a $has_fish -eq 1 ]; then
+if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then
bind_file=~/.config/fish/functions/fish_user_key_bindings.fish
- append_line "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
-cat << EOF
-Finished. Restart your shell or reload config file.
- source ~/.bashrc # bash
- source ~/.zshrc # zsh
-EOF
-[ $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.'
+ [[ "$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
+fi
+echo 'For more information, see: https://github.com/junegunn/fzf'
diff --git a/src/fzf/main.go b/main.go
similarity index 53%
rename from src/fzf/main.go
rename to main.go
index 29d4767..3b59244 100644
--- a/src/fzf/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/man/man1/fzf-tmux.1 b/man/man1/fzf-tmux.1
new file mode 100644
index 0000000..8de9f69
--- /dev/null
+++ b/man/man1/fzf-tmux.1
@@ -0,0 +1,54 @@
+.ig
+The MIT License (MIT)
+
+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
+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 "Oct 2017" "fzf 0.17.1" "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
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 4147371..3cc6c7a 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) 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 "Sep 2015" "fzf 0.10.5" "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
@@ -36,23 +36,37 @@ 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)
.TP
.B "+i"
Case-sensitive match
+.TP
+.B "--literal"
+Do not normalize latin script letters for matching.
+.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.
-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)
@@ -63,38 +77,145 @@ Do not sort the result
.TP
.B "--tac"
Reverse the order of the input
+
.RS
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
-.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
+- 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)
+.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 "--ansi"
-Enable processing of ANSI color codes
+.B "+m, --no-multi"
+Disable multi-select
.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
+.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
+.TP
+.BI "--height=" "HEIGHT[%]"
+Display fzf window below the cursor with the given height instead of using
+the full screen.
+.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
+.B "--border"
+Draw border above and below the finder
+.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
+.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
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.
.RS
e.g. \fBfzf --color=bg+:24\fR
@@ -118,6 +239,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
@@ -125,156 +247,12 @@ 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
-.TP
-.B "--reverse"
-Reverse orientation
-.TP
-.BI "--margin=" MARGIN
-Comma-separated expression for margins around the finder.
-.br
-.R ""
-.br
-.RS
-.BR TRBL " Same margin for top, right, bottom, and left"
-.br
-.BR TB,RL " Vertical, horizontal margin"
-.br
-.BR T,RL,B " Top, horizontal, bottom margin"
-.br
-.BR T,R,B,L " Top, right, bottom, left margin"
-.br
-.R ""
-.br
-Each part can be given in absolute number or in percentage relative to the
-terminal size with \fB%\fR suffix.
-.br
-.R ""
-.br
-e.g. \fBfzf --margin 10%\fR
- \fBfzf --margin 1,5%\fR
-.RE
-.TP
-.B "--cycle"
-Enable cyclic scroll
-.TP
-.B "--no-hscroll"
-Disable horizontal scroll
-.TP
-.B "--inline-info"
-Display finder info inline with the query
-.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:
- \fIctrl-[a-z]\fR
- \fIalt-[a-z]\fR
- \fIf[1-4]\fR
- \fIenter\fR (\fIreturn\fR)
- \fIspace\fR
- \fIbspace\fR (\fIbs\fR)
- \fIalt-bspace\fR (\fIalt-bs\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
- 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
- \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)
- \fBforward-char\fR \fIctrl-f right\fR
- \fBforward-word\fR \fIalt-f shift-right\fR
- \fBignore\fR
- \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)
- \fBselect-all\fR
- \fBtoggle\fR
- \fBtoggle-all\fR
- \fBtoggle-down\fR \fIctrl-i (tab)\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
-.RE
+.SS History
.TP
.BI "--history=" "HISTORY_FILE"
Load search history from the specified file and update the file on completion.
@@ -284,17 +262,52 @@ 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 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).
+
+.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
-.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[%]][: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. 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
+ \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"
@@ -318,19 +331,41 @@ 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"
+Read input delimited by ASCII NUL characters instead of newline characters
+.TP
+.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.
+
.RS
e.g. \fBfzf --multi | fzf --sync\fR
.RE
+.TP
+.B "--version"
+Display version information and exit
-.SH ENVIRONMENT
+.TP
+Note that most options have the opposite versions with \fB--no-\fR prefix.
+
+.SH ENVIRONMENT VARIABLES
.TP
.B FZF_DEFAULT_COMMAND
Default command to use when input is tty
@@ -341,7 +376,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
@@ -369,9 +408,12 @@ 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
+
+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
@@ -380,18 +422,162 @@ 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
-term from the result.
+If a term is prefixed by \fB!\fR, fzf will exclude the lines that satisfy the
+term from the result. In this case, fzf performs exact match by default.
-.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.
+
+.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 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
+ \fIctrl-space\fR
+ \fIctrl-alt-[a-z]\fR
+ \fIalt-[a-z]\fR
+ \fIalt-[0-9]\fR
+ \fIf[1-12]\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
+
+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
+ \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-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
+ \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
+ \fBhalf-page-down\fR
+ \fBhalf-page-up\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
+ \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-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
+ \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.
+
+ \fBfzf --bind "enter:execute(less {})"\fR
+
+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
+ \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 of key-action pairs.
+.RE
+
+fzf switches to the alternate screen when executing a command. However, if the
+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 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)
@@ -402,7 +588,7 @@ Junegunn Choi (\fIjunegunn.c@gmail.com\fR)
.I https://github.com/junegunn/fzf
.RE
.br
-.R ""
+
.br
.B Extra Vim plugin:
.RS
diff --git a/plugin/fzf.vim b/plugin/fzf.vim
index d9ced8e..0dbfe90 100644
--- a/plugin/fzf.vim
+++ b/plugin/fzf.vim
@@ -1,4 +1,4 @@
-" Copyright (c) 2015 Junegunn Choi
+" Copyright (c) 2017 Junegunn Choi
"
" MIT License
"
@@ -21,11 +21,89 @@
" 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:fzf_go = expand(':h:h').'/bin/fzf'
-let s:install = expand(':h:h').'/install'
+if exists('g:loaded_fzf')
+ finish
+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
+ let s:term_marker = '&::FZF'
+
+ function! s:fzf_call(fn, ...)
+ let shellslash = &shellslash
+ try
+ set noshellslash
+ return call(a:fn, a:000)
+ finally
+ 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
+ let s:term_marker = ";#FZF"
+
+ 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)
+ let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g')
+ let escaped = substitute(escaped, '%', '%%', 'g')
+ let escaped = substitute(escaped, '"', '\\^&', 'g')
+ let escaped = substitute(escaped, '\(\\\+\)\(\\^\)', '\1\1\2', 'g')
+ return '^"'.substitute(escaped, '\(\\\+\)$', '\1\1', '').'^"'
+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
+
+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, 1)
+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: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
@@ -36,13 +114,16 @@ function! s:fzf_exec()
let s:exec = s:fzf_go
elseif executable('fzf')
let s:exec = 'fzf'
+ 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')
+ throw 'fzf executable not found'
elseif !s:installed && executable(s:install) &&
\ 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()
@@ -51,11 +132,7 @@ function! s:fzf_exec()
throw 'fzf executable not found'
endif
endif
- return s:exec
-endfunction
-
-function! s:tmux_not_zoomed()
- return system('tmux list-panes -F "#F"') !~# 'Z'
+ return fzf#shellescape(s:exec)
endfunction
function! s:tmux_enabled()
@@ -64,7 +141,7 @@ function! s:tmux_enabled()
endif
if exists('s:tmux')
- return s:tmux && s:tmux_not_zoomed()
+ return s:tmux
endif
let s:tmux = 0
@@ -72,15 +149,12 @@ 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()
-endfunction
-
-function! s:shellesc(arg)
- return '"'.substitute(a:arg, '"', '\\"', 'g').'"'
+ return s:tmux
endfunction
function! s:escape(path)
- return escape(a:path, ' %#''"\')
+ let path = fnameescape(a:path)
+ return s:is_win ? escape(path, '$') : path
endfunction
" Upgrade legacy options
@@ -98,56 +172,250 @@ 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! 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 && s:fzf_fnamemodify(a:target, ':p') ==# s:fzf_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 type(Cmd) == type(function('call'))
+ return Cmd(a:lines)
+ endif
+ if len(a:lines) > 1
+ augroup fzf_swap
+ autocmd SwapExists * let v:swapchoice='o'
+ \| call s:warn('fzf: E325: swap file exists: '.s:fzf_expand(''))
+ augroup END
+ endif
+ try
+ let empty = empty(s:fzf_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 !has('patch-8.0.0177') && !has('nvim-0.2') && exists('#BufEnter')
+ \ && isdirectory(item)
+ doautocmd BufEnter
+ endif
+ endfor
+ finally
+ let &autochdir = autochdir
+ silent! autocmd! fzf_swap
+ endtry
+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, fam)
+ if code =~? pat
+ 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
+
+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
+
+function! s:evaluate_opts(options)
+ return type(a:options) == type([]) ?
+ \ join(map(copy(a:options), 'fzf#shellescape(v:val)')) : a:options
+endfunction
+
+" [name string,] [opts dict,] [fullscreen boolean]
+function! fzf#wrap(...)
+ let args = ['', {}, 0]
+ let expects = map(copy(args), 'type(v:val)')
+ let tidx = 0
+ for arg in copy(a:000)
+ let tidx = index(expects, type(arg), tidx)
+ if tidx < 0
+ throw 'Invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])'
+ endif
+ let args[tidx] = arg
+ let tidx += 1
+ unlet arg
+ endfor
+ let [name, opts, bang] = args
+
+ 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
+ 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, s:validate_layout(get(g:, 'fzf_layout', s:default_layout)))
+ endif
+ endif
+
+ " Colors: g:fzf_colors
+ 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 = s:fzf_expand(g:fzf_history_dir)
+ if !isdirectory(dir)
+ call mkdir(dir, 'p')
+ endif
+ let history = fzf#shellescape(dir.'/'.name)
+ let opts.options = join(['--history', history, 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! 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 oshell = &shell
- set shell=sh
- if has('nvim') && bufexists('term://*:FZF')
- echohl WarningMsg
- echomsg 'FZF is already running!'
- echohl None
- return []
- endif
+ let [shell, shellslash] = s:use_sh()
+
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
throw v:exception
endtry
+ 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) && !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).' '.fzf#shellescape(temps.source)
+ endif
+
if has_key(dict, 'source')
let source = dict.source
let type = type(source)
if type == 1
- let prefix = source.'|'
+ let prefix = '( '.source.' )|'
elseif type == 3
- let temps.input = tempname()
+ let temps.input = s:fzf_tempname()
call writefile(source, temps.input)
- let prefix = 'cat '.s:shellesc(temps.input).'|'
+ let prefix = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input).'|'
else
throw 'Invalid source type'
endif
else
let prefix = ''
endif
- 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
+ let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0)
+ 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_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('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
+ let use_term = 0
+ endif
+ if use_height
+ let height = s:calc_size(&lines, dict.down, dict)
+ let optstr .= ' --height='.height
+ elseif use_term
+ let optstr .= ' --no-height'
+ endif
+ let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
+
+ if use_term
+ return s:execute_term(dict, command, temps)
+ endif
+
+ let lines = use_tmux ? s:execute_tmux(dict, command, temps)
+ \ : s:execute(dict, command, use_height, temps)
+ call s:callback(dict, lines)
+ return lines
finally
- let &shell = oshell
+ let [&shell, &shellslash] = [shell, shellslash]
endtry
endfunction
@@ -166,97 +434,151 @@ 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
endfor
return printf('LINES=%d COLUMNS=%d %s %s %s --',
- \ &lines, &columns, 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)
- 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)
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 'chdir '.s:escape(a:dict.dir)
- let a:dict.dir = getcwd()
+ execute 'lcd' s:escape(a:dict.dir)
+ let a:dict.dir = s:fzf_getcwd()
return 1
endif
return 0
endfunction
-function! s:popd(dict)
- if has_key(a:dict, 'prev_dir')
- execute 'chdir '.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_dir') || s:fzf_getcwd() != w:fzf_dir[1]
+ return
endif
+ execute 'lcd' s:escape(w:fzf_dir[0])
+ unlet w:fzf_dir
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"),
\ &columns, &lines/2, getwinposx(), getwinposy())
endfunction
unlet! s:launcher
-let s:launcher = function('s:xterm_launcher')
+if s:is_win || has('win32unix')
+ let s:launcher = '%s'
+else
+ let s:launcher = function('s:xterm_launcher')
+endif
-function! s:execute(dict, command, temps)
+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, use_height, temps) abort
call s:pushd(a:dict)
- silent! !clear 2> /dev/null
- let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#')
+ if has('unix') && !a:use_height
+ silent! !clear 2> /dev/null
+ endif
+ 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
- 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
- execute 'silent !'.command
- redraw!
- 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
+ if s:is_win
+ 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 fzf = {}
+ let fzf.dict = a:dict
+ let fzf.temps = a:temps
+ function! fzf.on_exit(job_id, exit_status, event) dict
+ 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)
+ return []
endif
- return []
- else
- return s:callback(a:dict, a:temps)
+ 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'
+ 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
+ let exit_status = v:shell_error
+ redraw!
+ return s:exit_handler(exit_status, command) ? s:collect(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
- let command = 'cd '.s:escape(a:dict.dir).' && '.command
+ let command = join(['cd', fzf#shellescape(a:dict.dir), '&&', command])
endif
call system(command)
+ let exit_status = v:shell_error
redraw!
- return s:callback(a:dict, a:temps)
+ return s:exit_handler(exit_status, command) ? s:collect(a:temps) : []
endfunction
function! s:calc_size(max, val, dict)
- 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
@@ -266,11 +588,12 @@ 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
function! s:getpos()
- return {'tab': tabpagenr(), 'win': winnr(), 'cnt': winnr('$')}
+ return {'tab': tabpagenr(), 'win': winnr(), 'cnt': winnr('$'), 'tcnt': tabpagenr('$')}
endfunction
function! s:split(dict)
@@ -279,80 +602,151 @@ 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, '')
- 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)
- else
- let sz = s:calc_size(max, val, {})
- endif
- execute cmd sz.'new'
- execute resz sz
- return
- endif
- endfor
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
- tabnew
+ 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
- setlocal winfixwidth winfixheight buftype=nofile bufhidden=wipe nobuflisted
+ setlocal winfixwidth winfixheight
endtry
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' }
- function! fzf.on_exit(id, code)
- let pos = s:getpos()
- let inplace = pos == s:ppos " {'window': 'enew'}
- if !inplace
+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,
+ \ 'columns': &columns, 'command': a:command }
+ function! fzf.switch_back(inplace)
+ if a:inplace && bufnr('') == self.buf
+ if bufexists(self.pbuf)
+ execute 'keepalt b' self.pbuf
+ endif
+ " No other listed buffer
+ if bufnr('') == self.buf
+ enew
+ endif
+ endif
+ endfunction
+ function! fzf.on_exit(id, code, ...)
+ 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
- call s:pushd(self.dict)
- try
- redraw!
- call s:callback(self.dict, self.temps)
- if inplace && bufnr('') == self.buf
- execute "normal! \"
- " No other listed buffer
- if bufnr('') == self.buf
- bd!
- endif
- endif
- finally
- call s:popd(self.dict)
- endtry
+ if bufexists(self.buf)
+ 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
+
+ call s:pushd(self.dict)
+ let lines = s:collect(self.temps)
+ call s:callback(self.dict, lines)
+ call self.switch_back(s:getpos() == self.ppos)
endfunction
- call termopen(a:command, fzf)
+ try
+ if s:present(a:dict, 'dir')
+ execute 'lcd' s:escape(a:dict.dir)
+ endif
+ 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
+ let command .= s:term_marker
+ if has('nvim')
+ call termopen(command, fzf)
+ else
+ 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')
+ lcd -
+ endif
+ endtry
+ setlocal nospell bufhidden=wipe nobuflisted nonumber
+ setf fzf
startinsert
return []
endfunction
-function! s:callback(dict, temps)
-try
- if !filereadable(a:temps.result)
- let lines = []
- else
- 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_dir = [a:dict.prev_dir, a:dict.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
@@ -361,60 +755,54 @@ 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
-
- return lines
-catch
- if stridx(v:exception, ':E325:') < 0
- echoerr v:exception
+ " We may have opened a new window or tab
+ if popd
+ let w:fzf_dir = [a:dict.prev_dir, a:dict.dir]
+ call s:dopopd()
endif
-endtry
endfunction
let s:default_action = {
- \ 'ctrl-m': 'e',
\ 'ctrl-t': 'tab split',
\ 'ctrl-x': 'split',
\ 'ctrl-v': 'vsplit' }
-function! s:cmd_callback(lines) abort
- if empty(a:lines)
- return
+function! s:shortpath()
+ let short = fnamemodify(getcwd(), ':~:.')
+ if !has('win32unix')
+ let short = pathshorten(short)
endif
- let key = remove(a:lines, 0)
- let cmd = get(s:action, key, 'e')
- try
- let autochdir = &autochdir
- set noautochdir
- for item in a:lines
- execute cmd s:escape(item)
- endfor
- finally
- let &autochdir = autochdir
- endtry
+ let slash = (s:is_win && !&shellslash) ? '\' : '/'
+ return empty(short) ? '~'.slash : short . (short =~ escape(slash, '\').'$' ? '' : slash)
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 opts = {}
- if len(args) > 0 && isdirectory(expand(args[-1]))
- let opts.dir = substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g')
+ 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'), '[/\\]*$', '/', '')
+ if s:is_win && !&shellslash
+ let opts.dir = substitute(opts.dir, '/', '\\', 'g')
+ endif
+ let prompt = opts.dir
+ else
+ let prompt = s:shortpath()
endif
- if !a:bang
- let opts.down = get(g:, 'fzf_height', get(g:, 'fzf_tmux_height', s:default_height))
- endif
- call fzf#run(extend({'options': join(args), 'sink*': function('cmd_callback')}, opts))
+ 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))
endfunction
command! -nargs=* -complete=dir -bang FZF call s:cmd(0, )
let &cpo = s:cpo_save
unlet s:cpo_save
-
diff --git a/shell/completion.bash b/shell/completion.bash
index d7fdf26..445df17 100644
--- a/shell/completion.bash
+++ b/shell/completion.bash
@@ -5,14 +5,42 @@
# / __/ / /_/ __/
# /_/ /___/_/-completion.bash
#
-# - $FZF_TMUX (default: 1)
+# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_HEIGHT (default: '40%')
# - $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"
+ 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@^\./@@'
+ }
+fi
+
+if ! declare -f _fzf_compgen_dir > /dev/null; then
+ _fzf_compgen_dir() {
+ command find -L "$1" \
+ -name .git -prune -o -name .svn -prune -o -type d \
+ -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
+ }
+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"
+}
+
_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-Za-z0-9_= ;]/, "_", $1); print $1"="$2}'
}
_fzf_opts_completion() {
@@ -22,44 +50,52 @@ _fzf_opts_completion() {
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="
-x --extended
- -e --extended-exact
+ -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
+ --literal
+ --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)
- 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)
@@ -68,8 +104,8 @@ _fzf_opts_completion() {
;;
esac
- if [[ ${cur} =~ ^-|\+ ]]; then
- COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+ if [[ "$cur" =~ ^-|\+ ]]; then
+ COMPREPLY=( $(compgen -W "${opts}" -- "$cur") )
return 0
fi
@@ -77,42 +113,42 @@ _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
_completion_loader "$@"
ret=$?
- eval $(complete | \grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)
- source $BASH_SOURCE
+ eval "$(complete | command grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)"
+ source "${BASH_SOURCE[0]}"
return $ret
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')
+ fzf="$(__fzfcmd_complete)"
+ cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
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/#\/}
- [ "$dir" = './' ] && dir=''
- tput sc
- matches=$(find -L "$dir"* $1 2> /dev/null | $fzf $FZF_COMPLETION_OPTS $2 -q "$leftover" | while read item; do
+ [ -z "$dir" ] && dir='.'
+ [ "$dir" != "/" ] && dir="${dir/%\//}"
+ 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% }
@@ -121,7 +157,7 @@ _fzf_path_completion() {
else
COMPREPLY=( "$cur" )
fi
- tput rc
+ printf '\e[5n'
return 0
fi
dir=$(dirname "$dir")
@@ -135,20 +171,21 @@ _fzf_path_completion() {
fi
}
-_fzf_list_completion() {
- local cur selected trigger cmd src fzf
- [ ${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')
+_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="$(__fzfcmd_complete)"
+
+ cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
trigger=${FZF_COMPLETION_TRIGGER-'**'}
cur="${COMP_WORDS[COMP_CWORD]}"
- if [[ ${cur} == *"$trigger" ]]; then
+ if [[ "$cur" == *"$trigger" ]]; then
cur=${cur:0:${#cur}-${#trigger}}
- tput sc
- selected=$(eval "$src | $fzf $FZF_COMPLETION_OPTS $1 -q '$cur'" | tr '\n' ' ')
- selected=${selected% }
- tput rc
+ 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'
if [ -n "$selected" ]; then
COMPREPLY=("$selected")
@@ -160,32 +197,26 @@ _fzf_list_completion() {
fi
}
-_fzf_all_completion() {
- _fzf_path_completion \
- "-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \
- "-m" "" "$@"
+_fzf_path_completion() {
+ __fzf_generic_path_completion _fzf_compgen_path "-m" "" "$@"
}
+# Deprecated. No file only completion.
_fzf_file_completion() {
- _fzf_path_completion \
- "-name .git -prune -o -name .svn -prune -o -type f -print -o -type l -print" \
- "-m" "" "$@"
+ _fzf_path_completion "$@"
}
_fzf_dir_completion() {
- _fzf_path_completion \
- "-name .git -prune -o -name .svn -prune -o -type d -print" \
- "" "/" "$@"
+ __fzf_generic_path_completion _fzf_compgen_dir "" "/" "$@"
}
-_fzf_kill_completion() {
+_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"
- tput sc
- selected=$(ps -ef | sed 1d | $fzf -m $FZF_COMPLETION_OPTS | awk '{print $2}' | tr '\n' ' ')
- tput rc
+ fzf="$(__fzfcmd_complete)"
+ 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
COMPREPLY=( "$selected" )
@@ -193,40 +224,49 @@ _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' "$@" < <(
+ command grep -v '^\s*\(#\|$\)' /etc/hosts | command 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 | 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' | 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
+ )
}
-_fzf_env_var_completion() {
- _fzf_list_completion '-m' "$@" << "EOF"
- declare -xp | sed 's/=.*//' | sed 's/.* //'
-EOF
+_fzf_complete_unset() {
+ _fzf_complete '-m' "$@" < <(
+ declare -xp | sed 's/=.*//' | sed 's/.* //'
+ )
}
-_fzf_alias_completion() {
- _fzf_list_completion '-m' "$@" << "EOF"
- alias | sed 's/=.*//' | sed 's/.* //'
-EOF
+_fzf_complete_export() {
+ _fzf_complete '-m' "$@" < <(
+ declare -xp | sed 's/=.*//' | sed 's/.* //'
+ )
+}
+
+_fzf_complete_unalias() {
+ _fzf_complete '-m' "$@" < <(
+ alias | sed 's/=.*//' | sed 's/.* //'
+ )
}
# fzf options
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
- javac ld less more mvim patch perl python ruby
- sed sftp sort source tail tee uniq vi view vim wc"
+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
basename bunzip2 bzip2 chmod chown curl cp dirname du
find git grep gunzip gzip hg jar
ln ls mv open rm rsync scp
@@ -234,42 +274,51 @@ a_cmds="
x_cmds="kill ssh telnet unset unalias export"
# Preserve existing completion
-if [ "$_fzf_completion_loaded" != '0.9.12' ]; 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
-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
fi
-# Directory
-for cmd in $d_cmds; do
- complete -F _fzf_dir_completion -o nospace -o plusdirs $cmd
-done
-
-# File
-for cmd in $f_cmds; do
- complete -F _fzf_file_completion -o default -o bashdefault $cmd
-done
+_fzf_defc() {
+ local cmd func opts orig_var orig def
+ cmd="$1"
+ func="$2"
+ opts="$3"
+ orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
+ orig="${!orig_var}"
+ if [ -n "$orig" ]; then
+ printf -v def "$orig" "$func"
+ eval "$def"
+ else
+ complete -F "$func" $opts "$cmd"
+ fi
+}
# Anything
for cmd in $a_cmds; do
- complete -F _fzf_all_completion -o default -o bashdefault $cmd
+ _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 dirnames"
+done
+
+unset _fzf_defc
+
# 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
+unset cmd d_cmds a_cmds x_cmds
diff --git a/shell/completion.zsh b/shell/completion.zsh
index f45c538..3c083aa 100644
--- a/shell/completion.zsh
+++ b/shell/completion.zsh
@@ -5,111 +5,151 @@
# / __/ / /_/ __/
# /_/ /___/_/-completion.zsh
#
-# - $FZF_TMUX (default: 1)
+# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_HEIGHT (default: '40%')
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
-_fzf_path_completion() {
- local base lbuf find_opts fzf_opts suffix tail fzf dir leftover matches nnm
+# 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"
+ 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@^\./@@'
+ }
+fi
+
+if ! declare -f _fzf_compgen_dir > /dev/null; then
+ _fzf_compgen_dir() {
+ command find -L "$1" \
+ -name .git -prune -o -name .svn -prune -o -type d \
+ -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
+ }
+fi
+
+###########################################################
+
+__fzfcmd_complete() {
+ [ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
+ echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
+}
+
+__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"
base=${(Q)1}
lbuf=$2
- find_opts=$3
+ compgen=$3
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)"
- 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
+ if [[ -z "$dir" || -d ${~dir} ]]; then
leftover=${base/#"$dir"}
leftover=${leftover/#\/}
- [ "$dir" = './' ] && dir=''
+ [ -z "$dir" ] && 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
- printf "%q$suffix " "$item"
+ 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% }
if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches$tail"
fi
zle redisplay
+ typeset -f zle-line-init >/dev/null && zle zle-line-init
break
fi
dir=$(dirname "$dir")
dir=${dir%/}/
done
- [ -n "$nnm" ] && unsetopt nonomatch
}
-_fzf_all_completion() {
- _fzf_path_completion "$1" "$2" \
- "-name .git -prune -o -name .svn -prune -o -type d -print -o -type f -print -o -type l -print" \
+_fzf_path_completion() {
+ __fzf_generic_path_completion "$1" "$2" _fzf_compgen_path \
"-m" "" " "
}
_fzf_dir_completion() {
- _fzf_path_completion "$1" "$2" \
- "-name .git -prune -o -name .svn -prune -o -type d -print" \
+ __fzf_generic_path_completion "$1" "$2" _fzf_compgen_dir \
"" "/" ""
}
-_fzf_list_completion() {
- local prefix lbuf fzf_opts src fzf matches
- prefix=$1
- lbuf=$2
- fzf_opts=$3
- read -r src
- [ ${FZF_TMUX:-1} -eq 1 ] && fzf="fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%}" || fzf="fzf"
+_fzf_feed_fifo() (
+ command rm -f "$1"
+ mkfifo "$1"
+ cat <&0 > "$1" &
+)
- matches=$(eval "$src" | ${=fzf} ${=FZF_COMPLETION_OPTS} ${=fzf_opts} -q "$prefix")
+_fzf_complete() {
+ local fifo fzf_opts lbuf fzf matches post
+ fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
+ fzf_opts=$1
+ lbuf=$2
+ post="${funcstack[2]}_post"
+ type $post > /dev/null 2>&1 || post=cat
+
+ fzf="$(__fzfcmd_complete)"
+
+ _fzf_feed_fifo "$fifo"
+ 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 "
+ LBUFFER="$lbuf$matches"
fi
zle redisplay
+ typeset -f zle-line-init >/dev/null && zle zle-line-init
+ command rm -f "$fifo"
}
-_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
-EOF
+_fzf_complete_telnet() {
+ _fzf_complete '+m' "$@" < <(
+ command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
+ awk '{if (length($2) > 0) {print $2}}' | sort -u
+ )
}
-_fzf_ssh_completion() {
- _fzf_list_completion "$1" "$2" '+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' "$@" < <(
+ 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' | 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
+ )
}
-_fzf_env_var_completion() {
- _fzf_list_completion "$1" "$2" '+m' << "EOF"
- declare -xp | sed 's/=.*//' | sed 's/.* //'
-EOF
+_fzf_complete_export() {
+ _fzf_complete '-m' "$@" < <(
+ declare -xp | sed 's/=.*//' | sed 's/.* //'
+ )
}
-_fzf_alias_completion() {
- _fzf_list_completion "$1" "$2" '+m' << "EOF"
- alias | sed 's/=.*//'
-EOF
+_fzf_complete_unset() {
+ _fzf_complete '-m' "$@" < <(
+ declare -xp | sed 's/=.*//' | sed 's/.* //'
+ )
+}
+
+_fzf_complete_unalias() {
+ _fzf_complete '+m' "$@" < <(
+ alias | sed 's/=.*//'
+ )
}
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 noksh_arrays noposixbuiltins
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
# 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
@@ -122,42 +162,38 @@ 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"
- matches=$(ps -ef | sed 1d | ${=fzf} ${=FZF_COMPLETION_OPTS} -m | awk '{print $2}' | tr '\n' ' ')
+ fzf="$(__fzfcmd_complete)"
+ 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
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=(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]}}
- 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_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
- eval "zle ${fzf_default_completion:-expand-or-complete}"
+ zle ${fzf_default_completion:-expand-or-complete}
fi
- [ -n "$sws" ] && setopt shwordsplit
}
-[ -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[(s: :w)2]
+ unset binding
+}
zle -N fzf-completion
bindkey '^I' fzf-completion
-
diff --git a/shell/key-bindings.bash b/shell/key-bindings.bash
index 0bfd50d..4266a40 100644
--- a/shell/key-bindings.bash
+++ b/shell/key-bindings.bash
@@ -1,11 +1,11 @@
# 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 . -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 | sed 1d | cut -b3-"}"
- eval "$cmd" | fzf -m | while read item; do
+ -o -type l -print 2> /dev/null | cut -b3-"}"
+ 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
@@ -13,8 +13,13 @@ __fzf_select__() {
if [[ $- =~ i ]]; then
+__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"
}
__fzf_select_tmux__() {
@@ -25,13 +30,25 @@ __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"); 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__)"
+ READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
+ READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
+ fi
}
__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 . -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"
}
__fzf_history__() (
@@ -39,8 +56,8 @@ __fzf_history__() (
shopt -u nocaseglob nocasematch
line=$(
HISTTIMEFORMAT= history |
- $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r |
- \grep '^ *[0-9]') &&
+ 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"
else
@@ -48,49 +65,57 @@ __fzf_history__() (
fi
)
-__use_tmux=0
-[ -n "$TMUX_PANE" -a ${FZF_TMUX:-1} -ne 0 -a ${LINES:-40} -gt 15 ] && __use_tmux=1
-
-if [ -z "$(set -o | \grep '^vi.*on')" ]; then
+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_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"'
+ if [ $BASH_VERSINFO -gt 3 ]; then
+ bind -x '"\C-t": "fzf-file-widget"'
+ 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"'
+ 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
- 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,
+ # 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'
# 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
- bind '"\C-t": "\e$a \eddi$(__fzf_select_tmux__)\C-x\C-e\e0P$xa"'
+ if [ $BASH_VERSINFO -gt 3 ]; then
+ bind -x '"\C-t": "fzf-file-widget"'
+ 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": "\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 '"\ec": "ddi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
fi
-unset __use_tmux
-
fi
diff --git a/shell/key-bindings.fish b/shell/key-bindings.fish
index a08df3d..97a3ec5 100644
--- a/shell/key-bindings.fish
+++ b/shell/key-bindings.fish
@@ -1,69 +1,146 @@
# 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')' '
- end
- end
+ # Store current token in $dir as root for the 'find' command
+ function fzf-file-widget -d "List files and folders"
+ set -l commandline (__fzf_parse_commandline)
+ set -l dir $commandline[1]
+ set -l fzf_query $commandline[2]
- function __fzf_ctrl_t
+ # "-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 'dev' -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 > $TMPDIR/fzf.result"
- and commandline -i (cat $TMPDIR/fzf.result | __fzf_escape)
+ 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 | 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_CTRL_T_OPTS"
+ 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
+ return
+ else
+ # 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_ctrl_r
- history | eval (__fzfcmd) +s +m --tiebreak=index --toggle-sort=ctrl-r > $TMPDIR/fzf.result
- and commandline (cat $TMPDIR/fzf.result)
+ 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 --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m"
+
+ 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
- rm -f $TMPDIR/fzf.result
end
- function __fzf_alt_c
- # 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
- [ (cat $TMPDIR/fzf.result | wc -l) -gt 0 ]
- and cd (cat $TMPDIR/fzf.result)
+ 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 \$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 --query "'$fzf_query'"' | read -l result
+
+ if [ -n "$result" ]
+ cd $result
+
+ # Remove last token from commandline.
+ commandline -t ""
+ end
+ end
+
commandline -f repaint
- rm -f $TMPDIR/fzf.result
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"
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
+ 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
diff --git a/shell/key-bindings.zsh b/shell/key-bindings.zsh
index 735aa50..c25368c 100644
--- a/shell/key-bindings.zsh
+++ b/shell/key-bindings.zsh
@@ -1,52 +1,88 @@
# Key bindings
# ------------
-if [[ $- =~ i ]]; then
+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 . -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 | sed 1d | cut -b3-"}"
- eval "$cmd" | $(__fzfcmd) -m | while read item; do
- printf '%q ' "$item"
+ -o -type l -print 2> /dev/null | cut -b3-"}"
+ setopt localoptions pipefail 2> /dev/null
+ 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=$?
echo
+ 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"
}
fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)"
+ local ret=$?
zle redisplay
+ typeset -f zle-line-init >/dev/null && zle zle-line-init
+ return $ret
}
zle -N fzf-file-widget
bindkey '^T' fzf-file-widget
+# 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() {
- 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):-.}"
- zle reset-prompt
+ 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
+ return 0
+ fi
+ cd "$dir"
+ local ret=$?
+ zle fzf-redraw-prompt
+ typeset -f zle-line-init >/dev/null && zle zle-line-init
+ return $ret
}
zle -N fzf-cd-widget
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
- if selected=( $(fc -l 1 | $(__fzfcmd) +s --tac +m -n2..,.. --tiebreak=index --toggle-sort=ctrl-r -q "$LBUFFER") ); then
+ local selected num
+ setopt localoptions noglobsubst noposixbuiltins pipefail 2> /dev/null
+ 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]
if [ -n "$num" ]; then
zle vi-fetch-history -n $num
fi
fi
zle redisplay
+ typeset -f zle-line-init >/dev/null && zle zle-line-init
+ return $ret
}
zle -N fzf-history-widget
bindkey '^R' fzf-history-widget
fi
-
diff --git a/src/Dockerfile.arch b/src/Dockerfile.arch
deleted file mode 100644
index d014651..0000000
--- a/src/Dockerfile.arch
+++ /dev/null
@@ -1,28 +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 GOPATH /go
-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
-
-# Volume
-VOLUME /go
-
-# Default CMD
-CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash
-
diff --git a/src/Dockerfile.centos b/src/Dockerfile.centos
deleted file mode 100644
index c03f43a..0000000
--- a/src/Dockerfile.centos
+++ /dev/null
@@ -1,21 +0,0 @@
-FROM centos:centos7
-MAINTAINER Junegunn Choi
-
-# yum
-RUN yum install -y git gcc make tar ncurses-devel
-
-# 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 GOPATH /go
-ENV GOROOT /go1.4
-ENV PATH /go1.4/bin:$PATH
-
-# Volume
-VOLUME /go
-
-# Default CMD
-CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash
-
diff --git a/src/Dockerfile.ubuntu b/src/Dockerfile.ubuntu
deleted file mode 100644
index d9de120..0000000
--- a/src/Dockerfile.ubuntu
+++ /dev/null
@@ -1,26 +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 GOPATH /go
-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
-
-# Volume
-VOLUME /go
-
-# Default CMD
-CMD cd /go/src/github.com/junegunn/fzf/src && /bin/bash
-
diff --git a/src/LICENSE b/src/LICENSE
index fe4c31a..d222c9b 100644
--- a/src/LICENSE
+++ b/src/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2015 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/Makefile b/src/Makefile
deleted file mode 100644
index 3304f57..0000000
--- a/src/Makefile
+++ /dev/null
@@ -1,97 +0,0 @@
-ifndef GOPATH
-$(error GOPATH is undefined)
-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)
-$(error "Build on $(UNAME_M) is not supported, yet.")
-endif
-
-SOURCES := $(wildcard *.go */*.go)
-BINDIR := ../bin
-
-BINARY32 := fzf-$(GOOS)_386$(SUFFIX)
-BINARY64 := fzf-$(GOOS)_amd64$(SUFFIX)
-VERSION = $(shell fzf/$(BINARY64) --version)
-RELEASE32 = fzf-$(VERSION)-$(GOOS)_386$(SUFFIX)
-RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64$(SUFFIX)
-
-all: release
-
-release: 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)
-
-ifndef STATIC
-build: test fzf/$(BINARY32) fzf/$(BINARY64)
-else
-build: test fzf/$(BINARY64)
-endif
-
-test:
- go get
- go test -v ./...
-
-install: $(BINDIR)/fzf
-
-uninstall:
- rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY64)
-
-clean:
- cd fzf && rm -f fzf-*
-
-fzf/$(BINARY32): $(SOURCES)
- cd fzf && GOARCH=386 CGO_ENABLED=1 go build -o $(BINARY32)
-
-fzf/$(BINARY64): $(SOURCES)
- cd fzf && go build $(LDFLAGS) -o $(BINARY64)
-
-$(BINDIR)/fzf: fzf/$(BINARY64) | $(BINDIR)
- cp -f fzf/$(BINARY64) $(BINDIR)
- cd $(BINDIR) && ln -sf $(BINARY64) fzf
-
-$(BINDIR):
- mkdir -p $@
-
-docker-arch:
- docker build -t junegunn/arch-sandbox - < Dockerfile.arch
-
-docker-ubuntu:
- docker build -t junegunn/ubuntu-sandbox - < Dockerfile.ubuntu
-
-arch: docker-arch
- docker run -i -t -v $(GOPATH):/go junegunn/$@-sandbox \
- sh -c 'cd /go/src/github.com/junegunn/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'
-
-linux: docker-arch
- docker run -i -t -v $(GOPATH):/go junegunn/arch-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
diff --git a/src/README.md b/src/README.md
deleted file mode 100644
index 18ccec3..0000000
--- a/src/README.md
+++ /dev/null
@@ -1,121 +0,0 @@
-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.
-
-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
------
-
-```sh
-# Build fzf executables and tarballs
-make
-
-# Install the executable to ../bin directory
-make install
-
-# Build executables and tarballs for Linux using Docker
-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
---------------------------
-
-- [ncurses][ncurses]
-- [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
- - Licensed under [MIT](http://mattn.mit-license.org/2013)
-- [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
- - Licensed under [MIT](http://mattn.mit-license.org/2014)
-
-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
-[termbox]: https://github.com/nsf/termbox-go
diff --git a/src/algo/algo.go b/src/algo/algo.go
index ac7bd8b..2fe5c09 100644
--- a/src/algo/algo.go
+++ b/src/algo/algo.go
@@ -1,51 +1,634 @@
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 (
+ "bytes"
+ "fmt"
"strings"
"unicode"
+ "unicode/utf8"
"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 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
}
-// FuzzyMatch performs fuzzy-match
-func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune) (int, int) {
- if len(pattern) == 0 {
- return 0, 0
+// Result contains the results of running a match function.
+type Result struct {
+ // TODO int32 should suffice
+ Start int
+ End 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 (
+ charNonWord charClass = iota
+ charLower
+ charUpper
+ charLetter
+ charNumber
+)
+
+func posArray(withPos bool, len int) *[]int {
+ if withPos {
+ pos := make([]int, 0, len)
+ return &pos
+ }
+ return nil
+}
+
+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]
+ return offset + size, slice
+ }
+ return offset, make([]int16, size)
+}
+
+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]
+ 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)))
+}
+
+func normalizeRune(r rune) rune {
+ if r < 0x00C0 || r > 0x2184 {
+ return r
+ }
+
+ n := normalized[r]
+ if n > 0 {
+ return n
+ }
+ return r
+}
+
+// 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 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' {
+ if idx > 0 {
+ byteArray = byteArray[:idx]
+ }
+ uidx := bytes.IndexByte(byteArray, b-32)
+ if uidx >= 0 {
+ idx = uidx
+ }
+ }
+ if idx < 0 {
+ return -1
+ }
+ 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 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
+ }
+ if p > 0 {
+ fmt.Printf("%2d ", p)
+ } else {
+ fmt.Print(" ")
+ }
+ }
+ 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.
+ // If the input string is too long, consider finding the matching chars in
+ // this phase as well (non-optimal alignment).
+ M := len(pattern)
+ if M == 0 {
+ return Result{0, 0, 0}, posArray(withPos, M)
+ }
+ N := input.Length()
+
+ // 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, 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
+ 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
+ maxScore, maxScorePos := int16(0), 0
+ pidx, lastIdx := 0, 0
+ 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)
+ if !caseSensitive && class == charUpper {
+ char += 32
+ }
+ } else {
+ class = charClassOfNonAscii(char)
+ if !caseSensitive && class == charUpper {
+ char = unicode.To(unicode.LowerCase, char)
+ }
+ if normalize {
+ char = normalizeRune(char)
+ }
+ }
+
+ Tsub[off] = char
+ bonus := bonusFor(prevClass, class)
+ Bsub[off] = bonus
+ prevClass = class
+
+ if char == pchar {
+ if pidx < M {
+ F[pidx] = int32(idx + off)
+ pidx++
+ pchar = pattern[util.Min(pidx, M-1)]
+ }
+ lastIdx = idx + off
+ }
+
+ if char == pchar0 {
+ score := scoreMatch + bonus*bonusFirstCharMultiplier
+ H0sub[off] = score
+ C0sub[off] = 1
+ if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
+ maxScore, maxScorePos = score, idx+off
+ if forward && bonus == bonusBoundary {
+ break
+ }
+ }
+ inGap = false
+ } else {
+ if inGap {
+ H0sub[off] = util.Max16(prevH0+scoreGapExtention, 0)
+ } else {
+ H0sub[off] = util.Max16(prevH0+scoreGapStart, 0)
+ }
+ C0sub[off] = 0
+ inGap = true
+ }
+ prevH0 = H0sub[off]
+ }
+ if pidx != M {
+ return Result{-1, -1, 0}, nil
+ }
+ if M == 1 {
+ result := Result{maxScorePos, maxScorePos + 1, int(maxScore)}
+ if !withPos {
+ return result, nil
+ }
+ pos := []int{maxScorePos}
+ return result, &pos
+ }
+
+ // Phase 3. Fill in score matrix (H)
+ // Unlike the original algorithm, we do not allow omission.
+ 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])
+
+ 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
+ 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 inGap {
+ s2 = Hleft[off] + scoreGapExtention
+ } else {
+ s2 = Hleft[off] + scoreGapStart
+ }
+
+ 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[col-int(consecutive)+1]))
+ }
+ if s1+b < s2 {
+ s1 += Bsub[off]
+ consecutive = 0
+ } else {
+ s1 += b
+ }
+ }
+ Csub[off] = consecutive
+
+ inGap = s1 < s2
+ score := util.Max16(util.Max16(s1, s2), 0)
+ if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {
+ maxScore, maxScorePos = score, col
+ }
+ Hsub[off] = score
+ }
+ }
+
+ if DEBUG {
+ debugV2(T, pattern, F, lastIdx, H, C)
+ }
+
+ // Phase 4. (Optional) Backtrace to find character positions
+ pos := posArray(withPos, M)
+ j := f0
+ if withPos {
+ i := M - 1
+ j = maxScorePos
+ preferMatch := true
+ for {
+ I := i * width
+ j0 := j - f0
+ 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, 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
+ 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)
+ }
+ }
+ // pattern is already normalized
+ if normalize {
+ char = normalizeRune(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, 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 {
+ return Result{-1, -1, 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
- lenRunes := len(runes)
+ lenRunes := text.Length()
lenPattern := len(pattern)
- for index := range runes {
- char := runeAt(runes, index, lenRunes, forward)
+ for index := 0; index < lenRunes; index++ {
+ char := text.Get(indexAt(index, lenRunes, forward))
// This is considerably faster than blindly applying strings.ToLower to the
// whole string
if !caseSensitive {
@@ -58,7 +641,10 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
char = unicode.To(unicode.LowerCase, char)
}
}
- pchar := runeAt(pattern, pidx, lenPattern, forward)
+ if normalize {
+ char = normalizeRune(char)
+ }
+ pchar := pattern[indexAt(pidx, lenPattern, forward)]
if char == pchar {
if sidx < 0 {
sidx = index
@@ -73,7 +659,8 @@ 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)
+ tidx := indexAt(index, lenRunes, forward)
+ char := text.Get(tidx)
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
@@ -82,7 +669,8 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
}
}
- pchar := runeAt(pattern, pidx, lenPattern, forward)
+ pidx_ := indexAt(pidx, lenPattern, forward)
+ pchar := pattern[pidx_]
if char == pchar {
if pidx--; pidx < 0 {
sidx = index
@@ -90,12 +678,15 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
}
}
}
- if forward {
- return sidx, eidx
+
+ if !forward {
+ sidx, eidx = lenRunes-eidx, lenRunes-sidx
}
- return lenRunes - eidx, lenRunes - sidx
+
+ score, pos := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, withPos)
+ return Result{sidx, eidx, score}, pos
}
- return -1, -1
+ return Result{-1, -1, 0}, nil
}
// ExactMatchNaive is a basic string searching algorithm that handles case
@@ -103,23 +694,32 @@ func FuzzyMatch(caseSensitive bool, forward bool, runes []rune, pattern []rune)
// 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, runes []rune, pattern []rune) (int, int) {
+// 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, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
if len(pattern) == 0 {
- return 0, 0
+ return Result{0, 0, 0}, nil
}
- lenRunes := len(runes)
+ lenRunes := text.Length()
lenPattern := len(pattern)
if lenRunes < lenPattern {
- return -1, -1
+ 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)
for index := 0; index < lenRunes; index++ {
- char := runeAt(runes, index, lenRunes, forward)
+ index_ := indexAt(index, lenRunes, forward)
+ char := text.Get(index_)
if !caseSensitive {
if char >= 'A' && char <= 'Z' {
char += 32
@@ -127,73 +727,133 @@ func ExactMatchNaive(caseSensitive bool, forward bool, runes []rune, pattern []r
char = unicode.To(unicode.LowerCase, char)
}
}
- pchar := runeAt(pattern, pidx, lenPattern, forward)
+ if normalize {
+ char = normalizeRune(char)
+ }
+ pidx_ := indexAt(pidx, lenPattern, forward)
+ pchar := pattern[pidx_]
if pchar == char {
+ if pidx_ == 0 {
+ bonus = bonusAt(text, index_)
+ }
pidx++
if pidx == lenPattern {
- if forward {
- return index - lenPattern + 1, index + 1
+ if bonus > bestBonus {
+ bestPos, bestBonus = index, bonus
}
- return lenRunes - (index + 1), lenRunes - (index - lenPattern + 1)
+ if bonus == bonusBoundary {
+ break
+ }
+ index -= pidx - 1
+ pidx, bonus = 0, 0
}
} else {
index -= pidx
- pidx = 0
+ pidx, bonus = 0, 0
}
}
- return -1, -1
+ 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, 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, runes []rune, pattern []rune) (int, int) {
- if len(runes) < len(pattern) {
- return -1, -1
+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
+ }
+
+ if text.Length() < len(pattern) {
+ return Result{-1, -1, 0}, nil
}
for index, r := range pattern {
- char := runes[index]
+ char := text.Get(index)
if !caseSensitive {
char = unicode.ToLower(char)
}
+ if normalize {
+ char = normalizeRune(char)
+ }
if char != r {
- return -1, -1
+ return Result{-1, -1, 0}, nil
}
}
- return 0, len(pattern)
+ lenPattern := len(pattern)
+ 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, input []rune, pattern []rune) (int, int) {
- runes := util.TrimRight(input)
- trimmedLen := len(runes)
+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 {
+ return Result{trimmedLen, trimmedLen, 0}, nil
+ }
diff := trimmedLen - len(pattern)
if diff < 0 {
- return -1, -1
+ return Result{-1, -1, 0}, nil
}
for index, r := range pattern {
- char := runes[index+diff]
+ char := text.Get(index + diff)
if !caseSensitive {
char = unicode.ToLower(char)
}
+ if normalize {
+ char = normalizeRune(char)
+ }
if char != r {
- return -1, -1
+ return Result{-1, -1, 0}, nil
}
}
- return trimmedLen - len(pattern), trimmedLen
+ lenPattern := len(pattern)
+ sidx := trimmedLen - lenPattern
+ eidx := trimmedLen
+ 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, runes []rune, pattern []rune) (int, int) {
- if len(runes) != len(pattern) {
- return -1, -1
+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 := string(runes)
- 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) {
- return 0, len(pattern)
+ if match {
+ return Result{0, lenPattern, (scoreMatch+bonusBoundary)*lenPattern +
+ (bonusFirstCharMultiplier-1)*bonusBoundary}, nil
}
- return -1, -1
+ return Result{-1, -1, 0}, nil
}
diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go
index 95a020b..610c30e 100644
--- a/src/algo/algo_test.go
+++ b/src/algo/algo_test.go
@@ -1,69 +1,186 @@
package algo
import (
+ "math"
+ "sort"
"strings"
"testing"
+
+ "github.com/junegunn/fzf/src/util"
)
-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 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)
}
- 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)
+ 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
+ end = res.End
+ } else {
+ sort.Ints(*pos)
+ start = (*pos)[0]
+ end = (*pos)[len(*pos)-1] + 1
}
- if e != eidx {
- t.Errorf("Invalid end index: %d (expected: %d, %s / %s)", e, eidx, input, pattern)
+ if start != sidx {
+ t.Errorf("Invalid start index: %d (expected: %d, %s / %s)", start, sidx, 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)
- 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)
+ 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, 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)
- assertMatch(t, FuzzyMatch, false, false, "foobar fb", "fb", 7, 9)
+ 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)
- assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "oBA", -1, -1)
- assertMatch(t, ExactMatchNaive, true, dir, "fooBarbaz", "fooBarbazz", -1, -1)
+ 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, "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, 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,
+ 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, 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, 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, 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)
- assertMatch(t, SuffixMatch, false, dir, "fooBarbaz", "baz", 6, 9)
- assertMatch(t, SuffixMatch, true, dir, "fooBarbaz", "Baz", -1, -1)
+ 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)
- 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, 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)
}
}
+
+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 {
+ bytes[i] = 'x'
+ }
+ bytes[math.MaxUint16] = 'z'
+ assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive)
+}
diff --git a/src/algo/normalize.go b/src/algo/normalize.go
new file mode 100644
index 0000000..7a49644
--- /dev/null
+++ b/src/algo/normalize.go
@@ -0,0 +1,424 @@
+// 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
+}
+
+// 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/ansi.go b/src/ansi.go
index 876229f..d7c81d3 100644
--- a/src/ansi.go
+++ b/src/ansi.go
@@ -6,6 +6,8 @@ import (
"strconv"
"strings"
"unicode/utf8"
+
+ "github.com/junegunn/fzf/src/tui"
)
type ansiOffset struct {
@@ -14,29 +16,52 @@ type ansiOffset struct {
}
type ansiState struct {
- fg int
- bg int
- bold bool
+ fg tui.Color
+ bg tui.Color
+ attr tui.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
func init() {
- ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]")
+ /*
+ 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.|[\x0e\x0f]|.\x08)")
}
-func extractColor(str string, state *ansiState) (string, []ansiOffset, *ansiState) {
+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) {
var offsets []ansiOffset
var output bytes.Buffer
@@ -44,51 +69,87 @@ func extractColor(str string, state *ansiState) (string, []ansiOffset, *ansiStat
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
}
- idx := 0
- for _, offset := range ansiRegex.FindAllStringIndex(str, -1) {
- output.WriteString(str[idx:offset[0]])
- newState := interpretCode(str[offset[0]:offset[1]], state)
+ prevIdx := 0
+ runeCount := 0
+ for idx := 0; idx < len(str); {
+ idx += findAnsiStart(str[idx:])
+ if idx == len(str) {
+ break
+ }
+ // Make sure that we found an ANSI code
+ offset := ansiRegex.FindStringIndex(str[idx:])
+ if len(offset) < 2 {
+ idx++
+ continue
+ }
+ offset[0] += idx
+ offset[1] += idx
+ idx = offset[1]
+
+ // Check if we should continue
+ prev := str[prevIdx:offset[0]]
+ if proc != nil && !proc(prev, state) {
+ return "", nil, nil
+ }
+
+ 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:]
- 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
- (&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
- }
+ trimmed = output.String()
}
- return output.String(), offsets, state
+ if len(rest) > 0 && state != nil {
+ // Update last offset
+ runeCount += utf8.RuneCountInString(rest)
+ (&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
+ }
+ if proc != nil {
+ proc(rest, state)
+ }
+ if len(offsets) == 0 {
+ return trimmed, nil, state
+ }
+ return trimmed, &offsets, state
}
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[len(ansiCode)-1] == 'K' {
+ if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
return state
}
@@ -98,7 +159,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
init := func() {
state.fg = -1
state.bg = -1
- state.bold = false
+ state.attr = 0
state256 = 0
}
@@ -122,28 +183,56 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
case 49:
state.bg = -1
case 1:
- state.bold = true
+ state.attr = state.attr | tui.Bold
+ case 2:
+ state.attr = state.attr | tui.Dim
+ case 3:
+ state.attr = state.attr | tui.Italic
+ case 4:
+ state.attr = state.attr | tui.Underline
+ case 5:
+ state.attr = state.attr | tui.Blink
+ case 7:
+ state.attr = 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 = tui.Color(num - 90 + 8)
+ } else if num >= 100 && num <= 107 {
+ state.bg = tui.Color(num - 100 + 8)
}
}
case 1:
switch num {
+ case 2:
+ state256 = 10 // MAGIC
case 5:
state256++
default:
state256 = 0
}
case 2:
- *ptr = num
+ *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/ansi_test.go b/src/ansi_test.go
index e278fe9..a536677 100644
--- a/src/ansi_test.go
+++ b/src/ansi_test.go
@@ -3,150 +3,156 @@ package fzf
import (
"fmt"
"testing"
+
+ "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) {
+ assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) {
+ var attr tui.Attr
+ if bold {
+ attr = tui.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)
}
}
src := "hello world"
var state *ansiState
clean := "\x1b[0m"
- check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) {
- output, ansiOffsets, newState := extractColor(src, state)
+ check := func(assertion func(ansiOffsets *[]ansiOffset, state *ansiState)) {
+ 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)
}
- 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 {
+ src = "\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d"
+ 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 {
+ if state.fg != 2 || state.bg != -1 || state.attr == 0 {
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 {
+ if state.fg != 2 || state.bg != -1 || state.attr == 0 {
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 {
+ if state.fg != 200 || state.bg != 100 || state.attr > 0 {
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..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][]*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
}
@@ -33,10 +33,10 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) {
(*qc)[key] = list
}
-// Find is called to lookup ChunkCache
-func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Item, bool) {
+// Lookup is called to lookup ChunkCache
+func (cc *ChunkCache) Lookup(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) ([]*Item, 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/cache_test.go b/src/cache_test.go
index 05f904c..5d9c5cc 100644
--- a/src/cache_test.go
+++ b/src/cache_test.go
@@ -4,37 +4,36 @@ import "testing"
func TestChunkCache(t *testing.T) {
cache := NewChunkCache()
- chunk2 := make(Chunk, chunkSize)
chunk1p := &Chunk{}
- chunk2p := &chunk2
- items1 := []*Item{&Item{}}
- items2 := []*Item{&Item{}, &Item{}}
+ chunk2p := &Chunk{count: chunkSize}
+ items1 := []Result{Result{}}
+ items2 := []Result{Result{}, Result{}}
cache.Add(chunk1p, "foo", items1)
cache.Add(chunk2p, "foo", items1)
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.go b/src/chunklist.go
index a953fae..510cd73 100644
--- a/src/chunklist.go
+++ b/src/chunklist.go
@@ -2,17 +2,18 @@ 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 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
+// 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
}
@@ -21,15 +22,13 @@ 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 {
- item := trans(data, index)
- if item != nil {
- *c = append(*c, item)
+func (c *Chunk) push(trans ItemBuilder, data []byte) bool {
+ if trans(&c.items[c.count], data) {
+ c.count++
return true
}
return false
@@ -37,7 +36,7 @@ func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool {
// 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,45 +48,35 @@ 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
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))
- cl.chunks = append(cl.chunks, &newChunk)
+ cl.chunks = append(cl.chunks, &Chunk{})
}
- if cl.lastChunk().push(cl.trans, data, cl.count) {
- cl.count++
- return true
- }
- return false
+ ret := cl.lastChunk().push(cl.trans, data)
+ cl.mutex.Unlock()
+ return ret
}
// 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)
// 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
+ cl.mutex.Unlock()
+ return ret, CountItems(ret)
}
diff --git a/src/chunklist_test.go b/src/chunklist_test.go
index 26795ef..6c1d09e 100644
--- a/src/chunklist_test.go
+++ b/src/chunklist_test.go
@@ -3,11 +3,17 @@ package fzf
import (
"fmt"
"testing"
+
+ "github.com/junegunn/fzf/src/util"
)
func TestChunkList(t *testing.T) {
- cl := NewChunkList(func(s []byte, i int) *Item {
- return &Item{text: []rune(string(s)), rank: Rank{0, 0, uint32(i * 2)}}
+ // FIXME global
+ sortCriteria = []criterion{byScore, byLength}
+
+ cl := NewChunkList(func(item *Item, s []byte) bool {
+ item.text = util.ToChars(s)
+ return true
})
// Snapshot
@@ -33,11 +39,11 @@ 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 string((*chunk1)[0].text) != "hello" || (*chunk1)[0].rank.index != 0 ||
- string((*chunk1)[1].text) != "world" || (*chunk1)[1].rank.index != 2 {
+ if chunk1.items[0].text.ToString() != "hello" ||
+ chunk1.items[1].text.ToString() != "world" {
t.Error("Invalid data")
}
if chunk1.IsFull() {
@@ -60,14 +66,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/constants.go b/src/constants.go
index 1cc4f81..e6ed170 100644
--- a/src/constants.go
+++ b/src/constants.go
@@ -1,6 +1,7 @@
package fzf
import (
+ "os"
"time"
"github.com/junegunn/fzf/src/util"
@@ -8,25 +9,35 @@ import (
const (
// Current version
- version = "0.10.5"
+ version = "0.17.1"
// Core
coordinatorDelayMax time.Duration = 100 * time.Millisecond
coordinatorDelayStep time.Duration = 10 * time.Millisecond
// Reader
- defaultCommand = `find . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
+ readerBufferSize = 64 * 1024
+ readerPollIntervalMin = 10 * time.Millisecond
+ readerPollIntervalStep = 5 * time.Millisecond
+ readerPollIntervalMax = 50 * time.Millisecond
// Terminal
- initialDelay = 100 * time.Millisecond
+ initialDelay = 20 * time.Millisecond
+ initialDelayTac = 100 * time.Millisecond
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
@@ -35,8 +46,23 @@ const (
// History
defaultHistoryMax int = 1000
+
+ // Jump labels
+ defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+"
)
+var defaultCommand string
+
+func init() {
+ if !util.IsWindows() {
+ 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 {
+ defaultCommand = `dir /s/b`
+ }
+}
+
// fzf events
const (
EvtReadNew util.EventType = iota
@@ -45,7 +71,7 @@ const (
EvtSearchProgress
EvtSearchFin
EvtHeader
- EvtClose
+ EvtReady
)
const (
diff --git a/src/core.go b/src/core.go
index 35d7ced..023f7be 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) 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
@@ -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)
@@ -48,14 +43,16 @@ Matcher -> EvtHeader -> Terminal (update header)
*/
// Run starts fzf
-func Run(opts *Options) {
- initProcs()
-
+func Run(opts *Options, revision string) {
sort := opts.Sort > 0
- rankTiebreak = opts.Tiebreak
+ 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)
}
@@ -63,110 +60,110 @@ func Run(opts *Options) {
eventBox := util.NewEventBox()
// ANSI code processor
- ansiProcessor := func(data []byte) ([]rune, []ansiOffset) {
- return util.BytesToRunes(data), nil
- }
- ansiProcessorRunes := func(data []rune) ([]rune, []ansiOffset) {
- return data, nil
+ ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
+ return util.ToChars(data), nil
}
if opts.Ansi {
if opts.Theme != nil {
var state *ansiState
- ansiProcessor = func(data []byte) ([]rune, []ansiOffset) {
- trimmed, offsets, newState := extractColor(string(data), state)
+ ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
+ trimmed, offsets, newState := extractColor(string(data), state, nil)
state = newState
- return []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) ([]rune, []ansiOffset) {
- trimmed, _, _ := extractColor(string(data), nil)
- return []rune(trimmed), nil
+ ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
+ trimmed, _, _ := extractColor(string(data), nil, nil)
+ return util.ToChars([]byte(trimmed)), nil
}
}
- ansiProcessorRunes = func(data []rune) ([]rune, []ansiOffset) {
- return ansiProcessor([]byte(string(data)))
- }
}
// Chunk list
var chunkList *ChunkList
+ var itemIndex int32
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) bool {
if len(header) < opts.HeaderLines {
header = append(header, string(data))
eventBox.Set(EvtHeader, header)
- return nil
+ return false
}
- runes, colors := ansiProcessor(data)
- return &Item{
- text: runes,
- index: uint32(index),
- colors: colors,
- rank: Rank{0, 0, uint32(index)}}
+ item.text, item.colors = ansiProcessor(data)
+ item.text.Index = itemIndex
+ itemIndex++
+ return true
})
} else {
- chunkList = NewChunkList(func(data []byte, index int) *Item {
- runes := util.BytesToRunes(data)
- tokens := Tokenize(runes, opts.Delimiter)
+ chunkList = NewChunkList(func(item *Item, data []byte) bool {
+ 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 nil
+ return false
}
- item := Item{
- text: joinTokens(trans),
- origText: &runes,
- index: uint32(index),
- colors: nil,
- rank: Rank{0, 0, uint32(index)}}
-
- trimmed, colors := ansiProcessorRunes(item.text)
- item.text = trimmed
- item.colors = colors
- return &item
+ item.text, item.colors = ansiProcessor([]byte(transformed))
+ item.text.Index = itemIndex
+ item.origText = &data
+ itemIndex++
+ return true
})
}
// 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()
}
// 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.Mode, opts.Case, opts.Tiebreak != byEnd,
- opts.Nth, opts.Delimiter, runes)
+ 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)
// Filtering mode
if opts.Filter != nil {
if opts.PrintQuery {
- fmt.Println(*opts.Filter)
+ opts.Printer(*opts.Filter)
}
pattern := patternBuilder([]rune(*opts.Filter))
found := false
if streamingFilter {
- reader := Reader{
+ slab := util.MakeSlab(slab16Size, slab32Size)
+ reader := NewReader(
func(runes []byte) bool {
- item := chunkList.trans(runes, 0)
- if item != nil && pattern.MatchItem(item) {
- fmt.Println(string(item.text))
- found = true
+ item := Item{}
+ if chunkList.trans(&item, runes) {
+ if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
+ opts.Printer(item.text.ToString())
+ found = true
+ }
}
return false
- }, eventBox, opts.ReadZero}
+ }, eventBox, opts.ReadZero)
reader.ReadSource()
} else {
eventBox.Unwatch(EvtReadNew)
@@ -177,7 +174,7 @@ func Run(opts *Options) {
chunks: snapshot,
pattern: pattern})
for i := 0; i < merger.Length(); i++ {
- fmt.Println(merger.Get(i).AsString(opts.Ansi))
+ opts.Printer(merger.Get(i).item.AsString(opts.Ansi))
found = true
}
}
@@ -212,14 +209,19 @@ func Run(opts *Options) {
delay := true
ticks++
eventBox.Wait(func(events *util.Events) {
- defer events.Clear()
+ if _, fin := (*events)[EvtReadFin]; fin {
+ delete(*events, EvtReadNew)
+ }
for evt, value := range *events {
switch evt {
case EvtReadNew, EvtReadFin:
reading = reading && evt == EvtReadNew
snapshot, count := chunkList.Snapshot()
- terminal.UpdateCount(count, !reading)
+ 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:
@@ -251,13 +253,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).AsString(opts.Ansi))
+ opts.Printer(val.Get(i).item.AsString(opts.Ansi))
}
if count > 0 {
os.Exit(exitOk)
@@ -272,6 +274,7 @@ func Run(opts *Options) {
}
}
}
+ events.Clear()
})
if delay && reading {
dur := util.DurWithin(
diff --git a/src/curses/curses.go b/src/curses/curses.go
deleted file mode 100644
index 59cea3b..0000000
--- a/src/curses/curses.go
+++ /dev/null
@@ -1,556 +0,0 @@
-package curses
-
-/*
-#include
-#include
-#cgo LDFLAGS: -lncurses
-*/
-import "C"
-
-import (
- "fmt"
- "os"
- "os/signal"
- "syscall"
- "time"
- "unicode/utf8"
-)
-
-// 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
-
- BTab
- BSpace
-
- Del
- PgUp
- PgDn
-
- Up
- Down
- Left
- Right
- Home
- End
-
- SLeft
- SRight
-
- F1
- F2
- F3
- F4
-
- AltBS
- AltA
- AltB
- AltC
- AltD
- AltE
- AltF
-
- AltZ = AltA + 'z' - 'a'
-)
-
-// Pallete
-const (
- ColNormal = iota
- ColPrompt
- ColMatch
- ColCurrent
- ColCurrentMatch
- ColSpinner
- ColInfo
- ColCursor
- ColSelected
- ColHeader
- ColUser
-)
-
-const (
- doubleClickDuration = 500 * time.Millisecond
-)
-
-type ColorTheme struct {
- UseDefault bool
- Fg int16
- Bg int16
- DarkBg int16
- Prompt int16
- Match int16
- Current int16
- CurrentMatch int16
- Spinner int16
- Info int16
- Cursor int16
- Selected int16
- Header 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 func(int, bool) C.int
- _colorMap map[int]int
- _prevDownTime time.Time
- _clickY []int
- _screen *C.SCREEN
- Default16 *ColorTheme
- Dark256 *ColorTheme
- Light256 *ColorTheme
- FG int
- CurrentFG int
- BG int
- DarkBG int
-)
-
-func init() {
- _prevDownTime = time.Unix(0, 0)
- _clickY = []int{}
- _colorMap = make(map[int]int)
- Default16 = &ColorTheme{
- UseDefault: true,
- Fg: 15,
- Bg: 0,
- 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}
- Dark256 = &ColorTheme{
- UseDefault: true,
- Fg: 15,
- Bg: 0,
- DarkBg: 236,
- Prompt: 110,
- Match: 108,
- Current: 254,
- CurrentMatch: 151,
- Spinner: 148,
- Info: 144,
- Cursor: 161,
- Selected: 168,
- Header: 109}
- Light256 = &ColorTheme{
- UseDefault: true,
- Fg: 15,
- Bg: 0,
- DarkBg: 251,
- Prompt: 25,
- Match: 66,
- Current: 237,
- CurrentMatch: 23,
- Spinner: 65,
- Info: 101,
- Cursor: 161,
- Selected: 168,
- Header: 31}
-}
-
-func attrColored(pair int, bold bool) 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
-}
-
-func attrMono(pair int, bold bool) C.int {
- var attr C.int
- switch pair {
- case ColCurrent:
- if bold {
- attr = C.A_REVERSE
- }
- case ColMatch:
- attr = C.A_UNDERLINE
- case ColCurrentMatch:
- attr = C.A_UNDERLINE | C.A_REVERSE
- }
- if 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])
-}
-
-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.newterm(nil, C.stderr, C.stdin)
- if _screen == nil {
- fmt.Println("Invalid $TERM: " + os.Getenv("TERM"))
- os.Exit(2)
- }
- C.set_term(_screen)
- if mouse {
- C.mousemask(C.ALL_MOUSE_EVENTS, nil)
- }
- 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)
- _color = attrColored
- } else {
- _color = attrMono
- }
-}
-
-func initPairs(theme *ColorTheme, black bool) {
- fg := C.short(theme.Fg)
- bg := C.short(theme.Bg)
- if black {
- bg = C.COLOR_BLACK
- } else if theme.UseDefault {
- fg = -1
- bg = -1
- C.use_default_colors()
- }
- if theme.UseDefault {
- FG = -1
- BG = -1
- } else {
- FG = int(fg)
- BG = int(bg)
- C.assume_default_colors(C.int(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)
-}
-
-func Close() {
- C.endwin()
- C.delscreen(_screen)
-}
-
-func GetBytes() []byte {
- c := getch(false)
- _buf = append(_buf, byte(c))
-
- for {
- c = getch(true)
- if c == -1 {
- break
- }
- _buf = append(_buf, byte(c))
- }
-
- return _buf
-}
-
-// 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
- return Event{Mouse, 0, &MouseEvent{0, 0, s, false, false, mod}}
- }
- return Event{Invalid, 0, nil}
-}
-
-func escSequence(sz *int) Event {
- if len(_buf) < 2 {
- return Event{ESC, 0, nil}
- }
- *sz = 2
- switch _buf[1] {
- 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(_buf) < 3 {
- 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:
- 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 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}
- }
- 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}
- case 127:
- return Event{BSpace, 0, nil}
- case ESC:
- return escSequence(&sz)
- }
-
- // CTRL-A ~ CTRL-Z
- if _buf[0] <= CtrlZ {
- return Event{int(_buf[0]), 0, nil}
- }
- r, rsz := utf8.DecodeRune(_buf)
- if r == utf8.RuneError {
- return Event{ESC, 0, nil}
- }
- sz = rsz
- return Event{Rune, r, nil}
-}
-
-func Move(y int, x int) {
- C.move(C.int(y), C.int(x))
-}
-
-func MoveAndClear(y int, x int) {
- Move(y, x)
- C.clrtoeol()
-}
-
-func Print(text string) {
- C.addstr(C.CString(text))
-}
-
-func CPrint(pair int, bold bool, text string) {
- attr := _color(pair, bold)
- C.attron(attr)
- Print(text)
- C.attroff(attr)
-}
-
-func Clear() {
- C.clear()
-}
-
-func Endwin() {
- C.endwin()
-}
-
-func Refresh() {
- C.refresh()
-}
-
-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/curses/curses_test.go b/src/curses/curses_test.go
deleted file mode 100644
index db75c40..0000000
--- a/src/curses/curses_test.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package curses
-
-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/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/item.go b/src/item.go
index 12ca3df..cb778cb 100644
--- a/src/item.go
+++ b/src/item.go
@@ -1,267 +1,44 @@
package fzf
import (
- "math"
-
- "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
-}
-
-// Item represents each input line
+// Item represents each input line. 56 bytes.
type Item struct {
- text []rune
- origText *[]rune
- transformed []Token
- index uint32
- offsets []Offset
- colors []ansiOffset
- rank Rank
+ text util.Chars // 32 = 24 + 1 + 1 + 2 + 4
+ transformed *[]Token // 8
+ origText *[]byte // 8
+ colors *[]ansiOffset // 8
}
-// Rank is used to sort the search result
-type Rank struct {
- matchlen uint16
- tiebreak uint16
- index uint32
+// Index returns ordinal index of the Item
+func (item *Item) Index() int32 {
+ return item.text.Index
}
-// Tiebreak criterion to use. Never changes once fzf is started.
-var rankTiebreak tiebreak
+var minItem = Item{text: util.Chars{Index: -1}}
-// Rank calculates rank of the Item
-func (item *Item) Rank(cache bool) Rank {
- if cache && (item.rank.matchlen > 0 || item.rank.tiebreak > 0) {
- return item.rank
+func (item *Item) TrimLength() uint16 {
+ return item.text.TrimLength()
+}
+
+// Colors returns ansiOffsets of the Item
+func (item *Item) Colors() []ansiOffset {
+ if item.colors == nil {
+ return []ansiOffset{}
}
- matchlen := 0
- prevEnd := 0
- minBegin := math.MaxUint16
- for _, offset := range item.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
- }
- }
- var tiebreak uint16
- switch rankTiebreak {
- 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)
- }
- tiebreak = uint16(lenSum)
- } else {
- tiebreak = uint16(len(item.text))
- }
- 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 := Rank{uint16(matchlen), tiebreak, item.index}
- 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)
- return &trimmed
+ trimmed, _, _ := extractColor(string(*item.origText), nil, nil)
+ return trimmed
}
- orig := string(*item.origText)
- return &orig
+ return string(*item.origText)
}
- str := string(item.text)
- return &str
-}
-
-func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {
- if len(item.colors) == 0 {
- var offsets []colorOffset
- for _, off := range item.offsets {
- offsets = append(offsets, colorOffset{offset: off, 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: Offset{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: Offset{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 Rank, jrank Rank, tac bool) bool {
- if irank.matchlen < jrank.matchlen {
- return true
- } else if irank.matchlen > jrank.matchlen {
- return false
- }
-
- if irank.tiebreak < jrank.tiebreak {
- return true
- } else if irank.tiebreak > jrank.tiebreak {
- return false
- }
-
- return (irank.index <= jrank.index) != tac
+ return item.text.ToString()
}
diff --git a/src/item_test.go b/src/item_test.go
index 5b9232a..1efb5f1 100644
--- a/src/item_test.go
+++ b/src/item_test.go
@@ -1,104 +1,23 @@
package fzf
import (
- "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(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) {
- 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) {
- t.Error("Invalid order (tac)")
- }
-}
-
-// Match length, string length, index
-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 {
- t.Error(item1.Rank(true))
- }
- // Only differ in index
- item2 := Item{text: strs[0], index: 0, offsets: []Offset{}}
-
- items := []*Item{&item1, &item2}
- sort.Sort(ByRelevance(items))
- if items[0] != &item2 || items[1] != &item1 {
- t.Error(items)
- }
-
- items = []*Item{&item2, &item1, &item1, &item2}
- sort.Sort(ByRelevance(items))
- if items[0] != &item2 || items[1] != &item2 ||
- items[2] != &item1 || items[3] != &item1 {
- t.Error(items)
- }
-
- // Sort by relevance
- item3 := Item{text: strs[1], rank: 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}}}
- 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 {
- 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 3ea2fbe..c29f2b6 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: runtime.NumCPU(),
+ partitions: partitions,
+ slab: make([]*util.Slab, partitions),
mergerCache: make(map[string]*Merger)}
}
@@ -106,18 +109,19 @@ func (m *Matcher) Loop() {
}
func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
- perSlice := len(chunks) / m.partitions
+ partitions := m.partitions
+ perSlice := len(chunks) / partitions
- // No need to parallelize
if perSlice == 0 {
- return [][]*Chunk{chunks}
+ partitions = len(chunks)
+ perSlice = 1
}
- slices := make([][]*Chunk, m.partitions)
- for i := 0; i < m.partitions; i++ {
+ slices := make([][]*Chunk, partitions)
+ for i := 0; i < partitions; i++ {
start := i * perSlice
end := start + perSlice
- if i == m.partitions-1 {
+ if i == partitions-1 {
end = len(chunks)
}
slices[i] = chunks[start:end]
@@ -127,7 +131,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) {
@@ -152,17 +156,26 @@ 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() }()
- sliceMatches := []*Item{}
- for _, chunk := range chunks {
- matches := request.pattern.Match(chunk)
- sliceMatches = append(sliceMatches, matches...)
+ count := 0
+ allMatches := make([][]Result, len(chunks))
+ for idx, chunk := range chunks {
+ matches := request.pattern.Match(chunk, slab)
+ 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))
@@ -171,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 {
@@ -199,12 +212,12 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
}
}
- partialResults := make([][]*Item, numSlices)
- for range slices {
+ partialResults := make([][]Result, numSlices)
+ for _ = range slices {
partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches
}
- return NewMerger(partialResults, m.sort, m.tac), false
+ return NewMerger(pattern, partialResults, m.sort, m.tac), false
}
// Reset is called to interrupt/signal the ongoing search
diff --git a/src/merger.go b/src/merger.go
index cce8a94..d710f2a 100644
--- a/src/merger.go
+++ b/src/merger.go
@@ -2,14 +2,15 @@ package fzf
import "fmt"
-// Merger with no data
-var EmptyMerger = NewMerger([][]*Item{}, false, false)
+// EmptyMerger is a Merger with no data
+var EmptyMerger = NewMerger(nil, [][]Result{}, false, false)
// Merger holds a set of locally sorted lists of items and provides the view of
// a single, globally-sorted list
type Merger struct {
- lists [][]*Item
- merged []*Item
+ pattern *Pattern
+ lists [][]Result
+ merged []Result
chunks *[]*Chunk
cursors []int
sorted bool
@@ -22,21 +23,23 @@ 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)
+ mg.count += chunk.count
}
return &mg
}
// NewMerger returns a new Merger
-func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger {
+func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger {
mg := Merger{
+ pattern: pattern,
lists: lists,
- merged: []*Item{},
+ merged: []Result{},
chunks: nil,
cursors: make([]int, len(lists)),
sorted: sorted,
@@ -55,14 +58,14 @@ func (mg *Merger) Length() int {
return mg.count
}
-// Get returns the pointer to the Item object indexed by the given integer
-func (mg *Merger) Get(idx int) *Item {
+// Get returns the pointer to the Result object indexed by the given integer
+func (mg *Merger) Get(idx int) Result {
if mg.chunks != nil {
if mg.tac {
idx = mg.count - idx - 1
}
chunk := (*mg.chunks)[idx/chunkSize]
- return (*chunk)[idx%chunkSize]
+ return Result{item: &chunk.items[idx%chunkSize]}
}
if mg.sorted {
@@ -86,9 +89,9 @@ func (mg *Merger) cacheable() bool {
return mg.count < mergerCacheMax
}
-func (mg *Merger) mergedGet(idx int) *Item {
+func (mg *Merger) mergedGet(idx int) Result {
for i := len(mg.merged); i <= idx; i++ {
- minRank := Rank{0, 0, 0}
+ minRank := minRank()
minIdx := -1
for listIdx, list := range mg.lists {
cursor := mg.cursors[listIdx]
@@ -97,7 +100,7 @@ func (mg *Merger) mergedGet(idx int) *Item {
continue
}
if cursor >= 0 {
- rank := list[cursor].Rank(false)
+ rank := list[cursor]
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 b7a2993..c6af4f6 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) {
@@ -13,18 +15,11 @@ func assert(t *testing.T, cond bool, msg ...string) {
}
}
-func randItem() *Item {
+func randResult() Result {
str := fmt.Sprintf("%d", rand.Uint32())
- offsets := make([]Offset, rand.Int()%3)
- for idx := range offsets {
- sidx := int32(rand.Uint32() % 20)
- eidx := sidx + int32(rand.Uint32()%20)
- offsets[idx] = Offset{sidx, eidx}
- }
- return &Item{
- text: []rune(str),
- index: rand.Uint32(),
- offsets: offsets}
+ chars := util.ToChars([]byte(str))
+ chars.Index = rand.Int31()
+ return Result{item: &Item{text: chars}}
}
func TestEmptyMerger(t *testing.T) {
@@ -34,23 +29,23 @@ func TestEmptyMerger(t *testing.T) {
assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
}
-func buildLists(partiallySorted bool) ([][]*Item, []*Item) {
+func buildLists(partiallySorted bool) ([][]Result, []Result) {
numLists := 4
- lists := make([][]*Item, numLists)
+ lists := make([][]Result, numLists)
cnt := 0
for i := 0; i < numLists; i++ {
- numItems := rand.Int() % 20
- cnt += numItems
- lists[i] = make([]*Item, numItems)
- for j := 0; j < numItems; j++ {
- item := randItem()
+ numResults := rand.Int() % 20
+ cnt += numResults
+ lists[i] = make([]Result, numResults)
+ for j := 0; j < numResults; j++ {
+ item := randResult()
lists[i][j] = item
}
if partiallySorted {
sort.Sort(ByRelevance(lists[i]))
}
}
- items := []*Item{}
+ items := []Result{}
for _, list := range lists {
items = append(items, list...)
}
@@ -62,7 +57,7 @@ func TestMergerUnsorted(t *testing.T) {
cnt := len(items)
// Not sorted: same order
- mg := NewMerger(lists, false, false)
+ mg := NewMerger(nil, lists, false, false)
assert(t, cnt == mg.Length(), "Invalid Length")
for i := 0; i < cnt; i++ {
assert(t, items[i] == mg.Get(i), "Invalid Get")
@@ -74,7 +69,7 @@ func TestMergerSorted(t *testing.T) {
cnt := len(items)
// Sorted sorted order
- mg := NewMerger(lists, true, false)
+ mg := NewMerger(nil, lists, true, false)
assert(t, cnt == mg.Length(), "Invalid Length")
sort.Sort(ByRelevance(items))
for i := 0; i < cnt; i++ {
@@ -84,7 +79,7 @@ func TestMergerSorted(t *testing.T) {
}
// Inverse order
- mg2 := NewMerger(lists, true, false)
+ mg2 := NewMerger(nil, lists, true, false)
for i := cnt - 1; i >= 0; i-- {
if items[i] != mg2.Get(i) {
t.Error("Not sorted", items[i], mg2.Get(i))
diff --git a/src/options.go b/src/options.go
index 9d8aaa1..00b2afd 100644
--- a/src/options.go
+++ b/src/options.go
@@ -1,52 +1,81 @@
package fzf
import (
+ "fmt"
"os"
"regexp"
"strconv"
"strings"
"unicode/utf8"
- "github.com/junegunn/fzf/src/curses"
+ "github.com/junegunn/fzf/src/algo"
+ "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]
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
+ --algo=TYPE Fuzzy matching algorithm: [v1|v2] (default: v2)
-i Case-insensitive match (default: smart-case match)
+i Case-sensitive match
+ --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])
- --with-nth=N[,..] Transform item using index expressions within finder
- -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
+ integer or a range expression ([BEGIN]..[END]).
+ --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=CRITERION Sort criterion when the scores are tied;
- [length|begin|end|index] (default: length)
+ --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
+ 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)
+ --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)
+ --filepath-word Make word-wise movements respect path separators
+ --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
+ --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: '> ')
- --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
+ --no-bold Do not use bold text
+
+ 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[%]][:wrap][:hidden]
+
Scripting
-q, --query=STR Start the finder with the given query
-1, --select-1 Automatically select the only match
@@ -54,24 +83,17 @@ 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
+ --version Display version information and exit
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 Default 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
@@ -83,38 +105,69 @@ const (
)
// Sort criteria
-type tiebreak int
+type criterion int
const (
- byLength tiebreak = iota
+ byScore criterion = iota
+ byLength
byBegin
byEnd
- byIndex
)
-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
+ wrap bool
}
// Options stores the values of command-line options
type Options struct {
- Mode Mode
+ Fuzzy bool
+ FuzzyAlgo algo.Algo
+ Extended bool
Case Case
+ Normalize bool
Nth []Range
WithNth []Range
Delimiter Delimiter
Sort int
Tac bool
- Tiebreak tiebreak
+ Criteria []criterion
Multi bool
Ansi bool
Mouse bool
- Theme *curses.ColorTheme
+ Theme *tui.ColorTheme
Black bool
+ Bold bool
+ Height sizeSpec
+ MinHeight int
Reverse bool
Cycle bool
Hscroll bool
+ HscrollOff int
+ FileWord bool
InlineInfo bool
+ JumpLabels string
Prompt string
Query string
Select1 bool
@@ -122,44 +175,49 @@ 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
+ Printer func(string)
Sync bool
History *History
Header []string
HeaderLines int
- Margin [4]string
+ Margin [4]sizeSpec
+ Bordered bool
+ Tabstop int
+ ClearOnExit bool
Version bool
}
-func defaultTheme() *curses.ColorTheme {
- if strings.Contains(os.Getenv("TERM"), "256") {
- return curses.Dark256
- }
- return curses.Default16
-}
-
func defaultOptions() *Options {
return &Options{
- Mode: ModeFuzzy,
+ Fuzzy: true,
+ FuzzyAlgo: algo.FuzzyMatchV2,
+ Extended: true,
Case: CaseSmart,
+ Normalize: true,
Nth: make([]Range, 0),
WithNth: make([]Range, 0),
Delimiter: Delimiter{},
Sort: 1000,
Tac: false,
- Tiebreak: byLength,
+ Criteria: []criterion{byScore, byLength},
Multi: false,
Ansi: false,
Mouse: true,
- Theme: defaultTheme(),
+ Theme: tui.EmptyTheme(),
Black: false,
+ Bold: true,
+ MinHeight: 10,
Reverse: false,
Cycle: false,
Hscroll: true,
+ HscrollOff: 10,
+ FileWord: false,
InlineInfo: false,
+ JumpLabels: defaultJumpLabels,
Prompt: "> ",
Query: "",
Select1: false,
@@ -167,15 +225,18 @@ func defaultOptions() *Options {
Filter: nil,
ToggleSort: false,
Expect: make(map[int]string),
- Keymap: defaultKeymap(),
- Execmap: make(map[int]string),
+ Keymap: make(map[int][]action),
+ Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false},
PrintQuery: false,
ReadZero: false,
+ Printer: func(str string) { fmt.Println(str) },
Sync: false,
History: nil,
Header: make([]string, 0),
HeaderLines: 0,
Margin: defaultMargin(),
+ Tabstop: 8,
+ ClearOnExit: true,
Version: false}
}
@@ -208,7 +269,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]
}
@@ -289,6 +350,22 @@ 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":
+ 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)
@@ -308,50 +385,72 @@ 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 "ctrl-space":
+ chord = tui.CtrlSpace
+ case "change":
+ chord = tui.Change
+ case "alt-enter", "alt-return":
+ chord = tui.CtrlAltM
+ case "alt-space":
+ chord = tui.AltSpace
+ case "alt-/":
+ 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 = 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 = curses.CtrlA + int(lkey[5]) - 'a'
+ 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 = curses.AltA + int(lkey[4]) - 'a'
- } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '4' {
- chord = curses.F1 + int(key[1]) - '1'
+ 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 {
- chord = curses.AltZ + int([]rune(key)[0])
+ chord = tui.AltZ + int([]rune(key)[0])
} else {
errorExit("unsupported key: " + key)
}
@@ -363,37 +462,60 @@ 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{byScore}
+ 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")
+ 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)
+ }
+ }
+ return criteria
}
-func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
- dupe := *theme
- return &dupe
+func dupeTheme(theme *tui.ColorTheme) *tui.ColorTheme {
+ if theme != nil {
+ dupe := *theme
+ return &dupe
+ }
+ return nil
}
-func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.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":
- 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:
@@ -402,25 +524,29 @@ 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, ":")
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 := int16(ansi32)
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+":
@@ -429,6 +555,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":
@@ -461,20 +589,32 @@ func firstKey(keymap map[int]string) int {
const (
escapedColon = 0
escapedComma = 1
+ escapedPlus = 2
)
-func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) {
- 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:.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
- }
+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) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
- return ":execute(" + strings.Repeat(" ", len(src)-10) + ")"
+ prefix := ":execute"
+ if src[len(prefix)] == '-' {
+ c := src[len(prefix)+1]
+ if c == 's' || c == 'S' {
+ prefix += "-silent"
+ } else {
+ prefix += "-multi"
+ }
+ }
+ 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)
+ masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1)
idx := 0
for _, pairStr := range strings.Split(masked, ",") {
@@ -487,160 +627,277 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
}
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 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 "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 "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-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 "previous-history":
- keymap[key] = actPreviousHistory
- case "next-history":
- keymap[key] = actNextHistory
- case "toggle-sort":
- keymap[key] = actToggleSort
- toggleSort = true
- default:
- if isExecuteAction(actLower) {
- keymap[key] = actExecute
- if act[7] == ':' {
- execmap[key] = act[8:]
- } else {
- execmap[key] = act[8 : 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 "top":
+ appendAction(actTop)
+ 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-preview-wrap":
+ appendAction(actTogglePreviewWrap)
+ 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
+ switch t {
+ case actExecuteSilent:
+ offset = len("execute-silent")
+ case actExecuteMulti:
+ offset = len("execute-multi")
+ default:
+ 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
}
- return keymap, execmap, toggleSort
}
-func isExecuteAction(str string) bool {
- if !strings.HasPrefix(str, "execute") || len(str) < 9 {
- return false
+func isExecuteAction(str string) actionType {
+ matches := executeRegexp.FindAllStringSubmatch(":"+str, -1)
+ if matches == nil || len(matches) != 1 {
+ return actIgnore
}
- b := str[7]
- e := str[len(str)-1]
- if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' ||
- b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") {
- return true
+ prefix := matches[0][1]
+ if len(prefix) == 0 {
+ prefix = matches[0][2]
}
- return false
+ switch prefix {
+ case "execute":
+ return actExecute
+ case "execute-silent":
+ return actExecuteSilent
+ case "execute-multi":
+ return actExecuteMulti
+ }
+ return actIgnore
}
-func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
+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
- return keymap
+ keymap[firstKey(keys)] = toActions(actToggleSort)
}
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")
+ }
+ 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 parseHeight(str string) sizeSpec {
+ size := parseSize(str, 100, "height")
+ return size
+}
+
+func parsePreviewWindow(opts *previewOpts, input string) {
+ // Default
+ opts.position = posRight
+ opts.size = sizeSpec{50, true}
+ opts.hidden = false
+ opts.wrap = false
+
+ tokens := strings.Split(input, ":")
+ sizeRegex := regexp.MustCompile("^[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)
}
}
- return str
+ }
+ if !opts.size.percent && opts.size.size > 0 {
+ // Adjust size for border
+ opts.size.size += 2
+ // And padding
+ if opts.position == posLeft || opts.position == posRight {
+ opts.size.size += 2
+ }
+ }
+}
+
+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:
@@ -650,7 +907,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
@@ -673,39 +929,54 @@ func parseOptions(opts *Options, allArgs []string) {
opts.History.maxSize = historyMax
}
}
+ validateJumpLabels := false
for i := 0; i < len(allArgs); i++ {
arg := allArgs[i]
switch arg {
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":
filter := nextString(allArgs, &i, "query string required")
opts.Filter = &filter
+ 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":
- 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":
- 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"))
+ parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"))
case "--color":
spec := optionalNextString(allArgs, &i)
if len(spec) == 0 {
- opts.Theme = defaultTheme()
+ opts.Theme = tui.EmptyTheme()
} else {
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":
@@ -737,11 +1008,15 @@ 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":
opts.Black = false
+ case "--bold":
+ opts.Bold = true
+ case "--no-bold":
+ opts.Bold = false
case "--reverse":
opts.Reverse = true
case "--no-reverse":
@@ -754,10 +1029,19 @@ 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 "--filepath-word":
+ opts.FileWord = true
+ case "--no-filepath-word":
+ opts.FileWord = false
case "--inline-info":
opts.InlineInfo = true
case "--no-inline-info":
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":
@@ -770,6 +1054,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":
@@ -797,15 +1085,40 @@ 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[%]][:wrap][:hidden]"))
+ case "--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":
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)"))
+ 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:
- 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
@@ -819,18 +1132,22 @@ 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, "--min-height="); match {
+ opts.MinHeight = atoi(value)
} 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")
+ for k, v := range parseKeyChords(value, "key names required") {
+ opts.Expect[k] = v
+ }
} 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 {
- keymap, opts.Execmap, opts.ToggleSort =
- parseKeymap(keymap, opts.Execmap, opts.ToggleSort, value)
+ parseKeymap(opts.Keymap, value)
} else if match, value := optString(arg, "--history="); match {
setHistory(value)
} else if match, value := optString(arg, "--history-size="); match {
@@ -839,8 +1156,18 @@ 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 {
+ 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)
}
@@ -851,24 +1178,56 @@ func parseOptions(opts *Options, allArgs []string) {
errorExit("header lines must be a non-negative integer")
}
- // Change default actions for CTRL-N / CTRL-P when --history is used
- if opts.History != nil {
- if _, prs := keymap[curses.CtrlP]; !prs {
- keymap[curses.CtrlP] = actPreviousHistory
+ 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")
+ }
+
+ 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")
+ }
}
- if _, prs := keymap[curses.CtrlN]; !prs {
- keymap[curses.CtrlN] = actNextHistory
+ }
+}
+
+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 {
+ opts.Keymap[tui.CtrlP] = toActions(actPreviousHistory)
+ }
+ if _, prs := opts.Keymap[tui.CtrlN]; !prs {
+ opts.Keymap[tui.CtrlN] = toActions(actNextHistory)
}
}
- // Override default key bindings
- for key, act := range keymap {
- opts.Keymap[key] = act
+ // Extend the default key map
+ keymap := defaultKeymap()
+ for key, actions := range opts.Keymap {
+ for _, act := range actions {
+ if act.t == actToggleSort {
+ opts.ToggleSort = true
+ }
+ }
+ keymap[key] = actions
}
+ opts.Keymap = keymap
// 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)
@@ -884,9 +1243,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 1f96f78..22f4e4e 100644
--- a/src/options_test.go
+++ b/src/options_test.go
@@ -2,9 +2,10 @@ package fzf
import (
"fmt"
+ "io/ioutil"
"testing"
- "github.com/junegunn/fzf/src/curses"
+ "github.com/junegunn/fzf/src/tui"
)
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("-*--*---**---", 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("-*--*---**---", 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))
}
}
@@ -96,14 +97,16 @@ 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)
}
}
- 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)
+ postProcessOptions(opts)
if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %s", opts.Nth)
}
@@ -112,6 +115,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)
}
@@ -120,55 +124,58 @@ 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,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) != 9 {
- t.Error(9)
+ if len(pairs) != 12 {
+ t.Error(12)
}
- 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(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.CtrlAltA, "ctrl-alt-a")
+ check(tui.CtrlAltM, "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) {
@@ -185,94 +192,88 @@ 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) {
- 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[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")
+ 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(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(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{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
- keymap, execmap, toggleSort =
- parseKeymap(keymap, execmap, false, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
- checkString("foobar", execmap[curses.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)
}
- keymap, execmap, toggleSort = parseKeymap(keymap, execmap, false, "f1:abort")
- if toggleSort {
- t.Errorf("toggleSort set")
- }
- check(actAbort, keymap[curses.F1])
+ parseKeymap(keymap, "f1:abort")
+ check(tui.F1, "", actAbort)
}
func TestColorSpec(t *testing.T) {
- theme := curses.Dark256
+ theme := tui.Dark256
dark := parseTheme(theme, "dark")
if *dark != *theme {
t.Errorf("colors should be equivalent")
@@ -285,7 +286,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 {
@@ -296,23 +297,127 @@ 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")
+ 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")
}
- if customized.UseDefault {
- t.Errorf("not using default colors")
+}
+
+func TestParseNilTheme(t *testing.T) {
+ var theme *tui.ColorTheme
+ newTheme := parseTheme(theme, "prompt:12")
+ if newTheme != nil {
+ t.Errorf("color is disabled. keep it that way.")
}
- if !curses.Dark256.UseDefault {
- t.Errorf("using default colors")
+ newTheme = parseTheme(theme, "prompt:12,dark,prompt:13")
+ if newTheme.Prompt != 13 {
+ 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][0].t != expected {
+ t.Error()
+ }
+ }
+ check([]string{}, tui.CtrlN, actDown)
+ check([]string{}, tui.CtrlP, actUp)
+
+ check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
+ check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
+
+ f, _ := ioutil.TempFile("", "fzf-history")
+ f.Close()
+ hist := "--history=" + f.Name()
+ check([]string{hist}, tui.CtrlN, actNextHistory)
+ check([]string{hist}, tui.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"}, tui.CtrlN, actNextHistory)
+ check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
+}
+
+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()
+ }
+
+ 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()
+ }
+}
+
+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: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+2) {
+ t.Error(opts.Preview)
+ }
+ 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)
+ }
+}
+
+func TestAdditiveExpect(t *testing.T) {
+ opts := optsFor("--expect=a", "--expect", "b", "--expect=c")
+ if len(opts.Expect) != 3 {
+ t.Error(opts.Expect)
}
}
diff --git a/src/pattern.go b/src/pattern.go
index 5466b86..636ae1e 100644
--- a/src/pattern.go
+++ b/src/pattern.go
@@ -2,20 +2,20 @@ package fzf
import (
"regexp"
- "sort"
"strings"
"github.com/junegunn/fzf/src/algo"
+ "github.com/junegunn/fzf/src/util"
)
// 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,20 +32,25 @@ type term struct {
inv bool
text []rune
caseSensitive bool
- origText []rune
}
+type termSet []term
+
// Pattern represents search pattern
type Pattern struct {
- mode Mode
+ fuzzy bool
+ fuzzyAlgo algo.Algo
+ extended bool
caseSensitive bool
+ normalize bool
forward bool
text []rune
- terms []term
- hasInvTerm bool
+ termSets []termSet
+ cacheable bool
+ cacheKey string
delimiter Delimiter
nth []Range
- procFun map[termType]func(bool, bool, []rune, []rune) (int, int)
+ procFun map[termType]algo.Algo
}
var (
@@ -55,14 +60,14 @@ var (
)
func init() {
- _splitRegex = regexp.MustCompile("\\s+")
+ _splitRegex = regexp.MustCompile(" +")
clearPatternCache()
clearChunkCache()
}
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)
}
@@ -71,14 +76,16 @@ func clearChunkCache() {
}
// BuildPattern builds Pattern object from the given arguments
-func BuildPattern(mode Mode, caseMode Case, forward bool,
- nth []Range, delimiter Delimiter, runes []rune) *Pattern {
+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
- switch mode {
- case ModeExtended, ModeExtendedExact:
- asString = strings.Trim(string(runes), " ")
- default:
+ if extended {
+ asString = strings.TrimLeft(string(runes), " ")
+ for strings.HasSuffix(asString, " ") && !strings.HasSuffix(asString, "\\ ") {
+ asString = asString[:len(asString)-1]
+ }
+ } else {
asString = string(runes)
}
@@ -87,18 +94,23 @@ func BuildPattern(mode Mode, caseMode Case, forward bool,
return cached
}
- caseSensitive, hasInvTerm := true, false
- terms := []term{}
+ caseSensitive := true
+ termSets := []termSet{}
- switch mode {
- case ModeExtended, ModeExtendedExact:
- terms = parseTerms(mode, caseMode, asString)
- for _, term := range terms {
- if term.inv {
- hasInvTerm = true
+ if extended {
+ termSets = parseTerms(fuzzy, caseMode, normalize, 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 !cacheable || idx > 0 || term.inv || fuzzy && term.typ != termFuzzy || !fuzzy && term.typ != termExact {
+ cacheable = false
+ break Loop
+ }
}
}
- default:
+ } else {
lowerString := strings.ToLower(asString)
caseSensitive = caseMode == CaseRespect ||
caseMode == CaseSmart && lowerString != asString
@@ -108,17 +120,21 @@ func BuildPattern(mode Mode, caseMode Case, forward bool,
}
ptr := &Pattern{
- mode: mode,
+ fuzzy: fuzzy,
+ fuzzyAlgo: fuzzyAlgo,
+ extended: extended,
caseSensitive: caseSensitive,
+ normalize: normalize,
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))}
+ procFun: make(map[termType]algo.Algo)}
- ptr.procFun[termFuzzy] = algo.FuzzyMatch
+ ptr.cacheKey = ptr.buildCacheKey()
+ ptr.procFun[termFuzzy] = fuzzyAlgo
ptr.procFun[termEqual] = algo.EqualMatch
ptr.procFun[termExact] = algo.ExactMatchNaive
ptr.procFun[termPrefix] = algo.PrefixMatch
@@ -128,66 +144,90 @@ 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, normalize bool, str string) []termSet {
+ str = strings.Replace(str, "\\ ", "\t", -1)
tokens := _splitRegex.Split(str, -1)
- terms := []term{}
+ sets := []termSet{}
+ set := termSet{}
+ switchSet := false
+ afterBar := 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
if !caseSensitive {
text = lowerText
}
- origText := []rune(text)
- if mode == ModeExtendedExact {
+ if !fuzzy {
typ = termExact
}
+ if len(set) > 0 && !afterBar && text == "|" {
+ switchSet = false
+ afterBar = true
+ continue
+ }
+ afterBar = false
+
if strings.HasPrefix(text, "!") {
inv = true
+ typ = termExact
text = text[1:]
}
- if strings.HasPrefix(text, "'") {
- if mode == ModeExtended {
- typ = termExact
- text = text[1:]
- } else if mode == ModeExtendedExact {
- typ = termFuzzy
- text = text[1:]
- }
- } else if strings.HasPrefix(text, "^") {
- if strings.HasSuffix(text, "$") {
- typ = termEqual
- text = text[1 : len(text)-1]
- } else {
- typ = termPrefix
- text = text[1:]
- }
- } else if strings.HasSuffix(text, "$") {
+ if text != "$" && strings.HasSuffix(text, "$") {
typ = termSuffix
text = text[:len(text)-1]
}
+ if strings.HasPrefix(text, "'") {
+ // Flip exactness
+ if fuzzy && !inv {
+ typ = termExact
+ text = text[1:]
+ } else {
+ typ = termFuzzy
+ text = text[1:]
+ }
+ } else if strings.HasPrefix(text, "^") {
+ if typ == termSuffix {
+ typ = termEqual
+ } else {
+ typ = termPrefix
+ }
+ text = text[1:]
+ }
+
if len(text) > 0 {
- terms = append(terms, term{
+ if switchSet {
+ 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),
- caseSensitive: caseSensitive,
- origText: origText})
+ text: textRunes,
+ caseSensitive: caseSensitive})
+ switchSet = true
}
}
- return terms
+ if len(set) > 0 {
+ sets = append(sets, set)
+ }
+ return sets
}
// 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
+ return len(p.termSets) == 0
}
// AsString returns the search query in string type
@@ -195,71 +235,58 @@ 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 {
- if p.mode == ModeFuzzy {
+func (p *Pattern) buildCacheKey() string {
+ if !p.extended {
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 && (p.fuzzy || termSet[0].typ == termExact) {
+ cacheableTerms = append(cacheableTerms, string(termSet[0].text))
}
- cacheableTerms = append(cacheableTerms, string(term.origText))
}
- return strings.Join(cacheableTerms, " ")
+ 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) []*Item {
- space := chunk
-
+func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
// ChunkCache: Exact match
cacheKey := p.CacheKey()
- if !p.hasInvTerm { // Because we're excluding Inv-term from cache key
- if cached, found := _cache.Find(chunk, cacheKey); found {
+ if p.cacheable {
+ if cached := _cache.Lookup(chunk, cacheKey); cached != nil {
return cached
}
}
- // ChunkCache: Prefix/suffix match
-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 {
- cachedChunk := Chunk(cached)
- space = &cachedChunk
- break Loop
- }
- }
- }
+ // Prefix/suffix cache
+ space := _cache.Search(chunk, cacheKey)
- matches := p.matchChunk(space)
+ matches := p.matchChunk(chunk, space, slab)
- if !p.hasInvTerm {
+ if p.cacheable {
_cache.Add(chunk, cacheKey, matches)
}
return matches
}
-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 {
- matches = append(matches,
- dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}}))
+func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
+ matches := []Result{}
+
+ if space == nil {
+ for idx := 0; idx < chunk.count; idx++ {
+ if match, _, _ := p.MatchItem(&chunk.items[idx], false, slab); match != nil {
+ matches = append(matches, *match)
}
}
} else {
- for _, item := range *chunk {
- if offsets := p.extendedMatch(item); len(offsets) == len(p.terms) {
- matches = append(matches, dupItem(item, offsets))
+ for _, result := range space {
+ if match, _, _ := p.MatchItem(result.item, false, slab); match != nil {
+ matches = append(matches, *match)
}
}
}
@@ -267,72 +294,109 @@ 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)
- return sidx >= 0
+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) {
+ result := buildResult(item, offsets, bonus)
+ return &result, offsets, pos
+ }
+ return nil, nil, nil
}
- offsets := p.extendedMatch(item)
- return len(offsets) == len(p.terms)
+ offset, bonus, pos := p.basicMatch(item, withPos, slab)
+ if sidx := offset[0]; sidx >= 0 {
+ offsets := []Offset{offset}
+ result := buildResult(item, offsets, bonus)
+ return &result, offsets, pos
+ }
+ return nil, nil, nil
}
-func dupItem(item *Item, offsets []Offset) *Item {
- sort.Sort(ByOrder(offsets))
- return &Item{
- text: item.text,
- origText: item.origText,
- transformed: item.transformed,
- index: item.index,
- offsets: offsets,
- colors: item.colors,
- rank: Rank{0, 0, item.index}}
+func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
+ var input []Token
+ if len(p.nth) == 0 {
+ input = []Token{Token{text: &item.text, prefixLength: 0}}
+ } else {
+ input = p.transformInput(item)
+ }
+ if p.fuzzy {
+ 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.normalize, p.forward, p.text, withPos, slab)
}
-func (p *Pattern) fuzzyMatch(item *Item) (int, int) {
- input := p.prepareInput(item)
- return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text)
-}
-
-func (p *Pattern) extendedMatch(item *Item) []Offset {
- input := p.prepareInput(item)
+func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) {
+ var input []Token
+ if len(p.nth) == 0 {
+ input = []Token{Token{text: &item.text, prefixLength: 0}}
+ } else {
+ input = p.transformInput(item)
+ }
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 term.inv {
+ var totalScore int
+ var allPos *[]int
+ if withPos {
+ allPos = &[]int{}
+ }
+ for _, termSet := range p.termSets {
+ var offset Offset
+ var currentScore int
+ matched := false
+ for _, term := range termSet {
+ pfun := p.procFun[term.typ]
+ 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 = off, score
+ 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, currentScore = Offset{0, 0}, 0
+ matched = true
+ continue
}
- offsets = append(offsets, Offset{int32(sidx), int32(eidx)})
- } else if term.inv {
- offsets = append(offsets, Offset{0, 0})
+ }
+ if matched {
+ offsets = append(offsets, offset)
+ totalScore += currentScore
}
}
- return offsets
+ return offsets, totalScore, allPos
}
-func (p *Pattern) prepareInput(item *Item) []Token {
+func (p *Pattern) transformInput(item *Item) []Token {
if item.transformed != nil {
- return item.transformed
+ return *item.transformed
}
- var ret []Token
- if len(p.nth) > 0 {
- tokens := Tokenize(item.text, p.delimiter)
- ret = Transform(tokens, p.nth)
- } else {
- ret = []Token{Token{text: item.text, prefixLength: 0}}
- }
- item.transformed = ret
+ tokens := Tokenize(item.text.ToString(), p.delimiter)
+ ret := Transform(tokens, p.nth)
+ 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) {
+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 {
- prefixLength := part.prefixLength
- if sidx, eidx := pfun(caseSensitive, forward, part.text, pattern); sidx >= 0 {
- return sidx + prefixLength, eidx + prefixLength
+ 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 {
+ for idx := range *pos {
+ (*pos)[idx] += int(part.prefixLength)
+ }
+ }
+ return Offset{sidx, eidx}, res.Score, pos
}
}
- return -1, -1
+ return Offset{-1, -1}, 0, nil
}
diff --git a/src/pattern_test.go b/src/pattern_test.go
index d508612..1930ddd 100644
--- a/src/pattern_test.go
+++ b/src/pattern_test.go
@@ -5,51 +5,60 @@ import (
"testing"
"github.com/junegunn/fzf/src/algo"
+ "github.com/junegunn/fzf/src/util"
)
+var slab *util.Slab
+
+func init() {
+ slab = util.MakeSlab(slab16Size, slab32Size)
+}
+
func TestParseTermsExtended(t *testing.T) {
- terms := parseTerms(ModeExtended, CaseSmart,
- "aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ ^iii$")
+ terms := parseTerms(true, CaseSmart, false,
+ "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 != 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 != termExact || !terms[8][3].inv {
t.Errorf("%s", terms)
}
- for idx, term := range terms {
+ 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)
- }
}
}
func TestParseTermsExtendedExact(t *testing.T) {
- terms := parseTerms(ModeExtendedExact, CaseSmart,
+ terms := parseTerms(false, CaseSmart, false,
"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)
}
}
func TestParseTermsEmpty(t *testing.T) {
- terms := parseTerms(ModeExtended, CaseSmart, "' $ ^ !' !^ !$")
+ terms := parseTerms(true, CaseSmart, false, "' ^ !' !^")
if len(terms) != 0 {
t.Errorf("%s", terms)
}
@@ -58,25 +67,33 @@ func TestParseTermsEmpty(t *testing.T) {
func TestExact(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
- pattern := BuildPattern(ModeExtended, CaseSmart, true,
+ pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true,
[]Range{}, Delimiter{}, []rune("'abc"))
- sidx, eidx := algo.ExactMatchNaive(
- pattern.caseSensitive, pattern.forward, []rune("aabbcc abc"), pattern.terms[0].text)
- if sidx != 7 || eidx != 10 {
- t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
+ chars := util.ToChars([]byte("aabbcc abc"))
+ res, pos := algo.ExactMatchNaive(
+ 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)
+ }
+ if pos != nil {
+ t.Errorf("pos is expected to be nil")
}
}
func TestEqual(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
- pattern := BuildPattern(ModeExtended, CaseSmart, 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) {
- sidx, eidx := algo.EqualMatch(
- pattern.caseSensitive, pattern.forward, []rune(str), pattern.terms[0].text)
- if sidx != sidxExpected || eidx != eidxExpected {
- t.Errorf("%s / %d / %d", pattern.terms, sidx, eidx)
+ chars := util.ToChars([]byte(str))
+ res, pos := algo.EqualMatch(
+ 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)
+ }
+ if pos != nil {
+ t.Errorf("pos is expected to be nil")
}
}
match("ABC", -1, -1)
@@ -86,17 +103,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, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
- pat2 := BuildPattern(ModeFuzzy, CaseSmart, true, []Range{}, Delimiter{}, []rune("Abc"))
+ pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache()
- pat3 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("abc"))
+ pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
- pat4 := BuildPattern(ModeFuzzy, CaseIgnore, true, []Range{}, Delimiter{}, []rune("Abc"))
+ pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache()
- pat5 := BuildPattern(ModeFuzzy, CaseRespect, true, []Range{}, Delimiter{}, []rune("abc"))
+ pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
- pat6 := BuildPattern(ModeFuzzy, CaseRespect, 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 ||
@@ -109,24 +126,81 @@ func TestCaseSensitivity(t *testing.T) {
}
func TestOrigTextAndTransformed(t *testing.T) {
- pattern := BuildPattern(ModeExtended, CaseSmart, true, []Range{}, Delimiter{}, []rune("jg"))
- tokens := Tokenize([]rune("junegunn"), Delimiter{})
+ pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
+ tokens := Tokenize("junegunn", Delimiter{})
trans := Transform(tokens, []Range{Range{1, 1}})
- origRunes := []rune("junegunn.choi")
- for _, mode := range []Mode{ModeFuzzy, ModeExtended} {
- chunk := Chunk{
- &Item{
- text: []rune("junegunn"),
- origText: &origRunes,
- transformed: trans},
- }
- pattern.mode = mode
- matches := pattern.matchChunk(&chunk)
- if string(matches[0].text) != "junegunn" || string(*matches[0].origText) != "junegunn.choi" ||
- matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 ||
- !reflect.DeepEqual(matches[0].transformed, trans) {
+ origBytes := []byte("junegunn.choi")
+ for _, extended := range []bool{false, true} {
+ 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" &&
+ string(*matches[0].item.origText) == "junegunn.choi" &&
+ reflect.DeepEqual(*matches[0].item.transformed, trans)) {
t.Error("Invalid match result", matches)
}
+
+ 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 &&
+ 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) {
+ 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())
+ }
+ if pat.cacheable != cacheable {
+ 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\tbar\tbaz", true)
+ test(true, "foo !bar", "foo", 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)
+ test(true, "| | foo", "", false)
+ test(true, "| | | foo", "foo", false)
+}
+
+func TestCacheable(t *testing.T) {
+ 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", "foo\tbar", true)
+ test(true, "foo 'bar", "foo\tbar", false)
+ test(true, "foo !bar", "foo", false)
+
+ test(false, "foo bar", "foo\tbar", 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/src/reader.go b/src/reader.go
index 3e2cf0a..5fd6d87 100644
--- a/src/reader.go
+++ b/src/reader.go
@@ -4,7 +4,8 @@ import (
"bufio"
"io"
"os"
- "os/exec"
+ "sync/atomic"
+ "time"
"github.com/junegunn/fzf/src/util"
)
@@ -14,20 +15,56 @@ 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")
if len(cmd) == 0 {
- cmd = defaultCommand
+ // The default command for *nix requires bash
+ success = r.readFromCommand("bash", defaultCommand)
+ } else {
+ success = r.readFromCommand("sh", cmd)
}
- r.readFromCommand(cmd)
} else {
- r.readFromStdin()
+ success = r.readFromStdin()
}
- r.eventBox.Set(EvtReadFin, nil)
+ r.fin(success)
}
func (r *Reader) feed(src io.Reader) {
@@ -35,17 +72,23 @@ 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.
bytea, err := reader.ReadBytes(delim)
- if len(bytea) > 0 {
+ byteaLen := len(bytea)
+ if byteaLen > 0 {
if err == nil {
- bytea = bytea[:len(bytea)-1]
+ // get rid of carriage return if under Windows:
+ if util.IsWindows() && 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)
+ atomic.StoreInt32(&r.event, int32(EvtReadNew))
}
}
if err != nil {
@@ -54,20 +97,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) {
- listCommand := exec.Command("sh", "-c", cmd)
+func (r *Reader) readFromCommand(shell string, cmd string) bool {
+ listCommand := util.ExecCommandWith(shell, 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/reader_test.go b/src/reader_test.go
index d5c218c..c29936c 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("sh", `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("sh", `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")
}
}
diff --git a/src/result.go b/src/result.go
new file mode 100644
index 0000000..289d83a
--- /dev/null
+++ b/src/result.go
@@ -0,0 +1,226 @@
+package fzf
+
+import (
+ "math"
+ "sort"
+ "unicode"
+
+ "github.com/junegunn/fzf/src/tui"
+ "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 tui.ColorPair
+ attr tui.Attr
+ index int32
+}
+
+type Result struct {
+ item *Item
+ points [4]uint16
+}
+
+func buildResult(item *Item, offsets []Offset, score int) Result {
+ if len(offsets) > 1 {
+ sort.Sort(ByOrder(offsets))
+ }
+
+ result := Result{item: item}
+ 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
+ }
+ }
+
+ for idx, criterion := range sortCriteria {
+ val := uint16(math.MaxUint16)
+ switch criterion {
+ case byScore:
+ // Higher is better
+ val = math.MaxUint16 - util.AsUint16(score)
+ case byLength:
+ val = item.TrimLength()
+ case byBegin, byEnd:
+ if validOffsetFound {
+ whitePrefixLen := 0
+ for idx := 0; idx < numChars; idx++ {
+ r := item.text.Get(idx)
+ whitePrefixLen = idx
+ if idx == minBegin || !unicode.IsSpace(r) {
+ break
+ }
+ }
+ if criterion == byBegin {
+ val = util.AsUint16(minEnd - whitePrefixLen)
+ } else {
+ val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()))
+ }
+ }
+ }
+ result.points[3-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() Result {
+ 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 {
+ 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
+ }
+
+ // Find max column
+ var maxCol int32
+ for _, off := range matchOffsets {
+ if off[1] > maxCol {
+ maxCol = off[1]
+ }
+ }
+ for _, ansi := range itemColors {
+ if ansi.offset[1] > maxCol {
+ maxCol = ansi.offset[1]
+ }
+ }
+ cols := make([]int, maxCol)
+
+ for colorIndex, ansi := range itemColors {
+ for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
+ cols[i] = colorIndex + 1 // XXX
+ }
+ }
+
+ for _, off := range matchOffsets {
+ for i := off[0]; i < off[1]; i++ {
+ cols[i] = -1
+ }
+ }
+
+ // sort.Sort(ByOrder(offsets))
+
+ // Merge offsets
+ // ------------ ---- -- ----
+ // ++++++++ ++++++++++
+ // --++++++++-- --++++++++++---
+ curr := 0
+ start := 0
+ var colors []colorOffset
+ add := func(idx int) {
+ if curr != 0 && idx > start {
+ if curr == -1 {
+ colors = append(colors, colorOffset{
+ offset: [2]int32{int32(start), int32(idx)}, color: color, attr: attr})
+ } else {
+ ansi := itemColors[curr-1]
+ fg := ansi.color.fg
+ bg := ansi.color.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{
+ offset: [2]int32{int32(start), int32(idx)},
+ color: tui.NewColorPair(fg, bg),
+ attr: ansi.color.attr.Merge(attr)})
+ }
+ }
+ }
+ 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], a[j], 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], a[j], true)
+}
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
new file mode 100644
index 0000000..afd1730
--- /dev/null
+++ b/src/result_test.go
@@ -0,0 +1,136 @@
+// +build !tcell
+
+package fzf
+
+import (
+ "math"
+ "sort"
+ "testing"
+
+ "github.com/junegunn/fzf/src/tui"
+ "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},
+ 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) Result {
+ return Result{
+ points: [4]uint16{vals[0], vals[1], vals[2], vals[3]},
+ 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) ||
+ !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{byScore, byLength}
+
+ 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[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)
+ }
+ // Only differ in index
+ item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2)
+
+ 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(
+ 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 &&
+ items[2] == item4 && items[3] == item3 &&
+ items[4] == item2 && items[5] == item1) {
+ t.Error(items, item1, item2, item3, item4, item5, item6)
+ }
+}
+
+func TestColorOffset(t *testing.T) {
+ // ------------ 20 ---- -- ----
+ // ++++++++ ++++++++++
+ // --++++++++-- --++++++++++---
+
+ offsets := []Offset{Offset{5, 15}, Offset{25, 35}}
+ item := Result{
+ item: &Item{
+ colors: &[]ansiOffset{
+ ansiOffset{[2]int32{0, 20}, ansiState{1, 5, 0}},
+ 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, 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}]
+
+ 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 {
+ 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, 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/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
+}
diff --git a/src/terminal.go b/src/terminal.go
index 9a92ba4..98d837c 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -1,10 +1,11 @@
package fzf
import (
+ "bufio"
"bytes"
"fmt"
+ "io"
"os"
- "os/exec"
"os/signal"
"regexp"
"sort"
@@ -14,18 +15,56 @@ 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"
)
+// import "github.com/pkg/profile"
+
+var placeholder *regexp.Regexp
+
+func init() {
+ placeholder = regexp.MustCompile("\\\\?(?:{\\+?[0-9,-.]*}|{q})")
+}
+
+type jumpMode int
+
+const (
+ jumpDisabled jumpMode = iota
+ jumpEnabled
+ jumpAcceptEnabled
+)
+
+type previewer struct {
+ text string
+ lines int
+ offset int
+ enabled bool
+}
+
+type itemLine struct {
+ current bool
+ selected bool
+ label string
+ queryLen int
+ width int
+ result Result
+}
+
+var emptyLine = itemLine{}
+
// Terminal represents terminal input/output
type Terminal struct {
+ initDelay time.Duration
inlineInfo bool
prompt string
+ promptLen int
reverse bool
+ fullscreen bool
hscroll bool
+ hscrollOff int
+ wordRubout string
+ wordNext string
cx int
cy int
offset int
@@ -34,9 +73,9 @@ type Terminal struct {
multi bool
sort bool
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
@@ -44,24 +83,43 @@ type Terminal struct {
header []string
header0 []string
ansi bool
- margin [4]string
- marginInt [4]int
+ tabstop int
+ margin [4]sizeSpec
+ strong tui.Attr
+ bordered bool
+ cleanExit bool
+ border tui.Window
+ window tui.Window
+ pborder tui.Window
+ pwindow tui.Window
count int
progress int
reading bool
+ success bool
+ jumping jumpMode
+ jumpLabels string
+ printer func(string)
merger *Merger
- selected map[uint32]selectedItem
+ selected map[int32]selectedItem
+ version int64
reqBox *util.EventBox
+ preview previewOpts
+ previewer previewer
+ previewBox *util.EventBox
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 {
at time.Time
- text *string
+ item *Item
}
type byTimeOrder []selectedItem
@@ -79,19 +137,29 @@ func (a byTimeOrder) Less(i, j int) bool {
}
var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
-var _runeWidths = make(map[rune]int)
const (
reqPrompt util.EventType = iota
reqInfo
reqHeader
reqList
+ reqJump
reqRefresh
+ reqReinit
reqRedraw
reqClose
+ reqPrintQuery
+ reqPreviewEnqueue
+ reqPreviewDisplay
+ reqPreviewRefresh
reqQuit
)
+type action struct {
+ t actionType
+ a string
+}
+
type actionType int
const (
@@ -124,79 +192,170 @@ const (
actToggleAll
actToggleDown
actToggleUp
+ actToggleIn
+ actToggleOut
actDown
actUp
actPageUp
actPageDown
+ actHalfPageUp
+ actHalfPageDown
+ actJump
+ actJumpAccept
+ actPrintQuery
actToggleSort
+ actTogglePreview
+ actTogglePreviewWrap
+ actPreviewUp
+ actPreviewDown
+ actPreviewPageUp
+ actPreviewPageDown
actPreviousHistory
actNextHistory
actExecute
+ actExecuteSilent
+ actExecuteMulti // Deprecated
+ actSigStop
+ actTop
)
-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
+func toActions(types ...actionType) []action {
+ actions := make([]action, len(types))
+ for idx, t := range types {
+ actions[idx] = action{t: t, a: ""}
+ }
+ return actions
+}
- keymap[C.AltB] = actBackwardWord
- keymap[C.SLeft] = actBackwardWord
- keymap[C.AltF] = actForwardWord
- keymap[C.SRight] = actForwardWord
- keymap[C.AltD] = actKillWord
- keymap[C.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)
+ if !util.IsWindows() {
+ keymap[tui.CtrlZ] = toActions(actSigStop)
+ }
- keymap[C.Up] = actUp
- keymap[C.Down] = actDown
- keymap[C.Left] = actBackwardChar
- keymap[C.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[C.Home] = actBeginningOfLine
- keymap[C.End] = actEndOfLine
- keymap[C.Del] = actDeleteChar
- keymap[C.PgUp] = actPageUp
- keymap[C.PgDn] = actPageDown
+ keymap[tui.Up] = toActions(actUp)
+ keymap[tui.Down] = toActions(actDown)
+ keymap[tui.Left] = toActions(actBackwardChar)
+ keymap[tui.Right] = toActions(actForwardChar)
- keymap[C.Rune] = actRune
- keymap[C.Mouse] = actMouse
+ 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
}
+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
} else {
header = reverseStringArray(opts.Header)
}
- return &Terminal{
+ var delay time.Duration
+ if opts.Tac {
+ delay = initialDelayTac
+ } else {
+ delay = initialDelay
+ }
+ var previewBox *util.EventBox
+ if len(opts.Preview.command) > 0 {
+ previewBox = util.NewEventBox()
+ }
+ strongAttr := tui.Bold
+ if !opts.Bold {
+ strongAttr = tui.AttrRegular
+ }
+ var renderer tui.Renderer
+ 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 {
+ 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 {
+ maxHeight = util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight)
+ } else {
+ maxHeight = int(opts.Height.size)
+ }
+
+ effectiveMinHeight := minHeight
+ if previewBox != nil && (opts.Preview.position == posUp || opts.Preview.position == posDown) {
+ effectiveMinHeight *= 2
+ }
+ if opts.InlineInfo {
+ effectiveMinHeight -= 1
+ }
+ if opts.Bordered {
+ effectiveMinHeight += 2
+ }
+ return util.Min(termHeight, util.Max(maxHeight, effectiveMinHeight))
+ }
+ renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
+ }
+ 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)
+ }
+ t := Terminal{
+ initDelay: delay,
inlineInfo: opts.InlineInfo,
- prompt: opts.Prompt,
reverse: opts.Reverse,
+ fullscreen: fullscreen,
hscroll: opts.Hscroll,
+ hscrollOff: opts.HscrollOff,
+ wordRubout: wordRubout,
+ wordNext: wordNext,
cx: len(input),
cy: 0,
offset: 0,
@@ -205,29 +364,42 @@ 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,
pressed: "",
printQuery: opts.PrintQuery,
history: opts.History,
margin: opts.Margin,
- marginInt: [4]int{0, 0, 0, 0},
+ bordered: opts.Bordered,
+ cleanExit: opts.ClearOnExit,
+ strong: strongAttr,
cycle: opts.Cycle,
header: header,
header0: header,
ansi: opts.Ansi,
+ tabstop: opts.Tabstop,
reading: true,
+ success: true,
+ jumping: jumpDisabled,
+ jumpLabels: opts.JumpLabels,
+ printer: opts.Printer,
merger: EmptyMerger,
- selected: make(map[uint32]selectedItem),
+ selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(),
+ preview: opts.Preview,
+ previewer: previewer{"", 0, 0, previewBox != nil && !opts.Preview.hidden},
+ previewBox: previewBox,
eventBox: eventBox,
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)
- }}
+ tui: renderer,
+ initFunc: func() { renderer.Init() }}
+ t.prompt, t.promptLen = t.processTabs([]rune(opts.Prompt), 0)
+ return &t
}
// Input returns current query string
@@ -238,10 +410,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 {
@@ -291,138 +464,224 @@ 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.merger.Get(t.cy).AsString(t.ansi))
+ current := t.currentItem()
+ if current != nil {
+ t.printer(current.AsString(t.ansi))
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 {
- fmt.Println(*sel.text)
+ for _, sel := range t.sortSelected() {
+ t.printer(sel.item.AsString(t.ansi))
}
}
return found
}
-func runeWidth(r rune, prefixWidth int) int {
- if r == '\t' {
- return 8 - prefixWidth%8
- } else if w, found := _runeWidths[r]; found {
- return w
- } else {
- w := runewidth.RuneWidth(r)
- _runeWidths[r] = w
- return w
+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 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
}
-const minWidth = 16
-const minHeight = 4
+const (
+ minWidth = 16
+ minHeight = 4
-func (t *Terminal) calculateMargins() {
- screenWidth := C.MaxX()
- screenHeight := C.MaxY()
- for idx, str := range t.margin {
- if str == "0" {
- t.marginInt[idx] = 0
- } else if strings.HasSuffix(str, "%") {
- num, _ := strconv.ParseFloat(str[:len(str)-1], 64)
- var val float64
+ maxDisplayWidthCalc = 1024
+)
+
+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 := 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
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)
+ }
+ if t.bordered && idx%2 == 0 {
+ marginInt[idx] += 1
}
}
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)
+
+ previewVisible := t.isPreviewEnabled() && t.preview.size.size > 0
+ minAreaWidth := minWidth
+ minAreaHeight := minHeight
+ if previewVisible {
+ 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.border != nil {
+ t.border.Close()
+ }
+ if t.window != nil {
+ t.window.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.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
+ // column larger than the desired value.
+ if !t.preview.wrap && t.tui.DoesAutoWrap() {
+ 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:
+ pheight := calculateSize(height, t.preview.size, minHeight, 3)
+ t.window = t.tui.NewWindow(
+ 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, 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, 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, tui.BorderNone)
+ createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
+ }
+ } else {
+ t.window = t.tui.NewWindow(
+ marginInt[0],
+ marginInt[3],
+ width,
+ height, tui.BorderNone)
+ }
+ for i := 0; i < t.window.Height(); i++ {
+ t.window.MoveAndClear(i, 0)
+ }
+ t.truncateQuery()
}
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)
}
}
func (t *Terminal) placeCursor() {
- t.move(0, len(t.prompt)+displayWidth(t.input[:t.cx]), false)
+ t.move(0, t.promptLen+t.displayWidth(t.input[:t.cx]), false)
}
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(tui.ColPrompt, t.strong, t.prompt)
+ t.window.CPrint(tui.ColNormal, t.strong, string(t.input))
}
func (t *Terminal) printInfo() {
+ pos := 0
if t.inlineInfo {
- t.move(0, len(t.prompt)+displayWidth(t.input)+1, true)
- if t.reading {
- C.CPrint(C.ColSpinner, true, " < ")
- } else {
- C.CPrint(C.ColPrompt, true, " < ")
+ pos = t.promptLen + 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 {
duration := int64(spinnerDuration)
idx := (time.Now().UnixNano() % (duration * int64(len(_spinner)))) / duration
- C.CPrint(C.ColSpinner, true, _spinner[idx])
+ 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)
if t.toggleSort {
if t.sort {
- output += "/S"
+ output += " +S"
} else {
- output += " "
+ output += " -S"
}
}
if t.multi && len(t.selected) > 0 {
@@ -431,18 +690,19 @@ 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]
+ if !t.success && t.count == 0 {
+ output += " [ERROR]"
+ }
+ if pos+len(output) <= t.window.Width() {
+ t.window.CPrint(tui.ColInfo, 0, 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
@@ -452,16 +712,15 @@ 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),
- index: 0,
- colors: colors,
- rank: Rank{0, 0, 0}}
+ text: util.ToChars([]byte(trimmed)),
+ colors: colors}
t.move(line, 2, true)
- t.printHighlighted(item, false, C.ColHeader, 0, false)
+ t.printHighlighted(Result{item: item},
+ tui.AttrRegular, tui.ColHeader, tui.ColHeader, false, false)
}
}
@@ -470,55 +729,90 @@ 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 == 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(item *Item, current bool) {
- _, selected := t.selected[item.index]
+func (t *Terminal) printItem(result Result, line int, i int, current bool) {
+ item := result.item
+ _, 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 = ">"
+ }
+
+ // Avoid unnecessary redraw
+ 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
+ }
+
+ t.move(line, 0, false)
+ t.window.CPrint(tui.ColCursor, t.strong, label)
if current {
- C.CPrint(C.ColCursor, true, ">")
if selected {
- C.CPrint(C.ColSelected, true, ">")
+ t.window.CPrint(tui.ColSelected, t.strong, ">")
} else {
- C.CPrint(C.ColCurrent, true, " ")
+ t.window.CPrint(tui.ColCurrent, t.strong, " ")
}
- t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true)
+ newLine.width = t.printHighlighted(result, t.strong, tui.ColCurrent, tui.ColCurrentMatch, true, true)
} else {
- C.CPrint(C.ColCursor, true, " ")
if selected {
- C.CPrint(C.ColSelected, true, ">")
+ t.window.CPrint(tui.ColSelected, t.strong, ">")
} else {
- C.Print(" ")
+ t.window.Print(" ")
}
- t.printHighlighted(item, false, 0, C.ColMatch, false)
+ newLine.width = t.printHighlighted(result, 0, tui.ColNormal, tui.ColMatch, false, true)
}
+ fillSpaces := prevLine.width - newLine.width
+ if fillSpaces > 0 {
+ t.window.Print(strings.Repeat(" ", fillSpaces))
+ }
+ t.prevLines[i] = newLine
}
-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)
- if idx > 0 && l > width {
+ l += util.RuneWidth(r, l, t.tabstop)
+ if l > width {
return runes[:idx], len(runes) - idx
}
}
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
@@ -527,47 +821,70 @@ func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
return l
}
-func trimLeft(runes []rune, width int) ([]rune, int32) {
- currentWidth := displayWidth(runes)
+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 := 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 (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) {
- var maxe int32
- for _, offset := range item.offsets {
- if offset[1] > maxe {
- maxe = offset[1]
- }
- }
+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 {
+ item := result.item
// Overflow
- 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]
- fullWidth := displayWidth(text)
- if fullWidth > maxWidth {
+ text := make([]rune, item.text.Length())
+ copy(text, item.text.ToRunes())
+ matchOffsets := []Offset{}
+ var pos *[]int
+ if match && t.merger.pattern != nil {
+ _, 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 charOffsets {
+ maxe = util.Max(maxe, int(offset[1]))
+ }
+
+ 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))
+ displayWidth := t.displayWidthWithLimit(text, 0, maxWidth)
+ if displayWidth > maxWidth {
if t.hscroll {
// Stri..
- matchEndWidth := displayWidth(text[:maxe])
- if matchEndWidth <= 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 matchEndWidth < fullWidth-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 {
@@ -581,7 +898,7 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
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 {
@@ -589,6 +906,7 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
offsets[idx].offset[1] = util.Min32(offset.offset[1], int32(maxWidth))
}
}
+ displayWidth = t.displayWidthWithLimit(text, 0, displayWidth)
}
var index int32
@@ -599,12 +917,12 @@ func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, c
b := util.Constrain32(offset.offset[0], index, maxOffset)
e := util.Constrain32(offset.offset[1], index, maxOffset)
- substr, prefixWidth = processTabs(text[index:b], prefixWidth)
- C.CPrint(col1, bold, substr)
+ substr, prefixWidth = t.processTabs(text[index:b], prefixWidth)
+ t.window.CPrint(col1, attr, substr)
if b < e {
- substr, prefixWidth = processTabs(text[b:e], prefixWidth)
- C.CPrint(offset.color, offset.bold, substr)
+ substr, prefixWidth = t.processTabs(text[b:e], prefixWidth)
+ t.window.CPrint(offset.color, offset.attr, substr)
}
index = e
@@ -613,16 +931,93 @@ 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)
+ substr, _ = t.processTabs(text[index:], prefixWidth)
+ t.window.CPrint(col1, attr, substr)
+ }
+ return displayWidth
+}
+
+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() {
+ if !t.hasPreviewWindow() {
+ return
+ }
+ t.pwindow.Erase()
+
+ maxWidth := t.pwindow.Width()
+ if t.tui.DoesAutoWrap() {
+ maxWidth -= 1
+ }
+ 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')
+ eof := err == io.EOF
+ if !eof {
+ line = line[:len(line)-1]
+ }
+ lineNo++
+ if lineNo > height ||
+ t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
+ break
+ } else if lineNo > 0 {
+ var fillRet tui.FillReturn
+ _, _, 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())
+ }
+ str, _ = t.processTabs(trimmed, 0)
+ if t.theme != nil && ansi != nil && ansi.colored() {
+ fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
+ } else {
+ fillRet = t.pwindow.CFill(tui.ColNormal.Fg(), tui.ColNormal.Bg(), tui.AttrRegular, str)
+ }
+ return fillRet == tui.FillContinue
+ })
+ switch fillRet {
+ case tui.FillNextLine:
+ continue
+ case tui.FillSuspend:
+ break
+ }
+ t.pwindow.Fill("\n")
+ }
+ if eof {
+ break
+ }
+ }
+ t.pwindow.FinishFill()
+ 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() {
+ 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))
@@ -634,16 +1029,25 @@ func processTabs(runes []rune, prefixWidth int) (string, int) {
}
func (t *Terminal) printAll() {
- t.calculateMargins()
+ t.resizeWindows()
t.printList()
t.printPrompt()
t.printInfo()
t.printHeader()
+ t.printPreview()
}
func (t *Terminal) refresh() {
if !t.suppress {
- C.Refresh()
+ 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)
}
}
@@ -693,42 +1097,216 @@ func (t *Terminal) rubout(pattern string) {
t.input = append(t.input[:t.cx], after...)
}
-func keyMatch(key int, event C.Event) bool {
- return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ
+func keyMatch(key int, event tui.Event) bool {
+ return event.Type == key ||
+ event.Type == tui.Rune && int(event.Char) == key-tui.AltZ ||
+ event.Type == tui.Mouse && key == tui.DoubleClick && event.MouseEvent.Double
}
-func executeCommand(template string, current string) {
- command := strings.Replace(template, "{}", fmt.Sprintf("%q", current), -1)
- cmd := exec.Command("sh", "-c", command)
- cmd.Stdin = os.Stdin
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- C.Endwin()
- cmd.Run()
- C.Refresh()
+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 quoteEntryCmd(entry)
+ }
+ return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
+}
+
+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] == '\\' {
+ return match[1:]
+ }
+
+ // Current query
+ if match == "{q}" {
+ 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 == "{}" {
+ 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 {
+ tokens := Tokenize(item.AsString(stripAnsi), 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) 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 {
+ return
+ }
+ command := replacePlaceholder(template, t.ansi, t.delimiter, forcePlus, string(t.input), list)
+ cmd := util.ExecCommand(command)
+ if !background {
+ cmd.Stdin = os.Stdin
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ t.tui.Pause(true)
+ cmd.Run()
+ t.tui.Resume(true)
+ t.redraw()
+ t.refresh()
+ } else {
+ cmd.Run()
+ }
+}
+
+func (t *Terminal) hasPreviewer() bool {
+ return t.previewBox != nil
+}
+
+func (t *Terminal) isPreviewEnabled() bool {
+ return t.hasPreviewer() && t.previewer.enabled
+}
+
+func (t *Terminal) hasPreviewWindow() bool {
+ return t.pwindow != nil && t.isPreviewEnabled()
+}
+
+func (t *Terminal) currentItem() *Item {
+ cnt := t.merger.Length()
+ if cnt > 0 && cnt > t.cy {
+ return t.merger.Get(t.cy).item
+ }
+ return nil
+}
+
+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
+}
+
+func (t *Terminal) truncateQuery() {
+ 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))
+}
+
+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/"))
<-t.startChan
{ // Late initialization
- t.mutex.Lock()
- t.initFunc()
- t.calculateMargins()
- t.printPrompt()
- t.placeCursor()
- C.Refresh()
- t.printInfo()
- t.printHeader()
- t.mutex.Unlock()
+ intChan := make(chan os.Signal, 1)
+ signal.Notify(intChan, os.Interrupt, os.Kill, syscall.SIGTERM)
go func() {
- timer := time.NewTimer(initialDelay)
- <-timer.C
- t.reqBox.Set(reqRefresh, nil)
+ <-intChan
+ 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)
- signal.Notify(resizeChan, syscall.SIGWINCH)
+ notifyOnResize(resizeChan) // Non-portable
go func() {
for {
<-resizeChan
@@ -736,6 +1314,21 @@ func (t *Terminal) Loop() {
}
}()
+ t.mutex.Lock()
+ t.initFunc()
+ t.resizeWindows()
+ t.printPrompt()
+ t.placeCursor()
+ t.refresh()
+ t.printInfo()
+ t.printHeader()
+ t.mutex.Unlock()
+ go func() {
+ timer := time.NewTimer(t.initDelay)
+ <-timer.C
+ t.reqBox.Set(reqRefresh, nil)
+ }()
+
// Keep the spinner spinning
go func() {
for {
@@ -751,19 +1344,54 @@ func (t *Terminal) Loop() {
}()
}
- exit := func(code int) {
+ if t.hasPreviewer() {
+ go func() {
+ for {
+ var request []*Item
+ t.previewBox.Wait(func(events *util.Events) {
+ for req, value := range *events {
+ switch req {
+ case reqPreviewEnqueue:
+ request = value.([]*Item)
+ }
+ }
+ events.Clear()
+ })
+ // We don't display preview window if no match
+ if request[0] != nil {
+ command := replacePlaceholder(t.preview.command,
+ t.ansi, t.delimiter, false, string(t.input), request)
+ cmd := util.ExecCommand(command)
+ out, _ := cmd.CombinedOutput()
+ t.reqBox.Set(reqPreviewDisplay, string(out))
+ } else {
+ t.reqBox.Set(reqPreviewDisplay, "")
+ }
+ }
+ }()
+ }
+
+ exit := func(getCode func() int) {
+ if !t.cleanExit && t.fullscreen && t.inlineInfo {
+ t.placeCursor()
+ }
+ t.tui.Close()
+ code := getCode()
if code <= exitNoMatch && t.history != nil {
t.history.append(string(t.input))
}
+ // prof.Stop()
os.Exit(code)
}
go func() {
+ var focused *Item
+ var version int64
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()
@@ -774,24 +1402,50 @@ func (t *Terminal) Loop() {
t.printInfo()
case reqList:
t.printList()
+ currentFocus := t.currentItem()
+ if currentFocus != focused || version != t.version {
+ version = t.version
+ focused = currentFocus
+ if t.isPreviewEnabled() {
+ _, list := t.buildPlusList(t.preview.command, false)
+ t.previewBox.Set(reqPreviewEnqueue, list)
+ }
+ }
+ case reqJump:
+ if t.merger.Length() == 0 {
+ t.jumping = jumpDisabled
+ }
+ t.printList()
case reqHeader:
t.printHeader()
case reqRefresh:
t.suppress = false
+ case reqReinit:
+ t.tui.Resume(t.fullscreen)
+ t.redraw()
case reqRedraw:
- C.Clear()
- C.Endwin()
- C.Refresh()
- t.printAll()
+ t.redraw()
case reqClose:
- C.Close()
- if t.output() {
- exit(exitOk)
- }
- exit(exitNoMatch)
+ 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")
+ t.previewer.offset = 0
+ t.printPreview()
+ case reqPreviewRefresh:
+ t.printPreview()
+ case reqPrintQuery:
+ exit(func() int {
+ t.printer(string(t.input))
+ return exitOk
+ })
case reqQuit:
- C.Close()
- exit(exitInterrupt)
+ exit(func() int { return exitInterrupt })
}
}
t.placeCursor()
@@ -803,7 +1457,7 @@ func (t *Terminal) Loop() {
looping := true
for looping {
- event := C.GetChar()
+ event := t.tui.GetChar()
t.mutex.Lock()
previousInput := t.input
@@ -816,241 +1470,336 @@ 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)}
- return true
- }
- return false
- }
- toggleY := func(y int) {
- item := t.merger.Get(y)
- 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)
}
}
+ scrollPreview := func(amount int) {
+ t.previewer.offset = util.Constrain(
+ t.previewer.offset+amount, 0, t.previewer.lines-1)
+ req(reqPreviewRefresh)
+ }
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
}
}
- 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(action, int) bool
+ doActions := func(actions []action, mapkey int) bool {
+ for _, action := range actions {
+ if !doAction(action, mapkey) {
+ return false
+ }
}
+ return true
}
- 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{}
- 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)
- }
- 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)
- }
- 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())
- 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()
+ doAction = func(a action, mapkey int) bool {
+ switch a.t {
+ case actIgnore:
+ case actExecute, actExecuteSilent:
+ t.executeCommand(a.a, false, a.t == actExecuteSilent)
+ case actExecuteMulti:
+ t.executeCommand(a.a, true, false)
+ case actInvalid:
+ t.mutex.Unlock()
+ return false
+ case actTogglePreview:
+ if t.hasPreviewer() {
+ t.previewer.enabled = !t.previewer.enabled
+ t.tui.Clear()
+ t.resizeWindows()
+ if t.previewer.enabled {
+ valid, list := t.buildPlusList(t.preview.command, false)
+ if valid {
+ t.previewBox.Set(reqPreviewEnqueue, list)
+ }
}
- t.vmove(me.S)
+ 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)
+ t.mutex.Unlock()
+ return false
+ case actPreviewUp:
+ if t.hasPreviewWindow() {
+ scrollPreview(-1)
+ }
+ case actPreviewDown:
+ if t.hasPreviewWindow() {
+ scrollPreview(1)
+ }
+ case actPreviewPageUp:
+ if t.hasPreviewWindow() {
+ scrollPreview(-t.pwindow.Height())
+ }
+ case actPreviewPageDown:
+ if t.hasPreviewWindow() {
+ scrollPreview(t.pwindow.Height())
+ }
+ case actBeginningOfLine:
+ t.cx = 0
+ case actBackwardChar:
+ if t.cx > 0 {
+ t.cx--
+ }
+ case actPrintQuery:
+ req(reqPrintQuery)
+ 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{}
+ 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++ {
+ t.selectItem(t.merger.Get(i).item)
+ }
+ req(reqList, reqInfo)
+ }
+ case actDeselectAll:
+ if t.multi {
+ t.selected = make(map[int32]selectedItem)
+ t.version++
+ 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++ {
+ t.toggleItem(t.merger.Get(i).item)
}
- } 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 actToggleIn:
+ if t.reverse {
+ return doAction(action{t: actToggleUp}, mapkey)
+ }
+ return doAction(action{t: actToggleDown}, mapkey)
+ case actToggleOut:
+ if t.reverse {
+ return doAction(action{t: actToggleDown}, mapkey)
+ }
+ return doAction(action{t: actToggleUp}, mapkey)
+ case actToggleDown:
+ if t.multi && t.merger.Length() > 0 {
+ toggle()
+ t.vmove(-1, true)
+ req(reqList)
+ }
+ case actToggleUp:
+ if t.multi && t.merger.Length() > 0 {
+ toggle()
+ t.vmove(1, true)
+ req(reqList)
+ }
+ case actDown:
+ t.vmove(-1, true)
+ req(reqList)
+ case actUp:
+ t.vmove(1, true)
+ req(reqList)
+ case actAccept:
+ 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])
+ 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(t.wordRubout)
+ }
+ 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, false)
+ req(reqList)
+ case actPageDown:
+ t.vmove(-(t.maxItems() - 1), false)
+ req(reqList)
+ case actHalfPageUp:
+ t.vmove(t.maxItems()/2, false)
+ req(reqList)
+ case actHalfPageDown:
+ t.vmove(-(t.maxItems() / 2), false)
+ req(reqList)
+ case actJump:
+ t.jumping = jumpEnabled
+ req(reqJump)
+ case actJumpAccept:
+ t.jumping = jumpAcceptEnabled
+ req(reqJump)
+ case actBackwardWord:
+ t.cx = findLastMatch(t.wordRubout, string(t.input[:t.cx])) + 1
+ case actForwardWord:
+ t.cx += findFirstMatch(t.wordNext, string(t.input[t.cx:])) + 1
+ case actKillWord:
+ ncx := t.cx +
+ 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:]...)
+ }
+ 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 = trimQuery(t.history.previous())
+ t.cx = len(t.input)
+ }
+ case actNextHistory:
+ if t.history != nil {
+ t.history.override(string(t.input))
+ t.input = trimQuery(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
+ if me.S != 0 {
+ // Scroll
+ if t.window.Enclose(my, mx) && t.merger.Length() > 0 {
+ if t.multi && me.Mod {
toggle()
}
+ t.vmove(me.S, true)
req(reqList)
+ } else if t.hasPreviewWindow() && t.pwindow.Enclose(my, mx) {
+ scrollPreview(-me.S)
+ }
+ } else if t.window.Enclose(my, mx) {
+ mx -= t.window.Left()
+ my -= t.window.Top()
+ mx = util.Constrain(mx-t.promptLen, 0, len(t.input))
+ if !t.reverse {
+ my = t.window.Height() - 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 doActions(t.keymap[tui.DoubleClick], tui.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
+ }
+ changed := false
+ mapkey := event.Type
+ if t.jumping == jumpDisabled {
+ actions := t.keymap[mapkey]
+ if mapkey == tui.Rune {
+ mapkey = int(event.Char) + int(tui.AltZ)
+ if act, prs := t.keymap[mapkey]; prs {
+ actions = act
+ }
+ }
+ if !doActions(actions, mapkey) {
+ continue
+ }
+ 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() {
+ t.cy = idx + t.offset
+ if t.jumping == jumpAcceptEnabled {
+ req(reqClose)
+ }
+ }
+ }
+ t.jumping = jumpDisabled
+ req(reqList)
}
- changed := string(previousInput) != string(t.input)
t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed {
@@ -1068,15 +1817,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)
@@ -1085,12 +1826,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 {
@@ -1111,7 +1852,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/src/terminal_test.go b/src/terminal_test.go
new file mode 100644
index 0000000..60f2b1a
--- /dev/null
+++ b/src/terminal_test.go
@@ -0,0 +1,112 @@
+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.ToChars([]byte(trimmed))}
+}
+
+func TestReplacePlaceholder(t *testing.T) {
+ 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")}
+
+ 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{}, false, "query", items1)
+ check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
+
+ // {}, strip ansi
+ result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items1)
+ check("echo ' foo'\\''bar baz'")
+
+ // {}, with multiple items
+ result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items2)
+ check("echo 'foo'\\''bar baz'")
+
+ // {..}, strip leading whitespaces, preserve ansi
+ 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{}, false, "query", items1)
+ check("echo 'foo'\\''bar baz'")
+
+ // {q}
+ result = replacePlaceholder("echo {} {q}", true, Delimiter{}, false, "query", items1)
+ check("echo ' foo'\\''bar baz' 'query'")
+
+ // {q}, multiple items
+ 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 {}{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{}, 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}, 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}, 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/terminal_unix.go b/src/terminal_unix.go
new file mode 100644
index 0000000..2ae8175
--- /dev/null
+++ b/src/terminal_unix.go
@@ -0,0 +1,21 @@
+// +build !windows
+
+package fzf
+
+import (
+ "os"
+ "os/signal"
+ "syscall"
+)
+
+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
new file mode 100644
index 0000000..9de7ae4
--- /dev/null
+++ b/src/terminal_windows.go
@@ -0,0 +1,19 @@
+// +build windows
+
+package fzf
+
+import (
+ "os"
+)
+
+func notifyOnResize(resizeChan chan<- os.Signal) {
+ // TODO
+}
+
+func notifyStop(p *os.Process) {
+ // NOOP
+}
+
+func notifyOnCont(resizeChan chan<- os.Signal) {
+ // NOOP
+}
diff --git a/src/tokenizer.go b/src/tokenizer.go
index a8d0400..6c1d8ca 100644
--- a/src/tokenizer.go
+++ b/src/tokenizer.go
@@ -1,6 +1,7 @@
package fzf
import (
+ "bytes"
"regexp"
"strconv"
"strings"
@@ -18,8 +19,8 @@ type Range struct {
// Token contains the tokenized part of the strings and its prefix length
type Token struct {
- text []rune
- prefixLength int
+ text *util.Chars
+ prefixLength int32
}
// Delimiter for tokenizing the input
@@ -74,15 +75,14 @@ func ParseRange(str *string) (Range, bool) {
return newRange(n, n), true
}
-func withPrefixLengths(tokens [][]rune, begin int) []Token {
+func withPrefixLengths(tokens []string, 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{text: token, prefixLength: prefixLength}
- prefixLength += len(token)
+ for idx := range tokens {
+ chars := util.ToChars([]byte(tokens[idx]))
+ ret[idx] = Token{&chars, int32(prefixLength)}
+ prefixLength += chars.Length()
}
return ret
}
@@ -93,90 +93,77 @@ const (
awkWhite
)
-func awkTokenizer(input []rune) ([][]rune, int) {
+func awkTokenizer(input string) ([]string, int) {
// 9, 32
- ret := [][]rune{}
- str := []rune{}
+ ret := []string{}
prefixLength := 0
state := awkNil
- for _, r := range input {
+ begin := 0
+ end := 0
+ for idx := 0; idx < len(input); idx++ {
+ r := input[idx]
white := r == 9 || r == 32
switch state {
case awkNil:
if white {
prefixLength++
} else {
- state = awkBlack
- str = append(str, r)
+ state, begin, end = awkBlack, idx, idx+1
}
case awkBlack:
- str = append(str, r)
+ end = idx + 1
if white {
state = awkWhite
}
case awkWhite:
if white {
- str = append(str, r)
+ end = idx + 1
} else {
- ret = append(ret, str)
- state = awkBlack
- str = []rune{r}
+ ret = append(ret, input[begin:end])
+ state, begin, end = awkBlack, idx, idx+1
}
}
}
- if len(str) > 0 {
- ret = append(ret, str)
+ if begin < end {
+ ret = append(ret, input[begin:end])
}
return ret, prefixLength
}
// Tokenize tokenizes the given string with the delimiter
-func Tokenize(runes []rune, delimiter Delimiter) []Token {
+func Tokenize(text string, delimiter Delimiter) []Token {
if delimiter.str == nil && delimiter.regex == nil {
// AWK-style (\S+\s*)
- tokens, prefixLength := awkTokenizer(runes)
+ tokens, prefixLength := awkTokenizer(text)
return withPrefixLengths(tokens, prefixLength)
}
- var tokens []string
if delimiter.str != nil {
- tokens = strings.Split(string(runes), *delimiter.str)
- for i := 0; i < len(tokens)-1; i++ {
- tokens[i] = tokens[i] + *delimiter.str
- }
- } else if delimiter.regex != nil {
- str := string(runes)
- for len(str) > 0 {
- loc := delimiter.regex.FindStringIndex(str)
- if loc == nil {
- loc = []int{0, len(str)}
+ return withPrefixLengths(strings.SplitAfter(text, *delimiter.str), 0)
+ }
+
+ // FIXME performance
+ var tokens []string
+ if delimiter.regex != nil {
+ for len(text) > 0 {
+ loc := delimiter.regex.FindStringIndex(text)
+ if len(loc) < 2 {
+ 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([][]rune, len(tokens))
- for i, token := range tokens {
- asRunes[i] = []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...)
+ output.WriteString(token.text.ToString())
}
- return ret
-}
-
-func joinTokensAsRunes(tokens []Token) []rune {
- ret := []rune{}
- for _, token := range tokens {
- ret = append(ret, token.text...)
- }
- return ret
+ return output.String()
}
// Transform is used to transform the input when --with-nth option is given
@@ -184,19 +171,20 @@ func Transform(tokens []Token, withNth []Range) []Token {
transTokens := make([]Token, len(withNth))
numTokens := len(tokens)
for idx, r := range withNth {
- part := []rune{}
+ parts := []*util.Chars{}
minIdx := 0
if r.begin == r.end {
idx := r.begin
if idx == rangeEllipsis {
- part = append(part, joinTokensAsRunes(tokens)...)
+ chars := util.ToChars([]byte(joinTokens(tokens)))
+ parts = append(parts, &chars)
} else {
if idx < 0 {
idx += numTokens + 1
}
if idx >= 1 && idx <= numTokens {
minIdx = idx - 1
- part = append(part, tokens[idx-1].text...)
+ parts = append(parts, tokens[idx-1].text)
}
}
} else {
@@ -223,17 +211,32 @@ func Transform(tokens []Token, withNth []Range) []Token {
minIdx = util.Max(0, begin-1)
for idx := begin; idx <= end; idx++ {
if idx >= 1 && idx <= numTokens {
- part = append(part, tokens[idx-1].text...)
+ parts = append(parts, tokens[idx-1].text)
}
}
}
- var prefixLength int
+ // Merge multiple parts
+ var merged util.Chars
+ switch len(parts) {
+ case 0:
+ merged = util.ToChars([]byte{})
+ case 1:
+ merged = *parts[0]
+ default:
+ var output bytes.Buffer
+ for _, part := range parts {
+ output.WriteString(part.ToString())
+ }
+ merged = util.ToChars([]byte(output.String()))
+ }
+
+ var prefixLength int32
if minIdx < numTokens {
prefixLength = tokens[minIdx].prefixLength
} else {
prefixLength = 0
}
- transTokens[idx] = Token{part, prefixLength}
+ transTokens[idx] = Token{&merged, prefixLength}
}
return transTokens
}
diff --git a/src/tokenizer_test.go b/src/tokenizer_test.go
index 0f95aa1..110fd06 100644
--- a/src/tokenizer_test.go
+++ b/src/tokenizer_test.go
@@ -1,6 +1,8 @@
package fzf
-import "testing"
+import (
+ "testing"
+)
func TestParseRange(t *testing.T) {
{
@@ -43,23 +45,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 := Tokenize(input, Delimiter{})
+ if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 {
t.Errorf("%s", tokens)
}
// With delimiter
- tokens = Tokenize([]rune(input), delimiterRegexp(":"))
- if string(tokens[0].text) != " abc:" || tokens[0].prefixLength != 0 {
- t.Errorf("%s", tokens)
+ tokens = Tokenize(input, delimiterRegexp(":"))
+ if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 {
+ t.Error(tokens[0].text.ToString(), tokens[0].prefixLength)
}
// 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 {
+ 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 ||
+ tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 14 {
t.Errorf("%s", tokens)
}
}
@@ -67,7 +69,7 @@ func TestTokenize(t *testing.T) {
func TestTransform(t *testing.T) {
input := " abc: def: ghi: jkl"
{
- tokens := Tokenize([]rune(input), Delimiter{})
+ tokens := Tokenize(input, Delimiter{})
{
ranges := splitNth("1,2,3")
tx := Transform(tokens, ranges)
@@ -80,25 +82,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(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/tui/dummy.go b/src/tui/dummy.go
new file mode 100644
index 0000000..3965c9e
--- /dev/null
+++ b/src/tui/dummy.go
@@ -0,0 +1,44 @@
+// +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(bool) {}
+func (r *FullscreenRenderer) Resume(bool) {}
+func (r *FullscreenRenderer) Clear() {}
+func (r *FullscreenRenderer) Refresh() {}
+func (r *FullscreenRenderer) Close() {}
+
+func (r *FullscreenRenderer) DoesAutoWrap() 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, borderStyle BorderStyle) Window {
+ return nil
+}
diff --git a/src/tui/light.go b/src/tui/light.go
new file mode 100644
index 0000000..52e26ed
--- /dev/null
+++ b/src/tui/light.go
@@ -0,0 +1,907 @@
+package tui
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "regexp"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+ "unicode/utf8"
+
+ "github.com/junegunn/fzf/src/util"
+
+ "golang.org/x/crypto/ssh/terminal"
+)
+
+const (
+ defaultWidth = 80
+ defaultHeight = 24
+
+ defaultEscDelay = 100
+ 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 {
+ fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
+ os.Exit(2)
+ }
+ return in
+}
+
+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 < 32 &&
+ r != '\x1b' && (!allowNLCR || r != '\n' && r != '\r') {
+ 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
+ clearOnExit bool
+ prevDownTime time.Time
+ clickY []int
+ ttyin *os.File
+ buffer []byte
+ origState *terminal.State
+ width int
+ height int
+ yoffset int
+ tabstop int
+ escDelay int
+ fullscreen bool
+ upOneLine bool
+ queued string
+ y int
+ x int
+ maxHeightFunc func(int) int
+}
+
+type LightWindow struct {
+ renderer *LightRenderer
+ colored bool
+ border BorderStyle
+ top int
+ left int
+ width int
+ height int
+ posx int
+ posy int
+ tabstop int
+ fg Color
+ bg Color
+}
+
+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,
+ mouse: mouse,
+ clearOnExit: clearOnExit,
+ ttyin: openTtyIn(),
+ yoffset: 0,
+ tabstop: tabstop,
+ fullscreen: fullscreen,
+ upOneLine: false,
+ maxHeightFunc: maxHeightFunc}
+ 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
+ }
+ return Default16
+}
+
+func (r *LightRenderer) findOffset() (row int, col int) {
+ r.csi("6n")
+ r.flush()
+ 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
+}
+
+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() {
+ r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
+
+ 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)
+
+ if r.fullscreen {
+ r.smcup()
+ } else {
+ // 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
+ // 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()
+ }
+ for i := 1; i < r.MaxY(); i++ {
+ r.makeSpace()
+ }
+ }
+
+ if r.mouse {
+ r.csi("?1000h")
+ }
+ r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
+ r.csi("G")
+ r.csi("K")
+ if !r.clearOnExit && !r.fullscreen {
+ r.csi("s")
+ }
+ if !r.fullscreen && r.mouse {
+ r.yoffset, _ = r.findOffset()
+ }
+}
+
+func (r *LightRenderer) makeSpace() {
+ r.stderr("\n")
+ r.csi("G")
+}
+
+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 getEnv(name string, defaultValue int) int {
+ env := os.Getenv(name)
+ if len(env) == 0 {
+ return defaultValue
+ }
+ return atoi(env, defaultValue)
+}
+
+func (r *LightRenderer) updateTerminalSize() {
+ width, height, err := terminal.GetSize(r.fd())
+ if err == nil {
+ r.width = width
+ r.height = r.maxHeightFunc(height)
+ } else {
+ r.width = getEnv("COLUMNS", defaultWidth)
+ r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
+ }
+}
+
+func (r *LightRenderer) getch(nonblock bool) (int, bool) {
+ b := make([]byte, 1)
+ fd := r.fd()
+ util.SetNonblock(r.ttyin, nonblock)
+ _, err := util.Read(fd, b)
+ if err != nil {
+ return 0, false
+ }
+ return int(b[0]), true
+}
+
+func (r *LightRenderer) getBytes() []byte {
+ return r.getBytesInternal(r.buffer, false)
+}
+
+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 || nonblock {
+ retries = r.escDelay / escPollInterval
+ }
+ buffer = append(buffer, byte(c))
+
+ for {
+ c, ok = r.getch(true)
+ if !ok {
+ 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 0:
+ return Event{CtrlSpace, 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
+ 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 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] == '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:
+ 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.mouse {
+ 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) smcup() {
+ r.csi("?1049h")
+}
+
+func (r *LightRenderer) rmcup() {
+ r.csi("?1049l")
+}
+
+func (r *LightRenderer) Pause(clear bool) {
+ terminal.Restore(r.fd(), r.origState)
+ if clear {
+ if r.fullscreen {
+ r.rmcup()
+ } else {
+ r.smcup()
+ r.csi("H")
+ }
+ r.flush()
+ }
+}
+
+func (r *LightRenderer) Resume(clear bool) {
+ terminal.MakeRaw(r.fd())
+ 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
+ }
+}
+
+func (r *LightRenderer) Clear() {
+ if r.fullscreen {
+ r.csi("H")
+ }
+ // r.csi("u")
+ r.origin()
+ 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")
+ if r.clearOnExit {
+ if r.fullscreen {
+ r.rmcup()
+ } else {
+ r.origin()
+ if r.upOneLine {
+ r.csi("A")
+ }
+ r.csi("J")
+ }
+ } else if !r.fullscreen {
+ r.csi("u")
+ }
+ if r.mouse {
+ r.csi("?1000l")
+ }
+ r.flush()
+ terminal.Restore(r.fd(), r.origState)
+}
+
+func (r *LightRenderer) MaxX() int {
+ return r.width
+}
+
+func (r *LightRenderer) MaxY() int {
+ return r.height
+}
+
+func (r *LightRenderer) DoesAutoWrap() bool {
+ return false
+}
+
+func (r *LightRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
+ w := &LightWindow{
+ renderer: r,
+ colored: r.theme != nil,
+ border: borderStyle,
+ top: top,
+ left: left,
+ 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()
+ 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++ {
+ 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) stderrInternal(str string, allowNLCR bool) {
+ w.renderer.stderrInternal(str, allowNLCR)
+}
+
+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) 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)
+}
+
+func (w *LightWindow) Move(y int, x int) {
+ w.posx = x
+ w.posy = y
+
+ w.renderer.move(w.Top()+y, w.Left()+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 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(cleanse(text), false)
+ 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.stderrInternal(cleanse(text), false)
+}
+
+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()) 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.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, w.posx)
+ w.Move(w.posy+1, 0)
+ onMove()
+ }
+ }
+ }
+ return FillContinue
+}
+
+func (w *LightWindow) setBg() {
+ if w.bg != colDefault {
+ w.csiColor(colDefault, w.bg, AttrRegular)
+ }
+}
+
+func (w *LightWindow) Fill(text string) FillReturn {
+ 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.Move(w.posy, w.posx)
+ if fg == colDefault {
+ fg = w.fg
+ }
+ if bg == colDefault {
+ bg = w.bg
+ }
+ if 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)
+}
+
+func (w *LightWindow) FinishFill() {
+ w.MoveAndClear(w.posy, w.posx)
+ for y := w.posy + 1; y < w.height; y++ {
+ w.MoveAndClear(y, 0)
+ }
+}
+
+func (w *LightWindow) Erase() {
+ w.drawBorder()
+ // We don't erase the window here to avoid flickering during scroll
+ w.Move(0, 0)
+}
diff --git a/src/tui/tcell.go b/src/tui/tcell.go
new file mode 100644
index 0000000..7db37c4
--- /dev/null
+++ b/src/tui/tcell.go
@@ -0,0 +1,583 @@
+// +build tcell windows
+
+package tui
+
+import (
+ "os"
+ "time"
+ "unicode/utf8"
+
+ "runtime"
+
+ "github.com/gdamore/tcell"
+ "github.com/gdamore/tcell/encoding"
+
+ "github.com/mattn/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()))
+}
+
+type Attr tcell.Style
+
+type TcellWindow struct {
+ color bool
+ top int
+ left int
+ width int
+ height int
+ lastX int
+ lastY int
+ moveCursor bool
+ borderStyle BorderStyle
+}
+
+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
+ switch w.borderStyle {
+ case BorderAround:
+ w.drawBorder(true)
+ case BorderHorizontal:
+ w.drawBorder(false)
+ }
+}
+
+func (w *TcellWindow) FinishFill() {
+ // NO-OP
+}
+
+const (
+ Bold Attr = Attr(tcell.AttrBold)
+ Dim = Attr(tcell.AttrDim)
+ Blink = Attr(tcell.AttrBlink)
+ Reverse = Attr(tcell.AttrReverse)
+ Underline = Attr(tcell.AttrUnderline)
+ Italic = Attr(tcell.AttrNone) // Not supported
+)
+
+const (
+ AttrRegular Attr = 0
+)
+
+func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
+ if _screen.Colors() >= 256 {
+ return Dark256
+ }
+ return Default16
+}
+
+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 (r *FullscreenRenderer) initScreen() {
+ s, e := tcell.NewScreen()
+ if e != nil {
+ errorExit(e.Error())
+ }
+ if e = s.Init(); e != nil {
+ errorExit(e.Error())
+ }
+ if r.mouse {
+ s.EnableMouse()
+ } else {
+ s.DisableMouse()
+ }
+ _screen = s
+}
+
+func (r *FullscreenRenderer) Init() {
+ if os.Getenv("TERM") == "cygwin" {
+ os.Setenv("TERM", "")
+ }
+ encoding.Register()
+
+ r.initScreen()
+ initTheme(r.theme, r.defaultTheme(), r.forceBlack)
+}
+
+func (r *FullscreenRenderer) MaxX() int {
+ ncols, _ := _screen.Size()
+ return int(ncols)
+}
+
+func (r *FullscreenRenderer) MaxY() int {
+ _, nlines := _screen.Size()
+ return int(nlines)
+}
+
+func (w *TcellWindow) X() int {
+ return w.lastX
+}
+
+func (w *TcellWindow) Y() int {
+ return w.lastY
+}
+
+func (r *FullscreenRenderer) DoesAutoWrap() bool {
+ return false
+}
+
+func (r *FullscreenRenderer) Clear() {
+ _screen.Sync()
+ _screen.Clear()
+}
+
+func (r *FullscreenRenderer) Refresh() {
+ // noop
+}
+
+func (r *FullscreenRenderer) GetChar() Event {
+ ev := _screen.PollEvent()
+ switch ev := ev.(type) {
+ case *tcell.EventResize:
+ return Event{Resize, 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(r.prevDownTime) < doubleClickDuration {
+ r.clickY = append(r.clickY, x)
+ } else {
+ r.clickY = []int{x}
+ 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}}
+ }
+
+ // 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{keyfn('a'), 0, nil}
+ case tcell.KeyCtrlB:
+ return Event{keyfn('b'), 0, nil}
+ case tcell.KeyCtrlC:
+ return Event{keyfn('c'), 0, nil}
+ case tcell.KeyCtrlD:
+ return Event{keyfn('d'), 0, nil}
+ case tcell.KeyCtrlE:
+ return Event{keyfn('e'), 0, nil}
+ case tcell.KeyCtrlF:
+ return Event{keyfn('f'), 0, nil}
+ case tcell.KeyCtrlG:
+ 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{keyfn('j'), 0, nil}
+ case tcell.KeyCtrlK:
+ return Event{keyfn('k'), 0, nil}
+ case tcell.KeyCtrlL:
+ return Event{keyfn('l'), 0, nil}
+ case tcell.KeyCtrlM:
+ return Event{keyfn('m'), 0, nil}
+ case tcell.KeyCtrlN:
+ return Event{keyfn('n'), 0, nil}
+ case tcell.KeyCtrlO:
+ return Event{keyfn('o'), 0, nil}
+ case tcell.KeyCtrlP:
+ return Event{keyfn('p'), 0, nil}
+ case tcell.KeyCtrlQ:
+ return Event{keyfn('q'), 0, nil}
+ case tcell.KeyCtrlR:
+ return Event{keyfn('r'), 0, nil}
+ case tcell.KeyCtrlS:
+ return Event{keyfn('s'), 0, nil}
+ case tcell.KeyCtrlT:
+ return Event{keyfn('t'), 0, nil}
+ case tcell.KeyCtrlU:
+ return Event{keyfn('u'), 0, nil}
+ case tcell.KeyCtrlV:
+ return Event{keyfn('v'), 0, nil}
+ case tcell.KeyCtrlW:
+ return Event{keyfn('w'), 0, nil}
+ case tcell.KeyCtrlX:
+ return Event{keyfn('x'), 0, nil}
+ case tcell.KeyCtrlY:
+ return Event{keyfn('y'), 0, nil}
+ case tcell.KeyCtrlZ:
+ return Event{keyfn('z'), 0, nil}
+ case tcell.KeyCtrlSpace:
+ return Event{CtrlSpace, 0, nil}
+ case tcell.KeyBackspace2:
+ if alt {
+ return Event{AltBS, 0, nil}
+ }
+ 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.KeyBacktab:
+ return Event{BTab, 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{F11, 0, nil}
+ case tcell.KeyF12:
+ return Event{F12, 0, nil}
+
+ // ev.Ch doesn't work for some reason for space:
+ case tcell.KeyRune:
+ 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}
+ }
+ if r >= '0' && r <= '9' {
+ return Event{Alt0 + int(r) - '0', 0, nil}
+ }
+ }
+ return Event{Rune, r, nil}
+
+ case tcell.KeyEsc:
+ return Event{ESC, 0, nil}
+
+ }
+ }
+
+ return Event{Invalid, 0, nil}
+}
+
+func (r *FullscreenRenderer) Pause(bool) {
+ _screen.Fini()
+}
+
+func (r *FullscreenRenderer) Resume(bool) {
+ r.initScreen()
+}
+
+func (r *FullscreenRenderer) Close() {
+ _screen.Fini()
+}
+
+func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
+ // TODO
+ for _, w := range windows {
+ w.Refresh()
+ }
+ _screen.Show()
+}
+
+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,
+ borderStyle: borderStyle}
+}
+
+func (w *TcellWindow) 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, ColNormal.style())
+ }
+ }
+}
+
+func (w *TcellWindow) Erase() {
+ fill(w.left-1, w.top, w.width+1, 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 *TcellWindow) Move(y int, x int) {
+ w.lastX = x
+ w.lastY = y
+ w.moveCursor = true
+}
+
+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, ColNormal.style())
+ }
+ w.lastX = x
+}
+
+func (w *TcellWindow) Print(text string) {
+ w.printString(text, ColNormal, 0)
+}
+
+func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) {
+ t := text
+ lx := 0
+
+ var style tcell.Style
+ if w.color {
+ style = pair.style().
+ Reverse(a&Attr(tcell.AttrReverse) != 0).
+ Underline(a&Attr(tcell.AttrUnderline) != 0)
+ } else {
+ style = ColNormal.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)
+
+ 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.lastY++
+ lx = 0
+ } else {
+
+ if r == '\u000D' { // skip carriage return
+ continue
+ }
+
+ 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.lastX += lx
+}
+
+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) FillReturn {
+ lx := 0
+
+ var style tcell.Style
+ if w.color {
+ style = pair.style()
+ } else {
+ style = ColNormal.style()
+ }
+ 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)
+
+ for _, r := range text {
+ if r == '\n' {
+ w.lastY++
+ w.lastX = 0
+ lx = 0
+ } else {
+ var xPos = w.left + w.lastX + lx
+
+ // word wrap:
+ if xPos >= (w.left + w.width) {
+ w.lastY++
+ w.lastX = 0
+ lx = 0
+ xPos = w.left
+ }
+ var yPos = w.top + w.lastY
+
+ if yPos >= (w.top + w.height) {
+ return FillSuspend
+ }
+
+ _screen.SetContent(xPos, yPos, r, nil, style)
+ lx += runewidth.RuneWidth(r)
+ }
+ }
+ w.lastX += lx
+
+ return FillContinue
+}
+
+func (w *TcellWindow) Fill(str string) FillReturn {
+ return w.fillString(str, ColNormal, 0)
+}
+
+func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
+ 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) {
+ left := w.left
+ right := left + w.width
+ top := w.top
+ bot := top + w.height
+
+ var style tcell.Style
+ if w.color {
+ style = ColBorder.style()
+ } else {
+ style = ColNormal.style()
+ }
+
+ for x := left; x < right; x++ {
+ _screen.SetContent(x, top, tcell.RuneHLine, nil, style)
+ _screen.SetContent(x, bot-1, tcell.RuneHLine, 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)
+ }
+}
diff --git a/src/tui/tui.go b/src/tui/tui.go
new file mode 100644
index 0000000..244e0b0
--- /dev/null
+++ b/src/tui/tui.go
@@ -0,0 +1,421 @@
+package tui
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+ "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
+ CtrlSpace
+
+ Invalid
+ Resize
+ 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
+ F11
+ F12
+
+ Change
+
+ AltSpace
+ AltSlash
+ AltBS
+
+ Alt0
+)
+
+const ( // Reset iota
+ AltA = Alt0 + 'a' - '0' + iota
+ AltB
+ AltC
+ AltD
+ AltE
+ AltF
+ AltZ = AltA + 'z' - 'a'
+ CtrlAltA = AltZ + 1
+ CtrlAltM = CtrlAltA + 'm' - 'a'
+)
+
+const (
+ doubleClickDuration = 500 * time.Millisecond
+)
+
+type Color int32
+
+func (c Color) is24() bool {
+ return c > 0 && (c&(1<<24)) > 0
+}
+
+const (
+ colUndefined Color = -2
+ colDefault = -1
+)
+
+const (
+ colBlack Color = iota
+ colRed
+ colGreen
+ colYellow
+ colBlue
+ colMagenta
+ colCyan
+ colWhite
+)
+
+type FillReturn int
+
+const (
+ FillContinue FillReturn = iota
+ FillNextLine
+ FillSuspend
+)
+
+type ColorPair struct {
+ fg Color
+ bg Color
+ id int
+}
+
+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}
+}
+
+func (p ColorPair) Fg() Color {
+ return p.fg
+}
+
+func (p ColorPair) Bg() Color {
+ return p.bg
+}
+
+func (p ColorPair) is24() bool {
+ return p.fg.is24() || p.bg.is24()
+}
+
+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
+}
+
+type BorderStyle int
+
+const (
+ BorderNone BorderStyle = iota
+ BorderAround
+ BorderHorizontal
+)
+
+type Renderer interface {
+ Init()
+ Pause(clear bool)
+ Resume(clear bool)
+ Clear()
+ RefreshWindows(windows []Window)
+ Refresh()
+ Close()
+
+ GetChar() Event
+
+ MaxX() int
+ MaxY() int
+ DoesAutoWrap() bool
+
+ NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window
+}
+
+type Window interface {
+ Top() int
+ Left() int
+ Width() int
+ Height() int
+
+ Refresh()
+ FinishFill()
+ Close()
+
+ X() int
+ Y() 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) FillReturn
+ CFill(fg Color, bg Color, attr Attr, text string) FillReturn
+ 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
+
+ ColNormal ColorPair
+ ColPrompt ColorPair
+ ColMatch ColorPair
+ ColCurrent ColorPair
+ ColCurrentMatch ColorPair
+ ColSpinner ColorPair
+ ColInfo ColorPair
+ ColCursor ColorPair
+ ColSelected ColorPair
+ ColHeader ColorPair
+ ColBorder ColorPair
+)
+
+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 errorExit(message string) {
+ fmt.Fprintln(os.Stderr, message)
+ os.Exit(2)
+}
+
+func init() {
+ 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, baseTheme *ColorTheme, forceBlack bool) {
+ if theme == nil {
+ initPalette(theme)
+ return
+ }
+
+ if forceBlack {
+ 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)
+
+ initPalette(theme)
+}
+
+func initPalette(theme *ColorTheme) {
+ 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)
+ }
+}
+
+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
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)
+}
diff --git a/src/update_assets.rb b/src/update_assets.rb
index c849f9e..b4aaba5 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 "
@@ -22,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)
diff --git a/src/util/chars.go b/src/util/chars.go
new file mode 100644
index 0000000..9e58313
--- /dev/null
+++ b/src/util/chars.go
@@ -0,0 +1,167 @@
+package util
+
+import (
+ "unicode"
+ "unicode/utf8"
+ "unsafe"
+)
+
+const (
+ overflow64 uint64 = 0x8080808080808080
+ overflow32 uint32 = 0x80808080
+)
+
+type Chars struct {
+ 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
+}
+
+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)
+}
+
+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
+ }
+ return *(*[]rune)(unsafe.Pointer(&chars.slice))
+}
+
+func (chars *Chars) Get(i int) rune {
+ if runes := chars.optionalRunes(); runes != nil {
+ return runes[i]
+ }
+ return rune(chars.slice[i])
+}
+
+func (chars *Chars) Length() int {
+ if runes := chars.optionalRunes(); runes != nil {
+ return len(runes)
+ }
+ return len(chars.slice)
+}
+
+// TrimLength returns the length after trimming leading and trailing whitespaces
+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-- {
+ char := chars.Get(i)
+ if !unicode.IsSpace(char) {
+ break
+ }
+ }
+ // Completely empty
+ if i < 0 {
+ return 0
+ }
+
+ var j int
+ for j = 0; j < len; j++ {
+ char := chars.Get(j)
+ if !unicode.IsSpace(char) {
+ break
+ }
+ }
+ chars.trimLength = AsUint16(i - j + 1)
+ return chars.trimLength
+}
+
+func (chars *Chars) TrailingWhitespaces() int {
+ whitespaces := 0
+ for i := chars.Length() - 1; i >= 0; i-- {
+ char := chars.Get(i)
+ if !unicode.IsSpace(char) {
+ break
+ }
+ whitespaces++
+ }
+ return whitespaces
+}
+
+func (chars *Chars) ToString() string {
+ if runes := chars.optionalRunes(); runes != nil {
+ return string(runes)
+ }
+ return string(chars.slice)
+}
+
+func (chars *Chars) ToRunes() []rune {
+ if runes := chars.optionalRunes(); runes != nil {
+ return runes
+ }
+ bytes := chars.slice
+ runes := make([]rune, len(bytes))
+ for idx, b := range bytes {
+ runes[idx] = rune(b)
+ }
+ return runes
+}
+
+func (chars *Chars) CopyRunes(dest []rune) {
+ if runes := chars.optionalRunes(); runes != nil {
+ copy(dest, runes)
+ return
+ }
+ for idx, b := range chars.slice[:len(dest)] {
+ dest[idx] = rune(b)
+ }
+ return
+}
diff --git a/src/util/chars_test.go b/src/util/chars_test.go
new file mode 100644
index 0000000..b7983f3
--- /dev/null
+++ b/src/util/chars_test.go
@@ -0,0 +1,46 @@
+package util
+
+import "testing"
+
+func TestToCharsAscii(t *testing.T) {
+ chars := ToChars([]byte("foobar"))
+ if !chars.inBytes || chars.ToString() != "foobar" || !chars.inBytes {
+ t.Error()
+ }
+}
+
+func TestCharsLength(t *testing.T) {
+ chars := ToChars([]byte("\tabc한글 "))
+ if chars.inBytes || chars.Length() != 8 || chars.TrimLength() != 5 {
+ t.Error()
+ }
+}
+
+func TestCharsToString(t *testing.T) {
+ text := "\tabc한글 "
+ chars := ToChars([]byte(text))
+ if chars.ToString() != text {
+ t.Error()
+ }
+}
+
+func TestTrimLength(t *testing.T) {
+ check := func(str string, exp uint16) {
+ 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/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
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 aa5f227..867935a 100644
--- a/src/util/util.go
+++ b/src/util/util.go
@@ -1,28 +1,40 @@
package util
-// #include
-import "C"
-
import (
+ "math"
"os"
"time"
- "unicode/utf8"
+
+ "github.com/mattn/go-isatty"
+ "github.com/mattn/go-runewidth"
)
-// Max returns the largest integer
-func Max(first int, items ...int) int {
- max := first
- for _, item := range items {
- if item > max {
- max = item
- }
+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 := Max(runewidth.RuneWidth(r), 1)
+ _runeWidths[r] = w
+ return w
}
- return max
}
-// Min32 returns the smallest 32-bit integer
-func Min32(first int32, second int32) int32 {
- if first <= second {
+// Max returns the largest integer
+func Max(first int, second int) int {
+ if first >= second {
+ return first
+ }
+ return second
+}
+
+// Max16 returns the largest integer
+func Max16(first int16, second int16) int16 {
+ if first >= second {
return first
}
return second
@@ -36,6 +48,22 @@ func Max32(first int32, second int32) int32 {
return second
}
+// 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 {
+ 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 {
@@ -58,6 +86,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 {
@@ -72,31 +109,5 @@ func DurWithin(
// IsTty returns true is stdin is a terminal
func IsTty() bool {
- return int(C.isatty(C.int(os.Stdin.Fd()))) != 0
-}
-
-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]
-}
-
-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
+ return isatty.IsTerminal(os.Stdin.Fd())
}
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")
}
}
diff --git a/src/util/util_unix.go b/src/util/util_unix.go
new file mode 100644
index 0000000..fc63c02
--- /dev/null
+++ b/src/util/util_unix.go
@@ -0,0 +1,38 @@
+// +build !windows
+
+package util
+
+import (
+ "os"
+ "os/exec"
+ "syscall"
+)
+
+// ExecCommand executes the given command with $SHELL
+func ExecCommand(command string) *exec.Cmd {
+ shell := os.Getenv("SHELL")
+ 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)
+}
+
+// IsWindows returns true on Windows
+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)
+}
+
+// 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
new file mode 100644
index 0000000..67b7031
--- /dev/null
+++ b/src/util/util_windows.go
@@ -0,0 +1,42 @@
+// +build windows
+
+package util
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "syscall"
+)
+
+// 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 {
+ 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
+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)
+}
+
+// Read executes syscall.Read on file descriptor
+func Read(fd int, b []byte) (int, error) {
+ return syscall.Read(syscall.Handle(fd), b)
+}
diff --git a/test/fzf.vader b/test/fzf.vader
index c27fddc..faa9418 100644
--- a/test/fzf.vader
+++ b/test/fzf.vader
@@ -1,14 +1,16 @@
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
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
@@ -17,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)
@@ -35,6 +37,136 @@ 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()
+
+ 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()
+ 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 (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'
+
+ let g:fzf_colors = { 'fg': ['fg', 'Error'] }
+ let opts = fzf#wrap({})
+ Assert opts.options =~ '^--color=fg:'
+
+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 '''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 '^"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
diff --git a/test/test_go.rb b/test/test_go.rb
index e76b520..27c2a93 100644
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -1,25 +1,32 @@
#!/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
+FILE = File.expand_path(__FILE__)
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
+ 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
@@ -30,11 +37,15 @@ def wait
return if yield
sleep 0.05
end
- throw 'timeout'
+ raise 'timeout'
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
@@ -44,6 +55,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
@@ -52,31 +67,31 @@ class Tmux
attr_reader :win
- def initialize shell = :bash
+ def initialize(shell = :bash)
@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
+ 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
@@ -85,33 +100,69 @@ 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 capture pane = 0
- File.unlink TEMPNAME while File.exists? TEMPNAME
+ 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.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 pane = 0
+ def until(refresh = false, pane = 0)
lines = nil
begin
wait do
lines = capture(pane)
class << lines
+ def counts
+ 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
+
+ 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?
+ 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
+ rescue StandardError
+ puts $ERROR_INFO.backtrace
puts '>' * 80
puts lines
puts '<' * 80
@@ -123,16 +174,20 @@ class Tmux
def prepare
tries = 0
begin
- self.send_keys 'C-u', 'hello', 'Right'
- self.until { |lines| lines[-1].end_with?('hello') }
- rescue Exception
+ self.until do |lines|
+ send_keys 'C-u', 'hello'
+ lines[-1].end_with?('hello')
+ end
+ 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
@@ -142,21 +197,23 @@ 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
- ENV.delete 'FZF_DEFAULT_OPTS'
- ENV.delete 'FZF_CTRL_T_COMMAND'
- ENV.delete 'FZF_DEFAULT_COMMAND'
+ 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
@@ -165,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
@@ -203,41 +258,47 @@ 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
- 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
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 =~ /^>/ }
# 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
@@ -245,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
@@ -261,30 +322,43 @@ 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
+ 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 =~ /^>/ }
@@ -293,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
@@ -302,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
@@ -315,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
@@ -329,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
@@ -338,30 +412,30 @@ 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', '15', '25'], readonce.split($/)
+ assert_equal %w[5 5 50 51], readonce.split($INPUT_RECORD_SEPARATOR)
end
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($/)
+ assert_equal ['가나다'], readonce.split($INPUT_RECORD_SEPARATOR)
end
def test_sync
@@ -374,7 +448,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($INPUT_RECORD_SEPARATOR)
end
def test_tac
@@ -383,17 +457,18 @@ 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
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
- assert_equal %w[99 999 998], readonce.split($/)
+ assert_equal %w[99 999 998], readonce.split($INPUT_RECORD_SEPARATOR)
end
def test_tac_nosort
@@ -404,26 +479,31 @@ 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
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
- assert_equal [expected, '55'], readonce.split($/)
+ tmux.send_keys(*feed)
+ tmux.prepare
+ 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-/', %i[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
@@ -433,12 +513,21 @@ 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
+ 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 %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
@@ -446,20 +535,20 @@ 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($/)
+ 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], `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($INPUT_RECORD_SEPARATOR)
+ assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `#{FZF} -fс < #{tempname}`.split($INPUT_RECORD_SEPARATOR)
end
def test_tiebreak
@@ -471,7 +560,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($INPUT_RECORD_SEPARATOR)
by_length = %w[
----foobar--
@@ -479,8 +568,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($INPUT_RECORD_SEPARATOR)
+ assert_equal by_length, `#{FZF} -ffoobar --tiebreak=length < #{tempname}`.split($INPUT_RECORD_SEPARATOR)
by_begin = %w[
--foobar--------
@@ -488,17 +577,115 @@ 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($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--------
- ], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/)
+ ], `#{FZF} -ffoobar --tiebreak end < #{tempname}`.split($INPUT_RECORD_SEPARATOR)
- assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/)
+ assert_equal input, `#{FZF} -f"!z" -x --tiebreak end < #{tempname}`.split($INPUT_RECORD_SEPARATOR)
+ end
+
+ def test_tiebreak_index_begin
+ writelines tempname, [
+ 'xoxxxxxoxx',
+ 'xoxxxxxox',
+ 'xxoxxxoxx',
+ 'xxxoxoxxx',
+ 'xxxxoxox',
+ ' xxoxoxxx'
+ ]
+
+ assert_equal [
+ 'xxxxoxox',
+ ' xxoxoxxx',
+ 'xxxoxoxxx',
+ 'xxoxxxoxx',
+ 'xoxxxxxox',
+ 'xoxxxxxoxx'
+ ], `#{FZF} -foo < #{tempname}`.split($INPUT_RECORD_SEPARATOR)
+
+ assert_equal [
+ 'xxxoxoxxx',
+ 'xxxxoxox',
+ ' xxoxoxxx',
+ 'xxoxxxoxx',
+ 'xoxxxxxoxx',
+ '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
+ assert_equal [
+ ' xxoxoxxx',
+ 'xxxoxoxxx',
+ 'xxxxoxox',
+ 'xxoxxxoxx',
+ 'xoxxxxxoxx',
+ 'xoxxxxxox'
+ ], `#{FZF} -foo --tiebreak=begin < #{tempname}`.split($INPUT_RECORD_SEPARATOR)
+
+ assert_equal [
+ ' xxoxoxxx',
+ 'xxxoxoxxx',
+ 'xxxxoxox',
+ 'xxoxxxoxx',
+ 'xoxxxxxox',
+ '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'
+ ]
+ assert_equal [
+ 'foo bar baz',
+ 'baz foo bar'
+ ], `#{FZF} -fbar --tiebreak=begin --algo=v2 < #{tempname}`.split($INPUT_RECORD_SEPARATOR)
+ end
+
+ def test_tiebreak_end
+ writelines tempname, [
+ 'xoxxxxxxxx',
+ 'xxoxxxxxxx',
+ 'xxxoxxxxxx',
+ 'xxxxoxxxx',
+ 'xxxxxoxxx',
+ ' xxxxoxxx'
+ ]
+
+ assert_equal [
+ ' xxxxoxxx',
+ 'xxxxoxxxx',
+ 'xxxxxoxxx',
+ 'xoxxxxxxxx',
+ 'xxoxxxxxxx',
+ 'xxxoxxxxxx'
+ ], `#{FZF} -fo < #{tempname}`.split($INPUT_RECORD_SEPARATOR)
+
+ assert_equal [
+ 'xxxxxoxxx',
+ ' xxxxoxxx',
+ 'xxxxoxxxx',
+ 'xxxoxxxxxx',
+ 'xxoxxxxxxx',
+ 'xoxxxxxxxx'
+ ], `#{FZF} -fo --tiebreak=end < #{tempname}`.split($INPUT_RECORD_SEPARATOR)
+
+ assert_equal [
+ 'xxxxxoxxx',
+ ' xxxxoxxx',
+ 'xxxxoxxxx',
+ 'xxxoxxxxxx',
+ 'xxoxxxxxxx',
+ 'xoxxxxxxxx'
+ ], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.split($INPUT_RECORD_SEPARATOR)
end
def test_tiebreak_length_with_nth
@@ -516,26 +703,10 @@ class TestGoFZF < TestBase
123:hello
1234567:h
]
- assert_equal output, `cat #{tempname} | #{FZF} -fh`.split($/)
+ assert_equal output, `#{FZF} -fh < #{tempname}`.split($INPUT_RECORD_SEPARATOR)
- output = %w[
- 1234567:h
- 12345:he
- 1:hell
- 123:hello
- ]
- assert_equal output, `cat #{tempname} | #{FZF} -fh -n2 -d:`.split($/)
- end
-
- def test_tiebreak_end_backward_scan
- input = %w[
- foobar-fb
- fubar
- ]
- writelines tempname, input
-
- assert_equal input.reverse, `cat #{tempname} | #{FZF} -f fb`.split($/)
- assert_equal input, `cat #{tempname} | #{FZF} -f fb --tiebreak=end`.split($/)
+ # Since 0.16.8, --nth doesn't affect --tiebreak
+ assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.split($INPUT_RECORD_SEPARATOR)
end
def test_invalid_cache
@@ -548,16 +719,54 @@ 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
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($INPUT_RECORD_SEPARATOR)
end
def test_long_line
@@ -565,13 +774,16 @@ 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
- 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
@@ -594,16 +806,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' }
@@ -633,7 +850,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
@@ -647,23 +864,142 @@ 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
+ opts = %[--bind \\"alt-a:execute(echo [{}] >> #{output}),alt-b:execute[echo /{}{}/ >> #{output}],C:execute:echo /{}{}{}/ >> #{output}\\"]
+ 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']
+ tmux.send_keys :Escape, :a
+ wait['/3']
+ tmux.send_keys :Escape, :a
+ wait['/3']
tmux.send_keys :Up
- tmux.send_keys :Escape, :b, :Escape, :b
+ tmux.send_keys :Escape, :b
+ wait['/3']
+ tmux.send_keys :Escape, :b
+ wait['/3']
tmux.send_keys :Up
tmux.send_keys :C
- tmux.send_keys 'foobar'
- tmux.until { |lines| lines[-2].include? '0/100' }
- tmux.send_keys :Escape, :a, :Escape, :b, :Escape, :c
+ wait['3/3']
+ tmux.send_keys 'barfoo'
+ wait['0/3']
+ tmux.send_keys :Escape, :a
+ wait['/3']
+ tmux.send_keys :Escape, :b
+ wait['/3']
tmux.send_keys :Enter
readonce
- assert_equal ['["1"]', '["1"]', '("2"), ("2")', '("2"), ("2")', '("3"), ["3"], @"3"@'],
- File.readlines(output).map(&:chomp)
+ 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
+ begin
+ File.unlink output
+ rescue
+ nil
+ end
+ end
+
+ def test_execute_multi
+ output = '/tmp/fzf-test-execute-multi'
+ 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? '/4' }
+ tmux.send_keys :BTab, :BTab, :BTab
+ tmux.send_keys :Escape, :a
+ tmux.until { |lines| lines[-2].include? '/4' }
+ tmux.send_keys :Tab, :Tab
+ tmux.send_keys :Escape, :a
+ tmux.until { |lines| lines[-2].include? '/4' }
+ 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)
+ ensure
+ begin
+ File.unlink output
+ rescue
+ nil
+ end
+ end
+
+ def test_execute_plus_flag
+ 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-silent(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
+ begin
+ File.unlink output
+ rescue
+ nil
+ end
+ end
+
+ def test_execute_shell
+ # Custom script to use as $SHELL
+ output = tempname + '.out'
+ 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
+ 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
+ begin
+ File.unlink output
+ rescue
+ nil
+ end
end
def test_cycle
@@ -673,15 +1009,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
@@ -690,14 +1026,14 @@ class TestGoFZF < TestBase
2.times do
tmux.until do |lines|
lines[-2].include?('/90') &&
- lines[-3] == ' 1' &&
- lines[-4] == ' 2' &&
- lines[-13] == '> 15'
+ lines[-3] == ' 1' &&
+ lines[-4] == ' 2' &&
+ 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
@@ -705,78 +1041,78 @@ class TestGoFZF < TestBase
2.times do
tmux.until do |lines|
lines[1].include?('/90') &&
- lines[2] == ' 1' &&
- lines[3] == ' 2' &&
- lines[12] == '> 15'
+ lines[2] == ' 1' &&
+ lines[3] == ' 2' &&
+ 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
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
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
+ lines[-7..-3].map(&:strip) == header
end
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
+ lines[2..6].map(&:strip) == header
end
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 &&
- 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
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 &&
- 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_canel
- tmux.send_keys "seq 10 | #{fzf "--bind 2:cancel"}", :Enter
+ 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'
tmux.until { |lines| lines[-1] == '> 3' && lines[-2].include?('1/10') }
@@ -789,78 +1125,276 @@ 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
- def test_with_nth
+ 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(true) do |lines|
+ exp.start_with? lines[-3].to_s.strip.sub(/\.\.$/, '')
+ end
+ tmux.send_keys :Enter
+ end
+ end
+
+ def test_with_nth_basic
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
`echo foo | #{FZF} -q bar -0`
- assert_equal 1, $?.exitstatus
- end
-
- def test_invalid_term
- lines = `TERM=xxx #{FZF}`
- assert_equal 2, $?.exitstatus
- assert lines.include?('Invalid $TERM: xxx')
+ 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
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
-private
- def writelines path, lines
- File.unlink path while File.exists? path
- File.open(path, 'w') { |f| f << lines.join($/) }
+ 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
+
+ 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
+
+ def test_hscroll_off
+ writelines tempname, ['=' * 10_000 + '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
+
+ 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
+
+ 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($INPUT_RECORD_SEPARATOR)
+ 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
+
+ def test_preview
+ 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?(' {-}') }
+ 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?('{') }
+ 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
+ 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
+
+ def test_preview_size_0
+ 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.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
+
+ def test_no_clear
+ 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 { |_| %w[1] == File.readlines(tempname).map(&:chomp) }
+ tmux.until { |lines| lines[-1] == prompt }
+ 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
+
+ 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.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 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 ['foo bar'], `#{FZF} -f'^foo\\ bar$' < #{tempname}`.lines.map(&:chomp)
+ assert_equal input.lines.count - 1, `#{FZF} -f'!^foo\\ bar$' < #{tempname}`.lines.count
end
end
@@ -873,72 +1407,125 @@ 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 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
- 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' }
+ def test_ctrl_t
+ set_var 'FZF_CTRL_T_COMMAND', 'seq 100'
+
+ 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
+ tmux.until { |lines| lines.any_include? '1 2 3' }
+ tmux.send_keys 'C-c'
+ end
+
+ def test_ctrl_t_unicode
+ writelines tempname, ['fzf-unicode 테스트1', 'fzf-unicode 테스트2']
+ set_var 'FZF_CTRL_T_COMMAND', "cat #{tempname}"
+
+ 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 }
+
+ 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
+ tmux.until { |lines| lines.match_count == 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
+ 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
+ lines = retries do
+ tmux.prepare
+ tmux.send_keys :Escape, :c
+ tmux.until { |lines| lines.item_count.positive? }
+ end
+ 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) }
end
+ def test_alt_c_command
+ set_var 'FZF_ALT_C_COMMAND', 'echo /tmp'
+
+ tmux.prepare
+ tmux.send_keys 'cd /', :Enter
+
+ retries do
+ tmux.prepare
+ tmux.send_keys :Escape, :c
+ tmux.until { |lines| lines.item_count == 1 }
+ end
+ tmux.send_keys :Enter
+
+ 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
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', 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 'echo 4th', :Enter
+ retries do
+ tmux.prepare
+ tmux.send_keys 'C-r'
+ tmux.until { |lines| lines.item_count.positive? }
+ end
+ tmux.send_keys 'C-r'
+ 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' }
end
+
+ def retries(times = 3)
+ (times - 1).times do
+ begin
+ return yield
+ rescue RuntimeError
+ end
+ end
+ yield
+ end
end
module CompletionTest
@@ -950,67 +1537,76 @@ 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 :BTab, :BTab
- tmux.until(1) { |lines| lines[-2].include?('(2)') }
+ tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab
+ tmux.until { |lines| lines.item_count.positive? }
+ tmux.send_keys ' !d'
+ 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'
+ 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, 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.positive? }
+ 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, pane: 0
- tmux.until(1) { |lines| lines[-3].end_with? 'no~such~user' }
+ tmux.send_keys 'cat ~such**', :Tab
+ 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'
- tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab, pane: 0
- tmux.until(1) { |lines| lines.item_count > 0 }
+ tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab
+ tmux.until { |lines| lines.item_count.positive? }
+ tmux.send_keys 'foobar$'
+ tmux.until { |lines| lines.match_count == 1 }
+ tmux.send_keys :Enter
+ 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(true) { |lines| lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-') }
tmux.send_keys :Enter
- tmux.until do |lines|
- tmux.send_keys 'C-L'
- lines[-1].end_with?('/tmp/fzf\ test/foobar')
- end
ensure
['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f|
FileUtils.rm_rf File.expand_path(f)
end
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
- 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 55
- tmux.until(1) { |lines| lines[-2].start_with? ' 1/' }
+ def test_file_completion_root
+ tmux.send_keys 'ls /**', :Tab
+ tmux.until { |lines| lines.item_count.positive? }
tmux.send_keys :Enter
- tmux.until do |lines|
- tmux.send_keys 'C-L'
- lines[-1] == 'cd /tmp/fzf-test/d55/'
+ end
+
+ def test_dir_completion
+ (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
+ 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 }
+ tmux.send_keys :Enter
+ 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' }
@@ -1032,17 +1628,77 @@ 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.positive? }
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}"
- end
+ 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
+ tmux.send_keys '_fzf_compgen_path() { echo "\$1"; seq 10; }', :Enter
+ tmux.prepare
+ 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(true) { |lines| lines[-1] == 'ls /tmp 1 2' }
+ end
+
+ def test_unset_completion
+ tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
+ tmux.prepare
+
+ # Using tmux
+ 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' }
+ tmux.send_keys 'C-c'
+
+ # FZF_TMUX=1
+ new_shell
+ 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' }
+ 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
+ tmux.until { |lines| lines.match_count == 2 }
+
+ 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
+ tmux.until { |lines| lines.match_count == 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
+ tmux.until(true) { |lines| lines.any_include? 'cat' }
+ tmux.send_keys :Enter
+ tmux.until { |lines| lines[-1].include? 'test3test4' }
end
end
@@ -1052,7 +1708,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
@@ -1067,7 +1723,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
@@ -1081,12 +1737,12 @@ 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? }
+ 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
@@ -1097,4 +1753,3 @@ class TestFish < TestBase
@tmux = Tmux.new :fish
end
end
-