]> git.lizzy.rs Git - micro.git/blob - cmd/micro/highlight/highlighter.go
Fix out of bounds error on syntax highlighting
[micro.git] / cmd / micro / highlight / highlighter.go
1 package highlight
2
3 import (
4         "regexp"
5         "strings"
6         "unicode/utf8"
7
8         "github.com/dlclark/regexp2"
9 )
10
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 {
14         if p < 0 {
15                 return 0
16         }
17         if p >= len(str) {
18                 return utf8.RuneCountInString(str)
19         }
20         return utf8.RuneCountInString(str[:p])
21 }
22
23 func combineLineMatch(src, dst LineMatch) LineMatch {
24         for k, v := range src {
25                 if g, ok := dst[k]; ok {
26                         if g == 0 {
27                                 dst[k] = v
28                         }
29                 } else {
30                         dst[k] = v
31                 }
32         }
33         return dst
34 }
35
36 // A State represents the region at the end of a line
37 type State *Region
38
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 {
41         Line(n int) string
42         LinesNum() int
43         State(lineN int) State
44         SetState(lineN int, s State)
45         SetMatch(lineN int, m LineMatch)
46 }
47
48 // A Highlighter contains the information needed to highlight a string
49 type Highlighter struct {
50         lastRegion *Region
51         def        *Def
52 }
53
54 // NewHighlighter returns a new highlighter from the given syntax definition
55 func NewHighlighter(def *Def) *Highlighter {
56         h := new(Highlighter)
57         h.def = def
58         return h
59 }
60
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
64
65 func findIndex(regex *regexp2.Regexp, str []byte, canMatchStart, canMatchEnd bool) []int {
66         regexStr := regex.String()
67         if strings.Contains(regexStr, "^") {
68                 if !canMatchStart {
69                         return nil
70                 }
71         }
72         if strings.Contains(regexStr, "$") {
73                 if !canMatchEnd {
74                         return nil
75                 }
76         }
77         match, _ := regex.FindStringMatch(string(str))
78         if match == nil {
79                 return nil
80         }
81         return []int{match.Index, match.Index + match.Length}
82 }
83
84 func findAllIndex(regex *regexp.Regexp, str []byte, canMatchStart, canMatchEnd bool) [][]int {
85         regexStr := regex.String()
86         if strings.Contains(regexStr, "^") {
87                 if !canMatchStart {
88                         return nil
89                 }
90         }
91         if strings.Contains(regexStr, "$") {
92                 if !canMatchEnd {
93                         return nil
94                 }
95         }
96         return regex.FindAllIndex([]byte(string(str)), -1)
97 }
98
99 func (h *Highlighter) highlightRegion(highlights LineMatch, start int, canMatchEnd bool, lineNum int, line []byte, region *Region, statesOnly bool) LineMatch {
100         // highlights := make(LineMatch)
101
102         if start == 0 {
103                 if !statesOnly {
104                         highlights[0] = region.group
105                 }
106         }
107
108         loc := findIndex(region.end, line, start == 0, canMatchEnd)
109         if loc != nil {
110                 if !statesOnly {
111                         highlights[start+runePos(loc[1]-1, string(line))] = region.group
112                 }
113                 if region.parent == nil {
114                         if !statesOnly {
115                                 highlights[start+runePos(loc[1], string(line))] = 0
116                                 h.highlightRegion(highlights, start, false, lineNum, line[:loc[0]], region, statesOnly)
117                         }
118                         h.highlightEmptyRegion(highlights, start+runePos(loc[1], string(line)), canMatchEnd, lineNum, line[loc[1]:], statesOnly)
119                         return highlights
120                 }
121                 if !statesOnly {
122                         highlights[start+runePos(loc[1], string(line))] = region.parent.group
123                         h.highlightRegion(highlights, start, false, lineNum, line[:loc[0]], region, statesOnly)
124                 }
125                 h.highlightRegion(highlights, start+runePos(loc[1], string(line)), canMatchEnd, lineNum, line[loc[1]:], region.parent, statesOnly)
126                 return highlights
127         }
128
129         if len(line) == 0 || statesOnly {
130                 if canMatchEnd {
131                         h.lastRegion = region
132                 }
133
134                 return highlights
135         }
136
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)
141                 if loc != nil {
142                         if loc[0] < firstLoc[0] {
143                                 firstLoc = loc
144                                 firstRegion = r
145                         }
146                 }
147         }
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)
152                 return highlights
153         }
154
155         fullHighlights := make([]uint8, len(line))
156         for i := 0; i < len(fullHighlights); i++ {
157                 fullHighlights[i] = region.group
158         }
159
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
165                         }
166                 }
167         }
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
172                         }
173                 }
174         }
175
176         if canMatchEnd {
177                 h.lastRegion = region
178         }
179
180         return highlights
181 }
182
183 func (h *Highlighter) highlightEmptyRegion(highlights LineMatch, start int, canMatchEnd bool, lineNum int, line []byte, statesOnly bool) LineMatch {
184         if len(line) == 0 {
185                 if canMatchEnd {
186                         h.lastRegion = nil
187                 }
188                 return highlights
189         }
190
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)
195                 if loc != nil {
196                         if loc[0] < firstLoc[0] {
197                                 firstLoc = loc
198                                 firstRegion = r
199                         }
200                 }
201         }
202         if firstLoc[0] != len(line) {
203                 if !statesOnly {
204                         highlights[start+runePos(firstLoc[0], string(line))] = firstRegion.group
205                 }
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)
208                 return highlights
209         }
210
211         if statesOnly {
212                 if canMatchEnd {
213                         h.lastRegion = nil
214                 }
215
216                 return highlights
217         }
218
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
225                         }
226                 }
227         }
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
232                         }
233                 }
234         }
235
236         if canMatchEnd {
237                 h.lastRegion = nil
238         }
239
240         return highlights
241 }
242
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
250
251         for i := 0; i < len(lines); i++ {
252                 line := []byte(lines[i])
253                 highlights := make(LineMatch)
254
255                 if i == 0 || h.lastRegion == nil {
256                         lineMatches = append(lineMatches, h.highlightEmptyRegion(highlights, 0, true, i, line, false))
257                 } else {
258                         lineMatches = append(lineMatches, h.highlightRegion(highlights, 0, true, i, line, h.lastRegion, false))
259                 }
260         }
261
262         return lineMatches
263 }
264
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)
270
271                 if i == 0 || h.lastRegion == nil {
272                         h.highlightEmptyRegion(nil, 0, true, i, line, true)
273                 } else {
274                         h.highlightRegion(nil, 0, true, i, line, h.lastRegion, true)
275                 }
276
277                 curState := h.lastRegion
278
279                 input.SetState(i, curState)
280         }
281 }
282
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() {
289                         break
290                 }
291
292                 line := []byte(input.Line(i))
293                 highlights := make(LineMatch)
294
295                 var match LineMatch
296                 if i == 0 || input.State(i-1) == nil {
297                         match = h.highlightEmptyRegion(highlights, 0, true, i, line, false)
298                 } else {
299                         match = h.highlightRegion(highlights, 0, true, i, line, input.State(i-1), false)
300                 }
301
302                 input.SetMatch(i, match)
303         }
304 }
305
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()
310
311         h.lastRegion = nil
312         if startline > 0 {
313                 h.lastRegion = input.State(startline - 1)
314         }
315         for i := startline; i < input.LinesNum(); i++ {
316                 line := []byte(input.Line(i))
317                 // highlights := make(LineMatch)
318
319                 // var match LineMatch
320                 if i == 0 || h.lastRegion == nil {
321                         h.highlightEmptyRegion(nil, 0, true, i, line, true)
322                 } else {
323                         h.highlightRegion(nil, 0, true, i, line, h.lastRegion, true)
324                 }
325                 curState := h.lastRegion
326                 lastState := input.State(i)
327
328                 input.SetState(i, curState)
329
330                 if curState == lastState {
331                         break
332                 }
333         }
334 }
335
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)
340
341         h.lastRegion = nil
342         if lineN > 0 {
343                 h.lastRegion = input.State(lineN - 1)
344         }
345
346         var match LineMatch
347         if lineN == 0 || h.lastRegion == nil {
348                 match = h.highlightEmptyRegion(highlights, 0, true, lineN, line, false)
349         } else {
350                 match = h.highlightRegion(highlights, 0, true, lineN, line, h.lastRegion, false)
351         }
352         curState := h.lastRegion
353
354         input.SetMatch(lineN, match)
355         input.SetState(lineN, curState)
356 }