fix: infinite loop

Carlos Alexandro Becker created

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

Change summary

internal/tui/exp/list/list.go | 149 +++++++++++++-----------------------
1 file changed, 55 insertions(+), 94 deletions(-)

Detailed changes

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
 }