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