7 "github.com/dlclark/regexp2"
10 func combineLineMatch(src, dst LineMatch) LineMatch {
11 for k, v := range src {
12 if g, ok := dst[k]; ok {
23 // A State represents the region at the end of a line
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 {
29 State(lineN int) State
30 SetState(lineN int, s State)
31 SetMatch(lineN int, m LineMatch)
34 // A Highlighter contains the information needed to highlight a string
35 type Highlighter struct {
40 // NewHighlighter returns a new highlighter from the given syntax definition
41 func NewHighlighter(def *Def) *Highlighter {
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
51 func findIndex(regex *regexp2.Regexp, str []byte, canMatchStart, canMatchEnd bool) []int {
52 regexStr := regex.String()
53 if strings.Contains(regexStr, "^") {
58 if strings.Contains(regexStr, "$") {
63 match, _ := regex.FindStringMatch(string(str))
67 return []int{match.Index, match.Index + match.Length}
70 func findAllIndex(regex *regexp.Regexp, str []byte, canMatchStart, canMatchEnd bool) [][]int {
71 regexStr := regex.String()
72 if strings.Contains(regexStr, "^") {
77 if strings.Contains(regexStr, "$") {
82 return regex.FindAllIndex(str, -1)
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
91 highlights := make(LineMatch)
94 highlights[0] = region.group
97 loc := findIndex(region.end, line, start == 0, canMatchEnd)
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]:])))
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)))
114 h.lastRegion = region
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)
125 if loc[0] < firstLoc[0] {
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)))
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
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
155 h.lastRegion = region
161 func (h *Highlighter) highlightEmptyRegion(start int, canMatchEnd bool, lineNum int, line []byte) LineMatch {
162 fullHighlights := make([]uint8, len(line))
163 highlights := make(LineMatch)
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)
176 if loc[0] < firstLoc[0] {
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)))
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
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
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
220 for i := 0; i < len(lines); i++ {
221 line := []byte(lines[i])
223 if i == 0 || h.lastRegion == nil {
224 lineMatches = append(lineMatches, h.highlightEmptyRegion(0, true, i, line))
226 lineMatches = append(lineMatches, h.highlightRegion(0, true, i, line, h.lastRegion))
233 // HighlightStates correctly sets all states for the buffer
234 func (h *Highlighter) HighlightStates(input LineStates) {
235 lines := input.LineData()
237 for i := 0; i < len(lines); i++ {
238 line := []byte(lines[i])
240 if i == 0 || h.lastRegion == nil {
241 h.highlightEmptyRegion(0, true, i, line)
243 h.highlightRegion(0, true, i, line, h.lastRegion)
246 curState := h.lastRegion
248 input.SetState(i, curState)
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()
258 for i := 0; i < len(lines); i++ {
259 if i >= startline && i < endline {
260 line := []byte(lines[i])
263 if i == 0 || input.State(i-1) == nil {
264 match = h.highlightEmptyRegion(0, true, i, line)
266 match = h.highlightRegion(0, true, i, line, input.State(i-1))
269 input.SetMatch(i, match)
271 input.SetMatch(i, nil)
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()
283 h.lastRegion = input.State(startline - 1)
285 for i := startline; i < len(lines); i++ {
286 line := []byte(lines[i])
288 // var match LineMatch
289 if i == 0 || h.lastRegion == nil {
290 h.highlightEmptyRegion(0, true, i, line)
292 h.highlightRegion(0, true, i, line, h.lastRegion)
294 curState := h.lastRegion
295 lastState := input.State(i)
297 input.SetState(i, curState)
299 if curState == lastState {
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()
309 line := []byte(lines[lineN])
313 h.lastRegion = input.State(lineN - 1)
317 if lineN == 0 || h.lastRegion == nil {
318 match = h.highlightEmptyRegion(0, true, lineN, line)
320 match = h.highlightRegion(0, true, lineN, line, h.lastRegion)
322 curState := h.lastRegion
324 input.SetMatch(lineN, match)
325 input.SetState(lineN, curState)