From 3d4c31bf02c9155129772dab86aac66a70a6e09e Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Wed, 20 Aug 2025 21:20:36 +0200 Subject: [PATCH] fix: change index to int --- internal/tui/exp/list/grouped.go | 3 +- internal/tui/exp/list/list.go | 326 +++++++++++++++++------------ internal/tui/exp/list/list_test.go | 82 ++++---- 3 files changed, 240 insertions(+), 171 deletions(-) diff --git a/internal/tui/exp/list/grouped.go b/internal/tui/exp/list/grouped.go index cb54628a70e84cb80eeb162a0d9f836f14271641..0a538ea4c0d23fd0997a9d65ae02322fbe781858 100644 --- a/internal/tui/exp/list/grouped.go +++ b/internal/tui/exp/list/grouped.go @@ -42,7 +42,8 @@ func NewGroupedList[T Item](groups []Group[T], opts ...ListOption) GroupedList[T }, items: csync.NewSlice[Item](), indexMap: csync.NewMap[string, int](), - renderedItems: csync.NewMap[string, renderedItem](), + viewCache: csync.NewMap[string, string](), + itemPositions: nil, } for _, opt := range opts { opt(list.confOptions) diff --git a/internal/tui/exp/list/list.go b/internal/tui/exp/list/list.go index 65db55cff5fa886cee31313ba456b395035c3b6e..7c47e754bb900b87822d16eb430c44f658b8602b 100644 --- a/internal/tui/exp/list/list.go +++ b/internal/tui/exp/list/list.go @@ -71,9 +71,7 @@ const ( ViewportDefaultScrollSize = 2 ) -type renderedItem struct { - id string - view string +type itemPosition struct { height int start int end int @@ -100,15 +98,14 @@ type list[T Item] struct { indexMap *csync.Map[string, int] items *csync.Slice[T] - renderedItems *csync.Map[string, renderedItem] + // Virtual scrolling fields - using slices for O(1) index access + itemPositions []itemPosition // Position info for each item by index + virtualHeight int // Total height of all items + viewCache *csync.Map[string, string] // Optional cache for rendered views renderMu sync.Mutex rendered string - // Virtual scrolling fields - virtualHeight int // Total height of all items - itemHeights *csync.Map[string, int] // Cache of item heights - movingByItem bool selectionStartCol int selectionStartLine int @@ -195,8 +192,8 @@ func New[T Item](items []T, opts ...ListOption) List[T] { }, items: csync.NewSliceFrom(items), indexMap: csync.NewMap[string, int](), - renderedItems: csync.NewMap[string, renderedItem](), - itemHeights: csync.NewMap[string, int](), + itemPositions: make([]itemPosition, len(items)), + viewCache: csync.NewMap[string, string](), selectionStartCol: -1, selectionStartLine: -1, selectionEndLine: -1, @@ -231,16 +228,19 @@ func (l *list[T]) Init() tea.Cmd { } } + // Calculate positions for all items + l.calculateItemPositions() + // For backward lists, we need to position at the bottom after initial render if l.direction == DirectionBackward && l.offset == 0 && l.items.Len() > 0 { - // Calculate positions first - l.calculateItemPositions() // Set offset to show the bottom of the list if l.virtualHeight > l.height { l.offset = 0 // In backward mode, offset 0 means bottom } - // Select the last item - l.selectLastItem() + // Select the last item if no item is selected + if l.selectedItem == "" { + l.selectLastItem() + } } // Scroll to the selected item for initial positioning @@ -568,12 +568,18 @@ func (l *list[T]) setDefaultSelected() { } func (l *list[T]) scrollToSelection() { - rItem, ok := l.renderedItems.Get(l.selectedItem) - if !ok { + if l.selectedItem == "" { + return + } + + inx, ok := l.indexMap.Get(l.selectedItem) + if !ok || inx < 0 || inx >= len(l.itemPositions) { l.selectedItem = "" l.setDefaultSelected() return } + + rItem := l.itemPositions[inx] start, end := l.viewPosition() @@ -633,10 +639,12 @@ func (l *list[T]) scrollToSelection() { } func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd { - rItem, ok := l.renderedItems.Get(l.selectedItem) - if !ok { + inx, ok := l.indexMap.Get(l.selectedItem) + if !ok || inx < 0 || inx >= len(l.itemPositions) { return nil } + + rItem := l.itemPositions[inx] start, end := l.viewPosition() // item bigger than the viewport do nothing if rItem.start <= start && rItem.end >= end { @@ -652,10 +660,6 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd { if itemMiddle < start { // select the first item in the viewport // the item is most likely an item coming after this item - inx, ok := l.indexMap.Get(rItem.id) - if !ok { - return nil - } for { inx = l.firstSelectableItemBelow(inx) if inx == ItemNotFound { @@ -665,29 +669,25 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd { if !ok { continue } - renderedItem, ok := l.renderedItems.Get(item.ID()) - if !ok { + if inx >= len(l.itemPositions) { continue } + renderedItem := l.itemPositions[inx] // If the item is bigger than the viewport, select it if renderedItem.start <= start && renderedItem.end >= end { - l.selectedItem = renderedItem.id + l.selectedItem = item.ID() return l.render() } // item is in the view if renderedItem.start >= start && renderedItem.start <= end { - l.selectedItem = renderedItem.id + l.selectedItem = item.ID() return l.render() } } } else if itemMiddle > end { // select the first item in the viewport // the item is most likely an item coming after this item - inx, ok := l.indexMap.Get(rItem.id) - if !ok { - return nil - } for { inx = l.firstSelectableItemAbove(inx) if inx == ItemNotFound { @@ -697,19 +697,19 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd { if !ok { continue } - renderedItem, ok := l.renderedItems.Get(item.ID()) - if !ok { + if inx >= len(l.itemPositions) { continue } + renderedItem := l.itemPositions[inx] // If the item is bigger than the viewport, select it if renderedItem.start <= start && renderedItem.end >= end { - l.selectedItem = renderedItem.id + l.selectedItem = item.ID() return l.render() } // item is in the view if renderedItem.end >= start && renderedItem.end <= end { - l.selectedItem = renderedItem.id + l.selectedItem = item.ID() return l.render() } } @@ -779,10 +779,10 @@ func (l *list[T]) focusSelectedItem() tea.Cmd { if f, ok := any(item).(layout.Focusable); ok { if item.ID() == l.selectedItem && !f.IsFocused() { cmds = append(cmds, f.Focus()) - l.renderedItems.Del(item.ID()) + l.viewCache.Del(item.ID()) } else if item.ID() != l.selectedItem && f.IsFocused() { cmds = append(cmds, f.Blur()) - l.renderedItems.Del(item.ID()) + l.viewCache.Del(item.ID()) } } } @@ -798,7 +798,7 @@ func (l *list[T]) blurSelectedItem() tea.Cmd { if f, ok := any(item).(layout.Focusable); ok { if item.ID() == l.selectedItem && f.IsFocused() { cmds = append(cmds, f.Blur()) - l.renderedItems.Del(item.ID()) + l.viewCache.Del(item.ID()) } } } @@ -808,10 +808,16 @@ func (l *list[T]) blurSelectedItem() tea.Cmd { // calculateItemPositions calculates and caches the position and height of all items. +// This is O(n) but only called when the list structure changes significantly. func (l *list[T]) calculateItemPositions() { - currentHeight := 0 itemsLen := l.items.Len() - + + // Resize positions slice if needed + if len(l.itemPositions) != itemsLen { + l.itemPositions = make([]itemPosition, itemsLen) + } + + currentHeight := 0 // Always calculate positions in forward order (logical positions) for i := 0; i < itemsLen; i++ { item, ok := l.items.Get(i) @@ -819,34 +825,24 @@ func (l *list[T]) calculateItemPositions() { continue } - // Get or calculate item height - var height int - if cachedHeight, ok := l.itemHeights.Get(item.ID()); ok { - height = cachedHeight + // Get cached view or render new one + var view string + if cached, ok := l.viewCache.Get(item.ID()); ok { + view = cached } else { - // Calculate and cache the height - view := item.View() - height = lipgloss.Height(view) - l.itemHeights.Set(item.ID(), height) + view = item.View() + l.viewCache.Set(item.ID(), view) } - - // Update or create rendered item with position info - var rItem renderedItem - if cached, ok := l.renderedItems.Get(item.ID()); ok { - rItem = cached - rItem.height = height - } else { - rItem = renderedItem{ - id: item.ID(), - height: height, - } + + height := lipgloss.Height(view) + + l.itemPositions[i] = itemPosition{ + height: height, + start: currentHeight, + end: currentHeight + height - 1, } - rItem.start = currentHeight - rItem.end = currentHeight + rItem.height - 1 - l.renderedItems.Set(item.ID(), rItem) - - currentHeight = rItem.end + 1 + currentHeight += height if i < itemsLen-1 { currentHeight += l.gap } @@ -855,6 +851,58 @@ func (l *list[T]) calculateItemPositions() { l.virtualHeight = currentHeight } +// updateItemPosition updates a single item's position and adjusts subsequent items. +// This is O(n) in worst case but only for items after the changed one. +func (l *list[T]) updateItemPosition(index int) { + itemsLen := l.items.Len() + if index < 0 || index >= itemsLen { + return + } + + item, ok := l.items.Get(index) + if !ok { + return + } + + // Get new height + view := item.View() + l.viewCache.Set(item.ID(), view) + newHeight := lipgloss.Height(view) + + // If height hasn't changed, no need to update + if index < len(l.itemPositions) && l.itemPositions[index].height == newHeight { + return + } + + // Calculate starting position (from previous item or 0) + var startPos int + if index > 0 { + startPos = l.itemPositions[index-1].end + 1 + l.gap + } + + // Update this item + oldHeight := 0 + if index < len(l.itemPositions) { + oldHeight = l.itemPositions[index].height + } + heightDiff := newHeight - oldHeight + + l.itemPositions[index] = itemPosition{ + height: newHeight, + start: startPos, + end: startPos + newHeight - 1, + } + + // Update all subsequent items' positions (shift by heightDiff) + for i := index + 1; i < len(l.itemPositions); i++ { + l.itemPositions[i].start += heightDiff + l.itemPositions[i].end += heightDiff + } + + // Update total height + l.virtualHeight += heightDiff +} + // renderVirtualScrolling renders only the visible portion of the list. func (l *list[T]) renderVirtualScrolling() string { if l.items.Len() == 0 { @@ -874,8 +922,8 @@ func (l *list[T]) renderVirtualScrolling() string { return strings.Join(lines, "\n") } - // Debug: Check if we have any rendered items - if l.renderedItems.Len() == 0 { + // Check if we have any positions calculated + if len(l.itemPositions) == 0 { // No items have been calculated yet, return empty var lines []string for i := 0; i < l.height; i++ { @@ -886,9 +934,9 @@ func (l *list[T]) renderVirtualScrolling() string { // Find which items are visible var visibleItems []struct { - item T - rItem renderedItem - index int + item T + pos itemPosition + index int } itemsLen := l.items.Len() @@ -898,23 +946,24 @@ func (l *list[T]) renderVirtualScrolling() string { continue } - rItem, ok := l.renderedItems.Get(item.ID()) - if !ok { + if i >= len(l.itemPositions) { // Item not yet calculated, skip it continue } + pos := l.itemPositions[i] + // Check if item is visible (overlaps with viewport) - if rItem.end >= viewStart && rItem.start <= viewEnd { + if pos.end >= viewStart && pos.start <= viewEnd { visibleItems = append(visibleItems, struct { item T - rItem renderedItem + pos itemPosition index int - }{item, rItem, i}) + }{item, pos, i}) } // Early exit if we've passed the viewport - if rItem.start > viewEnd { + if pos.start > viewEnd { break } } @@ -935,25 +984,24 @@ func (l *list[T]) renderVirtualScrolling() string { // Handle first visible item firstVisible := visibleItems[0] - if firstVisible.rItem.start < viewStart { + if firstVisible.pos.start < viewStart { // We're starting mid-item, render partial - if cached, ok := l.renderedItems.Get(firstVisible.item.ID()); ok && cached.view != "" { - lines := strings.Split(cached.view, "\n") - skipLines := viewStart - firstVisible.rItem.start + if cached, ok := l.viewCache.Get(firstVisible.item.ID()); ok && cached != "" { + lines := strings.Split(cached, "\n") + skipLines := viewStart - firstVisible.pos.start if skipLines >= 0 && skipLines < len(lines) { - for i := skipLines; i < len(lines); i++ { - if currentLine > viewEnd { - break + for i := skipLines; i < len(lines) && currentLine <= viewEnd; i++ { + if b.Len() > 0 { + b.WriteByte('\n') } b.WriteString(lines[i]) - b.WriteByte('\n') currentLine++ } } } - } else if firstVisible.rItem.start > viewStart { + } else if firstVisible.pos.start > viewStart { // Add empty lines before first item - for currentLine < firstVisible.rItem.start && currentLine <= viewEnd { + for currentLine < firstVisible.pos.start && currentLine <= viewEnd { if b.Len() > 0 { b.WriteByte('\n') } @@ -968,28 +1016,33 @@ func (l *list[T]) renderVirtualScrolling() string { } // Skip first item if we already rendered it partially - if i == 0 && firstVisible.rItem.start < viewStart { + if i == 0 && firstVisible.pos.start < viewStart { + // Update currentLine to where we left off after partial rendering + currentLine = viewStart + (firstVisible.pos.end - firstVisible.pos.start + 1) - (viewStart - firstVisible.pos.start) continue } - // Add gap before item (except for first) - if i > 0 && currentLine <= viewEnd { - for j := 0; j < l.gap && currentLine <= viewEnd; j++ { - b.WriteByte('\n') - currentLine++ + // Add gap before item (except for first visible item in viewport) + if i > 0 || (i == 0 && firstVisible.pos.start >= viewStart) { + // Only add gap if this isn't the very first item in the viewport + if currentLine > viewStart && currentLine <= viewEnd { + for j := 0; j < l.gap && currentLine <= viewEnd; j++ { + if b.Len() > 0 { + b.WriteByte('\n') + } + currentLine++ + } } } // Render item or use cache var view string - if cached, ok := l.renderedItems.Get(vis.item.ID()); ok && cached.view != "" { - view = cached.view + if cached, ok := l.viewCache.Get(vis.item.ID()); ok && cached != "" { + view = cached } else { view = vis.item.View() // Update cache - rItem := vis.rItem - rItem.view = view - l.renderedItems.Set(vis.item.ID(), rItem) + l.viewCache.Set(vis.item.ID(), view) } // Handle partial rendering if item extends beyond viewport @@ -1049,8 +1102,10 @@ func (l *list[T]) AppendItem(item T) tea.Cmd { cmds = append(cmds, cmd) } } else { - newItem, ok := l.renderedItems.Get(item.ID()) - if ok { + // Get the new item's position to adjust offset + newInx := l.items.Len() - 1 + if newInx < len(l.itemPositions) { + newItem := l.itemPositions[newInx] newLines := newItem.height if l.items.Len() > 1 { newLines += l.gap @@ -1077,7 +1132,9 @@ func (l *list[T]) DeleteItem(id string) tea.Cmd { return nil } l.items.Delete(inx) - l.renderedItems.Del(id) + l.viewCache.Del(id) + // Rebuild index map + l.indexMap = csync.NewMap[string, int]() for inx, item := range slices.Collect(l.items.Seq()) { l.indexMap.Set(item.ID(), inx) } @@ -1258,8 +1315,10 @@ func (l *list[T]) PrependItem(item T) tea.Cmd { cmds = append(cmds, cmd) } } else { - newItem, ok := l.renderedItems.Get(item.ID()) - if ok { + // Get the new item's position to adjust offset + newInx := l.items.Len() - 1 + if newInx < len(l.itemPositions) { + newItem := l.itemPositions[newInx] newLines := newItem.height if l.items.Len() > 1 { newLines += l.gap @@ -1372,8 +1431,8 @@ func (l *list[T]) reset(selectedItem string) tea.Cmd { l.offset = 0 l.selectedItem = selectedItem l.indexMap = csync.NewMap[string, int]() - l.renderedItems = csync.NewMap[string, renderedItem]() - l.itemHeights = csync.NewMap[string, int]() + l.viewCache = csync.NewMap[string, string]() + l.itemPositions = nil // Will be recalculated l.virtualHeight = 0 for inx, item := range slices.Collect(l.items.Seq()) { l.indexMap.Set(item.ID(), inx) @@ -1401,20 +1460,28 @@ 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.Set(inx, item) - oldItem, hasOldItem := l.renderedItems.Get(id) + // Store old height if we have it + var oldHeight int + hasOldItem := false + if inx < len(l.itemPositions) { + oldHeight = l.itemPositions[inx].height + hasOldItem = true + } + oldPosition := l.offset if l.direction == DirectionBackward { if l.virtualHeight > 0 { - oldPosition = (l.virtualHeight - 1) - l.offset - } else { - oldPosition = 0 - } + oldPosition = (l.virtualHeight - 1) - l.offset + } else { + oldPosition = 0 + } } - // Clear caches for this item - l.renderedItems.Del(id) - l.itemHeights.Del(id) + // Update the item + l.items.Set(inx, item) + + // Clear cache for this item + l.viewCache.Del(id) cmd := l.render() @@ -1422,28 +1489,23 @@ func (l *list[T]) UpdateItem(id string, item T) tea.Cmd { if cmd != nil { cmds = append(cmds, 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 oldPosition < oldItem.end { - newItem, ok := l.renderedItems.Get(item.ID()) - if ok { - newLines := newItem.height - oldItem.height - if l.virtualHeight > 0 { - l.offset = util.Clamp(l.offset+newLines, 0, l.virtualHeight-1) - } else { - l.offset = 0 - } + + // Adjust offset if needed based on height change + if hasOldItem && inx < len(l.itemPositions) { + newHeight := l.itemPositions[inx].height + diff := newHeight - oldHeight + + if l.direction == DirectionBackward { + // if we are the last item and there is no offset + // make sure to go to the bottom + if oldPosition < l.itemPositions[inx].end { + if diff != 0 && l.virtualHeight > 0 { + l.offset = util.Clamp(l.offset+diff, 0, l.virtualHeight-1) + } } - } - } else if hasOldItem && l.offset > oldItem.start { - newItem, ok := l.renderedItems.Get(item.ID()) - if ok { - newLines := newItem.height - oldItem.height - if l.virtualHeight > 0 { - l.offset = util.Clamp(l.offset+newLines, 0, l.virtualHeight-1) - } else { - l.offset = 0 + } else if hasOldItem && l.offset > l.itemPositions[inx].start { + if diff != 0 && l.virtualHeight > 0 { + l.offset = util.Clamp(l.offset+diff, 0, l.virtualHeight-1) } } } diff --git a/internal/tui/exp/list/list_test.go b/internal/tui/exp/list/list_test.go index bf7b9f98e0b018f3604e5b8a17c2e3dcb8695e91..d61bca277618e3ce0993684ee8f36660e9242393 100644 --- a/internal/tui/exp/list/list_test.go +++ b/internal/tui/exp/list/list_test.go @@ -26,20 +26,19 @@ func TestList(t *testing.T) { l := New(items, WithDirectionForward(), WithSize(10, 20)).(*list[Item]) execCmd(l, l.Init()) - // should select the last item + // should select item 10 assert.Equal(t, items[0].ID(), l.selectedItem) assert.Equal(t, 0, l.offset) require.Equal(t, 5, l.indexMap.Len()) require.Equal(t, 5, l.items.Len()) - require.Equal(t, 5, l.renderedItems.Len()) + require.Equal(t, 5, len(l.itemPositions)) assert.Equal(t, 5, lipgloss.Height(l.rendered)) assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline") start, end := l.viewPosition() assert.Equal(t, 0, start) assert.Equal(t, 4, end) for i := range 5 { - item, ok := l.renderedItems.Get(items[i].ID()) - require.True(t, ok) + item := l.itemPositions[i] assert.Equal(t, i, item.start) assert.Equal(t, i, item.end) } @@ -56,20 +55,19 @@ func TestList(t *testing.T) { l := New(items, WithDirectionBackward(), WithSize(10, 20)).(*list[Item]) execCmd(l, l.Init()) - // should select the last item + // should select item 10 assert.Equal(t, items[4].ID(), l.selectedItem) assert.Equal(t, 0, l.offset) require.Equal(t, 5, l.indexMap.Len()) require.Equal(t, 5, l.items.Len()) - require.Equal(t, 5, l.renderedItems.Len()) + require.Equal(t, 5, len(l.itemPositions)) assert.Equal(t, 5, lipgloss.Height(l.rendered)) assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline") start, end := l.viewPosition() assert.Equal(t, 0, start) assert.Equal(t, 4, end) for i := range 5 { - item, ok := l.renderedItems.Get(items[i].ID()) - require.True(t, ok) + item := l.itemPositions[i] assert.Equal(t, i, item.start) assert.Equal(t, i, item.end) } @@ -87,20 +85,20 @@ func TestList(t *testing.T) { l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item]) execCmd(l, l.Init()) - // should select the last item + // should select item 10 assert.Equal(t, items[0].ID(), l.selectedItem) assert.Equal(t, 0, l.offset) require.Equal(t, 30, l.indexMap.Len()) require.Equal(t, 30, l.items.Len()) - require.Equal(t, 30, l.renderedItems.Len()) - assert.Equal(t, 30, lipgloss.Height(l.rendered)) + require.Equal(t, 30, len(l.itemPositions)) + // With virtual scrolling, rendered height should be viewport height (10) + assert.Equal(t, 10, lipgloss.Height(l.rendered)) assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline") start, end := l.viewPosition() assert.Equal(t, 0, start) assert.Equal(t, 9, end) for i := range 30 { - item, ok := l.renderedItems.Get(items[i].ID()) - require.True(t, ok) + item := l.itemPositions[i] assert.Equal(t, i, item.start) assert.Equal(t, i, item.end) } @@ -117,20 +115,20 @@ func TestList(t *testing.T) { l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item]) execCmd(l, l.Init()) - // should select the last item + // should select item 10 assert.Equal(t, items[29].ID(), l.selectedItem) assert.Equal(t, 0, l.offset) require.Equal(t, 30, l.indexMap.Len()) require.Equal(t, 30, l.items.Len()) - require.Equal(t, 30, l.renderedItems.Len()) - assert.Equal(t, 30, lipgloss.Height(l.rendered)) + require.Equal(t, 30, len(l.itemPositions)) + // With virtual scrolling, rendered height should be viewport height (10) + assert.Equal(t, 10, lipgloss.Height(l.rendered)) assert.NotEqual(t, "\n", string(l.rendered[len(l.rendered)-1]), "should not end in newline") start, end := l.viewPosition() assert.Equal(t, 20, start) assert.Equal(t, 29, end) for i := range 30 { - item, ok := l.renderedItems.Get(items[i].ID()) - require.True(t, ok) + item := l.itemPositions[i] assert.Equal(t, i, item.start) assert.Equal(t, i, item.end) } @@ -150,12 +148,12 @@ func TestList(t *testing.T) { l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item]) execCmd(l, l.Init()) - // should select the last item + // should select item 10 assert.Equal(t, items[0].ID(), l.selectedItem) assert.Equal(t, 0, l.offset) require.Equal(t, 30, l.indexMap.Len()) require.Equal(t, 30, l.items.Len()) - require.Equal(t, 30, l.renderedItems.Len()) + require.Equal(t, 30, len(l.itemPositions)) expectedLines := 0 for i := range 30 { expectedLines += (i + 1) * 1 @@ -170,8 +168,7 @@ func TestList(t *testing.T) { assert.Equal(t, 9, end) currentPosition := 0 for i := range 30 { - rItem, ok := l.renderedItems.Get(items[i].ID()) - require.True(t, ok) + rItem := l.itemPositions[i] assert.Equal(t, currentPosition, rItem.start) assert.Equal(t, currentPosition+i, rItem.end) currentPosition += i + 1 @@ -191,12 +188,12 @@ func TestList(t *testing.T) { l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item]) execCmd(l, l.Init()) - // should select the last item + // should select item 10 assert.Equal(t, items[29].ID(), l.selectedItem) assert.Equal(t, 0, l.offset) require.Equal(t, 30, l.indexMap.Len()) require.Equal(t, 30, l.items.Len()) - require.Equal(t, 30, l.renderedItems.Len()) + require.Equal(t, 30, len(l.itemPositions)) expectedLines := 0 for i := range 30 { expectedLines += (i + 1) * 1 @@ -211,8 +208,7 @@ func TestList(t *testing.T) { assert.Equal(t, expectedLines-1, end) currentPosition := 0 for i := range 30 { - rItem, ok := l.renderedItems.Get(items[i].ID()) - require.True(t, ok) + rItem := l.itemPositions[i] assert.Equal(t, currentPosition, rItem.start) assert.Equal(t, currentPosition+i, rItem.end) currentPosition += i + 1 @@ -233,7 +229,7 @@ func TestList(t *testing.T) { l := New(items, WithDirectionForward(), WithSize(10, 10), WithSelectedItem(items[10].ID())).(*list[Item]) execCmd(l, l.Init()) - // should select the last item + // should select item 10 assert.Equal(t, items[10].ID(), l.selectedItem) golden.RequireEqual(t, []byte(l.View())) @@ -251,7 +247,7 @@ func TestList(t *testing.T) { l := New(items, WithDirectionBackward(), WithSize(10, 10), WithSelectedItem(items[10].ID())).(*list[Item]) execCmd(l, l.Init()) - // should select the last item + // should select item 10 assert.Equal(t, items[10].ID(), l.selectedItem) golden.RequireEqual(t, []byte(l.View())) @@ -365,7 +361,8 @@ func TestListMovement(t *testing.T) { viewAfter := l.View() assert.Equal(t, viewBefore, viewAfter) assert.Equal(t, 5, l.offset) - assert.Equal(t, 33, lipgloss.Height(l.rendered)) + // With virtual scrolling, rendered height should be viewport height (10) + assert.Equal(t, 10, lipgloss.Height(l.rendered)) golden.RequireEqual(t, []byte(l.View())) }) t.Run("should stay at the position it is when the hight of an item below is increased in backwards list", func(t *testing.T) { @@ -385,7 +382,8 @@ func TestListMovement(t *testing.T) { viewAfter := l.View() assert.Equal(t, viewBefore, viewAfter) assert.Equal(t, 4, l.offset) - assert.Equal(t, 32, lipgloss.Height(l.rendered)) + // With virtual scrolling, rendered height should be viewport height (10) + assert.Equal(t, 10, lipgloss.Height(l.rendered)) golden.RequireEqual(t, []byte(l.View())) }) t.Run("should stay at the position it is when the hight of an item below is decreases in backwards list", func(t *testing.T) { @@ -406,7 +404,8 @@ func TestListMovement(t *testing.T) { viewAfter := l.View() assert.Equal(t, viewBefore, viewAfter) assert.Equal(t, 0, l.offset) - assert.Equal(t, 31, lipgloss.Height(l.rendered)) + // With virtual scrolling, rendered height should be viewport height (10) + assert.Equal(t, 10, lipgloss.Height(l.rendered)) golden.RequireEqual(t, []byte(l.View())) }) t.Run("should stay at the position it is when the hight of an item above is increased in backwards list", func(t *testing.T) { @@ -426,7 +425,8 @@ func TestListMovement(t *testing.T) { viewAfter := l.View() assert.Equal(t, viewBefore, viewAfter) assert.Equal(t, 2, l.offset) - assert.Equal(t, 32, lipgloss.Height(l.rendered)) + // With virtual scrolling, rendered height should be viewport height (10) + assert.Equal(t, 10, lipgloss.Height(l.rendered)) golden.RequireEqual(t, []byte(l.View())) }) t.Run("should stay at the position it is if an item is prepended and we are in backwards list", func(t *testing.T) { @@ -445,7 +445,8 @@ func TestListMovement(t *testing.T) { viewAfter := l.View() assert.Equal(t, viewBefore, viewAfter) assert.Equal(t, 2, l.offset) - assert.Equal(t, 31, lipgloss.Height(l.rendered)) + // With virtual scrolling, rendered height should be viewport height (10) + assert.Equal(t, 10, lipgloss.Height(l.rendered)) golden.RequireEqual(t, []byte(l.View())) }) @@ -482,7 +483,8 @@ func TestListMovement(t *testing.T) { viewAfter := l.View() assert.Equal(t, viewBefore, viewAfter) assert.Equal(t, 5, l.offset) - assert.Equal(t, 33, lipgloss.Height(l.rendered)) + // With virtual scrolling, rendered height should be viewport height (10) + assert.Equal(t, 10, lipgloss.Height(l.rendered)) golden.RequireEqual(t, []byte(l.View())) }) @@ -503,7 +505,8 @@ func TestListMovement(t *testing.T) { viewAfter := l.View() assert.Equal(t, viewBefore, viewAfter) assert.Equal(t, 4, l.offset) - assert.Equal(t, 32, lipgloss.Height(l.rendered)) + // With virtual scrolling, rendered height should be viewport height (10) + assert.Equal(t, 10, lipgloss.Height(l.rendered)) golden.RequireEqual(t, []byte(l.View())) }) @@ -525,7 +528,8 @@ func TestListMovement(t *testing.T) { viewAfter := l.View() assert.Equal(t, viewBefore, viewAfter) assert.Equal(t, 1, l.offset) - assert.Equal(t, 31, lipgloss.Height(l.rendered)) + // With virtual scrolling, rendered height should be viewport height (10) + assert.Equal(t, 10, lipgloss.Height(l.rendered)) golden.RequireEqual(t, []byte(l.View())) }) @@ -546,7 +550,8 @@ func TestListMovement(t *testing.T) { viewAfter := l.View() assert.Equal(t, viewBefore, viewAfter) assert.Equal(t, 2, l.offset) - assert.Equal(t, 32, lipgloss.Height(l.rendered)) + // With virtual scrolling, rendered height should be viewport height (10) + assert.Equal(t, 10, lipgloss.Height(l.rendered)) golden.RequireEqual(t, []byte(l.View())) }) t.Run("should stay at the position it is if an item is appended and we are in forward list", func(t *testing.T) { @@ -565,7 +570,8 @@ func TestListMovement(t *testing.T) { viewAfter := l.View() assert.Equal(t, viewBefore, viewAfter) assert.Equal(t, 2, l.offset) - assert.Equal(t, 31, lipgloss.Height(l.rendered)) + // With virtual scrolling, rendered height should be viewport height (10) + assert.Equal(t, 10, lipgloss.Height(l.rendered)) golden.RequireEqual(t, []byte(l.View())) }) }