8 "github.com/dlclark/regexp2"
11 // RunePos returns the rune index of a given byte index
12 // This could cause problems if the byte index is between code points
13 func runePos(p int, str string) int {
18 return utf8.RuneCountInString(str)
20 return utf8.RuneCountInString(str[:p])
23 func combineLineMatch(src, dst LineMatch) LineMatch {
24 for k, v := range src {
25 if g, ok := dst[k]; ok {
36 // A State represents the region at the end of a line
39 // LineStates is an interface for a buffer-like object which can also store the states and matches for every line
40 type LineStates interface {
43 State(lineN int) State
44 SetState(lineN int, s State)
45 SetMatch(lineN int, m LineMatch)
48 // A Highlighter contains the information needed to highlight a string
49 type Highlighter struct {
54 // NewHighlighter returns a new highlighter from the given syntax definition
55 func NewHighlighter(def *Def) *Highlighter {
61 // LineMatch represents the syntax highlighting matches for one line. Each index where the coloring is changed is marked with that
62 // color's group (represented as one byte)
63 type LineMatch map[int]uint8
65 func findIndex(regex *regexp2.Regexp, str []byte, canMatchStart, canMatchEnd bool) []int {
66 regexStr := regex.String()
67 if strings.Contains(regexStr, "^") {
72 if strings.Contains(regexStr, "$") {
77 match, _ := regex.FindStringMatch(string(str))
81 return []int{match.Index, match.Index + match.Length}
84 func findAllIndex(regex *regexp.Regexp, str []byte, canMatchStart, canMatchEnd bool) [][]int {
85 regexStr := regex.String()
86 if strings.Contains(regexStr, "^") {
91 if strings.Contains(regexStr, "$") {
96 return regex.FindAllIndex([]byte(string(str)), -1)
99 func (h *Highlighter) highlightRegion(highlights LineMatch, start int, canMatchEnd bool, lineNum int, line []byte, region *Region, statesOnly bool) LineMatch {
100 // highlights := make(LineMatch)
104 highlights[0] = region.group
108 loc := findIndex(region.end, line, start == 0, canMatchEnd)
111 highlights[start+runePos(loc[1]-1, string(line))] = region.group
113 if region.parent == nil {
115 highlights[start+runePos(loc[1], string(line))] = 0
116 h.highlightRegion(highlights, start, false, lineNum, line[:loc[0]], region, statesOnly)
118 h.highlightEmptyRegion(highlights, start+runePos(loc[1], string(line)), canMatchEnd, lineNum, line[loc[1]:], statesOnly)
122 highlights[start+runePos(loc[1], string(line))] = region.parent.group
123 h.highlightRegion(highlights, start, false, lineNum, line[:loc[0]], region, statesOnly)
125 h.highlightRegion(highlights, start+runePos(loc[1], string(line)), canMatchEnd, lineNum, line[loc[1]:], region.parent, statesOnly)
129 if len(line) == 0 || statesOnly {
131 h.lastRegion = region
137 firstLoc := []int{len(line), 0}
138 var firstRegion *Region
139 for _, r := range region.rules.regions {
140 loc := findIndex(r.start, line, start == 0, canMatchEnd)
142 if loc[0] < firstLoc[0] {
148 if firstLoc[0] != len(line) {
149 highlights[start+runePos(firstLoc[0], string(line))] = firstRegion.group
150 h.highlightRegion(highlights, start, false, lineNum, line[:firstLoc[0]], region, statesOnly)
151 h.highlightRegion(highlights, start+runePos(firstLoc[1], string(line)), canMatchEnd, lineNum, line[firstLoc[1]:], firstRegion, statesOnly)
155 fullHighlights := make([]uint8, len(line))
156 for i := 0; i < len(fullHighlights); i++ {
157 fullHighlights[i] = region.group
160 for _, p := range region.rules.patterns {
161 matches := findAllIndex(p.regex, line, start == 0, canMatchEnd)
162 for _, m := range matches {
163 for i := m[0]; i < m[1]; i++ {
164 fullHighlights[i] = p.group
168 for i, h := range fullHighlights {
169 if i == 0 || h != fullHighlights[i-1] {
170 if _, ok := highlights[start+runePos(i, string(line))]; !ok {
171 highlights[start+runePos(i, string(line))] = h
177 h.lastRegion = region
183 func (h *Highlighter) highlightEmptyRegion(highlights LineMatch, start int, canMatchEnd bool, lineNum int, line []byte, statesOnly bool) LineMatch {
191 firstLoc := []int{len(line), 0}
192 var firstRegion *Region
193 for _, r := range h.def.rules.regions {
194 loc := findIndex(r.start, line, start == 0, canMatchEnd)
196 if loc[0] < firstLoc[0] {
202 if firstLoc[0] != len(line) {
204 highlights[start+runePos(firstLoc[0], string(line))] = firstRegion.group
206 h.highlightEmptyRegion(highlights, start, false, lineNum, line[:firstLoc[0]], statesOnly)
207 h.highlightRegion(highlights, start+runePos(firstLoc[1], string(line)), canMatchEnd, lineNum, line[firstLoc[1]:], firstRegion, statesOnly)
219 fullHighlights := make([]uint8, len(line))
220 for _, p := range h.def.rules.patterns {
221 matches := findAllIndex(p.regex, line, start == 0, canMatchEnd)
222 for _, m := range matches {
223 for i := m[0]; i < m[1]; i++ {
224 fullHighlights[i] = p.group
228 for i, h := range fullHighlights {
229 if i == 0 || h != fullHighlights[i-1] {
230 if _, ok := highlights[start+runePos(i, string(line))]; !ok {
231 highlights[start+runePos(i, string(line))] = h
243 // HighlightString syntax highlights a string
244 // Use this function for simple syntax highlighting and use the other functions for
245 // more advanced syntax highlighting. They are optimized for quick rehighlighting of the same
246 // text with minor changes made
247 func (h *Highlighter) HighlightString(input string) []LineMatch {
248 lines := strings.Split(input, "\n")
249 var lineMatches []LineMatch
251 for i := 0; i < len(lines); i++ {
252 line := []byte(lines[i])
253 highlights := make(LineMatch)
255 if i == 0 || h.lastRegion == nil {
256 lineMatches = append(lineMatches, h.highlightEmptyRegion(highlights, 0, true, i, line, false))
258 lineMatches = append(lineMatches, h.highlightRegion(highlights, 0, true, i, line, h.lastRegion, false))
265 // HighlightStates correctly sets all states for the buffer
266 func (h *Highlighter) HighlightStates(input LineStates) {
267 for i := 0; i < input.LinesNum(); i++ {
268 line := []byte(input.Line(i))
269 // highlights := make(LineMatch)
271 if i == 0 || h.lastRegion == nil {
272 h.highlightEmptyRegion(nil, 0, true, i, line, true)
274 h.highlightRegion(nil, 0, true, i, line, h.lastRegion, true)
277 curState := h.lastRegion
279 input.SetState(i, curState)
283 // HighlightMatches sets the matches for each line in between startline and endline
284 // It sets all other matches in the buffer to nil to conserve memory
285 // This assumes that all the states are set correctly
286 func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int) {
287 for i := startline; i < endline; i++ {
288 if i >= input.LinesNum() {
292 line := []byte(input.Line(i))
293 highlights := make(LineMatch)
296 if i == 0 || input.State(i-1) == nil {
297 match = h.highlightEmptyRegion(highlights, 0, true, i, line, false)
299 match = h.highlightRegion(highlights, 0, true, i, line, input.State(i-1), false)
302 input.SetMatch(i, match)
306 // ReHighlightStates will scan down from `startline` and set the appropriate end of line state
307 // for each line until it comes across the same state in two consecutive lines
308 func (h *Highlighter) ReHighlightStates(input LineStates, startline int) {
309 // lines := input.LineData()
313 h.lastRegion = input.State(startline - 1)
315 for i := startline; i < input.LinesNum(); i++ {
316 line := []byte(input.Line(i))
317 // highlights := make(LineMatch)
319 // var match LineMatch
320 if i == 0 || h.lastRegion == nil {
321 h.highlightEmptyRegion(nil, 0, true, i, line, true)
323 h.highlightRegion(nil, 0, true, i, line, h.lastRegion, true)
325 curState := h.lastRegion
326 lastState := input.State(i)
328 input.SetState(i, curState)
330 if curState == lastState {
336 // ReHighlightLine will rehighlight the state and match for a single line
337 func (h *Highlighter) ReHighlightLine(input LineStates, lineN int) {
338 line := []byte(input.Line(lineN))
339 highlights := make(LineMatch)
343 h.lastRegion = input.State(lineN - 1)
347 if lineN == 0 || h.lastRegion == nil {
348 match = h.highlightEmptyRegion(highlights, 0, true, lineN, line, false)
350 match = h.highlightRegion(highlights, 0, true, lineN, line, h.lastRegion, false)
352 curState := h.lastRegion
354 input.SetMatch(lineN, match)
355 input.SetState(lineN, curState)