Add --tiebreak option for customizing sort criteria

Close #191
This commit is contained in:
Junegunn Choi 2015-04-16 14:19:28 +09:00
parent 48ab87294b
commit b8904a8c3e
7 changed files with 139 additions and 18 deletions

5
fzf
View File

@ -206,9 +206,10 @@ class FZF
@expect = true
when /^--expect=(.*)$/
@expect = true
when '--toggle-sort'
when '--toggle-sort', '--tiebreak'
argv.shift
when '--tac', '--sync', '--toggle-sort', /^--toggle-sort=(.*)$/
when '--tac', '--no-tac', '--sync', '--no-sync', '--hscroll', '--no-hscroll',
/^--toggle-sort=(.*)$/, /^--tiebreak=(.*)$/
# XXX
else
usage 1, "illegal option: #{o}"

View File

@ -66,6 +66,20 @@ Reverse the order of the input
.RS
e.g. \fBhistory | fzf --tac --no-sort\fR
.RE
.TP
.BI "--tiebreak=" "STR"
Sort criterion to use when the scores are tied
.br
.R ""
.br
.BR length " Prefers item with shorter length"
.br
.BR begin " Prefers item with matched substring closer to the beginning"
.br
.BR end " Prefers item with matched substring closer to the end""
.br
.BR index " Prefers item that appeared earlier in the input stream"
.br
.SS Interface
.TP
.B "-m, --multi"

View File

@ -55,6 +55,7 @@ func Run(options *Options) {
opts := ParseOptions()
sort := opts.Sort > 0
rankTiebreak = opts.Tiebreak
if opts.Version {
fmt.Println(Version)

View File

@ -1,6 +1,8 @@
package fzf
import (
"math"
"github.com/junegunn/fzf/src/curses"
)
@ -27,17 +29,21 @@ type Item struct {
// Rank is used to sort the search result
type Rank struct {
matchlen uint16
strlen uint16
tiebreak uint16
index uint32
}
// Tiebreak criterion to use. Never changes once fzf is started.
var rankTiebreak tiebreak
// Rank calculates rank of the Item
func (i *Item) Rank(cache bool) Rank {
if cache && (i.rank.matchlen > 0 || i.rank.strlen > 0) {
if cache && (i.rank.matchlen > 0 || i.rank.tiebreak > 0) {
return i.rank
}
matchlen := 0
prevEnd := 0
minBegin := math.MaxUint16
for _, offset := range i.offsets {
begin := int(offset[0])
end := int(offset[1])
@ -48,10 +54,30 @@ func (i *Item) Rank(cache bool) Rank {
prevEnd = end
}
if end > begin {
if begin < minBegin {
minBegin = begin
}
matchlen += end - begin
}
}
rank := Rank{uint16(matchlen), uint16(len(*i.text)), i.index}
var tiebreak uint16
switch rankTiebreak {
case byLength:
tiebreak = uint16(len(*i.text))
case byBegin:
// We can't just look at i.offsets[0][0] because it can be an inverse term
tiebreak = uint16(minBegin)
case byEnd:
if prevEnd > 0 {
tiebreak = uint16(1 + len(*i.text) - prevEnd)
} else {
// Empty offsets due to inverse terms.
tiebreak = 1
}
case byIndex:
tiebreak = 1
}
rank := Rank{uint16(matchlen), tiebreak, i.index}
if cache {
i.rank = rank
}
@ -199,9 +225,9 @@ func compareRanks(irank Rank, jrank Rank, tac bool) bool {
return false
}
if irank.strlen < jrank.strlen {
if irank.tiebreak < jrank.tiebreak {
return true
} else if irank.strlen > jrank.strlen {
} else if irank.tiebreak > jrank.tiebreak {
return false
}

View File

@ -42,7 +42,7 @@ func TestItemRank(t *testing.T) {
strs := []string{"foo", "foobar", "bar", "baz"}
item1 := Item{text: &strs[0], index: 1, offsets: []Offset{}}
rank1 := item1.Rank(true)
if rank1.matchlen != 0 || rank1.strlen != 3 || rank1.index != 1 {
if rank1.matchlen != 0 || rank1.tiebreak != 3 || rank1.index != 1 {
t.Error(item1.Rank(true))
}
// Only differ in index

View File

@ -28,7 +28,8 @@ const usage = `usage: fzf [options]
Search result
+s, --no-sort Do not sort the result
--tac Reverse the order of the input
(e.g. 'history | fzf --tac --no-sort')
--tiebreak=CRI Sort criterion when the scores are tied;
[length|begin|end|index] (default: length)
Interface
-m, --multi Enable multi-select with tab/shift-tab
@ -50,7 +51,6 @@ const usage = `usage: fzf [options]
--expect=KEYS Comma-separated list of keys to complete fzf
--toggle-sort=KEY Key to toggle sort
--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
@ -78,6 +78,16 @@ const (
CaseRespect
)
// Sort criteria
type tiebreak int
const (
byLength tiebreak = iota
byBegin
byEnd
byIndex
)
// Options stores the values of command-line options
type Options struct {
Mode Mode
@ -87,6 +97,7 @@ type Options struct {
Delimiter *regexp.Regexp
Sort int
Tac bool
Tiebreak tiebreak
Multi bool
Ansi bool
Mouse bool
@ -116,6 +127,7 @@ func defaultOptions() *Options {
Delimiter: nil,
Sort: 1000,
Tac: false,
Tiebreak: byLength,
Multi: false,
Ansi: false,
Mouse: true,
@ -238,6 +250,22 @@ func parseKeyChords(str string, message string) []int {
return chords
}
func parseTiebreak(str string) tiebreak {
switch strings.ToLower(str) {
case "length":
return byLength
case "index":
return byIndex
case "begin":
return byBegin
case "end":
return byEnd
default:
errorExit("invalid sort criterion: " + str)
}
return byLength
}
func checkToggleSort(str string) int {
keys := parseKeyChords(str, "key name required")
if len(keys) != 1 {
@ -265,6 +293,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Filter = &filter
case "--expect":
opts.Expect = parseKeyChords(nextString(allArgs, &i, "key names required"), "key names required")
case "--tiebreak":
opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--toggle-sort":
opts.ToggleSort = checkToggleSort(nextString(allArgs, &i, "key name required"))
case "-d", "--delimiter":
@ -352,6 +382,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.ToggleSort = checkToggleSort(value)
} else if match, value := optString(arg, "--expect="); match {
opts.Expect = parseKeyChords(value, "key names required")
} else if match, value := optString(arg, "--tiebreak="); match {
opts.Tiebreak = parseTiebreak(value)
} else {
errorExit("unknown option: " + arg)
}

View File

@ -476,19 +476,66 @@ class TestGoFZF < TestBase
def test_unicode_case
tempname = TEMPNAME + Time.now.to_f.to_s
File.open(tempname, 'w') do |f|
f << %w[строКА1 СТРОКА2 строка3 Строка4].join($/)
f.sync
end
since = Time.now
while `cat #{tempname}`.split($/).length != 4 && (Time.now - since) < 10
sleep 0.1
end
writelines tempname, %w[строКА1 СТРОКА2 строка3 Строка4]
assert_equal %w[СТРОКА2 Строка4], `cat #{tempname} | #{FZF} -fС`.split($/)
assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `cat #{tempname} | #{FZF} -fс`.split($/)
rescue
File.unlink tempname
end
def test_tiebreak
tempname = TEMPNAME + Time.now.to_f.to_s
input = %w[
--foobar--------
-----foobar---
----foobar--
-------foobar-
]
writelines tempname, input
assert_equal input, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=index`.split($/)
by_length = %w[
----foobar--
-----foobar---
-------foobar-
--foobar--------
]
assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar`.split($/)
assert_equal by_length, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=length`.split($/)
by_begin = %w[
--foobar--------
----foobar--
-----foobar---
-------foobar-
]
assert_equal by_begin, `cat #{tempname} | #{FZF} -ffoobar --tiebreak=begin`.split($/)
assert_equal by_begin, `cat #{tempname} | #{FZF} -f"!z foobar" -x --tiebreak begin`.split($/)
assert_equal %w[
-------foobar-
----foobar--
-----foobar---
--foobar--------
], `cat #{tempname} | #{FZF} -ffoobar --tiebreak end`.split($/)
assert_equal input, `cat #{tempname} | #{FZF} -f"!z" -x --tiebreak end`.split($/)
rescue
File.unlink tempname
end
private
def writelines path, lines, timeout = 10
File.open(path, 'w') do |f|
f << lines.join($/)
f.sync
end
since = Time.now
while `cat #{path}`.split($/).length != lines.length && (Time.now - since) < 10
sleep 0.1
end
end
end
module TestShell