]> git.lizzy.rs Git - micro.git/blob - pkg/highlight/highlighter.go
Merge branch 'master' into highlight-nimscript
[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 || statesOnly {
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                 highlights[start+firstLoc[0]] = firstRegion.limitGroup
201                 h.highlightRegion(highlights, start, false, lineNum, sliceEnd(line, firstLoc[0]), curRegion, statesOnly)
202                 h.highlightRegion(highlights, start+firstLoc[1], canMatchEnd, lineNum, sliceStart(line, firstLoc[1]), firstRegion, statesOnly)
203                 return highlights
204         }
205
206         fullHighlights := make([]Group, lineLen)
207         for i := 0; i < len(fullHighlights); i++ {
208                 fullHighlights[i] = curRegion.group
209         }
210
211         for _, p := range curRegion.rules.patterns {
212                 matches := findAllIndex(p.regex, line, start == 0, canMatchEnd)
213                 for _, m := range matches {
214                         for i := m[0]; i < m[1]; i++ {
215                                 fullHighlights[i] = p.group
216                         }
217                 }
218         }
219         for i, h := range fullHighlights {
220                 if i == 0 || h != fullHighlights[i-1] {
221                         highlights[start+i] = h
222                 }
223         }
224
225         if canMatchEnd {
226                 h.lastRegion = curRegion
227         }
228
229         return highlights
230 }
231
232 func (h *Highlighter) highlightEmptyRegion(highlights LineMatch, start int, canMatchEnd bool, lineNum int, line []byte, statesOnly bool) LineMatch {
233         lineLen := utf8.RuneCount(line)
234         if lineLen == 0 {
235                 if canMatchEnd {
236                         h.lastRegion = nil
237                 }
238                 return highlights
239         }
240
241         firstLoc := []int{lineLen, 0}
242         var firstRegion *region
243         for _, r := range h.Def.rules.regions {
244                 loc := findIndex(r.start, nil, line, start == 0, canMatchEnd)
245                 if loc != nil {
246                         if loc[0] < firstLoc[0] {
247                                 firstLoc = loc
248                                 firstRegion = r
249                         }
250                 }
251         }
252         if firstLoc[0] != lineLen {
253                 if !statesOnly {
254                         highlights[start+firstLoc[0]] = firstRegion.limitGroup
255                 }
256                 h.highlightEmptyRegion(highlights, start, false, lineNum, sliceEnd(line, firstLoc[0]), statesOnly)
257                 h.highlightRegion(highlights, start+firstLoc[1], canMatchEnd, lineNum, sliceStart(line, firstLoc[1]), firstRegion, statesOnly)
258                 return highlights
259         }
260
261         if statesOnly {
262                 if canMatchEnd {
263                         h.lastRegion = nil
264                 }
265
266                 return highlights
267         }
268
269         fullHighlights := make([]Group, len(line))
270         for _, p := range h.Def.rules.patterns {
271                 matches := findAllIndex(p.regex, line, start == 0, canMatchEnd)
272                 for _, m := range matches {
273                         for i := m[0]; i < m[1]; i++ {
274                                 fullHighlights[i] = p.group
275                         }
276                 }
277         }
278         for i, h := range fullHighlights {
279                 if i == 0 || h != fullHighlights[i-1] {
280                         // if _, ok := highlights[start+i]; !ok {
281                         highlights[start+i] = h
282                         // }
283                 }
284         }
285
286         if canMatchEnd {
287                 h.lastRegion = nil
288         }
289
290         return highlights
291 }
292
293 // HighlightString syntax highlights a string
294 // Use this function for simple syntax highlighting and use the other functions for
295 // more advanced syntax highlighting. They are optimized for quick rehighlighting of the same
296 // text with minor changes made
297 func (h *Highlighter) HighlightString(input string) []LineMatch {
298         lines := strings.Split(input, "\n")
299         var lineMatches []LineMatch
300
301         for i := 0; i < len(lines); i++ {
302                 line := []byte(lines[i])
303                 highlights := make(LineMatch)
304
305                 if i == 0 || h.lastRegion == nil {
306                         lineMatches = append(lineMatches, h.highlightEmptyRegion(highlights, 0, true, i, line, false))
307                 } else {
308                         lineMatches = append(lineMatches, h.highlightRegion(highlights, 0, true, i, line, h.lastRegion, false))
309                 }
310         }
311
312         return lineMatches
313 }
314
315 // HighlightStates correctly sets all states for the buffer
316 func (h *Highlighter) HighlightStates(input LineStates) {
317         for i := 0; i < input.LinesNum(); i++ {
318                 line := input.LineBytes(i)
319                 // highlights := make(LineMatch)
320
321                 if i == 0 || h.lastRegion == nil {
322                         h.highlightEmptyRegion(nil, 0, true, i, line, true)
323                 } else {
324                         h.highlightRegion(nil, 0, true, i, line, h.lastRegion, true)
325                 }
326
327                 curState := h.lastRegion
328
329                 input.SetState(i, curState)
330         }
331 }
332
333 // HighlightMatches sets the matches for each line in between startline and endline
334 // It sets all other matches in the buffer to nil to conserve memory
335 // This assumes that all the states are set correctly
336 func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int) {
337         for i := startline; i < endline; i++ {
338                 if i >= input.LinesNum() {
339                         break
340                 }
341
342                 line := input.LineBytes(i)
343                 highlights := make(LineMatch)
344
345                 var match LineMatch
346                 if i == 0 || input.State(i-1) == nil {
347                         match = h.highlightEmptyRegion(highlights, 0, true, i, line, false)
348                 } else {
349                         match = h.highlightRegion(highlights, 0, true, i, line, input.State(i-1), false)
350                 }
351
352                 input.SetMatch(i, match)
353         }
354 }
355
356 // ReHighlightStates will scan down from `startline` and set the appropriate end of line state
357 // for each line until it comes across the same state in two consecutive lines
358 func (h *Highlighter) ReHighlightStates(input LineStates, startline int) {
359         // lines := input.LineData()
360
361         h.lastRegion = nil
362         if startline > 0 {
363                 h.lastRegion = input.State(startline - 1)
364         }
365         for i := startline; i < input.LinesNum(); i++ {
366                 line := input.LineBytes(i)
367                 // highlights := make(LineMatch)
368
369                 // var match LineMatch
370                 if i == 0 || h.lastRegion == nil {
371                         h.highlightEmptyRegion(nil, 0, true, i, line, true)
372                 } else {
373                         h.highlightRegion(nil, 0, true, i, line, h.lastRegion, true)
374                 }
375                 curState := h.lastRegion
376                 lastState := input.State(i)
377
378                 input.SetState(i, curState)
379
380                 if curState == lastState {
381                         break
382                 }
383         }
384 }
385
386 // ReHighlightLine will rehighlight the state and match for a single line
387 func (h *Highlighter) ReHighlightLine(input LineStates, lineN int) {
388         line := input.LineBytes(lineN)
389         highlights := make(LineMatch)
390
391         h.lastRegion = nil
392         if lineN > 0 {
393                 h.lastRegion = input.State(lineN - 1)
394         }
395
396         var match LineMatch
397         if lineN == 0 || h.lastRegion == nil {
398                 match = h.highlightEmptyRegion(highlights, 0, true, lineN, line, false)
399         } else {
400                 match = h.highlightRegion(highlights, 0, true, lineN, line, h.lastRegion, false)
401         }
402         curState := h.lastRegion
403
404         input.SetMatch(lineN, match)
405         input.SetState(lineN, curState)
406 }