From d5b83c0dbbaa228bf3f783495d73e6db84489e2d Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 21 Jan 2026 11:43:30 -0500 Subject: [PATCH] fix(ui): implement Highlightable interface for message items --- internal/ui/chat/messages.go | 11 +++++-- internal/ui/list/highlight.go | 58 +++++++++++++++++++++++++++++------ internal/ui/list/item.go | 8 +++-- internal/ui/model/chat.go | 2 +- 4 files changed, 64 insertions(+), 15 deletions(-) diff --git a/internal/ui/chat/messages.go b/internal/ui/chat/messages.go index 68954b0f3f0168b9da91b1b28db1b5101e5f9c3b..a666aa44607c8dece9ac0be80b32e5297091cb33 100644 --- a/internal/ui/chat/messages.go +++ b/internal/ui/chat/messages.go @@ -77,6 +77,8 @@ type highlightableMessageItem struct { highlighter list.Highlighter } +var _ list.Highlightable = (*highlightableMessageItem)(nil) + // isHighlighted returns true if the item has a highlight range set. func (h *highlightableMessageItem) isHighlighted() bool { return h.startLine != -1 || h.endLine != -1 @@ -91,8 +93,8 @@ func (h *highlightableMessageItem) renderHighlighted(content string, width, heig return list.Highlight(content, area, h.startLine, h.startCol, h.endLine, h.endCol, h.highlighter) } -// Highlight implements MessageItem. -func (h *highlightableMessageItem) Highlight(startLine int, startCol int, endLine int, endCol int) { +// SetHighlight implements [MessageItem]. +func (h *highlightableMessageItem) SetHighlight(startLine int, startCol int, endLine int, endCol int) { // Adjust columns for the style's left inset (border + padding) since we // highlight the content only. offset := messageLeftPaddingTotal @@ -106,6 +108,11 @@ func (h *highlightableMessageItem) Highlight(startLine int, startCol int, endLin } } +// Highlight implements [MessageItem]. +func (h *highlightableMessageItem) Highlight() (startLine int, startCol int, endLine int, endCol int) { + return h.startLine, h.startCol, h.endLine, h.endCol +} + func defaultHighlighter(sty *styles.Styles) *highlightableMessageItem { return &highlightableMessageItem{ startLine: -1, diff --git a/internal/ui/list/highlight.go b/internal/ui/list/highlight.go index c61a53a18ffc2aced7f5ec21f31e2fe4f4916522..fefe836d110b52496028d21071fffc5262189d92 100644 --- a/internal/ui/list/highlight.go +++ b/internal/ui/list/highlight.go @@ -2,25 +2,60 @@ package list import ( "image" + "strings" "charm.land/lipgloss/v2" uv "github.com/charmbracelet/ultraviolet" ) // DefaultHighlighter is the default highlighter function that applies inverse style. -var DefaultHighlighter Highlighter = func(s uv.Style) uv.Style { - s.Attrs |= uv.AttrReverse - return s +var DefaultHighlighter Highlighter = func(x, y int, c *uv.Cell) *uv.Cell { + if c == nil { + return c + } + c.Style.Attrs |= uv.AttrReverse + return c } // Highlighter represents a function that defines how to highlight text. -type Highlighter func(uv.Style) uv.Style +type Highlighter func(x, y int, c *uv.Cell) *uv.Cell + +// HighlightContent returns the content with highlighted regions based on the specified parameters. +func HighlightContent(content string, area image.Rectangle, startLine, startCol, endLine, endCol int) string { + var sb strings.Builder + pos := image.Pt(-1, -1) + HighlightBuffer(content, area, startLine, startCol, endLine, endCol, func(x, y int, c *uv.Cell) *uv.Cell { + pos.X = x + if pos.Y == -1 { + pos.Y = y + } else if y > pos.Y { + sb.WriteString(strings.Repeat("\n", y-pos.Y)) + pos.Y = y + } + sb.WriteString(c.Content) + return c + }) + if sb.Len() > 0 { + sb.WriteString("\n") + } + return sb.String() +} // Highlight highlights a region of text within the given content and region. func Highlight(content string, area image.Rectangle, startLine, startCol, endLine, endCol int, highlighter Highlighter) string { - if startLine < 0 || startCol < 0 { + buf := HighlightBuffer(content, area, startLine, startCol, endLine, endCol, highlighter) + if buf == nil { return content } + return buf.Render() +} + +// HighlightBuffer highlights a region of text within the given content and +// region, returning a [uv.ScreenBuffer]. +func HighlightBuffer(content string, area image.Rectangle, startLine, startCol, endLine, endCol int, highlighter Highlighter) *uv.ScreenBuffer { + if startLine < 0 || startCol < 0 { + return nil + } if highlighter == nil { highlighter = DefaultHighlighter @@ -87,17 +122,22 @@ func Highlight(content string, area image.Rectangle, startLine, startCol, endLin continue } cell := line.At(x) - cell.Style = highlighter(cell.Style) + if cell != nil { + line.Set(x, highlighter(x, y, cell)) + } } } - return buf.Render() + return &buf } // ToHighlighter converts a [lipgloss.Style] to a [Highlighter]. func ToHighlighter(lgStyle lipgloss.Style) Highlighter { - return func(uv.Style) uv.Style { - return ToStyle(lgStyle) + return func(_ int, _ int, c *uv.Cell) *uv.Cell { + if c != nil { + c.Style = ToStyle(lgStyle) + } + return c } } diff --git a/internal/ui/list/item.go b/internal/ui/list/item.go index 62b31a696eee11b5dc11f0228d82ccfa8a0c91e5..dc6f9a345854672116a08f2fd988a15e53b160bd 100644 --- a/internal/ui/list/item.go +++ b/internal/ui/list/item.go @@ -21,9 +21,11 @@ type Focusable interface { // Highlightable represents an item that can highlight a portion of its content. type Highlightable interface { - // Highlight highlights the content from the given start to end positions. - // Use -1 for no highlight. - Highlight(startLine, startCol, endLine, endCol int) + // SetHighlight highlights the content from the given start to end + // positions. Use -1 for no highlight. + SetHighlight(startLine, startCol, endLine, endCol int) + // Highlight returns the current highlight positions within the item. + Highlight() (startLine, startCol, endLine, endCol int) } // MouseClickable represents an item that can handle mouse click events. diff --git a/internal/ui/model/chat.go b/internal/ui/model/chat.go index a6c8fb1cf213be37c8f095ba776f936bec96b57a..abe45997fb447a48bdbb6b2df2ef52ec3e1fb99a 100644 --- a/internal/ui/model/chat.go +++ b/internal/ui/model/chat.go @@ -515,7 +515,7 @@ func (m *Chat) applyHighlightRange(idx, selectedIdx int, item list.Item) list.It } } - hi.Highlight(sLine, sCol, eLine, eCol) + hi.SetHighlight(sLine, sCol, eLine, eCol) return hi.(list.Item) }