package fzf import ( "math" "math/rand" "slices" "sort" "testing" "github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/util" ) func withIndex(i *Item, index int) *Item { (*i).text.Index = int32(index) return i } func TestOffsetSort(t *testing.T) { offsets := []Offset{ {3, 5}, {2, 7}, {1, 3}, {2, 9}} slices.SortFunc(offsets, compareOffsets) 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) Result { return Result{ points: [4]uint16{vals[0], vals[1], vals[2], vals[3]}, item: &Item{text: util.Chars{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{byScore, byLength} str := []rune("foo") item1 := buildResult( withIndex(&Item{text: util.RunesToChars(str)}, 1), []Offset{}, 2) if item1.points[3] != math.MaxUint16-2 || // Bonus item1.points[2] != 3 || // Length item1.points[1] != 0 || // Unused item1.points[0] != 0 || // Unused item1.item.Index() != 1 { t.Error(item1) } // Only differ in index item2 := buildResult(&Item{text: util.RunesToChars(str)}, []Offset{}, 2) 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( withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 3) item4 := buildResult( withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 4) item5 := buildResult( withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 5) item6 := buildResult( withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 6) items = []Result{item1, item2, item3, item4, item5, item6} sort.Sort(ByRelevance(items)) if !(items[0] == item6 && items[1] == item5 && items[2] == item4 && items[3] == item3 && items[4] == item2 && items[5] == item1) { t.Error(items, item1, item2, item3, item4, item5, item6) } } func TestChunkTiebreak(t *testing.T) { // FIXME global sortCriteria = []criterion{byScore, byChunk} score := 100 test := func(input string, offset Offset, chunk string) { item := buildResult(withIndex(&Item{text: util.RunesToChars([]rune(input))}, 1), []Offset{offset}, score) if !(item.points[3] == math.MaxUint16-uint16(score) && item.points[2] == uint16(len(chunk))) { t.Error(item.points) } } test("hello foobar goodbye", Offset{8, 9}, "foobar") test("hello foobar goodbye", Offset{7, 18}, "foobar goodbye") test("hello foobar goodbye", Offset{0, 1}, "hello") test("hello foobar goodbye", Offset{5, 7}, "hello foobar") // TBD } func TestColorOffset(t *testing.T) { // ------------ 20 ---- -- ---- // ++++++++ ++++++++++ // --++++++++-- --++++++++++--- offsets := []Offset{{5, 15}, {10, 12}, {25, 35}} item := Result{ item: &Item{ colors: &[]ansiOffset{ {[2]int32{0, 20}, ansiState{1, 5, -1, 0, -1, nil}}, {[2]int32{22, 27}, ansiState{2, 6, -1, tui.Bold, -1, nil}}, {[2]int32{30, 32}, ansiState{3, 7, -1, 0, -1, nil}}, {[2]int32{33, 40}, ansiState{4, 8, -1, tui.Bold, -1, nil}}}}} colBase := tui.NewColorPair(89, 189, tui.AttrUndefined) colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined) colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, 0, false) assert := func(idx int, b int32, e int32, c tui.ColorPair) { o := colors[idx] if o.offset[0] != b || o.offset[1] != e || o.color != c { t.Error(o, b, e, c) } } // [{[0 5] {1 5 0}} {[5 15] {99 199 0}} {[15 20] {1 5 0}} // {[22 25] {2 6 1}} {[25 27] {99 199 1}} {[27 30] {99 199 0}} // {[30 32] {99 199 0}} {[32 33] {99 199 0}} {[33 35] {99 199 1}} // {[35 40] {4 8 1}}] assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined)) assert(1, 5, 15, colMatch) assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined)) assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold)) assert(4, 25, 27, colMatch.WithAttr(tui.Bold)) assert(5, 27, 30, colMatch) assert(6, 30, 32, colMatch) assert(7, 32, 33, colMatch) // TODO: Should we merge consecutive blocks? assert(8, 33, 35, colMatch.WithAttr(tui.Bold)) assert(9, 35, 40, tui.NewColorPair(4, 8, tui.Bold)) colRegular := tui.NewColorPair(-1, -1, tui.AttrUndefined) colUnderline := tui.NewColorPair(-1, -1, tui.Underline) nthOffsets := []Offset{{37, 39}, {42, 45}} for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} { colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, 0, false) // [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}} // {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}} // {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}} // {[35 37] {4 8 1}} {[37 39] {4 8 x|1}} {[39 40] {4 8 x|1}}] assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined)) assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline)) assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined)) assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold)) assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline)) assert(5, 27, 30, colUnderline) assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline)) assert(7, 32, 33, colUnderline) assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline)) assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold)) expected := tui.Bold | attr if attr == tui.AttrRegular { expected = tui.Bold } assert(10, 37, 39, tui.NewColorPair(4, 8, expected)) assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold)) } // Test nthOverlay: simulates nth:regular with current-fg:underline // The overlay (underline) should survive even though nth:regular clears attrs. // Precedence: fg < nth < current-fg colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, tui.AttrRegular, tui.Underline, false) // nth regions should have Underline (from overlay), not cleared by AttrRegular // Non-nth regions keep colBase attrs (AttrUndefined) assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined)) assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline)) assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined)) assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold)) assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline)) assert(5, 27, 30, colUnderline) assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline)) assert(7, 32, 33, colUnderline) assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline)) assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold)) // nth region within ANSI bold: AttrRegular clears, overlay adds Underline back assert(10, 37, 39, tui.NewColorPair(4, 8, tui.Bold|tui.Underline)) assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold)) // Test nthOverlay with additive attrs: nth:strikethrough with selected-fg:bold colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, tui.StrikeThrough, tui.Bold, false) // Non-nth entries unchanged from overlay=0 case assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined)) assert(5, 27, 30, colUnderline) // match only, no nth assert(7, 32, 33, colUnderline) // match only, no nth // nth region within ANSI bold: StrikeThrough|Bold merged with ANSI Bold assert(10, 37, 39, tui.NewColorPair(4, 8, tui.Bold|tui.StrikeThrough)) } func TestRadixSortResults(t *testing.T) { sortCriteria = []criterion{byScore, byLength} rng := rand.New(rand.NewSource(42)) for _, n := range []int{128, 256, 500, 1000} { for _, tac := range []bool{false, true} { // Build items with random points and indices items := make([]*Item, n) for i := range items { items[i] = &Item{text: util.Chars{Index: int32(i)}} } results := make([]Result, n) for i := range results { results[i] = Result{ item: items[i], points: [4]uint16{ uint16(rng.Intn(256)), uint16(rng.Intn(256)), uint16(rng.Intn(256)), uint16(rng.Intn(256)), }, } } // Make some duplicates to test stability for i := 0; i < n/4; i++ { j := rng.Intn(n) k := rng.Intn(n) results[j].points = results[k].points } // Copy for reference sort expected := make([]Result, n) copy(expected, results) if tac { sort.Sort(ByRelevanceTac(expected)) } else { sort.Sort(ByRelevance(expected)) } // Radix sort var scratch []Result scratch = radixSortResults(results, tac, scratch) for i := range results { if results[i] != expected[i] { t.Errorf("n=%d tac=%v: mismatch at index %d: got item %d, want item %d", n, tac, i, results[i].item.Index(), expected[i].item.Index()) break } } } } }