on main help sign in
chore: rebase fix + sync map
commitbb668b1b0fbf9a0c4494119c65fe0df11489e940 authorKujtim Hoxha <kujtimii.h@gmail.com> dateThu 24 Jul 2025, 22:04 (1 year ago) parent20d16fd
32 files changed+388−330
go.mod+1−2 go.sum+2−4 internal/tui/components/completions/completions.go+10−12 internal/tui/components/dialogs/models/list.go+3−3 internal/tui/exp/list/grouped.go+3−2 internal/tui/exp/list/list.go+68−50 internal/tui/exp/list/list_test.go+32−22 internal/tui/exp/list/testdata/TestFilterableList/should_create_simple_filterable_list.golden+9−5 internal/tui/exp/list/testdata/TestList/should_go_to_selected_item_at_the_beginning.golden+10−10 internal/tui/exp/list/testdata/TestList/should_go_to_selected_item_at_the_beginning_backwards.golden+10−10 internal/tui/exp/list/testdata/TestList/should_have_correct_positions_in_list_that_does_not_fits_the_items.golden+10−10 internal/tui/exp/list/testdata/TestList/should_have_correct_positions_in_list_that_does_not_fits_the_items_and_has_multi_line_items.golden+10−10 internal/tui/exp/list/testdata/TestList/should_have_correct_positions_in_list_that_does_not_fits_the_items_and_has_multi_line_items_backwards.golden+10−10 internal/tui/exp/list/testdata/TestList/should_have_correct_positions_in_list_that_does_not_fits_the_items_backwards.golden+10−10 internal/tui/exp/list/testdata/TestList/should_have_correct_positions_in_list_that_fits_the_items.golden+20−5 internal/tui/exp/list/testdata/TestList/should_have_correct_positions_in_list_that_fits_the_items_backwards.golden+20−5 internal/tui/exp/list/testdata/TestListMovement/should_move_viewport_down.golden+10−10 internal/tui/exp/list/testdata/TestListMovement/should_move_viewport_down_and_up.golden+10−10 internal/tui/exp/list/testdata/TestListMovement/should_move_viewport_up.golden+10−10 internal/tui/exp/list/testdata/TestListMovement/should_move_viewport_up_and_down.golden+10−10 internal/tui/exp/list/testdata/TestListMovement/should_not_change_offset_when_new_items_are_appended_and_we_are_at_the_bottom_in_backwards_list.golden+10−10 internal/tui/exp/list/testdata/TestListMovement/should_not_change_offset_when_new_items_are_prepended_and_we_are_at_the_top_in_forward_list.golden+10−10 internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_if_an_item_is_appended_and_we_are_in_forward_list.golden+10−10 internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_if_an_item_is_prepended_and_we_are_in_backwards_list.golden+10−10 internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_new_items_are_added_but_we_moved_down_in_forward_list.golden+10−10 internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_new_items_are_added_but_we_moved_up_in_backwards_list.golden+10−10 internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_above_is_decreases_in_forward_list.golden+10−10 internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_above_is_increased_in_backwards_list.golden+10−10 internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_above_is_increased_in_forward_list.golden+10−10 internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_below_is_decreases_in_backwards_list.golden+10−10 internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_below_is_increased_in_backwards_list.golden+10−10 internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_below_is_increased_in_forward_list.golden+10−10
go.mod+1−2
@@ -12,9 +12,8 @@ require (
1212 github.com/bmatcuk/doublestar/v4 v4.9.0
1313 github.com/charlievieth/fastwalk v1.0.11
1414 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5
15− github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250717140350-bb75e8f6b6ac
16− github.com/charmbracelet/catwalk v0.3.1
1715 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250724172607-5ba56e2bec69
16+ github.com/charmbracelet/catwalk v0.3.1
1817 github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674
1918 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
2019 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250716211347-10c048e36112
go.sum+2−4
@@ -70,12 +70,10 @@ github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr
7070github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
7171github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5 h1:GTcMIfDQJKyNKS+xVt7GkNIwz+tBuQtIuiP50WpzNgs=
7272github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
73−github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250717140350-bb75e8f6b6ac h1:murtkvFYxZ/73vk4Z/tpE4biB+WDZcFmmBp8je/yV6M=
74−github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250717140350-bb75e8f6b6ac/go.mod h1:m240IQxo1/eDQ7klblSzOCAUyc3LddHcV3Rc/YEGAgw=
75−github.com/charmbracelet/catwalk v0.3.1 h1:MkGWspcMyE659zDkqS+9wsaCMTKRFEDBFY2A2sap6+U=
76−github.com/charmbracelet/catwalk v0.3.1/go.mod h1:gUUCqqZ8bk4D7ZzGTu3I77k7cC2x4exRuJBN1H2u2pc=
7773github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250724172607-5ba56e2bec69 h1:nXLMl4ows2qogDXhuEtDNgFNXQiU+PJer+UEBsQZuns=
7874github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20250724172607-5ba56e2bec69/go.mod h1:XIQ1qQfRph6Z5o2EikCydjumo0oDInQySRHuPATzbZc=
75+github.com/charmbracelet/catwalk v0.3.1 h1:MkGWspcMyE659zDkqS+9wsaCMTKRFEDBFY2A2sap6+U=
76+github.com/charmbracelet/catwalk v0.3.1/go.mod h1:gUUCqqZ8bk4D7ZzGTu3I77k7cC2x4exRuJBN1H2u2pc=
7977github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
8078github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
8179github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674 h1:+Cz+VfxD5DO+JT1LlswXWhre0HYLj6l2HW8HVGfMuC0=
internal/tui/components/completions/completions.go+10−12
@@ -122,25 +122,23 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
122122 c.list = d.(listModel)
123123 return c, cmd
124124 case key.Matches(msg, c.keyMap.UpInsert):
125− selectedItemInx := c.list.SelectedIndex() - 1
126− items := c.list.Items()
127− if selectedItemInx == list.NoSelection || selectedItemInx < 0 {
128− return c, nil // No item selected, do nothing
125+ s := c.list.SelectedItem()
126+ if s == nil {
127+ return c, nil
129128 }
130− selectedItem := items[selectedItemInx].(CompletionItem).Value()
131− c.list.SetSelected(selectedItemInx)
129+ selectedItem := *s
130+ c.list.SetSelected(selectedItem.ID())
132131 return c, util.CmdHandler(SelectCompletionMsg{
133132 Value: selectedItem,
134133 Insert: true,
135134 })
136135 case key.Matches(msg, c.keyMap.DownInsert):
137− selectedItemInx := c.list.SelectedIndex() + 1
138− items := c.list.Items()
139− if selectedItemInx == list.NoSelection || selectedItemInx >= len(items) {
140− return c, nil // No item selected, do nothing
136+ s := c.list.SelectedItem()
137+ if s == nil {
138+ return c, nil
141139 }
142− selectedItem := items[selectedItemInx].(CompletionItem).Value()
143− c.list.SetSelected(selectedItemInx)
140+ selectedItem := *s
141+ c.list.SetSelected(selectedItem.ID())
144142 return c, util.CmdHandler(SelectCompletionMsg{
145143 Value: selectedItem,
146144 Insert: true,
internal/tui/components/dialogs/models/list.go+3−3
@@ -157,7 +157,7 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
157157 Section: section,
158158 }
159159 for _, model := range configProvider.Models {
160− item := list.NewCompletionItem(model.Model, ModelOption{
160+ item := list.NewCompletionItem(model.Name, ModelOption{
161161 Provider: configProvider,
162162 Model: model,
163163 },
@@ -195,14 +195,14 @@ func (m *ModelListComponent) SetModelType(modelType int) tea.Cmd {
195195 }
196196
197197 section := list.NewItemSection(name)
198− if _, ok := cfg.Providers[string(provider.ID)]; ok {
198+ if _, ok := cfg.Providers.Get(string(provider.ID)); ok {
199199 section.SetInfo(configured)
200200 }
201201 group := list.Group[list.CompletionItem[ModelOption]]{
202202 Section: section,
203203 }
204204 for _, model := range provider.Models {
205− item := list.NewCompletionItem(model.Model, ModelOption{
205+ item := list.NewCompletionItem(model.Name, ModelOption{
206206 Provider: provider,
207207 Model: model,
208208 },
internal/tui/exp/list/grouped.go+3−2
@@ -2,6 +2,7 @@ package list
22
33import (
44 tea "github.com/charmbracelet/bubbletea/v2"
5+ "github.com/charmbracelet/crush/internal/csync"
56 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
67 "github.com/charmbracelet/crush/internal/tui/util"
78)
@@ -37,8 +38,8 @@ func NewGroupedList[T Item](groups []Group[T], opts ...ListOption) GroupedList[T
3738 keyMap: DefaultKeyMap(),
3839 focused: true,
3940 },
40− indexMap: make(map[string]int),
41− renderedItems: map[string]renderedItem{},
41+ indexMap: csync.NewMap[string, int](),
42+ renderedItems: csync.NewMap[string, renderedItem](),
4243 }
4344 for _, opt := range opts {
4445 opt(list.confOptions)
internal/tui/exp/list/list.go+68−50
@@ -6,6 +6,7 @@ import (
66
77 "github.com/charmbracelet/bubbles/v2/key"
88 tea "github.com/charmbracelet/bubbletea/v2"
9+ "github.com/charmbracelet/crush/internal/csync"
910 "github.com/charmbracelet/crush/internal/tui/components/anim"
1011 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
1112 "github.com/charmbracelet/crush/internal/tui/styles"
@@ -84,10 +85,10 @@ type list[T Item] struct {
8485
8586 offset int
8687
87− indexMap map[string]int
88+ indexMap *csync.Map[string, int]
8889 items []T
8990
90− renderedItems map[string]renderedItem
91+ renderedItems *csync.Map[string, renderedItem]
9192
9293 rendered string
9394
@@ -164,8 +165,8 @@ func New[T Item](items []T, opts ...ListOption) List[T] {
164165 focused: true,
165166 },
166167 items: items,
167− indexMap: make(map[string]int),
168− renderedItems: map[string]renderedItem{},
168+ indexMap: csync.NewMap[string, int](),
169+ renderedItems: csync.NewMap[string, renderedItem](),
169170 }
170171 for _, opt := range opts {
171172 opt(list.confOptions)
@@ -175,7 +176,7 @@ func New[T Item](items []T, opts ...ListOption) List[T] {
175176 if i, ok := any(item).(Indexable); ok {
176177 i.SetIndex(inx)
177178 }
178− list.indexMap[item.ID()] = inx
179+ list.indexMap.Set(item.ID(), inx)
179180 }
180181 return list
181182}
@@ -267,13 +268,13 @@ func (l *list[T]) viewPosition() (int, int) {
267268func (l *list[T]) recalculateItemPositions() {
268269 currentContentHeight := 0
269270 for _, item := range l.items {
270− rItem, ok := l.renderedItems[item.ID()]
271+ rItem, ok := l.renderedItems.Get(item.ID())
271272 if !ok {
272273 continue
273274 }
274275 rItem.start = currentContentHeight
275276 rItem.end = currentContentHeight + rItem.height - 1
276− l.renderedItems[item.ID()] = rItem
277+ l.renderedItems.Set(item.ID(), rItem)
277278 currentContentHeight = rItem.end + 1 + l.gap
278279 }
279280}
@@ -337,7 +338,7 @@ func (l *list[T]) setDefaultSelected() {
337338}
338339
339340func (l *list[T]) scrollToSelection() {
340− rItem, ok := l.renderedItems[l.selectedItem]
341+ rItem, ok := l.renderedItems.Get(l.selectedItem)
341342 if !ok {
342343 l.selectedItem = ""
343344 l.setDefaultSelected()
@@ -395,7 +396,7 @@ func (l *list[T]) scrollToSelection() {
395396}
396397
397398func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
398− rItem, ok := l.renderedItems[l.selectedItem]
399+ rItem, ok := l.renderedItems.Get(l.selectedItem)
399400 if !ok {
400401 return nil
401402 }
@@ -414,13 +415,16 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
414415 if itemMiddle < start {
415416 // select the first item in the viewport
416417 // 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+ if !ok {
420+ return nil
421+ }
418422 for {
419423 inx = l.firstSelectableItemBelow(inx)
420424 if inx == ItemNotFound {
421425 return nil
422426 }
423− item, ok := l.renderedItems[l.items[inx].ID()]
427+ item, ok := l.renderedItems.Get(l.items[inx].ID())
424428 if !ok {
425429 continue
426430 }
@@ -439,13 +443,16 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd {
439443 } else if itemMiddle > end {
440444 // select the first item in the viewport
441445 // 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)
447+ if !ok {
448+ return nil
449+ }
443450 for {
444451 inx = l.firstSelectableItemAbove(inx)
445452 if inx == ItemNotFound {
446453 return nil
447454 }
448− item, ok := l.renderedItems[l.items[inx].ID()]
455+ item, ok := l.renderedItems.Get(l.items[inx].ID())
449456 if !ok {
450457 continue
451458 }
@@ -512,10 +519,10 @@ func (l *list[T]) focusSelectedItem() tea.Cmd {
512519 if f, ok := any(item).(layout.Focusable); ok {
513520 if item.ID() == l.selectedItem && !f.IsFocused() {
514521 cmds = append(cmds, f.Focus())
515− delete(l.renderedItems, item.ID())
522+ l.renderedItems.Del(item.ID())
516523 } else if item.ID() != l.selectedItem && f.IsFocused() {
517524 cmds = append(cmds, f.Blur())
518− delete(l.renderedItems, item.ID())
525+ l.renderedItems.Del(item.ID())
519526 }
520527 }
521528 }
@@ -531,7 +538,7 @@ func (l *list[T]) blurSelectedItem() tea.Cmd {
531538 if f, ok := any(item).(layout.Focusable); ok {
532539 if item.ID() == l.selectedItem && f.IsFocused() {
533540 cmds = append(cmds, f.Blur())
534− delete(l.renderedItems, item.ID())
541+ l.renderedItems.Del(item.ID())
535542 }
536543 }
537544 }
@@ -555,13 +562,13 @@ func (l *list[T]) renderIterator(startInx int, limitHeight bool) int {
555562
556563 item := l.items[inx]
557564 var rItem renderedItem
558− if cache, ok := l.renderedItems[item.ID()]; ok {
565+ if cache, ok := l.renderedItems.Get(item.ID()); ok {
559566 rItem = cache
560567 } else {
561568 rItem = l.renderItem(item)
562569 rItem.start = currentContentHeight
563570 rItem.end = currentContentHeight + rItem.height - 1
564− l.renderedItems[item.ID()] = rItem
571+ l.renderedItems.Set(item.ID(), rItem)
565572 }
566573 gap := l.gap + 1
567574 if inx == len(l.items)-1 {
@@ -596,9 +603,9 @@ func (l *list[T]) AppendItem(item T) tea.Cmd {
596603 }
597604
598605 l.items = append(l.items, item)
599− l.indexMap = make(map[string]int)
606+ l.indexMap = csync.NewMap[string, int]()
600607 for inx, item := range l.items {
601− l.indexMap[item.ID()] = inx
608+ l.indexMap.Set(item.ID(), inx)
602609 }
603610 if l.width > 0 && l.height > 0 {
604611 cmd = item.SetSize(l.width, l.height)
@@ -617,12 +624,14 @@ func (l *list[T]) AppendItem(item T) tea.Cmd {
617624 cmds = append(cmds, cmd)
618625 }
619626 } else {
620− newItem := l.renderedItems[item.ID()]
621− newLines := newItem.height
622− if len(l.items) > 1 {
623− newLines += l.gap
627+ newItem, ok := l.renderedItems.Get(item.ID())
628+ if ok {
629+ newLines := newItem.height
630+ if len(l.items) > 1 {
631+ newLines += l.gap
632+ }
633+ l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
624634 }
625− l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
626635 }
627636 }
628637 return tea.Sequence(cmds...)
@@ -636,11 +645,14 @@ func (l *list[T]) Blur() tea.Cmd {
636645
637646// DeleteItem implements List.
638647func (l *list[T]) DeleteItem(id string) tea.Cmd {
639− inx := l.indexMap[id]
648+ inx, ok := l.indexMap.Get(id)
649+ if !ok {
650+ return nil
651+ }
640652 l.items = slices.Delete(l.items, inx, inx+1)
641− delete(l.renderedItems, id)
653+ l.renderedItems.Del(id)
642654 for inx, item := range l.items {
643− l.indexMap[item.ID()] = inx
655+ l.indexMap.Set(item.ID(), inx)
644656 }
645657
646658 if l.selectedItem == id {
@@ -753,9 +765,9 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
753765 item.Init(),
754766 }
755767 l.items = append([]T{item}, l.items...)
756− l.indexMap = make(map[string]int)
768+ l.indexMap = csync.NewMap[string, int]()
757769 for inx, item := range l.items {
758− l.indexMap[item.ID()] = inx
770+ l.indexMap.Set(item.ID(), inx)
759771 }
760772 if l.width > 0 && l.height > 0 {
761773 cmds = append(cmds, item.SetSize(l.width, l.height))
@@ -768,12 +780,14 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
768780 cmds = append(cmds, cmd)
769781 }
770782 } else {
771− newItem := l.renderedItems[item.ID()]
772− newLines := newItem.height
773− if len(l.items) > 1 {
774− newLines += l.gap
783+ newItem, ok := l.renderedItems.Get(item.ID())
784+ if ok {
785+ newLines := newItem.height
786+ if len(l.items) > 1 {
787+ newLines += l.gap
788+ }
789+ l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
775790 }
776− l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
777791 }
778792 }
779793 return tea.Batch(cmds...)
@@ -781,7 +795,7 @@ func (l *list[T]) PrependItem(item T) tea.Cmd {
781795
782796// SelectItemAbove implements List.
783797func (l *list[T]) SelectItemAbove() tea.Cmd {
784− inx, ok := l.indexMap[l.selectedItem]
798+ inx, ok := l.indexMap.Get(l.selectedItem)
785799 if !ok {
786800 return nil
787801 }
@@ -815,7 +829,7 @@ func (l *list[T]) SelectItemAbove() tea.Cmd {
815829
816830// SelectItemBelow implements List.
817831func (l *list[T]) SelectItemBelow() tea.Cmd {
818− inx, ok := l.indexMap[l.selectedItem]
832+ inx, ok := l.indexMap.Get(l.selectedItem)
819833 if !ok {
820834 return nil
821835 }
@@ -833,7 +847,7 @@ func (l *list[T]) SelectItemBelow() tea.Cmd {
833847
834848// SelectedItem implements List.
835849func (l *list[T]) SelectedItem() *T {
836− inx, ok := l.indexMap[l.selectedItem]
850+ inx, ok := l.indexMap.Get(l.selectedItem)
837851 if !ok {
838852 return nil
839853 }
@@ -869,10 +883,10 @@ func (l *list[T]) reset(selectedItem string) tea.Cmd {
869883 l.rendered = ""
870884 l.offset = 0
871885 l.selectedItem = selectedItem
872− l.indexMap = make(map[string]int)
873− l.renderedItems = make(map[string]renderedItem)
886+ l.indexMap = csync.NewMap[string, int]()
887+ l.renderedItems = csync.NewMap[string, renderedItem]()
874888 for inx, item := range l.items {
875− l.indexMap[item.ID()] = inx
889+ l.indexMap.Set(item.ID(), inx)
876890 if l.width > 0 && l.height > 0 {
877891 cmds = append(cmds, item.SetSize(l.width, l.height))
878892 }
@@ -896,22 +910,22 @@ func (l *list[T]) SetSize(width int, height int) tea.Cmd {
896910// UpdateItem implements List.
897911func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
898912 var cmds []tea.Cmd
899− if inx, ok := l.indexMap[id]; ok {
913+ if inx, ok := l.indexMap.Get(id); ok {
900914 l.items[inx] = item
901− oldItem := l.renderedItems[id]
915+ oldItem, hasOldItem := l.renderedItems.Get(id)
902916 oldPosition := l.offset
903917 if l.direction == DirectionBackward {
904918 oldPosition = (lipgloss.Height(l.rendered) - 1) - l.offset
905919 }
906920
907− delete(l.renderedItems, id)
921+ l.renderedItems.Del(id)
908922 cmd := l.render()
909923
910924 // need to check for nil because of sequence not handling nil
911925 if cmd != nil {
912926 cmds = append(cmds, cmd)
913927 }
914− if l.direction == DirectionBackward {
928+ if hasOldItem && l.direction == DirectionBackward {
915929 // if we are the last item and there is no offset
916930 // make sure to go to the bottom
917931 if inx == len(l.items)-1 && l.offset == 0 {
@@ -921,14 +935,18 @@ func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
921935 }
922936 // if the item is at least partially below the viewport
923937 } else if oldPosition < oldItem.end {
924− newItem := l.renderedItems[item.ID()]
938+ newItem, ok := l.renderedItems.Get(item.ID())
939+ if ok {
940+ newLines := newItem.height - oldItem.height
941+ l.offset = util.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
942+ }
943+ }
944+ } else if hasOldItem && l.offset > oldItem.start {
945+ newItem, ok := l.renderedItems.Get(item.ID())
946+ if ok {
925947 newLines := newItem.height - oldItem.height
926948 l.offset = util.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
927949 }
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)
932950 }
933951 }
934952 return tea.Sequence(cmds...)
internal/tui/exp/list/list_test.go+32−22
@@ -29,17 +29,19 @@ func TestList(t *testing.T) {
2929 // should select the last item
3030 assert.Equal(t, items[0].ID(), l.selectedItem)
3131 assert.Equal(t, 0, l.offset)
32− require.Len(t, l.indexMap, 5)
32+ require.Equal(t, 5, l.indexMap.Len())
3333 require.Len(t, l.items, 5)
34− require.Len(t, l.renderedItems, 5)
34+ require.Equal(t, 5, l.renderedItems.Len())
3535 assert.Equal(t, 5, lipgloss.Height(l.rendered))
3636 assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
3737 start, end := l.viewPosition()
3838 assert.Equal(t, 0, start)
3939 assert.Equal(t, 4, end)
4040 for i := range 5 {
41− assert.Equal(t, i, l.renderedItems[items[i].ID()].start)
42− assert.Equal(t, i, l.renderedItems[items[i].ID()].end)
41+ item, ok := l.renderedItems.Get(items[i].ID())
42+ require.True(t, ok)
43+ assert.Equal(t, i, item.start)
44+ assert.Equal(t, i, item.end)
4345 }
4446
4547 golden.RequireEqual(t, []byte(l.View()))
@@ -57,17 +59,19 @@ func TestList(t *testing.T) {
5759 // should select the last item
5860 assert.Equal(t, items[4].ID(), l.selectedItem)
5961 assert.Equal(t, 0, l.offset)
60− require.Len(t, l.indexMap, 5)
62+ require.Equal(t, 5, l.indexMap.Len())
6163 require.Len(t, l.items, 5)
62− require.Len(t, l.renderedItems, 5)
64+ require.Equal(t, 5, l.renderedItems.Len())
6365 assert.Equal(t, 5, lipgloss.Height(l.rendered))
6466 assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
6567 start, end := l.viewPosition()
6668 assert.Equal(t, 0, start)
6769 assert.Equal(t, 4, end)
6870 for i := range 5 {
69− assert.Equal(t, i, l.renderedItems[items[i].ID()].start)
70− assert.Equal(t, i, l.renderedItems[items[i].ID()].end)
71+ item, ok := l.renderedItems.Get(items[i].ID())
72+ require.True(t, ok)
73+ assert.Equal(t, i, item.start)
74+ assert.Equal(t, i, item.end)
7175 }
7276
7377 golden.RequireEqual(t, []byte(l.View()))
@@ -86,17 +90,19 @@ func TestList(t *testing.T) {
8690 // should select the last item
8791 assert.Equal(t, items[0].ID(), l.selectedItem)
8892 assert.Equal(t, 0, l.offset)
89− require.Len(t, l.indexMap, 30)
93+ require.Equal(t, 30, l.indexMap.Len())
9094 require.Len(t, l.items, 30)
91− require.Len(t, l.renderedItems, 30)
95+ require.Equal(t, 30, l.renderedItems.Len())
9296 assert.Equal(t, 30, lipgloss.Height(l.rendered))
9397 assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
9498 start, end := l.viewPosition()
9599 assert.Equal(t, 0, start)
96100 assert.Equal(t, 9, end)
97101 for i := range 30 {
98− assert.Equal(t, i, l.renderedItems[items[i].ID()].start)
99− assert.Equal(t, i, l.renderedItems[items[i].ID()].end)
102+ item, ok := l.renderedItems.Get(items[i].ID())
103+ require.True(t, ok)
104+ assert.Equal(t, i, item.start)
105+ assert.Equal(t, i, item.end)
100106 }
101107
102108 golden.RequireEqual(t, []byte(l.View()))
@@ -114,17 +120,19 @@ func TestList(t *testing.T) {
114120 // should select the last item
115121 assert.Equal(t, items[29].ID(), l.selectedItem)
116122 assert.Equal(t, 0, l.offset)
117− require.Len(t, l.indexMap, 30)
123+ require.Equal(t, 30, l.indexMap.Len())
118124 require.Len(t, l.items, 30)
119− require.Len(t, l.renderedItems, 30)
125+ require.Equal(t, 30, l.renderedItems.Len())
120126 assert.Equal(t, 30, lipgloss.Height(l.rendered))
121127 assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline")
122128 start, end := l.viewPosition()
123129 assert.Equal(t, 20, start)
124130 assert.Equal(t, 29, end)
125131 for i := range 30 {
126− assert.Equal(t, i, l.renderedItems[items[i].ID()].start)
127− assert.Equal(t, i, l.renderedItems[items[i].ID()].end)
132+ item, ok := l.renderedItems.Get(items[i].ID())
133+ require.True(t, ok)
134+ assert.Equal(t, i, item.start)
135+ assert.Equal(t, i, item.end)
128136 }
129137
130138 golden.RequireEqual(t, []byte(l.View()))
@@ -145,9 +153,9 @@ func TestList(t *testing.T) {
145153 // should select the last item
146154 assert.Equal(t, items[0].ID(), l.selectedItem)
147155 assert.Equal(t, 0, l.offset)
148− require.Len(t, l.indexMap, 30)
156+ require.Equal(t, 30, l.indexMap.Len())
149157 require.Len(t, l.items, 30)
150− require.Len(t, l.renderedItems, 30)
158+ require.Equal(t, 30, l.renderedItems.Len())
151159 expectedLines := 0
152160 for i := range 30 {
153161 expectedLines += (i + 1) * 1
@@ -159,7 +167,8 @@ func TestList(t *testing.T) {
159167 assert.Equal(t, 9, end)
160168 currentPosition := 0
161169 for i := range 30 {
162− rItem := l.renderedItems[items[i].ID()]
170+ rItem, ok := l.renderedItems.Get(items[i].ID())
171+ require.True(t, ok)
163172 assert.Equal(t, currentPosition, rItem.start)
164173 assert.Equal(t, currentPosition+i, rItem.end)
165174 currentPosition += i + 1
@@ -182,9 +191,9 @@ func TestList(t *testing.T) {
182191 // should select the last item
183192 assert.Equal(t, items[29].ID(), l.selectedItem)
184193 assert.Equal(t, 0, l.offset)
185− require.Len(t, l.indexMap, 30)
194+ require.Equal(t, 30, l.indexMap.Len())
186195 require.Len(t, l.items, 30)
187− require.Len(t, l.renderedItems, 30)
196+ require.Equal(t, 30, l.renderedItems.Len())
188197 expectedLines := 0
189198 for i := range 30 {
190199 expectedLines += (i + 1) * 1
@@ -196,7 +205,8 @@ func TestList(t *testing.T) {
196205 assert.Equal(t, expectedLines-1, end)
197206 currentPosition := 0
198207 for i := range 30 {
199− rItem := l.renderedItems[items[i].ID()]
208+ rItem, ok := l.renderedItems.Get(items[i].ID())
209+ require.True(t, ok)
200210 assert.Equal(t, currentPosition, rItem.start)
201211 assert.Equal(t, currentPosition+i, rItem.end)
202212 currentPosition += i + 1
internal/tui/exp/list/testdata/TestFilterableList/should_create_simple_filterable_list.golden+9−5
@@ -1,6 +1,10 @@
11> Type to filter 
2−│Item 0
3−Item 1
4−Item 2
5−Item 3
6−Item 4
2+│Item 0 
3+Item 1 
4+Item 2 
5+Item 3 
6+Item 4 
7+
8+
9+
10+
internal/tui/exp/list/testdata/TestList/should_go_to_selected_item_at_the_beginning.golden+10−10
@@ -1,10 +1,10 @@
1−│Item 10
2−│Item 10
3−│Item 10
4−│Item 10
5−│Item 10
6−│Item 10
7−│Item 10
8−│Item 10
9−│Item 10
10−│Item 10
1+│Item 10
2+│Item 10
3+│Item 10
4+│Item 10
5+│Item 10
6+│Item 10
7+│Item 10
8+│Item 10
9+│Item 10
10+│Item 10
internal/tui/exp/list/testdata/TestList/should_go_to_selected_item_at_the_beginning_backwards.golden+10−10
@@ -1,10 +1,10 @@
1−│Item 10
2−│Item 10
3−│Item 10
4−│Item 10
5−│Item 10
6−│Item 10
7−│Item 10
8−│Item 10
9−│Item 10
10−│Item 10
1+│Item 10
2+│Item 10
3+│Item 10
4+│Item 10
5+│Item 10
6+│Item 10
7+│Item 10
8+│Item 10
9+│Item 10
10+│Item 10
internal/tui/exp/list/testdata/TestList/should_have_correct_positions_in_list_that_does_not_fits_the_items.golden+10−10
@@ -1,10 +1,10 @@
1−│Item 0
2−Item 1
3−Item 2
4−Item 3
5−Item 4
6−Item 5
7−Item 6
8−Item 7
9−Item 8
10−Item 9
1+│Item 0
2+Item 1
3+Item 2
4+Item 3
5+Item 4
6+Item 5
7+Item 6
8+Item 7
9+Item 8
10+Item 9
internal/tui/exp/list/testdata/TestList/should_have_correct_positions_in_list_that_does_not_fits_the_items_and_has_multi_line_items.golden+10−10
@@ -1,10 +1,10 @@
1−│Item 0
2−Item 1
3−Item 1
4−Item 2
5−Item 2
6−Item 2
7−Item 3
8−Item 3
9−Item 3
10−Item 3
1+│Item 0
2+Item 1
3+Item 1
4+Item 2
5+Item 2
6+Item 2
7+Item 3
8+Item 3
9+Item 3
10+Item 3
internal/tui/exp/list/testdata/TestList/should_have_correct_positions_in_list_that_does_not_fits_the_items_and_has_multi_line_items_backwards.golden+10−10
@@ -1,10 +1,10 @@
1−│Item 29
2−│Item 29
3−│Item 29
4−│Item 29
5−│Item 29
6−│Item 29
7−│Item 29
8−│Item 29
9−│Item 29
10−│Item 29
1+│Item 29
2+│Item 29
3+│Item 29
4+│Item 29
5+│Item 29
6+│Item 29
7+│Item 29
8+│Item 29
9+│Item 29
10+│Item 29
internal/tui/exp/list/testdata/TestList/should_have_correct_positions_in_list_that_does_not_fits_the_items_backwards.golden+10−10
@@ -1,10 +1,10 @@
1−Item 20
2−Item 21
3−Item 22
4−Item 23
5−Item 24
6−Item 25
7−Item 26
8−Item 27
9−Item 28
10−│Item 29
1+Item 20
2+Item 21
3+Item 22
4+Item 23
5+Item 24
6+Item 25
7+Item 26
8+Item 27
9+Item 28
10+│Item 29
internal/tui/exp/list/testdata/TestList/should_have_correct_positions_in_list_that_fits_the_items.golden+20−5
@@ -1,5 +1,20 @@
1−│Item 0
2−Item 1
3−Item 2
4−Item 3
5−Item 4
1+│Item 0
2+Item 1
3+Item 2
4+Item 3
5+Item 4
6+
7+
8+
9+
10+
11+
12+
13+
14+
15+
16+
17+
18+
19+
20+
internal/tui/exp/list/testdata/TestList/should_have_correct_positions_in_list_that_fits_the_items_backwards.golden+20−5
@@ -1,5 +1,20 @@
1−Item 0
2−Item 1
3−Item 2
4−Item 3
5−│Item 4
1+Item 0
2+Item 1
3+Item 2
4+Item 3
5+│Item 4
6+
7+
8+
9+
10+
11+
12+
13+
14+
15+
16+
17+
18+
19+
20+
internal/tui/exp/list/testdata/TestListMovement/should_move_viewport_down.golden+10−10
@@ -1,10 +1,10 @@
1−Item 6
2−Item 6
3−Item 6
4−│Item 7
5−│Item 7
6−│Item 7
7−│Item 7
8−│Item 7
9−│Item 7
10−│Item 7
1+Item 6
2+Item 6
3+Item 6
4+│Item 7
5+│Item 7
6+│Item 7
7+│Item 7
8+│Item 7
9+│Item 7
10+│Item 7
internal/tui/exp/list/testdata/TestListMovement/should_move_viewport_down_and_up.golden+10−10
@@ -1,10 +1,10 @@
1−Item 0
2−Item 1
3−Item 1
4−Item 2
5−Item 2
6−Item 2
7−│Item 3
8−│Item 3
9−│Item 3
10−│Item 3
1+Item 0
2+Item 1
3+Item 1
4+Item 2
5+Item 2
6+Item 2
7+│Item 3
8+│Item 3
9+│Item 3
10+│Item 3
internal/tui/exp/list/testdata/TestListMovement/should_move_viewport_up.golden+10−10
@@ -1,10 +1,10 @@
1−│Item 28
2−│Item 28
3−│Item 28
4−│Item 28
5−│Item 28
6−Item 29
7−Item 29
8−Item 29
9−Item 29
10−Item 29
1+│Item 28
2+│Item 28
3+│Item 28
4+│Item 28
5+│Item 28
6+Item 29
7+Item 29
8+Item 29
9+Item 29
10+Item 29
internal/tui/exp/list/testdata/TestListMovement/should_move_viewport_up_and_down.golden+10−10
@@ -1,10 +1,10 @@
1−│Item 29
2−│Item 29
3−│Item 29
4−│Item 29
5−│Item 29
6−│Item 29
7−│Item 29
8−│Item 29
9−│Item 29
10−│Item 29
1+│Item 29
2+│Item 29
3+│Item 29
4+│Item 29
5+│Item 29
6+│Item 29
7+│Item 29
8+│Item 29
9+│Item 29
10+│Item 29
internal/tui/exp/list/testdata/TestListMovement/should_not_change_offset_when_new_items_are_appended_and_we_are_at_the_bottom_in_backwards_list.golden+10−10
@@ -1,10 +1,10 @@
1−Item 29
2−Item 29
3−Item 29
4−Item 29
5−Item 29
6−Item 29
7−Item 29
8−Item 29
9−Item 29
10−│Testing
1+Item 29
2+Item 29
3+Item 29
4+Item 29
5+Item 29
6+Item 29
7+Item 29
8+Item 29
9+Item 29
10+│Testing 
internal/tui/exp/list/testdata/TestListMovement/should_not_change_offset_when_new_items_are_prepended_and_we_are_at_the_top_in_forward_list.golden+10−10
@@ -1,10 +1,10 @@
1−│Testing
2−Item 0
3−Item 1
4−Item 1
5−Item 2
6−Item 2
7−Item 2
8−Item 3
9−Item 3
10−Item 3
1+│Testing 
2+Item 0
3+Item 1
4+Item 1
5+Item 2
6+Item 2
7+Item 2
8+Item 3
9+Item 3
10+Item 3
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_if_an_item_is_appended_and_we_are_in_forward_list.golden+10−10
@@ -1,10 +1,10 @@
1−│Item 2
2−Item 3
3−Item 4
4−Item 5
5−Item 6
6−Item 7
7−Item 8
8−Item 9
9−Item 10
10−Item 11
1+│Item 2
2+Item 3
3+Item 4
4+Item 5
5+Item 6
6+Item 7
7+Item 8
8+Item 9
9+Item 10
10+Item 11
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_if_an_item_is_prepended_and_we_are_in_backwards_list.golden+10−10
@@ -1,10 +1,10 @@
1−Item 18
2−Item 19
3−Item 20
4−Item 21
5−Item 22
6−Item 23
7−Item 24
8−Item 25
9−Item 26
10−│Item 27
1+Item 18
2+Item 19
3+Item 20
4+Item 21
5+Item 22
6+Item 23
7+Item 24
8+Item 25
9+Item 26
10+│Item 27
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_new_items_are_added_but_we_moved_down_in_forward_list.golden+10−10
@@ -1,10 +1,10 @@
1−│Item 2
2−Item 3
3−Item 4
4−Item 5
5−Item 6
6−Item 7
7−Item 8
8−Item 9
9−Item 10
10−Item 11
1+│Item 2
2+Item 3
3+Item 4
4+Item 5
5+Item 6
6+Item 7
7+Item 8
8+Item 9
9+Item 10
10+Item 11
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_new_items_are_added_but_we_moved_up_in_backwards_list.golden+10−10
@@ -1,10 +1,10 @@
1−Item 18
2−Item 19
3−Item 20
4−Item 21
5−Item 22
6−Item 23
7−Item 24
8−Item 25
9−Item 26
10−│Item 27
1+Item 18
2+Item 19
3+Item 20
4+Item 21
5+Item 22
6+Item 23
7+Item 24
8+Item 25
9+Item 26
10+│Item 27
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_above_is_decreases_in_forward_list.golden+10−10
@@ -1,10 +1,10 @@
1−│Item 0
2−Item 1
3−Item 2
4−Item 3
5−Item 4
6−Item 5
7−Item 6
8−Item 7
9−Item 8
10−Item 9
1+│Item 0
2+Item 1
3+Item 2
4+Item 3
5+Item 4
6+Item 5
7+Item 6
8+Item 7
9+Item 8
10+Item 9
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_above_is_increased_in_backwards_list.golden+10−10
@@ -1,10 +1,10 @@
1−Item 18
2−Item 19
3−Item 20
4−Item 21
5−Item 22
6−Item 23
7−Item 24
8−Item 25
9−Item 26
10−│Item 27
1+Item 18
2+Item 19
3+Item 20
4+Item 21
5+Item 22
6+Item 23
7+Item 24
8+Item 25
9+Item 26
10+│Item 27
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_above_is_increased_in_forward_list.golden+10−10
@@ -1,10 +1,10 @@
1−│Item 2
2−Item 3
3−Item 4
4−Item 5
5−Item 6
6−Item 7
7−Item 8
8−Item 9
9−Item 10
10−Item 11
1+│Item 2
2+Item 3
3+Item 4
4+Item 5
5+Item 6
6+Item 7
7+Item 8
8+Item 9
9+Item 10
10+Item 11
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_below_is_decreases_in_backwards_list.golden+10−10
@@ -1,10 +1,10 @@
1−Item 21
2−Item 22
3−Item 23
4−Item 24
5−Item 25
6−Item 26
7−Item 27
8−Item 28
9−│Item 29
10−Item 30
1+Item 21
2+Item 22
3+Item 23
4+Item 24
5+Item 25
6+Item 26
7+Item 27
8+Item 28
9+│Item 29
10+Item 30
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_below_is_increased_in_backwards_list.golden+10−10
@@ -1,10 +1,10 @@
1−Item 18
2−Item 19
3−Item 20
4−Item 21
5−Item 22
6−Item 23
7−Item 24
8−Item 25
9−Item 26
10−│Item 27
1+Item 18
2+Item 19
3+Item 20
4+Item 21
5+Item 22
6+Item 23
7+Item 24
8+Item 25
9+Item 26
10+│Item 27
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_below_is_increased_in_forward_list.golden+10−10
@@ -1,10 +1,10 @@
1−│Item 2
2−Item 3
3−Item 4
4−Item 5
5−Item 6
6−Item 7
7−Item 8
8−Item 9
9−Item 10
10−Item 11
1+│Item 2
2+Item 3
3+Item 4
4+Item 5
5+Item 6
6+Item 7
7+Item 8
8+Item 9
9+Item 10
10+Item 11