diff --git a/src/algo/algo.go b/src/algo/algo.go index 8825a05..00265c6 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -24,12 +24,12 @@ func indexAt(index int, max int, forward bool) int { // Result conatins the results of running a match function. type Result struct { - Start int32 - End int32 + Start int + End int // Items are basically sorted by the lengths of matched substrings. // But we slightly adjust the score with bonus for better results. - Bonus int32 + Bonus int } type charClass int @@ -42,8 +42,8 @@ const ( charNumber ) -func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int) int32 { - var bonus int32 +func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int, eidx int) int { + var bonus int pidx := 0 lenPattern := len(pattern) consecutive := false @@ -63,7 +63,7 @@ func evaluateBonus(caseSensitive bool, text util.Chars, pattern []rune, sidx int class = charNonWord } - var point int32 + var point int if prevClass == charNonWord && class != charNonWord { // Word boundary point = 2 @@ -181,7 +181,7 @@ func FuzzyMatch(caseSensitive bool, forward bool, text util.Chars, pattern []run sidx, eidx = lenRunes-eidx, lenRunes-sidx } - return Result{int32(sidx), int32(eidx), + return Result{sidx, eidx, evaluateBonus(caseSensitive, text, pattern, sidx, eidx)} } return Result{-1, -1, 0} @@ -228,7 +228,7 @@ func ExactMatchNaive(caseSensitive bool, forward bool, text util.Chars, pattern sidx = lenRunes - (index + 1) eidx = lenRunes - (index - lenPattern + 1) } - return Result{int32(sidx), int32(eidx), + return Result{sidx, eidx, evaluateBonus(caseSensitive, text, pattern, sidx, eidx)} } } else { @@ -255,7 +255,7 @@ func PrefixMatch(caseSensitive bool, forward bool, text util.Chars, pattern []ru } } lenPattern := len(pattern) - return Result{0, int32(lenPattern), + return Result{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) sidx := trimmedLen - lenPattern eidx := trimmedLen - return Result{int32(sidx), int32(eidx), + return Result{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) } if runesStr == string(pattern) { - return Result{0, int32(len(pattern)), 0} + return Result{0, len(pattern), 0} } return Result{-1, -1, 0} } diff --git a/src/algo/algo_test.go b/src/algo/algo_test.go index d6a5d48..7034dce 100644 --- a/src/algo/algo_test.go +++ b/src/algo/algo_test.go @@ -7,7 +7,7 @@ import ( "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 { pattern = strings.ToLower(pattern) } diff --git a/src/ansi.go b/src/ansi.go index 56831ee..0a52567 100644 --- a/src/ansi.go +++ b/src/ansi.go @@ -36,7 +36,7 @@ func init() { 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 output bytes.Buffer @@ -84,7 +84,10 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo if proc != nil { 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 { diff --git a/src/ansi_test.go b/src/ansi_test.go index 31803f3..a80e98a 100644 --- a/src/ansi_test.go +++ b/src/ansi_test.go @@ -16,7 +16,7 @@ func TestExtractColor(t *testing.T) { src := "hello world" var state *ansiState 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) state = newState if output != "hello world" { @@ -26,127 +26,127 @@ func TestExtractColor(t *testing.T) { assertion(ansiOffsets, state) } - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) > 0 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if offsets != nil { t.Fail() } }) state = nil src = "\x1b[0mhello world" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) > 0 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if offsets != nil { t.Fail() } }) state = nil src = "\x1b[1mhello world" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } - assert(offsets[0], 0, 11, -1, -1, true) + assert((*offsets)[0], 0, 11, -1, -1, true) }) state = nil src = "\x1b[1mhello \x1b[mworld" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } - assert(offsets[0], 0, 6, -1, -1, true) + assert((*offsets)[0], 0, 6, -1, -1, true) }) state = nil src = "\x1b[1mhello \x1b[Kworld" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } - assert(offsets[0], 0, 11, -1, -1, true) + assert((*offsets)[0], 0, 11, -1, -1, true) }) state = nil src = "hello \x1b[34;45;1mworld" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } - assert(offsets[0], 6, 11, 4, 5, true) + assert((*offsets)[0], 6, 11, 4, 5, true) }) state = nil src = "hello \x1b[34;45;1mwor\x1b[34;45;1mld" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } - assert(offsets[0], 6, 11, 4, 5, true) + assert((*offsets)[0], 6, 11, 4, 5, true) }) state = nil src = "hello \x1b[34;45;1mwor\x1b[0mld" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } - assert(offsets[0], 6, 9, 4, 5, true) + assert((*offsets)[0], 6, 9, 4, 5, true) }) state = nil src = "hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 3 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 3 { t.Fail() } - assert(offsets[0], 6, 8, 4, 233, true) - assert(offsets[1], 8, 9, 161, 233, true) - assert(offsets[2], 10, 11, 161, -1, false) + assert((*offsets)[0], 6, 8, 4, 233, true) + assert((*offsets)[1], 8, 9, 161, 233, true) + assert((*offsets)[2], 10, 11, 161, -1, false) }) // {38,48};5;{38,48} state = nil 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) { - if len(offsets) != 2 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 2 { t.Fail() } - assert(offsets[0], 6, 9, 38, 48, true) - assert(offsets[1], 9, 10, 48, 38, true) + assert((*offsets)[0], 6, 9, 38, 48, true) + assert((*offsets)[1], 9, 10, 48, 38, true) }) src = "hello \x1b[32;1mworld" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } if state.fg != 2 || state.bg != -1 || !state.bold { t.Fail() } - assert(offsets[0], 6, 11, 2, -1, true) + assert((*offsets)[0], 6, 11, 2, -1, true) }) src = "hello world" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 1 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 1 { t.Fail() } if state.fg != 2 || state.bg != -1 || !state.bold { 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" - check(func(offsets []ansiOffset, state *ansiState) { - if len(offsets) != 2 { + check(func(offsets *[]ansiOffset, state *ansiState) { + if len(*offsets) != 2 { t.Fail() } if state.fg != 200 || state.bg != 100 || state.bold { t.Fail() } - assert(offsets[0], 0, 6, 2, -1, true) - assert(offsets[1], 6, 11, 200, 100, false) + assert((*offsets)[0], 0, 6, 2, -1, true) + assert((*offsets)[1], 6, 11, 200, 100, false) }) } diff --git a/src/cache.go b/src/cache.go index d2ec00b..baf88dd 100644 --- a/src/cache.go +++ b/src/cache.go @@ -3,7 +3,7 @@ package fzf import "sync" // 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 type ChunkCache struct { @@ -17,7 +17,7 @@ func NewChunkCache() ChunkCache { } // 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 { return } @@ -34,7 +34,7 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Item) { } // 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() { return nil, false } diff --git a/src/cache_test.go b/src/cache_test.go index 05f904c..8703fc4 100644 --- a/src/cache_test.go +++ b/src/cache_test.go @@ -7,8 +7,8 @@ func TestChunkCache(t *testing.T) { chunk2 := make(Chunk, chunkSize) chunk1p := &Chunk{} chunk2p := &chunk2 - items1 := []*Item{&Item{}} - items2 := []*Item{&Item{}, &Item{}} + items1 := []*Result{&Result{}} + items2 := []*Result{&Result{}, &Result{}} cache.Add(chunk1p, "foo", items1) cache.Add(chunk2p, "foo", items1) cache.Add(chunk2p, "bar", items2) diff --git a/src/chunklist_test.go b/src/chunklist_test.go index 2523675..594daeb 100644 --- a/src/chunklist_test.go +++ b/src/chunklist_test.go @@ -12,7 +12,7 @@ func TestChunkList(t *testing.T) { sortCriteria = []criterion{byMatchLen, byLength} 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 @@ -41,11 +41,8 @@ func TestChunkList(t *testing.T) { if len(*chunk1) != 2 { t.Error("Snapshot should contain only two items") } - last := func(arr [5]int32) int32 { - return arr[len(arr)-1] - } - if (*chunk1)[0].text.ToString() != "hello" || last((*chunk1)[0].rank) != 0 || - (*chunk1)[1].text.ToString() != "world" || last((*chunk1)[1].rank) != 2 { + if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].index != 0 || + (*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].index != 2 { t.Error("Invalid data") } if chunk1.IsFull() { diff --git a/src/core.go b/src/core.go index 76d30d1..290c0af 100644 --- a/src/core.go +++ b/src/core.go @@ -56,16 +56,16 @@ func Run(opts *Options) { eventBox := util.NewEventBox() // ANSI code processor - ansiProcessor := func(data []byte) (util.Chars, []ansiOffset) { + ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) { return util.ToChars(data), nil } - ansiProcessorRunes := func(data []rune) (util.Chars, []ansiOffset) { + ansiProcessorRunes := func(data []rune) (util.Chars, *[]ansiOffset) { return util.RunesToChars(data), nil } if opts.Ansi { if opts.Theme != nil { 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) state = newState return util.RunesToChars([]rune(trimmed)), offsets @@ -73,12 +73,12 @@ func Run(opts *Options) { } else { // When color is disabled but ansi option is given, // 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) 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))) } } @@ -95,14 +95,13 @@ func Run(opts *Options) { } chars, colors := ansiProcessor(data) return &Item{ + index: int32(index), text: chars, - colors: colors, - rank: buildEmptyRank(int32(index))} + colors: colors} }) } else { chunkList = NewChunkList(func(data []byte, index int) *Item { - chars := util.ToChars(data) - tokens := Tokenize(chars, opts.Delimiter) + tokens := Tokenize(util.ToChars(data), opts.Delimiter) trans := Transform(tokens, opts.WithNth) if len(header) < opts.HeaderLines { header = append(header, string(joinTokens(trans))) @@ -111,10 +110,9 @@ func Run(opts *Options) { } textRunes := joinTokens(trans) item := Item{ - text: util.RunesToChars(textRunes), + index: int32(index), origText: &data, - colors: nil, - rank: buildEmptyRank(int32(index))} + colors: nil} trimmed, colors := ansiProcessorRunes(textRunes) item.text = trimmed @@ -163,7 +161,7 @@ func Run(opts *Options) { reader := Reader{ func(runes []byte) bool { item := chunkList.trans(runes, 0) - if item != nil && pattern.MatchItem(item) { + if item != nil && pattern.MatchItem(item) != nil { fmt.Println(item.text.ToString()) found = true } @@ -179,7 +177,7 @@ func Run(opts *Options) { chunks: snapshot, pattern: pattern}) 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 } } @@ -259,7 +257,7 @@ func Run(opts *Options) { fmt.Println() } 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 { os.Exit(exitOk) diff --git a/src/item.go b/src/item.go index 584dfd8..4e60faf 100644 --- a/src/item.go +++ b/src/item.go @@ -1,295 +1,39 @@ package fzf import ( - "math" - - "github.com/junegunn/fzf/src/curses" "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 type Item struct { + index int32 text util.Chars origText *[]byte + colors *[]ansiOffset 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 func (item *Item) Index() int32 { - return item.rank[4] + return item.index } -// Rank calculates rank of the Item -func (item *Item) Rank(cache bool) [5]int32 { - if cache && isRankValid(item.rank) { - return item.rank +// Colors returns ansiOffsets of the Item +func (item *Item) Colors() []ansiOffset { + if item.colors == nil { + return []ansiOffset{} } - matchlen := 0 - 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 + return *item.colors } // AsString returns the original 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 stripAnsi { trimmed, _, _ := extractColor(string(*item.origText), nil, nil) - return &trimmed + return trimmed } - orig := string(*item.origText) - return &orig + return string(*item.origText) } - str := 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 + return item.text.ToString() } diff --git a/src/item_test.go b/src/item_test.go index 36a436d..1efb5f1 100644 --- a/src/item_test.go +++ b/src/item_test.go @@ -1,109 +1,23 @@ package fzf import ( - "math" - "sort" "testing" - "github.com/junegunn/fzf/src/curses" "github.com/junegunn/fzf/src/util" ) -func TestOffsetSort(t *testing.T) { - offsets := []Offset{ - Offset{3, 5}, Offset{2, 7}, - Offset{1, 3}, Offset{2, 9}} - sort.Sort(ByOrder(offsets)) - - if offsets[0][0] != 1 || offsets[0][1] != 3 || - offsets[1][0] != 2 || offsets[1][1] != 7 || - offsets[2][0] != 2 || offsets[2][1] != 9 || - offsets[3][0] != 3 || offsets[3][1] != 5 { - t.Error("Invalid order:", offsets) +func TestStringPtr(t *testing.T) { + orig := []byte("\x1b[34mfoo") + text := []byte("\x1b[34mbar") + item := Item{origText: &orig, text: util.ToChars(text)} + if item.AsString(true) != "foo" || item.AsString(false) != string(orig) { + t.Fail() + } + if item.AsString(true) != "foo" { + t.Fail() + } + 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) -} diff --git a/src/matcher.go b/src/matcher.go index 4c00db7..8cd9a9f 100644 --- a/src/matcher.go +++ b/src/matcher.go @@ -128,7 +128,7 @@ func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk { type partialResult struct { index int - matches []*Item + matches []*Result } func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { @@ -155,15 +155,21 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) { waitGroup.Add(1) go func(idx int, chunks []*Chunk) { defer func() { waitGroup.Done() }() - sliceMatches := []*Item{} - for _, chunk := range chunks { + count := 0 + allMatches := make([][]*Result, len(chunks)) + for idx, chunk := range chunks { matches := request.pattern.Match(chunk) - sliceMatches = append(sliceMatches, matches...) + allMatches[idx] = matches + count += len(matches) if cancelled.Get() { return } countChan <- len(matches) } + sliceMatches := make([]*Result, 0, count) + for _, matches := range allMatches { + sliceMatches = append(sliceMatches, matches...) + } if m.sort { if m.tac { 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 { partialResult := <-resultChan partialResults[partialResult.index] = partialResult.matches diff --git a/src/merger.go b/src/merger.go index 0d3fb80..3879ab7 100644 --- a/src/merger.go +++ b/src/merger.go @@ -3,13 +3,13 @@ package fzf import "fmt" // 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 // a single, globally-sorted list type Merger struct { - lists [][]*Item - merged []*Item + lists [][]*Result + merged []*Result chunks *[]*Chunk cursors []int sorted bool @@ -33,10 +33,10 @@ func PassMerger(chunks *[]*Chunk, tac bool) *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{ lists: lists, - merged: []*Item{}, + merged: []*Result{}, chunks: nil, cursors: make([]int, len(lists)), sorted: sorted, @@ -55,14 +55,14 @@ func (mg *Merger) Length() int { return mg.count } -// Get returns the pointer to the Item object indexed by the given integer -func (mg *Merger) Get(idx int) *Item { +// Get returns the pointer to the Result object indexed by the given integer +func (mg *Merger) Get(idx int) *Result { if mg.chunks != nil { if mg.tac { idx = mg.count - idx - 1 } chunk := (*mg.chunks)[idx/chunkSize] - return (*chunk)[idx%chunkSize] + return &Result{item: (*chunk)[idx%chunkSize]} } if mg.sorted { @@ -86,9 +86,9 @@ func (mg *Merger) cacheable() bool { 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++ { - minRank := buildEmptyRank(0) + minRank := minRank() minIdx := -1 for listIdx, list := range mg.lists { cursor := mg.cursors[listIdx] @@ -97,7 +97,7 @@ func (mg *Merger) mergedGet(idx int) *Item { continue } if cursor >= 0 { - rank := list[cursor].Rank(false) + rank := list[cursor].rank if minIdx < 0 || compareRanks(rank, minRank, mg.tac) { minRank = rank minIdx = listIdx diff --git a/src/merger_test.go b/src/merger_test.go index f62f975..d50470f 100644 --- a/src/merger_test.go +++ b/src/merger_test.go @@ -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()) offsets := make([]Offset, rand.Int()%3) for idx := range offsets { @@ -23,10 +23,10 @@ func randItem() *Item { eidx := sidx + int32(rand.Uint32()%20) offsets[idx] = Offset{sidx, eidx} } - return &Item{ - text: util.RunesToChars([]rune(str)), - rank: buildEmptyRank(rand.Int31()), - offsets: offsets} + return &Result{ + item: &Item{text: util.RunesToChars([]rune(str))}, + offsets: offsets, + rank: rank{index: rand.Int31()}} } func TestEmptyMerger(t *testing.T) { @@ -36,23 +36,23 @@ func TestEmptyMerger(t *testing.T) { assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list") } -func buildLists(partiallySorted bool) ([][]*Item, []*Item) { +func buildLists(partiallySorted bool) ([][]*Result, []*Result) { numLists := 4 - lists := make([][]*Item, numLists) + lists := make([][]*Result, numLists) cnt := 0 for i := 0; i < numLists; i++ { - numItems := rand.Int() % 20 - cnt += numItems - lists[i] = make([]*Item, numItems) - for j := 0; j < numItems; j++ { - item := randItem() + numResults := rand.Int() % 20 + cnt += numResults + lists[i] = make([]*Result, numResults) + for j := 0; j < numResults; j++ { + item := randResult() lists[i][j] = item } if partiallySorted { sort.Sort(ByRelevance(lists[i])) } } - items := []*Item{} + items := []*Result{} for _, list := range lists { items = append(items, list...) } diff --git a/src/options.go b/src/options.go index 31ad497..a19ff6f 100644 --- a/src/options.go +++ b/src/options.go @@ -95,6 +95,7 @@ type criterion int const ( byMatchLen criterion = iota + byBonus byLength byBegin byEnd @@ -178,7 +179,7 @@ func defaultOptions() *Options { Delimiter: Delimiter{}, Sort: 1000, Tac: false, - Criteria: []criterion{byMatchLen, byLength}, + Criteria: []criterion{byMatchLen, byBonus, byLength}, Multi: false, Ansi: false, Mouse: true, @@ -406,7 +407,7 @@ func parseKeyChords(str string, message string) map[int]string { } func parseTiebreak(str string) []criterion { - criteria := []criterion{byMatchLen} + criteria := []criterion{byMatchLen, byBonus} hasIndex := false hasLength := false hasBegin := false diff --git a/src/pattern.go b/src/pattern.go index 0bf9af3..4c35a12 100644 --- a/src/pattern.go +++ b/src/pattern.go @@ -2,7 +2,6 @@ package fzf import ( "regexp" - "sort" "strings" "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 -func (p *Pattern) Match(chunk *Chunk) []*Item { - space := chunk - +func (p *Pattern) Match(chunk *Chunk) []*Result { // ChunkCache: Exact match cacheKey := p.CacheKey() 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: for idx := 1; idx < len(cacheKey); idx++ { // [---------| ] | [ |---------] @@ -256,14 +254,13 @@ Loop: suffix := cacheKey[idx:] for _, substr := range [2]*string{&prefix, &suffix} { if cached, found := _cache.Find(chunk, *substr); found { - cachedChunk := Chunk(cached) - space = &cachedChunk + space = cached break Loop } } } - matches := p.matchChunk(space) + matches := p.matchChunk(chunk, space) if p.cacheable { _cache.Add(chunk, cacheKey, matches) @@ -271,20 +268,19 @@ Loop: return matches } -func (p *Pattern) matchChunk(chunk *Chunk) []*Item { - matches := []*Item{} - if !p.extended { +func (p *Pattern) matchChunk(chunk *Chunk, space []*Result) []*Result { + matches := []*Result{} + + if space == nil { for _, item := range *chunk { - offset, bonus := p.basicMatch(item) - if sidx := offset[0]; sidx >= 0 { - matches = append(matches, - dupItem(item, []Offset{offset}, bonus)) + if match := p.MatchItem(item); match != nil { + matches = append(matches, match) } } } else { - for _, item := range *chunk { - if offsets, bonus := p.extendedMatch(item); len(offsets) == len(p.termSets) { - matches = append(matches, dupItem(item, offsets, bonus)) + for _, result := range space { + if match := p.MatchItem(result.item); match != nil { + matches = append(matches, match) } } } @@ -292,29 +288,21 @@ func (p *Pattern) matchChunk(chunk *Chunk) []*Item { } // MatchItem returns true if the Item is a match -func (p *Pattern) MatchItem(item *Item) bool { - if !p.extended { - offset, _ := p.basicMatch(item) - sidx := offset[0] - return sidx >= 0 +func (p *Pattern) MatchItem(item *Item) *Result { + if p.extended { + if offsets, bonus, trimLen := p.extendedMatch(item); len(offsets) == len(p.termSets) { + return buildResult(item, offsets, bonus, trimLen) + } + return nil } - offsets, _ := p.extendedMatch(item) - return len(offsets) == len(p.termSets) + offset, bonus, trimLen := p.basicMatch(item) + if sidx := offset[0]; sidx >= 0 { + return buildResult(item, []Offset{offset}, bonus, trimLen) + } + return nil } -func dupItem(item *Item, offsets []Offset, bonus int32) *Item { - 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) { +func (p *Pattern) basicMatch(item *Item) (Offset, int, int) { input := p.prepareInput(item) if p.fuzzy { 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) } -func (p *Pattern) extendedMatch(item *Item) ([]Offset, int32) { +func (p *Pattern) extendedMatch(item *Item) ([]Offset, int, int) { input := p.prepareInput(item) offsets := []Offset{} - var totalBonus int32 + var totalBonus int + var totalTrimLen int for _, termSet := range p.termSets { - var offset *Offset - var bonus int32 + var offset Offset + var bonus int + var trimLen int + matched := false for _, term := range termSet { 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 term.inv { continue } - offset, bonus = &off, pen + offset, bonus, trimLen = off, pen, tLen + matched = true break } else if term.inv { - offset, bonus = &Offset{0, 0, 0}, 0 + offset, bonus, trimLen = Offset{0, 0}, 0, 0 + matched = true continue } } - if offset != nil { - offsets = append(offsets, *offset) + if matched { + offsets = append(offsets, offset) totalBonus += bonus + totalTrimLen += trimLen } } - return offsets, totalBonus + return offsets, totalBonus, totalTrimLen } func (p *Pattern) prepareInput(item *Item) []Token { @@ -357,26 +351,24 @@ func (p *Pattern) prepareInput(item *Item) []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) ret = Transform(tokens, p.nth) - } else { - ret = []Token{Token{text: item.text, prefixLength: 0, trimLength: item.text.TrimLength()}} } item.transformed = ret return ret } 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 { - prefixLength := int32(part.prefixLength) - if res := pfun(caseSensitive, forward, part.text, pattern); res.Start >= 0 { - sidx := res.Start + prefixLength - eidx := res.End + prefixLength - return Offset{sidx, eidx, int32(part.trimLength)}, res.Bonus + if res := pfun(caseSensitive, forward, *part.text, pattern); res.Start >= 0 { + sidx := int32(res.Start) + part.prefixLength + eidx := int32(res.End) + part.prefixLength + return Offset{sidx, eidx}, res.Bonus, int(part.trimLength) } } - // TODO: math.MaxUint16 - return Offset{-1, -1, -1}, 0.0 + return Offset{-1, -1}, 0, -1 } diff --git a/src/pattern_test.go b/src/pattern_test.go index 26f9284..3069110 100644 --- a/src/pattern_test.go +++ b/src/pattern_test.go @@ -83,7 +83,7 @@ func TestEqual(t *testing.T) { clearPatternCache() 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( pattern.caseSensitive, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text) if res.Start != sidxExpected || res.End != eidxExpected { @@ -133,10 +133,10 @@ func TestOrigTextAndTransformed(t *testing.T) { transformed: trans}, } pattern.extended = extended - matches := pattern.matchChunk(&chunk) - if matches[0].text.ToString() != "junegunn" || string(*matches[0].origText) != "junegunn.choi" || + matches := pattern.matchChunk(&chunk, nil) // No cache + 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 || - !reflect.DeepEqual(matches[0].transformed, trans) { + !reflect.DeepEqual(matches[0].item.transformed, trans) { t.Error("Invalid match result", matches) } } diff --git a/src/terminal.go b/src/terminal.go index b6b6156..f7bbccf 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -77,7 +77,7 @@ type Terminal struct { type selectedItem struct { at time.Time - text *string + text string } type byTimeOrder []selectedItem @@ -357,7 +357,7 @@ func (t *Terminal) output() bool { } } else { for _, sel := range t.sortSelected() { - fmt.Println(*sel.text) + fmt.Println(sel.text) } } return found @@ -565,11 +565,10 @@ func (t *Terminal) printHeader() { state = newState item := &Item{ text: util.RunesToChars([]rune(trimmed)), - colors: colors, - rank: buildEmptyRank(0)} + colors: colors} 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()] label := " " if t.jumping != jumpDisabled { @@ -609,14 +609,14 @@ func (t *Terminal) printItem(item *Item, i int, current bool) { } else { 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 { if selected { t.window.CPrint(C.ColSelected, true, ">") } else { 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 } -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 - for _, offset := range item.offsets { + for _, offset := range result.offsets { maxe = util.Max(maxe, int(offset[1])) } // Overflow text := make([]rune, item.text.Length()) copy(text, item.text.ToRunes()) - offsets := item.colorOffsets(col2, bold, current) + offsets := result.colorOffsets(col2, bold, current) maxWidth := t.window.Width - 3 maxe = util.Constrain(maxe+util.Min(maxWidth/2-2, t.hscrollOff), 0, len(text)) if overflow(text, maxWidth) { @@ -866,7 +867,7 @@ func (t *Terminal) isPreviewEnabled() bool { } 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 @@ -1037,13 +1038,13 @@ func (t *Terminal) Loop() { } selectItem := func(item *Item) bool { 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 false } toggleY := func(y int) { - item := t.merger.Get(y) + item := t.merger.Get(y).item if !selectItem(item) { delete(t.selected, item.Index()) } @@ -1068,14 +1069,14 @@ func (t *Terminal) Loop() { case actIgnore: case actExecute: 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))) } case actExecuteMulti: if len(t.selected) > 0 { sels := make([]string, len(t.selected)) for i, sel := range t.sortSelected() { - sels[i] = quoteEntry(*sel.text) + sels[i] = quoteEntry(sel.text) } t.executeCommand(t.execmap[mapkey], strings.Join(sels, " ")) } else { @@ -1137,7 +1138,7 @@ func (t *Terminal) Loop() { case actSelectAll: if t.multi { for i := 0; i < t.merger.Length(); i++ { - item := t.merger.Get(i) + item := t.merger.Get(i).item selectItem(item) } req(reqList, reqInfo) diff --git a/src/tokenizer.go b/src/tokenizer.go index b6aa0c6..ed87362 100644 --- a/src/tokenizer.go +++ b/src/tokenizer.go @@ -18,9 +18,9 @@ type Range struct { // Token contains the tokenized part of the strings and its prefix length type Token struct { - text util.Chars - prefixLength int - trimLength int + text *util.Chars + prefixLength int32 + trimLength int32 } // Delimiter for tokenizing the input @@ -80,9 +80,8 @@ func withPrefixLengths(tokens []util.Chars, begin int) []Token { prefixLength := begin for idx, token := range tokens { - // Need to define a new local variable instead of the reused token to take - // the pointer to it - ret[idx] = Token{token, prefixLength, token.TrimLength()} + // NOTE: &tokens[idx] instead of &tokens + ret[idx] = Token{&tokens[idx], int32(prefixLength), int32(token.TrimLength())} prefixLength += token.Length() } return ret @@ -178,12 +177,13 @@ func Transform(tokens []Token, withNth []Range) []Token { transTokens := make([]Token, len(withNth)) numTokens := len(tokens) for idx, r := range withNth { - parts := []util.Chars{} + parts := []*util.Chars{} minIdx := 0 if r.begin == r.end { idx := r.begin if idx == rangeEllipsis { - parts = append(parts, util.RunesToChars(joinTokens(tokens))) + chars := util.RunesToChars(joinTokens(tokens)) + parts = append(parts, &chars) } else { if idx < 0 { idx += numTokens + 1 @@ -227,7 +227,7 @@ func Transform(tokens []Token, withNth []Range) []Token { case 0: merged = util.RunesToChars([]rune{}) case 1: - merged = parts[0] + merged = *parts[0] default: runes := []rune{} for _, part := range parts { @@ -236,13 +236,13 @@ func Transform(tokens []Token, withNth []Range) []Token { merged = util.RunesToChars(runes) } - var prefixLength int + var prefixLength int32 if minIdx < numTokens { prefixLength = tokens[minIdx].prefixLength } else { prefixLength = 0 } - transTokens[idx] = Token{merged, prefixLength, merged.TrimLength()} + transTokens[idx] = Token{&merged, prefixLength, int32(merged.TrimLength())} } return transTokens } diff --git a/src/util/util.go b/src/util/util.go index 113fee9..2ebd0f6 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -4,6 +4,7 @@ package util import "C" import ( + "math" "os" "os/exec" "time" @@ -63,6 +64,15 @@ func Constrain(val int, min int, max int) int { 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 func DurWithin( val time.Duration, min time.Duration, max time.Duration) time.Duration {