From 1e0dbe775cb8832afae9ce255c35ec5fad8f802c Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Fri, 17 Oct 2025 15:25:13 -0300 Subject: [PATCH] fix: infinite loop Signed-off-by: Carlos Alexandro Becker --- internal/tui/exp/list/list.go | 149 +++++++++++++--------------------- 1 file changed, 55 insertions(+), 94 deletions(-) diff --git a/internal/tui/exp/list/list.go b/internal/tui/exp/list/list.go index 272c18d6ec17ed4b0fcf2d6c3462780cd4d20a35..0c82e40ebd3dedbadcf967efa8925ba35ff55165 100644 --- a/internal/tui/exp/list/list.go +++ b/internal/tui/exp/list/list.go @@ -696,6 +696,13 @@ func (l *list[T]) scrollToSelection() { } func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd { + if l.items.Len() == 0 { + return nil + } + if len(l.itemPositions) != l.items.Len() || l.shouldCalculateItemPositions { + l.calculateItemPositions() + l.shouldCalculateItemPositions = false + } if l.selectedIndex < 0 || l.selectedIndex >= len(l.itemPositions) { return nil } @@ -714,53 +721,57 @@ func (l *list[T]) changeSelectionWhenScrolling() tea.Cmd { itemMiddle := rItem.start + rItem.height/2 if itemMiddle < start { - // select the first item in the viewport - // the item is most likely an item coming after this item - for { - inx := l.firstSelectableItemBelow(l.selectedIndex) - if inx == ItemNotFound { + inx := l.firstSelectableItemBelow(l.selectedIndex) + if inx == ItemNotFound { + return nil + } + if inx >= len(l.itemPositions) { + if len(l.itemPositions) != l.items.Len() || l.shouldCalculateItemPositions { + l.calculateItemPositions() + l.shouldCalculateItemPositions = false + if inx >= len(l.itemPositions) { + return nil + } + } else { return nil } - 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.selectedIndex = inx - return l.renderWithScrollToSelection(false) - } - // item is in the view - if renderedItem.start >= start && renderedItem.start <= end { - l.selectedIndex = inx - return l.renderWithScrollToSelection(false) - } } + renderedItem := l.itemPositions[inx] + if renderedItem.start <= start && renderedItem.end >= end { + l.selectedIndex = inx + return l.renderWithScrollToSelection(false) + } + if renderedItem.start >= start && renderedItem.start <= end { + l.selectedIndex = inx + return l.renderWithScrollToSelection(false) + } + return nil } else if itemMiddle > end { - // select the first item in the viewport - // the item is most likely an item coming after this item - for { - inx := l.firstSelectableItemAbove(l.selectedIndex) - if inx == ItemNotFound { + inx := l.firstSelectableItemAbove(l.selectedIndex) + if inx == ItemNotFound { + return nil + } + if inx >= len(l.itemPositions) { + if len(l.itemPositions) != l.items.Len() || l.shouldCalculateItemPositions { + l.calculateItemPositions() + l.shouldCalculateItemPositions = false + if inx >= len(l.itemPositions) { + return nil + } + } else { return nil } - 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.selectedIndex = inx - return l.renderWithScrollToSelection(false) - } - // item is in the view - if renderedItem.end >= start && renderedItem.end <= end { - l.selectedIndex = inx - return l.renderWithScrollToSelection(false) - } } + renderedItem := l.itemPositions[inx] + if renderedItem.start <= start && renderedItem.end >= end { + l.selectedIndex = inx + return l.renderWithScrollToSelection(false) + } + if renderedItem.end >= start && renderedItem.end <= end { + l.selectedIndex = inx + return l.renderWithScrollToSelection(false) + } + return nil } return nil } @@ -891,58 +902,6 @@ 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 { @@ -966,7 +925,7 @@ func (l *list[T]) renderVirtualScrolling() string { } itemsLen := l.items.Len() - for i := 0; i < itemsLen; i++ { + for i := range itemsLen { if i >= len(l.itemPositions) { continue } @@ -1013,7 +972,7 @@ func (l *list[T]) renderVirtualScrolling() string { // Add gap lines before item if needed (except for first visible item) if idx > 0 && currentLine < vis.pos.start { gapLines := vis.pos.start - currentLine - for i := 0; i < gapLines; i++ { + for range gapLines { lines = append(lines, "") } currentLine = vis.pos.start @@ -1055,6 +1014,7 @@ func (l *list[T]) renderVirtualScrolling() string { } if len(lines) > initialLen { // Added padding lines + // TODO: ? } // Trim to viewport height @@ -1067,6 +1027,7 @@ func (l *list[T]) renderVirtualScrolling() string { resultHeight := lipgloss.Height(result) if resultHeight < l.height && len(visibleItems) > 0 { // Warning: rendered fewer lines than viewport + // TODO: ? } return result }