m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-15 06:43:47 -05:00

Compare commits

...

43 Commits
0.9.0 ... 0.9.3

Author SHA1 Message Date
Junegunn Choi
4a1752d3fc 0.9.3 2015-02-18 13:19:20 +09:00
Junegunn Choi
b9b1eeffce Update Vader tests 2015-02-18 12:12:59 +09:00
Junegunn Choi
5667667d1f Add test case for --sync option 2015-02-18 12:07:54 +09:00
Junegunn Choi
f5b034095a Fix race condition in asynchronous -1 and -0 2015-02-18 00:51:44 +09:00
Junegunn Choi
95e5beb34e Update Homebrew instruction 2015-02-18 00:22:17 +09:00
Junegunn Choi
e808151c28 Make --select-1 and --exit-0 asynchronous 2015-02-18 00:08:17 +09:00
Junegunn Choi
d760b790b3 Fix typo in code 2015-02-17 19:28:10 +09:00
Junegunn Choi
1b5599972a Update installation instruction 2015-02-17 13:15:16 +09:00
Junegunn Choi
6c2ce28d0d Add --sync option 2015-02-13 12:25:19 +09:00
Junegunn Choi
ff09c275d4 Fix bash script when fzf_base contains spaces 2015-02-12 10:14:05 +09:00
Junegunn Choi
93dcd932e8 Merge pull request #123 from junegunn/fix-travis-ci
Fix Travis CI build
2015-01-29 17:44:11 +09:00
Junegunn Choi
e6a0de4094 Fix Travis CI build 2015-01-29 17:41:28 +09:00
Junegunn Choi
9f39671e65 Update README.md
Update outdated --help output
2015-01-28 01:45:34 +09:00
Junegunn Choi
423317b82a Update README.md 2015-01-28 01:18:20 +09:00
Junegunn Choi
47201c2c4d Merge pull request #122 from blueyed/improve-find-cdwidget
Improve `find` command for ALT-C: exclude proc/dev
2015-01-25 11:20:20 +09:00
Daniel Hahler
53d5d9d162 Improve find command for cd widgets: exclude proc/dev etc
When using the widget in "/", it would descend into 'dev/'.
Using '*' for the starting path would do so also with the new '-fstype'
excludes.

`cut -b3-` and `sed 1d` have been added to massage the different format
of the list.

This also uses `-L` with all calls to find, especially for the file
finders.

Ref: https://github.com/junegunn/fzf/pull/122
2015-01-25 03:09:02 +01:00
Junegunn Choi
9cb0cdb4ac 0.9.2 2015-01-24 14:49:21 +09:00
Junegunn Choi
448132c46c Fix error when --query contains wide-length characters 2015-01-24 13:26:33 +09:00
Junegunn Choi
1476fc7f3b Refactor test code 2015-01-24 13:25:11 +09:00
Junegunn Choi
71a7b3a26f Improve rendering performance by caching rune widths
Related: 8bead4a
2015-01-24 12:28:00 +09:00
Junegunn Choi
a47c06cb61 Fix update_assets script 2015-01-23 20:32:56 +09:00
Junegunn Choi
48e16edb47 Redraw and adjust upon terminal resize 2015-01-23 20:30:50 +09:00
Junegunn Choi
c35d98dc42 Nullify --nth option when it's irrelevant 2015-01-23 06:26:00 +09:00
Junegunn Choi
8bead4ae34 Improved handling of tab characters 2015-01-18 16:59:04 +09:00
Junegunn Choi
1b6cb3532d Update src/README.md 2015-01-18 16:34:10 +09:00
Junegunn Choi
0a0955755a Add note on installation 2015-01-18 16:32:37 +09:00
Junegunn Choi
a3101120fd Update install script 2015-01-17 20:40:00 +09:00
Junegunn Choi
30f9651f99 0.9.1 2015-01-17 14:15:26 +09:00
Junegunn Choi
4dcc0f10b8 Fix Travis CI build by ignoring trailing empty lines
😭
2015-01-17 13:45:56 +09:00
Junegunn Choi
3d39ab5ded Fix flaky tests 2015-01-17 13:39:11 +09:00
Junegunn Choi
c3a198d0c7 Add test cases for --select-1 and --exit-0 2015-01-17 12:37:24 +09:00
Junegunn Choi
be5c17612a Add basic test case for --reverse 2015-01-17 12:21:38 +09:00
Junegunn Choi
fe89ac8a89 Add script for updating release assets 2015-01-17 11:57:21 +09:00
Junegunn Choi
4c3ae847b6 Add test case for --with-nth + --multi 2015-01-17 11:20:17 +09:00
Junegunn Choi
5c0dc79ffa Print selected items in the order they are selected 2015-01-17 11:07:04 +09:00
Junegunn Choi
0a83705d21 Use Go 1.4.1 to build linux binaries 2015-01-17 10:57:07 +09:00
Junegunn Choi
ea22292d2c Merge pull request #117 from junegunn/fix-ctrl-y
Fix CTRL-Y key binding
2015-01-17 10:55:05 +09:00
Junegunn Choi
1990f3c992 Do not build i386 binary on Travis CI to speed up the process 2015-01-17 10:51:39 +09:00
Junegunn Choi
c0b432f7b4 Fix Travis-CI build 2015-01-17 10:39:18 +09:00
Junegunn Choi
ae3180f919 Fix CTRL-Y key binding
With tmux-based test cases
2015-01-17 06:04:59 +09:00
Junegunn Choi
62acb9adc4 Fix error with empty list and release 0.9.1-dev 2015-01-15 06:06:22 +09:00
Junegunn Choi
0b5fa56444 Remove brew target 2015-01-14 02:26:47 +09:00
Junegunn Choi
789f26b1a5 Add GIF to src/README 2015-01-14 02:16:03 +09:00
24 changed files with 736 additions and 165 deletions

View File

@@ -1,10 +1,24 @@
language: ruby language: ruby
sudo: false
rvm:
- "1.8.7"
- "1.9.3"
- "2.0.0"
- "2.1.1"
install: gem install curses minitest install:
- sudo apt-get update
- sudo apt-get install -y libncurses-dev lib32ncurses5-dev
- sudo add-apt-repository -y ppa:pi-rho/dev
- sudo apt-get update
- sudo apt-get install -y tmux=1.9a-1~ppa1~p
script: |
export GOROOT=~/go1.4
export GOPATH=~/go
export FZF_BASE=~/go/src/github.com/junegunn/fzf
mkdir -p ~/go/src/github.com/junegunn
ln -s $(pwd) $FZF_BASE
curl https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | tar -xz
mv go $GOROOT
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 &&
tmux new "rake test > out && touch ok" && cat out && [ -e ok ]

View File

@@ -11,6 +11,20 @@ the likes.
Installation Installation
------------ ------------
fzf project consists of the followings:
- `fzf` executable
- Shell extensions
- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
- Fuzzy auto-completion (bash)
You can [download fzf executable][bin] alone, but it's recommended that you
install the extra stuff using the attached install script.
[bin]: https://github.com/junegunn/fzf-bin/releases
### Using git (recommended)
Clone this repository and run Clone this repository and run
[install](https://github.com/junegunn/fzf/blob/master/install) script. [install](https://github.com/junegunn/fzf/blob/master/install) script.
@@ -19,6 +33,8 @@ git clone https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install ~/.fzf/install
``` ```
### Using curl
In case you don't have git installed: In case you don't have git installed:
```sh ```sh
@@ -28,15 +44,16 @@ curl -L https://github.com/junegunn/fzf/archive/master.tar.gz |
~/.fzf/install ~/.fzf/install
``` ```
The script will setup: ### Using Homebrew
- `fzf` function (bash, zsh, fish) On OS X, you can use [Homebrew](http://brew.sh/) to install fzf.
- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
- Fuzzy auto-completion (bash)
If you don't use any of the aforementioned shells, you have to manually place ```sh
fzf executable in a directory included in `$PATH`. Key bindings and brew install fzf
auto-completion will not be available in that case.
# Install shell extensions - this should be done whenever fzf is updated
/usr/local/Cellar/fzf/$(fzf --version)/install
```
### Install as Vim plugin ### Install as Vim plugin
@@ -46,8 +63,7 @@ Once you have cloned the repository, add the following line to your .vimrc.
set rtp+=~/.fzf set rtp+=~/.fzf
``` ```
Or you may use [vim-plug](https://github.com/junegunn/vim-plug) to manage fzf Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf:
inside Vim:
```vim ```vim
Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' } Plug 'junegunn/fzf', { 'dir': '~/.fzf', 'do': 'yes \| ./install' }
@@ -71,7 +87,7 @@ usage: fzf [options]
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style) -d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
Search result Search result
-s, --sort=MAX Maximum number of matched items to sort (default: 1000) -s, --sort Sort the result
+s, --no-sort Do not sort the result. Keep the sequence unchanged. +s, --no-sort Do not sort the result. Keep the sequence unchanged.
Interface Interface
@@ -89,10 +105,12 @@ usage: fzf [options]
-0, --exit-0 Exit immediately when there's no match -0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder. -f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line --print-query Print query as the first line
--sync Synchronous search for multi-staged filtering
(e.g. 'fzf --multi | fzf --sync')
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000") FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m')
``` ```
fzf will launch curses-based finder, read the list from STDIN, and write the fzf will launch curses-based finder, read the list from STDIN, and write the
@@ -542,4 +560,3 @@ Author
------ ------
Junegunn Choi Junegunn Choi

View File

@@ -2,8 +2,11 @@ require "bundler/gem_tasks"
require 'rake/testtask' require 'rake/testtask'
Rake::TestTask.new(:test) do |test| Rake::TestTask.new(:test) do |test|
test.pattern = 'test/**/test_*.rb' test.pattern = 'test/test_go.rb'
test.verbose = true end
Rake::TestTask.new(:testall) do |test|
test.pattern = 'test/test_*.rb'
end end
task :default => :test task :default => :test

41
install
View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
version=0.9.0 version=0.9.3
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base=$(pwd)
@@ -16,6 +16,7 @@ check_binary() {
local output=$("$fzf_base"/bin/fzf --version 2>&1) local output=$("$fzf_base"/bin/fzf --version 2>&1)
if [ "$version" = "$output" ]; then if [ "$version" = "$output" ]; then
echo "$output" echo "$output"
binary_error=""
else else
echo "$output != $version" echo "$output != $version"
rm -f "$fzf_base"/bin/fzf rm -f "$fzf_base"/bin/fzf
@@ -27,18 +28,21 @@ check_binary() {
symlink() { symlink() {
echo " - Creating symlink: bin/$1 -> bin/fzf" echo " - Creating symlink: bin/$1 -> bin/fzf"
(cd "$fzf_base"/bin && (cd "$fzf_base"/bin &&
rm -f fzf rm -f fzf &&
ln -sf $1 fzf) ln -sf $1 fzf)
if [ $? -ne 0 ]; then
binary_error="Failed to create symlink"
return 1
fi
} }
download() { download() {
echo "Downloading bin/fzf ..." echo "Downloading bin/fzf ..."
if [ -x "$fzf_base"/bin/fzf ]; then if [[ ! $1 =~ dev && -x "$fzf_base"/bin/fzf ]]; then
echo " - Already exists" echo " - Already exists"
check_binary && return check_binary && return
elif [ -x "$fzf_base"/bin/$1 ]; then elif [ -x "$fzf_base"/bin/$1 ]; then
symlink $1 symlink $1 && check_binary && return
check_binary && return
fi fi
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
@@ -155,7 +159,7 @@ for shell in bash zsh; do
echo -n "Generate ~/.fzf.$shell ... " echo -n "Generate ~/.fzf.$shell ... "
src=~/.fzf.${shell} src=~/.fzf.${shell}
fzf_completion="[[ \$- =~ i ]] && source $fzf_base/fzf-completion.${shell}" fzf_completion="[[ \$- =~ i ]] && source \"$fzf_base/fzf-completion.${shell}\""
if [ $auto_completion -ne 0 ]; then if [ $auto_completion -ne 0 ]; then
fzf_completion="# $fzf_completion" fzf_completion="# $fzf_completion"
fi fi
@@ -198,10 +202,10 @@ EOF
# Key bindings # Key bindings
# ------------ # ------------
__fsel() { __fsel() {
command find * -path '*/\.*' -prune \ command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | fzf -m | while read item; do -o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do
printf '%q ' "$item" printf '%q ' "$item"
done done
echo echo
@@ -222,7 +226,8 @@ __fsel_tmux() {
__fcd() { __fcd() {
local dir local dir
dir=$(command find -L ${1:-*} -path '*/\.*' -prune -o -type d -print 2> /dev/null | fzf +m) && printf 'cd %q' "$dir" dir=$(command find -L ${1:-.} \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m) && printf 'cd %q' "$dir"
} }
__use_tmux=0 __use_tmux=0
@@ -270,17 +275,16 @@ unset __use_tmux
fi fi
EOFZF EOFZF
else else # zsh
cat >> $src << "EOFZF" cat >> $src << "EOFZF"
# Key bindings # Key bindings
# ------------ # ------------
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line
__fsel() { __fsel() {
set -o nonomatch command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
command find * -path '*/\.*' -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null | fzf -m | while read item; do -o -type l -print 2> /dev/null | sed 1d | cut -b3- | fzf -m | while read item; do
printf '%q ' "$item" printf '%q ' "$item"
done done
echo echo
@@ -310,8 +314,8 @@ bindkey '^T' fzf-file-widget
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
cd "${$(set -o nonomatch; command find -L * -path '*/\.*' -prune \ cd "${$(command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type d -print 2> /dev/null | fzf):-.}" -o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m):-.}"
zle reset-prompt zle reset-prompt
} }
zle -N fzf-cd-widget zle -N fzf-cd-widget
@@ -365,14 +369,15 @@ function fzf_key_bindings
end end
function __fzf_list function __fzf_list
command find * -path '*/\.*' -prune \ command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type f -print \ -o -type f -print \
-o -type d -print \ -o -type d -print \
-o -type l -print 2> /dev/null -o -type l -print 2> /dev/null | sed 1d | cut -b3-
end end
function __fzf_list_dir function __fzf_list_dir
command find -L * -path '*/\.*' -prune -o -type d -print 2> /dev/null command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) \
-prune -o -type d -print 2> /dev/null | sed 1d | cut -b3-
end end
function __fzf_escape function __fzf_escape

View File

@@ -6,7 +6,7 @@ RUN pacman-db-upgrade && pacman -Syu --noconfirm base-devel git
# Install Go 1.4 # Install Go 1.4
RUN cd / && curl \ RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | \ https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | \
tar -xz && mv go go1.4 tar -xz && mv go go1.4
ENV GOPATH /go ENV GOPATH /go

View File

@@ -6,7 +6,7 @@ RUN yum install -y git gcc make tar ncurses-devel
# Install Go 1.4 # Install Go 1.4
RUN cd / && curl \ RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | \ https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | \
tar -xz && mv go go1.4 tar -xz && mv go go1.4
ENV GOPATH /go ENV GOPATH /go

View File

@@ -7,7 +7,7 @@ RUN apt-get update && apt-get -y upgrade && \
# Install Go 1.4 # Install Go 1.4
RUN cd / && curl \ RUN cd / && curl \
https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | \ https://storage.googleapis.com/golang/go1.4.1.linux-amd64.tar.gz | \
tar -xz && mv go go1.4 tar -xz && mv go go1.4
ENV GOPATH /go ENV GOPATH /go

View File

@@ -21,31 +21,16 @@ BINARY64 := fzf-$(GOOS)_amd64
VERSION = $(shell fzf/$(BINARY64) --version) VERSION = $(shell fzf/$(BINARY64) --version)
RELEASE32 = fzf-$(VERSION)-$(GOOS)_386 RELEASE32 = fzf-$(VERSION)-$(GOOS)_386
RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64 RELEASE64 = fzf-$(VERSION)-$(GOOS)_amd64
BREW = fzf-$(VERSION)-homebrew.tgz
all: test release all: release
brew: ../$(BREW)
../$(BREW): release
ifneq ($(UNAME_S),Darwin)
$(error brew package must be built on OS X)
endif
mkdir -p ../bin && \
cp fzf/$(RELEASE64) fzf/$(RELEASE32) ../bin && \
cd .. && ln -sf . fzf-$(VERSION) && \
tar -cvzf $(BREW) \
fzf-$(VERSION)/{{,un}install,fzf-completion.{ba,z}sh,LICENSE} \
fzf-$(VERSION)/{plugin/fzf.vim,bin/{$(RELEASE64),$(RELEASE32)}} && \
rm fzf-$(VERSION) && \
openssl sha1 $(notdir $@)
release: build release: build
cd fzf && \ cd fzf && \
cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) && \ cp $(BINARY32) $(RELEASE32) && tar -czf $(RELEASE32).tgz $(RELEASE32) && \
cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) cp $(BINARY64) $(RELEASE64) && tar -czf $(RELEASE64).tgz $(RELEASE64) && \
rm $(RELEASE32) $(RELEASE64)
build: fzf/$(BINARY32) fzf/$(BINARY64) build: test fzf/$(BINARY32) fzf/$(BINARY64)
test: test:
go get go get
@@ -86,4 +71,4 @@ $(DISTRO): docker
docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \ docker run -i -t -v $(GOPATH):/go junegunn/$(DISTRO)-sandbox \
sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash' sh -c 'cd /go/src/github.com/junegunn/fzf/src; /bin/bash'
.PHONY: all brew build release test install uninstall clean docker linux $(DISTRO) .PHONY: all build release test install uninstall clean docker linux $(DISTRO)

View File

@@ -1,6 +1,8 @@
fzf in Go 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 This directory contains the source code for the new fzf implementation in
[Go][go]. [Go][go].
@@ -17,6 +19,9 @@ git pull
./install ./install
``` ```
Otherwise, follow [the instruction][install] as before. You can also install
fzf using Homebrew if you prefer that way.
Motivations Motivations
----------- -----------
@@ -81,9 +86,6 @@ make install
# Build executables and tarballs for Linux using Docker # Build executables and tarballs for Linux using Docker
make linux make linux
# Build tarball for Homebrew release
make brew
``` ```
Contribution Contribution
@@ -111,6 +113,7 @@ License
[MIT](LICENSE) [MIT](LICENSE)
[install]: https://github.com/junegunn/fzf#installation
[go]: https://golang.org/ [go]: https://golang.org/
[gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock [gil]: http://en.wikipedia.org/wiki/Global_Interpreter_Lock
[ncurses]: https://www.gnu.org/software/ncurses/ [ncurses]: https://www.gnu.org/software/ncurses/

View File

@@ -5,7 +5,7 @@ import (
) )
// Current version // Current version
const Version = "0.9.0" const Version = "0.9.3"
// fzf events // fzf events
const ( const (

View File

@@ -95,51 +95,31 @@ func Run(options *Options) {
} }
matcher := NewMatcher(patternBuilder, opts.Sort > 0, eventBox) matcher := NewMatcher(patternBuilder, opts.Sort > 0, eventBox)
// Defered-interactive / Non-interactive // Filtering mode
// --select-1 | --exit-0 | --filter if opts.Filter != nil {
if filtering := opts.Filter != nil; filtering || opts.Select1 || opts.Exit0 { pattern := patternBuilder([]rune(*opts.Filter))
limit := 0
var patternString string
if filtering {
patternString = *opts.Filter
} else {
if opts.Select1 || opts.Exit0 {
limit = 1
}
patternString = opts.Query
}
pattern := patternBuilder([]rune(patternString))
looping := true
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
for looping { eventBox.WaitFor(EvtReadFin)
eventBox.Wait(func(events *util.Events) {
for evt := range *events {
switch evt {
case EvtReadFin:
looping = false
return
}
}
})
}
snapshot, _ := chunkList.Snapshot() snapshot, _ := chunkList.Snapshot()
merger, cancelled := matcher.scan(MatchRequest{ merger, _ := matcher.scan(MatchRequest{
chunks: snapshot, chunks: snapshot,
pattern: pattern}, limit) pattern: pattern})
if !cancelled && (filtering ||
opts.Exit0 && merger.Length() == 0 ||
opts.Select1 && merger.Length() == 1) {
if opts.PrintQuery { if opts.PrintQuery {
fmt.Println(patternString) fmt.Println(*opts.Filter)
} }
for i := 0; i < merger.Length(); i++ { for i := 0; i < merger.Length(); i++ {
fmt.Println(merger.Get(i).AsString()) fmt.Println(merger.Get(i).AsString())
} }
os.Exit(0) os.Exit(0)
} }
// Synchronous search
if opts.Sync {
eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin)
} }
// Go interactive // Go interactive
@@ -147,7 +127,11 @@ func Run(options *Options) {
// Terminal I/O // Terminal I/O
terminal := NewTerminal(opts, eventBox) terminal := NewTerminal(opts, eventBox)
deferred := opts.Select1 || opts.Exit0
go terminal.Loop() go terminal.Loop()
if !deferred {
terminal.startChan <- true
}
// Event coordination // Event coordination
reading := true reading := true
@@ -165,11 +149,11 @@ func Run(options *Options) {
reading = reading && evt == EvtReadNew reading = reading && evt == EvtReadNew
snapshot, count := chunkList.Snapshot() snapshot, count := chunkList.Snapshot()
terminal.UpdateCount(count, !reading) terminal.UpdateCount(count, !reading)
matcher.Reset(snapshot, terminal.Input(), false) matcher.Reset(snapshot, terminal.Input(), false, !reading)
case EvtSearchNew: case EvtSearchNew:
snapshot, _ := chunkList.Snapshot() snapshot, _ := chunkList.Snapshot()
matcher.Reset(snapshot, terminal.Input(), true) matcher.Reset(snapshot, terminal.Input(), true, !reading)
delay = false delay = false
case EvtSearchProgress: case EvtSearchProgress:
@@ -181,6 +165,25 @@ func Run(options *Options) {
case EvtSearchFin: case EvtSearchFin:
switch val := value.(type) { switch val := value.(type) {
case *Merger: case *Merger:
if deferred {
count := val.Length()
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
deferred = false
terminal.startChan <- true
} else if val.final {
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
if opts.PrintQuery {
fmt.Println(opts.Query)
}
for i := 0; i < count; i++ {
fmt.Println(val.Get(i).AsString())
}
os.Exit(0)
}
deferred = false
terminal.startChan <- true
}
}
terminal.UpdateList(val) terminal.UpdateList(val)
} }
} }

View File

@@ -421,6 +421,10 @@ func Clear() {
C.clear() C.clear()
} }
func Endwin() {
C.endwin()
}
func Refresh() { func Refresh() {
C.refresh() C.refresh()
} }

View File

@@ -14,6 +14,7 @@ import (
type MatchRequest struct { type MatchRequest struct {
chunks []*Chunk chunks []*Chunk
pattern *Pattern pattern *Pattern
final bool
} }
// Matcher is responsible for performing search // Matcher is responsible for performing search
@@ -86,11 +87,12 @@ func (m *Matcher) Loop() {
} }
if !foundCache { if !foundCache {
merger, cancelled = m.scan(request, 0) merger, cancelled = m.scan(request)
} }
if !cancelled { if !cancelled {
m.mergerCache[patternString] = merger m.mergerCache[patternString] = merger
merger.final = request.final
m.eventBox.Set(EvtSearchFin, merger) m.eventBox.Set(EvtSearchFin, merger)
} }
} }
@@ -121,7 +123,7 @@ type partialResult struct {
matches []*Item matches []*Item
} }
func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) { func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
startedAt := time.Now() startedAt := time.Now()
numChunks := len(request.chunks) numChunks := len(request.chunks)
@@ -175,15 +177,11 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
count++ count++
matchCount += matchesInChunk matchCount += matchesInChunk
if limit > 0 && matchCount > limit {
return nil, wait() // For --select-1 and --exit-0
}
if count == numChunks { if count == numChunks {
break break
} }
if !empty && m.reqBox.Peak(reqReset) { if !empty && m.reqBox.Peek(reqReset) {
return nil, wait() return nil, wait()
} }
@@ -201,7 +199,7 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) { func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool) {
pattern := m.patternBuilder(patternRunes) pattern := m.patternBuilder(patternRunes)
var event util.EventType var event util.EventType
@@ -210,5 +208,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) {
} else { } else {
event = reqRetry event = reqRetry
} }
m.reqBox.Set(event, MatchRequest{chunks, pattern}) m.reqBox.Set(event, MatchRequest{chunks, pattern, final})
} }

View File

@@ -12,6 +12,7 @@ type Merger struct {
merged []*Item merged []*Item
cursors []int cursors []int
sorted bool sorted bool
final bool
count int count int
} }
@@ -22,6 +23,7 @@ func NewMerger(lists [][]*Item, sorted bool) *Merger {
merged: []*Item{}, merged: []*Item{},
cursors: make([]int, len(lists)), cursors: make([]int, len(lists)),
sorted: sorted, sorted: sorted,
final: false,
count: 0} count: 0}
for _, list := range mg.lists { for _, list := range mg.lists {

View File

@@ -41,10 +41,12 @@ const usage = `usage: fzf [options]
-0, --exit-0 Exit immediately when there's no match -0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder. -f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line --print-query Print query as the first line
--sync Synchronous search for multi-staged filtering
(e.g. 'fzf --multi | fzf --sync')
Environment variables Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m") FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m')
` `
@@ -88,6 +90,7 @@ type Options struct {
Exit0 bool Exit0 bool
Filter *string Filter *string
PrintQuery bool PrintQuery bool
Sync bool
Version bool Version bool
} }
@@ -111,6 +114,7 @@ func defaultOptions() *Options {
Exit0: false, Exit0: false,
Filter: nil, Filter: nil,
PrintQuery: false, PrintQuery: false,
Sync: false,
Version: false} Version: false}
} }
@@ -244,6 +248,12 @@ func parseOptions(opts *Options, allArgs []string) {
opts.PrintQuery = false opts.PrintQuery = false
case "--prompt": case "--prompt":
opts.Prompt = nextString(allArgs, &i, "prompt string required") opts.Prompt = nextString(allArgs, &i, "prompt string required")
case "--sync":
opts.Sync = true
case "--no-sync":
opts.Sync = false
case "--async":
opts.Sync = false
case "--version": case "--version":
opts.Version = true opts.Version = true
default: default:
@@ -266,6 +276,17 @@ func parseOptions(opts *Options, allArgs []string) {
} }
} }
} }
// If we're not using extended search mode, --nth option becomes irrelevant
// if it contains the whole range
if opts.Mode == ModeFuzzy || len(opts.Nth) == 1 {
for _, r := range opts.Nth {
if r.begin == rangeEllipsis && r.end == rangeEllipsis {
opts.Nth = make([]Range, 0)
return
}
}
}
} }
// ParseOptions parses command-line options // ParseOptions parses command-line options

View File

@@ -21,17 +21,47 @@ func TestSplitNth(t *testing.T) {
} }
} }
{ {
ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2") ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
if len(ranges) != 8 || if len(ranges) != 10 ||
ranges[0].begin != rangeEllipsis || ranges[0].end != 3 || ranges[0].begin != rangeEllipsis || ranges[0].end != 3 ||
ranges[1].begin != 1 || ranges[1].end != rangeEllipsis || ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis ||
ranges[2].begin != 2 || ranges[2].end != 3 || ranges[2].begin != 2 || ranges[2].end != 3 ||
ranges[3].begin != 4 || ranges[3].end != -1 || ranges[3].begin != 4 || ranges[3].end != rangeEllipsis ||
ranges[4].begin != -3 || ranges[4].end != -2 || ranges[4].begin != -3 || ranges[4].end != -2 ||
ranges[5].begin != rangeEllipsis || ranges[5].end != rangeEllipsis || ranges[5].begin != rangeEllipsis || ranges[5].end != rangeEllipsis ||
ranges[6].begin != 2 || ranges[6].end != 2 || ranges[6].begin != 2 || ranges[6].end != 2 ||
ranges[7].begin != -2 || ranges[7].end != -2 { ranges[7].begin != -2 || ranges[7].end != -2 ||
ranges[8].begin != 2 || ranges[8].end != -2 ||
ranges[9].begin != rangeEllipsis || ranges[9].end != rangeEllipsis {
t.Errorf("%s", ranges) t.Errorf("%s", ranges)
} }
} }
} }
func TestIrrelevantNth(t *testing.T) {
{
opts := defaultOptions()
words := []string{"--nth", "..", "-x"}
parseOptions(opts, words)
if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %s", opts.Nth)
}
}
for _, words := range [][]string{[]string{"--nth", "..,3"}, []string{"--nth", "3,1.."}, []string{"--nth", "..-1,1"}} {
{
opts := defaultOptions()
parseOptions(opts, words)
if len(opts.Nth) != 0 {
t.Errorf("nth should be empty: %s", opts.Nth)
}
}
{
opts := defaultOptions()
words = append(words, "-x")
parseOptions(opts, words)
if len(opts.Nth) != 2 {
t.Errorf("nth should not be empty: %s", opts.Nth)
}
}
}
}

View File

@@ -14,7 +14,7 @@ func TestReadFromCommand(t *testing.T) {
eventBox: eb} eventBox: eb}
// Check EventBox // Check EventBox
if eb.Peak(EvtReadNew) { if eb.Peek(EvtReadNew) {
t.Error("EvtReadNew should not be set yet") t.Error("EvtReadNew should not be set yet")
} }
@@ -25,7 +25,7 @@ func TestReadFromCommand(t *testing.T) {
} }
// Check EventBox again // Check EventBox again
if !eb.Peak(EvtReadNew) { if !eb.Peek(EvtReadNew) {
t.Error("EvtReadNew should be set yet") t.Error("EvtReadNew should be set yet")
} }
@@ -38,7 +38,7 @@ func TestReadFromCommand(t *testing.T) {
}) })
// EventBox is cleared // EventBox is cleared
if eb.Peak(EvtReadNew) { if eb.Peek(EvtReadNew) {
t.Error("EvtReadNew should not be set yet") t.Error("EvtReadNew should not be set yet")
} }
@@ -50,7 +50,7 @@ func TestReadFromCommand(t *testing.T) {
} }
// Check EventBox again // Check EventBox again
if eb.Peak(EvtReadNew) { if eb.Peek(EvtReadNew) {
t.Error("Command failed. EvtReadNew should be set") t.Error("Command failed. EvtReadNew should be set")
} }
} }

View File

@@ -1,11 +1,15 @@
package fzf package fzf
import ( import (
"bytes"
"fmt" "fmt"
"os" "os"
"os/signal"
"regexp" "regexp"
"sort" "sort"
"strings"
"sync" "sync"
"syscall"
"time" "time"
C "github.com/junegunn/fzf/src/curses" C "github.com/junegunn/fzf/src/curses"
@@ -30,15 +34,36 @@ type Terminal struct {
progress int progress int
reading bool reading bool
merger *Merger merger *Merger
selected map[*string]*string selected map[*string]selectedItem
reqBox *util.EventBox reqBox *util.EventBox
eventBox *util.EventBox eventBox *util.EventBox
mutex sync.Mutex mutex sync.Mutex
initFunc func() initFunc func()
suppress bool suppress bool
startChan chan bool
}
type selectedItem struct {
at time.Time
text *string
}
type ByTimeOrder []selectedItem
func (a ByTimeOrder) Len() int {
return len(a)
}
func (a ByTimeOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByTimeOrder) Less(i, j int) bool {
return a[i].at.Before(a[j].at)
} }
var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
var _runeWidths = make(map[rune]int)
const ( const (
reqPrompt util.EventType = iota reqPrompt util.EventType = iota
@@ -62,7 +87,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
prompt: opts.Prompt, prompt: opts.Prompt,
tac: opts.Sort == 0, tac: opts.Sort == 0,
reverse: opts.Reverse, reverse: opts.Reverse,
cx: displayWidth(input), cx: len(input),
cy: 0, cy: 0,
offset: 0, offset: 0,
yanked: []rune{}, yanked: []rune{},
@@ -70,11 +95,12 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
multi: opts.Multi, multi: opts.Multi,
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[*string]*string), selected: make(map[*string]selectedItem),
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
eventBox: eventBox, eventBox: eventBox,
mutex: sync.Mutex{}, mutex: sync.Mutex{},
suppress: true, suppress: true,
startChan: make(chan bool, 1),
initFunc: func() { initFunc: func() {
C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse) C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse)
}} }}
@@ -134,24 +160,38 @@ func (t *Terminal) output() {
fmt.Println(string(t.input)) fmt.Println(string(t.input))
} }
if len(t.selected) == 0 { if len(t.selected) == 0 {
if t.merger.Length() > t.cy { cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy {
fmt.Println(t.merger.Get(t.listIndex(t.cy)).AsString()) fmt.Println(t.merger.Get(t.listIndex(t.cy)).AsString())
} }
} else { } else {
for ptr, orig := range t.selected { sels := make([]selectedItem, 0, len(t.selected))
if orig != nil { for _, sel := range t.selected {
fmt.Println(*orig) sels = append(sels, sel)
}
sort.Sort(ByTimeOrder(sels))
for _, sel := range sels {
fmt.Println(*sel.text)
}
}
}
func runeWidth(r rune, prefixWidth int) int {
if r == '\t' {
return 8 - prefixWidth%8
} else if w, found := _runeWidths[r]; found {
return w
} else { } else {
fmt.Println(*ptr) w := runewidth.RuneWidth(r)
} _runeWidths[r] = w
} return w
} }
} }
func displayWidth(runes []rune) int { func displayWidth(runes []rune) int {
l := 0 l := 0
for _, r := range runes { for _, r := range runes {
l += runewidth.RuneWidth(r) l += runeWidth(r, l)
} }
return l return l
} }
@@ -233,16 +273,27 @@ func (t *Terminal) printItem(item *Item, current bool) {
} }
func trimRight(runes []rune, width int) ([]rune, int) { func trimRight(runes []rune, width int) ([]rune, int) {
currentWidth := displayWidth(runes) // We start from the beginning to handle tab characters
trimmed := 0 l := 0
for idx, r := range runes {
for currentWidth > width && len(runes) > 0 { l += runeWidth(r, l)
sz := len(runes) if idx > 0 && l > width {
currentWidth -= runewidth.RuneWidth(runes[sz-1]) return runes[:idx], len(runes) - idx
runes = runes[:sz-1]
trimmed++
} }
return runes, trimmed }
return runes, 0
}
func displayWidthWithLimit(runes []rune, prefixWidth int, limit int) int {
l := 0
for _, r := range runes {
l += runeWidth(r, l+prefixWidth)
if l > limit {
// Early exit
return l
}
}
return l
} }
func trimLeft(runes []rune, width int) ([]rune, int32) { func trimLeft(runes []rune, width int) ([]rune, int32) {
@@ -250,9 +301,9 @@ func trimLeft(runes []rune, width int) ([]rune, int32) {
var trimmed int32 var trimmed int32
for currentWidth > width && len(runes) > 0 { for currentWidth > width && len(runes) > 0 {
currentWidth -= runewidth.RuneWidth(runes[0])
runes = runes[1:] runes = runes[1:]
trimmed++ trimmed++
currentWidth = displayWidthWithLimit(runes, 2, width)
} }
return runes, trimmed return runes, trimmed
} }
@@ -302,18 +353,41 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) {
sort.Sort(ByOrder(offsets)) sort.Sort(ByOrder(offsets))
var index int32 var index int32
var substr string
var prefixWidth int
for _, offset := range offsets { for _, offset := range offsets {
b := util.Max32(index, offset[0]) b := util.Max32(index, offset[0])
e := util.Max32(index, offset[1]) e := util.Max32(index, offset[1])
C.CPrint(col1, bold, string(text[index:b]))
C.CPrint(col2, bold, string(text[b:e])) substr, prefixWidth = processTabs(text[index:b], prefixWidth)
C.CPrint(col1, bold, substr)
substr, prefixWidth = processTabs(text[b:e], prefixWidth)
C.CPrint(col2, bold, substr)
index = e index = e
} }
if index < int32(len(text)) { if index < int32(len(text)) {
C.CPrint(col1, bold, string(text[index:])) substr, _ = processTabs(text[index:], prefixWidth)
C.CPrint(col1, bold, substr)
} }
} }
func processTabs(runes []rune, prefixWidth int) (string, int) {
var strbuf bytes.Buffer
l := prefixWidth
for _, r := range runes {
w := runeWidth(r, l)
l += w
if r == '\t' {
strbuf.WriteString(strings.Repeat(" ", w))
} else {
strbuf.WriteRune(r)
}
}
return strbuf.String(), l
}
func (t *Terminal) printAll() { func (t *Terminal) printAll() {
t.printList() t.printList()
t.printInfo() t.printInfo()
@@ -374,6 +448,7 @@ func (t *Terminal) rubout(pattern string) {
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
func (t *Terminal) Loop() { func (t *Terminal) Loop() {
<-t.startChan
{ // Late initialization { // Late initialization
t.mutex.Lock() t.mutex.Lock()
t.initFunc() t.initFunc()
@@ -387,6 +462,15 @@ func (t *Terminal) Loop() {
<-timer.C <-timer.C
t.reqBox.Set(reqRefresh, nil) t.reqBox.Set(reqRefresh, nil)
}() }()
resizeChan := make(chan os.Signal, 1)
signal.Notify(resizeChan, syscall.SIGWINCH)
go func() {
for {
<-resizeChan
t.reqBox.Set(reqRedraw, nil)
}
}()
} }
go func() { go func() {
@@ -406,6 +490,8 @@ func (t *Terminal) Loop() {
t.suppress = false t.suppress = false
case reqRedraw: case reqRedraw:
C.Clear() C.Clear()
C.Endwin()
C.Refresh()
t.printAll() t.printAll()
case reqClose: case reqClose:
C.Close() C.Close()
@@ -443,7 +529,13 @@ func (t *Terminal) Loop() {
if idx < t.merger.Length() { if idx < t.merger.Length() {
item := t.merger.Get(idx) item := t.merger.Get(idx)
if _, found := t.selected[item.text]; !found { if _, found := t.selected[item.text]; !found {
t.selected[item.text] = item.origText var strptr *string
if item.origText != nil {
strptr = item.origText
} else {
strptr = item.text
}
t.selected[item.text] = selectedItem{time.Now(), strptr}
} else { } else {
delete(t.selected, item.text) delete(t.selected, item.text)
} }
@@ -514,7 +606,8 @@ func (t *Terminal) Loop() {
t.rubout("[^[:alnum:]][[:alnum:]]") t.rubout("[^[:alnum:]][[:alnum:]]")
} }
case C.CtrlY: case C.CtrlY:
t.input = append(append(t.input[:t.cx], t.yanked...), t.input[t.cx:]...) suffix := copySlice(t.input[t.cx:])
t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
t.cx += len(t.yanked) t.cx += len(t.yanked)
case C.Del: case C.Del:
t.delChar() t.delChar()

View File

@@ -28,22 +28,32 @@ type Token struct {
prefixLength int prefixLength int
} }
func newRange(begin int, end int) Range {
if begin == 1 {
begin = rangeEllipsis
}
if end == -1 {
end = rangeEllipsis
}
return Range{begin, end}
}
// ParseRange parses nth-expression and returns the corresponding Range object // ParseRange parses nth-expression and returns the corresponding Range object
func ParseRange(str *string) (Range, bool) { func ParseRange(str *string) (Range, bool) {
if (*str) == ".." { if (*str) == ".." {
return Range{rangeEllipsis, rangeEllipsis}, true return newRange(rangeEllipsis, rangeEllipsis), true
} else if strings.HasPrefix(*str, "..") { } else if strings.HasPrefix(*str, "..") {
end, err := strconv.Atoi((*str)[2:]) end, err := strconv.Atoi((*str)[2:])
if err != nil || end == 0 { if err != nil || end == 0 {
return Range{}, false return Range{}, false
} }
return Range{rangeEllipsis, end}, true return newRange(rangeEllipsis, end), true
} else if strings.HasSuffix(*str, "..") { } else if strings.HasSuffix(*str, "..") {
begin, err := strconv.Atoi((*str)[:len(*str)-2]) begin, err := strconv.Atoi((*str)[:len(*str)-2])
if err != nil || begin == 0 { if err != nil || begin == 0 {
return Range{}, false return Range{}, false
} }
return Range{begin, rangeEllipsis}, true return newRange(begin, rangeEllipsis), true
} else if strings.Contains(*str, "..") { } else if strings.Contains(*str, "..") {
ns := strings.Split(*str, "..") ns := strings.Split(*str, "..")
if len(ns) != 2 { if len(ns) != 2 {
@@ -51,17 +61,17 @@ func ParseRange(str *string) (Range, bool) {
} }
begin, err1 := strconv.Atoi(ns[0]) begin, err1 := strconv.Atoi(ns[0])
end, err2 := strconv.Atoi(ns[1]) end, err2 := strconv.Atoi(ns[1])
if err1 != nil || err2 != nil { if err1 != nil || err2 != nil || begin == 0 || end == 0 {
return Range{}, false return Range{}, false
} }
return Range{begin, end}, true return newRange(begin, end), true
} }
n, err := strconv.Atoi(*str) n, err := strconv.Atoi(*str)
if err != nil || n == 0 { if err != nil || n == 0 {
return Range{}, false return Range{}, false
} }
return Range{n, n}, true return newRange(n, n), true
} }
func withPrefixLengths(tokens []string, begin int) []Token { func withPrefixLengths(tokens []string, begin int) []Token {

42
src/update_assets.rb Executable file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env ruby
# http://www.rubydoc.info/github/rest-client/rest-client/RestClient
require 'rest_client'
if ARGV.length < 3
puts "usage: #$0 <token> <version> <files...>"
exit 1
end
token, version, *files = ARGV
base = "https://api.github.com/repos/junegunn/fzf-bin/releases"
# List releases
rels = JSON.parse(RestClient.get(base, :authorization => "token #{token}"))
rel = rels.find { |r| r['tag_name'] == version }
unless rel
puts "#{version} not found"
exit 1
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
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

View File

@@ -53,8 +53,8 @@ func (events *Events) Clear() {
} }
} }
// Peak peaks at the event box if the given event is set // Peek peeks at the event box if the given event is set
func (b *EventBox) Peak(event EventType) bool { func (b *EventBox) Peek(event EventType) bool {
b.cond.L.Lock() b.cond.L.Lock()
defer b.cond.L.Unlock() defer b.cond.L.Unlock()
_, ok := b.events[event] _, ok := b.events[event]
@@ -78,3 +78,18 @@ func (b *EventBox) Unwatch(events ...EventType) {
b.ignore[event] = true b.ignore[event] = true
} }
} }
func (b *EventBox) WaitFor(event EventType) {
looping := true
for looping {
b.Wait(func(events *Events) {
for evt := range *events {
switch evt {
case event:
looping = false
return
}
}
})
}
}

View File

@@ -7,16 +7,16 @@ Execute (fzf#run with dir option):
AssertEqual ['fzf.vader'], result AssertEqual ['fzf.vader'], result
let result = sort(fzf#run({ 'options': '--filter e', 'dir': g:dir })) let result = sort(fzf#run({ 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_fzf.rb'], result AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], result
Execute (fzf#run with Funcref command): Execute (fzf#run with Funcref command):
let g:ret = [] let g:ret = []
function! g:proc(e) function! g:FzfTest(e)
call add(g:ret, a:e) call add(g:ret, a:e)
endfunction endfunction
let result = sort(fzf#run({ 'sink': function('g:proc'), 'options': '--filter e', 'dir': g:dir })) let result = sort(fzf#run({ 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_fzf.rb'], result AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], result
AssertEqual ['fzf.vader', 'test_fzf.rb'], sort(g:ret) AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], sort(g:ret)
Execute (fzf#run with string source): Execute (fzf#run with string source):
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' })) let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))

326
test/test_go.rb Normal file
View File

@@ -0,0 +1,326 @@
#!/usr/bin/env ruby
# encoding: utf-8
require 'minitest/autorun'
class NilClass
def include? str
false
end
end
module Temp
def readonce
name = self.class::TEMPNAME
waited = 0
while waited < 5
begin
data = File.read(name)
return data unless data.empty?
rescue
sleep 0.1
waited += 0.1
end
end
raise "failed to read tempfile"
ensure
while File.exists? name
File.unlink name rescue nil
end
end
end
class Tmux
include Temp
TEMPNAME = '/tmp/fzf-test.txt'
attr_reader :win
def initialize shell = 'bash'
@win = go("new-window -d -P -F '#I' 'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.#{shell}'").first
@lines = `tput lines`.chomp.to_i
end
def closed?
!go("list-window -F '#I'").include?(win)
end
def close timeout = 1
send_keys 'C-c', 'C-u', 'exit', :Enter
wait(timeout) { closed? }
end
def kill
go("kill-window -t #{win} 2> /dev/null")
end
def send_keys *args
args = args.map { |a| %{"#{a}"} }.join ' '
go("send-keys -t #{win} #{args}")
end
def capture
go("capture-pane -t #{win} \\; save-buffer #{TEMPNAME}")
raise "Window not found" if $?.exitstatus != 0
readonce.split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
end
def until timeout = 1
wait(timeout) { yield capture }
end
private
def wait timeout = 1
waited = 0
until yield
waited += 0.1
sleep 0.1
if waited > timeout
hl = '=' * 10
puts hl
capture.each_with_index do |line, idx|
puts [idx.to_s.rjust(2), line].join(': ')
end
puts hl
raise "timeout"
end
end
end
def go *args
%x[tmux #{args.join ' '}].split($/)
end
end
class TestGoFZF < MiniTest::Unit::TestCase
include Temp
FIN = 'FIN'
TEMPNAME = '/tmp/output'
attr_reader :tmux
def setup
ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_DEFAULT_COMMAND'
@tmux = Tmux.new
end
def teardown
@tmux.kill
end
def fzf(*opts)
fzf!(*opts) + " > #{TEMPNAME} && echo #{FIN}"
end
def fzf!(*opts)
opts = opts.map { |o|
case o
when Symbol
o = o.to_s
o.length > 1 ? "--#{o.gsub('_', '-')}" : "-#{o}"
when String, Numeric
o.to_s
else
nil
end
}.compact
"fzf #{opts.join ' '}"
end
def test_vanilla
tmux.send_keys "seq 1 100000 | #{fzf}", :Enter
tmux.until(10) { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ }
lines = tmux.capture
assert_equal ' 2', lines[-4]
assert_equal '> 1', lines[-3]
assert_equal ' 100000/100000', lines[-2]
assert_equal '>', lines[-1]
# Testing basic key bindings
tmux.send_keys '99', 'C-a', '1', 'C-f', '3', 'C-b', 'C-h', 'C-u', 'C-e', 'C-y', 'C-k', 'Tab', 'BTab'
tmux.until { |lines| lines[-2] == ' 856/100000' }
lines = tmux.capture
assert_equal '> 1391', lines[-4]
assert_equal ' 391', lines[-3]
assert_equal ' 856/100000', lines[-2]
assert_equal '> 391', lines[-1]
tmux.send_keys :Enter
tmux.close
assert_equal '1391', readonce.chomp
end
def test_fzf_default_command
tmux.send_keys "FZF_DEFAULT_COMMAND='echo hello' #{fzf}", :Enter
tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Enter
tmux.close
assert_equal 'hello', readonce.chomp
end
def test_key_bindings
tmux.send_keys "fzf -q 'foo bar foo-bar'", :Enter
tmux.until { |lines| lines.last =~ /^>/ }
# CTRL-A
tmux.send_keys "C-A", "("
tmux.until { |lines| lines.last == '> (foo bar foo-bar' }
# META-F
tmux.send_keys :Escape, :f, ")"
tmux.until { |lines| lines.last == '> (foo) bar foo-bar' }
# CTRL-B
tmux.send_keys "C-B", "var"
tmux.until { |lines| lines.last == '> (foovar) bar foo-bar' }
# Left, CTRL-D
tmux.send_keys :Left, :Left, "C-D"
tmux.until { |lines| lines.last == '> (foovr) bar foo-bar' }
# META-BS
tmux.send_keys :Escape, :BSpace
tmux.until { |lines| lines.last == '> (r) bar foo-bar' }
# CTRL-Y
tmux.send_keys "C-Y", "C-Y"
tmux.until { |lines| lines.last == '> (foovfoovr) bar foo-bar' }
# META-B
tmux.send_keys :Escape, :b, :Space, :Space
tmux.until { |lines| lines.last == '> ( foovfoovr) bar foo-bar' }
# CTRL-F / Right
tmux.send_keys 'C-F', :Right, '/'
tmux.until { |lines| lines.last == '> ( fo/ovfoovr) bar foo-bar' }
# CTRL-H / BS
tmux.send_keys 'C-H', :BSpace
tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-bar' }
# CTRL-E
tmux.send_keys "C-E", 'baz'
tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-barbaz' }
# CTRL-U
tmux.send_keys "C-U"
tmux.until { |lines| lines.last == '>' }
# CTRL-Y
tmux.send_keys "C-Y"
tmux.until { |lines| lines.last == '> ( fovfoovr) bar foo-barbaz' }
# CTRL-W
tmux.send_keys "C-W", "bar-foo"
tmux.until { |lines| lines.last == '> ( fovfoovr) bar bar-foo' }
# META-D
tmux.send_keys :Escape, :b, :Escape, :b, :Escape, :d, "C-A", "C-Y"
tmux.until { |lines| lines.last == '> bar( fovfoovr) bar -foo' }
# CTRL-M
tmux.send_keys "C-M"
tmux.until { |lines| lines.last !~ /^>/ }
tmux.close
end
def test_multi_order
tmux.send_keys "seq 1 10 | #{fzf :multi}", :Enter
tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Tab, :Up, :Up, :Tab, :Tab, :Tab, # 3, 2
'C-K', 'C-K', 'C-K', 'C-K', :BTab, :BTab, # 5, 6
:PgUp, 'C-J', :Down, :Tab, :Tab # 8, 7
tmux.until { |lines| lines[-2].include? '(6)' }
tmux.send_keys "C-M"
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal %w[3 2 5 6 8 7], readonce.split($/)
tmux.close
end
def test_with_nth
[true, false].each do |multi|
tmux.send_keys "(echo ' 1st 2nd 3rd/';
echo ' first second third/') |
#{fzf multi && :multi, :x, :nth, 2, :with_nth, '2,-1,1'}",
:Enter
tmux.until { |lines| lines[-2].include?('2/2') }
# Transformed list
lines = tmux.capture
assert_equal ' second third/first', lines[-4]
assert_equal '> 2nd 3rd/1st', lines[-3]
# However, the output must not be transformed
if multi
tmux.send_keys :BTab, :BTab, :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal [' 1st 2nd 3rd/', ' first second third/'], readonce.split($/)
else
tmux.send_keys '^', '3'
tmux.until { |lines| lines[-2].include?('1/2') }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal [' 1st 2nd 3rd/'], readonce.split($/)
end
end
end
def test_scroll
[true, false].each do |rev|
tmux.send_keys "seq 1 100 | #{fzf rev && :reverse}", :Enter
tmux.until { |lines| lines.include? ' 100/100' }
tmux.send_keys *110.times.map { rev ? :Down : :Up }
tmux.until { |lines| lines.include? '> 100' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal '100', readonce.chomp
end
end
def test_select_1
tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 5555, :'1'}", :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['5555', '55'], readonce.split($/)
end
def test_exit_0
tmux.send_keys "seq 1 100 | #{fzf :with_nth, '..,..', :print_query, :q, 555555, :'0'}", :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['555555'], readonce.split($/)
end
def test_select_1_exit_0_fail
[:'0', :'1', [:'1', :'0']].each do |opt|
tmux.send_keys "seq 1 100 | #{fzf :print_query, :multi, :q, 5, *opt}", :Enter
tmux.until { |lines| lines.last =~ /^> 5/ }
tmux.send_keys :BTab, :BTab, :BTab, :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['5', '5', '15', '25'], readonce.split($/)
end
end
def test_query_unicode
tmux.send_keys "(echo abc; echo 가나다) | #{fzf :query, '가다'}", :Enter
tmux.until { |lines| lines.last.start_with? '>' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1].include?(FIN) }
assert_equal ['가나다'], readonce.split($/)
end
def test_sync
tmux.send_keys "seq 1 100 | #{fzf! :multi} | awk '{print \\$1 \\$1}' | #{fzf :sync}", :Enter
tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 9
tmux.until { |lines| lines[-2] == ' 19/100' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter
tmux.until { |lines| lines[-1] == '>' }
tmux.send_keys 'C-K', :Enter
assert_equal ['1919'], readonce.split($/)
end
end

View File

@@ -54,7 +54,7 @@ class MockTTY
end end
end end
class TestFZF < MiniTest::Unit::TestCase class TestRubyFZF < MiniTest::Unit::TestCase
def setup def setup
ENV.delete 'FZF_DEFAULT_SORT' ENV.delete 'FZF_DEFAULT_SORT'
ENV.delete 'FZF_DEFAULT_OPTS' ENV.delete 'FZF_DEFAULT_OPTS'