Accept comma-separated list of sort criteria

This commit is contained in:
Junegunn Choi 2016-01-13 03:07:42 +09:00
parent d635b3fd3c
commit 1d2d32c847
12 changed files with 298 additions and 120 deletions

View File

@ -1,6 +1,15 @@
CHANGELOG CHANGELOG
========= =========
0.11.2
------
- `--tiebreak` now accepts comma-separated list of sort criteria.
- Each criterion should appear only once in the list
- `index` is only allowed at the end of the list
- `index` is implicitly appended to the list when not specified
- Default is `length` (or equivalently `length,index`)
0.11.1 0.11.1
------ ------

View File

@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Dec 2015" "fzf 0.11.1" "fzf - a command-line fuzzy finder" .TH fzf 1 "Jan 2016" "fzf 0.11.2" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@ -68,8 +68,8 @@ Reverse the order of the input
e.g. \fBhistory | fzf --tac --no-sort\fR e.g. \fBhistory | fzf --tac --no-sort\fR
.RE .RE
.TP .TP
.BI "--tiebreak=" "CRI" .BI "--tiebreak=" "CRI[,..]"
Sort criterion to use when the scores are tied Comma-separated list of sort criteria to apply when the scores are tied.
.br .br
.R "" .R ""
.br .br
@ -81,6 +81,15 @@ Sort criterion to use when the scores are tied
.br .br
.BR index " Prefers item that appeared earlier in the input stream" .BR index " Prefers item that appeared earlier in the input stream"
.br .br
.R ""
.br
- Each criterion should appear only once in the list
.br
- \fBindex\fR is only allowed at the end of the list
.br
- \fBindex\fR is implicitly appended to the list when not specified
.br
- Default is \fBlength\fR (or equivalently \fBlength\fR,index)
.SS Interface .SS Interface
.TP .TP
.B "-m, --multi" .B "-m, --multi"

View File

@ -6,8 +6,11 @@ import (
) )
func TestChunkList(t *testing.T) { func TestChunkList(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byMatchLen, byLength, byIndex}
cl := NewChunkList(func(s []byte, i int) *Item { cl := NewChunkList(func(s []byte, i int) *Item {
return &Item{text: []rune(string(s)), rank: Rank{0, 0, uint32(i * 2)}} return &Item{text: []rune(string(s)), rank: buildEmptyRank(int32(i * 2))}
}) })
// Snapshot // Snapshot
@ -36,8 +39,11 @@ func TestChunkList(t *testing.T) {
if len(*chunk1) != 2 { if len(*chunk1) != 2 {
t.Error("Snapshot should contain only two items") t.Error("Snapshot should contain only two items")
} }
if string((*chunk1)[0].text) != "hello" || (*chunk1)[0].rank.index != 0 || last := func(arr []int32) int32 {
string((*chunk1)[1].text) != "world" || (*chunk1)[1].rank.index != 2 { return arr[len(arr)-1]
}
if string((*chunk1)[0].text) != "hello" || last((*chunk1)[0].rank) != 0 ||
string((*chunk1)[1].text) != "world" || last((*chunk1)[1].rank) != 2 {
t.Error("Invalid data") t.Error("Invalid data")
} }
if chunk1.IsFull() { if chunk1.IsFull() {

View File

@ -52,7 +52,7 @@ func Run(opts *Options) {
initProcs() initProcs()
sort := opts.Sort > 0 sort := opts.Sort > 0
rankTiebreak = opts.Tiebreak sortCriteria = opts.Criteria
if opts.Version { if opts.Version {
fmt.Println(version) fmt.Println(version)
@ -103,9 +103,9 @@ func Run(opts *Options) {
runes, colors := ansiProcessor(data) runes, colors := ansiProcessor(data)
return &Item{ return &Item{
text: runes, text: runes,
index: uint32(index), index: int32(index),
colors: colors, colors: colors,
rank: Rank{0, 0, uint32(index)}} rank: buildEmptyRank(int32(index))}
}) })
} else { } else {
chunkList = NewChunkList(func(data []byte, index int) *Item { chunkList = NewChunkList(func(data []byte, index int) *Item {
@ -120,9 +120,9 @@ func Run(opts *Options) {
item := Item{ item := Item{
text: joinTokens(trans), text: joinTokens(trans),
origText: &runes, origText: &runes,
index: uint32(index), index: int32(index),
colors: nil, colors: nil,
rank: Rank{0, 0, uint32(index)}} rank: buildEmptyRank(int32(index))}
trimmed, colors := ansiProcessorRunes(item.text) trimmed, colors := ansiProcessorRunes(item.text)
item.text = trimmed item.text = trimmed
@ -141,9 +141,19 @@ func Run(opts *Options) {
} }
// Matcher // Matcher
forward := true
for _, cri := range opts.Criteria[1:] {
if cri == byEnd {
forward = false
break
}
if cri == byBegin {
break
}
}
patternBuilder := func(runes []rune) *Pattern { patternBuilder := func(runes []rune) *Pattern {
return BuildPattern( return BuildPattern(
opts.Fuzzy, opts.Extended, opts.Case, opts.Tiebreak != byEnd, opts.Fuzzy, opts.Extended, opts.Case, forward,
opts.Nth, opts.Delimiter, runes) opts.Nth, opts.Delimiter, runes)
} }
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox) matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)

View File

@ -20,25 +20,35 @@ type Item struct {
text []rune text []rune
origText *[]rune origText *[]rune
transformed []Token transformed []Token
index uint32 index int32
offsets []Offset offsets []Offset
colors []ansiOffset colors []ansiOffset
rank Rank rank []int32
} }
// Rank is used to sort the search result // Sort criteria to use. Never changes once fzf is started.
type Rank struct { var sortCriteria []criterion
matchlen uint16
tiebreak uint16 func isRankValid(rank []int32) bool {
index uint32 // Exclude ordinal index
for i := 0; i < len(rank)-1; i++ {
if rank[i] > 0 {
return true
}
}
return false
} }
// Tiebreak criterion to use. Never changes once fzf is started. func buildEmptyRank(index int32) []int32 {
var rankTiebreak tiebreak len := len(sortCriteria)
arr := make([]int32, len)
arr[len-1] = index
return arr
}
// Rank calculates rank of the Item // Rank calculates rank of the Item
func (item *Item) Rank(cache bool) Rank { func (item *Item) Rank(cache bool) []int32 {
if cache && (item.rank.matchlen > 0 || item.rank.tiebreak > 0) { if cache && isRankValid(item.rank) {
return item.rank return item.rank
} }
matchlen := 0 matchlen := 0
@ -64,32 +74,37 @@ func (item *Item) Rank(cache bool) Rank {
} }
} }
if matchlen == 0 { if matchlen == 0 {
matchlen = math.MaxUint16 matchlen = math.MaxInt32
} }
var tiebreak uint16 rank := make([]int32, len(sortCriteria))
switch rankTiebreak { for idx, criterion := range sortCriteria {
var val int32
switch criterion {
case byMatchLen:
val = int32(matchlen)
case byLength: case byLength:
// It is guaranteed that .transformed in not null in normal execution // It is guaranteed that .transformed in not null in normal execution
if item.transformed != nil { if item.transformed != nil {
// If offsets is empty, lenSum will be 0, but we don't care // If offsets is empty, lenSum will be 0, but we don't care
tiebreak = uint16(lenSum) val = int32(lenSum)
} else { } else {
tiebreak = uint16(len(item.text)) val = int32(len(item.text))
} }
case byBegin: case byBegin:
// We can't just look at item.offsets[0][0] because it can be an inverse term // We can't just look at item.offsets[0][0] because it can be an inverse term
tiebreak = uint16(minBegin) val = int32(minBegin)
case byEnd: case byEnd:
if prevEnd > 0 { if prevEnd > 0 {
tiebreak = uint16(1 + len(item.text) - prevEnd) val = int32(1 + len(item.text) - prevEnd)
} else { } else {
// Empty offsets due to inverse terms. // Empty offsets due to inverse terms.
tiebreak = 1 val = 1
} }
case byIndex: case byIndex:
tiebreak = 1 val = item.index
}
rank[idx] = val
} }
rank := Rank{uint16(matchlen), tiebreak, item.index}
if cache { if cache {
item.rank = rank item.rank = rank
} }
@ -254,18 +269,19 @@ func (a ByRelevanceTac) Less(i, j int) bool {
return compareRanks(irank, jrank, true) return compareRanks(irank, jrank, true)
} }
func compareRanks(irank Rank, jrank Rank, tac bool) bool { func compareRanks(irank []int32, jrank []int32, tac bool) bool {
if irank.matchlen < jrank.matchlen { lastIdx := len(irank) - 1
for idx, left := range irank {
right := jrank[idx]
if tac && idx == lastIdx {
left = left * -1
right = right * -1
}
if left < right {
return true return true
} else if irank.matchlen > jrank.matchlen { } else if left > right {
return false return false
} }
}
if irank.tiebreak < jrank.tiebreak {
return true return true
} else if irank.tiebreak > jrank.tiebreak {
return false
}
return (irank.index <= jrank.index) != tac
} }

View File

@ -23,27 +23,30 @@ 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}, false) || if compareRanks([]int32{3, 0, 5}, []int32{2, 0, 7}, false) ||
!compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) || !compareRanks([]int32{3, 0, 5}, []int32{3, 0, 6}, false) ||
!compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, false) || !compareRanks([]int32{1, 2, 3}, []int32{1, 3, 2}, false) ||
!compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) { !compareRanks([]int32{0, 0, 0}, []int32{0, 0, 0}, false) {
t.Error("Invalid order") t.Error("Invalid order")
} }
if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, true) || if compareRanks([]int32{3, 0, 5}, []int32{2, 0, 7}, true) ||
!compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) || !compareRanks([]int32{3, 0, 5}, []int32{3, 0, 6}, false) ||
!compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, true) || !compareRanks([]int32{1, 2, 3}, []int32{1, 3, 2}, true) ||
!compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) { !compareRanks([]int32{0, 0, 0}, []int32{0, 0, 0}, false) {
t.Error("Invalid order (tac)") t.Error("Invalid order (tac)")
} }
} }
// Match length, string length, index // Match length, string length, index
func TestItemRank(t *testing.T) { func TestItemRank(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byMatchLen, byLength, byIndex}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := Item{text: strs[0], index: 1, offsets: []Offset{}} item1 := Item{text: strs[0], index: 1, offsets: []Offset{}}
rank1 := item1.Rank(true) rank1 := item1.Rank(true)
if rank1.matchlen != math.MaxUint16 || rank1.tiebreak != 3 || rank1.index != 1 { if rank1[0] != math.MaxInt32 || rank1[1] != 3 || rank1[2] != 1 {
t.Error(item1.Rank(true)) t.Error(item1.Rank(true))
} }
// Only differ in index // Only differ in index
@ -63,10 +66,10 @@ func TestItemRank(t *testing.T) {
} }
// Sort by relevance // Sort by relevance
item3 := Item{text: strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} item3 := Item{text: strs[1], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item4 := Item{text: strs[1], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} item4 := Item{text: strs[1], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
item5 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}} item5 := Item{text: strs[2], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item6 := Item{text: strs[2], rank: Rank{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}} item6 := Item{text: strs[2], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6} items = []*Item{&item1, &item2, &item3, &item4, &item5, &item6}
sort.Sort(ByRelevance(items)) sort.Sort(ByRelevance(items))
if items[0] != &item6 || items[1] != &item4 || if items[0] != &item6 || items[1] != &item4 ||

View File

@ -88,7 +88,7 @@ func (mg *Merger) cacheable() bool {
func (mg *Merger) mergedGet(idx int) *Item { func (mg *Merger) mergedGet(idx int) *Item {
for i := len(mg.merged); i <= idx; i++ { for i := len(mg.merged); i <= idx; i++ {
minRank := Rank{0, 0, 0} minRank := buildEmptyRank(0)
minIdx := -1 minIdx := -1
for listIdx, list := range mg.lists { for listIdx, list := range mg.lists {
cursor := mg.cursors[listIdx] cursor := mg.cursors[listIdx]

View File

@ -23,7 +23,7 @@ func randItem() *Item {
} }
return &Item{ return &Item{
text: []rune(str), text: []rune(str),
index: rand.Uint32(), index: rand.Int31(),
offsets: offsets} offsets: offsets}
} }

View File

@ -27,7 +27,8 @@ 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)
+s, --no-sort Do not sort the result +s, --no-sort Do not sort the result
--tac Reverse the order of the input --tac Reverse the order of the input
--tiebreak=CRITERION Sort criterion when the scores are tied; --tiebreak=CRI[,..] Comma-separated list of sort criteria to apply
when the scores are tied;
[length|begin|end|index] (default: length) [length|begin|end|index] (default: length)
Interface Interface
@ -75,10 +76,11 @@ const (
) )
// Sort criteria // Sort criteria
type tiebreak int type criterion int
const ( const (
byLength tiebreak = iota byMatchLen criterion = iota
byLength
byBegin byBegin
byEnd byEnd
byIndex byIndex
@ -98,7 +100,7 @@ type Options struct {
Delimiter Delimiter Delimiter Delimiter
Sort int Sort int
Tac bool Tac bool
Tiebreak tiebreak Criteria []criterion
Multi bool Multi bool
Ansi bool Ansi bool
Mouse bool Mouse bool
@ -145,7 +147,7 @@ func defaultOptions() *Options {
Delimiter: Delimiter{}, Delimiter: Delimiter{},
Sort: 1000, Sort: 1000,
Tac: false, Tac: false,
Tiebreak: byLength, Criteria: []criterion{byMatchLen, byLength, byIndex},
Multi: false, Multi: false,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
@ -361,20 +363,43 @@ func parseKeyChords(str string, message string) map[int]string {
return chords return chords
} }
func parseTiebreak(str string) tiebreak { func parseTiebreak(str string) []criterion {
switch strings.ToLower(str) { criteria := []criterion{byMatchLen}
case "length": hasIndex := false
return byLength hasLength := false
hasBegin := false
hasEnd := false
check := func(notExpected *bool, name string) {
if *notExpected {
errorExit("duplicate sort criteria: " + name)
}
if hasIndex {
errorExit("index should be the last criterion")
}
*notExpected = true
}
for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str {
case "index": case "index":
return byIndex check(&hasIndex, "index")
criteria = append(criteria, byIndex)
case "length":
check(&hasLength, "length")
criteria = append(criteria, byLength)
case "begin": case "begin":
return byBegin check(&hasBegin, "begin")
criteria = append(criteria, byBegin)
case "end": case "end":
return byEnd check(&hasEnd, "end")
criteria = append(criteria, byEnd)
default: default:
errorExit("invalid sort criterion: " + str) errorExit("invalid sort criterion: " + str)
} }
return byLength }
if !hasIndex {
criteria = append(criteria, byIndex)
}
return criteria
} }
func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme { func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
@ -715,7 +740,7 @@ func parseOptions(opts *Options, allArgs []string) {
case "--expect": case "--expect":
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required") opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
case "--tiebreak": case "--tiebreak":
opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind": case "--bind":
keymap, opts.Execmap, opts.ToggleSort = keymap, opts.Execmap, opts.ToggleSort =
parseKeymap(keymap, opts.Execmap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required")) parseKeymap(keymap, opts.Execmap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required"))
@ -850,7 +875,7 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--expect="); match { } else if match, value := optString(arg, "--expect="); match {
opts.Expect = parseKeyChords(value, "key names required") opts.Expect = parseKeyChords(value, "key names required")
} else if match, value := optString(arg, "--tiebreak="); match { } else if match, value := optString(arg, "--tiebreak="); match {
opts.Tiebreak = parseTiebreak(value) opts.Criteria = parseTiebreak(value)
} else if match, value := optString(arg, "--color="); match { } else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(opts.Theme, value) opts.Theme = parseTheme(opts.Theme, value)
} else if match, value := optString(arg, "--bind="); match { } else if match, value := optString(arg, "--bind="); match {

View File

@ -309,7 +309,7 @@ func dupItem(item *Item, offsets []Offset) *Item {
index: item.index, index: item.index,
offsets: offsets, offsets: offsets,
colors: item.colors, colors: item.colors,
rank: Rank{0, 0, item.index}} rank: buildEmptyRank(item.index)}
} }
func (p *Pattern) basicMatch(item *Item) (int, int, int) { func (p *Pattern) basicMatch(item *Item) (int, int, int) {

View File

@ -50,7 +50,7 @@ type Terminal struct {
progress int progress int
reading bool reading bool
merger *Merger merger *Merger
selected map[uint32]selectedItem selected map[int32]selectedItem
reqBox *util.EventBox reqBox *util.EventBox
eventBox *util.EventBox eventBox *util.EventBox
mutex sync.Mutex mutex sync.Mutex
@ -223,7 +223,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
ansi: opts.Ansi, ansi: opts.Ansi,
reading: true, reading: true,
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[uint32]selectedItem), selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
eventBox: eventBox, eventBox: eventBox,
mutex: sync.Mutex{}, mutex: sync.Mutex{},
@ -466,7 +466,7 @@ func (t *Terminal) printHeader() {
text: []rune(trimmed), text: []rune(trimmed),
index: 0, index: 0,
colors: colors, colors: colors,
rank: Rank{0, 0, 0}} rank: buildEmptyRank(0)}
t.move(line, 2, true) t.move(line, 2, true)
t.printHighlighted(item, false, C.ColHeader, 0, false) t.printHighlighted(item, false, C.ColHeader, 0, false)

View File

@ -459,8 +459,8 @@ class TestGoFZF < TestBase
def test_unicode_case def test_unicode_case
writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4] writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4]
assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/) assert_equal %w[СТРОКА2 Строка4], `#{FZF} -fС < #{tempname}`.split($/)
assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/) assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `#{FZF} -fс < #{tempname}`.split($/)
end end
def test_tiebreak def test_tiebreak
@ -472,7 +472,7 @@ class TestGoFZF < TestBase
] ]
writelines tempname, input writelines tempname, input
assert_equal input, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=index`.split($/) assert_equal input, `#{FZF} -ffoobar --tiebreak=index < #{tempname}`.split($/)
by_length = %w[ by_length = %w[
----foobar-- ----foobar--
@ -480,8 +480,8 @@ class TestGoFZF < TestBase
-------foobar- -------foobar-
--foobar-------- --foobar--------
] ]
assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar`.split($/) assert_equal by_length, `#{FZF} -ffoobar < #{tempname}`.split($/)
assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=length`.split($/) assert_equal by_length, `#{FZF} -ffoobar --tiebreak=length < #{tempname}`.split($/)
by_begin = %w[ by_begin = %w[
--foobar-------- --foobar--------
@ -489,17 +489,117 @@ class TestGoFZF < TestBase
-----foobar--- -----foobar---
-------foobar- -------foobar-
] ]
assert_equal by_begin, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=begin`.split($/) assert_equal by_begin, `#{FZF} -ffoobar --tiebreak=begin < #{tempname}`.split($/)
assert_equal by_begin, `cat #{tempname} | #{FZF} -f"!z foobar" -x --tiebreak begin`.split($/) assert_equal by_begin, `#{FZF} -f"!z foobar" -x --tiebreak begin < #{tempname}`.split($/)
assert_equal %w[ assert_equal %w[
-------foobar- -------foobar-
----foobar-- ----foobar--
-----foobar--- -----foobar---
--foobar-------- --foobar--------
], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/) ], `#{FZF} -ffoobar --tiebreak end < #{tempname}`.split($/)
assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/) assert_equal input, `#{FZF} -f"!z" -x --tiebreak end < #{tempname}`.split($/)
end
# Since 0.11.2
def test_tiebreak_list
input = %w[
f-o-o-b-a-r
foobar----
--foobar
----foobar
foobar--
--foobar--
foobar
]
writelines tempname, input
assert_equal %w[
foobar----
--foobar
----foobar
foobar--
--foobar--
foobar
f-o-o-b-a-r
], `#{FZF} -ffb --tiebreak=index < #{tempname}`.split($/)
by_length = %w[
foobar
--foobar
foobar--
foobar----
----foobar
--foobar--
f-o-o-b-a-r
]
assert_equal by_length, `#{FZF} -ffb < #{tempname}`.split($/)
assert_equal by_length, `#{FZF} -ffb --tiebreak=length < #{tempname}`.split($/)
assert_equal %w[
foobar
foobar--
--foobar
foobar----
--foobar--
----foobar
f-o-o-b-a-r
], `#{FZF} -ffb --tiebreak=length,begin < #{tempname}`.split($/)
assert_equal %w[
foobar
--foobar
foobar--
----foobar
--foobar--
foobar----
f-o-o-b-a-r
], `#{FZF} -ffb --tiebreak=length,end < #{tempname}`.split($/)
assert_equal %w[
foobar----
foobar--
foobar
--foobar
--foobar--
----foobar
f-o-o-b-a-r
], `#{FZF} -ffb --tiebreak=begin < #{tempname}`.split($/)
by_begin_end = %w[
foobar
foobar--
foobar----
--foobar
--foobar--
----foobar
f-o-o-b-a-r
]
assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=begin,length < #{tempname}`.split($/)
assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=begin,end < #{tempname}`.split($/)
assert_equal %w[
--foobar
----foobar
foobar
foobar--
--foobar--
foobar----
f-o-o-b-a-r
], `#{FZF} -ffb --tiebreak=end < #{tempname}`.split($/)
by_begin_end = %w[
foobar
--foobar
----foobar
foobar--
--foobar--
foobar----
f-o-o-b-a-r
]
assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=end,begin < #{tempname}`.split($/)
assert_equal by_begin_end, `#{FZF} -ffb --tiebreak=end,length < #{tempname}`.split($/)
end end
def test_tiebreak_length_with_nth def test_tiebreak_length_with_nth
@ -517,7 +617,7 @@ class TestGoFZF < TestBase
123:hello 123:hello
1234567:h 1234567:h
] ]
assert_equal output, `cat #{tempname} | #{FZF} -fh`.split($/) assert_equal output, `#{FZF} -fh < #{tempname}`.split($/)
output = %w[ output = %w[
1234567:h 1234567:h
@ -525,7 +625,7 @@ class TestGoFZF < TestBase
1:hell 1:hell
123:hello 123:hello
] ]
assert_equal output, `cat #{tempname} | #{FZF} -fh -n2 -d:`.split($/) assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.split($/)
end end
def test_tiebreak_length_with_nth_trim_length def test_tiebreak_length_with_nth_trim_length
@ -544,7 +644,7 @@ class TestGoFZF < TestBase
"apple juice bottle 1", "apple juice bottle 1",
"apple ui bottle 2", "apple ui bottle 2",
] ]
assert_equal output, `cat #{tempname} | #{FZF} -fa -n1`.split($/) assert_equal output, `#{FZF} -fa -n1 < #{tempname}`.split($/)
# len(1 ~ 2) # len(1 ~ 2)
output = [ output = [
@ -553,7 +653,7 @@ class TestGoFZF < TestBase
"apple juice bottle 1", "apple juice bottle 1",
"app ice bottle 3", "app ice bottle 3",
] ]
assert_equal output, `cat #{tempname} | #{FZF} -fai -n1..2`.split($/) assert_equal output, `#{FZF} -fai -n1..2 < #{tempname}`.split($/)
# len(1) + len(2) # len(1) + len(2)
output = [ output = [
@ -562,7 +662,7 @@ class TestGoFZF < TestBase
"apple ui bottle 2", "apple ui bottle 2",
"apple juice bottle 1", "apple juice bottle 1",
] ]
assert_equal output, `cat #{tempname} | #{FZF} -x -f"a i" -n1,2`.split($/) assert_equal output, `#{FZF} -x -f"a i" -n1,2 < #{tempname}`.split($/)
# len(2) # len(2)
output = [ output = [
@ -571,8 +671,8 @@ class TestGoFZF < TestBase
"app ice bottle 3", "app ice bottle 3",
"apple juice bottle 1", "apple juice bottle 1",
] ]
assert_equal output, `cat #{tempname} | #{FZF} -fi -n2`.split($/) assert_equal output, `#{FZF} -fi -n2 < #{tempname}`.split($/)
assert_equal output, `cat #{tempname} | #{FZF} -fi -n2,1..2`.split($/) assert_equal output, `#{FZF} -fi -n2,1..2 < #{tempname}`.split($/)
end end
def test_tiebreak_end_backward_scan def test_tiebreak_end_backward_scan
@ -582,8 +682,8 @@ class TestGoFZF < TestBase
] ]
writelines tempname, input writelines tempname, input
assert_equal input.reverse, `cat #{tempname} | #{FZF} -f fb`.split($/) assert_equal input.reverse, `#{FZF} -f fb < #{tempname}`.split($/)
assert_equal input, `cat #{tempname} | #{FZF} -f fb --tiebreak=end`.split($/) assert_equal input, `#{FZF} -f fb --tiebreak=end < #{tempname}`.split($/)
end end
def test_invalid_cache def test_invalid_cache
@ -613,7 +713,7 @@ class TestGoFZF < TestBase
File.open(tempname, 'w') do |f| File.open(tempname, 'w') do |f|
f << data f << data
end end
assert_equal data, `cat #{tempname} | #{FZF} -f .`.chomp assert_equal data, `#{FZF} -f . < #{tempname}`.chomp
end end
def test_read0 def test_read0
@ -888,18 +988,18 @@ class TestGoFZF < TestBase
def test_with_nth def test_with_nth
writelines tempname, ['hello world ', 'byebye'] writelines tempname, ['hello world ', 'byebye']
assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1`.chomp assert_equal 'hello world ', `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
end end
def test_with_nth_ansi def test_with_nth_ansi
writelines tempname, ["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'] writelines tempname, ["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye']
assert_equal 'hello world ', `cat #{tempname} | #{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi`.chomp assert_equal 'hello world ', `#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi < #{tempname}`.chomp
end end
def test_with_nth_no_ansi def test_with_nth_no_ansi
src = "\x1b[33mhello \x1b[34;1mworld\x1b[m " src = "\x1b[33mhello \x1b[34;1mworld\x1b[m "
writelines tempname, [src, 'byebye'] writelines tempname, [src, 'byebye']
assert_equal src, `cat #{tempname} | #{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi`.chomp assert_equal src, `#{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi < #{tempname}`.chomp
end end
def test_exit_0_exit_code def test_exit_0_exit_code