m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-17 15:53:39 -05:00

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

@@ -6,8 +6,11 @@ import (
)
func TestChunkList(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byMatchLen, byLength, byIndex}
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
@@ -36,8 +39,11 @@ func TestChunkList(t *testing.T) {
if len(*chunk1) != 2 {
t.Error("Snapshot should contain only two items")
}
if string((*chunk1)[0].text) != "hello" || (*chunk1)[0].rank.index != 0 ||
string((*chunk1)[1].text) != "world" || (*chunk1)[1].rank.index != 2 {
last := func(arr []int32) int32 {
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")
}
if chunk1.IsFull() {

View File

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

View File

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

View File

@@ -23,27 +23,30 @@ func TestOffsetSort(t *testing.T) {
}
func TestRankComparison(t *testing.T) {
if compareRanks(Rank{3, 0, 5}, Rank{2, 0, 7}, false) ||
!compareRanks(Rank{3, 0, 5}, Rank{3, 0, 6}, false) ||
!compareRanks(Rank{1, 2, 3}, Rank{1, 3, 2}, false) ||
!compareRanks(Rank{0, 0, 0}, Rank{0, 0, 0}, false) {
if compareRanks([]int32{3, 0, 5}, []int32{2, 0, 7}, false) ||
!compareRanks([]int32{3, 0, 5}, []int32{3, 0, 6}, false) ||
!compareRanks([]int32{1, 2, 3}, []int32{1, 3, 2}, false) ||
!compareRanks([]int32{0, 0, 0}, []int32{0, 0, 0}, false) {
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) {
if compareRanks([]int32{3, 0, 5}, []int32{2, 0, 7}, true) ||
!compareRanks([]int32{3, 0, 5}, []int32{3, 0, 6}, false) ||
!compareRanks([]int32{1, 2, 3}, []int32{1, 3, 2}, true) ||
!compareRanks([]int32{0, 0, 0}, []int32{0, 0, 0}, false) {
t.Error("Invalid order (tac)")
}
}
// Match length, string length, index
func TestItemRank(t *testing.T) {
// FIXME global
sortCriteria = []criterion{byMatchLen, byLength, byIndex}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := Item{text: strs[0], index: 1, offsets: []Offset{}}
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))
}
// Only differ in index
@@ -63,10 +66,10 @@ func TestItemRank(t *testing.T) {
}
// Sort by relevance
item3 := Item{text: strs[1], rank: Rank{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}}}
item5 := Item{text: strs[2], rank: Rank{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}}}
item3 := Item{text: strs[1], rank: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 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: []int32{0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 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}
sort.Sort(ByRelevance(items))
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 {
for i := len(mg.merged); i <= idx; i++ {
minRank := Rank{0, 0, 0}
minRank := buildEmptyRank(0)
minIdx := -1
for listIdx, list := range mg.lists {
cursor := mg.cursors[listIdx]

View File

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

View File

@@ -27,7 +27,8 @@ const usage = `usage: fzf [options]
-d, --delimiter=STR Field delimiter regex for --nth (default: AWK-style)
+s, --no-sort Do not sort the result
--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)
Interface
@@ -75,10 +76,11 @@ const (
)
// Sort criteria
type tiebreak int
type criterion int
const (
byLength tiebreak = iota
byMatchLen criterion = iota
byLength
byBegin
byEnd
byIndex
@@ -98,7 +100,7 @@ type Options struct {
Delimiter Delimiter
Sort int
Tac bool
Tiebreak tiebreak
Criteria []criterion
Multi bool
Ansi bool
Mouse bool
@@ -145,7 +147,7 @@ func defaultOptions() *Options {
Delimiter: Delimiter{},
Sort: 1000,
Tac: false,
Tiebreak: byLength,
Criteria: []criterion{byMatchLen, byLength, byIndex},
Multi: false,
Ansi: false,
Mouse: true,
@@ -361,20 +363,43 @@ func parseKeyChords(str string, message string) map[int]string {
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)
func parseTiebreak(str string) []criterion {
criteria := []criterion{byMatchLen}
hasIndex := false
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
}
return byLength
for _, str := range strings.Split(strings.ToLower(str), ",") {
switch str {
case "index":
check(&hasIndex, "index")
criteria = append(criteria, byIndex)
case "length":
check(&hasLength, "length")
criteria = append(criteria, byLength)
case "begin":
check(&hasBegin, "begin")
criteria = append(criteria, byBegin)
case "end":
check(&hasEnd, "end")
criteria = append(criteria, byEnd)
default:
errorExit("invalid sort criterion: " + str)
}
}
if !hasIndex {
criteria = append(criteria, byIndex)
}
return criteria
}
func dupeTheme(theme *curses.ColorTheme) *curses.ColorTheme {
@@ -715,7 +740,7 @@ func parseOptions(opts *Options, allArgs []string) {
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"))
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind":
keymap, opts.Execmap, opts.ToggleSort =
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 {
opts.Expect = parseKeyChords(value, "key names required")
} else if match, value := optString(arg, "--tiebreak="); match {
opts.Tiebreak = parseTiebreak(value)
opts.Criteria = parseTiebreak(value)
} else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(opts.Theme, value)
} else if match, value := optString(arg, "--bind="); match {

View File

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

View File

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