mirror of
https://github.com/charmbracelet/crush.git
synced 2025-08-02 05:20:46 +03:00
chore: safe slice
This commit is contained in:
@@ -1 +1 @@
|
||||
{"flagWords":[],"version":"0.2","words":["afero","agentic","alecthomas","anthropics","aymanbagabas","azidentity","bmatcuk","bubbletea","charlievieth","charmbracelet","charmtone","Charple","chkconfig","crush","curlie","cursorrules","diffview","doas","Dockerfiles","doublestar","dpkg","Emph","fastwalk","fdisk","filepicker","Focusable","fseventsd","fsext","genai","goquery","GROQ","Guac","imageorient","Inex","jetta","jsons","jsonschema","jspm","Kaufmann","killall","Lanczos","lipgloss","LOCALAPPDATA","lsps","lucasb","makepkg","mcps","MSYS","mvdan","natefinch","nfnt","noctx","nohup","nolint","nslookup","oksvg","Oneshot","openrouter","opkg","pacman","paru","pfctl","postamble","postambles","preconfigured","Preproc","Proactiveness","Puerkito","pycache","pytest","qjebbs","rasterx","rivo","sabhiram","sess","shortlog","sjson","Sourcegraph","srwiley","SSEMCP","Streamable","stretchr","Strikethrough","substrs","Suscriber","systeminfo","tasklist","termenv","textinput","tidwall","timedout","trashhalo","udiff","uniseg","Unticked","urllib","USERPROFILE","VERTEXAI","webp","whatis","whereis","sahilm"],"language":"en"}
|
||||
{"flagWords":[],"version":"0.2","language":"en","words":["afero","agentic","alecthomas","anthropics","aymanbagabas","azidentity","bmatcuk","bubbletea","charlievieth","charmbracelet","charmtone","Charple","chkconfig","crush","curlie","cursorrules","diffview","doas","Dockerfiles","doublestar","dpkg","Emph","fastwalk","fdisk","filepicker","Focusable","fseventsd","fsext","genai","goquery","GROQ","Guac","imageorient","Inex","jetta","jsons","jsonschema","jspm","Kaufmann","killall","Lanczos","lipgloss","LOCALAPPDATA","lsps","lucasb","makepkg","mcps","MSYS","mvdan","natefinch","nfnt","noctx","nohup","nolint","nslookup","oksvg","Oneshot","openrouter","opkg","pacman","paru","pfctl","postamble","postambles","preconfigured","Preproc","Proactiveness","Puerkito","pycache","pytest","qjebbs","rasterx","rivo","sabhiram","sess","shortlog","sjson","Sourcegraph","srwiley","SSEMCP","Streamable","stretchr","Strikethrough","substrs","Suscriber","systeminfo","tasklist","termenv","textinput","tidwall","timedout","trashhalo","udiff","uniseg","Unticked","urllib","USERPROFILE","VERTEXAI","webp","whatis","whereis","sahilm","csync"]}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user