mirror of
https://github.com/junegunn/fzf.git
synced 2025-07-26 09:42:02 -07:00
Compare commits
168 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6dbc108da2 | ||
|
bd98f988f0 | ||
|
06301c7847 | ||
|
18a1aeaa91 | ||
|
c9f16b6430 | ||
|
bc9d2abdb6 | ||
|
28810c178f | ||
|
a9e64efe45 | ||
|
6b5886c034 | ||
|
7727ad43af | ||
|
bbe10f4f77 | ||
|
5e72709613 | ||
|
9e85cba0d0 | ||
|
4b59ced08f | ||
|
8dbdd55730 | ||
|
6725151a99 | ||
|
d4f3d5a164 | ||
|
7b5ccc45bc | ||
|
940214a1a2 | ||
|
68bd410159 | ||
|
b13fcfd831 | ||
|
07ef2b051c | ||
|
3fc795340d | ||
|
70cfa6af13 | ||
|
dbcaec59ae | ||
|
faedae708e | ||
|
0c66521b23 | ||
|
bf92862459 | ||
|
1a68698d76 | ||
|
842a73357c | ||
|
5efdeccdbb | ||
|
050777b8c4 | ||
|
a4d78e2200 | ||
|
b49f22cdf9 | ||
|
7e483b0c25 | ||
|
3cf9ae04c7 | ||
|
bf0cb4bfe2 | ||
|
773133c4ce | ||
|
ca0b3b6fd7 | ||
|
f4731c0514 | ||
|
34f16e5b7d | ||
|
83e9af6601 | ||
|
8bbf9335e1 | ||
|
159f30b37f | ||
|
2e3dc75425 | ||
|
7d3575b362 | ||
|
35d407021c | ||
|
076f49d447 | ||
|
0665fe0413 | ||
|
669a6fee40 | ||
|
8aab0fc189 | ||
|
5d6eb5bfd6 | ||
|
cf4711d878 | ||
|
21d664d670 | ||
|
ab182e276b | ||
|
96a3250152 | ||
|
f5746002fd | ||
|
e1e3339770 | ||
|
3a5086796d | ||
|
11300913a4 | ||
|
e65f14cbed | ||
|
6898849e3e | ||
|
2d61691bb2 | ||
|
eba9e04e2e | ||
|
33f32de690 | ||
|
93b8f61551 | ||
|
7f17a9d1b4 | ||
|
d34e4cf698 | ||
|
6b592137b9 | ||
|
d5e72bf55d | ||
|
5677e5e133 | ||
|
7a11a06cbd | ||
|
a5862d4b9c | ||
|
a50909e806 | ||
|
7c8f7d3f20 | ||
|
9078197446 | ||
|
0fe07cf9fe | ||
|
2216169ca1 | ||
|
50e989ca85 | ||
|
fa1fc3d855 | ||
|
bbe696e925 | ||
|
5d12f523a3 | ||
|
d295d20dc4 | ||
|
2ba10071c9 | ||
|
505dc0491b | ||
|
54a4ab0f26 | ||
|
e03e91477b | ||
|
88ac397158 | ||
|
6fd4be580b | ||
|
53348feb89 | ||
|
337cdbb37c | ||
|
05fdf91fc5 | ||
|
c387689d1c | ||
|
cb9238dc4e | ||
|
a484811f78 | ||
|
111d1934c4 | ||
|
972fb1a29d | ||
|
3a6af27586 | ||
|
c89ac341e4 | ||
|
cd59e5d07b | ||
|
0b940e4b2b | ||
|
b29375c844 | ||
|
76d3f6d248 | ||
|
e87a85a179 | ||
|
11407bf656 | ||
|
c82fb3c9b9 | ||
|
309e1d8619 | ||
|
c2db67c1c0 | ||
|
9526594905 | ||
|
3d74d277aa | ||
|
fc274c2ba4 | ||
|
ce43ea9f42 | ||
|
21da02fac2 | ||
|
19569bd5c5 | ||
|
afa25d8c57 | ||
|
1ba7acf4bd | ||
|
a847fe8754 | ||
|
5bb18b6441 | ||
|
876c233a26 | ||
|
ee5aeb80a4 | ||
|
02ceae15a2 | ||
|
e514739280 | ||
|
72265298f9 | ||
|
4b700192c1 | ||
|
fe83589ade | ||
|
fcf63c74f1 | ||
|
c95bb109c8 | ||
|
bd9c46ee34 | ||
|
736aeaa1d3 | ||
|
dd1f26522c | ||
|
712b7b2188 | ||
|
5b749e2d5c | ||
|
d85a69a709 | ||
|
a425e96fb2 | ||
|
7763fdf6ba | ||
|
dd156b59fc | ||
|
36dceecd58 | ||
|
421b9b271a | ||
|
ed57dcb924 | ||
|
95c77bfb98 | ||
|
2e3e721344 | ||
|
da2c28d5c2 | ||
|
dbddee9de9 | ||
|
8731d75607 | ||
|
f2ce233a6d | ||
|
6a75e30941 | ||
|
a3244c4892 | ||
|
a5ad8fd3bd | ||
|
deccdb1ec5 | ||
|
12a43b5e62 | ||
|
e1291aa6d2 | ||
|
bb26f32ac7 | ||
|
4d928001b8 | ||
|
c4baa6a10c | ||
|
71dec3dc5e | ||
|
e5017c0431 | ||
|
cbb5134874 | ||
|
ff248d566d | ||
|
6ccc12c332 | ||
|
2a669e9a17 | ||
|
5130abe76f | ||
|
fa7c8977a8 | ||
|
24fa183297 | ||
|
131aa5dd15 | ||
|
a06ccc928f | ||
|
d09ad13208 | ||
|
8ac37d5927 | ||
|
7ef0e50507 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
bin
|
||||
src/fzf/fzf-*
|
||||
gopath
|
||||
bin/fzf
|
||||
target
|
||||
pkg
|
||||
Gemfile.lock
|
||||
.DS_Store
|
||||
doc/tags
|
||||
vendor
|
||||
gopath
|
||||
|
11
.travis.yml
11
.travis.yml
@@ -16,13 +16,6 @@ install:
|
||||
- sudo apt-get install -y zsh fish
|
||||
|
||||
script: |
|
||||
export GOPATH=~/go
|
||||
export FZF_BASE=$GOPATH/src/github.com/junegunn/fzf
|
||||
|
||||
mkdir -p $GOPATH/src/github.com/junegunn
|
||||
ln -s $(pwd) $FZF_BASE
|
||||
|
||||
cd $FZF_BASE/src && make test fzf/fzf-linux_amd64 install &&
|
||||
cd $FZF_BASE/bin && ln -sf fzf-linux_amd64 fzf-$(./fzf --version)-linux_amd64 &&
|
||||
cd $FZF_BASE && yes | ./install && rm -f fzf &&
|
||||
make test install &&
|
||||
./install --all &&
|
||||
tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ]
|
||||
|
65
BUILD.md
65
BUILD.md
@@ -13,77 +13,31 @@ Build instructions
|
||||
Makefile will set up and use its own `$GOPATH` under the project root.
|
||||
|
||||
```sh
|
||||
# Source files are located in src directory
|
||||
cd src
|
||||
|
||||
# Build fzf binary for your platform in src/fzf
|
||||
# Build fzf binary for your platform in target
|
||||
make
|
||||
|
||||
# Build fzf binary and copy it to bin directory
|
||||
make install
|
||||
|
||||
# Build 32-bit and 64-bit executables and tarballs
|
||||
# Build 32-bit and 64-bit executables and tarballs in target
|
||||
make release
|
||||
|
||||
# Build executables and tarballs for Linux using Docker
|
||||
make linux
|
||||
# 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
|
||||
cloning the repository.
|
||||
manually cloning the repository.
|
||||
|
||||
```sh
|
||||
go get -u github.com/junegunn/fzf/src/fzf
|
||||
go get -u github.com/junegunn/fzf
|
||||
```
|
||||
|
||||
Build options
|
||||
-------------
|
||||
|
||||
### With ncurses 6
|
||||
|
||||
The official binaries of fzf are built with ncurses 5 because it's widely
|
||||
supported by different platforms. However ncurses 5 is old and has a number of
|
||||
limitations.
|
||||
|
||||
1. Does not support more than 256 color pairs (See [357][357])
|
||||
2. Does not support italics
|
||||
3. Does not support 24-bit color
|
||||
|
||||
[357]: https://github.com/junegunn/fzf/issues/357
|
||||
|
||||
But you can manually build fzf with ncurses 6 to overcome some of these
|
||||
limitations. ncurses 6 supports up to 32767 color pairs (1), and supports
|
||||
italics (2). To build fzf with ncurses 6, you have to install it first. On
|
||||
macOS, you can use Homebrew to install it.
|
||||
|
||||
```sh
|
||||
brew install homebrew/dupes/ncurses
|
||||
LDFLAGS="-L/usr/local/opt/ncurses/lib" make install
|
||||
```
|
||||
|
||||
### With tcell
|
||||
|
||||
[tcell][tcell] is a portable alternative to ncurses and we currently use it to
|
||||
build Windows binaries. tcell has many benefits but most importantly, it
|
||||
supports 24-bit colors. To build fzf with tcell:
|
||||
|
||||
```sh
|
||||
TAGS=tcell make install
|
||||
```
|
||||
|
||||
However, note that tcell has its own issues.
|
||||
|
||||
- Poor rendering performance compared to ncurses
|
||||
- Does not support bracketed-paste mode
|
||||
- Does not support italics unlike ncurses 6
|
||||
- Some wide characters are not correctly displayed
|
||||
|
||||
Third-party libraries used
|
||||
--------------------------
|
||||
|
||||
- [ncurses][ncurses]
|
||||
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
|
||||
@@ -97,10 +51,3 @@ License
|
||||
-------
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
[install]: https://github.com/junegunn/fzf#installation
|
||||
[go]: https://golang.org/
|
||||
[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock
|
||||
[ncurses]: https://www.gnu.org/software/ncurses/
|
||||
[req]: http://golang.org/doc/install
|
||||
[tcell]: https://github.com/gdamore/tcell
|
||||
|
79
CHANGELOG.md
79
CHANGELOG.md
@@ -1,6 +1,85 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
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
|
||||
|
138
Makefile
Normal file
138
Makefile
Normal file
@@ -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
|
155
README-VIM.md
Normal file
155
README-VIM.md
Normal file
@@ -0,0 +1,155 @@
|
||||
FZF Vim integration
|
||||
===================
|
||||
|
||||
This repository only enables basic integration with Vim. If you're looking for
|
||||
more, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project.
|
||||
|
||||
(Note: To use fzf in GVim, an external terminal emulator is required.)
|
||||
|
||||
`:FZF[!]`
|
||||
---------
|
||||
|
||||
If you have set up fzf for Vim, `:FZF` command will be added.
|
||||
|
||||
```vim
|
||||
" Look for files under current directory
|
||||
:FZF
|
||||
|
||||
" Look for files under your home directory
|
||||
:FZF ~
|
||||
|
||||
" With options
|
||||
:FZF --no-sort --reverse --inline-info /tmp
|
||||
|
||||
" Bang version starts fzf in fullscreen mode
|
||||
:FZF!
|
||||
```
|
||||
|
||||
Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key,
|
||||
`CTRL-T`, `CTRL-X` or `CTRL-V` to open selected files in the current window,
|
||||
in new tabs, in horizontal splits, or in vertical splits respectively.
|
||||
|
||||
Note that the environment variables `FZF_DEFAULT_COMMAND` and
|
||||
`FZF_DEFAULT_OPTS` also apply here.
|
||||
|
||||
### Configuration
|
||||
|
||||
- `g:fzf_action`
|
||||
- Customizable extra key bindings for opening selected files in different ways
|
||||
- `g:fzf_layout`
|
||||
- Determines the size and position of fzf window (tmux pane or Neovim split)
|
||||
- `g:fzf_colors`
|
||||
- Customizes fzf colors to match the current color scheme
|
||||
- `g:fzf_history_dir`
|
||||
- Enables history feature
|
||||
- `g:fzf_launcher`
|
||||
- (Only in GVim) Terminal emulator to open fzf with
|
||||
- `g:Fzf_launcher` for function reference
|
||||
|
||||
#### Examples
|
||||
|
||||
```vim
|
||||
" This is the default extra key bindings
|
||||
let g:fzf_action = {
|
||||
\ 'ctrl-t': 'tab split',
|
||||
\ 'ctrl-x': 'split',
|
||||
\ 'ctrl-v': 'vsplit' }
|
||||
|
||||
" Default fzf layout
|
||||
" - down / up / left / right
|
||||
let g:fzf_layout = { 'down': '~40%' }
|
||||
|
||||
" In Neovim, you can set up fzf window using a Vim command
|
||||
let g:fzf_layout = { 'window': 'enew' }
|
||||
let g:fzf_layout = { 'window': '-tabnew' }
|
||||
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'],
|
||||
\ '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` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) |
|
||||
| `launcher` | string | External terminal emulator to start fzf with (GVim only) |
|
||||
| `launcher` | funcref | Function for generating `launcher` string (GVim only) |
|
||||
|
||||
`options` entry can be either a string or a list. For simple cases, string
|
||||
should suffice, but prefer to use list type if you're concerned about escaping
|
||||
issues on different platforms.
|
||||
|
||||
```vim
|
||||
call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'})
|
||||
call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']})
|
||||
```
|
||||
|
||||
`fzf#wrap`
|
||||
----------
|
||||
|
||||
`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'}, <bang>0))
|
||||
```
|
||||
|
||||
GVim
|
||||
----
|
||||
|
||||
In GVim, you need an external terminal emulator to start fzf with. `xterm`
|
||||
command is used by default, but you can customize it with `g:fzf_launcher`.
|
||||
|
||||
```vim
|
||||
" This is the default. %s is replaced with fzf command
|
||||
let g:fzf_launcher = 'xterm -e bash -ic %s'
|
||||
|
||||
" Use urxvt instead
|
||||
let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s'
|
||||
```
|
||||
|
||||
If you're running MacVim on OSX, I recommend you to use iTerm2 as the
|
||||
launcher. Refer to the [this wiki page][macvim-iterm2] to see how to set up.
|
||||
|
||||
[macvim-iterm2]: https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2
|
||||
|
||||
[License](LICENSE)
|
||||
------------------
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Junegunn Choi
|
174
README.md
174
README.md
@@ -1,4 +1,4 @@
|
||||
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf.png" height="170" alt="fzf - a command-line fuzzy finder"> [](https://travis-ci.org/junegunn/fzf)
|
||||
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf.png" height="170" alt="fzf - a command-line fuzzy finder"> [](https://travis-ci.org/junegunn/fzf) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=EKYAW9PGKPD2N)
|
||||
===
|
||||
|
||||
fzf is a general-purpose command-line fuzzy finder.
|
||||
@@ -15,6 +15,39 @@ Pros
|
||||
- Batteries included
|
||||
- Vim/Neovim plugin, key bindings and fuzzy auto-completion
|
||||
|
||||
Table of Contents
|
||||
-----------------
|
||||
|
||||
* [Installation](#installation)
|
||||
* [Using git](#using-git)
|
||||
* [Using Homebrew](#using-homebrew)
|
||||
* [As Vim plugin](#as-vim-plugin)
|
||||
* [Windows](#windows)
|
||||
* [Upgrading fzf](#upgrading-fzf)
|
||||
* [Building fzf](#building-fzf)
|
||||
* [Usage](#usage)
|
||||
* [Using the finder](#using-the-finder)
|
||||
* [Layout](#layout)
|
||||
* [Search syntax](#search-syntax)
|
||||
* [Environment variables](#environment-variables)
|
||||
* [Options](#options)
|
||||
* [Examples](#examples)
|
||||
* [fzf-tmux script](#fzf-tmux-script)
|
||||
* [Key bindings for command line](#key-bindings-for-command-line)
|
||||
* [Fuzzy completion for bash and zsh](#fuzzy-completion-for-bash-and-zsh)
|
||||
* [Files and directories](#files-and-directories)
|
||||
* [Process IDs](#process-ids)
|
||||
* [Host names](#host-names)
|
||||
* [Environment variables / Aliases](#environment-variables--aliases)
|
||||
* [Settings](#settings)
|
||||
* [Supported commands](#supported-commands)
|
||||
* [Vim plugin](#vim-plugin)
|
||||
* [Tips](#tips)
|
||||
* [Respecting .gitignore, <code>.hgignore</code>, and <code>svn:ignore</code>](#respecting-gitignore-hgignore-and-svnignore)
|
||||
* [git ls-tree for fast traversal](#git-ls-tree-for-fast-traversal)
|
||||
* [Fish shell](#fish-shell)
|
||||
* [<a href="LICENSE">License</a>](#license)
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
@@ -53,7 +86,7 @@ brew install fzf
|
||||
/usr/local/opt/fzf/install
|
||||
```
|
||||
|
||||
### Vim plugin
|
||||
### As Vim plugin
|
||||
|
||||
You can manually add the directory to `&runtimepath` as follows,
|
||||
|
||||
@@ -72,7 +105,25 @@ But it's recommended that you use a plugin manager like
|
||||
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': './install --all' }
|
||||
```
|
||||
|
||||
### Upgrading fzf
|
||||
### 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. You might
|
||||
want to consider installing fzf on [Windows Subsystem for Linux][wsl] where
|
||||
everything runs flawlessly.
|
||||
|
||||
[wsl]: https://blogs.msdn.microsoft.com/wsl/
|
||||
|
||||
Upgrading fzf
|
||||
-------------
|
||||
|
||||
fzf is being actively developed and you might want to upgrade it once in a
|
||||
while. Please follow the instruction below depending on the installation
|
||||
@@ -80,17 +131,9 @@ method used.
|
||||
|
||||
- git: `cd ~/.fzf && git pull && ./install`
|
||||
- brew: `brew update; brew reinstall fzf`
|
||||
- chocolatey: `choco upgrade fzf`
|
||||
- vim-plug: `:PlugUpdate fzf`
|
||||
|
||||
### Windows
|
||||
|
||||
Pre-built binaries for Windows can be downloaded [here][bin]. However, other
|
||||
components of the project may not work on Windows. You might want to consider
|
||||
installing fzf on [Windows Subsystem for Linux][wsl] where everything runs
|
||||
flawlessly.
|
||||
|
||||
[wsl]: https://blogs.msdn.microsoft.com/wsl/
|
||||
|
||||
Building fzf
|
||||
------------
|
||||
|
||||
@@ -99,7 +142,7 @@ See [BUILD.md](BUILD.md).
|
||||
Usage
|
||||
-----
|
||||
|
||||
fzf will launch curses-based finder, read the list from STDIN, and write the
|
||||
fzf will launch interactive finder, read the list from STDIN, and write the
|
||||
selected item to STDOUT.
|
||||
|
||||
```sh
|
||||
@@ -140,10 +183,10 @@ vim $(fzf --height 40% --reverse)
|
||||
```
|
||||
|
||||
You can add these options to `$FZF_DEFAULT_OPTS` so that they're applied by
|
||||
default.
|
||||
default. For example,
|
||||
|
||||
```sh
|
||||
export FZF_DEFAULT_OPTS='--height 40% --reverse'
|
||||
export FZF_DEFAULT_OPTS='--height 40% --reverse --border'
|
||||
```
|
||||
|
||||
#### Search syntax
|
||||
@@ -229,8 +272,8 @@ fish.
|
||||
- Set `FZF_CTRL_T_COMMAND` to override the default command
|
||||
- Set `FZF_CTRL_T_OPTS` to pass additional options
|
||||
- `CTRL-R` - Paste the selected command from history onto the command line
|
||||
- Sort is disabled by default to respect chronological ordering
|
||||
- Press `CTRL-R` again to toggle sort
|
||||
- If you want to see the commands in chronological order, press `CTRL-R`
|
||||
again which toggles sorting by relevance
|
||||
- Set `FZF_CTRL_R_OPTS` to pass additional options
|
||||
- `ALT-C` - cd into the selected directory
|
||||
- Set `FZF_ALT_C_COMMAND` to override the default command
|
||||
@@ -331,97 +374,18 @@ On bash, fuzzy completion is enabled only for a predefined set of commands
|
||||
commands as well like follows.
|
||||
|
||||
```sh
|
||||
# There are also _fzf_path_completion and _fzf_dir_completion
|
||||
complete -F _fzf_file_completion -o default -o bashdefault doge
|
||||
complete -F _fzf_path_completion -o default -o bashdefault ag
|
||||
complete -F _fzf_dir_completion -o default -o bashdefault tree
|
||||
```
|
||||
|
||||
Usage as Vim plugin
|
||||
-------------------
|
||||
Vim plugin
|
||||
----------
|
||||
|
||||
This repository only enables basic integration with Vim. If you're looking for
|
||||
more, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project.
|
||||
|
||||
(Note: To use fzf in GVim, an external terminal emulator is required.)
|
||||
|
||||
#### `:FZF[!]`
|
||||
|
||||
If you have set up fzf for Vim, `:FZF` command will be added.
|
||||
|
||||
```vim
|
||||
" Look for files under current directory
|
||||
:FZF
|
||||
|
||||
" Look for files under your home directory
|
||||
:FZF ~
|
||||
|
||||
" With options
|
||||
:FZF --no-sort --reverse --inline-info /tmp
|
||||
|
||||
" Bang version starts in fullscreen instead of using tmux pane or Neovim split
|
||||
:FZF!
|
||||
```
|
||||
|
||||
Similarly to [ctrlp.vim](https://github.com/kien/ctrlp.vim), use enter key,
|
||||
`CTRL-T`, `CTRL-X` or `CTRL-V` to open selected files in the current window,
|
||||
in new tabs, in horizontal splits, or in vertical splits respectively.
|
||||
|
||||
Note that the environment variables `FZF_DEFAULT_COMMAND` and
|
||||
`FZF_DEFAULT_OPTS` also apply here. Refer to [the wiki page][fzf-config] for
|
||||
customization.
|
||||
|
||||
[fzf-config]: https://github.com/junegunn/fzf/wiki/Configuring-Vim-plugin
|
||||
|
||||
#### `fzf#run`
|
||||
|
||||
For more advanced uses, you can use `fzf#run([options])` function with the
|
||||
following options.
|
||||
|
||||
| Option name | Type | Description |
|
||||
| -------------------------- | ------------- | ---------------------------------------------------------------- |
|
||||
| `source` | string | External command to generate input to fzf (e.g. `find .`) |
|
||||
| `source` | list | Vim list as input to fzf |
|
||||
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
|
||||
| `sink` | funcref | Reference to function to process each selected item |
|
||||
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
|
||||
| `options` | string | Options to fzf |
|
||||
| `dir` | string | Working directory |
|
||||
| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) |
|
||||
| `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) |
|
||||
| `launcher` | string | External terminal emulator to start fzf with (GVim only) |
|
||||
| `launcher` | funcref | Function for generating `launcher` string (GVim only) |
|
||||
|
||||
Examples can be found on [the wiki
|
||||
page](https://github.com/junegunn/fzf/wiki/Examples-(vim)).
|
||||
|
||||
#### `fzf#wrap`
|
||||
|
||||
`fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper
|
||||
function that decorates the options dictionary so that it understands
|
||||
`g:fzf_layout`, `g:fzf_action`, `g:fzf_colors`, and `g:fzf_history_dir` like
|
||||
`:FZF`.
|
||||
|
||||
```vim
|
||||
command! -bang MyStuff
|
||||
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0))
|
||||
```
|
||||
See [README-VIM.md](README-VIM.md).
|
||||
|
||||
Tips
|
||||
----
|
||||
|
||||
#### Rendering issues
|
||||
|
||||
If you have any rendering issues, check the following:
|
||||
|
||||
1. Make sure `$TERM` is correctly set. fzf will use 256-color only if it
|
||||
contains `256` (e.g. `xterm-256color`)
|
||||
2. If you're on screen or tmux, `$TERM` should be either `screen` or
|
||||
`screen-256color`
|
||||
3. Some terminal emulators (e.g. mintty) have problem displaying default
|
||||
background color and make some text unable to read. In that case, try
|
||||
`--black` option. And if it solves your problem, I recommend including it
|
||||
in `FZF_DEFAULT_OPTS` for further convenience.
|
||||
4. If you still have problem, try `--no-256` option or even `--no-color`.
|
||||
|
||||
#### Respecting `.gitignore`, `.hgignore`, and `svn:ignore`
|
||||
|
||||
[ag](https://github.com/ggreer/the_silver_searcher) or
|
||||
@@ -463,9 +427,9 @@ export FZF_DEFAULT_COMMAND='
|
||||
#### Fish shell
|
||||
|
||||
It's [a known bug of fish](https://github.com/fish-shell/fish-shell/issues/1362)
|
||||
that it doesn't allow reading from STDIN in command substitution, which means
|
||||
simple `vim (fzf)` won't work as expected. The workaround is to use the `read`
|
||||
fish command:
|
||||
(will be fixed in 2.6.0) that it doesn't allow reading from STDIN in command
|
||||
substitution, which means simple `vim (fzf)` won't work as expected. The
|
||||
workaround is to use the `read` fish command:
|
||||
|
||||
```sh
|
||||
fzf | read -l result; and vim $result
|
||||
@@ -493,7 +457,7 @@ make use of this feature. `$dir` defaults to `.` when the last token is not a
|
||||
valid directory. Example:
|
||||
|
||||
```sh
|
||||
set -l FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'"
|
||||
set -g FZF_CTRL_T_COMMAND "command find -L \$dir -type f 2> /dev/null | sed '1d; s#^\./##'"
|
||||
```
|
||||
|
||||
[License](LICENSE)
|
||||
|
20
bin/fzf-tmux
20
bin/fzf-tmux
@@ -121,7 +121,7 @@ args+=("--no-height")
|
||||
if tmux list-panes -F '#F' | grep -q Z; then
|
||||
zoomed=1
|
||||
original_window=$(tmux display-message -p "#{window_id}")
|
||||
tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - \\\\; do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'")
|
||||
tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'")
|
||||
tmux swap-pane -t $tmp_window \; select-window -t $tmp_window
|
||||
fi
|
||||
|
||||
@@ -134,17 +134,23 @@ fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
|
||||
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
|
||||
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
|
||||
cleanup() {
|
||||
rm -f $argsf $fifo1 $fifo2 $fifo3
|
||||
\rm -f $argsf $fifo1 $fifo2 $fifo3
|
||||
|
||||
# Remove temp window if we were zoomed
|
||||
if [[ -n "$zoomed" ]]; then
|
||||
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
|
||||
exit 130
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT SIGINT SIGTERM
|
||||
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")"
|
||||
@@ -163,18 +169,22 @@ for arg in "${args[@]}"; do
|
||||
opts="$opts \"$arg\""
|
||||
done
|
||||
|
||||
pppid=$$
|
||||
trap_set="trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM"
|
||||
trap_unset="trap - EXIT SIGINT SIGTERM"
|
||||
|
||||
if [[ -n "$term" ]] || [[ -t 0 ]]; then
|
||||
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf
|
||||
TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\
|
||||
set-window-option remain-on-exit off \;\
|
||||
split-window $opt "cd $(printf %q "$PWD");$envs bash $argsf" $swap \
|
||||
split-window $opt "$trap_set;cd $(printf %q "$PWD");$envs bash $argsf;$trap_unset" $swap \
|
||||
> /dev/null 2>&1
|
||||
else
|
||||
mkfifo $fifo1
|
||||
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf
|
||||
TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\
|
||||
set-window-option remain-on-exit off \;\
|
||||
split-window $opt "$envs bash $argsf" $swap \
|
||||
split-window $opt "$trap_set;$envs bash $argsf;$trap_unset" $swap \
|
||||
> /dev/null 2>&1
|
||||
cat <&0 > $fifo1 &
|
||||
fi
|
||||
|
182
doc/fzf.txt
Normal file
182
doc/fzf.txt
Normal file
@@ -0,0 +1,182 @@
|
||||
fzf.txt fzf Last change: April 28 2017
|
||||
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
|
||||
==============================================================================
|
||||
|
||||
FZF Vim integration
|
||||
:FZF[!]
|
||||
Configuration
|
||||
Examples
|
||||
fzf#run
|
||||
fzf#wrap
|
||||
GVim
|
||||
License
|
||||
|
||||
FZF VIM INTEGRATION *fzf-vim-integration*
|
||||
==============================================================================
|
||||
|
||||
This repository only enables basic integration with Vim. If you're looking for
|
||||
more, check out {fzf.vim}{1} project.
|
||||
|
||||
(Note: To use fzf in GVim, an external terminal emulator is required.)
|
||||
|
||||
{1} https://github.com/junegunn/fzf.vim
|
||||
|
||||
|
||||
:FZF[!]
|
||||
==============================================================================
|
||||
|
||||
*:FZF*
|
||||
|
||||
If you have set up fzf for Vim, `:FZF` command will be added.
|
||||
>
|
||||
" Look for files under current directory
|
||||
:FZF
|
||||
|
||||
" Look for files under your home directory
|
||||
:FZF ~
|
||||
|
||||
" With options
|
||||
:FZF --no-sort --reverse --inline-info /tmp
|
||||
|
||||
" Bang version starts fzf in fullscreen mode
|
||||
:FZF!
|
||||
<
|
||||
Similarly to {ctrlp.vim}{2}, use enter key, CTRL-T, CTRL-X or CTRL-V to open
|
||||
selected files in the current window, in new tabs, in horizontal splits, or in
|
||||
vertical splits respectively.
|
||||
|
||||
Note that the environment variables `FZF_DEFAULT_COMMAND` and
|
||||
`FZF_DEFAULT_OPTS` also apply here.
|
||||
|
||||
{2} https://github.com/kien/ctrlp.vim
|
||||
|
||||
|
||||
< Configuration >_____________________________________________________________~
|
||||
*fzf-configuration*
|
||||
|
||||
*g:fzf_action* *g:fzf_layout* *g:fzf_colors* *g:fzf_history_dir* *g:fzf_launcher*
|
||||
*g:Fzf_launcher*
|
||||
|
||||
- `g:fzf_action`
|
||||
- Customizable extra key bindings for opening selected files in different
|
||||
ways
|
||||
- `g:fzf_layout`
|
||||
- Determines the size and position of fzf window (tmux pane or Neovim split)
|
||||
- `g:fzf_colors`
|
||||
- Customizes fzf colors to match the current color scheme
|
||||
- `g:fzf_history_dir`
|
||||
- Enables history feature
|
||||
- `g:fzf_launcher`
|
||||
- (Only in GVim) Terminal emulator to open fzf with
|
||||
- `g:Fzf_launcher` for function reference
|
||||
|
||||
|
||||
Examples~
|
||||
*fzf-examples*
|
||||
>
|
||||
" This is the default extra key bindings
|
||||
let g:fzf_action = {
|
||||
\ 'ctrl-t': 'tab split',
|
||||
\ 'ctrl-x': 'split',
|
||||
\ 'ctrl-v': 'vsplit' }
|
||||
|
||||
" Default fzf layout
|
||||
" - down / up / left / right
|
||||
let g:fzf_layout = { 'down': '~40%' }
|
||||
|
||||
" In Neovim, you can set up fzf window using a Vim command
|
||||
let g:fzf_layout = { 'window': 'enew' }
|
||||
let g:fzf_layout = { 'window': '-tabnew' }
|
||||
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'],
|
||||
\ '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` (Neovim only) | string | Command to open fzf window (e.g. `vertical aboveleft 30new` )
|
||||
`launcher` | string | External terminal emulator to start fzf with (GVim only)
|
||||
`launcher` | funcref | Function for generating `launcher` string (GVim only)
|
||||
---------------------------+---------------+--------------------------------------------------------------
|
||||
|
||||
`options` entry can be either a string or a list. For simple cases, string
|
||||
should suffice, but prefer to use list type if you're concerned about escaping
|
||||
issues on different platforms.
|
||||
>
|
||||
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'}, <bang>0))
|
||||
<
|
||||
|
||||
GVIM *fzf-gvim*
|
||||
==============================================================================
|
||||
|
||||
In GVim, you need an external terminal emulator to start fzf with. `xterm`
|
||||
command is used by default, but you can customize it with `g:fzf_launcher`.
|
||||
>
|
||||
" This is the default. %s is replaced with fzf command
|
||||
let g:fzf_launcher = 'xterm -e bash -ic %s'
|
||||
|
||||
" Use urxvt instead
|
||||
let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s'
|
||||
<
|
||||
If you're running MacVim on OSX, I recommend you to use iTerm2 as the
|
||||
launcher. Refer to the {this wiki page}{3} to see how to set up.
|
||||
|
||||
{3} https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2
|
||||
|
||||
|
||||
LICENSE *fzf-license*
|
||||
==============================================================================
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Junegunn Choi
|
||||
|
||||
==============================================================================
|
||||
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:
|
38
glide.lock
generated
Normal file
38
glide.lock
generated
Normal file
@@ -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: []
|
16
glide.yaml
Normal file
16
glide.yaml
Normal file
@@ -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
|
165
install
165
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.16.1
|
||||
version=0.16.10
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -72,7 +72,7 @@ ask() {
|
||||
check_binary() {
|
||||
echo -n " - Checking fzf executable ... "
|
||||
local output
|
||||
output=$("$fzf_base"/bin/fzf --version 2>&1)
|
||||
output=$("$fzf_base"/bin/fzf --version 2>&1 | awk '{print $1}')
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: $output"
|
||||
binary_error="Invalid binary"
|
||||
@@ -88,17 +88,6 @@ check_binary() {
|
||||
return 1
|
||||
}
|
||||
|
||||
symlink() {
|
||||
echo " - Creating symlink: bin/$1 -> bin/fzf"
|
||||
(cd "$fzf_base"/bin &&
|
||||
rm -f fzf &&
|
||||
ln -sf $1 fzf)
|
||||
if [ $? -ne 0 ]; then
|
||||
binary_error="Failed to create symlink"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
link_fzf_in_path() {
|
||||
if which_fzf="$(command -v fzf)"; then
|
||||
echo " - Found in \$PATH"
|
||||
@@ -110,11 +99,23 @@ link_fzf_in_path() {
|
||||
}
|
||||
|
||||
try_curl() {
|
||||
command -v curl > /dev/null && curl -fL $1 | tar -xz
|
||||
command -v curl > /dev/null &&
|
||||
if [[ $1 =~ tgz$ ]]; then
|
||||
curl -fL $1 | tar -xzf -
|
||||
else
|
||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||
fi
|
||||
}
|
||||
|
||||
try_wget() {
|
||||
command -v wget > /dev/null && wget -O - $1 | tar -xz
|
||||
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() {
|
||||
@@ -124,9 +125,6 @@ download() {
|
||||
echo " - Already exists"
|
||||
check_binary && return
|
||||
fi
|
||||
if [ -x "$fzf_base"/bin/$1 ]; then
|
||||
symlink $1 && check_binary && return
|
||||
fi
|
||||
link_fzf_in_path && return
|
||||
fi
|
||||
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
|
||||
@@ -137,8 +135,8 @@ download() {
|
||||
|
||||
local url
|
||||
[[ "$version" =~ alpha ]] &&
|
||||
url=https://github.com/junegunn/fzf-bin/releases/download/alpha/${1}.tgz ||
|
||||
url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}.tgz
|
||||
url=https://github.com/junegunn/fzf-bin/releases/download/alpha/${1} ||
|
||||
url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}
|
||||
set -o pipefail
|
||||
if ! (try_curl $url || try_wget $url); then
|
||||
set +o pipefail
|
||||
@@ -147,12 +145,12 @@ download() {
|
||||
fi
|
||||
set +o pipefail
|
||||
|
||||
if [ ! -f $1 ]; then
|
||||
if [ ! -f fzf ]; then
|
||||
binary_error="Failed to download ${1}"
|
||||
return
|
||||
fi
|
||||
|
||||
chmod +x $1 && symlink $1 && check_binary
|
||||
chmod +x fzf && check_binary
|
||||
}
|
||||
|
||||
# Try to download binary executable
|
||||
@@ -160,84 +158,22 @@ archi=$(uname -sm)
|
||||
binary_available=1
|
||||
binary_error=""
|
||||
case "$archi" in
|
||||
Darwin\ x86_64) download fzf-$version-darwin_${binary_arch:-amd64} ;;
|
||||
Darwin\ i*86) download fzf-$version-darwin_${binary_arch:-386} ;;
|
||||
Linux\ x86_64) download fzf-$version-linux_${binary_arch:-amd64} ;;
|
||||
Linux\ i*86) download fzf-$version-linux_${binary_arch:-386} ;;
|
||||
*) 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\ *64) download fzf-$version-linux_${binary_arch:-amd64}.tgz ;;
|
||||
Linux\ *86) download fzf-$version-linux_${binary_arch:-386}.tgz ;;
|
||||
Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5}.tgz ;;
|
||||
Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6}.tgz ;;
|
||||
Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7}.tgz ;;
|
||||
Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;;
|
||||
FreeBSD\ *64) download fzf-$version-freebsd_${binary_arch:-amd64}.tgz ;;
|
||||
FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386}.tgz ;;
|
||||
OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64}.tgz ;;
|
||||
OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386}.tgz ;;
|
||||
CYGWIN*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
|
||||
*) binary_available=0 binary_error=1 ;;
|
||||
esac
|
||||
|
||||
install_ruby_fzf() {
|
||||
if [ -z "$allow_legacy" ]; then
|
||||
ask "Do you want to install legacy Ruby version instead?" && exit 1
|
||||
fi
|
||||
echo "Installing legacy Ruby version ..."
|
||||
|
||||
# ruby executable
|
||||
echo -n "Checking Ruby executable ... "
|
||||
ruby=$(command -v ruby)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ruby executable not found !!!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# System ruby is preferred
|
||||
system_ruby=/usr/bin/ruby
|
||||
if [ -x $system_ruby ] && [ $system_ruby != "$ruby" ]; then
|
||||
$system_ruby --disable-gems -rcurses -e0 2> /dev/null
|
||||
[ $? -eq 0 ] && ruby=$system_ruby
|
||||
fi
|
||||
|
||||
echo "OK ($ruby)"
|
||||
|
||||
# Curses-support
|
||||
echo -n "Checking Curses support ... "
|
||||
"$ruby" -rcurses -e0 2> /dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "OK"
|
||||
else
|
||||
echo "Not found"
|
||||
echo "Installing 'curses' gem ... "
|
||||
if (( EUID )); then
|
||||
/usr/bin/env gem install curses --user-install
|
||||
else
|
||||
/usr/bin/env gem install curses
|
||||
fi
|
||||
if [ $? -ne 0 ]; then
|
||||
echo
|
||||
echo "Failed to install 'curses' gem."
|
||||
if [[ $(uname -r) =~ 'ARCH' ]]; then
|
||||
echo "Make sure that base-devel package group is installed."
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ruby version
|
||||
echo -n "Checking Ruby version ... "
|
||||
"$ruby" -e 'exit RUBY_VERSION >= "1.9"'
|
||||
if [ $? -eq 0 ]; then
|
||||
echo ">= 1.9"
|
||||
"$ruby" --disable-gems -rcurses -e0 2> /dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
fzf_cmd="$ruby --disable-gems $fzf_base/fzf"
|
||||
else
|
||||
fzf_cmd="$ruby $fzf_base/fzf"
|
||||
fi
|
||||
else
|
||||
echo "< 1.9"
|
||||
fzf_cmd="$ruby $fzf_base/fzf"
|
||||
fi
|
||||
|
||||
# Create fzf script
|
||||
echo -n "Creating wrapper script for fzf ... "
|
||||
rm -f "$fzf_base"/bin/fzf
|
||||
echo "#!/bin/sh" > "$fzf_base"/bin/fzf
|
||||
echo "$fzf_cmd \"\$@\"" >> "$fzf_base"/bin/fzf
|
||||
chmod +x "$fzf_base"/bin/fzf
|
||||
echo "OK"
|
||||
}
|
||||
|
||||
cd "$fzf_base"
|
||||
if [ -n "$binary_error" ]; then
|
||||
if [ $binary_available -eq 0 ]; then
|
||||
@@ -246,21 +182,21 @@ if [ -n "$binary_error" ]; then
|
||||
echo " - $binary_error !!!"
|
||||
fi
|
||||
if command -v go > /dev/null; then
|
||||
echo -n "Building binary (go get -u github.com/junegunn/fzf/src/fzf) ... "
|
||||
echo -n "Building binary (go get -u github.com/junegunn/fzf) ... "
|
||||
if [ -z "${GOPATH-}" ]; then
|
||||
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
|
||||
mkdir -p "$GOPATH"
|
||||
fi
|
||||
if go get -u github.com/junegunn/fzf/src/fzf; then
|
||||
if go get -u github.com/junegunn/fzf; then
|
||||
echo "OK"
|
||||
cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
|
||||
else
|
||||
echo "Failed to build binary ..."
|
||||
install_ruby_fzf
|
||||
echo "Failed to build binary. Installation failed."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "go executable not found. Cannot build binary ..."
|
||||
install_ruby_fzf
|
||||
echo "go executable not found. Installation failed."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -374,6 +310,17 @@ append_line() {
|
||||
set +e
|
||||
}
|
||||
|
||||
create_file() {
|
||||
local file="$1"
|
||||
shift
|
||||
echo "Create $file:"
|
||||
for line in "$@"; do
|
||||
echo " $line"
|
||||
echo "$line" >> "$file"
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
if [ $update_config -eq 2 ]; then
|
||||
echo
|
||||
ask "Do you want to update your shell configuration files?"
|
||||
@@ -387,7 +334,14 @@ done
|
||||
|
||||
if [ $key_bindings -eq 1 ] && [ $has_fish -eq 1 ]; then
|
||||
bind_file=~/.config/fish/functions/fish_user_key_bindings.fish
|
||||
append_line $update_config "fzf_key_bindings" "$bind_file"
|
||||
if [ ! -e "$bind_file" ]; then
|
||||
create_file "$bind_file" \
|
||||
'function fish_user_key_bindings' \
|
||||
' fzf_key_bindings' \
|
||||
'end'
|
||||
else
|
||||
append_line $update_config "fzf_key_bindings" "$bind_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $update_config -eq 1 ]; then
|
||||
@@ -400,4 +354,3 @@ if [ $update_config -eq 1 ]; then
|
||||
echo
|
||||
fi
|
||||
echo 'For more information, see: https://github.com/junegunn/fzf'
|
||||
|
||||
|
@@ -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)
|
||||
}
|
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf-tmux 1 "Jan 2017" "fzf 0.16.1" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf-tmux 1 "Jul 2017" "fzf 0.16.10" "fzf-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf-tmux - open fzf in tmux split pane
|
||||
|
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf 1 "Jan 2017" "fzf 0.16.1" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Jul 2017" "fzf 0.16.10" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -156,6 +156,9 @@ Ignored when \fB--height\fR is not specified.
|
||||
.B "--reverse"
|
||||
Reverse orientation
|
||||
.TP
|
||||
.B "--border"
|
||||
Draw border above and below the finder
|
||||
.TP
|
||||
.BI "--margin=" MARGIN
|
||||
Comma-separated expression for margins around the finder.
|
||||
.br
|
||||
@@ -209,8 +212,7 @@ Number of spaces for a tab character (default: 8)
|
||||
Color configuration. The name of the base color scheme is followed by custom
|
||||
color mappings. Ansi color code of -1 denotes terminal default
|
||||
foreground/background color. You can also specify 24-bit color in \fB#rrggbb\fR
|
||||
format, but the support for 24-bit colors is experimental and only works when
|
||||
\fB--height\fR option is used.
|
||||
format.
|
||||
|
||||
.RS
|
||||
e.g. \fBfzf --color=bg+:24\fR
|
||||
@@ -234,6 +236,7 @@ e.g. \fBfzf --color=bg+:24\fR
|
||||
\fBbg+ \fRBackground (current line)
|
||||
\fBhl+ \fRHighlighted substrings (current line)
|
||||
\fBinfo \fRInfo
|
||||
\fBborder \fRBorder of the preview window and horizontal separators (\fB--border\fR)
|
||||
\fBprompt \fRPrompt
|
||||
\fBpointer \fRPointer to the current line
|
||||
\fBmarker \fRMulti-select marker
|
||||
@@ -263,13 +266,21 @@ Execute the given command for the current line and display the result on the
|
||||
preview window. \fB{}\fR in the command is the placeholder that is replaced to
|
||||
the single-quoted string of the current line. To transform the replacement
|
||||
string, specify field index expressions between the braces (See \fBFIELD INDEX
|
||||
EXPRESSION\fR for the details). Also, \fB{q}\fR is replaced to the current
|
||||
query string.
|
||||
EXPRESSION\fR for the details).
|
||||
|
||||
.RS
|
||||
e.g. \fBfzf --preview="head -$LINES {}"\fR
|
||||
\fBls -l | fzf --preview="echo user={3} when={-4..-2}; cat {-1}" --header-lines=1\fR
|
||||
|
||||
A placeholder expression starting with \fB+\fR flag will be replaced to the
|
||||
space-separated list of the selected lines (or the current line if no selection
|
||||
was made) individually quoted.
|
||||
|
||||
e.g. \fBfzf --multi --preview="head -10 {+}"\fR
|
||||
\fBgit log --oneline | fzf --multi --preview 'git show {+1}'\fR
|
||||
|
||||
Also, \fB{q}\fR is replaced to the current query string.
|
||||
|
||||
Note that you can escape a placeholder pattern by prepending a backslash.
|
||||
.RE
|
||||
.TP
|
||||
@@ -279,6 +290,9 @@ Determine the layout of the preview window. If the argument ends with
|
||||
\fBtoggle-preview\fR action is triggered. Long lines are truncated by default.
|
||||
Line wrap can be enabled with \fB:wrap\fR flag.
|
||||
|
||||
If size is given as 0, preview window will not be visible, but fzf will still
|
||||
execute the command in the background.
|
||||
|
||||
.RS
|
||||
.B POSITION: (default: right)
|
||||
\fBup
|
||||
@@ -321,10 +335,16 @@ e.g. \fBfzf --expect=ctrl-v,ctrl-t,alt-s,f1,f2,~,@\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "--read0"
|
||||
Read input delimited by ASCII NUL character instead of newline character
|
||||
Read input delimited by ASCII NUL characters instead of newline characters
|
||||
.TP
|
||||
.B "--print0"
|
||||
Print output delimited by ASCII NUL character instead of newline character
|
||||
Print output delimited by ASCII NUL characters instead of newline characters
|
||||
.TP
|
||||
.B "--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
|
||||
@@ -333,6 +353,9 @@ ncurses finder only after the input stream is complete.
|
||||
.RS
|
||||
e.g. \fBfzf --multi | fzf --sync\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "--version"
|
||||
Display version information and exit
|
||||
|
||||
.SH ENVIRONMENT VARIABLES
|
||||
.TP
|
||||
@@ -416,6 +439,8 @@ 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
|
||||
@@ -443,6 +468,11 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
|
||||
\fIdouble-click\fR
|
||||
or any single character
|
||||
|
||||
Additionally, a special event named \fIchange\fR is available which is
|
||||
triggered whenever the query string is changed.
|
||||
|
||||
e.g. \fBfzf --bind change:top\fR
|
||||
|
||||
\fBACTION: DEFAULT BINDINGS (NOTES):
|
||||
\fBabort\fR \fIctrl-c ctrl-g ctrl-q esc\fR
|
||||
\fBaccept\fR \fIenter double-click\fR
|
||||
@@ -459,7 +489,8 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
|
||||
\fBdown\fR \fIctrl-j ctrl-n down\fR
|
||||
\fBend-of-line\fR \fIctrl-e end\fR
|
||||
\fBexecute(...)\fR (see below for the details)
|
||||
\fBexecute-multi(...)\fR (see below for the details)
|
||||
\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
|
||||
@@ -481,17 +512,23 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
|
||||
\fBselect-all\fR
|
||||
\fBtoggle\fR
|
||||
\fBtoggle-all\fR
|
||||
\fBtoggle-down\fR \fIctrl-i (tab)\fR
|
||||
\fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle-up\fR : \fBtoggle-down\fR)
|
||||
\fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle-down\fR : \fBtoggle-up\fR)
|
||||
\fBtoggle+down\fR \fIctrl-i (tab)\fR
|
||||
\fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
|
||||
\fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
|
||||
\fBtoggle-preview\fR
|
||||
\fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR)
|
||||
\fBtoggle-up\fR \fIbtab (shift-tab)\fR
|
||||
\fBtoggle-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.
|
||||
@@ -524,10 +561,12 @@ the closing character. The catch is that it should be the last one in the
|
||||
comma-separated list of key-action pairs.
|
||||
.RE
|
||||
|
||||
\fBexecute-multi(...)\fR is an alternative action that executes the command
|
||||
with the selected entries when multi-select is enabled (\fB--multi\fR). With
|
||||
this action, \fB{}\fR is replaced with the quoted strings of the selected
|
||||
entries separated by spaces.
|
||||
fzf switches to the alternate screen when executing a command. However, if the
|
||||
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)
|
||||
|
264
plugin/fzf.vim
264
plugin/fzf.vim
@@ -1,4 +1,4 @@
|
||||
" Copyright (c) 2016 Junegunn Choi
|
||||
" Copyright (c) 2017 Junegunn Choi
|
||||
"
|
||||
" MIT License
|
||||
"
|
||||
@@ -26,12 +26,80 @@ if exists('g:loaded_fzf')
|
||||
endif
|
||||
let g:loaded_fzf = 1
|
||||
|
||||
let s:is_win = has('win32') || has('win64')
|
||||
if s:is_win && &shellslash
|
||||
set noshellslash
|
||||
let s:base_dir = expand('<sfile>:h:h')
|
||||
set shellslash
|
||||
else
|
||||
let s:base_dir = expand('<sfile>:h:h')
|
||||
endif
|
||||
if s:is_win
|
||||
function! s:fzf_call(fn, ...)
|
||||
let shellslash = &shellslash
|
||||
try
|
||||
set noshellslash
|
||||
return call(a:fn, a:000)
|
||||
finally
|
||||
let &shellslash = shellslash
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
" Use utf-8 for fzf.vim commands
|
||||
" Return array of shell commands for cmd.exe
|
||||
function! s:wrap_cmds(cmds)
|
||||
return ['@echo off', 'for /f "tokens=4" %%a in (''chcp'') do set origchcp=%%a', 'chcp 65001 > nul'] +
|
||||
\ (type(a:cmds) == type([]) ? a:cmds : [a:cmds]) +
|
||||
\ ['chcp %origchcp% > nul']
|
||||
endfunction
|
||||
else
|
||||
function! s:fzf_call(fn, ...)
|
||||
return call(a:fn, a:000)
|
||||
endfunction
|
||||
|
||||
function! s:wrap_cmds(cmds)
|
||||
return a:cmds
|
||||
endfunction
|
||||
endif
|
||||
|
||||
function! s:shellesc_cmd(arg)
|
||||
let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g')
|
||||
let escaped = substitute(escaped, '%', '%%', 'g')
|
||||
let escaped = substitute(escaped, '"', '\\^&', 'g')
|
||||
let escaped = substitute(escaped, '\\\+\(\\^\)', '\\\\\1', 'g')
|
||||
return '^"'.substitute(escaped, '[^\\]\zs\\$', '\\\\', '').'^"'
|
||||
endfunction
|
||||
|
||||
function! fzf#shellescape(arg, ...)
|
||||
let shell = get(a:000, 0, &shell)
|
||||
if shell =~# 'cmd.exe$'
|
||||
return s:shellesc_cmd(a:arg)
|
||||
endif
|
||||
return s:fzf_call('shellescape', a:arg)
|
||||
endfunction
|
||||
|
||||
function! s:fzf_getcwd()
|
||||
return s:fzf_call('getcwd')
|
||||
endfunction
|
||||
|
||||
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 = expand('<sfile>:h:h').'/bin/fzf'
|
||||
let s:install = expand('<sfile>:h:h').'/install'
|
||||
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('<sfile>:h:h').'/bin/fzf-tmux'
|
||||
|
||||
let s:cpo_save = &cpo
|
||||
set cpo&vim
|
||||
@@ -42,6 +110,11 @@ 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
|
||||
@@ -55,7 +128,7 @@ function! s:fzf_exec()
|
||||
throw 'fzf executable not found'
|
||||
endif
|
||||
endif
|
||||
return s:shellesc(s:exec)
|
||||
return fzf#shellescape(s:exec)
|
||||
endfunction
|
||||
|
||||
function! s:tmux_enabled()
|
||||
@@ -75,18 +148,9 @@ function! s:tmux_enabled()
|
||||
return s:tmux
|
||||
endfunction
|
||||
|
||||
function! s:shellesc(arg)
|
||||
return '"'.substitute(a:arg, '"', '\\"', 'g').'"'
|
||||
endfunction
|
||||
|
||||
function! s:escape(path)
|
||||
let escaped_chars = '$%#''"'
|
||||
|
||||
if has('unix')
|
||||
let escaped_chars .= ' \'
|
||||
endif
|
||||
|
||||
return escape(a:path, escaped_chars)
|
||||
let path = fnameescape(a:path)
|
||||
return s:is_win ? escape(path, '$') : path
|
||||
endfunction
|
||||
|
||||
" Upgrade legacy options
|
||||
@@ -126,7 +190,7 @@ function! s:has_any(dict, keys)
|
||||
endfunction
|
||||
|
||||
function! s:open(cmd, target)
|
||||
if stridx('edit', a:cmd) == 0 && fnamemodify(a:target, ':p') ==# expand('%:p')
|
||||
if stridx('edit', a:cmd) == 0 && s:fzf_fnamemodify(a:target, ':p') ==# s:fzf_expand('%:p')
|
||||
return
|
||||
endif
|
||||
execute a:cmd s:escape(a:target)
|
||||
@@ -141,11 +205,11 @@ function! s:common_sink(action, lines) abort
|
||||
if len(a:lines) > 1
|
||||
augroup fzf_swap
|
||||
autocmd SwapExists * let v:swapchoice='o'
|
||||
\| call s:warn('fzf: E325: swap file exists: '.expand('<afile>'))
|
||||
\| call s:warn('fzf: E325: swap file exists: '.s:fzf_expand('<afile>'))
|
||||
augroup END
|
||||
endif
|
||||
try
|
||||
let empty = empty(expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified
|
||||
let empty = empty(s:fzf_expand('%')) && line('$') == 1 && empty(getline(1)) && !&modified
|
||||
let autochdir = &autochdir
|
||||
set noautochdir
|
||||
for item in a:lines
|
||||
@@ -167,9 +231,12 @@ function! s:common_sink(action, lines) abort
|
||||
endfunction
|
||||
|
||||
function! s:get_color(attr, ...)
|
||||
let gui = has('termguicolors') && &termguicolors
|
||||
let fam = gui ? 'gui' : 'cterm'
|
||||
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
|
||||
for group in a:000
|
||||
let code = synIDattr(synIDtrans(hlID(group)), a:attr, 'cterm')
|
||||
if code =~ '^[0-9]\+$'
|
||||
let code = synIDattr(synIDtrans(hlID(group)), a:attr, fam)
|
||||
if code =~? pat
|
||||
return code
|
||||
endif
|
||||
endfor
|
||||
@@ -182,6 +249,21 @@ function! s:defaults()
|
||||
return empty(colors) ? '' : ('--color='.colors)
|
||||
endfunction
|
||||
|
||||
function! s:validate_layout(layout)
|
||||
for key in keys(a:layout)
|
||||
if index(s:layout_keys, key) < 0
|
||||
throw printf('Invalid entry in g:fzf_layout: %s (allowed: %s)%s',
|
||||
\ key, join(s:layout_keys, ', '), key == 'options' ? '. Use $FZF_DEFAULT_OPTS.' : '')
|
||||
endif
|
||||
endfor
|
||||
return a:layout
|
||||
endfunction
|
||||
|
||||
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]
|
||||
@@ -190,7 +272,7 @@ function! fzf#wrap(...)
|
||||
for arg in copy(a:000)
|
||||
let tidx = index(expects, type(arg), tidx)
|
||||
if tidx < 0
|
||||
throw 'invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])'
|
||||
throw 'Invalid arguments (expected: [name string] [opts dict] [fullscreen boolean])'
|
||||
endif
|
||||
let args[tidx] = arg
|
||||
let tidx += 1
|
||||
@@ -198,6 +280,10 @@ function! fzf#wrap(...)
|
||||
endfor
|
||||
let [name, opts, bang] = args
|
||||
|
||||
if len(name)
|
||||
let opts.name = name
|
||||
end
|
||||
|
||||
" Layout: g:fzf_layout (and deprecated g:fzf_height)
|
||||
if bang
|
||||
for key in s:layout_keys
|
||||
@@ -209,20 +295,21 @@ function! fzf#wrap(...)
|
||||
if !exists('g:fzf_layout') && exists('g:fzf_height')
|
||||
let opts.down = g:fzf_height
|
||||
else
|
||||
let opts = extend(opts, get(g:, 'fzf_layout', s:default_layout))
|
||||
let opts = extend(opts, s:validate_layout(get(g:, 'fzf_layout', s:default_layout)))
|
||||
endif
|
||||
endif
|
||||
|
||||
" Colors: g:fzf_colors
|
||||
let opts.options = s:defaults() .' '. get(opts, 'options', '')
|
||||
let opts.options = s:defaults() .' '. s:evaluate_opts(get(opts, 'options', ''))
|
||||
|
||||
" History: g:fzf_history_dir
|
||||
if len(name) && len(get(g:, 'fzf_history_dir', ''))
|
||||
let dir = expand(g:fzf_history_dir)
|
||||
let dir = s:fzf_expand(g:fzf_history_dir)
|
||||
if !isdirectory(dir)
|
||||
call mkdir(dir, 'p')
|
||||
endif
|
||||
let opts.options = join(['--history', s:escape(dir.'/'.name), opts.options])
|
||||
let history = fzf#shellescape(dir.'/'.name)
|
||||
let opts.options = join(['--history', history, opts.options])
|
||||
endif
|
||||
|
||||
" Action: g:fzf_action
|
||||
@@ -238,48 +325,45 @@ function! fzf#wrap(...)
|
||||
return opts
|
||||
endfunction
|
||||
|
||||
function! fzf#shellescape(path)
|
||||
if has('win32') || has('win64')
|
||||
let shellslash = &shellslash
|
||||
try
|
||||
set noshellslash
|
||||
return shellescape(a:path)
|
||||
finally
|
||||
let &shellslash = shellslash
|
||||
endtry
|
||||
endif
|
||||
return shellescape(a:path)
|
||||
endfunction
|
||||
|
||||
function! fzf#run(...) abort
|
||||
try
|
||||
let oshell = &shell
|
||||
let useshellslash = &shellslash
|
||||
|
||||
if has('win32') || has('win64')
|
||||
if s:is_win
|
||||
set shell=cmd.exe
|
||||
set noshellslash
|
||||
else
|
||||
set shell=sh
|
||||
endif
|
||||
|
||||
if has('nvim') && len(filter(range(1, bufnr('$')), 'bufname(v:val) =~# ";#FZF"'))
|
||||
call s:warn('FZF is already running!')
|
||||
return []
|
||||
if has('nvim')
|
||||
let running = filter(range(1, bufnr('$')), "bufname(v:val) =~# ';#FZF'")
|
||||
if len(running)
|
||||
call s:warn('FZF is already running (in buffer '.join(running, ', ').')!')
|
||||
return []
|
||||
endif
|
||||
endif
|
||||
let dict = exists('a:1') ? s:upgrade(a:1) : {}
|
||||
let temps = { 'result': tempname() }
|
||||
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_key(dict, 'source') && !empty($FZF_DEFAULT_COMMAND)
|
||||
let temps.source = tempname()
|
||||
call writefile(split($FZF_DEFAULT_COMMAND, "\n"), temps.source)
|
||||
let dict.source = (empty($SHELL) ? &shell : $SHELL) . ' ' . s:shellesc(temps.source)
|
||||
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')
|
||||
@@ -288,33 +372,39 @@ try
|
||||
if type == 1
|
||||
let prefix = source.'|'
|
||||
elseif type == 3
|
||||
let temps.input = tempname()
|
||||
let temps.input = s:fzf_tempname()
|
||||
call writefile(source, temps.input)
|
||||
let prefix = 'cat '.s:shellesc(temps.input).'|'
|
||||
let prefix = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input).'|'
|
||||
else
|
||||
throw 'invalid source type'
|
||||
throw 'Invalid source type'
|
||||
endif
|
||||
else
|
||||
let prefix = ''
|
||||
endif
|
||||
|
||||
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0)
|
||||
let use_height = has_key(dict, 'down') &&
|
||||
\ !(has('nvim') || has('win32') || has('win64') || s:present(dict, 'up', 'left', 'right')) &&
|
||||
\ !(has('nvim') || s:is_win || has('win32unix') || s:present(dict, 'up', 'left', 'right')) &&
|
||||
\ executable('tput') && filereadable('/dev/tty')
|
||||
let tmux = !use_height && (!has('nvim') || get(g:, 'fzf_prefer_tmux', 0)) && s:tmux_enabled() && s:splittable(dict)
|
||||
let term = has('nvim') && !tmux
|
||||
let use_term = has('nvim') && !s:is_win
|
||||
let use_tmux = (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:tmux_enabled() && s:splittable(dict)
|
||||
if prefer_tmux && use_tmux
|
||||
let use_height = 0
|
||||
let use_term = 0
|
||||
endif
|
||||
if use_height
|
||||
let optstr .= ' --height='.s:calc_size(&lines, dict.down, dict)
|
||||
elseif term
|
||||
let height = s:calc_size(&lines, dict.down, dict)
|
||||
let optstr .= ' --height='.height
|
||||
elseif use_term
|
||||
let optstr .= ' --no-height'
|
||||
endif
|
||||
let command = prefix.(tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
|
||||
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
|
||||
|
||||
if term
|
||||
if use_term
|
||||
return s:execute_term(dict, command, temps)
|
||||
endif
|
||||
|
||||
let lines = tmux ? s:execute_tmux(dict, command, temps)
|
||||
let lines = use_tmux ? s:execute_tmux(dict, command, temps)
|
||||
\ : s:execute(dict, command, use_height, temps)
|
||||
call s:callback(dict, lines)
|
||||
return lines
|
||||
@@ -348,7 +438,7 @@ function! s:fzf_tmux(dict)
|
||||
endif
|
||||
endfor
|
||||
return printf('LINES=%d COLUMNS=%d %s %s %s --',
|
||||
\ &lines, &columns, s:shellesc(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
|
||||
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
|
||||
endfunction
|
||||
|
||||
function! s:splittable(dict)
|
||||
@@ -358,13 +448,13 @@ endfunction
|
||||
|
||||
function! s:pushd(dict)
|
||||
if s:present(a:dict, 'dir')
|
||||
let cwd = getcwd()
|
||||
let cwd = s:fzf_getcwd()
|
||||
if get(a:dict, 'prev_dir', '') ==# cwd
|
||||
return 1
|
||||
endif
|
||||
let a:dict.prev_dir = cwd
|
||||
execute 'lcd' s:escape(a:dict.dir)
|
||||
let a:dict.dir = getcwd()
|
||||
let a:dict.dir = s:fzf_getcwd()
|
||||
return 1
|
||||
endif
|
||||
return 0
|
||||
@@ -393,7 +483,7 @@ function! s:xterm_launcher()
|
||||
\ &columns, &lines/2, getwinposx(), getwinposy())
|
||||
endfunction
|
||||
unlet! s:launcher
|
||||
if has('win32') || has('win64')
|
||||
if s:is_win || has('win32unix')
|
||||
let s:launcher = '%s'
|
||||
else
|
||||
let s:launcher = function('s:xterm_launcher')
|
||||
@@ -417,7 +507,7 @@ function! s:execute(dict, command, use_height, temps) abort
|
||||
if has('unix') && !a:use_height
|
||||
silent! !clear 2> /dev/null
|
||||
endif
|
||||
let escaped = escape(substitute(a:command, '\n', '\\n', 'g'), '%#')
|
||||
let escaped = (a:use_height || s:is_win) ? a:command : escape(substitute(a:command, '\n', '\\n', 'g'), '%#!')
|
||||
if has('gui_running')
|
||||
let Launcher = get(a:dict, 'launcher', get(g:, 'Fzf_launcher', get(g:, 'fzf_launcher', s:launcher)))
|
||||
let fmt = type(Launcher) == 2 ? call(Launcher, []) : Launcher
|
||||
@@ -428,6 +518,32 @@ function! s:execute(dict, command, use_height, temps) abort
|
||||
else
|
||||
let command = escaped
|
||||
endif
|
||||
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
|
||||
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))
|
||||
@@ -443,7 +559,7 @@ function! s:execute_tmux(dict, command, temps) abort
|
||||
let command = a:command
|
||||
if s:pushd(a:dict)
|
||||
" -c '#{pane_current_path}' is only available on tmux 1.9 or above
|
||||
let command = 'cd '.s:escape(a:dict.dir).' && '.command
|
||||
let command = join(['cd', fzf#shellescape(a:dict.dir), '&&', command])
|
||||
endif
|
||||
|
||||
call system(command)
|
||||
@@ -513,6 +629,7 @@ function! s:execute_term(dict, command, temps) abort
|
||||
let winrest = winrestcmd()
|
||||
let pbuf = bufnr('')
|
||||
let [ppos, winopts] = s:split(a:dict)
|
||||
let b:fzf = a:dict
|
||||
let fzf = { 'buf': bufnr(''), 'pbuf': pbuf, 'ppos': ppos, 'dict': a:dict, 'temps': a:temps,
|
||||
\ 'winopts': winopts, 'winrest': winrest, 'lines': &lines,
|
||||
\ 'columns': &columns, 'command': a:command }
|
||||
@@ -638,19 +755,24 @@ let s:default_action = {
|
||||
|
||||
function! s:shortpath()
|
||||
let short = pathshorten(fnamemodify(getcwd(), ':~:.'))
|
||||
return empty(short) ? '~/' : short . (short =~ '/$' ? '' : '/')
|
||||
let slash = (s:is_win && !&shellslash) ? '\' : '/'
|
||||
return empty(short) ? '~'.slash : short . (short =~ slash.'$' ? '' : slash)
|
||||
endfunction
|
||||
|
||||
function! s:cmd(bang, ...) abort
|
||||
let args = copy(a:000)
|
||||
let opts = { 'options': '--multi ' }
|
||||
let opts = { 'options': ['--multi'] }
|
||||
if len(args) && isdirectory(expand(args[-1]))
|
||||
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
||||
let opts.options .= ' --prompt '.fzf#shellescape(opts.dir)
|
||||
if s:is_win && !&shellslash
|
||||
let opts.dir = substitute(opts.dir, '/', '\\', 'g')
|
||||
endif
|
||||
let prompt = opts.dir
|
||||
else
|
||||
let opts.options .= ' --prompt '.fzf#shellescape(s:shortpath())
|
||||
let prompt = s:shortpath()
|
||||
endif
|
||||
let opts.options .= ' '.join(args)
|
||||
call extend(opts.options, ['--prompt', prompt])
|
||||
call extend(opts.options, args)
|
||||
call fzf#run(fzf#wrap('FZF', opts, a:bang))
|
||||
endfunction
|
||||
|
||||
|
@@ -234,7 +234,7 @@ _fzf_complete_telnet() {
|
||||
_fzf_complete_ssh() {
|
||||
_fzf_complete '+m' "$@" < <(
|
||||
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \
|
||||
<(command grep -oE '^[a-z0-9.,-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -oE '^[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
@@ -304,7 +304,7 @@ done
|
||||
|
||||
# Directory
|
||||
for cmd in $d_cmds; do
|
||||
_fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o plusdirs"
|
||||
_fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
|
||||
done
|
||||
|
||||
unset _fzf_defc
|
||||
|
@@ -117,7 +117,7 @@ _fzf_complete_telnet() {
|
||||
_fzf_complete_ssh() {
|
||||
_fzf_complete '+m' "$@" < <(
|
||||
command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \
|
||||
<(command grep -oE '^[a-z0-9.,-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -oE '^[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
@@ -143,7 +143,7 @@ _fzf_complete_unalias() {
|
||||
|
||||
fzf-completion() {
|
||||
local tokens cmd prefix trigger tail fzf matches lbuf d_cmds
|
||||
setopt localoptions noshwordsplit noksh_arrays
|
||||
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
|
||||
|
||||
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
|
||||
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# Key bindings
|
||||
# ------------
|
||||
__fzf_select__() {
|
||||
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | cut -b3-"}"
|
||||
@@ -46,8 +46,8 @@ fzf-file-widget() {
|
||||
|
||||
__fzf_cd__() {
|
||||
local cmd dir
|
||||
cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"}"
|
||||
cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype '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"
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ __fzf_history__() (
|
||||
shopt -u nocaseglob nocasematch
|
||||
line=$(
|
||||
HISTTIMEFORMAT= history |
|
||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m" $(__fzfcmd) |
|
||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --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"
|
||||
|
@@ -2,36 +2,29 @@
|
||||
# ------------
|
||||
function fzf_key_bindings
|
||||
|
||||
# Store last token in $dir as root for the 'find' command
|
||||
# Store current token in $dir as root for the 'find' command
|
||||
function fzf-file-widget -d "List files and folders"
|
||||
set -l dir (commandline -t)
|
||||
# The commandline token might be escaped, we need to unescape it.
|
||||
set dir (eval "printf '%s' $dir")
|
||||
if [ ! -d "$dir" ]
|
||||
set dir .
|
||||
end
|
||||
# Some 'find' versions print undesired duplicated slashes if the path ends with slashes.
|
||||
set dir (string replace --regex '(.)/+$' '$1' "$dir")
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -l dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
|
||||
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
|
||||
# $dir itself, even if hidden.
|
||||
set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND "
|
||||
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | sed 's#^\./##'"
|
||||
-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" | while read -l r; set result $result $r; end
|
||||
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
|
||||
end
|
||||
if [ -z "$result" ]
|
||||
commandline -f repaint
|
||||
return
|
||||
end
|
||||
|
||||
if [ "$dir" != . ]
|
||||
else
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
end
|
||||
@@ -45,23 +38,46 @@ function fzf_key_bindings
|
||||
function fzf-history-widget -d "Show command history"
|
||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS +s --no-reverse --tiebreak=index $FZF_CTRL_R_OPTS +m"
|
||||
history | eval (__fzfcmd) -q '(commandline)' | read -l result
|
||||
and commandline -- $result
|
||||
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
|
||||
end
|
||||
|
||||
function fzf-cd-widget -d "Change directory"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -l dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
|
||||
set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND "
|
||||
command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"
|
||||
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | sed 's@^\./@@'"
|
||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
|
||||
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m" | read -l result
|
||||
[ "$result" ]; and cd $result
|
||||
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
|
||||
|
||||
if [ -n "$result" ]
|
||||
cd $result
|
||||
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
end
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
@@ -84,4 +100,47 @@ function fzf_key_bindings
|
||||
bind -M insert \cr fzf-history-widget
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath and rest of token'
|
||||
# eval is used to do shell expansion on paths
|
||||
set -l commandline (eval "printf '%s' "(commandline -t))
|
||||
|
||||
if [ -z $commandline ]
|
||||
# Default to current directory with no --query
|
||||
set dir '.'
|
||||
set fzf_query ''
|
||||
else
|
||||
set dir (__fzf_get_dir $commandline)
|
||||
|
||||
if [ "$dir" = "." -a (string sub -l 1 $commandline) != '.' ]
|
||||
# if $dir is "." but commandline is not a relative path, this means no file path found
|
||||
set fzf_query $commandline
|
||||
else
|
||||
# Also remove trailing slash after dir, to "split" input properly
|
||||
set fzf_query (string replace -r "^$dir/?" '' "$commandline")
|
||||
end
|
||||
end
|
||||
|
||||
echo $dir
|
||||
echo $fzf_query
|
||||
end
|
||||
|
||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
||||
set dir $argv
|
||||
|
||||
# Strip all trailing slashes. Ignore if $dir is root dir (/)
|
||||
if [ (string length $dir) -gt 1 ]
|
||||
set dir (string replace -r '/*$' '' $dir)
|
||||
end
|
||||
|
||||
# Iteratively check if dir exists and strip tail end of path
|
||||
while [ ! -d "$dir" ]
|
||||
# If path is absolute, this can keep going until ends up at /
|
||||
# If path is relative, this can keep going until entire input is consumed, dirname returns "."
|
||||
set dir (dirname "$dir")
|
||||
end
|
||||
|
||||
echo $dir
|
||||
end
|
||||
|
||||
end
|
||||
|
@@ -4,7 +4,7 @@ if [[ $- == *i* ]]; then
|
||||
|
||||
# CTRL-T - Paste the selected file path(s) into the command line
|
||||
__fsel() {
|
||||
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | cut -b3-"}"
|
||||
@@ -38,10 +38,15 @@ bindkey '^T' fzf-file-widget
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
fzf-cd-widget() {
|
||||
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . \\( -path '*/\\.*' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | sed 1d | cut -b3-"}"
|
||||
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype '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
|
||||
cd "${$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m):-.}"
|
||||
local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)"
|
||||
if [[ -z "$dir" ]]; then
|
||||
zle redisplay
|
||||
return 0
|
||||
fi
|
||||
cd "$dir"
|
||||
local ret=$?
|
||||
zle reset-prompt
|
||||
typeset -f zle-line-init >/dev/null && zle zle-line-init
|
||||
@@ -53,9 +58,9 @@ bindkey '\ec' fzf-cd-widget
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
fzf-history-widget() {
|
||||
local selected num
|
||||
setopt localoptions noglobsubst pipefail 2> /dev/null
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail 2> /dev/null
|
||||
selected=( $(fc -l 1 |
|
||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS +s --tac --no-reverse -n2..,.. --tiebreak=index --toggle-sort=ctrl-r $FZF_CTRL_R_OPTS +m --query=${(q)LBUFFER}" $(__fzfcmd)) )
|
||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) )
|
||||
local ret=$?
|
||||
if [ -n "$selected" ]; then
|
||||
num=$selected[1]
|
||||
@@ -71,4 +76,3 @@ zle -N fzf-history-widget
|
||||
bindkey '^R' fzf-history-widget
|
||||
|
||||
fi
|
||||
|
||||
|
@@ -1,40 +0,0 @@
|
||||
FROM ubuntu:14.04
|
||||
MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
|
||||
|
||||
# apt-get
|
||||
RUN apt-get update && apt-get -y upgrade && \
|
||||
apt-get install -y --force-yes git curl build-essential
|
||||
|
||||
# Install Go 1.4
|
||||
RUN cd / && curl \
|
||||
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
|
||||
tar -xz && mv go go1.4 && \
|
||||
sed -i 's@#define PTHREAD_KEYS_MAX 128@@' /go1.4/src/runtime/cgo/gcc_android_arm.c
|
||||
|
||||
ENV GOROOT /go1.4
|
||||
ENV PATH /go1.4/bin:$PATH
|
||||
|
||||
RUN cd / && \
|
||||
curl -O http://dl.google.com/android/ndk/android-ndk-r10e-linux-x86_64.bin && \
|
||||
chmod 755 /android-ndk* && /android-ndk-r10e-linux-x86_64.bin && \
|
||||
mv android-ndk-r10e /android-ndk
|
||||
|
||||
RUN cd /android-ndk && bash ./build/tools/make-standalone-toolchain.sh --platform=android-21 --install-dir=/ndk --arch=arm
|
||||
|
||||
ENV NDK_CC /ndk/bin/arm-linux-androideabi-gcc
|
||||
|
||||
RUN cd $GOROOT/src && \
|
||||
CC_FOR_TARGET=$NDK_CC GOOS=android GOARCH=arm GOARM=7 ./make.bash
|
||||
|
||||
RUN cd / && curl \
|
||||
http://ftp.gnu.org/gnu/ncurses/ncurses-5.9.tar.gz | \
|
||||
tar -xz && cd /ncurses-5.9 && \
|
||||
./configure CC=$NDK_CC CFLAGS="-fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch" LDFLAGS="-march=armv7-a -Wl,--no-warn-mismatch" --host=arm-linux --enable-overwrite --enable-const --without-cxx-binding --without-shared --without-debug --enable-widec --enable-ext-colors --enable-ext-mouse --enable-pc-files --with-pkg-config-libdir=$PKG_CONFIG_LIBDIR --without-manpages --without-ada --disable-shared --without-tests --prefix=/ndk/sysroot/usr --with-default-terminfo-dirs=/usr/share/terminfo --with-terminfo-dirs=/usr/share/terminfo ac_cv_header_locale_h=n ac_cv_func_getpwent=no ac_cv_func_getpwnam=no ac_cv_func_getpwuid=no && \
|
||||
sed -i 's@#define HAVE_LOCALE_H 1@/* #undef HAVE_LOCALE_H */@' include/ncurses_cfg.h && \
|
||||
make && \
|
||||
sed -i '0,/echo.*/{s/echo.*/exit 0/}' misc/run_tic.sh && \
|
||||
make install && \
|
||||
mv /ndk/sysroot/usr/lib/libncursesw.a /ndk/sysroot/usr/lib/libncurses.a
|
||||
|
||||
# Default CMD
|
||||
CMD cd /fzf/src && /bin/bash
|
@@ -1,24 +0,0 @@
|
||||
FROM base/archlinux:2014.07.03
|
||||
MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
|
||||
|
||||
# apt-get
|
||||
RUN pacman-key --populate archlinux && pacman-key --refresh-keys
|
||||
RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git
|
||||
|
||||
# Install Go 1.4
|
||||
RUN cd / && curl \
|
||||
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
|
||||
tar -xz && mv go go1.4
|
||||
|
||||
ENV GOROOT /go1.4
|
||||
ENV PATH /go1.4/bin:$PATH
|
||||
|
||||
# For i386 build
|
||||
RUN echo '[multilib]' >> /etc/pacman.conf && \
|
||||
echo 'Include = /etc/pacman.d/mirrorlist' >> /etc/pacman.conf && \
|
||||
pacman-db-upgrade && yes | pacman -Sy gcc-multilib lib32-ncurses && \
|
||||
cd $GOROOT/src && GOARCH=386 ./make.bash
|
||||
|
||||
# Default CMD
|
||||
CMD cd /fzf/src && /bin/bash
|
||||
|
@@ -1,32 +0,0 @@
|
||||
FROM centos:centos6
|
||||
MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
|
||||
|
||||
# yum
|
||||
RUN yum install -y git gcc make tar glibc-devel glibc-devel.i686 \
|
||||
ncurses-devel ncurses-static ncurses-devel.i686 \
|
||||
gpm-devel gpm-static libgcc.i686
|
||||
|
||||
# Install Go 1.4
|
||||
RUN cd / && curl \
|
||||
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
|
||||
tar -xz && mv go go1.4
|
||||
|
||||
# Install Go 1.7
|
||||
RUN cd / && curl \
|
||||
https://storage.googleapis.com/golang/go1.7.linux-amd64.tar.gz | \
|
||||
tar -xz && mv go go1.7
|
||||
|
||||
# Install RPMs for building static 32-bit binary
|
||||
RUN curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.8/os/i386/Packages/ncurses-static-5.7-4.20090207.el6.i686.rpm -o rpm && rpm -i rpm && \
|
||||
curl ftp://ftp.pbone.net/mirror/ftp.centos.org/6.8/os/i386/Packages/gpm-static-1.20.6-12.el6.i686.rpm -o rpm && rpm -i rpm
|
||||
|
||||
ENV GOROOT_BOOTSTRAP /go1.4
|
||||
ENV GOROOT /go1.7
|
||||
ENV PATH /go1.7/bin:$PATH
|
||||
|
||||
# For i386 build
|
||||
RUN cd $GOROOT/src && GOARCH=386 ./make.bash
|
||||
|
||||
# Default CMD
|
||||
CMD cd /fzf/src && /bin/bash
|
||||
|
@@ -1,22 +0,0 @@
|
||||
FROM ubuntu:14.04
|
||||
MAINTAINER Junegunn Choi <junegunn.c@gmail.com>
|
||||
|
||||
# apt-get
|
||||
RUN apt-get update && apt-get -y upgrade && \
|
||||
apt-get install -y --force-yes git curl build-essential libncurses-dev libgpm-dev
|
||||
|
||||
# Install Go 1.4
|
||||
RUN cd / && curl \
|
||||
https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
|
||||
tar -xz && mv go go1.4
|
||||
|
||||
ENV GOROOT /go1.4
|
||||
ENV PATH /go1.4/bin:$PATH
|
||||
|
||||
# For i386 build
|
||||
RUN apt-get install -y lib32ncurses5-dev && \
|
||||
cd $GOROOT/src && GOARCH=386 ./make.bash
|
||||
|
||||
# Default CMD
|
||||
CMD cd /fzf/src && /bin/bash
|
||||
|
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Junegunn Choi
|
||||
Copyright (c) 2017 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
123
src/Makefile
123
src/Makefile
@@ -1,123 +0,0 @@
|
||||
ifndef GOOS
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
GOOS := darwin
|
||||
else ifeq ($(UNAME_S),Linux)
|
||||
GOOS := linux
|
||||
endif
|
||||
endif
|
||||
|
||||
SOURCES := $(wildcard *.go */*.go)
|
||||
ROOTDIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
BINDIR := $(shell dirname $(ROOTDIR))/bin
|
||||
GOPATH := $(shell dirname $(ROOTDIR))/gopath
|
||||
SRCDIR := $(GOPATH)/src/github.com/junegunn/fzf/src
|
||||
DOCKEROPTS := -i -t -v $(ROOTDIR):/fzf/src
|
||||
BINARY32 := fzf-$(GOOS)_386
|
||||
BINARY64 := fzf-$(GOOS)_amd64
|
||||
BINARYARM7 := fzf-$(GOOS)_arm7
|
||||
VERSION := $(shell awk -F= '/version =/ {print $$2}' constants.go | tr -d "\" ")
|
||||
RELEASE32 := fzf-$(VERSION)-$(GOOS)_386
|
||||
RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64
|
||||
RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7
|
||||
export GOPATH
|
||||
|
||||
UNAME_M := $(shell uname -m)
|
||||
ifeq ($(UNAME_M),x86_64)
|
||||
BINARY := $(BINARY64)
|
||||
else ifeq ($(UNAME_M),i686)
|
||||
BINARY := $(BINARY32)
|
||||
else
|
||||
$(error "Build on $(UNAME_M) is not supported, yet.")
|
||||
endif
|
||||
|
||||
all: fzf/$(BINARY)
|
||||
|
||||
ifeq ($(GOOS),windows)
|
||||
release: fzf/$(BINARY32) fzf/$(BINARY64)
|
||||
-cd fzf && cp $(BINARY32) $(RELEASE32).exe && zip $(RELEASE32).zip $(RELEASE32).exe
|
||||
cd fzf && cp $(BINARY64) $(RELEASE64).exe && zip $(RELEASE64).zip $(RELEASE64).exe && \
|
||||
rm -f $(RELEASE32).exe $(RELEASE64).exe
|
||||
else
|
||||
release: test fzf/$(BINARY32) fzf/$(BINARY64)
|
||||
-cd fzf && cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32)
|
||||
cd fzf && cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \
|
||||
rm -f $(RELEASE32) $(RELEASE64)
|
||||
endif
|
||||
|
||||
$(SRCDIR):
|
||||
mkdir -p $(shell dirname $(SRCDIR))
|
||||
ln -s $(ROOTDIR) $(SRCDIR)
|
||||
|
||||
deps: $(SRCDIR) $(SOURCES)
|
||||
cd $(SRCDIR) && go get -tags "$(TAGS)"
|
||||
|
||||
android-build: $(SRCDIR)
|
||||
cd $(SRCDIR) && GOARCH=arm GOARM=7 CGO_ENABLED=1 go get
|
||||
cd $(SRCDIR)/fzf && GOARCH=arm GOARM=7 CGO_ENABLED=1 go build -a -ldflags="-w -extldflags=-pie" -o $(BINARYARM7)
|
||||
cd $(SRCDIR)/fzf && cp $(BINARYARM7) $(RELEASEARM7) && tar -czf $(RELEASEARM7).tgz $(RELEASEARM7) && \
|
||||
rm -f $(RELEASEARM7)
|
||||
|
||||
test: deps
|
||||
SHELL=/bin/sh GOOS=$(GOOS) go test -v -tags "$(TAGS)" ./...
|
||||
|
||||
install: $(BINDIR)/fzf
|
||||
|
||||
uninstall:
|
||||
rm -f $(BINDIR)/fzf $(BINDIR)/$(BINARY)
|
||||
|
||||
clean:
|
||||
cd fzf && rm -f fzf-*
|
||||
|
||||
fzf/$(BINARY32): deps
|
||||
cd fzf && GOARCH=386 CGO_ENABLED=1 go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY32)
|
||||
|
||||
fzf/$(BINARY64): deps
|
||||
cd fzf && go build -a -ldflags "-w -extldflags=$(LDFLAGS)" -tags "$(TAGS)" -o $(BINARY64)
|
||||
|
||||
$(BINDIR)/fzf: fzf/$(BINARY) | $(BINDIR)
|
||||
cp -f fzf/$(BINARY) $(BINDIR)
|
||||
cd $(BINDIR) && ln -sf $(BINARY) fzf
|
||||
|
||||
$(BINDIR):
|
||||
mkdir -p $@
|
||||
|
||||
docker-arch:
|
||||
docker build -t junegunn/arch-sandbox - < Dockerfile.arch
|
||||
|
||||
docker-ubuntu:
|
||||
docker build -t junegunn/ubuntu-sandbox - < Dockerfile.ubuntu
|
||||
|
||||
docker-centos:
|
||||
docker build -t junegunn/centos-sandbox - < Dockerfile.centos
|
||||
|
||||
docker-android:
|
||||
docker build -t junegunn/android-sandbox - < Dockerfile.android
|
||||
|
||||
arch: docker-arch
|
||||
docker run $(DOCKEROPTS) junegunn/$@-sandbox \
|
||||
sh -c 'cd /fzf/src; /bin/bash'
|
||||
|
||||
ubuntu: docker-ubuntu
|
||||
docker run $(DOCKEROPTS) junegunn/$@-sandbox \
|
||||
sh -c 'cd /fzf/src; /bin/bash'
|
||||
|
||||
centos: docker-centos
|
||||
docker run $(DOCKEROPTS) junegunn/$@-sandbox \
|
||||
sh -c 'cd /fzf/src; /bin/bash'
|
||||
|
||||
linux: docker-centos
|
||||
docker run $(DOCKEROPTS) junegunn/centos-sandbox \
|
||||
/bin/bash -ci 'cd /fzf/src; make TAGS=static release'
|
||||
|
||||
ubuntu-android: docker-android
|
||||
docker run $(DOCKEROPTS) junegunn/android-sandbox \
|
||||
sh -c 'cd /fzf/src; /bin/bash'
|
||||
|
||||
android: docker-android
|
||||
docker run $(DOCKEROPTS) junegunn/android-sandbox \
|
||||
/bin/bash -ci 'cd /fzf/src; GOOS=android make android-build'
|
||||
|
||||
.PHONY: all deps release test install uninstall clean \
|
||||
linux arch ubuntu centos docker-arch docker-ubuntu docker-centos \
|
||||
android-build docker-android ubuntu-android android
|
@@ -1,95 +0,0 @@
|
||||
fzf in Go
|
||||
=========
|
||||
|
||||
<img src="https://cloud.githubusercontent.com/assets/700826/5725028/028ea834-9b93-11e4-9198-43088c3f295d.gif" height="463" alt="fzf in go">
|
||||
|
||||
This directory contains the source code for the new fzf implementation in
|
||||
[Go][go].
|
||||
|
||||
Upgrade from Ruby version
|
||||
-------------------------
|
||||
|
||||
The install script has been updated to download the right binary for your
|
||||
system. If you already have installed fzf, simply git-pull the repository and
|
||||
rerun the install script.
|
||||
|
||||
```sh
|
||||
cd ~/.fzf
|
||||
git pull
|
||||
./install
|
||||
```
|
||||
|
||||
Otherwise, follow [the instruction][install] as before. You can also install
|
||||
fzf using Homebrew if you prefer that way.
|
||||
|
||||
Motivations
|
||||
-----------
|
||||
|
||||
### No Ruby dependency
|
||||
|
||||
There have always been complaints about fzf being a Ruby script. To make
|
||||
matters worse, Ruby 2.1 removed ncurses binding from its standard libary.
|
||||
Because of the change, users running Ruby 2.1 or above are forced to build C
|
||||
extensions of curses gem to meet the requirement of fzf. The new Go version
|
||||
will be distributed as an executable binary so it will be much more accessible
|
||||
and should be easier to setup.
|
||||
|
||||
### Performance
|
||||
|
||||
Many people have been surprised to see how fast fzf is even when it was
|
||||
written in Ruby. It stays quite responsive even for 100k+ lines, which is
|
||||
well above the size of the usual input.
|
||||
|
||||
The new Go version, of course, is significantly faster than that. It has all
|
||||
the performance optimization techniques used in Ruby implementation and more.
|
||||
It also doesn't suffer from [GIL][gil], so the search performance scales
|
||||
proportional to the number of CPU cores. On my MacBook Pro (Mid 2012), the new
|
||||
version was shown to be an order of magnitude faster on certain cases. It also
|
||||
starts much faster though the difference may not be noticeable.
|
||||
|
||||
Build
|
||||
-----
|
||||
|
||||
See [BUILD.md](../BUILD.md)
|
||||
|
||||
Test
|
||||
----
|
||||
|
||||
Unit tests can be run with `make test`. Integration tests are written in Ruby
|
||||
script that should be run on tmux.
|
||||
|
||||
```sh
|
||||
# Unit tests
|
||||
make test
|
||||
|
||||
# Install the executable to ../bin directory
|
||||
make install
|
||||
|
||||
# Integration tests
|
||||
ruby ../test/test_go.rb
|
||||
```
|
||||
|
||||
Third-party libraries used
|
||||
--------------------------
|
||||
|
||||
- [ncurses][ncurses]
|
||||
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [mattn/go-isatty](https://github.com/mattn/go-isatty)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [tcell](https://github.com/gdamore/tcell)
|
||||
- Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE)
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
[install]: https://github.com/junegunn/fzf#installation
|
||||
[go]: https://golang.org/
|
||||
[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock
|
||||
[ncurses]: https://www.gnu.org/software/ncurses/
|
||||
[req]: http://golang.org/doc/install
|
||||
[tcell]: https://github.com/gdamore/tcell
|
@@ -283,8 +283,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C
|
||||
|
||||
// Phase 1. Check if there's a match and calculate bonus for each point
|
||||
pidx, lastIdx, prevClass := 0, 0, charNonWord
|
||||
input.CopyRunes(T)
|
||||
for idx := 0; idx < N; idx++ {
|
||||
char := input.Get(idx)
|
||||
char := T[idx]
|
||||
var class charClass
|
||||
if char <= unicode.MaxASCII {
|
||||
class = charClassOfAscii(char)
|
||||
@@ -389,7 +390,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C
|
||||
if i == 0 {
|
||||
fmt.Print(" ")
|
||||
for j := int(F[i]); j <= lastIdx; j++ {
|
||||
fmt.Printf(" " + string(input.Get(j)) + " ")
|
||||
fmt.Printf(" " + string(T[j]) + " ")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@ func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool
|
||||
if !caseSensitive {
|
||||
pattern = strings.ToLower(pattern)
|
||||
}
|
||||
res, pos := fun(caseSensitive, normalize, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil)
|
||||
res, pos := fun(caseSensitive, normalize, forward, util.ToChars([]byte(input)), []rune(pattern), true, nil)
|
||||
var start, end int
|
||||
if pos == nil || len(*pos) == 0 {
|
||||
start = res.Start
|
||||
|
60
src/ansi.go
60
src/ansi.go
@@ -44,7 +44,21 @@ func init() {
|
||||
*/
|
||||
// The following regular expression will include not all but most of the
|
||||
// frequently used ANSI sequences
|
||||
ansiRegex = regexp.MustCompile("\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x08\x0e\x0f]")
|
||||
ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x0e\x0f]|.\x08)")
|
||||
}
|
||||
|
||||
func findAnsiStart(str string) int {
|
||||
idx := 0
|
||||
for ; idx < len(str); idx++ {
|
||||
b := str[idx]
|
||||
if b == 0x1b || b == 0x0e || b == 0x0f {
|
||||
return idx
|
||||
}
|
||||
if b == 0x08 && idx > 0 {
|
||||
return idx - 1
|
||||
}
|
||||
}
|
||||
return idx
|
||||
}
|
||||
|
||||
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
|
||||
@@ -55,41 +69,61 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
|
||||
}
|
||||
|
||||
idx := 0
|
||||
for _, offset := range ansiRegex.FindAllStringIndex(str, -1) {
|
||||
prev := str[idx:offset[0]]
|
||||
output.WriteString(prev)
|
||||
prevIdx := 0
|
||||
runeCount := 0
|
||||
for idx := 0; idx < len(str); {
|
||||
idx += findAnsiStart(str[idx:])
|
||||
|
||||
// No sign of ANSI code
|
||||
if idx == len(str) {
|
||||
break
|
||||
}
|
||||
|
||||
// Make sure that we found an ANSI code
|
||||
offset := ansiRegex.FindStringIndex(str[idx:])
|
||||
if offset == nil {
|
||||
idx++
|
||||
continue
|
||||
}
|
||||
offset[0] += idx
|
||||
offset[1] += idx
|
||||
idx = offset[1]
|
||||
|
||||
// Check if we should continue
|
||||
prev := str[prevIdx:offset[0]]
|
||||
if proc != nil && !proc(prev, state) {
|
||||
return "", nil, nil
|
||||
}
|
||||
newState := interpretCode(str[offset[0]:offset[1]], state)
|
||||
|
||||
prevIdx = offset[1]
|
||||
runeCount += utf8.RuneCountInString(prev)
|
||||
output.WriteString(prev)
|
||||
|
||||
newState := interpretCode(str[offset[0]:offset[1]], state)
|
||||
if !newState.equals(state) {
|
||||
if state != nil {
|
||||
// Update last offset
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||
}
|
||||
|
||||
if newState.colored() {
|
||||
// Append new offset
|
||||
state = newState
|
||||
newLen := int32(utf8.RuneCount(output.Bytes()))
|
||||
offsets = append(offsets, ansiOffset{[2]int32{newLen, newLen}, *state})
|
||||
offsets = append(offsets, ansiOffset{[2]int32{int32(runeCount), int32(runeCount)}, *state})
|
||||
} else {
|
||||
// Discard state
|
||||
state = nil
|
||||
}
|
||||
}
|
||||
|
||||
idx = offset[1]
|
||||
}
|
||||
|
||||
rest := str[idx:]
|
||||
rest := str[prevIdx:]
|
||||
if len(rest) > 0 {
|
||||
output.WriteString(rest)
|
||||
if state != nil {
|
||||
// Update last offset
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
|
||||
runeCount += utf8.RuneCountInString(rest)
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||
}
|
||||
}
|
||||
if proc != nil {
|
||||
|
42
src/cache.go
42
src/cache.go
@@ -3,7 +3,7 @@ package fzf
|
||||
import "sync"
|
||||
|
||||
// queryCache associates strings to lists of items
|
||||
type queryCache map[string][]*Result
|
||||
type queryCache map[string][]Result
|
||||
|
||||
// ChunkCache associates Chunk and query string to lists of items
|
||||
type ChunkCache struct {
|
||||
@@ -17,7 +17,7 @@ func NewChunkCache() ChunkCache {
|
||||
}
|
||||
|
||||
// Add adds the list to the cache
|
||||
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) {
|
||||
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) {
|
||||
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
|
||||
return
|
||||
}
|
||||
@@ -33,10 +33,10 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) {
|
||||
(*qc)[key] = list
|
||||
}
|
||||
|
||||
// Find is called to lookup ChunkCache
|
||||
func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Result, 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) ([]*Result, bool) {
|
||||
if ok {
|
||||
list, ok := (*qc)[key]
|
||||
if ok {
|
||||
return list, true
|
||||
return list
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc *ChunkCache) Search(chunk *Chunk, key string) []Result {
|
||||
if len(key) == 0 || !chunk.IsFull() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cc.mutex.Lock()
|
||||
defer cc.mutex.Unlock()
|
||||
|
||||
qc, ok := cc.cache[chunk]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
for idx := 1; idx < len(key); idx++ {
|
||||
// [---------| ] | [ |---------]
|
||||
// [--------| ] | [ |--------]
|
||||
// [-------| ] | [ |-------]
|
||||
prefix := key[:len(key)-idx]
|
||||
suffix := key[idx:]
|
||||
for _, substr := range [2]string{prefix, suffix} {
|
||||
if cached, found := (*qc)[substr]; found {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@@ -7,34 +7,34 @@ func TestChunkCache(t *testing.T) {
|
||||
chunk2 := make(Chunk, chunkSize)
|
||||
chunk1p := &Chunk{}
|
||||
chunk2p := &chunk2
|
||||
items1 := []*Result{&Result{}}
|
||||
items2 := []*Result{&Result{}, &Result{}}
|
||||
items1 := []Result{Result{}}
|
||||
items2 := []Result{Result{}, Result{}}
|
||||
cache.Add(chunk1p, "foo", items1)
|
||||
cache.Add(chunk2p, "foo", items1)
|
||||
cache.Add(chunk2p, "bar", items2)
|
||||
|
||||
{ // 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,12 +2,12 @@ package fzf
|
||||
|
||||
import "sync"
|
||||
|
||||
// Chunk is a list of Item pointers whose size has the upper limit of chunkSize
|
||||
type Chunk []*Item // >>> []Item
|
||||
// Chunk is a list of Items whose size has the upper limit of chunkSize
|
||||
type Chunk []Item
|
||||
|
||||
// ItemBuilder is a closure type that builds Item object from a pointer to a
|
||||
// string and an integer
|
||||
type ItemBuilder func([]byte, int) *Item
|
||||
type ItemBuilder func([]byte, int) Item
|
||||
|
||||
// ChunkList is a list of Chunks
|
||||
type ChunkList struct {
|
||||
@@ -28,11 +28,11 @@ func NewChunkList(trans ItemBuilder) *ChunkList {
|
||||
|
||||
func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool {
|
||||
item := trans(data, index)
|
||||
if item != nil {
|
||||
*c = append(*c, item)
|
||||
return true
|
||||
if item.Nil() {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
*c = append(*c, item)
|
||||
return true
|
||||
}
|
||||
|
||||
// IsFull returns true if the Chunk is full
|
||||
@@ -58,7 +58,7 @@ func (cl *ChunkList) Push(data []byte) bool {
|
||||
defer cl.mutex.Unlock()
|
||||
|
||||
if len(cl.chunks) == 0 || cl.lastChunk().IsFull() {
|
||||
newChunk := Chunk(make([]*Item, 0, chunkSize))
|
||||
newChunk := Chunk(make([]Item, 0, chunkSize))
|
||||
cl.chunks = append(cl.chunks, &newChunk)
|
||||
}
|
||||
|
||||
@@ -79,15 +79,8 @@ func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
|
||||
|
||||
// Duplicate the last chunk
|
||||
if cnt := len(ret); cnt > 0 {
|
||||
ret[cnt-1] = ret[cnt-1].dupe()
|
||||
newChunk := *ret[cnt-1]
|
||||
ret[cnt-1] = &newChunk
|
||||
}
|
||||
return ret, cl.count
|
||||
}
|
||||
|
||||
func (c *Chunk) dupe() *Chunk {
|
||||
newChunk := make(Chunk, len(*c))
|
||||
for idx, ptr := range *c {
|
||||
newChunk[idx] = ptr
|
||||
}
|
||||
return &newChunk
|
||||
}
|
||||
|
@@ -11,8 +11,10 @@ func TestChunkList(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byScore, byLength}
|
||||
|
||||
cl := NewChunkList(func(s []byte, i int) *Item {
|
||||
return &Item{text: util.ToChars(s), index: int32(i * 2)}
|
||||
cl := NewChunkList(func(s []byte, i int) Item {
|
||||
chars := util.ToChars(s)
|
||||
chars.Index = int32(i * 2)
|
||||
return Item{text: chars}
|
||||
})
|
||||
|
||||
// Snapshot
|
||||
@@ -41,8 +43,8 @@ func TestChunkList(t *testing.T) {
|
||||
if len(*chunk1) != 2 {
|
||||
t.Error("Snapshot should contain only two items")
|
||||
}
|
||||
if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].index != 0 ||
|
||||
(*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].index != 2 {
|
||||
if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].Index() != 0 ||
|
||||
(*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].Index() != 2 {
|
||||
t.Error("Invalid data")
|
||||
}
|
||||
if chunk1.IsFull() {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
@@ -8,7 +9,7 @@ import (
|
||||
|
||||
const (
|
||||
// Current version
|
||||
version = "0.16.1"
|
||||
version = "0.16.10"
|
||||
|
||||
// Core
|
||||
coordinatorDelayMax time.Duration = 100 * time.Millisecond
|
||||
@@ -18,10 +19,9 @@ const (
|
||||
readerBufferSize = 64 * 1024
|
||||
|
||||
// Terminal
|
||||
initialDelay = 20 * time.Millisecond
|
||||
initialDelayTac = 100 * time.Millisecond
|
||||
spinnerDuration = 200 * time.Millisecond
|
||||
maxPatternLength = 100
|
||||
initialDelay = 20 * time.Millisecond
|
||||
initialDelayTac = 100 * time.Millisecond
|
||||
spinnerDuration = 200 * time.Millisecond
|
||||
|
||||
// Matcher
|
||||
numPartitionsMultiplier = 8
|
||||
@@ -48,6 +48,18 @@ const (
|
||||
defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+"
|
||||
)
|
||||
|
||||
var defaultCommand string
|
||||
|
||||
func init() {
|
||||
if !util.IsWindows() {
|
||||
defaultCommand = `command find -L . -mindepth 1 \( -path '*/\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-`
|
||||
} else if os.Getenv("TERM") == "cygwin" {
|
||||
defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"`
|
||||
} else {
|
||||
defaultCommand = `dir /s/b`
|
||||
}
|
||||
}
|
||||
|
||||
// fzf events
|
||||
const (
|
||||
EvtReadNew util.EventType = iota
|
||||
|
@@ -1,8 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package fzf
|
||||
|
||||
const (
|
||||
// Reader
|
||||
defaultCommand = `find -L . -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | sed s/^..//`
|
||||
)
|
@@ -1,8 +0,0 @@
|
||||
// +build windows
|
||||
|
||||
package fzf
|
||||
|
||||
const (
|
||||
// Reader
|
||||
defaultCommand = `dir /s/b`
|
||||
)
|
58
src/core.go
58
src/core.go
@@ -3,7 +3,7 @@ Package fzf implements fzf, a command-line fuzzy finder.
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Junegunn Choi
|
||||
Copyright (c) 2017 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -43,12 +43,16 @@ Matcher -> EvtHeader -> Terminal (update header)
|
||||
*/
|
||||
|
||||
// Run starts fzf
|
||||
func Run(opts *Options) {
|
||||
func Run(opts *Options, revision string) {
|
||||
sort := opts.Sort > 0
|
||||
sortCriteria = opts.Criteria
|
||||
|
||||
if opts.Version {
|
||||
fmt.Println(version)
|
||||
if len(revision) > 0 {
|
||||
fmt.Printf("%s (%s)\n", version, revision)
|
||||
} else {
|
||||
fmt.Println(version)
|
||||
}
|
||||
os.Exit(exitOk)
|
||||
}
|
||||
|
||||
@@ -59,65 +63,51 @@ func Run(opts *Options) {
|
||||
ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
return util.ToChars(data), nil
|
||||
}
|
||||
ansiProcessorRunes := func(data []rune) (util.Chars, *[]ansiOffset) {
|
||||
return util.RunesToChars(data), nil
|
||||
}
|
||||
if opts.Ansi {
|
||||
if opts.Theme != nil {
|
||||
var state *ansiState
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
trimmed, offsets, newState := extractColor(string(data), state, nil)
|
||||
state = newState
|
||||
return util.RunesToChars([]rune(trimmed)), offsets
|
||||
return util.ToChars([]byte(trimmed)), offsets
|
||||
}
|
||||
} else {
|
||||
// When color is disabled but ansi option is given,
|
||||
// we simply strip out ANSI codes from the input
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
trimmed, _, _ := extractColor(string(data), nil, nil)
|
||||
return util.RunesToChars([]rune(trimmed)), nil
|
||||
return util.ToChars([]byte(trimmed)), nil
|
||||
}
|
||||
}
|
||||
ansiProcessorRunes = func(data []rune) (util.Chars, *[]ansiOffset) {
|
||||
return ansiProcessor([]byte(string(data)))
|
||||
}
|
||||
}
|
||||
|
||||
// Chunk list
|
||||
var chunkList *ChunkList
|
||||
header := make([]string, 0, opts.HeaderLines)
|
||||
if len(opts.WithNth) == 0 {
|
||||
chunkList = NewChunkList(func(data []byte, index int) *Item {
|
||||
chunkList = NewChunkList(func(data []byte, index int) Item {
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, string(data))
|
||||
eventBox.Set(EvtHeader, header)
|
||||
return nil
|
||||
return nilItem
|
||||
}
|
||||
chars, colors := ansiProcessor(data)
|
||||
return &Item{
|
||||
index: int32(index),
|
||||
text: chars,
|
||||
colors: colors}
|
||||
chars.Index = int32(index)
|
||||
return Item{text: chars, colors: colors}
|
||||
})
|
||||
} else {
|
||||
chunkList = NewChunkList(func(data []byte, index int) *Item {
|
||||
tokens := Tokenize(util.ToChars(data), opts.Delimiter)
|
||||
chunkList = NewChunkList(func(data []byte, index int) Item {
|
||||
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 nilItem
|
||||
}
|
||||
textRunes := joinTokens(trans)
|
||||
item := Item{
|
||||
index: int32(index),
|
||||
origText: &data,
|
||||
colors: nil}
|
||||
|
||||
trimmed, colors := ansiProcessorRunes(textRunes)
|
||||
item.text = trimmed
|
||||
item.colors = colors
|
||||
return &item
|
||||
trimmed, colors := ansiProcessor([]byte(transformed))
|
||||
trimmed.Index = int32(index)
|
||||
return Item{text: trimmed, colors: colors, origText: &data}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -162,8 +152,8 @@ func Run(opts *Options) {
|
||||
reader := Reader{
|
||||
func(runes []byte) bool {
|
||||
item := chunkList.trans(runes, 0)
|
||||
if item != nil {
|
||||
if result, _, _ := pattern.MatchItem(item, false, slab); result != nil {
|
||||
if !item.Nil() {
|
||||
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
|
||||
opts.Printer(item.text.ToString())
|
||||
found = true
|
||||
}
|
||||
@@ -222,7 +212,7 @@ func Run(opts *Options) {
|
||||
case EvtReadNew, EvtReadFin:
|
||||
reading = reading && evt == EvtReadNew
|
||||
snapshot, count := chunkList.Snapshot()
|
||||
terminal.UpdateCount(count, !reading)
|
||||
terminal.UpdateCount(count, !reading, value.(bool))
|
||||
matcher.Reset(snapshot, terminal.Input(), false, !reading, sort)
|
||||
|
||||
case EvtSearchNew:
|
||||
|
23
src/item.go
23
src/item.go
@@ -4,18 +4,27 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
// Item represents each input line
|
||||
// Item represents each input line. 56 bytes.
|
||||
type Item struct {
|
||||
index int32
|
||||
text util.Chars
|
||||
origText *[]byte
|
||||
colors *[]ansiOffset
|
||||
transformed []Token
|
||||
text util.Chars // 32 = 24 + 1 + 1 + 2 + 4
|
||||
transformed *[]Token // 8
|
||||
origText *[]byte // 8
|
||||
colors *[]ansiOffset // 8
|
||||
}
|
||||
|
||||
// Index returns ordinal index of the Item
|
||||
func (item *Item) Index() int32 {
|
||||
return item.index
|
||||
return item.text.Index
|
||||
}
|
||||
|
||||
var nilItem = Item{text: util.Chars{Index: -1}}
|
||||
|
||||
func (item *Item) Nil() bool {
|
||||
return item.Index() < 0
|
||||
}
|
||||
|
||||
func (item *Item) TrimLength() uint16 {
|
||||
return item.text.TrimLength()
|
||||
}
|
||||
|
||||
// Colors returns ansiOffsets of the Item
|
||||
|
@@ -131,7 +131,7 @@ func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
|
||||
|
||||
type partialResult struct {
|
||||
index int
|
||||
matches []*Result
|
||||
matches []Result
|
||||
}
|
||||
|
||||
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
@@ -162,7 +162,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
go func(idx int, slab *util.Slab, chunks []*Chunk) {
|
||||
defer func() { waitGroup.Done() }()
|
||||
count := 0
|
||||
allMatches := make([][]*Result, len(chunks))
|
||||
allMatches := make([][]Result, len(chunks))
|
||||
for idx, chunk := range chunks {
|
||||
matches := request.pattern.Match(chunk, slab)
|
||||
allMatches[idx] = matches
|
||||
@@ -172,7 +172,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
}
|
||||
countChan <- len(matches)
|
||||
}
|
||||
sliceMatches := make([]*Result, 0, count)
|
||||
sliceMatches := make([]Result, 0, count)
|
||||
for _, matches := range allMatches {
|
||||
sliceMatches = append(sliceMatches, matches...)
|
||||
}
|
||||
@@ -212,7 +212,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
partialResults := make([][]*Result, numSlices)
|
||||
partialResults := make([][]Result, numSlices)
|
||||
for _ = range slices {
|
||||
partialResult := <-resultChan
|
||||
partialResults[partialResult.index] = partialResult.matches
|
||||
|
@@ -3,14 +3,14 @@ package fzf
|
||||
import "fmt"
|
||||
|
||||
// EmptyMerger is a Merger with no data
|
||||
var EmptyMerger = NewMerger(nil, [][]*Result{}, false, false)
|
||||
var EmptyMerger = NewMerger(nil, [][]Result{}, false, false)
|
||||
|
||||
// Merger holds a set of locally sorted lists of items and provides the view of
|
||||
// a single, globally-sorted list
|
||||
type Merger struct {
|
||||
pattern *Pattern
|
||||
lists [][]*Result
|
||||
merged []*Result
|
||||
lists [][]Result
|
||||
merged []Result
|
||||
chunks *[]*Chunk
|
||||
cursors []int
|
||||
sorted bool
|
||||
@@ -35,11 +35,11 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
|
||||
}
|
||||
|
||||
// NewMerger returns a new Merger
|
||||
func NewMerger(pattern *Pattern, lists [][]*Result, sorted bool, tac bool) *Merger {
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger {
|
||||
mg := Merger{
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
merged: []*Result{},
|
||||
merged: []Result{},
|
||||
chunks: nil,
|
||||
cursors: make([]int, len(lists)),
|
||||
sorted: sorted,
|
||||
@@ -59,13 +59,13 @@ func (mg *Merger) Length() int {
|
||||
}
|
||||
|
||||
// Get returns the pointer to the Result object indexed by the given integer
|
||||
func (mg *Merger) Get(idx int) *Result {
|
||||
func (mg *Merger) Get(idx int) Result {
|
||||
if mg.chunks != nil {
|
||||
if mg.tac {
|
||||
idx = mg.count - idx - 1
|
||||
}
|
||||
chunk := (*mg.chunks)[idx/chunkSize]
|
||||
return &Result{item: (*chunk)[idx%chunkSize]}
|
||||
return Result{item: &(*chunk)[idx%chunkSize]}
|
||||
}
|
||||
|
||||
if mg.sorted {
|
||||
@@ -89,7 +89,7 @@ func (mg *Merger) cacheable() bool {
|
||||
return mg.count < mergerCacheMax
|
||||
}
|
||||
|
||||
func (mg *Merger) mergedGet(idx int) *Result {
|
||||
func (mg *Merger) mergedGet(idx int) Result {
|
||||
for i := len(mg.merged); i <= idx; i++ {
|
||||
minRank := minRank()
|
||||
minIdx := -1
|
||||
@@ -100,7 +100,7 @@ func (mg *Merger) mergedGet(idx int) *Result {
|
||||
continue
|
||||
}
|
||||
if cursor >= 0 {
|
||||
rank := list[cursor].rank
|
||||
rank := list[cursor]
|
||||
if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
|
||||
minRank = rank
|
||||
minIdx = listIdx
|
||||
|
@@ -15,11 +15,11 @@ func assert(t *testing.T, cond bool, msg ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
func randResult() *Result {
|
||||
func randResult() Result {
|
||||
str := fmt.Sprintf("%d", rand.Uint32())
|
||||
return &Result{
|
||||
item: &Item{text: util.RunesToChars([]rune(str))},
|
||||
rank: rank{index: rand.Int31()}}
|
||||
chars := util.ToChars([]byte(str))
|
||||
chars.Index = rand.Int31()
|
||||
return Result{item: &Item{text: chars}}
|
||||
}
|
||||
|
||||
func TestEmptyMerger(t *testing.T) {
|
||||
@@ -29,14 +29,14 @@ func TestEmptyMerger(t *testing.T) {
|
||||
assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
|
||||
}
|
||||
|
||||
func buildLists(partiallySorted bool) ([][]*Result, []*Result) {
|
||||
func buildLists(partiallySorted bool) ([][]Result, []Result) {
|
||||
numLists := 4
|
||||
lists := make([][]*Result, numLists)
|
||||
lists := make([][]Result, numLists)
|
||||
cnt := 0
|
||||
for i := 0; i < numLists; i++ {
|
||||
numResults := rand.Int() % 20
|
||||
cnt += numResults
|
||||
lists[i] = make([]*Result, numResults)
|
||||
lists[i] = make([]Result, numResults)
|
||||
for j := 0; j < numResults; j++ {
|
||||
item := randResult()
|
||||
lists[i][j] = item
|
||||
@@ -45,7 +45,7 @@ func buildLists(partiallySorted bool) ([][]*Result, []*Result) {
|
||||
sort.Sort(ByRelevance(lists[i]))
|
||||
}
|
||||
}
|
||||
items := []*Result{}
|
||||
items := []Result{}
|
||||
for _, list := range lists {
|
||||
items = append(items, list...)
|
||||
}
|
||||
|
359
src/options.go
359
src/options.go
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
|
||||
"github.com/junegunn/go-shellwords"
|
||||
"github.com/mattn/go-shellwords"
|
||||
)
|
||||
|
||||
const usage = `usage: fzf [options]
|
||||
@@ -54,6 +54,7 @@ const usage = `usage: fzf [options]
|
||||
--min-height=HEIGHT Minimum height when --height is given in percent
|
||||
(default: 10)
|
||||
--reverse Reverse orientation
|
||||
--border Draw border above and below the finder
|
||||
--margin=MARGIN Screen margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
|
||||
--inline-info Display finder info inline with the query
|
||||
--prompt=STR Input prompt (default: '> ')
|
||||
@@ -82,7 +83,10 @@ 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
|
||||
@@ -171,8 +175,7 @@ type Options struct {
|
||||
Filter *string
|
||||
ToggleSort bool
|
||||
Expect map[int]string
|
||||
Keymap map[int]actionType
|
||||
Execmap map[int]string
|
||||
Keymap map[int][]action
|
||||
Preview previewOpts
|
||||
PrintQuery bool
|
||||
ReadZero bool
|
||||
@@ -182,7 +185,9 @@ type Options struct {
|
||||
Header []string
|
||||
HeaderLines int
|
||||
Margin [4]sizeSpec
|
||||
Bordered bool
|
||||
Tabstop int
|
||||
ClearOnExit bool
|
||||
Version bool
|
||||
}
|
||||
|
||||
@@ -220,8 +225,7 @@ func defaultOptions() *Options {
|
||||
Filter: nil,
|
||||
ToggleSort: false,
|
||||
Expect: make(map[int]string),
|
||||
Keymap: make(map[int]actionType),
|
||||
Execmap: make(map[int]string),
|
||||
Keymap: make(map[int][]action),
|
||||
Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false},
|
||||
PrintQuery: false,
|
||||
ReadZero: false,
|
||||
@@ -232,6 +236,7 @@ func defaultOptions() *Options {
|
||||
HeaderLines: 0,
|
||||
Margin: defaultMargin(),
|
||||
Tabstop: 8,
|
||||
ClearOnExit: true,
|
||||
Version: false}
|
||||
}
|
||||
|
||||
@@ -393,8 +398,12 @@ func parseKeyChords(str string, message string) map[int]string {
|
||||
chord = tui.AltZ + int(' ')
|
||||
case "bspace", "bs":
|
||||
chord = tui.BSpace
|
||||
case "ctrl-space":
|
||||
chord = tui.CtrlSpace
|
||||
case "change":
|
||||
chord = tui.Change
|
||||
case "alt-enter", "alt-return":
|
||||
chord = tui.AltEnter
|
||||
chord = tui.CtrlAltM
|
||||
case "alt-space":
|
||||
chord = tui.AltSpace
|
||||
case "alt-/":
|
||||
@@ -430,7 +439,9 @@ func parseKeyChords(str string, message string) map[int]string {
|
||||
case "f12":
|
||||
chord = tui.F12
|
||||
default:
|
||||
if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
|
||||
if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) {
|
||||
chord = tui.CtrlAltA + int(lkey[9]) - 'a'
|
||||
} else if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
|
||||
chord = tui.CtrlA + int(lkey[5]) - 'a'
|
||||
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) {
|
||||
chord = tui.AltA + int(lkey[4]) - 'a'
|
||||
@@ -578,23 +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, str string) {
|
||||
if executeRegexp == nil {
|
||||
// Backreferences are not supported.
|
||||
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
|
||||
executeRegexp = regexp.MustCompile(
|
||||
"(?s):execute(-multi)?:.*|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
|
||||
}
|
||||
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 {
|
||||
if strings.HasPrefix(src, ":execute-multi") {
|
||||
return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")"
|
||||
prefix := ":execute"
|
||||
if src[len(prefix)] == '-' {
|
||||
c := src[len(prefix)+1]
|
||||
if c == 's' || c == 'S' {
|
||||
prefix += "-silent"
|
||||
} else {
|
||||
prefix += "-multi"
|
||||
}
|
||||
}
|
||||
return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")"
|
||||
return prefix + "(" + strings.Repeat(" ", len(src)-len(prefix)-2) + ")"
|
||||
})
|
||||
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
|
||||
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
|
||||
masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1)
|
||||
|
||||
idx := 0
|
||||
for _, pairStr := range strings.Split(masked, ",") {
|
||||
@@ -610,151 +630,178 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
|
||||
key = ':' + tui.AltZ
|
||||
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
|
||||
key = ',' + tui.AltZ
|
||||
} else if len(pair[0]) == 1 && pair[0][0] == escapedPlus {
|
||||
key = '+' + tui.AltZ
|
||||
} else {
|
||||
keys := parseKeyChords(pair[0], "key name required")
|
||||
key = firstKey(keys)
|
||||
}
|
||||
|
||||
act := origPairStr[len(pair[0])+1 : len(origPairStr)]
|
||||
actLower := strings.ToLower(act)
|
||||
switch actLower {
|
||||
case "ignore":
|
||||
keymap[key] = actIgnore
|
||||
case "beginning-of-line":
|
||||
keymap[key] = actBeginningOfLine
|
||||
case "abort":
|
||||
keymap[key] = actAbort
|
||||
case "accept":
|
||||
keymap[key] = actAccept
|
||||
case "print-query":
|
||||
keymap[key] = actPrintQuery
|
||||
case "backward-char":
|
||||
keymap[key] = actBackwardChar
|
||||
case "backward-delete-char":
|
||||
keymap[key] = actBackwardDeleteChar
|
||||
case "backward-word":
|
||||
keymap[key] = actBackwardWord
|
||||
case "clear-screen":
|
||||
keymap[key] = actClearScreen
|
||||
case "delete-char":
|
||||
keymap[key] = actDeleteChar
|
||||
case "delete-char/eof":
|
||||
keymap[key] = actDeleteCharEOF
|
||||
case "end-of-line":
|
||||
keymap[key] = actEndOfLine
|
||||
case "cancel":
|
||||
keymap[key] = actCancel
|
||||
case "forward-char":
|
||||
keymap[key] = actForwardChar
|
||||
case "forward-word":
|
||||
keymap[key] = actForwardWord
|
||||
case "jump":
|
||||
keymap[key] = actJump
|
||||
case "jump-accept":
|
||||
keymap[key] = actJumpAccept
|
||||
case "kill-line":
|
||||
keymap[key] = actKillLine
|
||||
case "kill-word":
|
||||
keymap[key] = actKillWord
|
||||
case "unix-line-discard", "line-discard":
|
||||
keymap[key] = actUnixLineDiscard
|
||||
case "unix-word-rubout", "word-rubout":
|
||||
keymap[key] = actUnixWordRubout
|
||||
case "yank":
|
||||
keymap[key] = actYank
|
||||
case "backward-kill-word":
|
||||
keymap[key] = actBackwardKillWord
|
||||
case "toggle-down":
|
||||
keymap[key] = actToggleDown
|
||||
case "toggle-up":
|
||||
keymap[key] = actToggleUp
|
||||
case "toggle-in":
|
||||
keymap[key] = actToggleIn
|
||||
case "toggle-out":
|
||||
keymap[key] = actToggleOut
|
||||
case "toggle-all":
|
||||
keymap[key] = actToggleAll
|
||||
case "select-all":
|
||||
keymap[key] = actSelectAll
|
||||
case "deselect-all":
|
||||
keymap[key] = actDeselectAll
|
||||
case "toggle":
|
||||
keymap[key] = actToggle
|
||||
case "down":
|
||||
keymap[key] = actDown
|
||||
case "up":
|
||||
keymap[key] = actUp
|
||||
case "page-up":
|
||||
keymap[key] = actPageUp
|
||||
case "page-down":
|
||||
keymap[key] = actPageDown
|
||||
case "half-page-up":
|
||||
keymap[key] = actHalfPageUp
|
||||
case "half-page-down":
|
||||
keymap[key] = actHalfPageDown
|
||||
case "previous-history":
|
||||
keymap[key] = actPreviousHistory
|
||||
case "next-history":
|
||||
keymap[key] = actNextHistory
|
||||
case "toggle-preview":
|
||||
keymap[key] = actTogglePreview
|
||||
case "toggle-sort":
|
||||
keymap[key] = actToggleSort
|
||||
case "preview-up":
|
||||
keymap[key] = actPreviewUp
|
||||
case "preview-down":
|
||||
keymap[key] = actPreviewDown
|
||||
case "preview-page-up":
|
||||
keymap[key] = actPreviewPageUp
|
||||
case "preview-page-down":
|
||||
keymap[key] = actPreviewPageDown
|
||||
default:
|
||||
if isExecuteAction(actLower) {
|
||||
var offset int
|
||||
if strings.HasPrefix(actLower, "execute-multi") {
|
||||
keymap[key] = actExecuteMulti
|
||||
offset = len("execute-multi")
|
||||
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 {
|
||||
keymap[key] = actExecute
|
||||
offset = len("execute")
|
||||
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]})
|
||||
}
|
||||
}
|
||||
if act[offset] == ':' {
|
||||
execmap[key] = act[offset+1:]
|
||||
} else {
|
||||
execmap[key] = act[offset+1 : len(act)-1]
|
||||
}
|
||||
} else {
|
||||
errorExit("unknown action: " + act)
|
||||
}
|
||||
prevSpec = ""
|
||||
}
|
||||
keymap[key] = actions
|
||||
}
|
||||
}
|
||||
|
||||
func isExecuteAction(str string) bool {
|
||||
if !strings.HasPrefix(str, "execute") || len(str) < len("execute()") {
|
||||
return false
|
||||
func isExecuteAction(str string) actionType {
|
||||
matches := executeRegexp.FindAllStringSubmatch(":"+str, -1)
|
||||
if matches == nil || len(matches) != 1 {
|
||||
return actIgnore
|
||||
}
|
||||
b := str[len("execute")]
|
||||
if strings.HasPrefix(str, "execute-multi") {
|
||||
if len(str) < len("execute-multi()") {
|
||||
return false
|
||||
}
|
||||
b = str[len("execute-multi")]
|
||||
prefix := matches[0][1]
|
||||
if len(prefix) == 0 {
|
||||
prefix = matches[0][2]
|
||||
}
|
||||
e := str[len(str)-1]
|
||||
if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' ||
|
||||
b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") {
|
||||
return true
|
||||
switch prefix {
|
||||
case "execute":
|
||||
return actExecute
|
||||
case "execute-silent":
|
||||
return actExecuteSilent
|
||||
case "execute-multi":
|
||||
return actExecuteMulti
|
||||
}
|
||||
return false
|
||||
return actIgnore
|
||||
}
|
||||
|
||||
func parseToggleSort(keymap map[int]actionType, str string) {
|
||||
func parseToggleSort(keymap map[int][]action, str string) {
|
||||
keys := parseKeyChords(str, "key name required")
|
||||
if len(keys) != 1 {
|
||||
errorExit("multiple keys specified")
|
||||
}
|
||||
keymap[firstKey(keys)] = actToggleSort
|
||||
keymap[firstKey(keys)] = toActions(actToggleSort)
|
||||
}
|
||||
|
||||
func strLines(str string) []string {
|
||||
@@ -801,7 +848,7 @@ func parsePreviewWindow(opts *previewOpts, input string) {
|
||||
opts.wrap = false
|
||||
|
||||
tokens := strings.Split(input, ":")
|
||||
sizeRegex := regexp.MustCompile("^[1-9][0-9]*%?$")
|
||||
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
|
||||
for _, token := range tokens {
|
||||
switch token {
|
||||
case "hidden":
|
||||
@@ -916,10 +963,12 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.FuzzyAlgo = parseAlgo(nextString(allArgs, &i, "algorithm required (v1|v2)"))
|
||||
case "--expect":
|
||||
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
|
||||
case "--no-expect":
|
||||
opts.Expect = make(map[int]string)
|
||||
case "--tiebreak":
|
||||
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
|
||||
case "--bind":
|
||||
parseKeymap(opts.Keymap, opts.Execmap, nextString(allArgs, &i, "bind expression required"))
|
||||
parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required"))
|
||||
case "--color":
|
||||
spec := optionalNextString(allArgs, &i)
|
||||
if len(spec) == 0 {
|
||||
@@ -1052,11 +1101,19 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.Height = sizeSpec{}
|
||||
case "--no-margin":
|
||||
opts.Margin = defaultMargin()
|
||||
case "--no-border":
|
||||
opts.Bordered = false
|
||||
case "--border":
|
||||
opts.Bordered = true
|
||||
case "--margin":
|
||||
opts.Margin = parseMargin(
|
||||
nextString(allArgs, &i, "margin required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
|
||||
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:
|
||||
@@ -1089,7 +1146,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
} else if match, value := optString(arg, "--color="); match {
|
||||
opts.Theme = parseTheme(opts.Theme, value)
|
||||
} else if match, value := optString(arg, "--bind="); match {
|
||||
parseKeymap(opts.Keymap, opts.Execmap, value)
|
||||
parseKeymap(opts.Keymap, value)
|
||||
} else if match, value := optString(arg, "--history="); match {
|
||||
setHistory(value)
|
||||
} else if match, value := optString(arg, "--history-size="); match {
|
||||
@@ -1145,20 +1202,22 @@ func postProcessOptions(opts *Options) {
|
||||
// Default actions for CTRL-N / CTRL-P when --history is set
|
||||
if opts.History != nil {
|
||||
if _, prs := opts.Keymap[tui.CtrlP]; !prs {
|
||||
opts.Keymap[tui.CtrlP] = actPreviousHistory
|
||||
opts.Keymap[tui.CtrlP] = toActions(actPreviousHistory)
|
||||
}
|
||||
if _, prs := opts.Keymap[tui.CtrlN]; !prs {
|
||||
opts.Keymap[tui.CtrlN] = actNextHistory
|
||||
opts.Keymap[tui.CtrlN] = toActions(actNextHistory)
|
||||
}
|
||||
}
|
||||
|
||||
// Extend the default key map
|
||||
keymap := defaultKeymap()
|
||||
for key, act := range opts.Keymap {
|
||||
if act == actToggleSort {
|
||||
opts.ToggleSort = true
|
||||
for key, actions := range opts.Keymap {
|
||||
for _, act := range actions {
|
||||
if act.t == actToggleSort {
|
||||
opts.ToggleSort = true
|
||||
}
|
||||
}
|
||||
keymap[key] = act
|
||||
keymap[key] = actions
|
||||
}
|
||||
opts.Keymap = keymap
|
||||
|
||||
|
@@ -6,7 +6,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func TestDelimiterRegex(t *testing.T) {
|
||||
@@ -44,7 +43,7 @@ func TestDelimiterRegex(t *testing.T) {
|
||||
|
||||
func TestDelimiterRegexString(t *testing.T) {
|
||||
delim := delimiterRegexp("*")
|
||||
tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
|
||||
tokens := Tokenize("-*--*---**---", delim)
|
||||
if delim.regex != nil ||
|
||||
tokens[0].text.ToString() != "-*" ||
|
||||
tokens[1].text.ToString() != "--*" ||
|
||||
@@ -57,7 +56,7 @@ func TestDelimiterRegexString(t *testing.T) {
|
||||
|
||||
func TestDelimiterRegexRegex(t *testing.T) {
|
||||
delim := delimiterRegexp("--\\*")
|
||||
tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
|
||||
tokens := Tokenize("-*--*---**---", delim)
|
||||
if delim.str != nil ||
|
||||
tokens[0].text.ToString() != "-*--*" ||
|
||||
tokens[1].text.ToString() != "---*" ||
|
||||
@@ -125,14 +124,14 @@ func TestIrrelevantNth(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseKeys(t *testing.T) {
|
||||
pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ALT-enter,alt-SPACE", "")
|
||||
pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
|
||||
check := func(i int, s string) {
|
||||
if pairs[i] != s {
|
||||
t.Errorf("%s != %s", pairs[i], s)
|
||||
}
|
||||
}
|
||||
if len(pairs) != 11 {
|
||||
t.Error(11)
|
||||
if len(pairs) != 12 {
|
||||
t.Error(12)
|
||||
}
|
||||
check(tui.CtrlZ, "ctrl-z")
|
||||
check(tui.AltZ, "alt-z")
|
||||
@@ -143,7 +142,8 @@ func TestParseKeys(t *testing.T) {
|
||||
check(tui.CtrlA+'g'-'a', "ctrl-G")
|
||||
check(tui.AltZ+'J', "J")
|
||||
check(tui.AltZ+'g', "g")
|
||||
check(tui.AltEnter, "ALT-enter")
|
||||
check(tui.CtrlAltA, "ctrl-alt-a")
|
||||
check(tui.CtrlAltM, "ALT-enter")
|
||||
check(tui.AltSpace, "alt-SPACE")
|
||||
|
||||
// Synonyms
|
||||
@@ -225,49 +225,51 @@ func TestParseKeysWithComma(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
check := func(action actionType, expected actionType) {
|
||||
if action != expected {
|
||||
t.Errorf("%d != %d", action, expected)
|
||||
}
|
||||
}
|
||||
checkString := func(action string, expected string) {
|
||||
if action != expected {
|
||||
t.Errorf("%d != %d", action, expected)
|
||||
}
|
||||
}
|
||||
keymap := defaultKeymap()
|
||||
execmap := make(map[int]string)
|
||||
check(actBeginningOfLine, keymap[tui.CtrlA])
|
||||
parseKeymap(keymap, execmap,
|
||||
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+
|
||||
"f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
|
||||
"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+
|
||||
",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)")
|
||||
check(actKillLine, keymap[tui.CtrlA])
|
||||
check(actToggleSort, keymap[tui.CtrlB])
|
||||
check(actPageUp, keymap[tui.AltZ+'c'])
|
||||
check(actAbort, keymap[tui.AltZ+','])
|
||||
check(actAccept, keymap[tui.AltZ+':'])
|
||||
check(actPageDown, keymap[tui.AltZ])
|
||||
check(actExecute, keymap[tui.F1])
|
||||
check(actExecute, keymap[tui.F2])
|
||||
check(actExecute, keymap[tui.F3])
|
||||
check(actExecute, keymap[tui.F4])
|
||||
checkString("ls {}", execmap[tui.F1])
|
||||
checkString("echo {}, {}, {}", execmap[tui.F2])
|
||||
checkString("echo '({})'", execmap[tui.F3])
|
||||
checkString("less {}", execmap[tui.F4])
|
||||
checkString("echo (,),[,],/,:,;,%,{}", execmap[tui.AltA])
|
||||
checkString("echo (,),[,],/,:,@,%,{}", execmap[tui.AltB])
|
||||
checkString("\nfoobar,Y:execute(baz)", execmap[tui.AltZ+'X'])
|
||||
check := func(keyName int, arg1 string, types ...actionType) {
|
||||
if len(keymap[keyName]) != len(types) {
|
||||
t.Errorf("invalid number of actions (%d != %d)", len(types), len(keymap[keyName]))
|
||||
return
|
||||
}
|
||||
for idx, action := range keymap[keyName] {
|
||||
if types[idx] != action.t {
|
||||
t.Errorf("invalid action type (%d != %d)", types[idx], action.t)
|
||||
}
|
||||
}
|
||||
if len(arg1) > 0 && keymap[keyName][0].a != arg1 {
|
||||
t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[keyName][0].a)
|
||||
}
|
||||
}
|
||||
check(tui.CtrlA, "", actBeginningOfLine)
|
||||
parseKeymap(keymap,
|
||||
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
|
||||
"f1:execute(ls {})+abort,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
|
||||
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
|
||||
"x:Execute(foo+bar),X:execute/bar+baz/"+
|
||||
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
|
||||
check(tui.CtrlA, "", actKillLine)
|
||||
check(tui.CtrlB, "", actToggleSort, actUp, actDown)
|
||||
check(tui.AltZ+'c', "", actPageUp)
|
||||
check(tui.AltZ+',', "", actAbort)
|
||||
check(tui.AltZ+':', "", actAccept)
|
||||
check(tui.AltZ, "", actPageDown)
|
||||
check(tui.F1, "ls {}", actExecute, actAbort)
|
||||
check(tui.F2, "echo {}, {}, {}", actExecute)
|
||||
check(tui.F3, "echo '({})'", actExecute)
|
||||
check(tui.F4, "less {}", actExecute)
|
||||
check(tui.AltZ+'x', "foo+bar", actExecute)
|
||||
check(tui.AltZ+'X', "bar+baz", actExecute)
|
||||
check(tui.AltA, "echo (,),[,],/,:,;,%,{}", actExecuteMulti)
|
||||
check(tui.AltB, "echo (,),[,],/,:,@,%,{}", actExecute)
|
||||
check(tui.AltZ+'+', "++\nfoobar,Y:execute(baz)+up", actExecute)
|
||||
|
||||
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
|
||||
parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
|
||||
checkString("foobar", execmap[tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])])
|
||||
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
|
||||
check(tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
|
||||
}
|
||||
|
||||
parseKeymap(keymap, execmap, "f1:abort")
|
||||
check(actAbort, keymap[tui.F1])
|
||||
parseKeymap(keymap, "f1:abort")
|
||||
check(tui.F1, "", actAbort)
|
||||
}
|
||||
|
||||
func TestColorSpec(t *testing.T) {
|
||||
@@ -327,7 +329,7 @@ func TestDefaultCtrlNP(t *testing.T) {
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
postProcessOptions(opts)
|
||||
if opts.Keymap[key] != expected {
|
||||
if opts.Keymap[key][0].t != expected {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
@@ -101,7 +101,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
||||
for idx, term := range termSet {
|
||||
// If the query contains inverse search terms or OR operators,
|
||||
// we cannot cache the search scope
|
||||
if !cacheable || idx > 0 || term.inv {
|
||||
if !cacheable || idx > 0 || term.inv || !fuzzy && term.typ != termExact {
|
||||
cacheable = false
|
||||
break Loop
|
||||
}
|
||||
@@ -243,31 +243,17 @@ func (p *Pattern) CacheKey() string {
|
||||
}
|
||||
|
||||
// Match returns the list of matches Items in the given Chunk
|
||||
func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []*Result {
|
||||
func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
|
||||
// ChunkCache: Exact match
|
||||
cacheKey := p.CacheKey()
|
||||
if p.cacheable {
|
||||
if cached, found := _cache.Find(chunk, cacheKey); found {
|
||||
if cached := _cache.Lookup(chunk, cacheKey); cached != nil {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix/suffix cache
|
||||
var space []*Result
|
||||
Loop:
|
||||
for idx := 1; idx < len(cacheKey); idx++ {
|
||||
// [---------| ] | [ |---------]
|
||||
// [--------| ] | [ |--------]
|
||||
// [-------| ] | [ |-------]
|
||||
prefix := cacheKey[:len(cacheKey)-idx]
|
||||
suffix := cacheKey[idx:]
|
||||
for _, substr := range [2]*string{&prefix, &suffix} {
|
||||
if cached, found := _cache.Find(chunk, *substr); found {
|
||||
space = cached
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
}
|
||||
space := _cache.Search(chunk, cacheKey)
|
||||
|
||||
matches := p.matchChunk(chunk, space, slab)
|
||||
|
||||
@@ -277,19 +263,19 @@ Loop:
|
||||
return matches
|
||||
}
|
||||
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []*Result {
|
||||
matches := []*Result{}
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||
matches := []Result{}
|
||||
|
||||
if space == nil {
|
||||
for _, item := range *chunk {
|
||||
if match, _, _ := p.MatchItem(item, false, slab); match != nil {
|
||||
matches = append(matches, match)
|
||||
for idx := range *chunk {
|
||||
if match, _, _ := p.MatchItem(&(*chunk)[idx], false, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, result := range space {
|
||||
if match, _, _ := p.MatchItem(result.item, false, slab); match != nil {
|
||||
matches = append(matches, match)
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,20 +285,22 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []*
|
||||
// MatchItem returns true if the Item is a match
|
||||
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
|
||||
if p.extended {
|
||||
if offsets, bonus, trimLen, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
|
||||
return buildResult(item, offsets, bonus, trimLen), offsets, pos
|
||||
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
|
||||
result := buildResult(item, offsets, bonus)
|
||||
return &result, offsets, pos
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
offset, bonus, trimLen, pos := p.basicMatch(item, withPos, slab)
|
||||
offset, bonus, pos := p.basicMatch(item, withPos, slab)
|
||||
if sidx := offset[0]; sidx >= 0 {
|
||||
offsets := []Offset{offset}
|
||||
return buildResult(item, offsets, bonus, trimLen), offsets, pos
|
||||
result := buildResult(item, offsets, bonus)
|
||||
return &result, offsets, pos
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) {
|
||||
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
||||
input := p.prepareInput(item)
|
||||
if p.fuzzy {
|
||||
return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
|
||||
@@ -320,11 +308,10 @@ func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset,
|
||||
return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
|
||||
}
|
||||
|
||||
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, int, *[]int) {
|
||||
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) {
|
||||
input := p.prepareInput(item)
|
||||
offsets := []Offset{}
|
||||
var totalScore int
|
||||
var totalTrimLen int
|
||||
var allPos *[]int
|
||||
if withPos {
|
||||
allPos = &[]int{}
|
||||
@@ -332,16 +319,15 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
|
||||
for _, termSet := range p.termSets {
|
||||
var offset Offset
|
||||
var currentScore int
|
||||
var trimLen int
|
||||
matched := false
|
||||
for _, term := range termSet {
|
||||
pfun := p.procFun[term.typ]
|
||||
off, score, tLen, pos := p.iter(pfun, input, term.caseSensitive, p.normalize, p.forward, term.text, withPos, slab)
|
||||
off, score, pos := p.iter(pfun, input, term.caseSensitive, p.normalize, p.forward, term.text, withPos, slab)
|
||||
if sidx := off[0]; sidx >= 0 {
|
||||
if term.inv {
|
||||
continue
|
||||
}
|
||||
offset, currentScore, trimLen = off, score, tLen
|
||||
offset, currentScore = off, score
|
||||
matched = true
|
||||
if withPos {
|
||||
if pos != nil {
|
||||
@@ -354,7 +340,7 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
|
||||
}
|
||||
break
|
||||
} else if term.inv {
|
||||
offset, currentScore, trimLen = Offset{0, 0}, 0, 0
|
||||
offset, currentScore = Offset{0, 0}, 0
|
||||
matched = true
|
||||
continue
|
||||
}
|
||||
@@ -362,29 +348,27 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
|
||||
if matched {
|
||||
offsets = append(offsets, offset)
|
||||
totalScore += currentScore
|
||||
totalTrimLen += trimLen
|
||||
}
|
||||
}
|
||||
return offsets, totalScore, totalTrimLen, allPos
|
||||
return offsets, totalScore, allPos
|
||||
}
|
||||
|
||||
func (p *Pattern) prepareInput(item *Item) []Token {
|
||||
if item.transformed != nil {
|
||||
return item.transformed
|
||||
if len(p.nth) == 0 {
|
||||
return []Token{Token{text: &item.text, prefixLength: 0}}
|
||||
}
|
||||
|
||||
var ret []Token
|
||||
if len(p.nth) == 0 {
|
||||
ret = []Token{Token{text: &item.text, prefixLength: 0, trimLength: int32(item.text.TrimLength())}}
|
||||
} else {
|
||||
tokens := Tokenize(item.text, p.delimiter)
|
||||
ret = Transform(tokens, p.nth)
|
||||
if item.transformed != nil {
|
||||
return *item.transformed
|
||||
}
|
||||
item.transformed = ret
|
||||
|
||||
tokens := Tokenize(item.text.ToString(), p.delimiter)
|
||||
ret := Transform(tokens, p.nth)
|
||||
item.transformed = &ret
|
||||
return ret
|
||||
}
|
||||
|
||||
func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, int, *[]int) {
|
||||
func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
||||
for _, part := range tokens {
|
||||
if res, pos := pfun(caseSensitive, normalize, forward, *part.text, pattern, withPos, slab); res.Start >= 0 {
|
||||
sidx := int32(res.Start) + part.prefixLength
|
||||
@@ -394,8 +378,8 @@ func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, norma
|
||||
(*pos)[idx] += int(part.prefixLength)
|
||||
}
|
||||
}
|
||||
return Offset{sidx, eidx}, res.Score, int(part.trimLength), pos
|
||||
return Offset{sidx, eidx}, res.Score, pos
|
||||
}
|
||||
}
|
||||
return Offset{-1, -1}, 0, -1, nil
|
||||
return Offset{-1, -1}, 0, nil
|
||||
}
|
||||
|
@@ -78,7 +78,7 @@ func TestExact(t *testing.T) {
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true,
|
||||
[]Range{}, Delimiter{}, []rune("'abc"))
|
||||
res, pos := algo.ExactMatchNaive(
|
||||
pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text, true, nil)
|
||||
pattern.caseSensitive, pattern.normalize, pattern.forward, util.ToChars([]byte("aabbcc abc")), pattern.termSets[0][0].text, true, nil)
|
||||
if res.Start != 7 || res.End != 10 {
|
||||
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func TestEqual(t *testing.T) {
|
||||
|
||||
match := func(str string, sidxExpected int, eidxExpected int) {
|
||||
res, pos := algo.EqualMatch(
|
||||
pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text, true, nil)
|
||||
pattern.caseSensitive, pattern.normalize, pattern.forward, util.ToChars([]byte(str)), pattern.termSets[0][0].text, true, nil)
|
||||
if res.Start != sidxExpected || res.End != eidxExpected {
|
||||
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
|
||||
}
|
||||
@@ -133,30 +133,30 @@ func TestCaseSensitivity(t *testing.T) {
|
||||
|
||||
func TestOrigTextAndTransformed(t *testing.T) {
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
|
||||
tokens := Tokenize(util.RunesToChars([]rune("junegunn")), Delimiter{})
|
||||
tokens := Tokenize("junegunn", Delimiter{})
|
||||
trans := Transform(tokens, []Range{Range{1, 1}})
|
||||
|
||||
origBytes := []byte("junegunn.choi")
|
||||
for _, extended := range []bool{false, true} {
|
||||
chunk := Chunk{
|
||||
&Item{
|
||||
text: util.RunesToChars([]rune("junegunn")),
|
||||
Item{
|
||||
text: util.ToChars([]byte("junegunn")),
|
||||
origText: &origBytes,
|
||||
transformed: trans},
|
||||
transformed: &trans},
|
||||
}
|
||||
pattern.extended = extended
|
||||
matches := pattern.matchChunk(&chunk, nil, slab) // No cache
|
||||
if !(matches[0].item.text.ToString() == "junegunn" &&
|
||||
string(*matches[0].item.origText) == "junegunn.choi" &&
|
||||
reflect.DeepEqual(matches[0].item.transformed, trans)) {
|
||||
reflect.DeepEqual(*matches[0].item.transformed, trans)) {
|
||||
t.Error("Invalid match result", matches)
|
||||
}
|
||||
|
||||
match, offsets, pos := pattern.MatchItem(chunk[0], true, slab)
|
||||
match, offsets, pos := pattern.MatchItem(&chunk[0], true, slab)
|
||||
if !(match.item.text.ToString() == "junegunn" &&
|
||||
string(*match.item.origText) == "junegunn.choi" &&
|
||||
offsets[0][0] == 0 && offsets[0][1] == 5 &&
|
||||
reflect.DeepEqual(match.item.transformed, trans)) {
|
||||
reflect.DeepEqual(*match.item.transformed, trans)) {
|
||||
t.Error("Invalid match result", match, offsets, extended)
|
||||
}
|
||||
if !((*pos)[0] == 4 && (*pos)[1] == 0) {
|
||||
@@ -186,3 +186,21 @@ func TestCacheKey(t *testing.T) {
|
||||
test(true, "foo | bar !baz", "", false)
|
||||
test(true, "| | | foo", "foo", true)
|
||||
}
|
||||
|
||||
func TestCacheable(t *testing.T) {
|
||||
test := func(fuzzy bool, str string, cacheable bool) {
|
||||
clearPatternCache()
|
||||
pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, true, []Range{}, Delimiter{}, []rune(str))
|
||||
if cacheable != pat.cacheable {
|
||||
t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable)
|
||||
}
|
||||
}
|
||||
test(true, "foo bar", true)
|
||||
test(true, "foo 'bar", true)
|
||||
test(true, "foo !bar", false)
|
||||
|
||||
test(false, "foo bar", true)
|
||||
test(false, "foo '", true)
|
||||
test(false, "foo 'bar", false)
|
||||
test(false, "foo !bar", false)
|
||||
}
|
||||
|
@@ -17,16 +17,17 @@ type Reader struct {
|
||||
|
||||
// ReadSource reads data from the default command or from standard input
|
||||
func (r *Reader) ReadSource() {
|
||||
var success bool
|
||||
if util.IsTty() {
|
||||
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
|
||||
if len(cmd) == 0 {
|
||||
cmd = defaultCommand
|
||||
}
|
||||
r.readFromCommand(cmd)
|
||||
success = r.readFromCommand(cmd)
|
||||
} else {
|
||||
r.readFromStdin()
|
||||
success = r.readFromStdin()
|
||||
}
|
||||
r.eventBox.Set(EvtReadFin, nil)
|
||||
r.eventBox.Set(EvtReadFin, success)
|
||||
}
|
||||
|
||||
func (r *Reader) feed(src io.Reader) {
|
||||
@@ -50,7 +51,7 @@ func (r *Reader) feed(src io.Reader) {
|
||||
}
|
||||
}
|
||||
if r.pusher(bytea) {
|
||||
r.eventBox.Set(EvtReadNew, nil)
|
||||
r.eventBox.Set(EvtReadNew, true)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
@@ -59,20 +60,21 @@ func (r *Reader) feed(src io.Reader) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) readFromStdin() {
|
||||
func (r *Reader) readFromStdin() bool {
|
||||
r.feed(os.Stdin)
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Reader) readFromCommand(cmd string) {
|
||||
func (r *Reader) readFromCommand(cmd string) bool {
|
||||
listCommand := util.ExecCommand(cmd)
|
||||
out, err := listCommand.StdoutPipe()
|
||||
if err != nil {
|
||||
return
|
||||
return false
|
||||
}
|
||||
err = listCommand.Start()
|
||||
if err != nil {
|
||||
return
|
||||
return false
|
||||
}
|
||||
defer listCommand.Wait()
|
||||
r.feed(out)
|
||||
return listCommand.Wait() == nil
|
||||
}
|
||||
|
@@ -19,30 +19,27 @@ type colorOffset struct {
|
||||
index int32
|
||||
}
|
||||
|
||||
type rank struct {
|
||||
points [4]uint16
|
||||
index int32
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
item *Item
|
||||
rank rank
|
||||
item *Item
|
||||
points [4]uint16
|
||||
}
|
||||
|
||||
func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result {
|
||||
func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
if len(offsets) > 1 {
|
||||
sort.Sort(ByOrder(offsets))
|
||||
}
|
||||
|
||||
result := Result{item: item, rank: rank{index: item.index}}
|
||||
result := Result{item: item}
|
||||
numChars := item.text.Length()
|
||||
minBegin := math.MaxUint16
|
||||
minEnd := math.MaxUint16
|
||||
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
|
||||
}
|
||||
@@ -55,8 +52,7 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result {
|
||||
// Higher is better
|
||||
val = math.MaxUint16 - util.AsUint16(score)
|
||||
case byLength:
|
||||
// If offsets is empty, trimLen will be 0, but we don't care
|
||||
val = util.AsUint16(trimLen)
|
||||
val = item.TrimLength()
|
||||
case byBegin, byEnd:
|
||||
if validOffsetFound {
|
||||
whitePrefixLen := 0
|
||||
@@ -68,16 +64,16 @@ func buildResult(item *Item, offsets []Offset, score int, trimLen int) *Result {
|
||||
}
|
||||
}
|
||||
if criterion == byBegin {
|
||||
val = util.AsUint16(minBegin - whitePrefixLen)
|
||||
val = util.AsUint16(minEnd - whitePrefixLen)
|
||||
} else {
|
||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/trimLen)
|
||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()))
|
||||
}
|
||||
}
|
||||
}
|
||||
result.rank.points[idx] = val
|
||||
result.points[idx] = val
|
||||
}
|
||||
|
||||
return &result
|
||||
return result
|
||||
}
|
||||
|
||||
// Sort criteria to use. Never changes once fzf is started.
|
||||
@@ -85,11 +81,11 @@ var sortCriteria []criterion
|
||||
|
||||
// Index returns ordinal index of the Item
|
||||
func (result *Result) Index() int32 {
|
||||
return result.item.index
|
||||
return result.item.Index()
|
||||
}
|
||||
|
||||
func minRank() rank {
|
||||
return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
||||
func minRank() Result {
|
||||
return Result{item: &nilItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
||||
}
|
||||
|
||||
func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset {
|
||||
@@ -200,7 +196,7 @@ func (a ByOrder) Less(i, j int) bool {
|
||||
}
|
||||
|
||||
// ByRelevance is for sorting Items
|
||||
type ByRelevance []*Result
|
||||
type ByRelevance []Result
|
||||
|
||||
func (a ByRelevance) Len() int {
|
||||
return len(a)
|
||||
@@ -211,11 +207,11 @@ func (a ByRelevance) Swap(i, j int) {
|
||||
}
|
||||
|
||||
func (a ByRelevance) Less(i, j int) bool {
|
||||
return compareRanks((*a[i]).rank, (*a[j]).rank, false)
|
||||
return compareRanks(a[i], a[j], false)
|
||||
}
|
||||
|
||||
// ByRelevanceTac is for sorting Items
|
||||
type ByRelevanceTac []*Result
|
||||
type ByRelevanceTac []Result
|
||||
|
||||
func (a ByRelevanceTac) Len() int {
|
||||
return len(a)
|
||||
@@ -226,10 +222,10 @@ func (a ByRelevanceTac) Swap(i, j int) {
|
||||
}
|
||||
|
||||
func (a ByRelevanceTac) Less(i, j int) bool {
|
||||
return compareRanks((*a[i]).rank, (*a[j]).rank, true)
|
||||
return compareRanks(a[i], a[j], true)
|
||||
}
|
||||
|
||||
func compareRanks(irank rank, jrank rank, tac bool) bool {
|
||||
func compareRanks(irank Result, jrank Result, tac bool) bool {
|
||||
for idx := 0; idx < 4; idx++ {
|
||||
left := irank.points[idx]
|
||||
right := jrank.points[idx]
|
||||
@@ -239,5 +235,5 @@ func compareRanks(irank rank, jrank rank, tac bool) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return (irank.index <= jrank.index) != tac
|
||||
return (irank.item.Index() <= jrank.item.Index()) != tac
|
||||
}
|
||||
|
@@ -11,6 +11,11 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func withIndex(i *Item, index int) *Item {
|
||||
(*i).text.Index = int32(index)
|
||||
return i
|
||||
}
|
||||
|
||||
func TestOffsetSort(t *testing.T) {
|
||||
offsets := []Offset{
|
||||
Offset{3, 5}, Offset{2, 7},
|
||||
@@ -26,10 +31,10 @@ func TestOffsetSort(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRankComparison(t *testing.T) {
|
||||
rank := func(vals ...uint16) rank {
|
||||
return rank{
|
||||
rank := func(vals ...uint16) Result {
|
||||
return Result{
|
||||
points: [4]uint16{vals[0], vals[1], vals[2], vals[3]},
|
||||
index: int32(vals[4])}
|
||||
item: &Item{text: util.Chars{Index: int32(vals[4])}}}
|
||||
}
|
||||
if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) ||
|
||||
!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
|
||||
@@ -52,36 +57,41 @@ func TestResultRank(t *testing.T) {
|
||||
sortCriteria = []criterion{byScore, byLength}
|
||||
|
||||
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
|
||||
item1 := buildResult(&Item{text: util.RunesToChars(strs[0]), index: 1}, []Offset{}, 2, 3)
|
||||
if item1.rank.points[0] != math.MaxUint16-2 || // Bonus
|
||||
item1.rank.points[1] != 3 || // Length
|
||||
item1.rank.points[2] != 0 || // Unused
|
||||
item1.rank.points[3] != 0 || // Unused
|
||||
item1.item.index != 1 {
|
||||
t.Error(item1.rank)
|
||||
item1 := buildResult(
|
||||
withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2)
|
||||
if item1.points[0] != math.MaxUint16-2 || // Bonus
|
||||
item1.points[1] != 3 || // Length
|
||||
item1.points[2] != 0 || // Unused
|
||||
item1.points[3] != 0 || // Unused
|
||||
item1.item.Index() != 1 {
|
||||
t.Error(item1)
|
||||
}
|
||||
// Only differ in index
|
||||
item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2, 3)
|
||||
item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2)
|
||||
|
||||
items := []*Result{item1, item2}
|
||||
items := []Result{item1, item2}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if items[0] != item2 || items[1] != item1 {
|
||||
t.Error(items)
|
||||
}
|
||||
|
||||
items = []*Result{item2, item1, item1, item2}
|
||||
items = []Result{item2, item1, item1, item2}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if items[0] != item2 || items[1] != item2 ||
|
||||
items[2] != item1 || items[3] != item1 {
|
||||
t.Error(items, item1, item1.item.index, item2, item2.item.index)
|
||||
t.Error(items, item1, item1.item.Index(), item2, item2.item.Index())
|
||||
}
|
||||
|
||||
// Sort by relevance
|
||||
item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 3, 0)
|
||||
item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 4, 0)
|
||||
item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 5, 0)
|
||||
item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 6, 0)
|
||||
items = []*Result{item1, item2, item3, item4, item5, item6}
|
||||
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 &&
|
||||
|
567
src/terminal.go
567
src/terminal.go
File diff suppressed because it is too large
Load Diff
@@ -10,12 +10,14 @@ import (
|
||||
func newItem(str string) *Item {
|
||||
bytes := []byte(str)
|
||||
trimmed, _, _ := extractColor(str, nil, nil)
|
||||
return &Item{origText: &bytes, text: util.RunesToChars([]rune(trimmed))}
|
||||
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
|
||||
}
|
||||
|
||||
func TestReplacePlaceholder(t *testing.T) {
|
||||
items1 := []*Item{newItem(" foo'bar \x1b[31mbaz\x1b[m")}
|
||||
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
||||
items1 := []*Item{item1, item1}
|
||||
items2 := []*Item{
|
||||
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
|
||||
|
||||
@@ -27,47 +29,65 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
}
|
||||
|
||||
// {}, preserve ansi
|
||||
result = replacePlaceholder("echo {}", false, Delimiter{}, "query", items1)
|
||||
result = replacePlaceholder("echo {}", false, Delimiter{}, false, "query", items1)
|
||||
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
|
||||
|
||||
// {}, strip ansi
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items1)
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz'")
|
||||
|
||||
// {}, with multiple items
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, "query", items2)
|
||||
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items2)
|
||||
check("echo 'foo'\\''bar baz'")
|
||||
|
||||
// {..}, strip leading whitespaces, preserve ansi
|
||||
result = replacePlaceholder("echo {..}", false, Delimiter{}, "query", items1)
|
||||
result = replacePlaceholder("echo {..}", false, Delimiter{}, false, "query", items1)
|
||||
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
|
||||
|
||||
// {..}, strip leading whitespaces, strip ansi
|
||||
result = replacePlaceholder("echo {..}", true, Delimiter{}, "query", items1)
|
||||
result = replacePlaceholder("echo {..}", true, Delimiter{}, false, "query", items1)
|
||||
check("echo 'foo'\\''bar baz'")
|
||||
|
||||
// {q}
|
||||
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, "query", items1)
|
||||
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz' 'query'")
|
||||
|
||||
// {q}, multiple items
|
||||
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, "query 'string'", items2)
|
||||
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, false, "query 'string'", items2)
|
||||
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
|
||||
|
||||
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "query", items1)
|
||||
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, false, "query 'string'", items2)
|
||||
check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'")
|
||||
|
||||
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items1)
|
||||
check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
|
||||
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, "query", items2)
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, false, "query", items2)
|
||||
check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
|
||||
|
||||
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, false, "query", items2)
|
||||
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
|
||||
|
||||
// forcePlus
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, true, "query", items2)
|
||||
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
|
||||
|
||||
// No match
|
||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, nil})
|
||||
check("echo /")
|
||||
|
||||
// No match, but with selections
|
||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, item1})
|
||||
check("echo /' foo'\\''bar baz'")
|
||||
|
||||
// String delimiter
|
||||
delim := "'"
|
||||
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, "query", items1)
|
||||
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz'/'foo'/'bar baz'")
|
||||
|
||||
// Regex delimiter
|
||||
regex := regexp.MustCompile("[oa]+")
|
||||
// foo'bar baz
|
||||
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, "query", items1)
|
||||
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1)
|
||||
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
|
||||
}
|
||||
|
@@ -11,3 +11,11 @@ import (
|
||||
func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||
signal.Notify(resizeChan, syscall.SIGWINCH)
|
||||
}
|
||||
|
||||
func notifyStop(p *os.Process) {
|
||||
p.Signal(syscall.SIGSTOP)
|
||||
}
|
||||
|
||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||
signal.Notify(resizeChan, syscall.SIGCONT)
|
||||
}
|
||||
|
@@ -9,3 +9,11 @@ import (
|
||||
func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func notifyStop(p *os.Process) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||
// NOOP
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -20,7 +21,6 @@ type Range struct {
|
||||
type Token struct {
|
||||
text *util.Chars
|
||||
prefixLength int32
|
||||
trimLength int32
|
||||
}
|
||||
|
||||
// Delimiter for tokenizing the input
|
||||
@@ -75,14 +75,14 @@ func ParseRange(str *string) (Range, bool) {
|
||||
return newRange(n, n), true
|
||||
}
|
||||
|
||||
func withPrefixLengths(tokens []util.Chars, begin int) []Token {
|
||||
func withPrefixLengths(tokens []string, begin int) []Token {
|
||||
ret := make([]Token, len(tokens))
|
||||
|
||||
prefixLength := begin
|
||||
for idx, token := range tokens {
|
||||
// NOTE: &tokens[idx] instead of &tokens
|
||||
ret[idx] = Token{&tokens[idx], int32(prefixLength), int32(token.TrimLength())}
|
||||
prefixLength += token.Length()
|
||||
for idx := range tokens {
|
||||
chars := util.ToChars([]byte(tokens[idx]))
|
||||
ret[idx] = Token{&chars, int32(prefixLength)}
|
||||
prefixLength += chars.Length()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -93,16 +93,15 @@ const (
|
||||
awkWhite
|
||||
)
|
||||
|
||||
func awkTokenizer(input util.Chars) ([]util.Chars, int) {
|
||||
func awkTokenizer(input string) ([]string, int) {
|
||||
// 9, 32
|
||||
ret := []util.Chars{}
|
||||
ret := []string{}
|
||||
prefixLength := 0
|
||||
state := awkNil
|
||||
numChars := input.Length()
|
||||
begin := 0
|
||||
end := 0
|
||||
for idx := 0; idx < numChars; idx++ {
|
||||
r := input.Get(idx)
|
||||
for idx := 0; idx < len(input); idx++ {
|
||||
r := input[idx]
|
||||
white := r == 9 || r == 32
|
||||
switch state {
|
||||
case awkNil:
|
||||
@@ -120,19 +119,19 @@ func awkTokenizer(input util.Chars) ([]util.Chars, int) {
|
||||
if white {
|
||||
end = idx + 1
|
||||
} else {
|
||||
ret = append(ret, input.Slice(begin, end))
|
||||
ret = append(ret, input[begin:end])
|
||||
state, begin, end = awkBlack, idx, idx+1
|
||||
}
|
||||
}
|
||||
}
|
||||
if begin < end {
|
||||
ret = append(ret, input.Slice(begin, end))
|
||||
ret = append(ret, input[begin:end])
|
||||
}
|
||||
return ret, prefixLength
|
||||
}
|
||||
|
||||
// Tokenize tokenizes the given string with the delimiter
|
||||
func Tokenize(text util.Chars, delimiter Delimiter) []Token {
|
||||
func Tokenize(text string, delimiter Delimiter) []Token {
|
||||
if delimiter.str == nil && delimiter.regex == nil {
|
||||
// AWK-style (\S+\s*)
|
||||
tokens, prefixLength := awkTokenizer(text)
|
||||
@@ -140,36 +139,31 @@ func Tokenize(text util.Chars, delimiter Delimiter) []Token {
|
||||
}
|
||||
|
||||
if delimiter.str != nil {
|
||||
return withPrefixLengths(text.Split(*delimiter.str), 0)
|
||||
return withPrefixLengths(strings.SplitAfter(text, *delimiter.str), 0)
|
||||
}
|
||||
|
||||
// FIXME performance
|
||||
var tokens []string
|
||||
if delimiter.regex != nil {
|
||||
str := text.ToString()
|
||||
for len(str) > 0 {
|
||||
loc := delimiter.regex.FindStringIndex(str)
|
||||
for len(text) > 0 {
|
||||
loc := delimiter.regex.FindStringIndex(text)
|
||||
if loc == nil {
|
||||
loc = []int{0, len(str)}
|
||||
loc = []int{0, len(text)}
|
||||
}
|
||||
last := util.Max(loc[1], 1)
|
||||
tokens = append(tokens, str[:last])
|
||||
str = str[last:]
|
||||
tokens = append(tokens, text[:last])
|
||||
text = text[last:]
|
||||
}
|
||||
}
|
||||
asRunes := make([]util.Chars, len(tokens))
|
||||
for i, token := range tokens {
|
||||
asRunes[i] = util.RunesToChars([]rune(token))
|
||||
}
|
||||
return withPrefixLengths(asRunes, 0)
|
||||
return withPrefixLengths(tokens, 0)
|
||||
}
|
||||
|
||||
func joinTokens(tokens []Token) []rune {
|
||||
ret := []rune{}
|
||||
func joinTokens(tokens []Token) string {
|
||||
var output bytes.Buffer
|
||||
for _, token := range tokens {
|
||||
ret = append(ret, token.text.ToRunes()...)
|
||||
output.WriteString(token.text.ToString())
|
||||
}
|
||||
return ret
|
||||
return output.String()
|
||||
}
|
||||
|
||||
// Transform is used to transform the input when --with-nth option is given
|
||||
@@ -182,7 +176,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
||||
if r.begin == r.end {
|
||||
idx := r.begin
|
||||
if idx == rangeEllipsis {
|
||||
chars := util.RunesToChars(joinTokens(tokens))
|
||||
chars := util.ToChars([]byte(joinTokens(tokens)))
|
||||
parts = append(parts, &chars)
|
||||
} else {
|
||||
if idx < 0 {
|
||||
@@ -225,15 +219,15 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
||||
var merged util.Chars
|
||||
switch len(parts) {
|
||||
case 0:
|
||||
merged = util.RunesToChars([]rune{})
|
||||
merged = util.ToChars([]byte{})
|
||||
case 1:
|
||||
merged = *parts[0]
|
||||
default:
|
||||
runes := []rune{}
|
||||
var output bytes.Buffer
|
||||
for _, part := range parts {
|
||||
runes = append(runes, part.ToRunes()...)
|
||||
output.WriteString(part.ToString())
|
||||
}
|
||||
merged = util.RunesToChars(runes)
|
||||
merged = util.ToChars([]byte(output.String()))
|
||||
}
|
||||
|
||||
var prefixLength int32
|
||||
@@ -242,7 +236,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
||||
} else {
|
||||
prefixLength = 0
|
||||
}
|
||||
transTokens[idx] = Token{&merged, prefixLength, int32(merged.TrimLength())}
|
||||
transTokens[idx] = Token{&merged, prefixLength}
|
||||
}
|
||||
return transTokens
|
||||
}
|
||||
|
@@ -2,8 +2,6 @@ package fzf
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func TestParseRange(t *testing.T) {
|
||||
@@ -47,23 +45,23 @@ func TestParseRange(t *testing.T) {
|
||||
func TestTokenize(t *testing.T) {
|
||||
// AWK-style
|
||||
input := " abc: def: ghi "
|
||||
tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
|
||||
if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 || tokens[0].trimLength != 4 {
|
||||
tokens := Tokenize(input, Delimiter{})
|
||||
if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 {
|
||||
t.Errorf("%s", tokens)
|
||||
}
|
||||
|
||||
// With delimiter
|
||||
tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
|
||||
if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 || tokens[0].trimLength != 4 {
|
||||
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(util.RunesToChars([]rune(input)), delimiterRegexp("\\s+"))
|
||||
if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 || tokens[0].trimLength != 0 ||
|
||||
tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 || tokens[1].trimLength != 4 ||
|
||||
tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 || tokens[2].trimLength != 4 ||
|
||||
tokens[3].text.ToString() != "ghi " || tokens[3].prefixLength != 14 || tokens[3].trimLength != 3 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -71,7 +69,7 @@ func TestTokenize(t *testing.T) {
|
||||
func TestTransform(t *testing.T) {
|
||||
input := " abc: def: ghi: jkl"
|
||||
{
|
||||
tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
|
||||
tokens := Tokenize(input, Delimiter{})
|
||||
{
|
||||
ranges := splitNth("1,2,3")
|
||||
tx := Transform(tokens, ranges)
|
||||
@@ -93,7 +91,7 @@ func TestTransform(t *testing.T) {
|
||||
}
|
||||
}
|
||||
{
|
||||
tokens := Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
|
||||
tokens := Tokenize(input, delimiterRegexp(":"))
|
||||
{
|
||||
ranges := splitNth("1..2,3,2..,1")
|
||||
tx := Transform(tokens, ranges)
|
||||
|
45
src/tui/dummy.go
Normal file
45
src/tui/dummy.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// +build !ncurses
|
||||
// +build !tcell
|
||||
// +build !windows
|
||||
|
||||
package tui
|
||||
|
||||
type Attr int
|
||||
|
||||
func HasFullscreenRenderer() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a Attr) Merge(b Attr) Attr {
|
||||
return a | b
|
||||
}
|
||||
|
||||
const (
|
||||
AttrRegular Attr = Attr(0)
|
||||
Bold = Attr(1)
|
||||
Dim = Attr(1 << 1)
|
||||
Italic = Attr(1 << 2)
|
||||
Underline = Attr(1 << 3)
|
||||
Blink = Attr(1 << 4)
|
||||
Blink2 = Attr(1 << 5)
|
||||
Reverse = Attr(1 << 6)
|
||||
)
|
||||
|
||||
func (r *FullscreenRenderer) Init() {}
|
||||
func (r *FullscreenRenderer) Pause(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) IsOptimized() bool { return false }
|
||||
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
|
||||
func (r *FullscreenRenderer) MaxX() int { return 0 }
|
||||
func (r *FullscreenRenderer) MaxY() int { return 0 }
|
||||
|
||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
|
||||
|
||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
|
||||
return nil
|
||||
}
|
230
src/tui/light.go
230
src/tui/light.go
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -19,11 +20,15 @@ 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 {
|
||||
@@ -69,6 +74,7 @@ type LightRenderer struct {
|
||||
theme *ColorTheme
|
||||
mouse bool
|
||||
forceBlack bool
|
||||
clearOnExit bool
|
||||
prevDownTime time.Time
|
||||
clickY []int
|
||||
ttyin *os.File
|
||||
@@ -79,6 +85,7 @@ type LightRenderer struct {
|
||||
yoffset int
|
||||
tabstop int
|
||||
escDelay int
|
||||
fullscreen bool
|
||||
upOneLine bool
|
||||
queued string
|
||||
y int
|
||||
@@ -89,7 +96,7 @@ type LightRenderer struct {
|
||||
type LightWindow struct {
|
||||
renderer *LightRenderer
|
||||
colored bool
|
||||
border bool
|
||||
border BorderStyle
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
@@ -100,14 +107,16 @@ type LightWindow struct {
|
||||
bg Color
|
||||
}
|
||||
|
||||
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, maxHeightFunc func(int) int) Renderer {
|
||||
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer {
|
||||
r := LightRenderer{
|
||||
theme: theme,
|
||||
forceBlack: forceBlack,
|
||||
mouse: mouse,
|
||||
clearOnExit: clearOnExit,
|
||||
ttyin: openTtyIn(),
|
||||
yoffset: -1,
|
||||
yoffset: 0,
|
||||
tabstop: tabstop,
|
||||
fullscreen: fullscreen,
|
||||
upOneLine: false,
|
||||
maxHeightFunc: maxHeightFunc}
|
||||
return &r
|
||||
@@ -131,18 +140,14 @@ func (r *LightRenderer) defaultTheme() *ColorTheme {
|
||||
func (r *LightRenderer) findOffset() (row int, col int) {
|
||||
r.csi("6n")
|
||||
r.flush()
|
||||
bytes := r.getBytesInternal([]byte{})
|
||||
|
||||
// ^[[*;*R
|
||||
if len(bytes) > 5 && bytes[0] == 27 && bytes[1] == 91 && bytes[len(bytes)-1] == 'R' {
|
||||
nums := strings.Split(string(bytes[2:len(bytes)-1]), ";")
|
||||
if len(nums) == 2 {
|
||||
return atoi(nums[0], 0) - 1, atoi(nums[1], 0) - 1
|
||||
bytes := []byte{}
|
||||
for tries := 0; tries < offsetPollTries; tries++ {
|
||||
bytes = r.getBytesInternal(bytes, tries > 0)
|
||||
offsets := offsetRegexp.FindSubmatch(bytes)
|
||||
if len(offsets) > 2 {
|
||||
return atoi(string(offsets[1]), 0) - 1, atoi(string(offsets[2]), 0) - 1
|
||||
}
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
// No idea
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
@@ -162,15 +167,7 @@ func atoi(s string, defaultValue int) int {
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Init() {
|
||||
delay := 100
|
||||
delayEnv := os.Getenv("ESCDELAY")
|
||||
if len(delayEnv) > 0 {
|
||||
num, err := strconv.Atoi(delayEnv)
|
||||
if err == nil && num >= 0 {
|
||||
delay = num
|
||||
}
|
||||
}
|
||||
r.escDelay = delay
|
||||
r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
|
||||
|
||||
fd := r.fd()
|
||||
origState, err := terminal.GetState(fd)
|
||||
@@ -182,14 +179,27 @@ func (r *LightRenderer) Init() {
|
||||
r.updateTerminalSize()
|
||||
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
|
||||
|
||||
_, x := r.findOffset()
|
||||
if x > 0 {
|
||||
r.upOneLine = true
|
||||
r.stderr("\n")
|
||||
}
|
||||
for i := 1; i < r.MaxY(); i++ {
|
||||
r.stderr("\n")
|
||||
r.csi("G")
|
||||
if r.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 {
|
||||
@@ -197,12 +207,18 @@ func (r *LightRenderer) Init() {
|
||||
}
|
||||
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
|
||||
r.csi("G")
|
||||
// r.csi("s")
|
||||
if r.mouse {
|
||||
r.csi("K")
|
||||
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 {
|
||||
@@ -243,8 +259,9 @@ func (r *LightRenderer) updateTerminalSize() {
|
||||
|
||||
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||
b := make([]byte, 1)
|
||||
fd := r.fd()
|
||||
util.SetNonblock(r.ttyin, nonblock)
|
||||
_, err := r.ttyin.Read(b)
|
||||
_, err := util.Read(fd, b)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
@@ -252,18 +269,18 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getBytes() []byte {
|
||||
return r.getBytesInternal(r.buffer)
|
||||
return r.getBytesInternal(r.buffer, false)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getBytesInternal(buffer []byte) []byte {
|
||||
c, ok := r.getch(false)
|
||||
if !ok {
|
||||
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
|
||||
c, ok := r.getch(nonblock)
|
||||
if !nonblock && !ok {
|
||||
r.Close()
|
||||
errorExit("Failed to read " + consoleDevice)
|
||||
}
|
||||
|
||||
retries := 0
|
||||
if c == ESC {
|
||||
if c == ESC || nonblock {
|
||||
retries = r.escDelay / escPollInterval
|
||||
}
|
||||
buffer = append(buffer, byte(c))
|
||||
@@ -307,6 +324,8 @@ func (r *LightRenderer) GetChar() Event {
|
||||
return Event{CtrlQ, 0, nil}
|
||||
case 127:
|
||||
return Event{BSpace, 0, nil}
|
||||
case 0:
|
||||
return Event{CtrlSpace, 0, nil}
|
||||
case ESC:
|
||||
ev := r.escSequence(&sz)
|
||||
// Second chance
|
||||
@@ -334,9 +353,10 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
return Event{ESC, 0, nil}
|
||||
}
|
||||
*sz = 2
|
||||
if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 {
|
||||
return Event{int(CtrlAltA + r.buffer[1] - 1), 0, nil}
|
||||
}
|
||||
switch r.buffer[1] {
|
||||
case 13:
|
||||
return Event{AltEnter, 0, nil}
|
||||
case 32:
|
||||
return Event{AltSpace, 0, nil}
|
||||
case 47:
|
||||
@@ -399,10 +419,12 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
return Event{F12, 0, nil}
|
||||
}
|
||||
}
|
||||
// Bracketed paste mode \e[200~ / \e[201
|
||||
if r.buffer[3] == 48 && (r.buffer[4] == 48 || r.buffer[4] == 49) && r.buffer[5] == 126 {
|
||||
*sz = 6
|
||||
return Event{Invalid, 0, nil}
|
||||
// Bracketed paste mode: \e[200~ ... \e[201~
|
||||
if r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' {
|
||||
// Immediately discard the sequence from the buffer and reread input
|
||||
r.buffer = r.buffer[6:]
|
||||
*sz = 0
|
||||
return r.GetChar()
|
||||
}
|
||||
return Event{Invalid, 0, nil} // INS
|
||||
case 51:
|
||||
@@ -464,7 +486,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
}
|
||||
|
||||
func (r *LightRenderer) mouseSequence(sz *int) Event {
|
||||
if len(r.buffer) < 6 || r.yoffset < 0 {
|
||||
if len(r.buffer) < 6 || !r.mouse {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 6
|
||||
@@ -503,21 +525,49 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Pause() {
|
||||
terminal.Restore(r.fd(), r.origState)
|
||||
func (r *LightRenderer) smcup() {
|
||||
r.csi("?1049h")
|
||||
r.flush()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Resume() bool {
|
||||
terminal.MakeRaw(r.fd())
|
||||
func (r *LightRenderer) rmcup() {
|
||||
r.csi("?1049l")
|
||||
r.flush()
|
||||
// Should redraw
|
||||
return true
|
||||
}
|
||||
|
||||
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")
|
||||
@@ -534,14 +584,22 @@ func (r *LightRenderer) Refresh() {
|
||||
|
||||
func (r *LightRenderer) Close() {
|
||||
// r.csi("u")
|
||||
r.origin()
|
||||
r.csi("J")
|
||||
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")
|
||||
}
|
||||
if r.upOneLine {
|
||||
r.csi("A")
|
||||
}
|
||||
r.flush()
|
||||
terminal.Restore(r.fd(), r.origState)
|
||||
}
|
||||
@@ -555,18 +613,18 @@ func (r *LightRenderer) MaxY() int {
|
||||
}
|
||||
|
||||
func (r *LightRenderer) DoesAutoWrap() bool {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *LightRenderer) IsOptimized() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, border bool) Window {
|
||||
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
|
||||
w := &LightWindow{
|
||||
renderer: r,
|
||||
colored: r.theme != nil,
|
||||
border: border,
|
||||
border: borderStyle,
|
||||
top: top,
|
||||
left: left,
|
||||
width: width,
|
||||
@@ -576,13 +634,27 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, bord
|
||||
if r.theme != nil {
|
||||
w.bg = r.theme.Bg
|
||||
}
|
||||
if w.border {
|
||||
w.drawBorder()
|
||||
}
|
||||
w.drawBorder()
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorder() {
|
||||
switch w.border {
|
||||
case BorderAround:
|
||||
w.drawBorderAround()
|
||||
case BorderHorizontal:
|
||||
w.drawBorderHorizontal()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorderHorizontal() {
|
||||
w.Move(0, 0)
|
||||
w.CPrint(ColBorder, AttrRegular, repeat("─", w.width))
|
||||
w.Move(w.height-1, 0)
|
||||
w.CPrint(ColBorder, AttrRegular, repeat("─", w.width))
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorderAround() {
|
||||
w.Move(0, 0)
|
||||
w.CPrint(ColBorder, AttrRegular, "┌"+repeat("─", w.width-2)+"┐")
|
||||
for y := 1; y < w.height-1; y++ {
|
||||
@@ -633,6 +705,10 @@ func (w *LightWindow) X() int {
|
||||
return w.posx
|
||||
}
|
||||
|
||||
func (w *LightWindow) Y() int {
|
||||
return w.posy
|
||||
}
|
||||
|
||||
func (w *LightWindow) Enclose(y int, x int) bool {
|
||||
return x >= w.left && x < (w.left+w.width) &&
|
||||
y >= w.top && y < (w.top+w.height)
|
||||
@@ -710,13 +786,17 @@ func (w *LightWindow) Print(text string) {
|
||||
w.cprint2(colDefault, w.bg, AttrRegular, text)
|
||||
}
|
||||
|
||||
func cleanse(str string) string {
|
||||
return strings.Replace(str, "\x1b", "?", -1)
|
||||
}
|
||||
|
||||
func (w *LightWindow) CPrint(pair ColorPair, attr Attr, text string) {
|
||||
if !w.colored {
|
||||
w.csiColor(colDefault, colDefault, attrFor(pair, attr))
|
||||
} else {
|
||||
w.csiColor(pair.Fg(), pair.Bg(), attr)
|
||||
}
|
||||
w.stderrInternal(text, false)
|
||||
w.stderrInternal(cleanse(text), false)
|
||||
w.csi("m")
|
||||
}
|
||||
|
||||
@@ -724,7 +804,7 @@ func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
||||
if w.csiColor(fg, bg, attr) {
|
||||
defer w.csi("m")
|
||||
}
|
||||
w.stderrInternal(text, false)
|
||||
w.stderrInternal(cleanse(text), false)
|
||||
}
|
||||
|
||||
type wrappedLine struct {
|
||||
@@ -763,17 +843,20 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
||||
for j, wl := range lines {
|
||||
if w.posx >= w.Width()-1 && wl.displayWidth == 0 {
|
||||
if w.posy < w.height-1 {
|
||||
w.MoveAndClear(w.posy+1, 0)
|
||||
w.Move(w.posy+1, 0)
|
||||
}
|
||||
return FillNextLine
|
||||
}
|
||||
w.stderrInternal(wl.text, false)
|
||||
w.posx += wl.displayWidth
|
||||
|
||||
// Wrap line
|
||||
if j < len(lines)-1 || i < len(allLines)-1 {
|
||||
if w.posy+1 >= w.height {
|
||||
return FillSuspend
|
||||
}
|
||||
w.MoveAndClear(w.posy+1, 0)
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
w.Move(w.posy+1, 0)
|
||||
onMove()
|
||||
}
|
||||
}
|
||||
@@ -788,33 +871,32 @@ func (w *LightWindow) setBg() {
|
||||
}
|
||||
|
||||
func (w *LightWindow) Fill(text string) FillReturn {
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
w.Move(w.posy, w.posx)
|
||||
w.setBg()
|
||||
return w.fill(text, w.setBg)
|
||||
}
|
||||
|
||||
func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillReturn {
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
w.Move(w.posy, w.posx)
|
||||
if bg == colDefault {
|
||||
bg = w.bg
|
||||
}
|
||||
if w.csiColor(fg, bg, attr) {
|
||||
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
|
||||
defer w.csi("m")
|
||||
return w.fill(text, func() { w.csiColor(fg, bg, attr) })
|
||||
}
|
||||
return w.fill(text, w.setBg)
|
||||
}
|
||||
|
||||
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() {
|
||||
if w.border {
|
||||
w.drawBorder()
|
||||
}
|
||||
w.drawBorder()
|
||||
// We don't erase the window here to avoid flickering during scroll
|
||||
w.Move(0, 0)
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
// +build ncurses
|
||||
// +build !windows
|
||||
// +build !tcell
|
||||
|
||||
@@ -32,6 +33,10 @@ import (
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func HasFullscreenRenderer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type Attr C.uint
|
||||
|
||||
type CursesWindow struct {
|
||||
@@ -171,12 +176,11 @@ func initPairs(theme *ColorTheme) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Pause() {
|
||||
func (r *FullscreenRenderer) Pause(bool) {
|
||||
C.endwin()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Resume() bool {
|
||||
return false
|
||||
func (r *FullscreenRenderer) Resume(bool) {
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Close() {
|
||||
@@ -184,12 +188,13 @@ func (r *FullscreenRenderer) Close() {
|
||||
C.delscreen(_screen)
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window {
|
||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
|
||||
win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
|
||||
if r.theme != nil {
|
||||
C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
|
||||
}
|
||||
if border {
|
||||
// FIXME Does not implement BorderHorizontal
|
||||
if borderStyle != BorderNone {
|
||||
pair, attr := _colorFn(ColBorder, 0)
|
||||
C.wcolor_set(win, pair, nil)
|
||||
C.wattron(win, attr)
|
||||
@@ -347,7 +352,7 @@ func escSequence() Event {
|
||||
case C.ERR:
|
||||
return Event{ESC, 0, nil}
|
||||
case CtrlM:
|
||||
return Event{AltEnter, 0, nil}
|
||||
return Event{CtrlAltM, 0, nil}
|
||||
case '/':
|
||||
return Event{AltSlash, 0, nil}
|
||||
case ' ':
|
||||
@@ -470,6 +475,8 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
return escSequence()
|
||||
case 127:
|
||||
return Event{BSpace, 0, nil}
|
||||
case 0:
|
||||
return Event{CtrlSpace, 0, nil}
|
||||
}
|
||||
// CTRL-A ~ CTRL-Z
|
||||
if c >= CtrlA && c <= CtrlZ {
|
||||
|
150
src/tui/tcell.go
150
src/tui/tcell.go
@@ -3,18 +3,22 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"runtime"
|
||||
|
||||
// https://github.com/gdamore/tcell/pull/135
|
||||
"github.com/junegunn/tcell"
|
||||
"github.com/junegunn/tcell/encoding"
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/gdamore/tcell/encoding"
|
||||
|
||||
"github.com/junegunn/go-runewidth"
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
func HasFullscreenRenderer() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p ColorPair) style() tcell.Style {
|
||||
style := tcell.StyleDefault
|
||||
return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg()))
|
||||
@@ -23,15 +27,15 @@ func (p ColorPair) style() tcell.Style {
|
||||
type Attr tcell.Style
|
||||
|
||||
type TcellWindow struct {
|
||||
color bool
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
height int
|
||||
lastX int
|
||||
lastY int
|
||||
moveCursor bool
|
||||
border bool
|
||||
color bool
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
height int
|
||||
lastX int
|
||||
lastY int
|
||||
moveCursor bool
|
||||
borderStyle BorderStyle
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Top() int {
|
||||
@@ -57,8 +61,11 @@ func (w *TcellWindow) Refresh() {
|
||||
}
|
||||
w.lastX = 0
|
||||
w.lastY = 0
|
||||
if w.border {
|
||||
w.drawBorder()
|
||||
switch w.borderStyle {
|
||||
case BorderAround:
|
||||
w.drawBorder(true)
|
||||
case BorderHorizontal:
|
||||
w.drawBorder(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +141,9 @@ func (r *FullscreenRenderer) initScreen() {
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Init() {
|
||||
if os.Getenv("TERM") == "cygwin" {
|
||||
os.Setenv("TERM", "")
|
||||
}
|
||||
encoding.Register()
|
||||
|
||||
r.initScreen()
|
||||
@@ -154,6 +164,10 @@ func (w *TcellWindow) X() int {
|
||||
return w.lastX
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Y() int {
|
||||
return w.lastY
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) DoesAutoWrap() bool {
|
||||
return false
|
||||
}
|
||||
@@ -214,59 +228,68 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
// process keyboard:
|
||||
case *tcell.EventKey:
|
||||
alt := (ev.Modifiers() & tcell.ModAlt) > 0
|
||||
keyfn := func(r rune) int {
|
||||
if alt {
|
||||
return CtrlAltA - 'a' + int(r)
|
||||
}
|
||||
return CtrlA - 'a' + int(r)
|
||||
}
|
||||
switch ev.Key() {
|
||||
case tcell.KeyCtrlA:
|
||||
return Event{CtrlA, 0, nil}
|
||||
return Event{keyfn('a'), 0, nil}
|
||||
case tcell.KeyCtrlB:
|
||||
return Event{CtrlB, 0, nil}
|
||||
return Event{keyfn('b'), 0, nil}
|
||||
case tcell.KeyCtrlC:
|
||||
return Event{CtrlC, 0, nil}
|
||||
return Event{keyfn('c'), 0, nil}
|
||||
case tcell.KeyCtrlD:
|
||||
return Event{CtrlD, 0, nil}
|
||||
return Event{keyfn('d'), 0, nil}
|
||||
case tcell.KeyCtrlE:
|
||||
return Event{CtrlE, 0, nil}
|
||||
return Event{keyfn('e'), 0, nil}
|
||||
case tcell.KeyCtrlF:
|
||||
return Event{CtrlF, 0, nil}
|
||||
return Event{keyfn('f'), 0, nil}
|
||||
case tcell.KeyCtrlG:
|
||||
return Event{CtrlG, 0, nil}
|
||||
return Event{keyfn('g'), 0, nil}
|
||||
case tcell.KeyCtrlH:
|
||||
return Event{keyfn('h'), 0, nil}
|
||||
case tcell.KeyCtrlI:
|
||||
return Event{keyfn('i'), 0, nil}
|
||||
case tcell.KeyCtrlJ:
|
||||
return Event{CtrlJ, 0, nil}
|
||||
return Event{keyfn('j'), 0, nil}
|
||||
case tcell.KeyCtrlK:
|
||||
return Event{CtrlK, 0, nil}
|
||||
return Event{keyfn('k'), 0, nil}
|
||||
case tcell.KeyCtrlL:
|
||||
return Event{CtrlL, 0, nil}
|
||||
return Event{keyfn('l'), 0, nil}
|
||||
case tcell.KeyCtrlM:
|
||||
if alt {
|
||||
return Event{AltEnter, 0, nil}
|
||||
}
|
||||
return Event{CtrlM, 0, nil}
|
||||
return Event{keyfn('m'), 0, nil}
|
||||
case tcell.KeyCtrlN:
|
||||
return Event{CtrlN, 0, nil}
|
||||
return Event{keyfn('n'), 0, nil}
|
||||
case tcell.KeyCtrlO:
|
||||
return Event{CtrlO, 0, nil}
|
||||
return Event{keyfn('o'), 0, nil}
|
||||
case tcell.KeyCtrlP:
|
||||
return Event{CtrlP, 0, nil}
|
||||
return Event{keyfn('p'), 0, nil}
|
||||
case tcell.KeyCtrlQ:
|
||||
return Event{CtrlQ, 0, nil}
|
||||
return Event{keyfn('q'), 0, nil}
|
||||
case tcell.KeyCtrlR:
|
||||
return Event{CtrlR, 0, nil}
|
||||
return Event{keyfn('r'), 0, nil}
|
||||
case tcell.KeyCtrlS:
|
||||
return Event{CtrlS, 0, nil}
|
||||
return Event{keyfn('s'), 0, nil}
|
||||
case tcell.KeyCtrlT:
|
||||
return Event{CtrlT, 0, nil}
|
||||
return Event{keyfn('t'), 0, nil}
|
||||
case tcell.KeyCtrlU:
|
||||
return Event{CtrlU, 0, nil}
|
||||
return Event{keyfn('u'), 0, nil}
|
||||
case tcell.KeyCtrlV:
|
||||
return Event{CtrlV, 0, nil}
|
||||
return Event{keyfn('v'), 0, nil}
|
||||
case tcell.KeyCtrlW:
|
||||
return Event{CtrlW, 0, nil}
|
||||
return Event{keyfn('w'), 0, nil}
|
||||
case tcell.KeyCtrlX:
|
||||
return Event{CtrlX, 0, nil}
|
||||
return Event{keyfn('x'), 0, nil}
|
||||
case tcell.KeyCtrlY:
|
||||
return Event{CtrlY, 0, nil}
|
||||
return Event{keyfn('y'), 0, nil}
|
||||
case tcell.KeyCtrlZ:
|
||||
return Event{CtrlZ, 0, nil}
|
||||
case tcell.KeyBackspace, tcell.KeyBackspace2:
|
||||
return Event{keyfn('z'), 0, nil}
|
||||
case tcell.KeyCtrlSpace:
|
||||
return Event{CtrlSpace, 0, nil}
|
||||
case tcell.KeyBackspace2:
|
||||
if alt {
|
||||
return Event{AltBS, 0, nil}
|
||||
}
|
||||
@@ -292,8 +315,6 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
case tcell.KeyPgDn:
|
||||
return Event{PgDn, 0, nil}
|
||||
|
||||
case tcell.KeyTab:
|
||||
return Event{Tab, 0, nil}
|
||||
case tcell.KeyBacktab:
|
||||
return Event{BTab, 0, nil}
|
||||
|
||||
@@ -350,13 +371,12 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Pause() {
|
||||
func (r *FullscreenRenderer) Pause(bool) {
|
||||
_screen.Fini()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Resume() bool {
|
||||
func (r *FullscreenRenderer) Resume(bool) {
|
||||
r.initScreen()
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Close() {
|
||||
@@ -371,15 +391,15 @@ func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
||||
_screen.Show()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, border bool) Window {
|
||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
|
||||
// TODO
|
||||
return &TcellWindow{
|
||||
color: r.theme != nil,
|
||||
top: top,
|
||||
left: left,
|
||||
width: width,
|
||||
height: height,
|
||||
border: border}
|
||||
color: r.theme != nil,
|
||||
top: top,
|
||||
left: left,
|
||||
width: width,
|
||||
height: height,
|
||||
borderStyle: borderStyle}
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Close() {
|
||||
@@ -530,7 +550,7 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
|
||||
return w.fillString(str, ColorPair{fg, bg, -1}, a)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) drawBorder() {
|
||||
func (w *TcellWindow) drawBorder(around bool) {
|
||||
left := w.left
|
||||
right := left + w.width
|
||||
top := w.top
|
||||
@@ -548,13 +568,15 @@ func (w *TcellWindow) drawBorder() {
|
||||
_screen.SetContent(x, bot-1, tcell.RuneHLine, nil, style)
|
||||
}
|
||||
|
||||
for y := top; y < bot; y++ {
|
||||
_screen.SetContent(left, y, tcell.RuneVLine, nil, style)
|
||||
_screen.SetContent(right-1, y, tcell.RuneVLine, nil, style)
|
||||
}
|
||||
if around {
|
||||
for y := top; y < bot; y++ {
|
||||
_screen.SetContent(left, y, tcell.RuneVLine, nil, style)
|
||||
_screen.SetContent(right-1, y, tcell.RuneVLine, nil, style)
|
||||
}
|
||||
|
||||
_screen.SetContent(left, top, tcell.RuneULCorner, nil, style)
|
||||
_screen.SetContent(right-1, top, tcell.RuneURCorner, nil, style)
|
||||
_screen.SetContent(left, bot-1, tcell.RuneLLCorner, nil, style)
|
||||
_screen.SetContent(right-1, bot-1, tcell.RuneLRCorner, nil, style)
|
||||
_screen.SetContent(left, top, tcell.RuneULCorner, nil, style)
|
||||
_screen.SetContent(right-1, top, tcell.RuneURCorner, nil, style)
|
||||
_screen.SetContent(left, bot-1, tcell.RuneLLCorner, nil, style)
|
||||
_screen.SetContent(right-1, bot-1, tcell.RuneLRCorner, nil, style)
|
||||
}
|
||||
}
|
||||
|
@@ -38,6 +38,7 @@ const (
|
||||
CtrlY
|
||||
CtrlZ
|
||||
ESC
|
||||
CtrlSpace
|
||||
|
||||
Invalid
|
||||
Resize
|
||||
@@ -74,7 +75,8 @@ const (
|
||||
F11
|
||||
F12
|
||||
|
||||
AltEnter
|
||||
Change
|
||||
|
||||
AltSpace
|
||||
AltSlash
|
||||
AltBS
|
||||
@@ -89,7 +91,9 @@ const ( // Reset iota
|
||||
AltD
|
||||
AltE
|
||||
AltF
|
||||
AltZ = AltA + 'z' - 'a'
|
||||
AltZ = AltA + 'z' - 'a'
|
||||
CtrlAltA = AltZ + 1
|
||||
CtrlAltM = CtrlAltA + 'm' - 'a'
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -194,10 +198,18 @@ type MouseEvent struct {
|
||||
Mod bool
|
||||
}
|
||||
|
||||
type BorderStyle int
|
||||
|
||||
const (
|
||||
BorderNone BorderStyle = iota
|
||||
BorderAround
|
||||
BorderHorizontal
|
||||
)
|
||||
|
||||
type Renderer interface {
|
||||
Init()
|
||||
Pause()
|
||||
Resume() bool
|
||||
Pause(clear bool)
|
||||
Resume(clear bool)
|
||||
Clear()
|
||||
RefreshWindows(windows []Window)
|
||||
Refresh()
|
||||
@@ -210,7 +222,7 @@ type Renderer interface {
|
||||
DoesAutoWrap() bool
|
||||
IsOptimized() bool
|
||||
|
||||
NewWindow(top int, left int, width int, height int, border bool) Window
|
||||
NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window
|
||||
}
|
||||
|
||||
type Window interface {
|
||||
@@ -224,6 +236,7 @@ type Window interface {
|
||||
Close()
|
||||
|
||||
X() int
|
||||
Y() int
|
||||
Enclose(y int, x int) bool
|
||||
|
||||
Move(y int, x int)
|
||||
|
@@ -23,21 +23,23 @@ end
|
||||
# List assets
|
||||
assets = Hash[rel['assets'].map { |a| a.values_at *%w[name id] }]
|
||||
|
||||
files.select { |f| File.exists? f }.each do |file|
|
||||
name = File.basename file
|
||||
files.select { |f| File.exists? f }.map do |file|
|
||||
Thread.new do
|
||||
name = File.basename file
|
||||
|
||||
if asset_id = assets[name]
|
||||
puts "#{name} found. Deleting asset id #{asset_id}."
|
||||
RestClient.delete "#{base}/assets/#{asset_id}",
|
||||
:authorization => "token #{token}"
|
||||
else
|
||||
puts "#{name} not found"
|
||||
if asset_id = assets[name]
|
||||
puts "#{name} found. Deleting asset id #{asset_id}."
|
||||
RestClient.delete "#{base}/assets/#{asset_id}",
|
||||
:authorization => "token #{token}"
|
||||
else
|
||||
puts "#{name} not found"
|
||||
end
|
||||
|
||||
puts "Uploading #{name}"
|
||||
RestClient.post(
|
||||
"#{base.sub 'api', 'uploads'}/#{rel['id']}/assets?name=#{name}",
|
||||
File.read(file),
|
||||
:authorization => "token #{token}",
|
||||
:content_type => "application/octet-stream")
|
||||
end
|
||||
|
||||
puts "Uploading #{name}"
|
||||
RestClient.post(
|
||||
"#{base.sub 'api', 'uploads'}/#{rel['id']}/assets?name=#{name}",
|
||||
File.read(file),
|
||||
:authorization => "token #{token}",
|
||||
:content_type => "application/octet-stream")
|
||||
end
|
||||
end.each(&:join)
|
||||
|
@@ -3,63 +3,95 @@ package util
|
||||
import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
overflow64 uint64 = 0x8080808080808080
|
||||
overflow32 uint32 = 0x80808080
|
||||
)
|
||||
|
||||
type Chars struct {
|
||||
runes []rune
|
||||
bytes []byte
|
||||
slice []byte // or []rune
|
||||
inBytes bool
|
||||
trimLengthKnown bool
|
||||
trimLength uint16
|
||||
|
||||
// XXX Piggybacking item index here is a horrible idea. But I'm trying to
|
||||
// minimize the memory footprint by not wasting padded spaces.
|
||||
Index int32
|
||||
}
|
||||
|
||||
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(bytea []byte) Chars {
|
||||
var runes []rune
|
||||
ascii := true
|
||||
numBytes := len(bytea)
|
||||
for i := 0; i < numBytes; {
|
||||
if bytea[i] < utf8.RuneSelf {
|
||||
if !ascii {
|
||||
runes = append(runes, rune(bytea[i]))
|
||||
}
|
||||
i++
|
||||
} else {
|
||||
if ascii {
|
||||
ascii = false
|
||||
runes = make([]rune, i, numBytes)
|
||||
for j := 0; j < i; j++ {
|
||||
runes[j] = rune(bytea[j])
|
||||
}
|
||||
}
|
||||
r, sz := utf8.DecodeRune(bytea[i:])
|
||||
i += sz
|
||||
runes = append(runes, r)
|
||||
}
|
||||
func ToChars(bytes []byte) Chars {
|
||||
inBytes, bytesUntil := checkAscii(bytes)
|
||||
if inBytes {
|
||||
return Chars{slice: bytes, inBytes: inBytes}
|
||||
}
|
||||
if ascii {
|
||||
return Chars{bytes: bytea}
|
||||
|
||||
runes := make([]rune, bytesUntil, len(bytes))
|
||||
for i := 0; i < bytesUntil; i++ {
|
||||
runes[i] = rune(bytes[i])
|
||||
}
|
||||
return Chars{runes: runes}
|
||||
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{runes: runes}
|
||||
return Chars{slice: *(*[]byte)(unsafe.Pointer(&runes)), inBytes: false}
|
||||
}
|
||||
|
||||
func (chars *Chars) optionalRunes() []rune {
|
||||
if chars.inBytes {
|
||||
return nil
|
||||
}
|
||||
return *(*[]rune)(unsafe.Pointer(&chars.slice))
|
||||
}
|
||||
|
||||
func (chars *Chars) Get(i int) rune {
|
||||
if chars.runes != nil {
|
||||
return chars.runes[i]
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
return runes[i]
|
||||
}
|
||||
return rune(chars.bytes[i])
|
||||
return rune(chars.slice[i])
|
||||
}
|
||||
|
||||
func (chars *Chars) Length() int {
|
||||
if chars.runes != nil {
|
||||
return len(chars.runes)
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
return len(runes)
|
||||
}
|
||||
return len(chars.bytes)
|
||||
return len(chars.slice)
|
||||
}
|
||||
|
||||
// TrimLength returns the length after trimming leading and trailing whitespaces
|
||||
func (chars *Chars) TrimLength() int {
|
||||
func (chars *Chars) TrimLength() uint16 {
|
||||
if chars.trimLengthKnown {
|
||||
return chars.trimLength
|
||||
}
|
||||
chars.trimLengthKnown = true
|
||||
var i int
|
||||
len := chars.Length()
|
||||
for i = len - 1; i >= 0; i-- {
|
||||
@@ -80,7 +112,8 @@ func (chars *Chars) TrimLength() int {
|
||||
break
|
||||
}
|
||||
}
|
||||
return i - j + 1
|
||||
chars.trimLength = AsUint16(i - j + 1)
|
||||
return chars.trimLength
|
||||
}
|
||||
|
||||
func (chars *Chars) TrailingWhitespaces() int {
|
||||
@@ -96,62 +129,31 @@ func (chars *Chars) TrailingWhitespaces() int {
|
||||
}
|
||||
|
||||
func (chars *Chars) ToString() string {
|
||||
if chars.runes != nil {
|
||||
return string(chars.runes)
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
return string(runes)
|
||||
}
|
||||
return string(chars.bytes)
|
||||
return string(chars.slice)
|
||||
}
|
||||
|
||||
func (chars *Chars) ToRunes() []rune {
|
||||
if chars.runes != nil {
|
||||
return chars.runes
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
return runes
|
||||
}
|
||||
runes := make([]rune, len(chars.bytes))
|
||||
for idx, b := range chars.bytes {
|
||||
bytes := chars.slice
|
||||
runes := make([]rune, len(bytes))
|
||||
for idx, b := range bytes {
|
||||
runes[idx] = rune(b)
|
||||
}
|
||||
return runes
|
||||
}
|
||||
|
||||
func (chars *Chars) Slice(b int, e int) Chars {
|
||||
if chars.runes != nil {
|
||||
return Chars{runes: chars.runes[b:e]}
|
||||
func (chars *Chars) CopyRunes(dest []rune) {
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
copy(dest, runes)
|
||||
return
|
||||
}
|
||||
return Chars{bytes: chars.bytes[b:e]}
|
||||
}
|
||||
|
||||
func (chars *Chars) Split(delimiter string) []Chars {
|
||||
delim := []rune(delimiter)
|
||||
numChars := chars.Length()
|
||||
numDelim := len(delim)
|
||||
begin := 0
|
||||
ret := make([]Chars, 0, 1)
|
||||
|
||||
for index := 0; index < numChars; {
|
||||
if index+numDelim <= numChars {
|
||||
match := true
|
||||
for off, d := range delim {
|
||||
if chars.Get(index+off) != d {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
// Found the delimiter
|
||||
if match {
|
||||
incr := Max(numDelim, 1)
|
||||
ret = append(ret, chars.Slice(begin, index+incr))
|
||||
index += incr
|
||||
begin = index
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// Impossible to find the delimiter in the remaining substring
|
||||
break
|
||||
}
|
||||
index++
|
||||
}
|
||||
if begin < numChars || len(ret) == 0 {
|
||||
ret = append(ret, chars.Slice(begin, numChars))
|
||||
}
|
||||
return ret
|
||||
for idx, b := range chars.slice {
|
||||
dest[idx] = rune(b)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@@ -2,27 +2,16 @@ package util
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestToCharsNil(t *testing.T) {
|
||||
bs := Chars{bytes: []byte{}}
|
||||
if bs.bytes == nil || bs.runes != nil {
|
||||
t.Error()
|
||||
}
|
||||
rs := RunesToChars([]rune{})
|
||||
if rs.bytes != nil || rs.runes == nil {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestToCharsAscii(t *testing.T) {
|
||||
chars := ToChars([]byte("foobar"))
|
||||
if chars.ToString() != "foobar" || chars.runes != nil {
|
||||
if !chars.inBytes || chars.ToString() != "foobar" || !chars.inBytes {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func TestCharsLength(t *testing.T) {
|
||||
chars := ToChars([]byte("\tabc한글 "))
|
||||
if chars.Length() != 8 || chars.TrimLength() != 5 {
|
||||
if chars.inBytes || chars.Length() != 8 || chars.TrimLength() != 5 {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
@@ -36,7 +25,7 @@ func TestCharsToString(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTrimLength(t *testing.T) {
|
||||
check := func(str string, exp int) {
|
||||
check := func(str string, exp uint16) {
|
||||
chars := ToChars([]byte(str))
|
||||
trimmed := chars.TrimLength()
|
||||
if trimmed != exp {
|
||||
@@ -55,28 +44,3 @@ func TestTrimLength(t *testing.T) {
|
||||
check(" h o ", 5)
|
||||
check(" ", 0)
|
||||
}
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
check := func(str string, delim string, tokens ...string) {
|
||||
input := ToChars([]byte(str))
|
||||
result := input.Split(delim)
|
||||
if len(result) != len(tokens) {
|
||||
t.Errorf("Invalid Split result for '%s': %d tokens found (expected %d): %s",
|
||||
str, len(result), len(tokens), result)
|
||||
}
|
||||
for idx, token := range tokens {
|
||||
if result[idx].ToString() != token {
|
||||
t.Errorf("Invalid Split result for '%s': %s (expected %s)",
|
||||
str, result[idx].ToString(), token)
|
||||
}
|
||||
}
|
||||
}
|
||||
check("abc:def::", ":", "abc:", "def:", ":")
|
||||
check("abc:def::", "-", "abc:def::")
|
||||
check("abc", "", "a", "b", "c")
|
||||
check("abc", "a", "a", "bc")
|
||||
check("abc", "ab", "ab", "c")
|
||||
check("abc", "abc", "abc")
|
||||
check("abc", "abcd", "abc")
|
||||
check("", "abcd", "")
|
||||
}
|
||||
|
@@ -5,8 +5,8 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/go-isatty"
|
||||
"github.com/junegunn/go-runewidth"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
var _runeWidths = make(map[rune]int)
|
||||
|
@@ -26,3 +26,8 @@ func IsWindows() bool {
|
||||
func SetNonblock(file *os.File, nonblock bool) {
|
||||
syscall.SetNonblock(int(file.Fd()), nonblock)
|
||||
}
|
||||
|
||||
// Read executes syscall.Read on file descriptor
|
||||
func Read(fd int, b []byte) (int, error) {
|
||||
return syscall.Read(int(fd), b)
|
||||
}
|
||||
|
@@ -7,20 +7,16 @@ import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"github.com/junegunn/go-shellwords"
|
||||
"github.com/mattn/go-shellwords"
|
||||
)
|
||||
|
||||
// ExecCommand executes the given command with $SHELL
|
||||
func ExecCommand(command string) *exec.Cmd {
|
||||
shell := os.Getenv("SHELL")
|
||||
if len(shell) == 0 {
|
||||
shell = "cmd"
|
||||
}
|
||||
args, _ := shellwords.Parse(command)
|
||||
allArgs := make([]string, len(args)+1)
|
||||
allArgs[0] = "/c"
|
||||
copy(allArgs[1:], args)
|
||||
return exec.Command(shell, allArgs...)
|
||||
return exec.Command("cmd", allArgs...)
|
||||
}
|
||||
|
||||
// IsWindows returns true on Windows
|
||||
@@ -32,3 +28,8 @@ func IsWindows() bool {
|
||||
func SetNonblock(file *os.File, nonblock bool) {
|
||||
syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock)
|
||||
}
|
||||
|
||||
// Read executes syscall.Read on file descriptor
|
||||
func Read(fd int, b []byte) (int, error) {
|
||||
return syscall.Read(syscall.Handle(fd), b)
|
||||
}
|
||||
|
@@ -147,6 +147,24 @@ Execute (fzf#wrap):
|
||||
let opts = fzf#wrap({})
|
||||
Assert opts.options =~ '^--color=fg:'
|
||||
|
||||
Execute (fzf#shellescape with sh):
|
||||
AssertEqual '''''', fzf#shellescape('', 'sh')
|
||||
AssertEqual '''""''', fzf#shellescape('""', 'sh')
|
||||
AssertEqual '''foobar>''', fzf#shellescape('foobar>', 'sh')
|
||||
AssertEqual '''\"''', fzf#shellescape('\"', 'sh')
|
||||
AssertEqual '''echo ''\''''a''\'''' && echo ''\''''b''\''''''', fzf#shellescape('echo ''a'' && echo ''b''', 'sh')
|
||||
|
||||
Execute (fzf#shellescape with cmd.exe):
|
||||
AssertEqual '^"^"', fzf#shellescape('', 'cmd.exe')
|
||||
AssertEqual '^"\^"\^"^"', fzf#shellescape('""', 'cmd.exe')
|
||||
AssertEqual '^"foobar^>^"', fzf#shellescape('foobar>', 'cmd.exe')
|
||||
AssertEqual '^"\\\^"\\^"', fzf#shellescape('\\\\\\\\"\', 'cmd.exe')
|
||||
AssertEqual '^"echo ''a'' ^&^& echo ''b''^"', fzf#shellescape('echo ''a'' && echo ''b''', 'cmd.exe')
|
||||
|
||||
AssertEqual '^"C:\Program Files ^(x86^)\\^"', fzf#shellescape('C:\Program Files (x86)\', 'cmd.exe')
|
||||
AssertEqual '^"C:/Program Files ^(x86^)/^"', fzf#shellescape('C:/Program Files (x86)/', 'cmd.exe')
|
||||
AssertEqual '^"%%USERPROFILE%%^"', fzf#shellescape('%USERPROFILE%', 'cmd.exe')
|
||||
|
||||
Execute (Cleanup):
|
||||
unlet g:dir
|
||||
Restore
|
||||
|
236
test/test_go.rb
236
test/test_go.rb
@@ -73,6 +73,7 @@ class Tmux
|
||||
else
|
||||
raise "Unknown shell: #{shell}"
|
||||
end
|
||||
go("set-window-option -t #{@win} pane-base-index 0")
|
||||
@lines = `tput lines`.chomp.to_i
|
||||
|
||||
if shell == :fish
|
||||
@@ -111,7 +112,7 @@ class Tmux
|
||||
File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
|
||||
end
|
||||
|
||||
def until pane = 0
|
||||
def until refresh = false, pane = 0
|
||||
lines = nil
|
||||
begin
|
||||
wait do
|
||||
@@ -141,7 +142,9 @@ class Tmux
|
||||
self.select { |line| line.send method, val }.first
|
||||
end
|
||||
end
|
||||
yield lines
|
||||
yield(lines).tap do |ok|
|
||||
send_keys 'C-l' if refresh && !ok
|
||||
end
|
||||
end
|
||||
rescue Exception
|
||||
puts $!.backtrace
|
||||
@@ -257,6 +260,12 @@ class TestGoFZF < TestBase
|
||||
assert_equal 'hello', readonce.chomp
|
||||
end
|
||||
|
||||
def test_fzf_default_command_failure
|
||||
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', 'FZF_DEFAULT_COMMAND=false'), :Enter
|
||||
tmux.until { |lines| lines[-2].include?('ERROR') }
|
||||
tmux.send_keys :Enter
|
||||
end
|
||||
|
||||
def test_key_bindings
|
||||
tmux.send_keys "#{FZF} -q 'foo bar foo-bar'", :Enter
|
||||
tmux.until { |lines| lines.last =~ /^>/ }
|
||||
@@ -511,11 +520,11 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys "seq 1 111 | #{fzf "-m +s --tac #{opt} -q11"}", :Enter
|
||||
tmux.until { |lines| lines[-3].include? '> 111' }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| lines[-2].include? '4/111 (1)' }
|
||||
tmux.until { |lines| lines[-2].include? '4/111 -S (1)' }
|
||||
tmux.send_keys 'C-R'
|
||||
tmux.until { |lines| lines[-3].include? '> 11' }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| lines[-2].include? '4/111/S (2)' }
|
||||
tmux.until { |lines| lines[-2].include? '4/111 +S (2)' }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal ['111', '11'], readonce.split($/)
|
||||
end
|
||||
@@ -615,6 +624,17 @@ class TestGoFZF < TestBase
|
||||
], `#{FZF} -foo --tiebreak=begin,length < #{tempname}`.split($/)
|
||||
end
|
||||
|
||||
def test_tiebreak_begin_algo_v2
|
||||
writelines tempname, [
|
||||
'baz foo bar',
|
||||
'foo bar baz',
|
||||
]
|
||||
assert_equal [
|
||||
'foo bar baz',
|
||||
'baz foo bar',
|
||||
], `#{FZF} -fbar --tiebreak=begin --algo=v2 < #{tempname}`.split($/)
|
||||
end
|
||||
|
||||
def test_tiebreak_end
|
||||
writelines tempname, [
|
||||
'xoxxxxxxxx',
|
||||
@@ -670,62 +690,10 @@ class TestGoFZF < TestBase
|
||||
]
|
||||
assert_equal output, `#{FZF} -fh < #{tempname}`.split($/)
|
||||
|
||||
output = %w[
|
||||
1234567:h
|
||||
12345:he
|
||||
1:hell
|
||||
123:hello
|
||||
]
|
||||
# Since 0.16.8, --nth doesn't affect --tiebreak
|
||||
assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.split($/)
|
||||
end
|
||||
|
||||
def test_tiebreak_length_with_nth_trim_length
|
||||
input = [
|
||||
"apple juice bottle 1",
|
||||
"apple ui bottle 2",
|
||||
"app ice bottle 3",
|
||||
"app ic bottle 4",
|
||||
]
|
||||
writelines tempname, input
|
||||
|
||||
# len(1)
|
||||
output = [
|
||||
"app ice bottle 3",
|
||||
"app ic bottle 4",
|
||||
"apple juice bottle 1",
|
||||
"apple ui bottle 2",
|
||||
]
|
||||
assert_equal output, `#{FZF} -fa -n1 < #{tempname}`.split($/)
|
||||
|
||||
# len(1 ~ 2)
|
||||
output = [
|
||||
"app ic bottle 4",
|
||||
"app ice bottle 3",
|
||||
"apple ui bottle 2",
|
||||
"apple juice bottle 1",
|
||||
]
|
||||
assert_equal output, `#{FZF} -fai -n1..2 < #{tempname}`.split($/)
|
||||
|
||||
# len(1) + len(2)
|
||||
output = [
|
||||
"app ic bottle 4",
|
||||
"app ice bottle 3",
|
||||
"apple ui bottle 2",
|
||||
"apple juice bottle 1",
|
||||
]
|
||||
assert_equal output, `#{FZF} -x -f"a i" -n1,2 < #{tempname}`.split($/)
|
||||
|
||||
# len(2)
|
||||
output = [
|
||||
"app ic bottle 4",
|
||||
"app ice bottle 3",
|
||||
"apple ui bottle 2",
|
||||
"apple juice bottle 1",
|
||||
]
|
||||
assert_equal output, `#{FZF} -fi -n2 < #{tempname}`.split($/)
|
||||
assert_equal output, `#{FZF} -fi -n2,1..2 < #{tempname}`.split($/)
|
||||
end
|
||||
|
||||
def test_invalid_cache
|
||||
tmux.send_keys "(echo d; echo D; echo x) | #{fzf '-q d'}", :Enter
|
||||
tmux.until { |lines| lines[-2].include? '2/3' }
|
||||
@@ -877,7 +845,7 @@ class TestGoFZF < TestBase
|
||||
|
||||
def test_execute_multi
|
||||
output = '/tmp/fzf-test-execute-multi'
|
||||
opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{} >> #{output}; sync)\\"]
|
||||
opts = %[--multi --bind \\"alt-a:execute-multi(echo {}/{+} >> #{output}; sync)\\"]
|
||||
writelines tempname, %w[foo'bar foo"bar foo$bar foobar]
|
||||
tmux.send_keys "cat #{tempname} | #{fzf opts}", :Enter
|
||||
tmux.until { |lines| lines[-2].include? '4/4' }
|
||||
@@ -900,6 +868,43 @@ class TestGoFZF < TestBase
|
||||
File.unlink output rescue nil
|
||||
end
|
||||
|
||||
def test_execute_plus_flag
|
||||
output = tempname + ".tmp"
|
||||
File.unlink output rescue nil
|
||||
writelines tempname, ["foo bar", "123 456"]
|
||||
|
||||
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
|
||||
|
||||
execute = lambda do
|
||||
tmux.send_keys 'x', 'y'
|
||||
tmux.until { |lines| lines[-2].include? '0/2' }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| lines[-2].include? '2/2' }
|
||||
end
|
||||
|
||||
tmux.until { |lines| lines[-2].include? '2/2' }
|
||||
execute.call
|
||||
|
||||
tmux.send_keys :Up
|
||||
tmux.send_keys :Tab
|
||||
execute.call
|
||||
|
||||
tmux.send_keys :Tab
|
||||
execute.call
|
||||
|
||||
tmux.send_keys :Enter
|
||||
tmux.prepare
|
||||
readonce
|
||||
|
||||
assert_equal [
|
||||
%[foo bar/foo bar/bar/bar],
|
||||
%[123 456/foo bar/456/bar],
|
||||
%[123 456 foo bar/foo bar/456 bar/bar]
|
||||
], File.readlines(output).map(&:chomp)
|
||||
rescue
|
||||
File.unlink output rescue nil
|
||||
end
|
||||
|
||||
def test_execute_shell
|
||||
# Custom script to use as $SHELL
|
||||
output = tempname + '.out'
|
||||
@@ -925,15 +930,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
|
||||
|
||||
@@ -1067,7 +1072,7 @@ class TestGoFZF < TestBase
|
||||
}.each do |ts, exp|
|
||||
tmux.prepare
|
||||
tmux.send_keys %[cat #{tempname} | fzf --tabstop=#{ts}], :Enter
|
||||
tmux.until { |lines| exp.start_with? lines[-3].to_s.strip.sub(/\.\.$/, '') }
|
||||
tmux.until(true) { |lines| exp.start_with? lines[-3].to_s.strip.sub(/\.\.$/, '') }
|
||||
tmux.send_keys :Enter
|
||||
end
|
||||
end
|
||||
@@ -1093,12 +1098,6 @@ class TestGoFZF < TestBase
|
||||
assert_equal 1, $?.exitstatus
|
||||
end
|
||||
|
||||
def test_invalid_term
|
||||
lines = `TERM=xxx #{FZF} 2>&1`
|
||||
assert_equal 2, $?.exitstatus
|
||||
assert lines.include?('Invalid $TERM: xxx') || lines.include?('terminal entry not found')
|
||||
end
|
||||
|
||||
def test_invalid_option
|
||||
lines = `#{FZF} --foobar 2>&1`
|
||||
assert_equal 2, $?.exitstatus
|
||||
@@ -1202,7 +1201,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_preview
|
||||
tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} --preview 'sleep 0.2; echo {{}-{}}' --bind ?:toggle-preview], :Enter
|
||||
tmux.send_keys %[seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview], :Enter
|
||||
tmux.until { |lines| lines[1].include?(' {1-1}') }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| lines[1].include?(' {-}') }
|
||||
@@ -1216,6 +1215,17 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| lines[-2].start_with? ' 28/1000' }
|
||||
tmux.send_keys 'foobar'
|
||||
tmux.until { |lines| !lines[1].include?('{') }
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.until { |lines| lines.match_count == 1000 }
|
||||
tmux.until { |lines| lines[1].include?(' {1-1}') }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| lines[1].include?(' {-1}') }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| lines[1].include?(' {3-1 }') }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| lines[1].include?(' {4-1 3}') }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| lines[1].include?(' {5-1 3 4}') }
|
||||
end
|
||||
|
||||
def test_preview_hidden
|
||||
@@ -1228,6 +1238,42 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| lines[-1] == '> 555' }
|
||||
end
|
||||
|
||||
def test_preview_size_0
|
||||
File.unlink tempname rescue nil
|
||||
tmux.send_keys %[seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0], :Enter
|
||||
tmux.until { |lines| lines.item_count == 100 && lines[1] == ' 100/100' && lines[2] == '> 1' }
|
||||
tmux.until { |_| %w[1] == File.readlines(tempname).map(&:chomp) }
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| lines[3] == '> 2' }
|
||||
tmux.until { |_| %w[1 2] == File.readlines(tempname).map(&:chomp) }
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| lines[4] == '> 3' }
|
||||
tmux.until { |_| %w[1 2 3] == File.readlines(tempname).map(&:chomp) }
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
module TestShell
|
||||
@@ -1340,6 +1386,7 @@ module TestShell
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| lines.item_count > 0 }
|
||||
end
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.send_keys '3d'
|
||||
tmux.until { |lines| lines[-3].end_with? 'echo 3rd' }
|
||||
tmux.send_keys :Enter
|
||||
@@ -1375,8 +1422,7 @@ module CompletionTest
|
||||
tmux.send_keys :Tab, :Tab
|
||||
tmux.until { |lines| lines.select_count == 2 }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
tmux.until(true) do |lines|
|
||||
lines[-1].include?('/tmp/fzf-test/10') &&
|
||||
lines[-1].include?('/tmp/fzf-test/100')
|
||||
end
|
||||
@@ -1388,20 +1434,16 @@ module CompletionTest
|
||||
tmux.send_keys "'.fzf-home"
|
||||
tmux.until { |lines| lines.select { |l| l.include? '.fzf-home' }.count > 1 }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
tmux.until(true) do |lines|
|
||||
lines[-1].end_with?('.fzf-home')
|
||||
end
|
||||
|
||||
# ~INVALID_USERNAME**<TAB>
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys "cat ~such**", :Tab
|
||||
tmux.until { |lines| lines.any_include? 'no~such~user' }
|
||||
tmux.until(true) { |lines| lines.any_include? 'no~such~user' }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
lines[-1].end_with?('no~such~user')
|
||||
end
|
||||
tmux.until(true) { |lines| lines[-1].end_with?('no~such~user') }
|
||||
|
||||
# /tmp/fzf\ test**<TAB>
|
||||
tmux.send_keys 'C-u'
|
||||
@@ -1410,19 +1452,13 @@ module CompletionTest
|
||||
tmux.send_keys 'foobar$'
|
||||
tmux.until { |lines| lines.match_count == 1 }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
lines[-1].end_with?('/tmp/fzf\ test/foobar')
|
||||
end
|
||||
tmux.until(true) { |lines| lines[-1].end_with?('/tmp/fzf\ test/foobar') }
|
||||
|
||||
# Should include hidden files
|
||||
(1..100).each { |i| FileUtils.touch "/tmp/fzf-test/.hidden-#{i}" }
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-')
|
||||
end
|
||||
tmux.until(true) { |lines| lines.match_count == 100 && lines.any_include?('/tmp/fzf-test/.hidden-') }
|
||||
tmux.send_keys :Enter
|
||||
ensure
|
||||
['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f|
|
||||
@@ -1448,10 +1484,7 @@ module CompletionTest
|
||||
tmux.send_keys 55
|
||||
tmux.until { |lines| lines.match_count == 1 }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
lines[-1] == 'cd /tmp/fzf-test/d55/'
|
||||
end
|
||||
tmux.until(true) { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/' }
|
||||
tmux.send_keys :xx
|
||||
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55/xx' }
|
||||
|
||||
@@ -1479,10 +1512,7 @@ module CompletionTest
|
||||
tmux.send_keys 'sleep12345'
|
||||
tmux.until { |lines| lines.any_include? 'sleep 12345' }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
lines[-1].include? "kill #{pid}"
|
||||
end
|
||||
tmux.until(true) { |lines| lines[-1].include? "kill #{pid}" }
|
||||
ensure
|
||||
Process.kill 'KILL', pid.to_i rescue nil if pid
|
||||
end
|
||||
@@ -1495,10 +1525,7 @@ module CompletionTest
|
||||
tmux.send_keys :Tab, :Tab, :Tab
|
||||
tmux.until { |lines| lines.select_count == 3 }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-L'
|
||||
lines[-1] == "ls /tmp 1 2"
|
||||
end
|
||||
tmux.until(true) { |lines| lines[-1] == "ls /tmp 1 2" }
|
||||
end
|
||||
|
||||
def test_unset_completion
|
||||
@@ -1506,7 +1533,7 @@ module CompletionTest
|
||||
tmux.prepare
|
||||
|
||||
# Using tmux
|
||||
tmux.send_keys 'unset FZFFOO**', :Tab
|
||||
tmux.send_keys 'unset FZFFOOBR**', :Tab
|
||||
tmux.until { |lines| lines.match_count == 1 }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| lines[-1].include? 'unset FZFFOOBAR' }
|
||||
@@ -1514,8 +1541,8 @@ module CompletionTest
|
||||
|
||||
# FZF_TMUX=1
|
||||
new_shell
|
||||
tmux.send_keys 'unset FZFFO**', :Tab, pane: 0
|
||||
tmux.until(1) { |lines| lines.match_count == 1 }
|
||||
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
|
||||
@@ -1541,10 +1568,7 @@ module CompletionTest
|
||||
tmux.until { |lines| lines.select_count == 2 }
|
||||
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do |lines|
|
||||
tmux.send_keys 'C-l'
|
||||
lines.any_include? 'cat'
|
||||
end
|
||||
tmux.until(true) { |lines| lines.any_include? 'cat' }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| lines[-1].include? 'test3test4' }
|
||||
end
|
||||
|
Reference in New Issue
Block a user