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