@@ -6,6 +6,7 @@ import (
7 "github.com/charmbracelet/bubbles/v2/key"
7 "github.com/charmbracelet/bubbles/v2/key"
8 tea "github.com/charmbracelet/bubbletea/v2"
8 tea "github.com/charmbracelet/bubbletea/v2"
9+ "github.com/charmbracelet/crush/internal/csync"
9 "github.com/charmbracelet/crush/internal/tui/components/anim"
10 "github.com/charmbracelet/crush/internal/tui/components/anim"
10 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
11 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
11 "github.com/charmbracelet/crush/internal/tui/styles"
12 "github.com/charmbracelet/crush/internal/tui/styles"
@@ -84,10 +85,10 @@ type list[T Item] struct {
85 offset int
86 offset int
87− indexMap map[string]int
88+ indexMap *csync.Map[string, int]
90− renderedItems map[string]renderedItem
91+ renderedItems *csync.Map[string, renderedItem]
92 rendered string
93 rendered string
@@ -164,8 +165,8 @@ func New[T Item](items []T, opts ...ListOption) List[T] {
164 focused: true,
165 focused: true,
166 items: items,
167 items: items,
167− indexMap: make(map[string]int),
168+ indexMap: csync.NewMap[string, int](),
168− renderedItems: map[string]renderedItem{},
169+ renderedItems: csync.NewMap[string, renderedItem](),
170 for _, opt := range opts {
171 for _, opt := range opts {
171 opt(list.confOptions)
172 opt(list.confOptions)
@@ -175,7 +176,7 @@ func New[T Item](items []T, opts ...ListOption) List[T] {
175 if i, ok := any(item).(Indexable); ok {
176 if i, ok := any(item).(Indexable); ok {
176 i.SetIndex(inx)
177 i.SetIndex(inx)
178− list.indexMap[item.ID()] = inx
179+ list.indexMap.Set(item.ID(), inx)
180 return list
181 return list
@@ -267,13 +268,13 @@ func (l *list[T]) viewPosition() (int, int) {
267func (l *list[T]) recalculateItemPositions() {
268func (l *list[T]) recalculateItemPositions() {
268 currentContentHeight := 0
269 currentContentHeight := 0
269 for _, item := range l.items {
270 for _, item := range l.items {
270− rItem, ok := l.renderedItems[item.ID()]
271+ rItem, ok := l.renderedItems.Get(item.ID())
274 rItem.start = currentContentHeight
275 rItem.start = currentContentHeight
275 rItem.end = currentContentHeight + rItem.height - 1
276 rItem.end = currentContentHeight + rItem.height - 1
276− l.renderedItems[item.ID()] = rItem
277+ l.renderedItems.Set(item.ID(), rItem)
277 currentContentHeight = rItem.end + 1 + l.gap
278 currentContentHeight = rItem.end + 1 + l.gap
@@ -337,7 +338,7 @@ func (l *list[T]) setDefaultSelected() {
339func (l *list[T]) scrollToSelection() {
340func (l *list[T]) scrollToSelection() {
340− rItem, ok := l.renderedItems[l.selectedItem]
341+ rItem, ok := l.renderedItems.Get(l.selectedItem)
342 l.selectedItem = ""
343 l.selectedItem = ""
343 l.setDefaultSelected()
344 l.setDefaultSelected()
@@ -395,7 +396,7 @@ func (l *list[T]) scrollToSelection() {
397func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
398func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
398− rItem, ok := l.renderedItems[l.selectedItem]
399+ rItem, ok := l.renderedItems.Get(l.selectedItem)
400 return nil
401 return nil
@@ -414,13 +415,16 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
414 if itemMiddle < start {
415 if itemMiddle < start {
415 // select the first item in the viewport
416 // select the first item in the viewport
416 // the item is most likely an item coming after this item
417 // the item is most likely an item coming after this item
417− inx := l.indexMap[rItem.id]
418+ inx, ok := l.indexMap.Get(rItem.id)
419 inx = l.firstSelectableItemBelow(inx)
423 inx = l.firstSelectableItemBelow(inx)
420 if inx == ItemNotFound {
424 if inx == ItemNotFound {
421 return nil
425 return nil
423− item, ok := l.renderedItems[l.items[inx].ID()]
427+ item, ok := l.renderedItems.Get(l.items[inx].ID())
@@ -439,13 +443,16 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
439 } else if itemMiddle > end {
443 } else if itemMiddle > end {
440 // select the first item in the viewport
444 // select the first item in the viewport
441 // the item is most likely an item coming after this item
445 // the item is most likely an item coming after this item
442− inx := l.indexMap[rItem.id]
446+ inx, ok := l.indexMap.Get(rItem.id)
444 inx = l.firstSelectableItemAbove(inx)
451 inx = l.firstSelectableItemAbove(inx)
445 if inx == ItemNotFound {
452 if inx == ItemNotFound {
446 return nil
453 return nil
448− item, ok := l.renderedItems[l.items[inx].ID()]
455+ item, ok := l.renderedItems.Get(l.items[inx].ID())
@@ -512,10 +519,10 @@ func (l *list[T]) focusSelectedItem() tea.Cmd {
512 if f, ok := any(item).(layout.Focusable); ok {
519 if f, ok := any(item).(layout.Focusable); ok {
513 if item.ID() == l.selectedItem && !f.IsFocused() {
520 if item.ID() == l.selectedItem && !f.IsFocused() {
514 cmds = append(cmds, f.Focus())
521 cmds = append(cmds, f.Focus())
515− delete(l.renderedItems, item.ID())
522+ l.renderedItems.Del(item.ID())
516 } else if item.ID() != l.selectedItem && f.IsFocused() {
523 } else if item.ID() != l.selectedItem && f.IsFocused() {
517 cmds = append(cmds, f.Blur())
524 cmds = append(cmds, f.Blur())
518− delete(l.renderedItems, item.ID())
525+ l.renderedItems.Del(item.ID())
@@ -531,7 +538,7 @@ func (l *list[T]) blurSelectedItem() tea.Cmd {
531 if f, ok := any(item).(layout.Focusable); ok {
538 if f, ok := any(item).(layout.Focusable); ok {
532 if item.ID() == l.selectedItem && f.IsFocused() {
539 if item.ID() == l.selectedItem && f.IsFocused() {
533 cmds = append(cmds, f.Blur())
540 cmds = append(cmds, f.Blur())
534− delete(l.renderedItems, item.ID())
541+ l.renderedItems.Del(item.ID())
@@ -555,13 +562,13 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
556 item := l.items[inx]
563 item := l.items[inx]
557 var rItem renderedItem
564 var rItem renderedItem
558− if cache, ok := l.renderedItems[item.ID()]; ok {
565+ if cache, ok := l.renderedItems.Get(item.ID()); ok {
559 rItem = cache
566 rItem = cache
561 rItem = l.renderItem(item)
568 rItem = l.renderItem(item)
562 rItem.start = currentContentHeight
569 rItem.start = currentContentHeight
563 rItem.end = currentContentHeight + rItem.height - 1
570 rItem.end = currentContentHeight + rItem.height - 1
564− l.renderedItems[item.ID()] = rItem
571+ l.renderedItems.Set(item.ID(), rItem)
566 gap := l.gap + 1
573 gap := l.gap + 1
567 if inx == len(l.items)-1 {
574 if inx == len(l.items)-1 {
@@ -596,9 +603,9 @@ func (l *list[T]) AppendItem(item T) tea.Cmd {
598 l.items = append(l.items, item)
605 l.items = append(l.items, item)
599− l.indexMap = make(map[string]int)
606+ l.indexMap = csync.NewMap[string, int]()
600 for inx, item := range l.items {
607 for inx, item := range l.items {
601− l.indexMap[item.ID()] = inx
608+ l.indexMap.Set(item.ID(), inx)
603 if l.width > 0 && l.height > 0 {
610 if l.width > 0 && l.height > 0 {
604 cmd = item.SetSize(l.width, l.height)
611 cmd = item.SetSize(l.width, l.height)
@@ -617,12 +624,14 @@ func (l *list[T]) AppendItem(item T) tea.Cmd {
617 cmds = append(cmds, cmd)
624 cmds = append(cmds, cmd)
620− newItem := l.renderedItems[item.ID()]
627+ newItem, ok := l.renderedItems.Get(item.ID())
621− newLines := newItem.height
629+ newLines := newItem.height
622− if len(l.items) > 1 {
630+ if len(l.items) > 1 {
623− newLines += l.gap
631+ newLines += l.gap
633+ l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
625− l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
628 return tea.Sequence(cmds...)
637 return tea.Sequence(cmds...)
@@ -636,11 +645,14 @@ func (l *list[T]) Blur() tea.Cmd {
637// DeleteItem implements List.
646// DeleteItem implements List.
638func (l *list[T]) DeleteItem(id string) tea.Cmd {
647func (l *list[T]) DeleteItem(id string) tea.Cmd {
639− inx := l.indexMap[id]
648+ inx, ok := l.indexMap.Get(id)
640 l.items = slices.Delete(l.items, inx, inx+1)
652 l.items = slices.Delete(l.items, inx, inx+1)
641− delete(l.renderedItems, id)
653+ l.renderedItems.Del(id)
642 for inx, item := range l.items {
654 for inx, item := range l.items {
643− l.indexMap[item.ID()] = inx
655+ l.indexMap.Set(item.ID(), inx)
646 if l.selectedItem == id {
658 if l.selectedItem == id {
@@ -753,9 +765,9 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
753 item.Init(),
765 item.Init(),
755 l.items = append([]T{item}, l.items...)
767 l.items = append([]T{item}, l.items...)
756− l.indexMap = make(map[string]int)
768+ l.indexMap = csync.NewMap[string, int]()
757 for inx, item := range l.items {
769 for inx, item := range l.items {
758− l.indexMap[item.ID()] = inx
770+ l.indexMap.Set(item.ID(), inx)
760 if l.width > 0 && l.height > 0 {
772 if l.width > 0 && l.height > 0 {
761 cmds = append(cmds, item.SetSize(l.width, l.height))
773 cmds = append(cmds, item.SetSize(l.width, l.height))
@@ -768,12 +780,14 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
768 cmds = append(cmds, cmd)
780 cmds = append(cmds, cmd)
771− newItem := l.renderedItems[item.ID()]
783+ newItem, ok := l.renderedItems.Get(item.ID())
772− newLines := newItem.height
785+ newLines := newItem.height
773− if len(l.items) > 1 {
786+ if len(l.items) > 1 {
774− newLines += l.gap
787+ newLines += l.gap
789+ l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
776− l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
779 return tea.Batch(cmds...)
793 return tea.Batch(cmds...)
@@ -781,7 +795,7 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
782// SelectItemAbove implements List.
796// SelectItemAbove implements List.
783func (l *list[T]) SelectItemAbove() tea.Cmd {
797func (l *list[T]) SelectItemAbove() tea.Cmd {
784− inx, ok := l.indexMap[l.selectedItem]
798+ inx, ok := l.indexMap.Get(l.selectedItem)
786 return nil
800 return nil
@@ -815,7 +829,7 @@ func (l *list[T]) SelectItemAbove() tea.Cmd {
816// SelectItemBelow implements List.
830// SelectItemBelow implements List.
817func (l *list[T]) SelectItemBelow() tea.Cmd {
831func (l *list[T]) SelectItemBelow() tea.Cmd {
818− inx, ok := l.indexMap[l.selectedItem]
832+ inx, ok := l.indexMap.Get(l.selectedItem)
820 return nil
834 return nil
@@ -833,7 +847,7 @@ func (l *list[T]) SelectItemBelow() tea.Cmd {
834// SelectedItem implements List.
848// SelectedItem implements List.
835func (l *list[T]) SelectedItem() *T {
849func (l *list[T]) SelectedItem() *T {
836− inx, ok := l.indexMap[l.selectedItem]
850+ inx, ok := l.indexMap.Get(l.selectedItem)
838 return nil
852 return nil
@@ -869,10 +883,10 @@ func (l *list[T]) reset(selectedItem string) tea.Cmd {
869 l.rendered = ""
883 l.rendered = ""
870 l.offset = 0
884 l.offset = 0
871 l.selectedItem = selectedItem
885 l.selectedItem = selectedItem
872− l.indexMap = make(map[string]int)
886+ l.indexMap = csync.NewMap[string, int]()
873− l.renderedItems = make(map[string]renderedItem)
887+ l.renderedItems = csync.NewMap[string, renderedItem]()
874 for inx, item := range l.items {
888 for inx, item := range l.items {
875− l.indexMap[item.ID()] = inx
889+ l.indexMap.Set(item.ID(), inx)
876 if l.width > 0 && l.height > 0 {
890 if l.width > 0 && l.height > 0 {
877 cmds = append(cmds, item.SetSize(l.width, l.height))
891 cmds = append(cmds, item.SetSize(l.width, l.height))
@@ -896,22 +910,22 @@ func (l *list[T]) SetSize(width int, height int) tea.Cmd {
896// UpdateItem implements List.
910// UpdateItem implements List.
897func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
911func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
898 var cmds []tea.Cmd
912 var cmds []tea.Cmd
899− if inx, ok := l.indexMap[id]; ok {
913+ if inx, ok := l.indexMap.Get(id); ok {
900 l.items[inx] = item
914 l.items[inx] = item
901− oldItem := l.renderedItems[id]
915+ oldItem, hasOldItem := l.renderedItems.Get(id)
902 oldPosition := l.offset
916 oldPosition := l.offset
903 if l.direction == DirectionBackward {
917 if l.direction == DirectionBackward {
904 oldPosition = (lipgloss.Height(l.rendered) - 1) - l.offset
918 oldPosition = (lipgloss.Height(l.rendered) - 1) - l.offset
907− delete(l.renderedItems, id)
921+ l.renderedItems.Del(id)
908 cmd := l.render()
922 cmd := l.render()
910 // need to check for nil because of sequence not handling nil
924 // need to check for nil because of sequence not handling nil
911 if cmd != nil {
925 if cmd != nil {
912 cmds = append(cmds, cmd)
926 cmds = append(cmds, cmd)
914− if l.direction == DirectionBackward {
928+ if hasOldItem && l.direction == DirectionBackward {
915 // if we are the last item and there is no offset
929 // if we are the last item and there is no offset
916 // make sure to go to the bottom
930 // make sure to go to the bottom
917 if inx == len(l.items)-1 && l.offset == 0 {
931 if inx == len(l.items)-1 && l.offset == 0 {
@@ -921,14 +935,18 @@ func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
922 // if the item is at least partially below the viewport
936 // if the item is at least partially below the viewport
923 } else if oldPosition < oldItem.end {
937 } else if oldPosition < oldItem.end {
924− newItem := l.renderedItems[item.ID()]
938+ newItem, ok := l.renderedItems.Get(item.ID())
940+ newLines := newItem.height - oldItem.height
941+ l.offset = util.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
944+ } else if hasOldItem && l.offset > oldItem.start {
945+ newItem, ok := l.renderedItems.Get(item.ID())
925 newLines := newItem.height - oldItem.height
947 newLines := newItem.height - oldItem.height
926 l.offset = util.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
948 l.offset = util.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
928− } else if l.offset > oldItem.start {
929− newItem := l.renderedItems[item.ID()]
930− newLines := newItem.height - oldItem.height
931− l.offset = util.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
934 return tea.Sequence(cmds...)
952 return tea.Sequence(cmds...)