Micro-optimizations

- Make structs smaller
- Introduce Result struct and use it to represent matched items instead of
  reusing Item struct for that purpose
- Avoid unnecessary memory allocation
- Avoid growing slice from the initial capacity
- Code cleanup
This commit is contained in:
Junegunn Choi 2016-08-19 02:39:32 +09:00
parent f7f01d109e
commit 37dc273148
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
19 changed files with 236 additions and 570 deletions

View File

@ -24,12 +24,12 @@ func indexAt(index int, max int, forward bool) int {
// Result conatins the results of running a match function. // Result conatins the results of running a match function.
type Result struct { type Result struct {
Start int32 Start int
End int32 End int
// Items are basically sorted by the lengths of matched substrings. // Items are basically sorted by the lengths of matched substrings.
// But we slightly adjust the score with bonus for better results. // But we slightly adjust the score with bonus for better results.
Bonus int32 Bonus int
} }
type charClass int type charClass int
@ -42,8 +42,8 @@ const (
charNumber charNumber
) )
func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int) int32 { func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int) int {
var bonus int32 var bonus int
pidx := 0 pidx := 0
lenPattern := len(pattern) lenPattern := len(pattern)
consecutive := false consecutive := false
@ -63,7 +63,7 @@ func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int
class = charNonWord class = charNonWord
} }
var point int32 var point int
if prevClass == charNonWord && class != charNonWord { if prevClass == charNonWord && class != charNonWord {
// Word boundary // Word boundary
point = 2 point = 2
@ -181,7 +181,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, text util.Chars, pattern []run
sidx, eidx = lenRunes-eidx, lenRunes-sidx sidx, eidx = lenRunes-eidx, lenRunes-sidx
} }
return Result{int32(sidx), int32(eidx), return Result{sidx, eidx,
evaluateBonus(caseSensitive, text, pattern, sidx, eidx)} evaluateBonus(caseSensitive, text, pattern, sidx, eidx)}
} }
return Result{-1, -1, 0} return Result{-1, -1, 0}
@ -228,7 +228,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern
sidx = lenRunes - (index + 1) sidx = lenRunes - (index + 1)
eidx = lenRunes - (index - lenPattern + 1) eidx = lenRunes - (index - lenPattern + 1)
} }
return Result{int32(sidx), int32(eidx), return Result{sidx, eidx,
evaluateBonus(caseSensitive, text, pattern, sidx, eidx)} evaluateBonus(caseSensitive, text, pattern, sidx, eidx)}
} }
} else { } else {
@ -255,7 +255,7 @@ func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru
} }
} }
lenPattern := len(pattern) lenPattern := len(pattern)
return Result{0, int32(lenPattern), return Result{0, lenPattern,
evaluateBonus(caseSensitive, text, pattern, 0, lenPattern)} evaluateBonus(caseSensitive, text, pattern, 0, lenPattern)}
} }
@ -279,7 +279,7 @@ func SuffixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru
lenPattern := len(pattern) lenPattern := len(pattern)
sidx := trimmedLen - lenPattern sidx := trimmedLen - lenPattern
eidx := trimmedLen eidx := trimmedLen
return Result{int32(sidx), int32(eidx), return Result{sidx, eidx,
evaluateBonus(caseSensitive, text, pattern, sidx, eidx)} evaluateBonus(caseSensitive, text, pattern, sidx, eidx)}
} }
@ -294,7 +294,7 @@ func EqualMatch(caseSensitive bool, forward bool, text util.Chars, pattern []run
runesStr = strings.ToLower(runesStr) runesStr = strings.ToLower(runesStr)
} }
if runesStr == string(pattern) { if runesStr == string(pattern) {
return Result{0, int32(len(pattern)), 0} return Result{0, len(pattern), 0}
} }
return Result{-1, -1, 0} return Result{-1, -1, 0}
} }

View File

@ -7,7 +7,7 @@ import (
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
func assertMatch(t *testing.T, fun func(bool, bool, util.Chars, []rune) Result, caseSensitive, forward bool, input, pattern string, sidx int32, eidx int32, bonus int32) { func assertMatch(t *testing.T, fun func(bool, bool, util.Chars, []rune) Result, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, bonus int) {
if !caseSensitive { if !caseSensitive {
pattern = strings.ToLower(pattern) pattern = strings.ToLower(pattern)
} }

View File

@ -36,7 +36,7 @@ func init() {
ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]") ansiRegex = regexp.MustCompile("\x1b\\[[0-9;]*[mK]")
} }
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, []ansiOffset, *ansiState) { func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
var offsets []ansiOffset var offsets []ansiOffset
var output bytes.Buffer var output bytes.Buffer
@ -84,7 +84,10 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
if proc != nil { if proc != nil {
proc(rest, state) proc(rest, state)
} }
return output.String(), offsets, state if len(offsets) == 0 {
return output.String(), nil, state
}
return output.String(), &offsets, state
} }
func interpretCode(ansiCode string, prevState *ansiState) *ansiState { func interpretCode(ansiCode string, prevState *ansiState) *ansiState {

View File

@ -16,7 +16,7 @@ func TestExtractColor(t *testing.T) {
src := "hello world" src := "hello world"
var state *ansiState var state *ansiState
clean := "\x1b[0m" clean := "\x1b[0m"
check := func(assertion func(ansiOffsets []ansiOffset, state *ansiState)) { check := func(assertion func(ansiOffsets *[]ansiOffset, state *ansiState)) {
output, ansiOffsets, newState := extractColor(src, state, nil) output, ansiOffsets, newState := extractColor(src, state, nil)
state = newState state = newState
if output != "hello world" { if output != "hello world" {
@ -26,127 +26,127 @@ func TestExtractColor(t *testing.T) {
assertion(ansiOffsets, state) assertion(ansiOffsets, state)
} }
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) > 0 { if offsets != nil {
t.Fail() t.Fail()
} }
}) })
state = nil state = nil
src = "\x1b[0mhello world" src = "\x1b[0mhello world"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) > 0 { if offsets != nil {
t.Fail() t.Fail()
} }
}) })
state = nil state = nil
src = "\x1b[1mhello world" src = "\x1b[1mhello world"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 11, -1, -1, true) assert((*offsets)[0], 0, 11, -1, -1, true)
}) })
state = nil state = nil
src = "\x1b[1mhello \x1b[mworld" src = "\x1b[1mhello \x1b[mworld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 6, -1, -1, true) assert((*offsets)[0], 0, 6, -1, -1, true)
}) })
state = nil state = nil
src = "\x1b[1mhello \x1b[Kworld" src = "\x1b[1mhello \x1b[Kworld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 11, -1, -1, true) assert((*offsets)[0], 0, 11, -1, -1, true)
}) })
state = nil state = nil
src = "hello \x1b[34;45;1mworld" src = "hello \x1b[34;45;1mworld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 11, 4, 5, true) assert((*offsets)[0], 6, 11, 4, 5, true)
}) })
state = nil state = nil
src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld" src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 11, 4, 5, true) assert((*offsets)[0], 6, 11, 4, 5, true)
}) })
state = nil state = nil
src = "hello \x1b[34;45;1mwor\x1b[0mld" src = "hello \x1b[34;45;1mwor\x1b[0mld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 9, 4, 5, true) assert((*offsets)[0], 6, 9, 4, 5, true)
}) })
state = nil state = nil
src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md" src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 3 { if len(*offsets) != 3 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 8, 4, 233, true) assert((*offsets)[0], 6, 8, 4, 233, true)
assert(offsets[1], 8, 9, 161, 233, true) assert((*offsets)[1], 8, 9, 161, 233, true)
assert(offsets[2], 10, 11, 161, -1, false) assert((*offsets)[2], 10, 11, 161, -1, false)
}) })
// {38,48};5;{38,48} // {38,48};5;{38,48}
state = nil state = nil
src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md" src = "hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 2 { if len(*offsets) != 2 {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 9, 38, 48, true) assert((*offsets)[0], 6, 9, 38, 48, true)
assert(offsets[1], 9, 10, 48, 38, true) assert((*offsets)[1], 9, 10, 48, 38, true)
}) })
src = "hello \x1b[32;1mworld" src = "hello \x1b[32;1mworld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
if state.fg != 2 || state.bg != -1 || !state.bold { if state.fg != 2 || state.bg != -1 || !state.bold {
t.Fail() t.Fail()
} }
assert(offsets[0], 6, 11, 2, -1, true) assert((*offsets)[0], 6, 11, 2, -1, true)
}) })
src = "hello world" src = "hello world"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 1 { if len(*offsets) != 1 {
t.Fail() t.Fail()
} }
if state.fg != 2 || state.bg != -1 || !state.bold { if state.fg != 2 || state.bg != -1 || !state.bold {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 11, 2, -1, true) assert((*offsets)[0], 0, 11, 2, -1, true)
}) })
src = "hello \x1b[0;38;5;200;48;5;100mworld" src = "hello \x1b[0;38;5;200;48;5;100mworld"
check(func(offsets []ansiOffset, state *ansiState) { check(func(offsets *[]ansiOffset, state *ansiState) {
if len(offsets) != 2 { if len(*offsets) != 2 {
t.Fail() t.Fail()
} }
if state.fg != 200 || state.bg != 100 || state.bold { if state.fg != 200 || state.bg != 100 || state.bold {
t.Fail() t.Fail()
} }
assert(offsets[0], 0, 6, 2, -1, true) assert((*offsets)[0], 0, 6, 2, -1, true)
assert(offsets[1], 6, 11, 200, 100, false) assert((*offsets)[1], 6, 11, 200, 100, false)
}) })
} }

View File

@ -3,7 +3,7 @@ package fzf
import "sync" import "sync"
// queryCache associates strings to lists of items // queryCache associates strings to lists of items
type queryCache map[string][]*Item type queryCache map[string][]*Result
// ChunkCache associates Chunk and query string to lists of items // ChunkCache associates Chunk and query string to lists of items
type ChunkCache struct { type ChunkCache struct {
@ -17,7 +17,7 @@ func NewChunkCache() ChunkCache {
} }
// Add adds the list to the cache // Add adds the list to the cache
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) { func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) {
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax { if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
return return
} }
@ -34,7 +34,7 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) {
} }
// Find is called to lookup ChunkCache // Find is called to lookup ChunkCache
func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Item, bool) { func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Result, bool) {
if len(key) == 0 || !chunk.IsFull() { if len(key) == 0 || !chunk.IsFull() {
return nil, false return nil, false
} }

View File

@ -7,8 +7,8 @@ func TestChunkCache(t *testing.T) {
chunk2 := make(Chunk, chunkSize) chunk2 := make(Chunk, chunkSize)
chunk1p := &Chunk{} chunk1p := &Chunk{}
chunk2p := &chunk2 chunk2p := &chunk2
items1 := []*Item{&Item{}} items1 := []*Result{&Result{}}
items2 := []*Item{&Item{}, &Item{}} items2 := []*Result{&Result{}, &Result{}}
cache.Add(chunk1p, "foo", items1) cache.Add(chunk1p, "foo", items1)
cache.Add(chunk2p, "foo", items1) cache.Add(chunk2p, "foo", items1)
cache.Add(chunk2p, "bar", items2) cache.Add(chunk2p, "bar", items2)

View File

@ -12,7 +12,7 @@ func TestChunkList(t *testing.T) {
sortCriteria = []criterion{byMatchLen, byLength} sortCriteria = []criterion{byMatchLen, byLength}
cl := NewChunkList(func(s []byte, i int) *Item { cl := NewChunkList(func(s []byte, i int) *Item {
return &Item{text: util.ToChars(s), rank: buildEmptyRank(int32(i * 2))} return &Item{text: util.ToChars(s), index: int32(i * 2)}
}) })
// Snapshot // Snapshot
@ -41,11 +41,8 @@ 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")
} }
last := func(arr [5]int32) int32 { if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].index != 0 ||
return arr[len(arr)-1] (*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].index != 2 {
}
if (*chunk1)[0].text.ToString() != "hello" || last((*chunk1)[0].rank) != 0 ||
(*chunk1)[1].text.ToString() != "world" || last((*chunk1)[1].rank) != 2 {
t.Error("Invalid data") t.Error("Invalid data")
} }
if chunk1.IsFull() { if chunk1.IsFull() {

View File

@ -56,16 +56,16 @@ func Run(opts *Options) {
eventBox := util.NewEventBox() eventBox := util.NewEventBox()
// ANSI code processor // ANSI code processor
ansiProcessor := func(data []byte) (util.Chars, []ansiOffset) { ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
return util.ToChars(data), nil return util.ToChars(data), nil
} }
ansiProcessorRunes := func(data []rune) (util.Chars, []ansiOffset) { ansiProcessorRunes := func(data []rune) (util.Chars, *[]ansiOffset) {
return util.RunesToChars(data), nil return util.RunesToChars(data), nil
} }
if opts.Ansi { if opts.Ansi {
if opts.Theme != nil { if opts.Theme != nil {
var state *ansiState var state *ansiState
ansiProcessor = func(data []byte) (util.Chars, []ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, offsets, newState := extractColor(string(data), state, nil) trimmed, offsets, newState := extractColor(string(data), state, nil)
state = newState state = newState
return util.RunesToChars([]rune(trimmed)), offsets return util.RunesToChars([]rune(trimmed)), offsets
@ -73,12 +73,12 @@ func Run(opts *Options) {
} else { } else {
// When color is disabled but ansi option is given, // When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input // we simply strip out ANSI codes from the input
ansiProcessor = func(data []byte) (util.Chars, []ansiOffset) { ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(string(data), nil, nil) trimmed, _, _ := extractColor(string(data), nil, nil)
return util.RunesToChars([]rune(trimmed)), nil return util.RunesToChars([]rune(trimmed)), nil
} }
} }
ansiProcessorRunes = func(data []rune) (util.Chars, []ansiOffset) { ansiProcessorRunes = func(data []rune) (util.Chars, *[]ansiOffset) {
return ansiProcessor([]byte(string(data))) return ansiProcessor([]byte(string(data)))
} }
} }
@ -95,14 +95,13 @@ func Run(opts *Options) {
} }
chars, colors := ansiProcessor(data) chars, colors := ansiProcessor(data)
return &Item{ return &Item{
index: int32(index),
text: chars, text: chars,
colors: colors, colors: colors}
rank: buildEmptyRank(int32(index))}
}) })
} else { } else {
chunkList = NewChunkList(func(data []byte, index int) *Item { chunkList = NewChunkList(func(data []byte, index int) *Item {
chars := util.ToChars(data) tokens := Tokenize(util.ToChars(data), opts.Delimiter)
tokens := Tokenize(chars, opts.Delimiter)
trans := Transform(tokens, opts.WithNth) trans := Transform(tokens, opts.WithNth)
if len(header) < opts.HeaderLines { if len(header) < opts.HeaderLines {
header = append(header, string(joinTokens(trans))) header = append(header, string(joinTokens(trans)))
@ -111,10 +110,9 @@ func Run(opts *Options) {
} }
textRunes := joinTokens(trans) textRunes := joinTokens(trans)
item := Item{ item := Item{
text: util.RunesToChars(textRunes), index: int32(index),
origText: &data, origText: &data,
colors: nil, colors: nil}
rank: buildEmptyRank(int32(index))}
trimmed, colors := ansiProcessorRunes(textRunes) trimmed, colors := ansiProcessorRunes(textRunes)
item.text = trimmed item.text = trimmed
@ -163,7 +161,7 @@ func Run(opts *Options) {
reader := Reader{ reader := Reader{
func(runes []byte) bool { func(runes []byte) bool {
item := chunkList.trans(runes, 0) item := chunkList.trans(runes, 0)
if item != nil && pattern.MatchItem(item) { if item != nil && pattern.MatchItem(item) != nil {
fmt.Println(item.text.ToString()) fmt.Println(item.text.ToString())
found = true found = true
} }
@ -179,7 +177,7 @@ func Run(opts *Options) {
chunks: snapshot, chunks: snapshot,
pattern: pattern}) pattern: pattern})
for i := 0; i < merger.Length(); i++ { for i := 0; i < merger.Length(); i++ {
fmt.Println(merger.Get(i).AsString(opts.Ansi)) fmt.Println(merger.Get(i).item.AsString(opts.Ansi))
found = true found = true
} }
} }
@ -259,7 +257,7 @@ func Run(opts *Options) {
fmt.Println() fmt.Println()
} }
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
fmt.Println(val.Get(i).AsString(opts.Ansi)) fmt.Println(val.Get(i).item.AsString(opts.Ansi))
} }
if count > 0 { if count > 0 {
os.Exit(exitOk) os.Exit(exitOk)

View File

@ -1,295 +1,39 @@
package fzf package fzf
import ( import (
"math"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
// Offset holds three 32-bit integers denoting the offsets of a matched substring
type Offset [3]int32
type colorOffset struct {
offset [2]int32
color int
bold bool
}
// Item represents each input line // Item represents each input line
type Item struct { type Item struct {
index int32
text util.Chars text util.Chars
origText *[]byte origText *[]byte
colors *[]ansiOffset
transformed []Token transformed []Token
offsets []Offset
colors []ansiOffset
rank [5]int32
bonus int32
}
// Sort criteria to use. Never changes once fzf is started.
var sortCriteria []criterion
func isRankValid(rank [5]int32) bool {
// Exclude ordinal index
for _, r := range rank[:4] {
if r > 0 {
return true
}
}
return false
}
func buildEmptyRank(index int32) [5]int32 {
return [5]int32{0, 0, 0, 0, index}
} }
// Index returns ordinal index of the Item // Index returns ordinal index of the Item
func (item *Item) Index() int32 { func (item *Item) Index() int32 {
return item.rank[4] return item.index
} }
// Rank calculates rank of the Item // Colors returns ansiOffsets of the Item
func (item *Item) Rank(cache bool) [5]int32 { func (item *Item) Colors() []ansiOffset {
if cache && isRankValid(item.rank) { if item.colors == nil {
return item.rank return []ansiOffset{}
} }
matchlen := 0 return *item.colors
prevEnd := 0
lenSum := 0
minBegin := math.MaxInt32
for _, offset := range item.offsets {
begin := int(offset[0])
end := int(offset[1])
trimLen := int(offset[2])
lenSum += trimLen
if prevEnd > begin {
begin = prevEnd
}
if end > prevEnd {
prevEnd = end
}
if end > begin {
if begin < minBegin {
minBegin = begin
}
matchlen += end - begin
}
}
rank := buildEmptyRank(item.Index())
for idx, criterion := range sortCriteria {
var val int32
switch criterion {
case byMatchLen:
if matchlen == 0 {
val = math.MaxInt32
} else {
// It is extremely unlikely that bonus exceeds 128
val = 128*int32(matchlen) - item.bonus
}
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(item.text.Length())
}
case byBegin:
// We can't just look at item.offsets[0][0] because it can be an inverse term
whitePrefixLen := 0
numChars := item.text.Length()
for idx := 0; idx < numChars; idx++ {
r := item.text.Get(idx)
whitePrefixLen = idx
if idx == minBegin || r != ' ' && r != '\t' {
break
}
}
val = int32(minBegin - whitePrefixLen)
case byEnd:
if prevEnd > 0 {
val = int32(1 + item.text.Length() - prevEnd)
} else {
// Empty offsets due to inverse terms.
val = 1
}
}
rank[idx] = val
}
if cache {
item.rank = rank
}
return rank
} }
// AsString returns the original string // AsString returns the original string
func (item *Item) AsString(stripAnsi bool) string { func (item *Item) AsString(stripAnsi bool) string {
return *item.StringPtr(stripAnsi)
}
// StringPtr returns the pointer to the original string
func (item *Item) StringPtr(stripAnsi bool) *string {
if item.origText != nil { if item.origText != nil {
if stripAnsi { if stripAnsi {
trimmed, _, _ := extractColor(string(*item.origText), nil, nil) trimmed, _, _ := extractColor(string(*item.origText), nil, nil)
return &trimmed return trimmed
} }
orig := string(*item.origText) return string(*item.origText)
return &orig
} }
str := item.text.ToString() return item.text.ToString()
return &str
}
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {
if len(item.colors) == 0 {
var offsets []colorOffset
for _, off := range item.offsets {
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, bold: bold})
}
return offsets
}
// Find max column
var maxCol int32
for _, off := range item.offsets {
if off[1] > maxCol {
maxCol = off[1]
}
}
for _, ansi := range item.colors {
if ansi.offset[1] > maxCol {
maxCol = ansi.offset[1]
}
}
cols := make([]int, maxCol)
for colorIndex, ansi := range item.colors {
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
cols[i] = colorIndex + 1 // XXX
}
}
for _, off := range item.offsets {
for i := off[0]; i < off[1]; i++ {
cols[i] = -1
}
}
// sort.Sort(ByOrder(offsets))
// Merge offsets
// ------------ ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
curr := 0
start := 0
var offsets []colorOffset
add := func(idx int) {
if curr != 0 && idx > start {
if curr == -1 {
offsets = append(offsets, colorOffset{
offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold})
} else {
ansi := item.colors[curr-1]
fg := ansi.color.fg
if fg == -1 {
if current {
fg = curses.CurrentFG
} else {
fg = curses.FG
}
}
bg := ansi.color.bg
if bg == -1 {
if current {
bg = curses.DarkBG
} else {
bg = curses.BG
}
}
offsets = append(offsets, colorOffset{
offset: [2]int32{int32(start), int32(idx)},
color: curses.PairFor(fg, bg),
bold: ansi.color.bold || bold})
}
}
}
for idx, col := range cols {
if col != curr {
add(idx)
start = idx
curr = col
}
}
add(int(maxCol))
return offsets
}
// ByOrder is for sorting substring offsets
type ByOrder []Offset
func (a ByOrder) Len() int {
return len(a)
}
func (a ByOrder) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByOrder) Less(i, j int) bool {
ioff := a[i]
joff := a[j]
return (ioff[0] < joff[0]) || (ioff[0] == joff[0]) && (ioff[1] <= joff[1])
}
// ByRelevance is for sorting Items
type ByRelevance []*Item
func (a ByRelevance) Len() int {
return len(a)
}
func (a ByRelevance) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a ByRelevance) Less(i, j int) bool {
irank := a[i].Rank(true)
jrank := a[j].Rank(true)
return compareRanks(irank, jrank, false)
}
// 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 [5]int32, jrank [5]int32, tac bool) bool {
for idx := 0; idx < 4; idx++ {
left := irank[idx]
right := jrank[idx]
if left < right {
return true
} else if left > right {
return false
}
}
return (irank[4] <= jrank[4]) != tac
} }

View File

@ -1,109 +1,23 @@
package fzf package fzf
import ( import (
"math"
"sort"
"testing" "testing"
"github.com/junegunn/fzf/src/curses"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
) )
func TestOffsetSort(t *testing.T) { func TestStringPtr(t *testing.T) {
offsets := []Offset{ orig := []byte("\x1b[34mfoo")
Offset{3, 5}, Offset{2, 7}, text := []byte("\x1b[34mbar")
Offset{1, 3}, Offset{2, 9}} item := Item{origText: &orig, text: util.ToChars(text)}
sort.Sort(ByOrder(offsets)) if item.AsString(true) != "foo" || item.AsString(false) != string(orig) {
t.Fail()
if offsets[0][0] != 1 || offsets[0][1] != 3 || }
offsets[1][0] != 2 || offsets[1][1] != 7 || if item.AsString(true) != "foo" {
offsets[2][0] != 2 || offsets[2][1] != 9 || t.Fail()
offsets[3][0] != 3 || offsets[3][1] != 5 { }
t.Error("Invalid order:", offsets) item.origText = nil
if item.AsString(true) != string(text) || item.AsString(false) != string(text) {
t.Fail()
} }
} }
func TestRankComparison(t *testing.T) {
if compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{2, 0, 0, 0, 7}, false) ||
!compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{3, 0, 0, 0, 6}, false) ||
!compareRanks([5]int32{1, 2, 0, 0, 3}, [5]int32{1, 3, 0, 0, 2}, false) ||
!compareRanks([5]int32{0, 0, 0, 0, 0}, [5]int32{0, 0, 0, 0, 0}, false) {
t.Error("Invalid order")
}
if compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{2, 0, 0, 0, 7}, true) ||
!compareRanks([5]int32{3, 0, 0, 0, 5}, [5]int32{3, 0, 0, 0, 6}, false) ||
!compareRanks([5]int32{1, 2, 0, 0, 3}, [5]int32{1, 3, 0, 0, 2}, true) ||
!compareRanks([5]int32{0, 0, 0, 0, 0}, [5]int32{0, 0, 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}
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
item1 := Item{text: util.RunesToChars(strs[0]), offsets: []Offset{}, rank: [5]int32{0, 0, 0, 0, 1}}
rank1 := item1.Rank(true)
if rank1[0] != math.MaxInt32 || rank1[1] != 3 || rank1[4] != 1 {
t.Error(item1.Rank(true))
}
// Only differ in index
item2 := Item{text: util.RunesToChars(strs[0]), offsets: []Offset{}}
items := []*Item{&item1, &item2}
sort.Sort(ByRelevance(items))
if items[0] != &item2 || items[1] != &item1 {
t.Error(items)
}
items = []*Item{&item2, &item1, &item1, &item2}
sort.Sort(ByRelevance(items))
if items[0] != &item2 || items[1] != &item2 ||
items[2] != &item1 || items[3] != &item1 {
t.Error(items)
}
// Sort by relevance
item3 := Item{text: util.RunesToChars(strs[1]), rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item4 := Item{text: util.RunesToChars(strs[1]), rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 2}, Offset{6, 7}}}
item5 := Item{text: util.RunesToChars(strs[2]), rank: [5]int32{0, 0, 0, 0, 2}, offsets: []Offset{Offset{1, 3}, Offset{5, 7}}}
item6 := Item{text: util.RunesToChars(strs[2]), rank: [5]int32{0, 0, 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 ||
items[2] != &item5 || items[3] != &item3 ||
items[4] != &item2 || items[5] != &item1 {
t.Error(items)
}
}
func TestColorOffset(t *testing.T) {
// ------------ 20 ---- -- ----
// ++++++++ ++++++++++
// --++++++++-- --++++++++++---
item := Item{
offsets: []Offset{Offset{5, 15}, Offset{25, 35}},
colors: []ansiOffset{
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, false}},
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, true}},
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, false}},
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, true}}}}
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
offsets := item.colorOffsets(99, false, true)
assert := func(idx int, b int32, e int32, c int, bold bool) {
o := offsets[idx]
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.bold != bold {
t.Error(o)
}
}
assert(0, 0, 5, curses.ColUser, false)
assert(1, 5, 15, 99, false)
assert(2, 15, 20, curses.ColUser, false)
assert(3, 22, 25, curses.ColUser+1, true)
assert(4, 25, 35, 99, false)
assert(5, 35, 40, curses.ColUser+2, true)
}

View File

@ -128,7 +128,7 @@ func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
type partialResult struct { type partialResult struct {
index int index int
matches []*Item matches []*Result
} }
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
@ -155,15 +155,21 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
waitGroup.Add(1) waitGroup.Add(1)
go func(idx int, chunks []*Chunk) { go func(idx int, chunks []*Chunk) {
defer func() { waitGroup.Done() }() defer func() { waitGroup.Done() }()
sliceMatches := []*Item{} count := 0
for _, chunk := range chunks { allMatches := make([][]*Result, len(chunks))
for idx, chunk := range chunks {
matches := request.pattern.Match(chunk) matches := request.pattern.Match(chunk)
sliceMatches = append(sliceMatches, matches...) allMatches[idx] = matches
count += len(matches)
if cancelled.Get() { if cancelled.Get() {
return return
} }
countChan <- len(matches) countChan <- len(matches)
} }
sliceMatches := make([]*Result, 0, count)
for _, matches := range allMatches {
sliceMatches = append(sliceMatches, matches...)
}
if m.sort { if m.sort {
if m.tac { if m.tac {
sort.Sort(ByRelevanceTac(sliceMatches)) sort.Sort(ByRelevanceTac(sliceMatches))
@ -200,7 +206,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
} }
} }
partialResults := make([][]*Item, numSlices) partialResults := make([][]*Result, numSlices)
for _ = range slices { for _ = range slices {
partialResult := <-resultChan partialResult := <-resultChan
partialResults[partialResult.index] = partialResult.matches partialResults[partialResult.index] = partialResult.matches

View File

@ -3,13 +3,13 @@ package fzf
import "fmt" import "fmt"
// EmptyMerger is a Merger with no data // EmptyMerger is a Merger with no data
var EmptyMerger = NewMerger([][]*Item{}, false, false) var EmptyMerger = NewMerger([][]*Result{}, 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
type Merger struct { type Merger struct {
lists [][]*Item lists [][]*Result
merged []*Item merged []*Result
chunks *[]*Chunk chunks *[]*Chunk
cursors []int cursors []int
sorted bool sorted bool
@ -33,10 +33,10 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
} }
// NewMerger returns a new Merger // NewMerger returns a new Merger
func NewMerger(lists [][]*Item, sorted bool, tac bool) *Merger { func NewMerger(lists [][]*Result, sorted bool, tac bool) *Merger {
mg := Merger{ mg := Merger{
lists: lists, lists: lists,
merged: []*Item{}, merged: []*Result{},
chunks: nil, chunks: nil,
cursors: make([]int, len(lists)), cursors: make([]int, len(lists)),
sorted: sorted, sorted: sorted,
@ -55,14 +55,14 @@ func (mg *Merger) Length() int {
return mg.count return mg.count
} }
// Get returns the pointer to the Item object indexed by the given integer // Get returns the pointer to the Result object indexed by the given integer
func (mg *Merger) Get(idx int) *Item { func (mg *Merger) Get(idx int) *Result {
if mg.chunks != nil { if mg.chunks != nil {
if mg.tac { if mg.tac {
idx = mg.count - idx - 1 idx = mg.count - idx - 1
} }
chunk := (*mg.chunks)[idx/chunkSize] chunk := (*mg.chunks)[idx/chunkSize]
return (*chunk)[idx%chunkSize] return &Result{item: (*chunk)[idx%chunkSize]}
} }
if mg.sorted { if mg.sorted {
@ -86,9 +86,9 @@ func (mg *Merger) cacheable() bool {
return mg.count < mergerCacheMax return mg.count < mergerCacheMax
} }
func (mg *Merger) mergedGet(idx int) *Item { func (mg *Merger) mergedGet(idx int) *Result {
for i := len(mg.merged); i <= idx; i++ { for i := len(mg.merged); i <= idx; i++ {
minRank := buildEmptyRank(0) minRank := minRank()
minIdx := -1 minIdx := -1
for listIdx, list := range mg.lists { for listIdx, list := range mg.lists {
cursor := mg.cursors[listIdx] cursor := mg.cursors[listIdx]
@ -97,7 +97,7 @@ func (mg *Merger) mergedGet(idx int) *Item {
continue continue
} }
if cursor >= 0 { if cursor >= 0 {
rank := list[cursor].Rank(false) rank := list[cursor].rank
if minIdx < 0 || compareRanks(rank, minRank, mg.tac) { if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
minRank = rank minRank = rank
minIdx = listIdx minIdx = listIdx

View File

@ -15,7 +15,7 @@ func assert(t *testing.T, cond bool, msg ...string) {
} }
} }
func randItem() *Item { func randResult() *Result {
str := fmt.Sprintf("%d", rand.Uint32()) str := fmt.Sprintf("%d", rand.Uint32())
offsets := make([]Offset, rand.Int()%3) offsets := make([]Offset, rand.Int()%3)
for idx := range offsets { for idx := range offsets {
@ -23,10 +23,10 @@ func randItem() *Item {
eidx := sidx + int32(rand.Uint32()%20) eidx := sidx + int32(rand.Uint32()%20)
offsets[idx] = Offset{sidx, eidx} offsets[idx] = Offset{sidx, eidx}
} }
return &Item{ return &Result{
text: util.RunesToChars([]rune(str)), item: &Item{text: util.RunesToChars([]rune(str))},
rank: buildEmptyRank(rand.Int31()), offsets: offsets,
offsets: offsets} rank: rank{index: rand.Int31()}}
} }
func TestEmptyMerger(t *testing.T) { func TestEmptyMerger(t *testing.T) {
@ -36,23 +36,23 @@ func TestEmptyMerger(t *testing.T) {
assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list") assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
} }
func buildLists(partiallySorted bool) ([][]*Item, []*Item) { func buildLists(partiallySorted bool) ([][]*Result, []*Result) {
numLists := 4 numLists := 4
lists := make([][]*Item, numLists) lists := make([][]*Result, numLists)
cnt := 0 cnt := 0
for i := 0; i < numLists; i++ { for i := 0; i < numLists; i++ {
numItems := rand.Int() % 20 numResults := rand.Int() % 20
cnt += numItems cnt += numResults
lists[i] = make([]*Item, numItems) lists[i] = make([]*Result, numResults)
for j := 0; j < numItems; j++ { for j := 0; j < numResults; j++ {
item := randItem() item := randResult()
lists[i][j] = item lists[i][j] = item
} }
if partiallySorted { if partiallySorted {
sort.Sort(ByRelevance(lists[i])) sort.Sort(ByRelevance(lists[i]))
} }
} }
items := []*Item{} items := []*Result{}
for _, list := range lists { for _, list := range lists {
items = append(items, list...) items = append(items, list...)
} }

View File

@ -95,6 +95,7 @@ type criterion int
const ( const (
byMatchLen criterion = iota byMatchLen criterion = iota
byBonus
byLength byLength
byBegin byBegin
byEnd byEnd
@ -178,7 +179,7 @@ func defaultOptions() *Options {
Delimiter: Delimiter{}, Delimiter: Delimiter{},
Sort: 1000, Sort: 1000,
Tac: false, Tac: false,
Criteria: []criterion{byMatchLen, byLength}, Criteria: []criterion{byMatchLen, byBonus, byLength},
Multi: false, Multi: false,
Ansi: false, Ansi: false,
Mouse: true, Mouse: true,
@ -406,7 +407,7 @@ func parseKeyChords(str string, message string) map[int]string {
} }
func parseTiebreak(str string) []criterion { func parseTiebreak(str string) []criterion {
criteria := []criterion{byMatchLen} criteria := []criterion{byMatchLen, byBonus}
hasIndex := false hasIndex := false
hasLength := false hasLength := false
hasBegin := false hasBegin := false

View File

@ -2,7 +2,6 @@ package fzf
import ( import (
"regexp" "regexp"
"sort"
"strings" "strings"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
@ -235,9 +234,7 @@ func (p *Pattern) CacheKey() string {
} }
// Match returns the list of matches Items in the given Chunk // Match returns the list of matches Items in the given Chunk
func (p *Pattern) Match(chunk *Chunk) []*Item { func (p *Pattern) Match(chunk *Chunk) []*Result {
space := chunk
// ChunkCache: Exact match // ChunkCache: Exact match
cacheKey := p.CacheKey() cacheKey := p.CacheKey()
if p.cacheable { if p.cacheable {
@ -246,7 +243,8 @@ func (p *Pattern) Match(chunk *Chunk) []*Item {
} }
} }
// ChunkCache: Prefix/suffix match // Prefix/suffix cache
var space []*Result
Loop: Loop:
for idx := 1; idx < len(cacheKey); idx++ { for idx := 1; idx < len(cacheKey); idx++ {
// [---------| ] | [ |---------] // [---------| ] | [ |---------]
@ -256,14 +254,13 @@ Loop:
suffix := cacheKey[idx:] suffix := cacheKey[idx:]
for _, substr := range [2]*string{&prefix, &suffix} { for _, substr := range [2]*string{&prefix, &suffix} {
if cached, found := _cache.Find(chunk, *substr); found { if cached, found := _cache.Find(chunk, *substr); found {
cachedChunk := Chunk(cached) space = cached
space = &cachedChunk
break Loop break Loop
} }
} }
} }
matches := p.matchChunk(space) matches := p.matchChunk(chunk, space)
if p.cacheable { if p.cacheable {
_cache.Add(chunk, cacheKey, matches) _cache.Add(chunk, cacheKey, matches)
@ -271,20 +268,19 @@ Loop:
return matches return matches
} }
func (p *Pattern) matchChunk(chunk *Chunk) []*Item { func (p *Pattern) matchChunk(chunk *Chunk, space []*Result) []*Result {
matches := []*Item{} matches := []*Result{}
if !p.extended {
if space == nil {
for _, item := range *chunk { for _, item := range *chunk {
offset, bonus := p.basicMatch(item) if match := p.MatchItem(item); match != nil {
if sidx := offset[0]; sidx >= 0 { matches = append(matches, match)
matches = append(matches,
dupItem(item, []Offset{offset}, bonus))
} }
} }
} else { } else {
for _, item := range *chunk { for _, result := range space {
if offsets, bonus := p.extendedMatch(item); len(offsets) == len(p.termSets) { if match := p.MatchItem(result.item); match != nil {
matches = append(matches, dupItem(item, offsets, bonus)) matches = append(matches, match)
} }
} }
} }
@ -292,29 +288,21 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item {
} }
// MatchItem returns true if the Item is a match // MatchItem returns true if the Item is a match
func (p *Pattern) MatchItem(item *Item) bool { func (p *Pattern) MatchItem(item *Item) *Result {
if !p.extended { if p.extended {
offset, _ := p.basicMatch(item) if offsets, bonus, trimLen := p.extendedMatch(item); len(offsets) == len(p.termSets) {
sidx := offset[0] return buildResult(item, offsets, bonus, trimLen)
return sidx >= 0 }
return nil
} }
offsets, _ := p.extendedMatch(item) offset, bonus, trimLen := p.basicMatch(item)
return len(offsets) == len(p.termSets) if sidx := offset[0]; sidx >= 0 {
return buildResult(item, []Offset{offset}, bonus, trimLen)
}
return nil
} }
func dupItem(item *Item, offsets []Offset, bonus int32) *Item { func (p *Pattern) basicMatch(item *Item) (Offset, int, int) {
sort.Sort(ByOrder(offsets))
return &Item{
text: item.text,
origText: item.origText,
transformed: item.transformed,
offsets: offsets,
bonus: bonus,
colors: item.colors,
rank: buildEmptyRank(item.Index())}
}
func (p *Pattern) basicMatch(item *Item) (Offset, int32) {
input := p.prepareInput(item) input := p.prepareInput(item)
if p.fuzzy { if p.fuzzy {
return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text) return p.iter(algo.FuzzyMatch, input, p.caseSensitive, p.forward, p.text)
@ -322,33 +310,39 @@ func (p *Pattern) basicMatch(item *Item) (Offset, int32) {
return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text) return p.iter(algo.ExactMatchNaive, input, p.caseSensitive, p.forward, p.text)
} }
func (p *Pattern) extendedMatch(item *Item) ([]Offset, int32) { func (p *Pattern) extendedMatch(item *Item) ([]Offset, int, int) {
input := p.prepareInput(item) input := p.prepareInput(item)
offsets := []Offset{} offsets := []Offset{}
var totalBonus int32 var totalBonus int
var totalTrimLen int
for _, termSet := range p.termSets { for _, termSet := range p.termSets {
var offset *Offset var offset Offset
var bonus int32 var bonus int
var trimLen int
matched := false
for _, term := range termSet { for _, term := range termSet {
pfun := p.procFun[term.typ] pfun := p.procFun[term.typ]
off, pen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text) off, pen, tLen := p.iter(pfun, input, term.caseSensitive, p.forward, term.text)
if sidx := off[0]; sidx >= 0 { if sidx := off[0]; sidx >= 0 {
if term.inv { if term.inv {
continue continue
} }
offset, bonus = &off, pen offset, bonus, trimLen = off, pen, tLen
matched = true
break break
} else if term.inv { } else if term.inv {
offset, bonus = &Offset{0, 0, 0}, 0 offset, bonus, trimLen = Offset{0, 0}, 0, 0
matched = true
continue continue
} }
} }
if offset != nil { if matched {
offsets = append(offsets, *offset) offsets = append(offsets, offset)
totalBonus += bonus totalBonus += bonus
totalTrimLen += trimLen
} }
} }
return offsets, totalBonus return offsets, totalBonus, totalTrimLen
} }
func (p *Pattern) prepareInput(item *Item) []Token { func (p *Pattern) prepareInput(item *Item) []Token {
@ -357,26 +351,24 @@ func (p *Pattern) prepareInput(item *Item) []Token {
} }
var ret []Token var ret []Token
if len(p.nth) > 0 { if len(p.nth) == 0 {
ret = []Token{Token{text: &item.text, prefixLength: 0, trimLength: int32(item.text.TrimLength())}}
} else {
tokens := Tokenize(item.text, p.delimiter) tokens := Tokenize(item.text, p.delimiter)
ret = Transform(tokens, p.nth) ret = Transform(tokens, p.nth)
} else {
ret = []Token{Token{text: item.text, prefixLength: 0, trimLength: item.text.TrimLength()}}
} }
item.transformed = ret item.transformed = ret
return ret return ret
} }
func (p *Pattern) iter(pfun func(bool, bool, util.Chars, []rune) algo.Result, func (p *Pattern) iter(pfun func(bool, bool, util.Chars, []rune) algo.Result,
tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int32) { tokens []Token, caseSensitive bool, forward bool, pattern []rune) (Offset, int, int) {
for _, part := range tokens { for _, part := range tokens {
prefixLength := int32(part.prefixLength) if res := pfun(caseSensitive, forward, *part.text, pattern); res.Start >= 0 {
if res := pfun(caseSensitive, forward, part.text, pattern); res.Start >= 0 { sidx := int32(res.Start) + part.prefixLength
sidx := res.Start + prefixLength eidx := int32(res.End) + part.prefixLength
eidx := res.End + prefixLength return Offset{sidx, eidx}, res.Bonus, int(part.trimLength)
return Offset{sidx, eidx, int32(part.trimLength)}, res.Bonus
} }
} }
// TODO: math.MaxUint16 return Offset{-1, -1}, 0, -1
return Offset{-1, -1, -1}, 0.0
} }

View File

@ -83,7 +83,7 @@ func TestEqual(t *testing.T) {
clearPatternCache() clearPatternCache()
pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$")) pattern := BuildPattern(true, true, CaseSmart, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int32, eidxExpected int32) { match := func(str string, sidxExpected int, eidxExpected int) {
res := algo.EqualMatch( res := algo.EqualMatch(
pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text) pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text)
if res.Start != sidxExpected || res.End != eidxExpected { if res.Start != sidxExpected || res.End != eidxExpected {
@ -133,10 +133,10 @@ func TestOrigTextAndTransformed(t *testing.T) {
transformed: trans}, transformed: trans},
} }
pattern.extended = extended pattern.extended = extended
matches := pattern.matchChunk(&chunk) matches := pattern.matchChunk(&chunk, nil) // No cache
if matches[0].text.ToString() != "junegunn" || string(*matches[0].origText) != "junegunn.choi" || if matches[0].item.text.ToString() != "junegunn" || string(*matches[0].item.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 ||
!reflect.DeepEqual(matches[0].transformed, trans) { !reflect.DeepEqual(matches[0].item.transformed, trans) {
t.Error("Invalid match result", matches) t.Error("Invalid match result", matches)
} }
} }

View File

@ -77,7 +77,7 @@ type Terminal struct {
type selectedItem struct { type selectedItem struct {
at time.Time at time.Time
text *string text string
} }
type byTimeOrder []selectedItem type byTimeOrder []selectedItem
@ -357,7 +357,7 @@ func (t *Terminal) output() bool {
} }
} else { } else {
for _, sel := range t.sortSelected() { for _, sel := range t.sortSelected() {
fmt.Println(*sel.text) fmt.Println(sel.text)
} }
} }
return found return found
@ -565,11 +565,10 @@ func (t *Terminal) printHeader() {
state = newState state = newState
item := &Item{ item := &Item{
text: util.RunesToChars([]rune(trimmed)), text: util.RunesToChars([]rune(trimmed)),
colors: colors, colors: colors}
rank: buildEmptyRank(0)}
t.move(line, 2, true) t.move(line, 2, true)
t.printHighlighted(item, false, C.ColHeader, 0, false) t.printHighlighted(&Result{item: item}, false, C.ColHeader, 0, false)
} }
} }
@ -590,7 +589,8 @@ func (t *Terminal) printList() {
} }
} }
func (t *Terminal) printItem(item *Item, i int, current bool) { func (t *Terminal) printItem(result *Result, i int, current bool) {
item := result.item
_, selected := t.selected[item.Index()] _, selected := t.selected[item.Index()]
label := " " label := " "
if t.jumping != jumpDisabled { if t.jumping != jumpDisabled {
@ -609,14 +609,14 @@ func (t *Terminal) printItem(item *Item, i int, current bool) {
} else { } else {
t.window.CPrint(C.ColCurrent, true, " ") t.window.CPrint(C.ColCurrent, true, " ")
} }
t.printHighlighted(item, true, C.ColCurrent, C.ColCurrentMatch, true) t.printHighlighted(result, true, C.ColCurrent, C.ColCurrentMatch, true)
} else { } else {
if selected { if selected {
t.window.CPrint(C.ColSelected, true, ">") t.window.CPrint(C.ColSelected, true, ">")
} else { } else {
t.window.Print(" ") t.window.Print(" ")
} }
t.printHighlighted(item, false, 0, C.ColMatch, false) t.printHighlighted(result, false, 0, C.ColMatch, false)
} }
} }
@ -667,16 +667,17 @@ func overflow(runes []rune, max int) bool {
return false return false
} }
func (t *Terminal) printHighlighted(item *Item, bold bool, col1 int, col2 int, current bool) { func (t *Terminal) printHighlighted(result *Result, bold bool, col1 int, col2 int, current bool) {
item := result.item
var maxe int var maxe int
for _, offset := range item.offsets { for _, offset := range result.offsets {
maxe = util.Max(maxe, int(offset[1])) maxe = util.Max(maxe, int(offset[1]))
} }
// Overflow // Overflow
text := make([]rune, item.text.Length()) text := make([]rune, item.text.Length())
copy(text, item.text.ToRunes()) copy(text, item.text.ToRunes())
offsets := item.colorOffsets(col2, bold, current) offsets := result.colorOffsets(col2, bold, current)
maxWidth := t.window.Width - 3 maxWidth := t.window.Width - 3
maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text))
if overflow(text, maxWidth) { if overflow(text, maxWidth) {
@ -866,7 +867,7 @@ func (t *Terminal) isPreviewEnabled() bool {
} }
func (t *Terminal) current() string { func (t *Terminal) current() string {
return t.merger.Get(t.cy).AsString(t.ansi) return t.merger.Get(t.cy).item.AsString(t.ansi)
} }
// Loop is called to start Terminal I/O // Loop is called to start Terminal I/O
@ -1037,13 +1038,13 @@ func (t *Terminal) Loop() {
} }
selectItem := func(item *Item) bool { selectItem := func(item *Item) bool {
if _, found := t.selected[item.Index()]; !found { if _, found := t.selected[item.Index()]; !found {
t.selected[item.Index()] = selectedItem{time.Now(), item.StringPtr(t.ansi)} t.selected[item.Index()] = selectedItem{time.Now(), item.AsString(t.ansi)}
return true return true
} }
return false return false
} }
toggleY := func(y int) { toggleY := func(y int) {
item := t.merger.Get(y) item := t.merger.Get(y).item
if !selectItem(item) { if !selectItem(item) {
delete(t.selected, item.Index()) delete(t.selected, item.Index())
} }
@ -1068,14 +1069,14 @@ func (t *Terminal) Loop() {
case actIgnore: case actIgnore:
case actExecute: case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() { if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy) item := t.merger.Get(t.cy).item
t.executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi))) t.executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi)))
} }
case actExecuteMulti: case actExecuteMulti:
if len(t.selected) > 0 { if len(t.selected) > 0 {
sels := make([]string, len(t.selected)) sels := make([]string, len(t.selected))
for i, sel := range t.sortSelected() { for i, sel := range t.sortSelected() {
sels[i] = quoteEntry(*sel.text) sels[i] = quoteEntry(sel.text)
} }
t.executeCommand(t.execmap[mapkey], strings.Join(sels, " ")) t.executeCommand(t.execmap[mapkey], strings.Join(sels, " "))
} else { } else {
@ -1137,7 +1138,7 @@ func (t *Terminal) Loop() {
case actSelectAll: case actSelectAll:
if t.multi { if t.multi {
for i := 0; i < t.merger.Length(); i++ { for i := 0; i < t.merger.Length(); i++ {
item := t.merger.Get(i) item := t.merger.Get(i).item
selectItem(item) selectItem(item)
} }
req(reqList, reqInfo) req(reqList, reqInfo)

View File

@ -18,9 +18,9 @@ type Range struct {
// Token contains the tokenized part of the strings and its prefix length // Token contains the tokenized part of the strings and its prefix length
type Token struct { type Token struct {
text util.Chars text *util.Chars
prefixLength int prefixLength int32
trimLength int trimLength int32
} }
// Delimiter for tokenizing the input // Delimiter for tokenizing the input
@ -80,9 +80,8 @@ func withPrefixLengths(tokens []util.Chars, begin int) []Token {
prefixLength := begin prefixLength := begin
for idx, token := range tokens { for idx, token := range tokens {
// Need to define a new local variable instead of the reused token to take // NOTE: &tokens[idx] instead of &tokens
// the pointer to it ret[idx] = Token{&tokens[idx], int32(prefixLength), int32(token.TrimLength())}
ret[idx] = Token{token, prefixLength, token.TrimLength()}
prefixLength += token.Length() prefixLength += token.Length()
} }
return ret return ret
@ -178,12 +177,13 @@ func Transform(tokens []Token, withNth []Range) []Token {
transTokens := make([]Token, len(withNth)) transTokens := make([]Token, len(withNth))
numTokens := len(tokens) numTokens := len(tokens)
for idx, r := range withNth { for idx, r := range withNth {
parts := []util.Chars{} parts := []*util.Chars{}
minIdx := 0 minIdx := 0
if r.begin == r.end { if r.begin == r.end {
idx := r.begin idx := r.begin
if idx == rangeEllipsis { if idx == rangeEllipsis {
parts = append(parts, util.RunesToChars(joinTokens(tokens))) chars := util.RunesToChars(joinTokens(tokens))
parts = append(parts, &chars)
} else { } else {
if idx < 0 { if idx < 0 {
idx += numTokens + 1 idx += numTokens + 1
@ -227,7 +227,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
case 0: case 0:
merged = util.RunesToChars([]rune{}) merged = util.RunesToChars([]rune{})
case 1: case 1:
merged = parts[0] merged = *parts[0]
default: default:
runes := []rune{} runes := []rune{}
for _, part := range parts { for _, part := range parts {
@ -236,13 +236,13 @@ func Transform(tokens []Token, withNth []Range) []Token {
merged = util.RunesToChars(runes) merged = util.RunesToChars(runes)
} }
var prefixLength int var prefixLength int32
if minIdx < numTokens { if minIdx < numTokens {
prefixLength = tokens[minIdx].prefixLength prefixLength = tokens[minIdx].prefixLength
} else { } else {
prefixLength = 0 prefixLength = 0
} }
transTokens[idx] = Token{merged, prefixLength, merged.TrimLength()} transTokens[idx] = Token{&merged, prefixLength, int32(merged.TrimLength())}
} }
return transTokens return transTokens
} }

View File

@ -4,6 +4,7 @@ package util
import "C" import "C"
import ( import (
"math"
"os" "os"
"os/exec" "os/exec"
"time" "time"
@ -63,6 +64,15 @@ func Constrain(val int, min int, max int) int {
return val return val
} }
func AsUint16(val int) uint16 {
if val > math.MaxUint16 {
return math.MaxUint16
} else if val < 0 {
return 0
}
return uint16(val)
}
// DurWithin limits the given time.Duration with the upper and lower bounds // DurWithin limits the given time.Duration with the upper and lower bounds
func DurWithin( func DurWithin(
val time.Duration, min time.Duration, max time.Duration) time.Duration { val time.Duration, min time.Duration, max time.Duration) time.Duration {