]> git.lizzy.rs Git - micro.git/blob - cmd/micro/highlight/highlighter.go
Fix small issue with regions
[micro.git] / cmd / micro / highlight / highlighter.go
1 package highlight
2
3 import (
4         "regexp"
5         "strings"
6 )
7
8 func combineLineMatch(src, dst LineMatch) LineMatch {
9         for k, v := range src {
10                 if g, ok := dst[k]; ok {
11                         if g == "" {
12                                 dst[k] = v
13                         }
14                 } else {
15                         dst[k] = v
16                 }
17         }
18         return dst
19 }
20
21 type State *Region
22
23 type LineStates interface {
24         LineData() [][]byte
25         State(lineN int) State
26         SetState(lineN int, s State)
27         SetMatch(lineN int, m LineMatch)
28 }
29
30 type Highlighter struct {
31         lastRegion *Region
32         def        *Def
33 }
34
35 func NewHighlighter(def *Def) *Highlighter {
36         h := new(Highlighter)
37         h.def = def
38         return h
39 }
40
41 type LineMatch map[int]string
42
43 func FindIndex(regex *regexp.Regexp, str []byte, canMatchStart, canMatchEnd bool) []int {
44         regexStr := regex.String()
45         if strings.Contains(regexStr, "^") {
46                 if !canMatchStart {
47                         return nil
48                 }
49         }
50         if strings.Contains(regexStr, "$") {
51                 if !canMatchEnd {
52                         return nil
53                 }
54         }
55         return regex.FindIndex(str)
56 }
57
58 func FindAllIndex(regex *regexp.Regexp, str []byte, canMatchStart, canMatchEnd bool) [][]int {
59         regexStr := regex.String()
60         if strings.Contains(regexStr, "^") {
61                 if !canMatchStart {
62                         return nil
63                 }
64         }
65         if strings.Contains(regexStr, "$") {
66                 if !canMatchEnd {
67                         return nil
68                 }
69         }
70         return regex.FindAllIndex(str, -1)
71 }
72
73 func (h *Highlighter) highlightRegion(start int, canMatchEnd bool, lineNum int, line []byte, region *Region) LineMatch {
74         highlights := make(LineMatch)
75
76         loc := FindIndex(region.end, line, start == 0, canMatchEnd)
77         if loc != nil {
78                 if region.parent == nil {
79                         highlights[start+loc[1]] = ""
80                         return combineLineMatch(highlights,
81                                 combineLineMatch(h.highlightRegion(start, false, lineNum, line[:loc[0]], region),
82                                         h.highlightEmptyRegion(start+loc[1], canMatchEnd, lineNum, line[loc[1]:])))
83                 }
84                 highlights[start+loc[1]] = region.parent.group
85                 return combineLineMatch(highlights,
86                         combineLineMatch(h.highlightRegion(start, false, lineNum, line[:loc[0]], region),
87                                 h.highlightRegion(start+loc[1], canMatchEnd, lineNum, line[loc[1]:], region.parent)))
88         }
89
90         if len(line) == 0 {
91                 if canMatchEnd {
92                         h.lastRegion = region
93                 }
94
95                 return highlights
96         }
97
98         firstLoc := []int{len(line), 0}
99         var firstRegion *Region
100         for _, r := range region.rules.regions {
101                 loc := FindIndex(r.start, line, start == 0, canMatchEnd)
102                 if loc != nil {
103                         if loc[0] < firstLoc[0] {
104                                 firstLoc = loc
105                                 firstRegion = r
106                         }
107                 }
108         }
109         if firstLoc[0] != len(line) {
110                 highlights[start+firstLoc[0]] = firstRegion.group
111                 return combineLineMatch(highlights,
112                         combineLineMatch(h.highlightRegion(start, false, lineNum, line[:firstLoc[0]], region),
113                                 h.highlightRegion(start+firstLoc[1], canMatchEnd, lineNum, line[firstLoc[1]:], firstRegion)))
114         }
115
116         for _, p := range region.rules.patterns {
117                 matches := FindAllIndex(p.regex, line, start == 0, canMatchEnd)
118                 for _, m := range matches {
119                         highlights[start+m[0]] = p.group
120                         if _, ok := highlights[start+m[1]]; !ok {
121                                 highlights[start+m[1]] = region.group
122                         }
123                 }
124         }
125
126         if canMatchEnd {
127                 h.lastRegion = region
128         }
129
130         return highlights
131 }
132
133 func (h *Highlighter) highlightEmptyRegion(start int, canMatchEnd bool, lineNum int, line []byte) LineMatch {
134         highlights := make(LineMatch)
135         if len(line) == 0 {
136                 if canMatchEnd {
137                         h.lastRegion = nil
138                 }
139                 return highlights
140         }
141
142         firstLoc := []int{len(line), 0}
143         var firstRegion *Region
144         for _, r := range h.def.rules.regions {
145                 loc := FindIndex(r.start, line, start == 0, canMatchEnd)
146                 if loc != nil {
147                         if loc[0] < firstLoc[0] {
148                                 firstLoc = loc
149                                 firstRegion = r
150                         }
151                 }
152         }
153         if firstLoc[0] != len(line) {
154                 highlights[start+firstLoc[0]] = firstRegion.group
155                 return combineLineMatch(highlights,
156                         combineLineMatch(h.highlightEmptyRegion(start, false, lineNum, line[:firstLoc[0]]),
157                                 h.highlightRegion(start+firstLoc[1], canMatchEnd, lineNum, line[firstLoc[1]:], firstRegion)))
158         }
159
160         for _, p := range h.def.rules.patterns {
161                 matches := FindAllIndex(p.regex, line, start == 0, canMatchEnd)
162                 for _, m := range matches {
163                         highlights[start+m[0]] = p.group
164                         if _, ok := highlights[start+m[1]]; !ok {
165                                 highlights[start+m[1]] = ""
166                         }
167                 }
168         }
169
170         if canMatchEnd {
171                 h.lastRegion = nil
172         }
173
174         return highlights
175 }
176
177 func (h *Highlighter) HighlightString(input string) []LineMatch {
178         lines := strings.Split(input, "\n")
179         var lineMatches []LineMatch
180
181         for i := 0; i < len(lines); i++ {
182                 line := []byte(lines[i])
183
184                 if i == 0 || h.lastRegion == nil {
185                         lineMatches = append(lineMatches, h.highlightEmptyRegion(0, true, i, line))
186                 } else {
187                         lineMatches = append(lineMatches, h.highlightRegion(0, true, i, line, h.lastRegion))
188                 }
189         }
190
191         return lineMatches
192 }
193
194 func (h *Highlighter) Highlight(input LineStates, startline int) {
195         lines := input.LineData()
196
197         for i := startline; i < len(lines); i++ {
198                 line := []byte(lines[i])
199
200                 var match LineMatch
201                 if i == 0 || h.lastRegion == nil {
202                         match = h.highlightEmptyRegion(0, true, i, line)
203                 } else {
204                         match = h.highlightRegion(0, true, i, line, h.lastRegion)
205                 }
206
207                 curState := h.lastRegion
208
209                 input.SetMatch(i, match)
210                 input.SetState(i, curState)
211         }
212 }
213
214 func (h *Highlighter) ReHighlightLine(input LineStates, lineN int) {
215         lines := input.LineData()
216
217         line := []byte(lines[lineN])
218
219         h.lastRegion = nil
220         if lineN > 0 {
221                 h.lastRegion = input.State(lineN - 1)
222         }
223
224         var match LineMatch
225         if lineN == 0 || h.lastRegion == nil {
226                 match = h.highlightEmptyRegion(0, true, lineN, line)
227         } else {
228                 match = h.highlightRegion(0, true, lineN, line, h.lastRegion)
229         }
230         curState := h.lastRegion
231
232         input.SetMatch(lineN, match)
233         input.SetState(lineN, curState)
234 }
235
236 func (h *Highlighter) ReHighlight(input LineStates, startline int) {
237         lines := input.LineData()
238
239         h.lastRegion = nil
240         if startline > 0 {
241                 h.lastRegion = input.State(startline - 1)
242         }
243         for i := startline; i < len(lines); i++ {
244                 line := []byte(lines[i])
245
246                 var match LineMatch
247                 if i == 0 || h.lastRegion == nil {
248                         match = h.highlightEmptyRegion(0, true, i, line)
249                 } else {
250                         match = h.highlightRegion(0, true, i, line, h.lastRegion)
251                 }
252                 curState := h.lastRegion
253                 lastState := input.State(i)
254
255                 input.SetMatch(i, match)
256                 input.SetState(i, curState)
257
258                 if curState == lastState {
259                         break
260                 }
261         }
262 }