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