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