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