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