cspell.json 🔗
@@ -1 +1 @@
Kujtim Hoxha created
cspell.json | 0
internal/csync/slices.go | 127 +++++++++++++++
internal/csync/slices_test.go | 209 +++++++++++++++++++++++++
internal/tui/exp/list/filterable.go | 2
internal/tui/exp/list/filterable_group.go | 2
internal/tui/exp/list/grouped.go | 5
internal/tui/exp/list/list.go | 140 ++++++++++-----
internal/tui/exp/list/list_test.go | 12
8 files changed, 437 insertions(+), 60 deletions(-)
@@ -1 +1 @@
@@ -2,6 +2,7 @@ package csync
import (
"iter"
+ "slices"
"sync"
)
@@ -34,3 +35,129 @@ func (s *LazySlice[K]) Seq() iter.Seq[K] {
}
}
}
+
+// Slice is a thread-safe slice implementation that provides concurrent access.
+type Slice[T any] struct {
+ inner []T
+ mu sync.RWMutex
+}
+
+// NewSlice creates a new thread-safe slice.
+func NewSlice[T any]() *Slice[T] {
+ return &Slice[T]{
+ inner: make([]T, 0),
+ }
+}
+
+// NewSliceFrom creates a new thread-safe slice from an existing slice.
+func NewSliceFrom[T any](s []T) *Slice[T] {
+ inner := make([]T, len(s))
+ copy(inner, s)
+ return &Slice[T]{
+ inner: inner,
+ }
+}
+
+// Append adds an element to the end of the slice.
+func (s *Slice[T]) Append(item T) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.inner = append(s.inner, item)
+}
+
+// Prepend adds an element to the beginning of the slice.
+func (s *Slice[T]) Prepend(item T) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.inner = append([]T{item}, s.inner...)
+}
+
+// Delete removes the element at the specified index.
+func (s *Slice[T]) Delete(index int) bool {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if index < 0 || index >= len(s.inner) {
+ return false
+ }
+ s.inner = slices.Delete(s.inner, index, index+1)
+ return true
+}
+
+// Get returns the element at the specified index.
+func (s *Slice[T]) Get(index int) (T, bool) {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ var zero T
+ if index < 0 || index >= len(s.inner) {
+ return zero, false
+ }
+ return s.inner[index], true
+}
+
+// Set updates the element at the specified index.
+func (s *Slice[T]) Set(index int, item T) bool {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if index < 0 || index >= len(s.inner) {
+ return false
+ }
+ s.inner[index] = item
+ return true
+}
+
+// Len returns the number of elements in the slice.
+func (s *Slice[T]) Len() int {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ return len(s.inner)
+}
+
+// Slice returns a copy of the underlying slice.
+func (s *Slice[T]) Slice() []T {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ result := make([]T, len(s.inner))
+ copy(result, s.inner)
+ return result
+}
+
+// SetSlice replaces the entire slice with a new one.
+func (s *Slice[T]) SetSlice(items []T) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.inner = make([]T, len(items))
+ copy(s.inner, items)
+}
+
+// Clear removes all elements from the slice.
+func (s *Slice[T]) Clear() {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.inner = s.inner[:0]
+}
+
+// Seq returns an iterator that yields elements from the slice.
+func (s *Slice[T]) Seq() iter.Seq[T] {
+ // Take a snapshot to avoid holding the lock during iteration
+ items := s.Slice()
+ return func(yield func(T) bool) {
+ for _, v := range items {
+ if !yield(v) {
+ return
+ }
+ }
+ }
+}
+
+// SeqWithIndex returns an iterator that yields index-value pairs from the slice.
+func (s *Slice[T]) SeqWithIndex() iter.Seq2[int, T] {
+ // Take a snapshot to avoid holding the lock during iteration
+ items := s.Slice()
+ return func(yield func(int, T) bool) {
+ for i, v := range items {
+ if !yield(i, v) {
+ return
+ }
+ }
+ }
+}
@@ -1,11 +1,13 @@
package csync
import (
+ "sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestLazySlice_Seq(t *testing.T) {
@@ -85,3 +87,210 @@ func TestLazySlice_EarlyBreak(t *testing.T) {
assert.Equal(t, []string{"a", "b"}, result)
}
+
+func TestSlice(t *testing.T) {
+ t.Run("NewSlice", func(t *testing.T) {
+ s := NewSlice[int]()
+ assert.Equal(t, 0, s.Len())
+ })
+
+ t.Run("NewSliceFrom", func(t *testing.T) {
+ original := []int{1, 2, 3}
+ s := NewSliceFrom(original)
+ assert.Equal(t, 3, s.Len())
+
+ // Verify it's a copy, not a reference
+ original[0] = 999
+ val, ok := s.Get(0)
+ require.True(t, ok)
+ assert.Equal(t, 1, val)
+ })
+
+ t.Run("Append", func(t *testing.T) {
+ s := NewSlice[string]()
+ s.Append("hello")
+ s.Append("world")
+
+ assert.Equal(t, 2, s.Len())
+ val, ok := s.Get(0)
+ require.True(t, ok)
+ assert.Equal(t, "hello", val)
+
+ val, ok = s.Get(1)
+ require.True(t, ok)
+ assert.Equal(t, "world", val)
+ })
+
+ t.Run("Prepend", func(t *testing.T) {
+ s := NewSlice[string]()
+ s.Append("world")
+ s.Prepend("hello")
+
+ assert.Equal(t, 2, s.Len())
+ val, ok := s.Get(0)
+ require.True(t, ok)
+ assert.Equal(t, "hello", val)
+
+ val, ok = s.Get(1)
+ require.True(t, ok)
+ assert.Equal(t, "world", val)
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ s := NewSliceFrom([]int{1, 2, 3, 4, 5})
+
+ // Delete middle element
+ ok := s.Delete(2)
+ assert.True(t, ok)
+ assert.Equal(t, 4, s.Len())
+
+ expected := []int{1, 2, 4, 5}
+ actual := s.Slice()
+ assert.Equal(t, expected, actual)
+
+ // Delete out of bounds
+ ok = s.Delete(10)
+ assert.False(t, ok)
+ assert.Equal(t, 4, s.Len())
+
+ // Delete negative index
+ ok = s.Delete(-1)
+ assert.False(t, ok)
+ assert.Equal(t, 4, s.Len())
+ })
+
+ t.Run("Get", func(t *testing.T) {
+ s := NewSliceFrom([]string{"a", "b", "c"})
+
+ val, ok := s.Get(1)
+ require.True(t, ok)
+ assert.Equal(t, "b", val)
+
+ // Out of bounds
+ _, ok = s.Get(10)
+ assert.False(t, ok)
+
+ // Negative index
+ _, ok = s.Get(-1)
+ assert.False(t, ok)
+ })
+
+ t.Run("Set", func(t *testing.T) {
+ s := NewSliceFrom([]string{"a", "b", "c"})
+
+ ok := s.Set(1, "modified")
+ assert.True(t, ok)
+
+ val, ok := s.Get(1)
+ require.True(t, ok)
+ assert.Equal(t, "modified", val)
+
+ // Out of bounds
+ ok = s.Set(10, "invalid")
+ assert.False(t, ok)
+
+ // Negative index
+ ok = s.Set(-1, "invalid")
+ assert.False(t, ok)
+ })
+
+ t.Run("SetSlice", func(t *testing.T) {
+ s := NewSlice[int]()
+ s.Append(1)
+ s.Append(2)
+
+ newItems := []int{10, 20, 30}
+ s.SetSlice(newItems)
+
+ assert.Equal(t, 3, s.Len())
+ assert.Equal(t, newItems, s.Slice())
+
+ // Verify it's a copy
+ newItems[0] = 999
+ val, ok := s.Get(0)
+ require.True(t, ok)
+ assert.Equal(t, 10, val)
+ })
+
+ t.Run("Clear", func(t *testing.T) {
+ s := NewSliceFrom([]int{1, 2, 3})
+ assert.Equal(t, 3, s.Len())
+
+ s.Clear()
+ assert.Equal(t, 0, s.Len())
+ })
+
+ t.Run("Slice", func(t *testing.T) {
+ original := []int{1, 2, 3}
+ s := NewSliceFrom(original)
+
+ copy := s.Slice()
+ assert.Equal(t, original, copy)
+
+ // Verify it's a copy
+ copy[0] = 999
+ val, ok := s.Get(0)
+ require.True(t, ok)
+ assert.Equal(t, 1, val)
+ })
+
+ t.Run("Seq", func(t *testing.T) {
+ s := NewSliceFrom([]int{1, 2, 3})
+
+ var result []int
+ for v := range s.Seq() {
+ result = append(result, v)
+ }
+
+ assert.Equal(t, []int{1, 2, 3}, result)
+ })
+
+ t.Run("SeqWithIndex", func(t *testing.T) {
+ s := NewSliceFrom([]string{"a", "b", "c"})
+
+ var indices []int
+ var values []string
+ for i, v := range s.SeqWithIndex() {
+ indices = append(indices, i)
+ values = append(values, v)
+ }
+
+ assert.Equal(t, []int{0, 1, 2}, indices)
+ assert.Equal(t, []string{"a", "b", "c"}, values)
+ })
+
+ t.Run("ConcurrentAccess", func(t *testing.T) {
+ s := NewSlice[int]()
+ const numGoroutines = 100
+ const itemsPerGoroutine = 10
+
+ var wg sync.WaitGroup
+
+ // Concurrent appends
+ for i := 0; i < numGoroutines; i++ {
+ wg.Add(1)
+ go func(start int) {
+ defer wg.Done()
+ for j := 0; j < itemsPerGoroutine; j++ {
+ s.Append(start*itemsPerGoroutine + j)
+ }
+ }(i)
+ }
+
+ // Concurrent reads
+ for i := 0; i < numGoroutines; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for j := 0; j < itemsPerGoroutine; j++ {
+ s.Len() // Just read the length
+ }
+ }()
+ }
+
+ wg.Wait()
+
+ // Should have all items
+ assert.Equal(t, numGoroutines*itemsPerGoroutine, s.Len())
+ })
+}
@@ -97,7 +97,7 @@ func NewFilterableList[T FilterableItem](items []T, opts ...filterableListOption
f.list = New[T](items, f.listOptions...).(*list[T])
f.updateKeyMaps()
- f.items = f.list.items
+ f.items = f.list.items.Slice()
if f.inputHidden {
return f
@@ -179,7 +179,7 @@ func (f *filterableGroupList[T]) inputHeight() int {
func (f *filterableGroupList[T]) Filter(query string) tea.Cmd {
var cmds []tea.Cmd
- for _, item := range f.items {
+ for _, item := range f.items.Slice() {
if i, ok := any(item).(layout.Focusable); ok {
cmds = append(cmds, i.Blur())
}
@@ -38,6 +38,7 @@ func NewGroupedList[T Item](groups []Group[T], opts ...ListOption) GroupedList[T
keyMap: DefaultKeyMap(),
focused: true,
},
+ items: csync.NewSlice[Item](),
indexMap: csync.NewMap[string, int](),
renderedItems: csync.NewMap[string, renderedItem](),
}
@@ -82,13 +83,13 @@ func (g *groupedList[T]) convertItems() {
items = append(items, g)
}
}
- g.items = items
+ g.items.SetSlice(items)
}
func (g *groupedList[T]) SetGroups(groups []Group[T]) tea.Cmd {
g.groups = groups
g.convertItems()
- return g.SetItems(g.items)
+ return g.SetItems(g.items.Slice())
}
func (g *groupedList[T]) Groups() []Group[T] {
@@ -1,7 +1,6 @@
package list
import (
- "slices"
"strings"
"github.com/charmbracelet/bubbles/v2/key"
@@ -86,7 +85,7 @@ type list[T Item] struct {
offset int
indexMap *csync.Map[string, int]
- items []T
+ items *csync.Slice[T]
renderedItems *csync.Map[string, renderedItem]
@@ -164,7 +163,7 @@ func New[T Item](items []T, opts ...ListOption) List[T] {
keyMap: DefaultKeyMap(),
focused: true,
},
- items: items,
+ items: csync.NewSliceFrom(items),
indexMap: csync.NewMap[string, int](),
renderedItems: csync.NewMap[string, renderedItem](),
}
@@ -191,7 +190,7 @@ func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case anim.StepMsg:
var cmds []tea.Cmd
- for _, item := range l.items {
+ for _, item := range l.items.Slice() {
if i, ok := any(item).(HasAnim); ok && i.Spinning() {
updated, cmd := i.Update(msg)
cmds = append(cmds, cmd)
@@ -267,7 +266,7 @@ func (l *list[T]) viewPosition() (int, int) {
func (l *list[T]) recalculateItemPositions() {
currentContentHeight := 0
- for _, item := range l.items {
+ for _, item := range l.items.Slice() {
rItem, ok := l.renderedItems.Get(item.ID())
if !ok {
continue
@@ -280,7 +279,7 @@ func (l *list[T]) recalculateItemPositions() {
}
func (l *list[T]) render() tea.Cmd {
- if l.width <= 0 || l.height <= 0 || len(l.items) == 0 {
+ if l.width <= 0 || l.height <= 0 || l.items.Len() == 0 {
return nil
}
l.setDefaultSelected()
@@ -424,19 +423,23 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
if inx == ItemNotFound {
return nil
}
- item, ok := l.renderedItems.Get(l.items[inx].ID())
+ item, ok := l.items.Get(inx)
+ if !ok {
+ continue
+ }
+ renderedItem, ok := l.renderedItems.Get(item.ID())
if !ok {
continue
}
// If the item is bigger than the viewport, select it
- if item.start <= start && item.end >= end {
- l.selectedItem = item.id
+ if renderedItem.start <= start && renderedItem.end >= end {
+ l.selectedItem = renderedItem.id
return l.render()
}
// item is in the view
- if item.start >= start && item.start <= end {
- l.selectedItem = item.id
+ if renderedItem.start >= start && renderedItem.start <= end {
+ l.selectedItem = renderedItem.id
return l.render()
}
}
@@ -452,19 +455,23 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
if inx == ItemNotFound {
return nil
}
- item, ok := l.renderedItems.Get(l.items[inx].ID())
+ item, ok := l.items.Get(inx)
+ if !ok {
+ continue
+ }
+ renderedItem, ok := l.renderedItems.Get(item.ID())
if !ok {
continue
}
// If the item is bigger than the viewport, select it
- if item.start <= start && item.end >= end {
- l.selectedItem = item.id
+ if renderedItem.start <= start && renderedItem.end >= end {
+ l.selectedItem = renderedItem.id
return l.render()
}
// item is in the view
- if item.end >= start && item.end <= end {
- l.selectedItem = item.id
+ if renderedItem.end >= start && renderedItem.end <= end {
+ l.selectedItem = renderedItem.id
return l.render()
}
}
@@ -475,36 +482,51 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
func (l *list[T]) selectFirstItem() {
inx := l.firstSelectableItemBelow(-1)
if inx != ItemNotFound {
- l.selectedItem = l.items[inx].ID()
+ item, ok := l.items.Get(inx)
+ if ok {
+ l.selectedItem = item.ID()
+ }
}
}
func (l *list[T]) selectLastItem() {
- inx := l.firstSelectableItemAbove(len(l.items))
+ inx := l.firstSelectableItemAbove(l.items.Len())
if inx != ItemNotFound {
- l.selectedItem = l.items[inx].ID()
+ item, ok := l.items.Get(inx)
+ if ok {
+ l.selectedItem = item.ID()
+ }
}
}
func (l *list[T]) firstSelectableItemAbove(inx int) int {
for i := inx - 1; i >= 0; i-- {
- if _, ok := any(l.items[i]).(layout.Focusable); ok {
+ item, ok := l.items.Get(i)
+ if !ok {
+ continue
+ }
+ if _, ok := any(item).(layout.Focusable); ok {
return i
}
}
if inx == 0 && l.wrap {
- return l.firstSelectableItemAbove(len(l.items))
+ return l.firstSelectableItemAbove(l.items.Len())
}
return ItemNotFound
}
func (l *list[T]) firstSelectableItemBelow(inx int) int {
- for i := inx + 1; i < len(l.items); i++ {
- if _, ok := any(l.items[i]).(layout.Focusable); ok {
+ itemsLen := l.items.Len()
+ for i := inx + 1; i < itemsLen; i++ {
+ item, ok := l.items.Get(i)
+ if !ok {
+ continue
+ }
+ if _, ok := any(item).(layout.Focusable); ok {
return i
}
}
- if inx == len(l.items)-1 && l.wrap {
+ if inx == itemsLen-1 && l.wrap {
return l.firstSelectableItemBelow(-1)
}
return ItemNotFound
@@ -515,7 +537,7 @@ func (l *list[T]) focusSelectedItem() tea.Cmd {
return nil
}
var cmds []tea.Cmd
- for _, item := range l.items {
+ for _, item := range l.items.Slice() {
if f, ok := any(item).(layout.Focusable); ok {
if item.ID() == l.selectedItem && !f.IsFocused() {
cmds = append(cmds, f.Focus())
@@ -534,7 +556,7 @@ func (l *list[T]) blurSelectedItem() tea.Cmd {
return nil
}
var cmds []tea.Cmd
- for _, item := range l.items {
+ for _, item := range l.items.Slice() {
if f, ok := any(item).(layout.Focusable); ok {
if item.ID() == l.selectedItem && f.IsFocused() {
cmds = append(cmds, f.Blur())
@@ -549,7 +571,8 @@ func (l *list[T]) blurSelectedItem() tea.Cmd {
// returns the last index
func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
currentContentHeight := lipgloss.Height(l.rendered) - 1
- for i := startInx; i < len(l.items); i++ {
+ itemsLen := l.items.Len()
+ for i := startInx; i < itemsLen; i++ {
if currentContentHeight >= l.height && limitHeight {
return i
}
@@ -557,10 +580,13 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
inx := i
if l.direction != DirectionForward {
- inx = (len(l.items) - 1) - i
+ inx = (itemsLen - 1) - i
}
- item := l.items[inx]
+ item, ok := l.items.Get(inx)
+ if !ok {
+ continue
+ }
var rItem renderedItem
if cache, ok := l.renderedItems.Get(item.ID()); ok {
rItem = cache
@@ -571,7 +597,7 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
l.renderedItems.Set(item.ID(), rItem)
}
gap := l.gap + 1
- if inx == len(l.items)-1 {
+ if inx == itemsLen-1 {
gap = 0
}
@@ -582,7 +608,7 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
}
currentContentHeight = rItem.end + 1 + l.gap
}
- return len(l.items)
+ return itemsLen
}
func (l *list[T]) renderItem(item Item) renderedItem {
@@ -602,9 +628,9 @@ func (l *list[T]) AppendItem(item T) tea.Cmd {
cmds = append(cmds, cmd)
}
- l.items = append(l.items, item)
+ l.items.Append(item)
l.indexMap = csync.NewMap[string, int]()
- for inx, item := range l.items {
+ for inx, item := range l.items.Slice() {
l.indexMap.Set(item.ID(), inx)
}
if l.width > 0 && l.height > 0 {
@@ -627,7 +653,7 @@ func (l *list[T]) AppendItem(item T) tea.Cmd {
newItem, ok := l.renderedItems.Get(item.ID())
if ok {
newLines := newItem.height
- if len(l.items) > 1 {
+ if l.items.Len() > 1 {
newLines += l.gap
}
l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
@@ -649,15 +675,20 @@ func (l *list[T]) DeleteItem(id string) tea.Cmd {
if !ok {
return nil
}
- l.items = slices.Delete(l.items, inx, inx+1)
+ l.items.Delete(inx)
l.renderedItems.Del(id)
- for inx, item := range l.items {
+ for inx, item := range l.items.Slice() {
l.indexMap.Set(item.ID(), inx)
}
if l.selectedItem == id {
if inx > 0 {
- l.selectedItem = l.items[inx-1].ID()
+ item, ok := l.items.Get(inx - 1)
+ if ok {
+ l.selectedItem = item.ID()
+ } else {
+ l.selectedItem = ""
+ }
} else {
l.selectedItem = ""
}
@@ -711,7 +742,7 @@ func (l *list[T]) IsFocused() bool {
// Items implements List.
func (l *list[T]) Items() []T {
- return l.items
+ return l.items.Slice()
}
func (l *list[T]) incrementOffset(n int) {
@@ -764,9 +795,9 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
cmds := []tea.Cmd{
item.Init(),
}
- l.items = append([]T{item}, l.items...)
+ l.items.Prepend(item)
l.indexMap = csync.NewMap[string, int]()
- for inx, item := range l.items {
+ for inx, item := range l.items.Slice() {
l.indexMap.Set(item.ID(), inx)
}
if l.width > 0 && l.height > 0 {
@@ -783,7 +814,7 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
newItem, ok := l.renderedItems.Get(item.ID())
if ok {
newLines := newItem.height
- if len(l.items) > 1 {
+ if l.items.Len() > 1 {
newLines += l.gap
}
l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
@@ -817,7 +848,10 @@ func (l *list[T]) SelectItemAbove() tea.Cmd {
}
}
- item := l.items[newIndex]
+ item, ok := l.items.Get(newIndex)
+ if !ok {
+ return nil
+ }
l.selectedItem = item.ID()
l.movingByItem = true
renderCmd := l.render()
@@ -839,7 +873,10 @@ func (l *list[T]) SelectItemBelow() tea.Cmd {
// no item above
return nil
}
- item := l.items[newIndex]
+ item, ok := l.items.Get(newIndex)
+ if !ok {
+ return nil
+ }
l.selectedItem = item.ID()
l.movingByItem = true
return l.render()
@@ -851,18 +888,21 @@ func (l *list[T]) SelectedItem() *T {
if !ok {
return nil
}
- if inx > len(l.items)-1 {
+ if inx > l.items.Len()-1 {
+ return nil
+ }
+ item, ok := l.items.Get(inx)
+ if !ok {
return nil
}
- item := l.items[inx]
return &item
}
// SetItems implements List.
func (l *list[T]) SetItems(items []T) tea.Cmd {
- l.items = items
+ l.items.SetSlice(items)
var cmds []tea.Cmd
- for inx, item := range l.items {
+ for inx, item := range l.items.Slice() {
if i, ok := any(item).(Indexable); ok {
i.SetIndex(inx)
}
@@ -885,7 +925,7 @@ func (l *list[T]) reset(selectedItem string) tea.Cmd {
l.selectedItem = selectedItem
l.indexMap = csync.NewMap[string, int]()
l.renderedItems = csync.NewMap[string, renderedItem]()
- for inx, item := range l.items {
+ for inx, item := range l.items.Slice() {
l.indexMap.Set(item.ID(), inx)
if l.width > 0 && l.height > 0 {
cmds = append(cmds, item.SetSize(l.width, l.height))
@@ -911,7 +951,7 @@ func (l *list[T]) SetSize(width int, height int) tea.Cmd {
func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
var cmds []tea.Cmd
if inx, ok := l.indexMap.Get(id); ok {
- l.items[inx] = item
+ l.items.Set(inx, item)
oldItem, hasOldItem := l.renderedItems.Get(id)
oldPosition := l.offset
if l.direction == DirectionBackward {
@@ -928,7 +968,7 @@ func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
if hasOldItem && l.direction == DirectionBackward {
// if we are the last item and there is no offset
// make sure to go to the bottom
- if inx == len(l.items)-1 && l.offset == 0 {
+ if inx == l.items.Len()-1 && l.offset == 0 {
cmd = l.GoToBottom()
if cmd != nil {
cmds = append(cmds, cmd)
@@ -30,7 +30,7 @@ func TestList(t *testing.T) {
assert.Equal(t, items[0].ID(), l.selectedItem)
assert.Equal(t, 0, l.offset)
require.Equal(t, 5, l.indexMap.Len())
- require.Len(t, l.items, 5)
+ require.Equal(t, 5, l.items.Len())
require.Equal(t, 5, l.renderedItems.Len())
assert.Equal(t, 5, lipgloss.Height(l.rendered))
assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
@@ -60,7 +60,7 @@ func TestList(t *testing.T) {
assert.Equal(t, items[4].ID(), l.selectedItem)
assert.Equal(t, 0, l.offset)
require.Equal(t, 5, l.indexMap.Len())
- require.Len(t, l.items, 5)
+ require.Equal(t, 5, l.items.Len())
require.Equal(t, 5, l.renderedItems.Len())
assert.Equal(t, 5, lipgloss.Height(l.rendered))
assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
@@ -91,7 +91,7 @@ func TestList(t *testing.T) {
assert.Equal(t, items[0].ID(), l.selectedItem)
assert.Equal(t, 0, l.offset)
require.Equal(t, 30, l.indexMap.Len())
- require.Len(t, l.items, 30)
+ require.Equal(t, 30, l.items.Len())
require.Equal(t, 30, l.renderedItems.Len())
assert.Equal(t, 30, lipgloss.Height(l.rendered))
assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
@@ -121,7 +121,7 @@ func TestList(t *testing.T) {
assert.Equal(t, items[29].ID(), l.selectedItem)
assert.Equal(t, 0, l.offset)
require.Equal(t, 30, l.indexMap.Len())
- require.Len(t, l.items, 30)
+ require.Equal(t, 30, l.items.Len())
require.Equal(t, 30, l.renderedItems.Len())
assert.Equal(t, 30, lipgloss.Height(l.rendered))
assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
@@ -154,7 +154,7 @@ func TestList(t *testing.T) {
assert.Equal(t, items[0].ID(), l.selectedItem)
assert.Equal(t, 0, l.offset)
require.Equal(t, 30, l.indexMap.Len())
- require.Len(t, l.items, 30)
+ require.Equal(t, 30, l.items.Len())
require.Equal(t, 30, l.renderedItems.Len())
expectedLines := 0
for i := range 30 {
@@ -192,7 +192,7 @@ func TestList(t *testing.T) {
assert.Equal(t, items[29].ID(), l.selectedItem)
assert.Equal(t, 0, l.offset)
require.Equal(t, 30, l.indexMap.Len())
- require.Len(t, l.items, 30)
+ require.Equal(t, 30, l.items.Len())
require.Equal(t, 30, l.renderedItems.Len())
expectedLines := 0
for i := range 30 {