highlight.go

  1package list
  2
  3import (
  4	"image"
  5	"strings"
  6
  7	"charm.land/lipgloss/v2"
  8	uv "github.com/charmbracelet/ultraviolet"
  9)
 10
 11// DefaultHighlighter is the default highlighter function that applies inverse style.
 12var DefaultHighlighter Highlighter = func(x, y int, c *uv.Cell) *uv.Cell {
 13	if c == nil {
 14		return c
 15	}
 16	c.Style.Attrs |= uv.AttrReverse
 17	return c
 18}
 19
 20// Highlighter represents a function that defines how to highlight text.
 21type Highlighter func(x, y int, c *uv.Cell) *uv.Cell
 22
 23// HighlightContent returns the content with highlighted regions based on the specified parameters.
 24func HighlightContent(content string, area image.Rectangle, startLine, startCol, endLine, endCol int) string {
 25	var sb strings.Builder
 26	pos := image.Pt(-1, -1)
 27	HighlightBuffer(content, area, startLine, startCol, endLine, endCol, func(x, y int, c *uv.Cell) *uv.Cell {
 28		pos.X = x
 29		if pos.Y == -1 {
 30			pos.Y = y
 31		} else if y > pos.Y {
 32			sb.WriteString(strings.Repeat("\n", y-pos.Y))
 33			pos.Y = y
 34		}
 35		sb.WriteString(c.Content)
 36		return c
 37	})
 38	if sb.Len() > 0 {
 39		sb.WriteString("\n")
 40	}
 41	return sb.String()
 42}
 43
 44// Highlight highlights a region of text within the given content and region.
 45func Highlight(content string, area image.Rectangle, startLine, startCol, endLine, endCol int, highlighter Highlighter) string {
 46	buf := HighlightBuffer(content, area, startLine, startCol, endLine, endCol, highlighter)
 47	if buf == nil {
 48		return content
 49	}
 50	return buf.Render()
 51}
 52
 53// HighlightBuffer highlights a region of text within the given content and
 54// region, returning a [uv.ScreenBuffer].
 55func HighlightBuffer(content string, area image.Rectangle, startLine, startCol, endLine, endCol int, highlighter Highlighter) *uv.ScreenBuffer {
 56	if startLine < 0 || startCol < 0 {
 57		return nil
 58	}
 59
 60	if highlighter == nil {
 61		highlighter = DefaultHighlighter
 62	}
 63
 64	width, height := area.Dx(), area.Dy()
 65	buf := uv.NewScreenBuffer(width, height)
 66	styled := uv.NewStyledString(content)
 67	styled.Draw(&buf, area)
 68
 69	// Treat -1 as "end of content"
 70	if endLine < 0 {
 71		endLine = height - 1
 72	}
 73	if endCol < 0 {
 74		endCol = width
 75	}
 76
 77	for y := startLine; y <= endLine && y < height; y++ {
 78		if y >= buf.Height() {
 79			break
 80		}
 81
 82		line := buf.Line(y)
 83
 84		// Determine column range for this line
 85		colStart := 0
 86		if y == startLine {
 87			colStart = min(startCol, len(line))
 88		}
 89
 90		colEnd := len(line)
 91		if y == endLine {
 92			colEnd = min(endCol, len(line))
 93		}
 94
 95		// Track last non-empty position as we go
 96		lastContentX := -1
 97
 98		// Single pass: check content and track last non-empty position
 99		for x := colStart; x < colEnd; x++ {
100			cell := line.At(x)
101			if cell == nil {
102				continue
103			}
104
105			// Update last content position if non-empty
106			if cell.Content != "" && cell.Content != " " {
107				lastContentX = x
108			}
109		}
110
111		// Only apply highlight up to last content position
112		highlightEnd := colEnd
113		if lastContentX >= 0 {
114			highlightEnd = lastContentX + 1
115		} else if lastContentX == -1 {
116			highlightEnd = colStart // No content on this line
117		}
118
119		// Apply highlight style only to cells with content
120		for x := colStart; x < highlightEnd; x++ {
121			if !image.Pt(x, y).In(area) {
122				continue
123			}
124			cell := line.At(x)
125			if cell != nil {
126				line.Set(x, highlighter(x, y, cell))
127			}
128		}
129	}
130
131	return &buf
132}
133
134// ToHighlighter converts a [lipgloss.Style] to a [Highlighter].
135func ToHighlighter(lgStyle lipgloss.Style) Highlighter {
136	return func(_ int, _ int, c *uv.Cell) *uv.Cell {
137		if c != nil {
138			c.Style = ToStyle(lgStyle)
139		}
140		return c
141	}
142}
143
144// ToStyle converts an inline [lipgloss.Style] to a [uv.Style].
145func ToStyle(lgStyle lipgloss.Style) uv.Style {
146	var uvStyle uv.Style
147
148	// Colors are already color.Color
149	uvStyle.Fg = lgStyle.GetForeground()
150	uvStyle.Bg = lgStyle.GetBackground()
151
152	// Build attributes using bitwise OR
153	var attrs uint8
154
155	if lgStyle.GetBold() {
156		attrs |= uv.AttrBold
157	}
158
159	if lgStyle.GetItalic() {
160		attrs |= uv.AttrItalic
161	}
162
163	if lgStyle.GetUnderline() {
164		uvStyle.Underline = uv.UnderlineSingle
165	}
166
167	if lgStyle.GetStrikethrough() {
168		attrs |= uv.AttrStrikethrough
169	}
170
171	if lgStyle.GetFaint() {
172		attrs |= uv.AttrFaint
173	}
174
175	if lgStyle.GetBlink() {
176		attrs |= uv.AttrBlink
177	}
178
179	if lgStyle.GetReverse() {
180		attrs |= uv.AttrReverse
181	}
182
183	uvStyle.Attrs = attrs
184
185	return uvStyle
186}
187
188// AdjustArea adjusts the given area rectangle by subtracting margins, borders,
189// and padding from the style.
190func AdjustArea(area image.Rectangle, style lipgloss.Style) image.Rectangle {
191	topMargin, rightMargin, bottomMargin, leftMargin := style.GetMargin()
192	topBorder, rightBorder, bottomBorder, leftBorder := style.GetBorderTopSize(),
193		style.GetBorderRightSize(),
194		style.GetBorderBottomSize(),
195		style.GetBorderLeftSize()
196	topPadding, rightPadding, bottomPadding, leftPadding := style.GetPadding()
197
198	return image.Rectangle{
199		Min: image.Point{
200			X: area.Min.X + leftMargin + leftBorder + leftPadding,
201			Y: area.Min.Y + topMargin + topBorder + topPadding,
202		},
203		Max: image.Point{
204			X: area.Max.X - (rightMargin + rightBorder + rightPadding),
205			Y: area.Max.Y - (bottomMargin + bottomBorder + bottomPadding),
206		},
207	}
208}