From 608c41620755041d6fc216ae43de8b21d56c969c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Fri, 19 Aug 2016 03:27:42 +0900 Subject: [PATCH] Add missing sources --- src/result.go | 259 +++++++++++++++++++++++++++++++++++++++++++++ src/result_test.go | 114 ++++++++++++++++++++ 2 files changed, 373 insertions(+) create mode 100644 src/result.go create mode 100644 src/result_test.go diff --git a/src/result.go b/src/result.go new file mode 100644 index 0000000..c295e7a --- /dev/null +++ b/src/result.go @@ -0,0 +1,259 @@ +package fzf + +import ( + "math" + "sort" + + "github.com/junegunn/fzf/src/curses" + "github.com/junegunn/fzf/src/util" +) + +// Offset holds two 32-bit integers denoting the offsets of a matched substring +type Offset [2]int32 + +type colorOffset struct { + offset [2]int32 + color int + bold bool + index int32 +} + +type rank struct { + index int32 + // byMatchLen, byBonus, ... + points [5]uint16 +} + +type Result struct { + item *Item + offsets []Offset + rank rank +} + +func buildResult(item *Item, offsets []Offset, bonus int, trimLen int) *Result { + if len(offsets) > 1 { + sort.Sort(ByOrder(offsets)) + } + + result := Result{item: item, offsets: offsets, rank: rank{index: item.index}} + + matchlen := 0 + prevEnd := 0 + minBegin := math.MaxInt32 + numChars := item.text.Length() + for _, offset := range offsets { + begin := int(offset[0]) + end := int(offset[1]) + if prevEnd > begin { + begin = prevEnd + } + if end > prevEnd { + prevEnd = end + } + if end > begin { + if begin < minBegin { + minBegin = begin + } + matchlen += end - begin + } + } + + for idx, criterion := range sortCriteria { + var val uint16 + switch criterion { + case byMatchLen: + if matchlen == 0 { + val = math.MaxUint16 + } else { + val = util.AsUint16(matchlen) + } + case byBonus: + // Higher is better + val = math.MaxUint16 - util.AsUint16(bonus) + case byLength: + // If offsets is empty, trimLen will be 0, but we don't care + val = util.AsUint16(trimLen) + case byBegin: + // We can't just look at item.offsets[0][0] because it can be an inverse term + whitePrefixLen := 0 + for idx := 0; idx < numChars; idx++ { + r := item.text.Get(idx) + whitePrefixLen = idx + if idx == minBegin || r != ' ' && r != '\t' { + break + } + } + val = util.AsUint16(minBegin - whitePrefixLen) + case byEnd: + if prevEnd > 0 { + val = util.AsUint16(1 + numChars - prevEnd) + } else { + // Empty offsets due to inverse terms. + val = 1 + } + } + result.rank.points[idx] = val + } + + return &result +} + +// Sort criteria to use. Never changes once fzf is started. +var sortCriteria []criterion + +// Index returns ordinal index of the Item +func (result *Result) Index() int32 { + return result.item.index +} + +func minRank() rank { + return rank{index: 0, points: [5]uint16{0, math.MaxUint16, 0, 0, 0}} +} + +func (result *Result) colorOffsets(color int, bold bool, current bool) []colorOffset { + itemColors := result.item.Colors() + + if len(itemColors) == 0 { + var offsets []colorOffset + for _, off := range result.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 result.offsets { + if off[1] > maxCol { + maxCol = off[1] + } + } + for _, ansi := range itemColors { + if ansi.offset[1] > maxCol { + maxCol = ansi.offset[1] + } + } + cols := make([]int, maxCol) + + for colorIndex, ansi := range itemColors { + for i := ansi.offset[0]; i < ansi.offset[1]; i++ { + cols[i] = colorIndex + 1 // XXX + } + } + + for _, off := range result.offsets { + for i := off[0]; i < off[1]; i++ { + cols[i] = -1 + } + } + + // sort.Sort(ByOrder(offsets)) + + // Merge offsets + // ------------ ---- -- ---- + // ++++++++ ++++++++++ + // --++++++++-- --++++++++++--- + curr := 0 + start := 0 + var colors []colorOffset + add := func(idx int) { + if curr != 0 && idx > start { + if curr == -1 { + colors = append(colors, colorOffset{ + offset: [2]int32{int32(start), int32(idx)}, color: color, bold: bold}) + } else { + ansi := itemColors[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 + } + } + colors = append(colors, 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 colors +} + +// 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 []*Result + +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 { + return compareRanks((*a[i]).rank, (*a[j]).rank, false) +} + +// ByRelevanceTac is for sorting Items +type ByRelevanceTac []*Result + +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 { + return compareRanks((*a[i]).rank, (*a[j]).rank, true) +} + +func compareRanks(irank rank, jrank rank, tac bool) bool { + for idx := 0; idx < 5; idx++ { + left := irank.points[idx] + right := jrank.points[idx] + if left < right { + return true + } else if left > right { + return false + } + } + return (irank.index <= jrank.index) != tac +} diff --git a/src/result_test.go b/src/result_test.go new file mode 100644 index 0000000..c8478fd --- /dev/null +++ b/src/result_test.go @@ -0,0 +1,114 @@ +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 TestRankComparison(t *testing.T) { + rank := func(vals ...uint16) rank { + return rank{ + points: [5]uint16{vals[0], 0, vals[1], vals[2], vals[3]}, + index: int32(vals[4])} + } + if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) || + !compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) || + !compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), false) || + !compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) { + t.Error("Invalid order") + } + + if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), true) || + !compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) || + !compareRanks(rank(1, 2, 0, 0, 3), rank(1, 3, 0, 0, 2), true) || + !compareRanks(rank(0, 0, 0, 0, 0), rank(0, 0, 0, 0, 0), false) { + t.Error("Invalid order (tac)") + } +} + +// Match length, string length, index +func TestResultRank(t *testing.T) { + // FIXME global + sortCriteria = []criterion{byMatchLen, byBonus, byLength} + + strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")} + item1 := buildResult(&Item{text: util.RunesToChars(strs[0]), index: 1}, []Offset{}, 2, 3) + if item1.rank.points[0] != math.MaxUint16 || item1.rank.points[1] != math.MaxUint16-2 || item1.rank.points[2] != 3 || item1.item.index != 1 { + t.Error(item1.rank) + } + // Only differ in index + item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2, 3) + + items := []*Result{item1, item2} + sort.Sort(ByRelevance(items)) + if items[0] != item2 || items[1] != item1 { + t.Error(items) + } + + items = []*Result{item2, item1, item1, item2} + sort.Sort(ByRelevance(items)) + if items[0] != item2 || items[1] != item2 || + items[2] != item1 || items[3] != item1 { + t.Error(items, item1, item1.item.index, item2, item2.item.index) + } + + // Sort by relevance + item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 0, 0) + item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 0, 0) + item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 0, 0) + item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 0, 0) + items = []*Result{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 := Result{ + offsets: []Offset{Offset{5, 15}, Offset{25, 35}}, + item: &Item{ + 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) +}