Merge pull request #134 from junegunn/devel

0.9.4
This commit is contained in:
Junegunn Choi 2015-03-01 12:35:08 +09:00
commit d4b41c5e03
20 changed files with 495 additions and 143 deletions

View File

@ -1,11 +1,15 @@
language: ruby language: ruby
rvm:
- 2.2.0
install: install:
- sudo apt-get update - sudo apt-get update
- sudo apt-get install -y libncurses-dev lib32ncurses5-dev - sudo apt-get install -y libncurses-dev lib32ncurses5-dev
- sudo add-apt-repository -y ppa:pi-rho/dev - sudo add-apt-repository -y ppa:pi-rho/dev
- sudo apt-add-repository -y ppa:fish-shell/release-2
- sudo apt-get update - sudo apt-get update
- sudo apt-get install -y tmux=1.9a-1~ppa1~p - sudo apt-get install -y tmux=1.9a-1~ppa1~p
- sudo apt-get install -y zsh fish
script: | script: |
export GOROOT=~/go1.4 export GOROOT=~/go1.4

60
CHANGELOG.md Normal file
View File

@ -0,0 +1,60 @@
CHANGELOG
=========
0.9.4
-----
### New features
#### Added `--tac` option to reverse the order of the input.
One might argue that this option is unnecessary since we can already put `tac`
or `tail -r` in the command pipeline to achieve the same result. However, the
advantage of `--tac` is that it does not block until the input is complete.
### *Backward incompatible changes*
#### Changed behavior on `--no-sort`
`--no-sort` option will no longer reverse the display order within finder. You
may want to use the new `--tac` option with `--no-sort`.
```
history | fzf +s --tac
```
### Improvements
#### `--filter` will not block when sort is disabled
When fzf works in filtering mode (`--filter`) and sort is disabled
(`--no-sort`), there's no need to block until input is complete. The new
version of fzf will print the matches on-the-fly when the following condition
is met:
--filter TERM --no-sort [--no-tac --no-sync]
or simply:
-f TERM +s
This change removes unnecessary delay in the use cases like the following:
fzf -f xxx +s | head -5
However, in this case, fzf processes the lines sequentially, so it cannot
utilize multiple cores, and fzf will run slightly slower than the previous
mode of execution where filtering is done in parallel after the entire input
is loaded. If the user is concerned about this performance problem, one can
add `--sync` option to re-enable buffering.
0.9.3
-----
### New features
- Added `--sync` option for multi-staged filtering
### Improvements
- `--select-1` and `--exit-0` will start finder immediately when the condition
cannot be met

View File

@ -75,7 +75,7 @@ Usage
``` ```
usage: fzf [options] usage: fzf [options]
Search Search mode
-x, --extended Extended-search mode -x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match) -e, --extended-exact Extended-search mode (exact match)
-i Case-insensitive match (default: smart-case match) -i Case-insensitive match (default: smart-case match)
@ -87,8 +87,9 @@ 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 Sort the result +s, --no-sort Do not sort the result
+s, --no-sort Do not sort the result. Keep the sequence unchanged. --tac Reverse the order of the input
(e.g. 'history | fzf --tac --no-sort')
Interface Interface
-m, --multi Enable multi-select with tab/shift-tab -m, --multi Enable multi-select with tab/shift-tab
@ -128,13 +129,6 @@ files excluding hidden ones. (You can override the default command with
vim $(fzf) vim $(fzf)
``` ```
If you want to preserve the exact sequence of the input, provide `--no-sort` (or
`+s`) option.
```sh
history | fzf +s
```
### Keys ### Keys
Use CTRL-J and CTRL-K (or CTRL-N and CTRL-P) to change the selection, press Use CTRL-J and CTRL-K (or CTRL-N and CTRL-P) to change the selection, press
@ -197,7 +191,7 @@ fd() {
# fh - repeat history # fh - repeat history
fh() { fh() {
eval $(([ -n "$ZSH_NAME" ] && fc -l 1 || history) | fzf +s | sed 's/ *[0-9]* *//') eval $(([ -n "$ZSH_NAME" ] && fc -l 1 || history) | fzf +s --tac | sed 's/ *[0-9]* *//')
} }
# fkill - kill process # fkill - kill process

10
install
View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
version=0.9.3 version=0.9.4
cd $(dirname $BASH_SOURCE) cd $(dirname $BASH_SOURCE)
fzf_base=$(pwd) fzf_base=$(pwd)
@ -245,7 +245,7 @@ if [ -z "$(set -o | \grep '^vi.*on')" ]; then
fi fi
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s +m -n2..,.. | sed \"s/ *[0-9]* *//\")\e\C-e\er"' bind '"\C-r": " \C-e\C-u$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. | sed \"s/ *[0-9]* *//\")\e\C-e\er"'
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"' bind '"\ec": " \C-e\C-u$(__fcd)\e\C-e\er\C-m"'
@ -263,7 +263,7 @@ else
bind -m vi-command '"\C-t": "i\C-t"' bind -m vi-command '"\C-t": "i\C-t"'
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s +m -n2..,.. | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"' bind '"\C-r": "\eddi$(HISTTIMEFORMAT= history | fzf +s --tac +m -n2..,.. | sed \"s/ *[0-9]* *//\")\C-x\C-e\e$a\C-x\C-r"'
bind -m vi-command '"\C-r": "i\C-r"' bind -m vi-command '"\C-r": "i\C-r"'
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
@ -323,7 +323,7 @@ bindkey '\ec' fzf-cd-widget
# CTRL-R - Paste the selected command from history into the command line # CTRL-R - Paste the selected command from history into the command line
fzf-history-widget() { fzf-history-widget() {
LBUFFER=$(fc -l 1 | fzf +s +m -n2..,.. | sed "s/ *[0-9*]* *//") LBUFFER=$(fc -l 1 | fzf +s --tac +m -n2..,.. | sed "s/ *[0-9*]* *//")
zle redisplay zle redisplay
} }
zle -N fzf-history-widget zle -N fzf-history-widget
@ -412,7 +412,7 @@ function fzf_key_bindings
end end
function __fzf_ctrl_r function __fzf_ctrl_r
history | __fzf_reverse | fzf +s +m > $TMPDIR/fzf.result history | __fzf_reverse | fzf +s --tac +m > $TMPDIR/fzf.result
and commandline (cat $TMPDIR/fzf.result) and commandline (cat $TMPDIR/fzf.result)
commandline -f repaint commandline -f repaint
rm -f $TMPDIR/fzf.result rm -f $TMPDIR/fzf.result

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.1.linux-amd64.tar.gz | \ https://storage.googleapis.com/golang/go1.4.2.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.1.linux-amd64.tar.gz | \ https://storage.googleapis.com/golang/go1.4.2.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.1.linux-amd64.tar.gz | \ https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | \
tar -xz && mv go go1.4 tar -xz && mv go go1.4
ENV GOPATH /go ENV GOPATH /go

View File

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

View File

@ -85,33 +85,47 @@ func Run(options *Options) {
} }
// Reader // Reader
reader := Reader{func(str string) { chunkList.Push(str) }, eventBox} streamingFilter := opts.Filter != nil && opts.Sort == 0 && !opts.Tac && !opts.Sync
go reader.ReadSource() if !streamingFilter {
reader := Reader{func(str string) { chunkList.Push(str) }, eventBox}
go reader.ReadSource()
}
// Matcher // Matcher
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern( return BuildPattern(
opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes) opts.Mode, opts.Case, opts.Nth, opts.Delimiter, runes)
} }
matcher := NewMatcher(patternBuilder, opts.Sort > 0, eventBox) matcher := NewMatcher(patternBuilder, opts.Sort > 0, opts.Tac, eventBox)
// Filtering mode // Filtering mode
if opts.Filter != nil { if opts.Filter != nil {
pattern := patternBuilder([]rune(*opts.Filter))
eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin)
snapshot, _ := chunkList.Snapshot()
merger, _ := matcher.scan(MatchRequest{
chunks: snapshot,
pattern: pattern})
if opts.PrintQuery { if opts.PrintQuery {
fmt.Println(*opts.Filter) fmt.Println(*opts.Filter)
} }
for i := 0; i < merger.Length(); i++ {
fmt.Println(merger.Get(i).AsString()) pattern := patternBuilder([]rune(*opts.Filter))
if streamingFilter {
reader := Reader{
func(str string) {
item := chunkList.trans(&str, 0)
if pattern.MatchItem(item) {
fmt.Println(*item.text)
}
}, eventBox}
reader.ReadSource()
} else {
eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin)
snapshot, _ := chunkList.Snapshot()
merger, _ := matcher.scan(MatchRequest{
chunks: snapshot,
pattern: pattern})
for i := 0; i < merger.Length(); i++ {
fmt.Println(merger.Get(i).AsString())
}
} }
os.Exit(0) os.Exit(0)
} }

View File

@ -87,10 +87,28 @@ func (a ByRelevance) Less(i, j int) bool {
irank := a[i].Rank(true) irank := a[i].Rank(true)
jrank := a[j].Rank(true) jrank := a[j].Rank(true)
return compareRanks(irank, jrank) return compareRanks(irank, jrank, false)
} }
func compareRanks(irank Rank, jrank Rank) bool { // ByRelevanceTac is for sorting Items
type ByRelevanceTac []*Item
func (a ByRelevanceTac) Len() int {
return len(a)
}
func (a ByRelevanceTac) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevanceTac) Less(i, j int) bool {
irank := a[i].Rank(true)
jrank := a[j].Rank(true)
return compareRanks(irank, jrank, true)
}
func compareRanks(irank Rank, jrank Rank, tac bool) bool {
if irank.matchlen < jrank.matchlen { if irank.matchlen < jrank.matchlen {
return true return true
} else if irank.matchlen > jrank.matchlen { } else if irank.matchlen > jrank.matchlen {
@ -103,8 +121,5 @@ func compareRanks(irank Rank, jrank Rank) bool {
return false return false
} }
if irank.index <= jrank.index { return (irank.index <= jrank.index) != tac
return true
}
return false
} }

View File

@ -20,12 +20,19 @@ func TestOffsetSort(t *testing.T) {
} }
func TestRankComparison(t *testing.T) { func TestRankComparison(t *testing.T) {
if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}) || if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, false) ||
!compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}) || !compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) ||
!compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}) || !compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, false) ||
!compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}) { !compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) {
t.Error("Invalid order") t.Error("Invalid order")
} }
if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, true) ||
!compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) ||
!compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, true) ||
!compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) {
t.Error("Invalid order (tac)")
}
} }
// Match length, string length, index // Match length, string length, index

View File

@ -21,6 +21,7 @@ type MatchRequest struct {
type Matcher struct { type Matcher struct {
patternBuilder func([]rune) *Pattern patternBuilder func([]rune) *Pattern
sort bool sort bool
tac bool
eventBox *util.EventBox eventBox *util.EventBox
reqBox *util.EventBox reqBox *util.EventBox
partitions int partitions int
@ -38,10 +39,11 @@ const (
// NewMatcher returns a new Matcher // NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern, func NewMatcher(patternBuilder func([]rune) *Pattern,
sort bool, eventBox *util.EventBox) *Matcher { sort bool, tac bool, eventBox *util.EventBox) *Matcher {
return &Matcher{ return &Matcher{
patternBuilder: patternBuilder, patternBuilder: patternBuilder,
sort: sort, sort: sort,
tac: tac,
eventBox: eventBox, eventBox: eventBox,
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
partitions: runtime.NumCPU(), partitions: runtime.NumCPU(),
@ -159,7 +161,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
countChan <- len(matches) countChan <- len(matches)
} }
if !empty && m.sort { if !empty && m.sort {
sort.Sort(ByRelevance(sliceMatches)) if m.tac {
sort.Sort(ByRelevanceTac(sliceMatches))
} else {
sort.Sort(ByRelevance(sliceMatches))
}
} }
resultChan <- partialResult{idx, sliceMatches} resultChan <- partialResult{idx, sliceMatches}
}(idx, chunks) }(idx, chunks)
@ -195,7 +201,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches partialResults[partialResult.index] = partialResult.matches
} }
return NewMerger(partialResults, !empty && m.sort), false return NewMerger(partialResults, !empty && m.sort, m.tac), false
} }
// Reset is called to interrupt/signal the ongoing search // Reset is called to interrupt/signal the ongoing search

View File

@ -3,7 +3,7 @@ package fzf
import "fmt" import "fmt"
// Merger with no data // Merger with no data
var EmptyMerger = NewMerger([][]*Item{}, false) var EmptyMerger = NewMerger([][]*Item{}, false, false)
// Merger holds a set of locally sorted lists of items and provides the view of // Merger holds a set of locally sorted lists of items and provides the view of
// a single, globally-sorted list // a single, globally-sorted list
@ -12,17 +12,19 @@ type Merger struct {
merged []*Item merged []*Item
cursors []int cursors []int
sorted bool sorted bool
tac bool
final bool final bool
count int count int
} }
// NewMerger returns a new Merger // NewMerger returns a new Merger
func NewMerger(lists [][]*Item, sorted bool) *Merger { func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger {
mg := Merger{ mg := Merger{
lists: lists, lists: lists,
merged: []*Item{}, merged: []*Item{},
cursors: make([]int, len(lists)), cursors: make([]int, len(lists)),
sorted: sorted, sorted: sorted,
tac: tac,
final: false, final: false,
count: 0} count: 0}
@ -39,19 +41,21 @@ func (mg *Merger) Length() int {
// Get returns the pointer to the Item object indexed by the given integer // Get returns the pointer to the Item object indexed by the given integer
func (mg *Merger) Get(idx int) *Item { func (mg *Merger) Get(idx int) *Item {
if len(mg.lists) == 1 { if mg.sorted {
return mg.lists[0][idx] return mg.mergedGet(idx)
} else if !mg.sorted {
for _, list := range mg.lists {
numItems := len(list)
if idx < numItems {
return list[idx]
}
idx -= numItems
}
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
} }
return mg.mergedGet(idx)
if mg.tac {
idx = mg.Length() - idx - 1
}
for _, list := range mg.lists {
numItems := len(list)
if idx < numItems {
return list[idx]
}
idx -= numItems
}
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
} }
func (mg *Merger) mergedGet(idx int) *Item { func (mg *Merger) mergedGet(idx int) *Item {
@ -66,7 +70,7 @@ func (mg *Merger) mergedGet(idx int) *Item {
} }
if cursor >= 0 { if cursor >= 0 {
rank := list[cursor].Rank(false) rank := list[cursor].Rank(false)
if minIdx < 0 || compareRanks(rank, minRank) { if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
minRank = rank minRank = rank
minIdx = listIdx minIdx = listIdx
} }

View File

@ -62,7 +62,7 @@ func TestMergerUnsorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Not sorted: same order // Not sorted: same order
mg := NewMerger(lists, false) mg := NewMerger(lists, false, false)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
assert(t, items[i] == mg.Get(i), "Invalid Get") assert(t, items[i] == mg.Get(i), "Invalid Get")
@ -74,7 +74,7 @@ func TestMergerSorted(t *testing.T) {
cnt := len(items) cnt := len(items)
// Sorted sorted order // Sorted sorted order
mg := NewMerger(lists, true) mg := NewMerger(lists, true, false)
assert(t, cnt == mg.Length(), "Invalid Length") assert(t, cnt == mg.Length(), "Invalid Length")
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
for i := 0; i < cnt; i++ { for i := 0; i < cnt; i++ {
@ -84,7 +84,7 @@ func TestMergerSorted(t *testing.T) {
} }
// Inverse order // Inverse order
mg2 := NewMerger(lists, true) mg2 := NewMerger(lists, true, false)
for i := cnt - 1; i >= 0; i-- { for i := cnt - 1; i >= 0; i-- {
if items[i] != mg2.Get(i) { if items[i] != mg2.Get(i) {
t.Error("Not sorted", items[i], mg2.Get(i)) t.Error("Not sorted", items[i], mg2.Get(i))

View File

@ -11,7 +11,7 @@ import (
const usage = `usage: fzf [options] const usage = `usage: fzf [options]
Search Search mode
-x, --extended Extended-search mode -x, --extended Extended-search mode
-e, --extended-exact Extended-search mode (exact match) -e, --extended-exact Extended-search mode (exact match)
-i Case-insensitive match (default: smart-case match) -i Case-insensitive match (default: smart-case match)
@ -23,8 +23,9 @@ const usage = `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 Sort the result +s, --no-sort Do not sort the result
+s, --no-sort Do not sort the result. Keep the sequence unchanged. --tac Reverse the order of the input
(e.g. 'history | fzf --tac --no-sort')
Interface Interface
-m, --multi Enable multi-select with tab/shift-tab -m, --multi Enable multi-select with tab/shift-tab
@ -78,6 +79,7 @@ type Options struct {
WithNth []Range WithNth []Range
Delimiter *regexp.Regexp Delimiter *regexp.Regexp
Sort int Sort int
Tac bool
Multi bool Multi bool
Mouse bool Mouse bool
Color bool Color bool
@ -102,6 +104,7 @@ func defaultOptions() *Options {
WithNth: make([]Range, 0), WithNth: make([]Range, 0),
Delimiter: nil, Delimiter: nil,
Sort: 1000, Sort: 1000,
Tac: false,
Multi: false, Multi: false,
Mouse: true, Mouse: true,
Color: true, Color: true,
@ -212,6 +215,10 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Sort = optionalNumeric(allArgs, &i) opts.Sort = optionalNumeric(allArgs, &i)
case "+s", "--no-sort": case "+s", "--no-sort":
opts.Sort = 0 opts.Sort = 0
case "--tac":
opts.Tac = true
case "--no-tac":
opts.Tac = false
case "-i": case "-i":
opts.Case = CaseIgnore opts.Case = CaseIgnore
case "+i": case "+i":

View File

@ -219,12 +219,7 @@ Loop:
} }
} }
var matches []*Item matches := p.matchChunk(space)
if p.mode == ModeFuzzy {
matches = p.fuzzyMatch(space)
} else {
matches = p.extendedMatch(space)
}
if !p.hasInvTerm { if !p.hasInvTerm {
_cache.Add(chunk, cacheKey, matches) _cache.Add(chunk, cacheKey, matches)
@ -232,6 +227,35 @@ Loop:
return matches return matches
} }
func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
matches := []*Item{}
if p.mode == ModeFuzzy {
for _, item := range *chunk {
if sidx, eidx := p.fuzzyMatch(item); sidx >= 0 {
matches = append(matches,
dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}}))
}
}
} else {
for _, item := range *chunk {
if offsets := p.extendedMatch(item); len(offsets) == len(p.terms) {
matches = append(matches, dupItem(item, offsets))
}
}
}
return matches
}
// MatchItem returns true if the Item is a match
func (p *Pattern) MatchItem(item *Item) bool {
if p.mode == ModeFuzzy {
sidx, _ := p.fuzzyMatch(item)
return sidx >= 0
}
offsets := p.extendedMatch(item)
return len(offsets) == len(p.terms)
}
func dupItem(item *Item, offsets []Offset) *Item { func dupItem(item *Item, offsets []Offset) *Item {
sort.Sort(ByOrder(offsets)) sort.Sort(ByOrder(offsets))
return &Item{ return &Item{
@ -243,39 +267,26 @@ func dupItem(item *Item, offsets []Offset) *Item {
rank: Rank{0, 0, item.index}} rank: Rank{0, 0, item.index}}
} }
func (p *Pattern) fuzzyMatch(chunk *Chunk) []*Item { func (p *Pattern) fuzzyMatch(item *Item) (int, int) {
matches := []*Item{} input := p.prepareInput(item)
for _, item := range *chunk { return p.iter(algo.FuzzyMatch, input, p.text)
input := p.prepareInput(item)
if sidx, eidx := p.iter(algo.FuzzyMatch, input, p.text); sidx >= 0 {
matches = append(matches,
dupItem(item, []Offset{Offset{int32(sidx), int32(eidx)}}))
}
}
return matches
} }
func (p *Pattern) extendedMatch(chunk *Chunk) []*Item { func (p *Pattern) extendedMatch(item *Item) []Offset {
matches := []*Item{} input := p.prepareInput(item)
for _, item := range *chunk { offsets := []Offset{}
input := p.prepareInput(item) for _, term := range p.terms {
offsets := []Offset{} pfun := p.procFun[term.typ]
for _, term := range p.terms { if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 {
pfun := p.procFun[term.typ] if term.inv {
if sidx, eidx := p.iter(pfun, input, term.text); sidx >= 0 { break
if term.inv {
break
}
offsets = append(offsets, Offset{int32(sidx), int32(eidx)})
} else if term.inv {
offsets = append(offsets, Offset{0, 0})
} }
} offsets = append(offsets, Offset{int32(sidx), int32(eidx)})
if len(offsets) == len(p.terms) { } else if term.inv {
matches = append(matches, dupItem(item, offsets)) offsets = append(offsets, Offset{0, 0})
} }
} }
return matches return offsets
} }
func (p *Pattern) prepareInput(item *Item) *Transformed { func (p *Pattern) prepareInput(item *Item) *Transformed {

View File

@ -98,14 +98,15 @@ func TestOrigTextAndTransformed(t *testing.T) {
tokens := Tokenize(strptr("junegunn"), nil) tokens := Tokenize(strptr("junegunn"), nil)
trans := Transform(tokens, []Range{Range{1, 1}}) trans := Transform(tokens, []Range{Range{1, 1}})
for _, fun := range []func(*Chunk) []*Item{pattern.fuzzyMatch, pattern.extendedMatch} { for _, mode := range []Mode{ModeFuzzy, ModeExtended} {
chunk := Chunk{ chunk := Chunk{
&Item{ &Item{
text: strptr("junegunn"), text: strptr("junegunn"),
origText: strptr("junegunn.choi"), origText: strptr("junegunn.choi"),
transformed: trans}, transformed: trans},
} }
matches := fun(&chunk) pattern.mode = mode
matches := pattern.matchChunk(&chunk)
if *matches[0].text != "junegunn" || *matches[0].origText != "junegunn.choi" || if *matches[0].text != "junegunn" || *matches[0].origText != "junegunn.choi" ||
matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 || matches[0].offsets[0][0] != 0 || matches[0].offsets[0][1] != 5 ||
matches[0].transformed != trans { matches[0].transformed != trans {

View File

@ -22,7 +22,6 @@ import (
type Terminal struct { type Terminal struct {
prompt string prompt string
reverse bool reverse bool
tac bool
cx int cx int
cy int cy int
offset int offset int
@ -85,7 +84,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
input := []rune(opts.Query) input := []rune(opts.Query)
return &Terminal{ return &Terminal{
prompt: opts.Prompt, prompt: opts.Prompt,
tac: opts.Sort == 0,
reverse: opts.Reverse, reverse: opts.Reverse,
cx: len(input), cx: len(input),
cy: 0, cy: 0,
@ -148,13 +146,6 @@ func (t *Terminal) UpdateList(merger *Merger) {
t.reqBox.Set(reqList, nil) t.reqBox.Set(reqList, nil)
} }
func (t *Terminal) listIndex(y int) int {
if t.tac {
return t.merger.Length() - y - 1
}
return y
}
func (t *Terminal) output() { func (t *Terminal) output() {
if t.printQuery { if t.printQuery {
fmt.Println(string(t.input)) fmt.Println(string(t.input))
@ -162,7 +153,7 @@ func (t *Terminal) output() {
if len(t.selected) == 0 { if len(t.selected) == 0 {
cnt := t.merger.Length() cnt := t.merger.Length()
if cnt > 0 && cnt > t.cy { if cnt > 0 && cnt > t.cy {
fmt.Println(t.merger.Get(t.listIndex(t.cy)).AsString()) fmt.Println(t.merger.Get(t.cy).AsString())
} }
} else { } else {
sels := make([]selectedItem, 0, len(t.selected)) sels := make([]selectedItem, 0, len(t.selected))
@ -246,7 +237,7 @@ func (t *Terminal) printList() {
for i := 0; i < maxy; i++ { for i := 0; i < maxy; i++ {
t.move(i+2, 0, true) t.move(i+2, 0, true)
if i < count { if i < count {
t.printItem(t.merger.Get(t.listIndex(i+t.offset)), i == t.cy-t.offset) t.printItem(t.merger.Get(i+t.offset), i == t.cy-t.offset)
} }
} }
} }
@ -525,9 +516,8 @@ func (t *Terminal) Loop() {
} }
} }
toggle := func() { toggle := func() {
idx := t.listIndex(t.cy) if t.cy < t.merger.Length() {
if idx < t.merger.Length() { item := t.merger.Get(t.cy)
item := t.merger.Get(idx)
if _, found := t.selected[item.text]; !found { if _, found := t.selected[item.text]; !found {
var strptr *string var strptr *string
if item.origText != nil { if item.origText != nil {
@ -650,7 +640,7 @@ func (t *Terminal) Loop() {
} else if me.Double { } else if me.Double {
// Double-click // Double-click
if my >= 2 { if my >= 2 {
if t.vset(my-2) && t.listIndex(t.cy) < t.merger.Length() { if t.vset(my-2) && t.cy < t.merger.Length() {
req(reqClose) req(reqClose)
} }
} }

View File

@ -2,11 +2,20 @@
# encoding: utf-8 # encoding: utf-8
require 'minitest/autorun' require 'minitest/autorun'
require 'fileutils'
class NilClass class NilClass
def include? str def include? str
false false
end end
def start_with? str
false
end
def end_with? str
false
end
end end
module Temp module Temp
@ -15,7 +24,7 @@ module Temp
waited = 0 waited = 0
while waited < 5 while waited < 5
begin begin
data = File.read(name) data = `cat #{name}`
return data unless data.empty? return data unless data.empty?
rescue rescue
sleep 0.1 sleep 0.1
@ -30,6 +39,20 @@ module Temp
end end
end end
class Shell
class << self
def bash
'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.bash'
end
def zsh
FileUtils.mkdir_p '/tmp/fzf-zsh'
FileUtils.cp File.expand_path('~/.fzf.zsh'), '/tmp/fzf-zsh/.zshrc'
'PS1= PROMPT_COMMAND= HISTSIZE=100 ZDOTDIR=/tmp/fzf-zsh zsh'
end
end
end
class Tmux class Tmux
include Temp include Temp
@ -37,18 +60,33 @@ class Tmux
attr_reader :win attr_reader :win
def initialize shell = 'bash' def initialize shell = :bash
@win = go("new-window -d -P -F '#I' 'PS1= PROMPT_COMMAND= bash --rcfile ~/.fzf.#{shell}'").first @win =
case shell
when :bash
go("new-window -d -P -F '#I' '#{Shell.bash}'").first
when :zsh
go("new-window -d -P -F '#I' '#{Shell.zsh}'").first
when :fish
go("new-window -d -P -F '#I' 'fish'").first
else
raise "Unknown shell: #{shell}"
end
@lines = `tput lines`.chomp.to_i @lines = `tput lines`.chomp.to_i
if shell == :fish
send_keys('function fish_prompt; end; clear', :Enter)
self.until { |lines| lines.empty? }
end
end end
def closed? def closed?
!go("list-window -F '#I'").include?(win) !go("list-window -F '#I'").include?(win)
end end
def close timeout = 1 def close
send_keys 'C-c', 'C-u', 'exit', :Enter send_keys 'C-c', 'C-u', 'exit', :Enter
wait(timeout) { closed? } wait { closed? }
end end
def kill def kill
@ -56,35 +94,68 @@ class Tmux
end end
def send_keys *args def send_keys *args
target =
if args.last.is_a?(Hash)
hash = args.pop
go("select-window -t #{win}")
"#{win}.#{hash[:pane]}"
else
win
end
args = args.map { |a| %{"#{a}"} }.join ' ' args = args.map { |a| %{"#{a}"} }.join ' '
go("send-keys -t #{win} #{args}") go("send-keys -t #{target} #{args}")
end end
def capture def capture opts = {}
go("capture-pane -t #{win} \\; save-buffer #{TEMPNAME}") timeout, pane = defaults(opts).values_at(:timeout, :pane)
raise "Window not found" if $?.exitstatus != 0 waited = 0
loop do
go("capture-pane -t #{win}.#{pane} \\; save-buffer #{TEMPNAME}")
break if $?.exitstatus == 0
if waited > timeout
raise "Window not found"
end
waited += 0.1
sleep 0.1
end
readonce.split($/)[0, @lines].reverse.drop_while(&:empty?).reverse readonce.split($/)[0, @lines].reverse.drop_while(&:empty?).reverse
end end
def until timeout = 1 def until opts = {}
wait(timeout) { yield capture } lines = nil
wait(opts) do
yield lines = capture(opts)
end
lines
end end
def prepare
self.send_keys 'echo hello', :Enter
self.until { |lines| lines[-1].start_with?('hello') }
self.send_keys 'clear', :Enter
self.until { |lines| lines.empty? }
end
private private
def wait timeout = 1 def defaults opts
{ timeout: 5, pane: 0 }.merge(opts)
end
def wait opts = {}
timeout, pane = defaults(opts).values_at(:timeout, :pane)
waited = 0 waited = 0
until yield until yield
waited += 0.1
sleep 0.1
if waited > timeout if waited > timeout
hl = '=' * 10 hl = '=' * 10
puts hl puts hl
capture.each_with_index do |line, idx| capture(opts).each_with_index do |line, idx|
puts [idx.to_s.rjust(2), line].join(': ') puts [idx.to_s.rjust(2), line].join(': ')
end end
puts hl puts hl
raise "timeout" raise "timeout"
end end
waited += 0.1
sleep 0.1
end end
end end
@ -93,7 +164,7 @@ private
end end
end end
class TestGoFZF < MiniTest::Unit::TestCase class TestBase < Minitest::Test
include Temp include Temp
FIN = 'FIN' FIN = 'FIN'
@ -104,11 +175,6 @@ class TestGoFZF < MiniTest::Unit::TestCase
def setup def setup
ENV.delete 'FZF_DEFAULT_OPTS' ENV.delete 'FZF_DEFAULT_OPTS'
ENV.delete 'FZF_DEFAULT_COMMAND' ENV.delete 'FZF_DEFAULT_COMMAND'
@tmux = Tmux.new
end
def teardown
@tmux.kill
end end
def fzf(*opts) def fzf(*opts)
@ -129,10 +195,22 @@ class TestGoFZF < MiniTest::Unit::TestCase
}.compact }.compact
"fzf #{opts.join ' '}" "fzf #{opts.join ' '}"
end end
end
class TestGoFZF < TestBase
def setup
super
@tmux = Tmux.new
end
def teardown
@tmux.kill
end
def test_vanilla def test_vanilla
tmux.send_keys "seq 1 100000 | #{fzf}", :Enter tmux.send_keys "seq 1 100000 | #{fzf}", :Enter
tmux.until(10) { |lines| lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ } tmux.until(timeout: 10) { |lines|
lines.last =~ /^>/ && lines[-2] =~ /^ 100000/ }
lines = tmux.capture lines = tmux.capture
assert_equal ' 2', lines[-4] assert_equal ' 2', lines[-4]
assert_equal '> 1', lines[-3] assert_equal '> 1', lines[-3]
@ -322,5 +400,166 @@ class TestGoFZF < MiniTest::Unit::TestCase
tmux.send_keys 'C-K', :Enter tmux.send_keys 'C-K', :Enter
assert_equal ['1919'], readonce.split($/) assert_equal ['1919'], readonce.split($/)
end end
def test_tac
tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys :BTab, :BTab, :BTab, :Enter
assert_equal %w[1000 999 998], readonce.split($/)
end
def test_tac_sort
tmux.send_keys "seq 1 1000 | #{fzf :tac, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys '99'
tmux.send_keys :BTab, :BTab, :BTab, :Enter
assert_equal %w[99 999 998], readonce.split($/)
end
def test_tac_nosort
tmux.send_keys "seq 1 1000 | #{fzf :tac, :no_sort, :multi}", :Enter
tmux.until { |lines| lines[-2].include? '1000/1000' }
tmux.send_keys '00'
tmux.send_keys :BTab, :BTab, :BTab, :Enter
assert_equal %w[1000 900 800], readonce.split($/)
end
end
module TestShell
def setup
super
end
def teardown
@tmux.kill
end
def test_ctrl_t
tmux.prepare
tmux.send_keys 'C-t', pane: 0
lines = tmux.until(pane: 1) { |lines| lines[-1].start_with? '>' }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, :Enter, pane: 1
tmux.until(pane: 0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c'
# FZF_TMUX=0
new_shell
tmux.send_keys 'C-t', pane: 0
lines = tmux.until(pane: 0) { |lines| lines[-1].start_with? '>' }
expected = lines.values_at(-3, -4).map { |line| line[2..-1] }.join(' ')
tmux.send_keys :BTab, :BTab, :Enter, pane: 0
tmux.until(pane: 0) { |lines| lines[-1].include? expected }
tmux.send_keys 'C-c', 'C-d'
end
def test_alt_c
tmux.prepare
tmux.send_keys :Escape, :c
lines = tmux.until { |lines| lines[-1].start_with? '>' }
expected = lines[-3][2..-1]
p expected
tmux.send_keys :Enter
tmux.prepare
tmux.send_keys :pwd, :Enter
tmux.until { |lines| p lines; lines[-1].end_with?(expected) }
end
def test_ctrl_r
tmux.prepare
tmux.send_keys 'echo 1st', :Enter; tmux.prepare
tmux.send_keys 'echo 2nd', :Enter; tmux.prepare
tmux.send_keys 'echo 3d', :Enter; tmux.prepare
tmux.send_keys 'echo 3rd', :Enter; tmux.prepare
tmux.send_keys 'echo 4th', :Enter; tmux.prepare
tmux.send_keys 'C-r'
tmux.until { |lines| lines[-1].start_with? '>' }
tmux.send_keys '3d'
tmux.until { |lines| lines[-3].end_with? 'echo 3rd' } # --no-sort
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == 'echo 3rd' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == '3rd' }
end
end
class TestBash < TestBase
include TestShell
def new_shell
tmux.send_keys "FZF_TMUX=0 #{Shell.bash}", :Enter
tmux.prepare
end
def setup
super
@tmux = Tmux.new :bash
end
def test_file_completion
tmux.send_keys 'mkdir -p /tmp/fzf-test; touch /tmp/fzf-test/{1..100}', :Enter
tmux.prepare
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab
tmux.until { |lines| lines[-1].start_with? '>' }
tmux.send_keys :BTab, :BTab, :Enter
tmux.until { |lines|
lines[-1].include?('/tmp/fzf-test/10') &&
lines[-1].include?('/tmp/fzf-test/100')
}
end
def test_dir_completion
tmux.send_keys 'mkdir -p /tmp/fzf-test/d{1..100}', :Enter
tmux.prepare
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab
tmux.until { |lines| lines[-1].start_with? '>' }
tmux.send_keys :BTab, :BTab # BTab does not work here
tmux.send_keys 55
tmux.until { |lines| lines[-2].start_with? ' 1/' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == 'cd /tmp/fzf-test/d55' }
end
def test_process_completion
tmux.send_keys 'sleep 12345 &', :Enter
lines = tmux.until { |lines| lines[-1].start_with? '[1]' }
pid = lines[-1].split.last
tmux.prepare
tmux.send_keys 'kill ', :Tab
tmux.until { |lines| lines[-1].start_with? '>' }
tmux.send_keys 'sleep12345'
tmux.until { |lines| lines[-3].include? 'sleep 12345' }
tmux.send_keys :Enter
tmux.until { |lines| lines[-1] == "kill #{pid}" }
end
end
class TestZsh < TestBase
include TestShell
def new_shell
tmux.send_keys "FZF_TMUX=0 #{Shell.zsh}", :Enter
tmux.prepare
end
def setup
super
@tmux = Tmux.new :zsh
end
end
class TestFish < TestBase
include TestShell
def new_shell
tmux.send_keys 'env FZF_TMUX=0 fish', :Enter
tmux.send_keys 'function fish_prompt; end; clear', :Enter
tmux.until { |lines| lines.empty? }
end
def setup
super
@tmux = Tmux.new :fish
end
end end

View File

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