]> git.lizzy.rs Git - micro.git/blob - cmd/micro/highlight/highlighter.go
Fix possible overflow error
[micro.git] / cmd / micro / highlight / highlighter.go
1 package highlight
2
3 import (
4         "regexp"
5         "strings"
6
7         "github.com/dlclark/regexp2"
8 )
9
10 func combineLineMatch(src, dst LineMatch) LineMatch {
11         for k, v := range src {
12                 if g, ok := dst[k]; ok {
13                         if g == 0 {
14                                 dst[k] = v
15                         }
16                 } else {
17                         dst[k] = v
18                 }
19         }
20         return dst
21 }
22
23 // A State represents the region at the end of a line
24 type State *Region
25
26 // LineStates is an interface for a buffer-like object which can also store the states and matches for every line
27 type LineStates interface {
28         Line(n int) string
29         LinesNum() int
30         State(lineN int) State
31         SetState(lineN int, s State)
32         SetMatch(lineN int, m LineMatch)
33 }
34
35 // A Highlighter contains the information needed to highlight a string
36 type Highlighter struct {
37         lastRegion *Region
38         def        *Def
39 }
40
41 // NewHighlighter returns a new highlighter from the given syntax definition
42 func NewHighlighter(def *Def) *Highlighter {
43         h := new(Highlighter)
44         h.def = def
45         return h
46 }
47
48 // LineMatch represents the syntax highlighting matches for one line. Each index where the coloring is changed is marked with that
49 // color's group (represented as one byte)
50 type LineMatch map[int]uint8
51
52 func findIndex(regex *regexp2.Regexp, str []byte, canMatchStart, canMatchEnd bool) []int {
53         regexStr := regex.String()
54         if strings.Contains(regexStr, "^") {
55                 if !canMatchStart {
56                         return nil
57                 }
58         }
59         if strings.Contains(regexStr, "$") {
60                 if !canMatchEnd {
61                         return nil
62                 }
63         }
64         match, _ := regex.FindStringMatch(string(str))
65         if match == nil {
66                 return nil
67         }
68         return []int{match.Index, match.Index + match.Length}
69 }
70
71 func findAllIndex(regex *regexp.Regexp, str []byte, canMatchStart, canMatchEnd bool) [][]int {
72         regexStr := regex.String()
73         if strings.Contains(regexStr, "^") {
74                 if !canMatchStart {
75                         return nil
76                 }
77         }
78         if strings.Contains(regexStr, "$") {
79                 if !canMatchEnd {
80                         return nil
81                 }
82         }
83         return regex.FindAllIndex(str, -1)
84 }
85
86 func (h *Highlighter) highlightRegion(highlights LineMatch, start int, canMatchEnd bool, lineNum int, line []byte, region *Region, statesOnly bool) LineMatch {
87         // highlights := make(LineMatch)
88
89         if start == 0 {
90                 if !statesOnly {
91                         highlights[0] = region.group
92                 }
93         }
94
95         loc := findIndex(region.end, line, start == 0, canMatchEnd)
96         if loc != nil {
97                 if !statesOnly {
98                         highlights[start+loc[1]-1] = region.group
99                 }
100                 if region.parent == nil {
101                         if !statesOnly {
102                                 highlights[start+loc[1]] = 0
103                         }
104                         h.highlightRegion(highlights, start, false, lineNum, line[:loc[0]], region, statesOnly)
105                         h.highlightEmptyRegion(highlights, start+loc[1], canMatchEnd, lineNum, line[loc[1]:], statesOnly)
106                         return highlights
107                 }
108                 if !statesOnly {
109                         highlights[start+loc[1]] = region.parent.group
110                 }
111                 h.highlightRegion(highlights, start, false, lineNum, line[:loc[0]], region, statesOnly)
112                 h.highlightRegion(highlights, start+loc[1], canMatchEnd, lineNum, line[loc[1]:], region.parent, statesOnly)
113                 return highlights
114         }
115
116         if len(line) == 0 || statesOnly {
117                 if canMatchEnd {
118                         h.lastRegion = region
119                 }
120
121                 return highlights
122         }
123
124         firstLoc := []int{len(line), 0}
125         var firstRegion *Region
126         for _, r := range region.rules.regions {
127                 loc := findIndex(r.start, line, start == 0, canMatchEnd)
128                 if loc != nil {
129                         if loc[0] < firstLoc[0] {
130                                 firstLoc = loc
131                                 firstRegion = r
132                         }
133                 }
134         }
135         if firstLoc[0] != len(line) {
136                 highlights[start+firstLoc[0]] = firstRegion.group
137                 h.highlightRegion(highlights, start, false, lineNum, line[:firstLoc[0]], region, statesOnly)
138                 h.highlightRegion(highlights, start+firstLoc[1], canMatchEnd, lineNum, line[firstLoc[1]:], firstRegion, statesOnly)
139                 return highlights
140         }
141
142         fullHighlights := make([]uint8, len([]rune(string(line))))
143         for i := 0; i < len(fullHighlights); i++ {
144                 fullHighlights[i] = region.group
145         }
146
147         for _, p := range region.rules.patterns {
148                 matches := findAllIndex(p.regex, line, start == 0, canMatchEnd)
149                 for _, m := range matches {
150                         for i := m[0]; i < m[1]; i++ {
151                                 fullHighlights[i] = p.group
152                         }
153                 }
154         }
155         for i, h := range fullHighlights {
156                 if i == 0 || h != fullHighlights[i-1] {
157                         if _, ok := highlights[start+i]; !ok {
158                                 highlights[start+i] = h
159                         }
160                 }
161         }
162
163         if canMatchEnd {
164                 h.lastRegion = region
165         }
166
167         return highlights
168 }
169
170 func (h *Highlighter) highlightEmptyRegion(highlights LineMatch, start int, canMatchEnd bool, lineNum int, line []byte, statesOnly bool) LineMatch {
171         if len(line) == 0 {
172                 if canMatchEnd {
173                         h.lastRegion = nil
174                 }
175                 return highlights
176         }
177
178         firstLoc := []int{len(line), 0}
179         var firstRegion *Region
180         for _, r := range h.def.rules.regions {
181                 loc := findIndex(r.start, line, start == 0, canMatchEnd)
182                 if loc != nil {
183                         if loc[0] < firstLoc[0] {
184                                 firstLoc = loc
185                                 firstRegion = r
186                         }
187                 }
188         }
189         if firstLoc[0] != len(line) {
190                 if !statesOnly {
191                         highlights[start+firstLoc[0]] = firstRegion.group
192                 }
193                 h.highlightEmptyRegion(highlights, start, false, lineNum, line[:firstLoc[0]], statesOnly)
194                 h.highlightRegion(highlights, start+firstLoc[1], canMatchEnd, lineNum, line[firstLoc[1]:], firstRegion, statesOnly)
195                 return highlights
196         }
197
198         if statesOnly {
199                 if canMatchEnd {
200                         h.lastRegion = nil
201                 }
202
203                 return highlights
204         }
205
206         fullHighlights := make([]uint8, len(line))
207         for _, p := range h.def.rules.patterns {
208                 matches := findAllIndex(p.regex, line, start == 0, canMatchEnd)
209                 for _, m := range matches {
210                         for i := m[0]; i < m[1]; i++ {
211                                 fullHighlights[i] = p.group
212                         }
213                 }
214         }
215         for i, h := range fullHighlights {
216                 if i == 0 || h != fullHighlights[i-1] {
217                         if _, ok := highlights[start+i]; !ok {
218                                 highlights[start+i] = h
219                         }
220                 }
221         }
222
223         if canMatchEnd {
224                 h.lastRegion = nil
225         }
226
227         return highlights
228 }
229
230 // HighlightString syntax highlights a string
231 // Use this function for simple syntax highlighting and use the other functions for
232 // more advanced syntax highlighting. They are optimized for quick rehighlighting of the same
233 // text with minor changes made
234 func (h *Highlighter) HighlightString(input string) []LineMatch {
235         lines := strings.Split(input, "\n")
236         var lineMatches []LineMatch
237
238         for i := 0; i < len(lines); i++ {
239                 line := []byte(lines[i])
240                 highlights := make(LineMatch)
241
242                 if i == 0 || h.lastRegion == nil {
243                         lineMatches = append(lineMatches, h.highlightEmptyRegion(highlights, 0, true, i, line, false))
244                 } else {
245                         lineMatches = append(lineMatches, h.highlightRegion(highlights, 0, true, i, line, h.lastRegion, false))
246                 }
247         }
248
249         return lineMatches
250 }
251
252 // HighlightStates correctly sets all states for the buffer
253 func (h *Highlighter) HighlightStates(input LineStates) {
254         for i := 0; i < input.LinesNum(); i++ {
255                 line := []byte(input.Line(i))
256                 // highlights := make(LineMatch)
257
258                 if i == 0 || h.lastRegion == nil {
259                         h.highlightEmptyRegion(nil, 0, true, i, line, true)
260                 } else {
261                         h.highlightRegion(nil, 0, true, i, line, h.lastRegion, true)
262                 }
263
264                 curState := h.lastRegion
265
266                 input.SetState(i, curState)
267         }
268 }
269
270 // HighlightMatches sets the matches for each line in between startline and endline
271 // It sets all other matches in the buffer to nil to conserve memory
272 // This assumes that all the states are set correctly
273 func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int) {
274         for i := startline; i < endline; i++ {
275                 if i >= input.LinesNum() {
276                         break
277                 }
278
279                 line := []byte(input.Line(i))
280                 highlights := make(LineMatch)
281
282                 var match LineMatch
283                 if i == 0 || input.State(i-1) == nil {
284                         match = h.highlightEmptyRegion(highlights, 0, true, i, line, false)
285                 } else {
286                         match = h.highlightRegion(highlights, 0, true, i, line, input.State(i-1), false)
287                 }
288
289                 input.SetMatch(i, match)
290         }
291 }
292
293 // ReHighlightStates will scan down from `startline` and set the appropriate end of line state
294 // for each line until it comes across the same state in two consecutive lines
295 func (h *Highlighter) ReHighlightStates(input LineStates, startline int) {
296         // lines := input.LineData()
297
298         h.lastRegion = nil
299         if startline > 0 {
300                 h.lastRegion = input.State(startline - 1)
301         }
302         for i := startline; i < input.LinesNum(); i++ {
303                 line := []byte(input.Line(i))
304                 // highlights := make(LineMatch)
305
306                 // var match LineMatch
307                 if i == 0 || h.lastRegion == nil {
308                         h.highlightEmptyRegion(nil, 0, true, i, line, true)
309                 } else {
310                         h.highlightRegion(nil, 0, true, i, line, h.lastRegion, true)
311                 }
312                 curState := h.lastRegion
313                 lastState := input.State(i)
314
315                 input.SetState(i, curState)
316
317                 if curState == lastState {
318                         break
319                 }
320         }
321 }
322
323 // ReHighlightLine will rehighlight the state and match for a single line
324 func (h *Highlighter) ReHighlightLine(input LineStates, lineN int) {
325         line := []byte(input.Line(lineN))
326         highlights := make(LineMatch)
327
328         h.lastRegion = nil
329         if lineN > 0 {
330                 h.lastRegion = input.State(lineN - 1)
331         }
332
333         var match LineMatch
334         if lineN == 0 || h.lastRegion == nil {
335                 match = h.highlightEmptyRegion(highlights, 0, true, lineN, line, false)
336         } else {
337                 match = h.highlightRegion(highlights, 0, true, lineN, line, h.lastRegion, false)
338         }
339         curState := h.lastRegion
340
341         input.SetMatch(lineN, match)
342         input.SetState(lineN, curState)
343 }