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