|
| 1 | +package viewport |
| 2 | + |
| 3 | +import ( |
| 4 | + "github.com/charmbracelet/lipgloss/v2" |
| 5 | + "github.com/charmbracelet/x/ansi" |
| 6 | + "github.com/rivo/uniseg" |
| 7 | +) |
| 8 | + |
| 9 | +// parseMatches converts the given matches into highlight ranges. |
| 10 | +// |
| 11 | +// Assumptions: |
| 12 | +// - matches are measured in bytes, e.g. what [regex.FindAllStringIndex] would return |
| 13 | +// - matches were made against the given content |
| 14 | +// - matches are in order |
| 15 | +// - matches do not overlap |
| 16 | +// - content is line terminated with \n only |
| 17 | +// |
| 18 | +// We'll then convert the ranges into [highlightInfo]s, which hold the starting |
| 19 | +// line and the grapheme positions. |
| 20 | +func parseMatches( |
| 21 | + content string, |
| 22 | + matches [][]int, |
| 23 | +) []highlightInfo { |
| 24 | + if len(matches) == 0 { |
| 25 | + return nil |
| 26 | + } |
| 27 | + |
| 28 | + line := 0 |
| 29 | + graphemePos := 0 |
| 30 | + previousLinesOffset := 0 |
| 31 | + bytePos := 0 |
| 32 | + |
| 33 | + highlights := make([]highlightInfo, 0, len(matches)) |
| 34 | + gr := uniseg.NewGraphemes(ansi.Strip(content)) |
| 35 | + |
| 36 | + for _, match := range matches { |
| 37 | + byteStart, byteEnd := match[0], match[1] |
| 38 | + |
| 39 | + // hilight for this match: |
| 40 | + hi := highlightInfo{ |
| 41 | + lines: map[int][2]int{}, |
| 42 | + } |
| 43 | + |
| 44 | + // find the beginning of this byte range, setup current line and |
| 45 | + // grapheme position. |
| 46 | + for byteStart > bytePos { |
| 47 | + if !gr.Next() { |
| 48 | + break |
| 49 | + } |
| 50 | + if content[bytePos] == '\n' { |
| 51 | + previousLinesOffset = graphemePos + 1 |
| 52 | + line++ |
| 53 | + } |
| 54 | + graphemePos += max(1, gr.Width()) |
| 55 | + bytePos += len(gr.Str()) |
| 56 | + } |
| 57 | + |
| 58 | + hi.lineStart = line |
| 59 | + hi.lineEnd = line |
| 60 | + |
| 61 | + graphemeStart := graphemePos |
| 62 | + |
| 63 | + // loop until we find the end |
| 64 | + for byteEnd > bytePos { |
| 65 | + if !gr.Next() { |
| 66 | + break |
| 67 | + } |
| 68 | + |
| 69 | + // if it ends with a new line, add the range, increase line, and continue |
| 70 | + if content[bytePos] == '\n' { |
| 71 | + colstart := max(0, graphemeStart-previousLinesOffset) |
| 72 | + colend := max(graphemePos-previousLinesOffset+1, colstart) // +1 its \n itself |
| 73 | + |
| 74 | + if colend > colstart { |
| 75 | + hi.lines[line] = [2]int{colstart, colend} |
| 76 | + hi.lineEnd = line |
| 77 | + } |
| 78 | + |
| 79 | + previousLinesOffset = graphemePos + 1 |
| 80 | + line++ |
| 81 | + } |
| 82 | + |
| 83 | + graphemePos += max(1, gr.Width()) |
| 84 | + bytePos += len(gr.Str()) |
| 85 | + } |
| 86 | + |
| 87 | + // we found it!, add highlight and continue |
| 88 | + if bytePos == byteEnd { |
| 89 | + colstart := max(0, graphemeStart-previousLinesOffset) |
| 90 | + colend := max(graphemePos-previousLinesOffset, colstart) |
| 91 | + |
| 92 | + if colend > colstart { |
| 93 | + hi.lines[line] = [2]int{colstart, colend} |
| 94 | + hi.lineEnd = line |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + highlights = append(highlights, hi) |
| 99 | + } |
| 100 | + |
| 101 | + return highlights |
| 102 | +} |
| 103 | + |
| 104 | +type highlightInfo struct { |
| 105 | + // in which line this highlight starts and ends |
| 106 | + lineStart, lineEnd int |
| 107 | + |
| 108 | + // the grapheme highlight ranges for each of these lines |
| 109 | + lines map[int][2]int |
| 110 | +} |
| 111 | + |
| 112 | +// coords returns the line x column of this highlight. |
| 113 | +func (hi highlightInfo) coords() (int, int, int) { |
| 114 | + for i := hi.lineStart; i <= hi.lineEnd; i++ { |
| 115 | + hl, ok := hi.lines[i] |
| 116 | + if !ok { |
| 117 | + continue |
| 118 | + } |
| 119 | + return i, hl[0], hl[1] |
| 120 | + } |
| 121 | + return hi.lineStart, 0, 0 |
| 122 | +} |
| 123 | + |
| 124 | +func makeHighlightRanges( |
| 125 | + highlights []highlightInfo, |
| 126 | + line int, |
| 127 | + style lipgloss.Style, |
| 128 | +) []lipgloss.Range { |
| 129 | + result := []lipgloss.Range{} |
| 130 | + for _, hi := range highlights { |
| 131 | + lihi, ok := hi.lines[line] |
| 132 | + if !ok { |
| 133 | + continue |
| 134 | + } |
| 135 | + if lihi == [2]int{} { |
| 136 | + continue |
| 137 | + } |
| 138 | + result = append(result, lipgloss.NewRange(lihi[0], lihi[1], style)) |
| 139 | + } |
| 140 | + return result |
| 141 | +} |
0 commit comments