refactor(ui): rename lazylist package to list and update imports

Ayman Bagabas created

Change summary

internal/ui/lazylist/list.go.bak | 413 ----------------------------------
internal/ui/list/highlight.go    |   2 
internal/ui/list/item.go         |   2 
internal/ui/list/list.go         |   2 
internal/ui/model/chat.go        |  12 
internal/ui/model/items.go       |  18 
6 files changed, 18 insertions(+), 431 deletions(-)

Detailed changes

internal/ui/lazylist/list.go.bak 🔗

@@ -1,413 +0,0 @@
-package lazylist
-
-import (
-	"log/slog"
-	"strings"
-)
-
-// List represents a list of items that can be lazily rendered. A list is
-// always rendered like a chat conversation where items are stacked vertically
-// from top to bottom.
-type List struct {
-	// Viewport size
-	width, height int
-
-	// Items in the list
-	items []Item
-
-	// Gap between items (0 or less means no gap)
-	gap int
-
-	// Focus and selection state
-	focused     bool
-	selectedIdx int // The current selected index -1 means no selection
-
-	// Item positioning. If a position exists in the map, it means the item has
-	// been rendered and measured.
-	itemPositions map[int]itemPosition
-
-	// Rendered content and cache
-	lines         []string
-	renderedItems map[int]renderedItem
-	offsetIdx     int // Index of the first visible item in the viewport
-	offsetLine    int // The offset line from the start of the offsetIdx item (can be negative)
-
-	// Dirty tracking
-	dirtyItems map[int]struct{}
-}
-
-// renderedItem holds the rendered content and height of an item.
-type renderedItem struct {
-	content string
-	height  int
-}
-
-// itemPosition holds the start and end line of an item in the list.
-type itemPosition struct {
-	startLine int
-	endLine   int
-}
-
-// Height returns the height of item based on its start and end lines.
-func (ip itemPosition) Height() int {
-	return ip.endLine - ip.startLine
-}
-
-// NewList creates a new lazy-loaded list.
-func NewList(items ...Item) *List {
-	l := new(List)
-	l.items = items
-	l.itemPositions = make(map[int]itemPosition)
-	l.renderedItems = make(map[int]renderedItem)
-	l.dirtyItems = make(map[int]struct{})
-	return l
-}
-
-// SetSize sets the size of the list viewport.
-func (l *List) SetSize(width, height int) {
-	if width != l.width {
-		// Mark all rendered items as dirty if width changes because their
-		// layout may change.
-		for idx := range l.itemPositions {
-			l.dirtyItems[idx] = struct{}{}
-		}
-	}
-	l.width = width
-	l.height = height
-}
-
-// SetGap sets the gap between items.
-func (l *List) SetGap(gap int) {
-	l.gap = gap
-}
-
-// Width returns the width of the list viewport.
-func (l *List) Width() int {
-	return l.width
-}
-
-// Height returns the height of the list viewport.
-func (l *List) Height() int {
-	return l.height
-}
-
-// Len returns the number of items in the list.
-func (l *List) Len() int {
-	return len(l.items)
-}
-
-// renderItem renders the item at the given index and updates its cache and
-// position.
-func (l *List) renderItem(idx int) {
-	if idx < 0 || idx >= len(l.items) {
-		return
-	}
-
-	item := l.items[idx]
-	rendered := item.Render(l.width)
-	height := countLines(rendered)
-
-	l.renderedItems[idx] = renderedItem{
-		content: rendered,
-		height:  height,
-	}
-
-	// Calculate item position
-	var startLine int
-	if idx == 0 {
-		startLine = 0
-	} else {
-		prevPos, ok := l.itemPositions[idx-1]
-		if !ok {
-			l.renderItem(idx - 1)
-			prevPos = l.itemPositions[idx-1]
-		}
-		startLine = prevPos.endLine
-		if l.gap > 0 {
-			startLine += l.gap
-		}
-	}
-	endLine := startLine + height
-
-	l.itemPositions[idx] = itemPosition{
-		startLine: startLine,
-		endLine:   endLine,
-	}
-}
-
-// ScrollToIndex scrolls the list to the given item index.
-func (l *List) ScrollToIndex(index int) {
-	if index < 0 || index >= len(l.items) {
-		return
-	}
-	l.offsetIdx = index
-	l.offsetLine = 0
-}
-
-// ScrollBy scrolls the list by the given number of lines.
-func (l *List) ScrollBy(lines int) {
-	l.offsetLine += lines
-	if l.offsetIdx <= 0 && l.offsetLine < 0 {
-		l.offsetIdx = 0
-		l.offsetLine = 0
-		return
-	}
-
-	// Adjust offset index and line if needed
-	for l.offsetLine < 0 && l.offsetIdx > 0 {
-		// Move up to previous item
-		l.offsetIdx--
-		prevPos, ok := l.itemPositions[l.offsetIdx]
-		if !ok {
-			l.renderItem(l.offsetIdx)
-			prevPos = l.itemPositions[l.offsetIdx]
-		}
-		l.offsetLine += prevPos.Height()
-		if l.gap > 0 {
-			l.offsetLine += l.gap
-		}
-	}
-
-	for {
-		currentPos, ok := l.itemPositions[l.offsetIdx]
-		if !ok {
-			l.renderItem(l.offsetIdx)
-			currentPos = l.itemPositions[l.offsetIdx]
-		}
-		if l.offsetLine >= currentPos.Height() {
-			// Move down to next item
-			l.offsetLine -= currentPos.Height()
-			if l.gap > 0 {
-				l.offsetLine -= l.gap
-			}
-			l.offsetIdx++
-			if l.offsetIdx >= len(l.items) {
-				l.offsetIdx = len(l.items) - 1
-				l.offsetLine = currentPos.Height() - 1
-				break
-			}
-		} else {
-			break
-		}
-	}
-}
-
-// findVisibleItems finds the range of items that are visible in the viewport.
-func (l *List) findVisibleItems() (startIdx, endIdx int) {
-	startIdx = l.offsetIdx
-	endIdx = startIdx + 1
-
-	// Render items until we fill the viewport
-	visibleHeight := -l.offsetLine
-	for endIdx < len(l.items) {
-		pos, ok := l.itemPositions[endIdx-1]
-		if !ok {
-			l.renderItem(endIdx - 1)
-			pos = l.itemPositions[endIdx-1]
-		}
-		visibleHeight += pos.Height()
-		if endIdx-1 < len(l.items)-1 && l.gap > 0 {
-			visibleHeight += l.gap
-		}
-		if visibleHeight >= l.height {
-			break
-		}
-		endIdx++
-	}
-
-	if endIdx > len(l.items)-1 {
-		endIdx = len(l.items) - 1
-	}
-
-	return startIdx, endIdx
-}
-
-// renderLines renders the items between startIdx and endIdx into lines.
-func (l *List) renderLines(startIdx, endIdx int) []string {
-	var lines []string
-	for idx := startIdx; idx < endIdx+1; idx++ {
-		rendered, ok := l.renderedItems[idx]
-		if !ok {
-			l.renderItem(idx)
-			rendered = l.renderedItems[idx]
-		}
-		itemLines := strings.Split(rendered.content, "\n")
-		lines = append(lines, itemLines...)
-		if l.gap > 0 && idx < endIdx {
-			for i := 0; i < l.gap; i++ {
-				lines = append(lines, "")
-			}
-		}
-	}
-	return lines
-}
-
-// Render renders the list and returns the visible lines.
-func (l *List) Render() string {
-	viewStartIdx, viewEndIdx := l.findVisibleItems()
-	slog.Info("Render", "viewStartIdx", viewStartIdx, "viewEndIdx", viewEndIdx, "offsetIdx", l.offsetIdx, "offsetLine", l.offsetLine)
-
-	for idx := range l.dirtyItems {
-		if idx >= viewStartIdx && idx <= viewEndIdx {
-			l.renderItem(idx)
-			delete(l.dirtyItems, idx)
-		}
-	}
-
-	lines := l.renderLines(viewStartIdx, viewEndIdx)
-	for len(lines) < l.height {
-		viewStartIdx--
-		if viewStartIdx <= 0 {
-			break
-		}
-
-		lines = l.renderLines(viewStartIdx, viewEndIdx)
-	}
-
-	if len(lines) > l.height {
-		lines = lines[:l.height]
-	}
-
-	return strings.Join(lines, "\n")
-}
-
-// PrependItems prepends items to the list.
-func (l *List) PrependItems(items ...Item) {
-	l.items = append(items, l.items...)
-	// Shift existing item positions
-	newItemPositions := make(map[int]itemPosition)
-	for idx, pos := range l.itemPositions {
-		newItemPositions[idx+len(items)] = pos
-	}
-	l.itemPositions = newItemPositions
-
-	// Mark all items as dirty
-	for idx := range l.items {
-		l.dirtyItems[idx] = struct{}{}
-	}
-
-	// Adjust offset index
-	l.offsetIdx += len(items)
-}
-
-// AppendItems appends items to the list.
-func (l *List) AppendItems(items ...Item) {
-	l.items = append(l.items, items...)
-	for idx := len(l.items) - len(items); idx < len(l.items); idx++ {
-		l.dirtyItems[idx] = struct{}{}
-	}
-}
-
-// Focus sets the focus state of the list.
-func (l *List) Focus() {
-	l.focused = true
-}
-
-// Blur removes the focus state from the list.
-func (l *List) Blur() {
-	l.focused = false
-}
-
-// ScrollToTop scrolls the list to the top.
-func (l *List) ScrollToTop() {
-	l.offsetIdx = 0
-	l.offsetLine = 0
-}
-
-// ScrollToBottom scrolls the list to the bottom.
-func (l *List) ScrollToBottom() {
-	l.offsetIdx = len(l.items) - 1
-	pos, ok := l.itemPositions[l.offsetIdx]
-	if !ok {
-		l.renderItem(l.offsetIdx)
-		pos = l.itemPositions[l.offsetIdx]
-	}
-	l.offsetLine = l.height - pos.Height()
-}
-
-// ScrollToSelected scrolls the list to the selected item.
-func (l *List) ScrollToSelected() {
-	if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
-		return
-	}
-	l.offsetIdx = l.selectedIdx
-	l.offsetLine = 0
-}
-
-// SelectedItemInView returns whether the selected item is currently in view.
-func (l *List) SelectedItemInView() bool {
-	if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
-		return false
-	}
-	startIdx, endIdx := l.findVisibleItems()
-	return l.selectedIdx >= startIdx && l.selectedIdx <= endIdx
-}
-
-// SetSelected sets the selected item index in the list.
-func (l *List) SetSelected(index int) {
-	if index < 0 || index >= len(l.items) {
-		l.selectedIdx = -1
-	} else {
-		l.selectedIdx = index
-	}
-}
-
-// SelectPrev selects the previous item in the list.
-func (l *List) SelectPrev() {
-	if l.selectedIdx > 0 {
-		l.selectedIdx--
-	}
-}
-
-// SelectNext selects the next item in the list.
-func (l *List) SelectNext() {
-	if l.selectedIdx < len(l.items)-1 {
-		l.selectedIdx++
-	}
-}
-
-// SelectFirst selects the first item in the list.
-func (l *List) SelectFirst() {
-	if len(l.items) > 0 {
-		l.selectedIdx = 0
-	}
-}
-
-// SelectLast selects the last item in the list.
-func (l *List) SelectLast() {
-	if len(l.items) > 0 {
-		l.selectedIdx = len(l.items) - 1
-	}
-}
-
-// SelectFirstInView selects the first item currently in view.
-func (l *List) SelectFirstInView() {
-	startIdx, _ := l.findVisibleItems()
-	l.selectedIdx = startIdx
-}
-
-// SelectLastInView selects the last item currently in view.
-func (l *List) SelectLastInView() {
-	_, endIdx := l.findVisibleItems()
-	l.selectedIdx = endIdx
-}
-
-// HandleMouseDown handles mouse down events at the given line in the viewport.
-func (l *List) HandleMouseDown(x, y int) {
-}
-
-// HandleMouseUp handles mouse up events at the given line in the viewport.
-func (l *List) HandleMouseUp(x, y int) {
-}
-
-// HandleMouseDrag handles mouse drag events at the given line in the viewport.
-func (l *List) HandleMouseDrag(x, y int) {
-}
-
-// countLines counts the number of lines in a string.
-func countLines(s string) int {
-	if s == "" {
-		return 0
-	}
-	return strings.Count(s, "\n") + 1
-}

internal/ui/lazylist/item.go → internal/ui/list/item.go 🔗

@@ -1,4 +1,4 @@
-package lazylist
+package list
 
 import (
 	"charm.land/lipgloss/v2"

internal/ui/model/chat.go 🔗

@@ -2,7 +2,7 @@ package model
 
 import (
 	"github.com/charmbracelet/crush/internal/ui/common"
-	"github.com/charmbracelet/crush/internal/ui/lazylist"
+	"github.com/charmbracelet/crush/internal/ui/list"
 	uv "github.com/charmbracelet/ultraviolet"
 )
 
@@ -10,13 +10,13 @@ import (
 // messages.
 type Chat struct {
 	com  *common.Common
-	list *lazylist.List
+	list *list.List
 }
 
 // NewChat creates a new instance of [Chat] that handles chat interactions and
 // messages.
 func NewChat(com *common.Common) *Chat {
-	l := lazylist.NewList()
+	l := list.NewList()
 	l.SetGap(1)
 	return &Chat{
 		com:  com,
@@ -45,14 +45,14 @@ func (m *Chat) Len() int {
 }
 
 // PrependItems prepends new items to the chat list.
-func (m *Chat) PrependItems(items ...lazylist.Item) {
+func (m *Chat) PrependItems(items ...list.Item) {
 	m.list.PrependItems(items...)
 	m.list.ScrollToIndex(0)
 }
 
 // AppendMessages appends a new message item to the chat list.
 func (m *Chat) AppendMessages(msgs ...MessageItem) {
-	items := make([]lazylist.Item, len(msgs))
+	items := make([]list.Item, len(msgs))
 	for i, msg := range msgs {
 		items[i] = msg
 	}
@@ -60,7 +60,7 @@ func (m *Chat) AppendMessages(msgs ...MessageItem) {
 }
 
 // AppendItems appends new items to the chat list.
-func (m *Chat) AppendItems(items ...lazylist.Item) {
+func (m *Chat) AppendItems(items ...list.Item) {
 	m.list.AppendItems(items...)
 	m.list.ScrollToIndex(m.list.Len() - 1)
 }

internal/ui/model/items.go 🔗

@@ -12,7 +12,7 @@ import (
 	"github.com/charmbracelet/crush/internal/config"
 	"github.com/charmbracelet/crush/internal/message"
 	"github.com/charmbracelet/crush/internal/ui/common"
-	"github.com/charmbracelet/crush/internal/ui/lazylist"
+	"github.com/charmbracelet/crush/internal/ui/list"
 	"github.com/charmbracelet/crush/internal/ui/styles"
 	"github.com/charmbracelet/crush/internal/ui/toolrender"
 )
@@ -23,10 +23,10 @@ type Identifiable interface {
 }
 
 // MessageItem represents a [message.Message] item that can be displayed in the
-// UI and be part of a [lazylist.List] identifiable by a unique ID.
+// UI and be part of a [list.List] identifiable by a unique ID.
 type MessageItem interface {
-	lazylist.Item
-	lazylist.Item
+	list.Item
+	list.Item
 	Identifiable
 }
 
@@ -81,7 +81,7 @@ func (m *MessageContentItem) HighlightStyle() lipgloss.Style {
 
 // Render renders the content at the given width, using cache if available.
 //
-// It implements [lazylist.Item].
+// It implements [list.Item].
 func (m *MessageContentItem) Render(width int) string {
 	contentWidth := width
 	// Cap width to maxWidth for markdown
@@ -163,7 +163,7 @@ func (t *ToolCallItem) HighlightStyle() lipgloss.Style {
 	return t.sty.TextSelection
 }
 
-// Render implements lazylist.Item.
+// Render implements list.Item.
 func (t *ToolCallItem) Render(width int) string {
 	// Render the tool call
 	ctx := &toolrender.RenderContext{
@@ -218,7 +218,7 @@ func (a *AttachmentItem) HighlightStyle() lipgloss.Style {
 	return a.sty.TextSelection
 }
 
-// Render implements lazylist.Item.
+// Render implements list.Item.
 func (a *AttachmentItem) Render(width int) string {
 	const maxFilenameWidth = 10
 	content := a.sty.Chat.Message.Attachment.Render(fmt.Sprintf(
@@ -275,7 +275,7 @@ func (t *ThinkingItem) HighlightStyle() lipgloss.Style {
 	return t.sty.TextSelection
 }
 
-// Render implements lazylist.Item.
+// Render implements list.Item.
 func (t *ThinkingItem) Render(width int) string {
 	cappedWidth := min(width, t.maxWidth)
 
@@ -353,7 +353,7 @@ func (s *SectionHeaderItem) BlurStyle() lipgloss.Style {
 	return s.sty.Chat.Message.AssistantBlurred
 }
 
-// Render implements lazylist.Item.
+// Render implements list.Item.
 func (s *SectionHeaderItem) Render(width int) string {
 	content := fmt.Sprintf("%s %s %s",
 		s.sty.Subtle.Render(styles.ModelIcon),