m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-13 22:03:47 -05:00

Compare commits

..

26 Commits
0.9.1 ... 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
18 changed files with 386 additions and 166 deletions

View File

@@ -1,6 +1,7 @@
language: ruby
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

View File

@@ -11,6 +11,20 @@ the likes.
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
[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
```
### Using curl
In case you don't have git installed:
```sh
@@ -28,15 +44,16 @@ curl -L https://github.com/junegunn/fzf/archive/master.tar.gz |
~/.fzf/install
```
The script will setup:
### Using Homebrew
- `fzf` function (bash, zsh, fish)
- Key bindings (`CTRL-T`, `CTRL-R`, and `ALT-C`) (bash, zsh, fish)
- Fuzzy auto-completion (bash)
On OS X, you can use [Homebrew](http://brew.sh/) to install fzf.
If you don't use any of the aforementioned shells, you have to manually place
fzf executable in a directory included in `$PATH`. Key bindings and
auto-completion will not be available in that case.
```sh
brew install fzf
# Install shell extensions - this should be done whenever fzf is updated
/usr/local/Cellar/fzf/$(fzf --version)/install
```
### Install as Vim plugin
@@ -46,8 +63,7 @@ Once you have cloned the repository, add the following line to your .vimrc.
set rtp+=~/.fzf
```
Or you may use [vim-plug](https://github.com/junegunn/vim-plug) to manage fzf
inside Vim:
Or you can have [vim-plug](https://github.com/junegunn/vim-plug) manage fzf:
```vim
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)
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.
Interface
@@ -89,10 +105,12 @@ usage: fzf [options]
-0, --exit-0 Exit immediately when there's no match
-f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line
--sync Synchronous search for multi-staged filtering
(e.g. 'fzf --multi | fzf --sync')
Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m --sort 10000")
FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m')
```
fzf will launch curses-based finder, read the list from STDIN, and write the
@@ -542,4 +560,3 @@ Author
------
Junegunn Choi

29
install
View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
version=0.9.1
version=0.9.3
cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd)
@@ -159,7 +159,7 @@ for shell in bash zsh; do
echo -n "Generate ~/.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
fzf_completion="# $fzf_completion"
fi
@@ -202,10 +202,10 @@ EOF
# Key bindings
# ------------
__fsel() {
command find * -path '*/\.*' -prune \
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | 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"
done
echo
@@ -226,7 +226,8 @@ __fsel_tmux() {
__fcd() {
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
@@ -274,17 +275,16 @@ unset __use_tmux
fi
EOFZF
else
else # zsh
cat >> $src << "EOFZF"
# Key bindings
# ------------
# CTRL-T - Paste the selected file path(s) into the command line
__fsel() {
set -o nonomatch
command find * -path '*/\.*' -prune \
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null | 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"
done
echo
@@ -314,8 +314,8 @@ bindkey '^T' fzf-file-widget
# ALT-C - cd into the selected directory
fzf-cd-widget() {
cd "${$(set -o nonomatch; command find -L * -path '*/\.*' -prune \
-o -type d -print 2> /dev/null | fzf):-.}"
cd "${$(command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type d -print 2> /dev/null | sed 1d | cut -b3- | fzf +m):-.}"
zle reset-prompt
}
zle -N fzf-cd-widget
@@ -369,14 +369,15 @@ function fzf_key_bindings
end
function __fzf_list
command find * -path '*/\.*' -prune \
command find -L . \( -path '*/\.*' -o -fstype 'dev' -o -fstype 'proc' \) -prune \
-o -type f -print \
-o -type d -print \
-o -type l -print 2> /dev/null
-o -type l -print 2> /dev/null | sed 1d | cut -b3-
end
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
function __fzf_escape

View File

@@ -19,6 +19,9 @@ git pull
./install
```
Otherwise, follow [the instruction][install] as before. You can also install
fzf using Homebrew if you prefer that way.
Motivations
-----------
@@ -110,6 +113,7 @@ 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/

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ import (
type MatchRequest struct {
chunks []*Chunk
pattern *Pattern
final bool
}
// Matcher is responsible for performing search
@@ -86,11 +87,12 @@ func (m *Matcher) Loop() {
}
if !foundCache {
merger, cancelled = m.scan(request, 0)
merger, cancelled = m.scan(request)
}
if !cancelled {
m.mergerCache[patternString] = merger
merger.final = request.final
m.eventBox.Set(EvtSearchFin, merger)
}
}
@@ -121,7 +123,7 @@ type partialResult struct {
matches []*Item
}
func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
startedAt := time.Now()
numChunks := len(request.chunks)
@@ -175,15 +177,11 @@ func (m *Matcher) scan(request MatchRequest, limit int) (*Merger, bool) {
count++
matchCount += matchesInChunk
if limit > 0 && matchCount > limit {
return nil, wait() // For --select-1 and --exit-0
}
if count == numChunks {
break
}
if !empty && m.reqBox.Peak(reqReset) {
if !empty && m.reqBox.Peek(reqReset) {
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
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)
var event util.EventType
@@ -210,5 +208,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool) {
} else {
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
cursors []int
sorted bool
final bool
count int
}
@@ -22,6 +23,7 @@ func NewMerger(lists [][]*Item, sorted bool) *Merger {
merged: []*Item{},
cursors: make([]int, len(lists)),
sorted: sorted,
final: false,
count: 0}
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
-f, --filter=STR Filter mode. Do not start interactive finder.
--print-query Print query as the first line
--sync Synchronous search for multi-staged filtering
(e.g. 'fzf --multi | fzf --sync')
Environment variables
FZF_DEFAULT_COMMAND Default command to use when input is tty
FZF_DEFAULT_OPTS Defaults options. (e.g. "-x -m")
FZF_DEFAULT_OPTS Defaults options. (e.g. '-x -m')
`
@@ -88,6 +90,7 @@ type Options struct {
Exit0 bool
Filter *string
PrintQuery bool
Sync bool
Version bool
}
@@ -111,6 +114,7 @@ func defaultOptions() *Options {
Exit0: false,
Filter: nil,
PrintQuery: false,
Sync: false,
Version: false}
}
@@ -244,6 +248,12 @@ func parseOptions(opts *Options, allArgs []string) {
opts.PrintQuery = false
case "--prompt":
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":
opts.Version = true
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

View File

@@ -21,17 +21,47 @@ func TestSplitNth(t *testing.T) {
}
}
{
ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2")
if len(ranges) != 8 ||
ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
if len(ranges) != 10 ||
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[3].begin != 4 || ranges[3].end != -1 ||
ranges[3].begin != 4 || ranges[3].end != rangeEllipsis ||
ranges[4].begin != -3 || ranges[4].end != -2 ||
ranges[5].begin != rangeEllipsis || ranges[5].end != rangeEllipsis ||
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)
}
}
}
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}
// Check EventBox
if eb.Peak(EvtReadNew) {
if eb.Peek(EvtReadNew) {
t.Error("EvtReadNew should not be set yet")
}
@@ -25,7 +25,7 @@ func TestReadFromCommand(t *testing.T) {
}
// Check EventBox again
if !eb.Peak(EvtReadNew) {
if !eb.Peek(EvtReadNew) {
t.Error("EvtReadNew should be set yet")
}
@@ -38,7 +38,7 @@ func TestReadFromCommand(t *testing.T) {
})
// EventBox is cleared
if eb.Peak(EvtReadNew) {
if eb.Peek(EvtReadNew) {
t.Error("EvtReadNew should not be set yet")
}
@@ -50,7 +50,7 @@ func TestReadFromCommand(t *testing.T) {
}
// Check EventBox again
if eb.Peak(EvtReadNew) {
if eb.Peek(EvtReadNew) {
t.Error("Command failed. EvtReadNew should be set")
}
}

View File

@@ -1,11 +1,15 @@
package fzf
import (
"bytes"
"fmt"
"os"
"os/signal"
"regexp"
"sort"
"strings"
"sync"
"syscall"
"time"
C "github.com/junegunn/fzf/src/curses"
@@ -36,6 +40,7 @@ type Terminal struct {
mutex sync.Mutex
initFunc func()
suppress bool
startChan chan bool
}
type selectedItem struct {
@@ -58,6 +63,7 @@ func (a ByTimeOrder) Less(i, j int) bool {
}
var _spinner = []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
var _runeWidths = make(map[rune]int)
const (
reqPrompt util.EventType = iota
@@ -81,7 +87,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
prompt: opts.Prompt,
tac: opts.Sort == 0,
reverse: opts.Reverse,
cx: displayWidth(input),
cx: len(input),
cy: 0,
offset: 0,
yanked: []rune{},
@@ -94,6 +100,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
eventBox: eventBox,
mutex: sync.Mutex{},
suppress: true,
startChan: make(chan bool, 1),
initFunc: func() {
C.Init(opts.Color, opts.Color256, opts.Black, opts.Mouse)
}}
@@ -169,10 +176,22 @@ func (t *Terminal) output() {
}
}
func runeWidth(r rune, prefixWidth int) int {
if r == '\t' {
return 8 - prefixWidth%8
} else if w, found := _runeWidths[r]; found {
return w
} else {
w := runewidth.RuneWidth(r)
_runeWidths[r] = w
return w
}
}
func displayWidth(runes []rune) int {
l := 0
for _, r := range runes {
l += runewidth.RuneWidth(r)
l += runeWidth(r, l)
}
return l
}
@@ -254,16 +273,27 @@ func (t *Terminal) printItem(item *Item, current bool) {
}
func trimRight(runes []rune, width int) ([]rune, int) {
currentWidth := displayWidth(runes)
trimmed := 0
for currentWidth > width && len(runes) > 0 {
sz := len(runes)
currentWidth -= runewidth.RuneWidth(runes[sz-1])
runes = runes[:sz-1]
trimmed++
// We start from the beginning to handle tab characters
l := 0
for idx, r := range runes {
l += runeWidth(r, l)
if idx > 0 && l > width {
return runes[:idx], len(runes) - idx
}
}
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) {
@@ -271,9 +301,9 @@ func trimLeft(runes []rune, width int) ([]rune, int32) {
var trimmed int32
for currentWidth > width && len(runes) > 0 {
currentWidth -= runewidth.RuneWidth(runes[0])
runes = runes[1:]
trimmed++
currentWidth = displayWidthWithLimit(runes, 2, width)
}
return runes, trimmed
}
@@ -323,18 +353,41 @@ func (*Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int) {
sort.Sort(ByOrder(offsets))
var index int32
var substr string
var prefixWidth int
for _, offset := range offsets {
b := util.Max32(index, offset[0])
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
}
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() {
t.printList()
t.printInfo()
@@ -395,6 +448,7 @@ func (t *Terminal) rubout(pattern string) {
// Loop is called to start Terminal I/O
func (t *Terminal) Loop() {
<-t.startChan
{ // Late initialization
t.mutex.Lock()
t.initFunc()
@@ -408,6 +462,15 @@ func (t *Terminal) Loop() {
<-timer.C
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() {
@@ -427,6 +490,8 @@ func (t *Terminal) Loop() {
t.suppress = false
case reqRedraw:
C.Clear()
C.Endwin()
C.Refresh()
t.printAll()
case reqClose:
C.Close()

View File

@@ -28,22 +28,32 @@ type Token struct {
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
func ParseRange(str *string) (Range, bool) {
if (*str) == ".." {
return Range{rangeEllipsis, rangeEllipsis}, true
return newRange(rangeEllipsis, rangeEllipsis), true
} else if strings.HasPrefix(*str, "..") {
end, err := strconv.Atoi((*str)[2:])
if err != nil || end == 0 {
return Range{}, false
}
return Range{rangeEllipsis, end}, true
return newRange(rangeEllipsis, end), true
} else if strings.HasSuffix(*str, "..") {
begin, err := strconv.Atoi((*str)[:len(*str)-2])
if err != nil || begin == 0 {
return Range{}, false
}
return Range{begin, rangeEllipsis}, true
return newRange(begin, rangeEllipsis), true
} else if strings.Contains(*str, "..") {
ns := strings.Split(*str, "..")
if len(ns) != 2 {
@@ -51,17 +61,17 @@ func ParseRange(str *string) (Range, bool) {
}
begin, err1 := strconv.Atoi(ns[0])
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{begin, end}, true
return newRange(begin, end), true
}
n, err := strconv.Atoi(*str)
if err != nil || n == 0 {
return Range{}, false
}
return Range{n, n}, true
return newRange(n, n), true
}
func withPrefixLengths(tokens []string, begin int) []Token {

View File

@@ -5,6 +5,7 @@ require 'rest_client'
if ARGV.length < 3
puts "usage: #$0 <token> <version> <files...>"
exit 1
end
token, version, *files = ARGV

View File

@@ -53,8 +53,8 @@ func (events *Events) Clear() {
}
}
// Peak peaks at the event box if the given event is set
func (b *EventBox) Peak(event EventType) bool {
// Peek peeks at the event box if the given event is set
func (b *EventBox) Peek(event EventType) bool {
b.cond.L.Lock()
defer b.cond.L.Unlock()
_, ok := b.events[event]
@@ -78,3 +78,18 @@ func (b *EventBox) Unwatch(events ...EventType) {
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
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):
let g:ret = []
function! g:proc(e)
function! g:FzfTest(e)
call add(g:ret, a:e)
endfunction
let result = sort(fzf#run({ 'sink': function('g:proc'), 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_fzf.rb'], result
AssertEqual ['fzf.vader', 'test_fzf.rb'], sort(g:ret)
let result = sort(fzf#run({ 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], result
AssertEqual ['fzf.vader', 'test_go.rb', 'test_ruby.rb'], sort(g:ret)
Execute (fzf#run with string source):
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))

View File

@@ -9,13 +9,36 @@ class NilClass
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= bash --rcfile ~/.fzf.#{shell}'").first
@win = go("new-window -d -P -F '#I' 'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.#{shell}'").first
@lines = `tput lines`.chomp.to_i
end
@@ -40,7 +63,7 @@ class Tmux
def capture
go("capture-pane -t #{win} \\; save-buffer #{TEMPNAME}")
raise "Window not found" if $?.exitstatus != 0
File.read(TEMPNAME).split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
readonce.split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
end
def until timeout = 1
@@ -71,44 +94,44 @@ private
end
class TestGoFZF < MiniTest::Unit::TestCase
include Temp
FIN = 'FIN'
TEMPNAME = '/tmp/output'
attr_reader :tmux
def tempname
'/tmp/output'
end
def rmtemp
while File.exists? tempname
File.unlink tempname rescue nil
end
end
def readtemp
waited = 0
while waited < 5
begin
return File.read(tempname)
rescue
sleep 0.1
waited += 0.1
end
end
raise "failed to read tempfile"
end
def setup
ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_DEFAULT_COMMAND'
@tmux = Tmux.new
rmtemp
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 > #{tempname}", :Enter
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]
@@ -127,16 +150,16 @@ class TestGoFZF < MiniTest::Unit::TestCase
tmux.send_keys :Enter
tmux.close
assert_equal '1391', readtemp.chomp
assert_equal '1391', readonce.chomp
end
def test_fzf_default_command
tmux.send_keys "FZF_DEFAULT_COMMAND='echo hello' fzf > #{tempname}", :Enter
tmux.send_keys "FZF_DEFAULT_COMMAND='echo hello' #{fzf}", :Enter
tmux.until { |lines| lines.last =~ /^>/ }
tmux.send_keys :Enter
tmux.close
assert_equal 'hello', readtemp.chomp
assert_equal 'hello', readonce.chomp
end
def test_key_bindings
@@ -206,7 +229,7 @@ class TestGoFZF < MiniTest::Unit::TestCase
end
def test_multi_order
tmux.send_keys "seq 1 10 | fzf --multi > #{tempname} && echo -n done", :Enter
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
@@ -214,18 +237,16 @@ class TestGoFZF < MiniTest::Unit::TestCase
: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?('done') }
assert_equal %w[3 2 5 6 8 7], readtemp.split($/)
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|
rmtemp
tmux.send_keys "(echo ' 1st 2nd 3rd/';
echo ' first second third/') |
fzf #{"--multi" if multi} -x --nth 2 --with-nth 2,-1,1 > #{tempname} && echo -n done",
#{fzf multi && :multi, :x, :nth, 2, :with_nth, '2,-1,1'}",
:Enter
tmux.until { |lines| lines[-2].include?('2/2') }
@@ -237,42 +258,69 @@ class TestGoFZF < MiniTest::Unit::TestCase
# However, the output must not be transformed
if multi
tmux.send_keys :BTab, :BTab, :Enter
tmux.until { |lines| lines[-1].include?('done') }
assert_equal [' 1st 2nd 3rd/', ' first second third/'], readtemp.split($/)
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?('done') }
assert_equal [' 1st 2nd 3rd/'], readtemp.split($/)
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|
rmtemp
tmux.send_keys "seq 1 100 | fzf #{'--reverse' if rev} > #{tempname} && echo -n done", :Enter
tmux.until { |lines| rev ? lines.first == '>' : lines.last == '>' }
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?('done') }
assert_equal '100', readtemp.chomp
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 > #{tempname} && echo -n done", :Enter
tmux.until { |lines| lines[-1].include?('done') }
assert_equal ['5555', '55'], readtemp.split($/)
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 > #{tempname} && echo -n done", :Enter
tmux.until { |lines| lines[-1].include?('done') }
assert_equal ['555555'], readtemp.split($/)
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