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 {
30 State(lineN int) State
31 SetState(lineN int, s State)
32 SetMatch(lineN int, m LineMatch)
35 // A Highlighter contains the information needed to highlight a string
36 type Highlighter struct {
41 // NewHighlighter returns a new highlighter from the given syntax definition
42 func NewHighlighter(def *Def) *Highlighter {
48 // LineMatch represents the syntax highlighting matches for one line. Each index where the coloring is changed is marked with that
49 // color's group (represented as one byte)
50 type LineMatch map[int]uint8
52 func findIndex(regex *regexp2.Regexp, str []byte, canMatchStart, canMatchEnd bool) []int {
53 regexStr := regex.String()
54 if strings.Contains(regexStr, "^") {
59 if strings.Contains(regexStr, "$") {
64 match, _ := regex.FindStringMatch(string(str))
68 return []int{match.Index, match.Index + match.Length}
71 func findAllIndex(regex *regexp.Regexp, str []byte, canMatchStart, canMatchEnd bool) [][]int {
72 regexStr := regex.String()
73 if strings.Contains(regexStr, "^") {
78 if strings.Contains(regexStr, "$") {
83 return regex.FindAllIndex(str, -1)
86 func (h *Highlighter) highlightRegion(highlights LineMatch, start int, canMatchEnd bool, lineNum int, line []byte, region *Region, statesOnly bool) LineMatch {
87 // highlights := make(LineMatch)
91 highlights[0] = region.group
95 loc := findIndex(region.end, line, start == 0, canMatchEnd)
98 highlights[start+loc[1]-1] = region.group
100 if region.parent == nil {
102 highlights[start+loc[1]] = 0
104 h.highlightRegion(highlights, start, false, lineNum, line[:loc[0]], region, statesOnly)
105 h.highlightEmptyRegion(highlights, start+loc[1], canMatchEnd, lineNum, line[loc[1]:], statesOnly)
109 highlights[start+loc[1]] = region.parent.group
111 h.highlightRegion(highlights, start, false, lineNum, line[:loc[0]], region, statesOnly)
112 h.highlightRegion(highlights, start+loc[1], canMatchEnd, lineNum, line[loc[1]:], region.parent, statesOnly)
116 if len(line) == 0 || statesOnly {
118 h.lastRegion = region
124 firstLoc := []int{len(line), 0}
125 var firstRegion *Region
126 for _, r := range region.rules.regions {
127 loc := findIndex(r.start, line, start == 0, canMatchEnd)
129 if loc[0] < firstLoc[0] {
135 if firstLoc[0] != len(line) {
136 highlights[start+firstLoc[0]] = firstRegion.group
137 h.highlightRegion(highlights, start, false, lineNum, line[:firstLoc[0]], region, statesOnly)
138 h.highlightRegion(highlights, start+firstLoc[1], canMatchEnd, lineNum, line[firstLoc[1]:], firstRegion, statesOnly)
142 fullHighlights := make([]uint8, len([]rune(string(line))))
143 for i := 0; i < len(fullHighlights); i++ {
144 fullHighlights[i] = region.group
147 for _, p := range region.rules.patterns {
148 matches := findAllIndex(p.regex, line, start == 0, canMatchEnd)
149 for _, m := range matches {
150 for i := m[0]; i < m[1]; i++ {
151 fullHighlights[i] = p.group
155 for i, h := range fullHighlights {
156 if i == 0 || h != fullHighlights[i-1] {
157 if _, ok := highlights[start+i]; !ok {
158 highlights[start+i] = h
164 h.lastRegion = region
170 func (h *Highlighter) highlightEmptyRegion(highlights LineMatch, start int, canMatchEnd bool, lineNum int, line []byte, statesOnly bool) LineMatch {
178 firstLoc := []int{len(line), 0}
179 var firstRegion *Region
180 for _, r := range h.def.rules.regions {
181 loc := findIndex(r.start, line, start == 0, canMatchEnd)
183 if loc[0] < firstLoc[0] {
189 if firstLoc[0] != len(line) {
191 highlights[start+firstLoc[0]] = firstRegion.group
193 h.highlightEmptyRegion(highlights, start, false, lineNum, line[:firstLoc[0]], statesOnly)
194 h.highlightRegion(highlights, start+firstLoc[1], canMatchEnd, lineNum, line[firstLoc[1]:], firstRegion, statesOnly)
206 fullHighlights := make([]uint8, len(line))
207 for _, p := range h.def.rules.patterns {
208 matches := findAllIndex(p.regex, line, start == 0, canMatchEnd)
209 for _, m := range matches {
210 for i := m[0]; i < m[1]; i++ {
211 fullHighlights[i] = p.group
215 for i, h := range fullHighlights {
216 if i == 0 || h != fullHighlights[i-1] {
217 if _, ok := highlights[start+i]; !ok {
218 highlights[start+i] = h
230 // HighlightString syntax highlights a string
231 // Use this function for simple syntax highlighting and use the other functions for
232 // more advanced syntax highlighting. They are optimized for quick rehighlighting of the same
233 // text with minor changes made
234 func (h *Highlighter) HighlightString(input string) []LineMatch {
235 lines := strings.Split(input, "\n")
236 var lineMatches []LineMatch
238 for i := 0; i < len(lines); i++ {
239 line := []byte(lines[i])
240 highlights := make(LineMatch)
242 if i == 0 || h.lastRegion == nil {
243 lineMatches = append(lineMatches, h.highlightEmptyRegion(highlights, 0, true, i, line, false))
245 lineMatches = append(lineMatches, h.highlightRegion(highlights, 0, true, i, line, h.lastRegion, false))
252 // HighlightStates correctly sets all states for the buffer
253 func (h *Highlighter) HighlightStates(input LineStates) {
254 for i := 0; i < input.LinesNum(); i++ {
255 line := []byte(input.Line(i))
256 // highlights := make(LineMatch)
258 if i == 0 || h.lastRegion == nil {
259 h.highlightEmptyRegion(nil, 0, true, i, line, true)
261 h.highlightRegion(nil, 0, true, i, line, h.lastRegion, true)
264 curState := h.lastRegion
266 input.SetState(i, curState)
270 // HighlightMatches sets the matches for each line in between startline and endline
271 // It sets all other matches in the buffer to nil to conserve memory
272 // This assumes that all the states are set correctly
273 func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int) {
274 for i := startline; i < endline; i++ {
275 if i >= input.LinesNum() {
279 line := []byte(input.Line(i))
280 highlights := make(LineMatch)
283 if i == 0 || input.State(i-1) == nil {
284 match = h.highlightEmptyRegion(highlights, 0, true, i, line, false)
286 match = h.highlightRegion(highlights, 0, true, i, line, input.State(i-1), false)
289 input.SetMatch(i, match)
293 // ReHighlightStates will scan down from `startline` and set the appropriate end of line state
294 // for each line until it comes across the same state in two consecutive lines
295 func (h *Highlighter) ReHighlightStates(input LineStates, startline int) {
296 // lines := input.LineData()
300 h.lastRegion = input.State(startline - 1)
302 for i := startline; i < input.LinesNum(); i++ {
303 line := []byte(input.Line(i))
304 // highlights := make(LineMatch)
306 // var match LineMatch
307 if i == 0 || h.lastRegion == nil {
308 h.highlightEmptyRegion(nil, 0, true, i, line, true)
310 h.highlightRegion(nil, 0, true, i, line, h.lastRegion, true)
312 curState := h.lastRegion
313 lastState := input.State(i)
315 input.SetState(i, curState)
317 if curState == lastState {
323 // ReHighlightLine will rehighlight the state and match for a single line
324 func (h *Highlighter) ReHighlightLine(input LineStates, lineN int) {
325 line := []byte(input.Line(lineN))
326 highlights := make(LineMatch)
330 h.lastRegion = input.State(lineN - 1)
334 if lineN == 0 || h.lastRegion == nil {
335 match = h.highlightEmptyRegion(highlights, 0, true, lineN, line, false)
337 match = h.highlightRegion(highlights, 0, true, lineN, line, h.lastRegion, false)
339 curState := h.lastRegion
341 input.SetMatch(lineN, match)
342 input.SetState(lineN, curState)