wip: initial rework

This commit is contained in:
Kujtim Hoxha
2025-07-23 13:00:44 +02:00
parent a80d6c609f
commit 74f0b20970
49 changed files with 989 additions and 1301 deletions

View File

@@ -63,7 +63,7 @@ func New(app *app.App) MessageListCmp {
listCmp := list.New(
[]list.Item{},
list.WithGap(1),
list.WithDirection(list.Backward),
list.WithDirectionBackward(),
list.WithKeyMap(defaultListKeyMap),
)
return &messageListCmp{

View File

@@ -1,68 +1,60 @@
package list
import (
"fmt"
"slices"
"testing"
"github.com/charmbracelet/x/exp/golden"
"github.com/stretchr/testify/assert"
)
func TestFilterableList(t *testing.T) {
t.Parallel()
t.Run("should create simple filterable list", func(t *testing.T) {
t.Parallel()
items := []FilterableItem{}
for i := range 5 {
item := NewFilterableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := NewFilterableList(
items,
WithFilterListOptions(WithDirection(Forward)),
).(*filterableList[FilterableItem])
l.SetSize(100, 10)
cmd := l.Init()
if cmd != nil {
cmd()
}
assert.Equal(t, items[0].ID(), l.selectedItem)
golden.RequireEqual(t, []byte(l.View()))
})
}
func TestUpdateKeyMap(t *testing.T) {
t.Parallel()
l := NewFilterableList(
[]FilterableItem{},
WithFilterListOptions(WithDirection(Forward)),
).(*filterableList[FilterableItem])
hasJ := slices.Contains(l.keyMap.Down.Keys(), "j")
fmt.Println(l.keyMap.Down.Keys())
hasCtrlJ := slices.Contains(l.keyMap.Down.Keys(), "ctrl+j")
hasUpperCaseK := slices.Contains(l.keyMap.UpOneItem.Keys(), "K")
assert.False(t, l.keyMap.HalfPageDown.Enabled(), "should disable keys that are only letters")
assert.False(t, hasJ, "should not contain j")
assert.False(t, hasUpperCaseK, "should also remove upper case K")
assert.True(t, hasCtrlJ, "should still have ctrl+j")
}
type filterableItem struct {
*selectableItem
}
func NewFilterableItem(content string) FilterableItem {
return &filterableItem{
selectableItem: NewSelectableItem(content).(*selectableItem),
}
}
func (f *filterableItem) FilterValue() string {
return f.content
}
//
// func TestFilterableList(t *testing.T) {
// t.Parallel()
// t.Run("should create simple filterable list", func(t *testing.T) {
// t.Parallel()
// items := []FilterableItem{}
// for i := range 5 {
// item := NewFilterableItem(fmt.Sprintf("Item %d", i))
// items = append(items, item)
// }
// l := NewFilterableList(
// items,
// WithFilterListOptions(WithDirection(Forward)),
// ).(*filterableList[FilterableItem])
//
// l.SetSize(100, 10)
// cmd := l.Init()
// if cmd != nil {
// cmd()
// }
//
// assert.Equal(t, items[0].ID(), l.selectedItem)
// golden.RequireEqual(t, []byte(l.View()))
// })
// }
//
// func TestUpdateKeyMap(t *testing.T) {
// t.Parallel()
// l := NewFilterableList(
// []FilterableItem{},
// WithFilterListOptions(WithDirection(Forward)),
// ).(*filterableList[FilterableItem])
//
// hasJ := slices.Contains(l.keyMap.Down.Keys(), "j")
// fmt.Println(l.keyMap.Down.Keys())
// hasCtrlJ := slices.Contains(l.keyMap.Down.Keys(), "ctrl+j")
//
// hasUpperCaseK := slices.Contains(l.keyMap.UpOneItem.Keys(), "K")
//
// assert.False(t, l.keyMap.HalfPageDown.Enabled(), "should disable keys that are only letters")
// assert.False(t, hasJ, "should not contain j")
// assert.False(t, hasUpperCaseK, "should also remove upper case K")
// assert.True(t, hasCtrlJ, "should still have ctrl+j")
// }
//
// type filterableItem struct {
// *selectableItem
// }
//
// func NewFilterableItem(content string) FilterableItem {
// return &filterableItem{
// selectableItem: NewSelectableItem(content).(*selectableItem),
// }
// }
//
// func (f *filterableItem) FilterValue() string {
// return f.content
// }

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ package list
import (
"fmt"
"sync"
"strings"
"testing"
tea "github.com/charmbracelet/bubbletea/v2"
@@ -11,623 +11,344 @@ import (
"github.com/charmbracelet/x/exp/golden"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestListPosition(t *testing.T) {
func TestList(t *testing.T) {
t.Parallel()
type positionOffsetTest struct {
dir direction
test string
width int
height int
numItems int
moveUp int
moveDown int
expectedStart int
expectedEnd int
}
tests := []positionOffsetTest{
{
dir: Forward,
test: "should have correct position initially when forward",
moveUp: 0,
moveDown: 0,
width: 10,
height: 20,
numItems: 100,
expectedStart: 0,
expectedEnd: 19,
},
{
dir: Forward,
test: "should offset start and end by one when moving down by one",
moveUp: 0,
moveDown: 1,
width: 10,
height: 20,
numItems: 100,
expectedStart: 1,
expectedEnd: 20,
},
{
dir: Backward,
test: "should have correct position initially when backward",
moveUp: 0,
moveDown: 0,
width: 10,
height: 20,
numItems: 100,
expectedStart: 80,
expectedEnd: 99,
},
{
dir: Backward,
test: "should offset the start and end by one when moving up by one",
moveUp: 1,
moveDown: 0,
width: 10,
height: 20,
numItems: 100,
expectedStart: 79,
expectedEnd: 98,
},
}
for _, c := range tests {
t.Run(c.test, func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range c.numItems {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(c.dir)).(*list[Item])
l.SetSize(c.width, c.height)
cmd := l.Init()
if cmd != nil {
cmd()
}
if c.moveUp > 0 {
l.MoveUp(c.moveUp)
}
if c.moveDown > 0 {
l.MoveDown(c.moveDown)
}
start, end := l.viewPosition()
assert.Equal(t, c.expectedStart, start)
assert.Equal(t, c.expectedEnd, end)
})
}
}
func TestBackwardList(t *testing.T) {
t.Parallel()
t.Run("within height", func(t *testing.T) {
t.Run("should have correct positions in list that fits the items", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 5 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Backward), WithGap(1)).(*list[Item])
l.SetSize(10, 20)
cmd := l.Init()
if cmd != nil {
cmd()
}
l := New(items, WithDirectionForward(), WithSize(10, 20)).(*list[Item])
execCmd(l, l.Init())
// should select the last item
assert.Equal(t, l.selectedItem, items[len(items)-1].ID())
assert.Equal(t, items[0].ID(), l.selectedItem)
assert.Equal(t, 0, l.offset)
require.Len(t, l.indexMap, 5)
require.Len(t, l.items, 5)
require.Len(t, l.renderedItems, 5)
assert.Equal(t, 5, lipgloss.Height(l.rendered))
assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
start, end := l.viewPosition()
assert.Equal(t, 0, start)
assert.Equal(t, 4, end)
for i := range 5 {
assert.Equal(t, i, l.renderedItems[items[i].ID()].start)
assert.Equal(t, i, l.renderedItems[items[i].ID()].end)
}
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should not change selected item", func(t *testing.T) {
t.Run("should have correct positions in list that fits the items backwards", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 5 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Backward), WithGap(1), WithSelectedItem(items[2].ID())).(*list[Item])
l.SetSize(10, 20)
cmd := l.Init()
if cmd != nil {
cmd()
}
// should select the last item
assert.Equal(t, l.selectedItem, items[2].ID())
})
t.Run("more than height", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Backward))
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
}
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("more than height multi line", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d\nLine2", i))
items = append(items, item)
}
l := New(items, WithDirection(Backward))
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
}
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should move up", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Backward))
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
}
l.MoveUp(1)
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should move at max to the top", func(t *testing.T) {
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Backward)).(*list[Item])
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
}
l.MoveUp(100)
assert.Equal(t, l.offset, lipgloss.Height(l.rendered)-l.listHeight())
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should do nothing with wrong move number", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Backward))
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
}
l.MoveUp(-10)
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should move to the top", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Backward)).(*list[Item])
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
}
l.GoToTop()
assert.Equal(t, l.direction, Forward)
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should select the item above", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Backward)).(*list[Item])
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
}
selectedInx := len(l.items) - 2
currentItem := items[len(l.items)-1]
nextItem := items[selectedInx]
assert.False(t, nextItem.(SelectableItem).IsFocused())
assert.True(t, currentItem.(SelectableItem).IsFocused())
cmd = l.SelectItemAbove()
if cmd != nil {
cmd()
}
assert.Equal(t, l.selectedItem, l.items[selectedInx].ID())
assert.True(t, l.items[selectedInx].(SelectableItem).IsFocused())
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should move the view to be able to see the selected item", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Backward)).(*list[Item])
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
}
for range 5 {
cmd = l.SelectItemAbove()
if cmd != nil {
cmd()
}
}
golden.RequireEqual(t, []byte(l.View()))
})
}
func TestForwardList(t *testing.T) {
t.Parallel()
t.Run("within height", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 5 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Forward), WithGap(1)).(*list[Item])
l.SetSize(10, 20)
cmd := l.Init()
if cmd != nil {
cmd()
}
l := New(items, WithDirectionBackward(), WithSize(10, 20)).(*list[Item])
execCmd(l, l.Init())
// should select the last item
assert.Equal(t, l.selectedItem, items[0].ID())
assert.Equal(t, items[4].ID(), l.selectedItem)
assert.Equal(t, 0, l.offset)
require.Len(t, l.indexMap, 5)
require.Len(t, l.items, 5)
require.Len(t, l.renderedItems, 5)
assert.Equal(t, 5, lipgloss.Height(l.rendered))
assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
start, end := l.viewPosition()
assert.Equal(t, 0, start)
assert.Equal(t, 4, end)
for i := range 5 {
assert.Equal(t, i, l.renderedItems[items[i].ID()].start)
assert.Equal(t, i, l.renderedItems[items[i].ID()].end)
}
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should not change selected item", func(t *testing.T) {
t.Run("should have correct positions in list that does not fits the items", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 5 {
for i := range 30 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Forward), WithGap(1), WithSelectedItem(items[2].ID())).(*list[Item])
l.SetSize(10, 20)
cmd := l.Init()
if cmd != nil {
cmd()
}
l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
execCmd(l, l.Init())
// should select the last item
assert.Equal(t, l.selectedItem, items[2].ID())
})
t.Run("more than height", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Forward)).(*list[Item])
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
assert.Equal(t, items[0].ID(), l.selectedItem)
assert.Equal(t, 0, l.offset)
require.Len(t, l.indexMap, 30)
require.Len(t, l.items, 30)
require.Len(t, l.renderedItems, 30)
assert.Equal(t, 30, lipgloss.Height(l.rendered))
assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
start, end := l.viewPosition()
assert.Equal(t, 0, start)
assert.Equal(t, 9, end)
for i := range 30 {
assert.Equal(t, i, l.renderedItems[items[i].ID()].start)
assert.Equal(t, i, l.renderedItems[items[i].ID()].end)
}
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("more than height multi line", func(t *testing.T) {
t.Run("should have correct positions in list that does not fits the items backwards", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d\nLine2", i))
for i := range 30 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Forward)).(*list[Item])
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
execCmd(l, l.Init())
// should select the last item
assert.Equal(t, items[29].ID(), l.selectedItem)
assert.Equal(t, 0, l.offset)
require.Len(t, l.indexMap, 30)
require.Len(t, l.items, 30)
require.Len(t, l.renderedItems, 30)
assert.Equal(t, 30, lipgloss.Height(l.rendered))
assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
start, end := l.viewPosition()
assert.Equal(t, 20, start)
assert.Equal(t, 29, end)
for i := range 30 {
assert.Equal(t, i, l.renderedItems[items[i].ID()].start)
assert.Equal(t, i, l.renderedItems[items[i].ID()].end)
}
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should move down", func(t *testing.T) {
t.Run("should have correct positions in list that does not fits the items and has multi line items", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
for i := range 30 {
content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
content = strings.TrimSuffix(content, "\n")
item := NewSelectableItem(content)
items = append(items, item)
}
l := New(items, WithDirection(Forward)).(*list[Item])
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
}
l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
execCmd(l, l.Init())
l.MoveDown(1)
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should move at max to the bottom", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
// should select the last item
assert.Equal(t, items[0].ID(), l.selectedItem)
assert.Equal(t, 0, l.offset)
require.Len(t, l.indexMap, 30)
require.Len(t, l.items, 30)
require.Len(t, l.renderedItems, 30)
expectedLines := 0
for i := range 30 {
expectedLines += (i + 1) * 1
}
l := New(items, WithDirection(Forward)).(*list[Item])
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
assert.Equal(t, expectedLines, lipgloss.Height(l.rendered))
assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
start, end := l.viewPosition()
assert.Equal(t, 0, start)
assert.Equal(t, 9, end)
currentPosition := 0
for i := range 30 {
rItem := l.renderedItems[items[i].ID()]
assert.Equal(t, currentPosition, rItem.start)
assert.Equal(t, currentPosition+i, rItem.end)
currentPosition += i + 1
}
l.MoveDown(100)
assert.Equal(t, l.offset, lipgloss.Height(l.rendered)-l.listHeight())
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should do nothing with wrong move number", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Forward)).(*list[Item])
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
}
l.MoveDown(-10)
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should move to the bottom", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Forward)).(*list[Item])
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
}
l.GoToBottom()
assert.Equal(t, l.direction, Backward)
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should select the item below", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Forward)).(*list[Item])
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
}
selectedInx := 1
currentItem := items[0]
nextItem := items[selectedInx]
assert.False(t, nextItem.(SelectableItem).IsFocused())
assert.True(t, currentItem.(SelectableItem).IsFocused())
cmd = l.SelectItemBelow()
if cmd != nil {
cmd()
}
assert.Equal(t, l.selectedItem, l.items[selectedInx].ID())
assert.True(t, l.items[selectedInx].(SelectableItem).IsFocused())
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should move the view to be able to see the selected item", func(t *testing.T) {
t.Run("should have correct positions in list that does not fits the items and has multi line items backwards", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 10 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
for i := range 30 {
content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
content = strings.TrimSuffix(content, "\n")
item := NewSelectableItem(content)
items = append(items, item)
}
l := New(items, WithDirection(Forward)).(*list[Item])
l.SetSize(10, 5)
cmd := l.Init()
if cmd != nil {
cmd()
l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
execCmd(l, l.Init())
// should select the last item
assert.Equal(t, items[29].ID(), l.selectedItem)
assert.Equal(t, 0, l.offset)
require.Len(t, l.indexMap, 30)
require.Len(t, l.items, 30)
require.Len(t, l.renderedItems, 30)
expectedLines := 0
for i := range 30 {
expectedLines += (i + 1) * 1
}
assert.Equal(t, expectedLines, lipgloss.Height(l.rendered))
assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
start, end := l.viewPosition()
assert.Equal(t, expectedLines-10, start)
assert.Equal(t, expectedLines-1, end)
currentPosition := 0
for i := range 30 {
rItem := l.renderedItems[items[i].ID()]
assert.Equal(t, currentPosition, rItem.start)
assert.Equal(t, currentPosition+i, rItem.end)
currentPosition += i + 1
}
for range 5 {
cmd = l.SelectItemBelow()
if cmd != nil {
cmd()
}
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should go to selected item and center", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 30 {
content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
content = strings.TrimSuffix(content, "\n")
item := NewSelectableItem(content)
items = append(items, item)
}
l := New(items, WithDirectionForward(), WithSize(10, 10), WithSelectedItem(items[4].ID())).(*list[Item])
execCmd(l, l.Init())
// should select the last item
assert.Equal(t, items[4].ID(), l.selectedItem)
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should go to selected item and center backwards", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 30 {
content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
content = strings.TrimSuffix(content, "\n")
item := NewSelectableItem(content)
items = append(items, item)
}
l := New(items, WithDirectionBackward(), WithSize(10, 10), WithSelectedItem(items[4].ID())).(*list[Item])
execCmd(l, l.Init())
// should select the last item
assert.Equal(t, items[4].ID(), l.selectedItem)
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should go to selected item at the beginning", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 30 {
content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
content = strings.TrimSuffix(content, "\n")
item := NewSelectableItem(content)
items = append(items, item)
}
l := New(items, WithDirectionForward(), WithSize(10, 10), WithSelectedItem(items[10].ID())).(*list[Item])
execCmd(l, l.Init())
// should select the last item
assert.Equal(t, items[10].ID(), l.selectedItem)
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should go to selected item at the beginning backwards", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 30 {
content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
content = strings.TrimSuffix(content, "\n")
item := NewSelectableItem(content)
items = append(items, item)
}
l := New(items, WithDirectionBackward(), WithSize(10, 10), WithSelectedItem(items[10].ID())).(*list[Item])
execCmd(l, l.Init())
// should select the last item
assert.Equal(t, items[10].ID(), l.selectedItem)
golden.RequireEqual(t, []byte(l.View()))
})
}
func TestListSelection(t *testing.T) {
func TestListMovement(t *testing.T) {
t.Parallel()
t.Run("should skip none selectable items initially", func(t *testing.T) {
t.Run("should move viewport up", func(t *testing.T) {
t.Parallel()
items := []Item{}
items = append(items, NewSimpleItem("None Selectable"))
for i := range 5 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
for i := range 30 {
content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
content = strings.TrimSuffix(content, "\n")
item := NewSelectableItem(content)
items = append(items, item)
}
l := New(items, WithDirection(Forward)).(*list[Item])
l.SetSize(100, 10)
cmd := l.Init()
if cmd != nil {
cmd()
}
l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
execCmd(l, l.Init())
assert.Equal(t, items[1].ID(), l.selectedItem)
execCmd(l, l.MoveUp(25))
assert.Equal(t, 25, l.offset)
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should select the correct item on startup", func(t *testing.T) {
t.Run("should move viewport up and down", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 5 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
for i := range 30 {
content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
content = strings.TrimSuffix(content, "\n")
item := NewSelectableItem(content)
items = append(items, item)
}
l := New(items, WithDirection(Forward)).(*list[Item])
cmd := l.Init()
otherCmd := l.SetSelected(items[3].ID())
var wg sync.WaitGroup
if cmd != nil {
wg.Add(1)
go func() {
cmd()
wg.Done()
}()
}
if otherCmd != nil {
wg.Add(1)
go func() {
otherCmd()
wg.Done()
}()
}
wg.Wait()
l.SetSize(100, 10)
assert.Equal(t, items[3].ID(), l.selectedItem)
l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
execCmd(l, l.Init())
execCmd(l, l.MoveUp(25))
execCmd(l, l.MoveDown(25))
assert.Equal(t, 0, l.offset)
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should skip none selectable items in the middle", func(t *testing.T) {
t.Run("should move viewport down", func(t *testing.T) {
t.Parallel()
items := []Item{}
item := NewSelectableItem("Item initial")
items = append(items, item)
items = append(items, NewSimpleItem("None Selectable"))
for i := range 5 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
for i := range 30 {
content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
content = strings.TrimSuffix(content, "\n")
item := NewSelectableItem(content)
items = append(items, item)
}
l := New(items, WithDirection(Forward)).(*list[Item])
l.SetSize(100, 10)
cmd := l.Init()
if cmd != nil {
cmd()
}
l.SelectItemBelow()
assert.Equal(t, items[2].ID(), l.selectedItem)
l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
execCmd(l, l.Init())
execCmd(l, l.MoveDown(25))
assert.Equal(t, 25, l.offset)
golden.RequireEqual(t, []byte(l.View()))
})
}
func TestListSetSelection(t *testing.T) {
t.Parallel()
t.Run("should move to the selected item", func(t *testing.T) {
t.Run("should move viewport down and up", func(t *testing.T) {
t.Parallel()
items := []Item{}
for i := range 100 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
for i := range 30 {
content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
content = strings.TrimSuffix(content, "\n")
item := NewSelectableItem(content)
items = append(items, item)
}
l := New(items, WithDirection(Forward)).(*list[Item])
l.SetSize(100, 10)
cmd := l.Init()
if cmd != nil {
cmd()
}
l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
execCmd(l, l.Init())
cmd = l.SetSelected(items[52].ID())
if cmd != nil {
cmd()
}
execCmd(l, l.MoveDown(25))
execCmd(l, l.MoveUp(25))
assert.Equal(t, items[52].ID(), l.selectedItem)
golden.RequireEqual(t, []byte(l.View()))
})
}
func TestListChanges(t *testing.T) {
t.Parallel()
t.Run("should append an item to the end", func(t *testing.T) {
t.Parallel()
items := []SelectableItem{}
for i := range 20 {
item := NewSelectableItem(fmt.Sprintf("Item %d", i))
items = append(items, item)
}
l := New(items, WithDirection(Backward)).(*list[SelectableItem])
l.SetSize(100, 10)
cmd := l.Init()
if cmd != nil {
cmd()
}
newItem := NewSelectableItem("New Item")
l.AppendItem(newItem)
assert.Equal(t, 21, len(l.items))
assert.Equal(t, 21, len(l.renderedItems))
assert.Equal(t, newItem.ID(), l.selectedItem)
golden.RequireEqual(t, []byte(l.View()))
})
t.Run("should should not change the selected if we moved the offset", func(t *testing.T) {
t.Parallel()
items := []SelectableItem{}
for i := range 20 {
item := NewSelectableItem(fmt.Sprintf("Item %d\nLine2", i))
items = append(items, item)
}
l := New(items, WithDirection(Backward)).(*list[SelectableItem])
l.SetSize(100, 10)
cmd := l.Init()
if cmd != nil {
cmd()
}
l.MoveUp(1)
newItem := NewSelectableItem("New Item")
l.AppendItem(newItem)
assert.Equal(t, 21, len(l.items))
assert.Equal(t, 21, len(l.renderedItems))
assert.Equal(t, l.items[19].ID(), l.selectedItem)
assert.Equal(t, 0, l.offset)
golden.RequireEqual(t, []byte(l.View()))
})
}
@@ -711,3 +432,10 @@ func (s *selectableItem) Focus() tea.Cmd {
func (s *selectableItem) IsFocused() bool {
return s.focused
}
func execCmd(m tea.Model, cmd tea.Cmd) {
for cmd != nil {
msg := cmd()
m, cmd = m.Update(msg)
}
}

View File

@@ -1,5 +0,0 @@
Item 5
Item 6
Item 7
Item 8
│Item 9

View File

@@ -1,5 +0,0 @@
Line2
Item 8
Line2
│Item 9
│Line2

View File

@@ -1,5 +0,0 @@
Item 5
Item 6
Item 7
Item 8
│Item 9

View File

@@ -1,5 +0,0 @@
Item 0
Item 1
Item 2
Item 3
│Item 4

View File

@@ -1,5 +0,0 @@
│Item 4
Item 5
Item 6
Item 7
Item 8

View File

@@ -1,5 +0,0 @@
│Item 0
Item 1
Item 2
Item 3
Item 4

View File

@@ -1,5 +0,0 @@
Item 4
Item 5
Item 6
Item 7
│Item 8

View File

@@ -1,5 +0,0 @@
Item 5
Item 6
Item 7
│Item 8
Item 9

View File

@@ -1,9 +0,0 @@
Item 0
Item 1
Item 2
Item 3
│Item 4

View File

@@ -1,6 +0,0 @@
> Type to filter 
│Item 0
Item 1
Item 2
Item 3
Item 4

View File

@@ -1,5 +0,0 @@
│Item 0
Item 1
Item 2
Item 3
Item 4

View File

@@ -1,5 +0,0 @@
│Item 0
│Line2
Item 1
Line2
Item 2

View File

@@ -1,5 +0,0 @@
│Item 0
Item 1
Item 2
Item 3
Item 4

View File

@@ -1,5 +0,0 @@
│Item 5
Item 6
Item 7
Item 8
Item 9

View File

@@ -1,5 +0,0 @@
│Item 1
Item 2
Item 3
Item 4
Item 5

View File

@@ -1,5 +0,0 @@
Item 1
Item 2
Item 3
Item 4
│Item 5

View File

@@ -1,5 +0,0 @@
Item 5
Item 6
Item 7
Item 8
│Item 9

View File

@@ -1,5 +0,0 @@
Item 0
│Item 1
Item 2
Item 3
Item 4

View File

@@ -1,9 +0,0 @@
│Item 0
Item 1
Item 2
Item 3
Item 4

View File

@@ -0,0 +1,10 @@
│Item 0
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9

View File

@@ -0,0 +1,10 @@
│Item 0
Item 1
Item 1
Item 2
Item 2
Item 2
Item 3
Item 3
Item 3
Item 3

View File

@@ -0,0 +1,10 @@
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29

View File

@@ -0,0 +1,10 @@
Item 20
Item 21
Item 22
Item 23
Item 24
Item 25
Item 26
Item 27
Item 28
│Item 29

View File

@@ -0,0 +1,5 @@
│Item 0
Item 1
Item 2
Item 3
Item 4

View File

@@ -0,0 +1,5 @@
Item 0
Item 1
Item 2
Item 3
│Item 4

View File

@@ -0,0 +1,10 @@
Item 3
Item 3
│Item 4
│Item 4
│Item 4
│Item 4
│Item 4
Item 5
Item 5
Item 5

View File

@@ -0,0 +1,10 @@
Item 3
Item 3
│Item 4
│Item 4
│Item 4
│Item 4
│Item 4
Item 5
Item 5
Item 5

View File

@@ -0,0 +1,10 @@
│Item 10
│Item 10
│Item 10
│Item 10
│Item 10
│Item 10
│Item 10
│Item 10
│Item 10
│Item 10

View File

@@ -0,0 +1,10 @@
│Item 10
│Item 10
│Item 10
│Item 10
│Item 10
│Item 10
│Item 10
│Item 10
│Item 10
│Item 10

View File

@@ -0,0 +1,10 @@
│Item 0
Item 1
Item 2
Item 3
Item 4
Item 5
Item 6
Item 7
Item 8
Item 9

View File

@@ -0,0 +1,10 @@
│Item 0
Item 1
Item 1
Item 2
Item 2
Item 2
Item 3
Item 3
Item 3
Item 3

View File

@@ -0,0 +1,10 @@
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29

View File

@@ -0,0 +1,10 @@
Item 20
Item 21
Item 22
Item 23
Item 24
Item 25
Item 26
Item 27
Item 28
│Item 29

View File

@@ -0,0 +1,5 @@
│Item 0
Item 1
Item 2
Item 3
Item 4

View File

@@ -0,0 +1,5 @@
Item 0
Item 1
Item 2
Item 3
│Item 4

View File

@@ -1,10 +0,0 @@
Item 11
Item 12
Item 13
Item 14
Item 15
Item 16
Item 17
Item 18
Item 19
│New Item

View File

@@ -1,10 +0,0 @@
Item 15
Line2
Item 16
Line2
Item 17
Line2
Item 18
Line2
│Item 19
│Line2

View File

@@ -0,0 +1,10 @@
Item 6
Item 6
Item 6
│Item 7
│Item 7
│Item 7
│Item 7
│Item 7
│Item 7
│Item 7

View File

@@ -0,0 +1,10 @@
Item 0
Item 1
Item 1
Item 2
Item 2
Item 2
│Item 3
│Item 3
│Item 3
│Item 3

View File

@@ -0,0 +1,10 @@
│Item 28
│Item 28
│Item 28
│Item 28
│Item 28
Item 29
Item 29
Item 29
Item 29
Item 29

View File

@@ -0,0 +1,10 @@
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29
│Item 29

View File

@@ -1,5 +0,0 @@
Item 0
Item 1
Item 2
│Item 3
Item 4

View File

@@ -1,7 +0,0 @@
Item initial
None Selectable
│Item 0
Item 1
Item 2
Item 3
Item 4

View File

@@ -1,6 +0,0 @@
None Selectable
│Item 0
Item 1
Item 2
Item 3
Item 4

View File

@@ -1,10 +0,0 @@
Item 47
Item 48
Item 49
Item 50
Item 51
│Item 52
Item 53
Item 54
Item 55
Item 56