From b25730d0c30129f642b0ade59e9804bfceab52ad Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 10 Dec 2025 13:02:05 -0500 Subject: [PATCH] refactor(ui): rename lazylist package to list and update imports --- internal/ui/lazylist/list.go.bak | 413 -------------------- internal/ui/{lazylist => list}/highlight.go | 2 +- internal/ui/{lazylist => list}/item.go | 2 +- internal/ui/{lazylist => list}/list.go | 2 +- internal/ui/model/chat.go | 12 +- internal/ui/model/items.go | 18 +- 6 files changed, 18 insertions(+), 431 deletions(-) delete mode 100644 internal/ui/lazylist/list.go.bak rename internal/ui/{lazylist => list}/highlight.go (99%) rename internal/ui/{lazylist => list}/item.go (98%) rename internal/ui/{lazylist => list}/list.go (99%) diff --git a/internal/ui/lazylist/list.go.bak b/internal/ui/lazylist/list.go.bak deleted file mode 100644 index 9aec6442e1ace230cc660b8d1cca6bf9b685c845..0000000000000000000000000000000000000000 --- a/internal/ui/lazylist/list.go.bak +++ /dev/null @@ -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 -} diff --git a/internal/ui/lazylist/highlight.go b/internal/ui/list/highlight.go similarity index 99% rename from internal/ui/lazylist/highlight.go rename to internal/ui/list/highlight.go index e53d10353dd286fcfe2642db102736c27df34adc..a1454a7edafb3cd623b022eb517593e86b90364e 100644 --- a/internal/ui/lazylist/highlight.go +++ b/internal/ui/list/highlight.go @@ -1,4 +1,4 @@ -package lazylist +package list import ( "image" diff --git a/internal/ui/lazylist/item.go b/internal/ui/list/item.go similarity index 98% rename from internal/ui/lazylist/item.go rename to internal/ui/list/item.go index 2a1b68a9bd666d8bba104274d51e984edc30b76e..48d53b75d057d40f76bf2b16ce2060601c1222f5 100644 --- a/internal/ui/lazylist/item.go +++ b/internal/ui/list/item.go @@ -1,4 +1,4 @@ -package lazylist +package list import ( "charm.land/lipgloss/v2" diff --git a/internal/ui/lazylist/list.go b/internal/ui/list/list.go similarity index 99% rename from internal/ui/lazylist/list.go rename to internal/ui/list/list.go index 319d69a777409c8c10528911aed30b34a83d623e..4414449a746b948031c1c5ed634ee5cf70bd57ab 100644 --- a/internal/ui/lazylist/list.go +++ b/internal/ui/list/list.go @@ -1,4 +1,4 @@ -package lazylist +package list import ( "image" diff --git a/internal/ui/model/chat.go b/internal/ui/model/chat.go index b34b6e43aeb9294bf33a835bbc4c5ba786082e49..4acf84cabf995275287221971fae5775e363bf9f 100644 --- a/internal/ui/model/chat.go +++ b/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) } diff --git a/internal/ui/model/items.go b/internal/ui/model/items.go index 92e7de403eef9ba4dffd7df85872d3c30bddeb4f..789208df297fa4ab5ddeaa25651a8ac66ef5812a 100644 --- a/internal/ui/model/items.go +++ b/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),