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 {
16 return utf8.RuneCountInString(str)
18 return utf8.RuneCountInString(str[:p])
21 func combineLineMatch(src, dst LineMatch) LineMatch {
22 for k, v := range src {
23 if g, ok := dst[k]; ok {
34 // A State represents the region at the end of a line
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 {
41 State(lineN int) State
42 SetState(lineN int, s State)
43 SetMatch(lineN int, m LineMatch)
46 // A Highlighter contains the information needed to highlight a string
47 type Highlighter struct {
52 // NewHighlighter returns a new highlighter from the given syntax definition
53 func NewHighlighter(def *Def) *Highlighter {
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
63 func findIndex(regex *regexp.Regexp, skip []*regexp.Regexp, str []rune, canMatchStart, canMatchEnd bool) []int {
64 regexStr := regex.String()
65 if strings.Contains(regexStr, "^") {
70 if strings.Contains(regexStr, "$") {
76 strbytes := []byte(string(str))
77 if skip != nil && len(skip) > 0 {
78 for _, r := range skip {
80 strbytes = r.ReplaceAllFunc(strbytes, func(match []byte) []byte {
81 res := make([]byte, utf8.RuneCount(match))
88 match := regex.FindIndex(strbytes)
92 // return []int{match.Index, match.Index + match.Length}
93 return []int{runePos(match[0], string(str)), runePos(match[1], string(str))}
96 func findAllIndex(regex *regexp.Regexp, str []rune, canMatchStart, canMatchEnd bool) [][]int {
97 regexStr := regex.String()
98 if strings.Contains(regexStr, "^") {
103 if strings.Contains(regexStr, "$") {
108 matches := regex.FindAllIndex([]byte(string(str)), -1)
109 for i, m := range matches {
110 matches[i][0] = runePos(m[0], string(str))
111 matches[i][1] = runePos(m[1], string(str))
116 func (h *Highlighter) highlightRegion(highlights LineMatch, start int, canMatchEnd bool, lineNum int, line []rune, curRegion *region, statesOnly bool) LineMatch {
117 // highlights := make(LineMatch)
121 highlights[0] = curRegion.group
125 skips := make([]*regexp.Regexp, len(curRegion.rules.patterns)+1)
126 for i := range skips {
127 if i != len(skips)-1 {
128 skips[i] = curRegion.rules.patterns[i].regex
130 skips[i] = curRegion.skip
133 loc := findIndex(curRegion.end, skips, line, start == 0, canMatchEnd)
136 highlights[start+loc[1]-1] = curRegion.group
138 if curRegion.parent == nil {
140 highlights[start+loc[1]] = 0
141 h.highlightRegion(highlights, start, false, lineNum, line[:loc[0]], curRegion, statesOnly)
143 h.highlightEmptyRegion(highlights, start+loc[1], canMatchEnd, lineNum, line[loc[1]:], statesOnly)
147 highlights[start+loc[1]] = curRegion.parent.group
148 h.highlightRegion(highlights, start, false, lineNum, line[:loc[0]], curRegion, statesOnly)
150 h.highlightRegion(highlights, start+loc[1], canMatchEnd, lineNum, line[loc[1]:], curRegion.parent, statesOnly)
154 if len(line) == 0 || statesOnly {
156 h.lastRegion = curRegion
162 firstLoc := []int{len(line), 0}
164 var firstRegion *region
165 for _, r := range curRegion.rules.regions {
166 loc := findIndex(r.start, nil, line, start == 0, canMatchEnd)
168 if loc[0] < firstLoc[0] {
174 if firstLoc[0] != len(line) {
175 highlights[start+firstLoc[0]] = firstRegion.group
176 h.highlightRegion(highlights, start, false, lineNum, line[:firstLoc[0]], curRegion, statesOnly)
177 h.highlightRegion(highlights, start+firstLoc[1], canMatchEnd, lineNum, line[firstLoc[1]:], firstRegion, statesOnly)
181 fullHighlights := make([]Group, len([]rune(string(line))))
182 for i := 0; i < len(fullHighlights); i++ {
183 fullHighlights[i] = curRegion.group
186 for _, p := range curRegion.rules.patterns {
187 matches := findAllIndex(p.regex, line, start == 0, canMatchEnd)
188 for _, m := range matches {
189 for i := m[0]; i < m[1]; i++ {
190 fullHighlights[i] = p.group
194 for i, h := range fullHighlights {
195 if i == 0 || h != fullHighlights[i-1] {
196 if _, ok := highlights[start+i]; !ok {
197 highlights[start+i] = h
203 h.lastRegion = curRegion
209 func (h *Highlighter) highlightEmptyRegion(highlights LineMatch, start int, canMatchEnd bool, lineNum int, line []rune, statesOnly bool) LineMatch {
217 firstLoc := []int{len(line), 0}
218 var firstRegion *region
219 for _, r := range h.Def.rules.regions {
220 loc := findIndex(r.start, nil, line, start == 0, canMatchEnd)
222 if loc[0] < firstLoc[0] {
228 if firstLoc[0] != len(line) {
230 highlights[start+firstLoc[0]] = firstRegion.group
232 h.highlightEmptyRegion(highlights, start, false, lineNum, line[:firstLoc[0]], statesOnly)
233 h.highlightRegion(highlights, start+firstLoc[1], canMatchEnd, lineNum, line[firstLoc[1]:], firstRegion, statesOnly)
245 fullHighlights := make([]Group, len(line))
246 for _, p := range h.Def.rules.patterns {
247 matches := findAllIndex(p.regex, line, start == 0, canMatchEnd)
248 for _, m := range matches {
249 for i := m[0]; i < m[1]; i++ {
250 fullHighlights[i] = p.group
254 for i, h := range fullHighlights {
255 if i == 0 || h != fullHighlights[i-1] {
256 if _, ok := highlights[start+i]; !ok {
257 highlights[start+i] = h
269 // HighlightString syntax highlights a string
270 // Use this function for simple syntax highlighting and use the other functions for
271 // more advanced syntax highlighting. They are optimized for quick rehighlighting of the same
272 // text with minor changes made
273 func (h *Highlighter) HighlightString(input string) []LineMatch {
274 lines := strings.Split(input, "\n")
275 var lineMatches []LineMatch
277 for i := 0; i < len(lines); i++ {
278 line := []rune(lines[i])
279 highlights := make(LineMatch)
281 if i == 0 || h.lastRegion == nil {
282 lineMatches = append(lineMatches, h.highlightEmptyRegion(highlights, 0, true, i, line, false))
284 lineMatches = append(lineMatches, h.highlightRegion(highlights, 0, true, i, line, h.lastRegion, false))
291 // HighlightStates correctly sets all states for the buffer
292 func (h *Highlighter) HighlightStates(input LineStates) {
293 for i := 0; i < input.LinesNum(); i++ {
294 line := []rune(input.Line(i))
295 // highlights := make(LineMatch)
297 if i == 0 || h.lastRegion == nil {
298 h.highlightEmptyRegion(nil, 0, true, i, line, true)
300 h.highlightRegion(nil, 0, true, i, line, h.lastRegion, true)
303 curState := h.lastRegion
305 input.SetState(i, curState)
309 // HighlightMatches sets the matches for each line in between startline and endline
310 // It sets all other matches in the buffer to nil to conserve memory
311 // This assumes that all the states are set correctly
312 func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int) {
313 for i := startline; i < endline; i++ {
314 if i >= input.LinesNum() {
318 line := []rune(input.Line(i))
319 highlights := make(LineMatch)
322 if i == 0 || input.State(i-1) == nil {
323 match = h.highlightEmptyRegion(highlights, 0, true, i, line, false)
325 match = h.highlightRegion(highlights, 0, true, i, line, input.State(i-1), false)
328 input.SetMatch(i, match)
332 // ReHighlightStates will scan down from `startline` and set the appropriate end of line state
333 // for each line until it comes across the same state in two consecutive lines
334 func (h *Highlighter) ReHighlightStates(input LineStates, startline int) {
335 // lines := input.LineData()
339 h.lastRegion = input.State(startline - 1)
341 for i := startline; i < input.LinesNum(); i++ {
342 line := []rune(input.Line(i))
343 // highlights := make(LineMatch)
345 // var match LineMatch
346 if i == 0 || h.lastRegion == nil {
347 h.highlightEmptyRegion(nil, 0, true, i, line, true)
349 h.highlightRegion(nil, 0, true, i, line, h.lastRegion, true)
351 curState := h.lastRegion
352 lastState := input.State(i)
354 input.SetState(i, curState)
356 if curState == lastState {
362 // ReHighlightLine will rehighlight the state and match for a single line
363 func (h *Highlighter) ReHighlightLine(input LineStates, lineN int) {
364 line := []rune(input.Line(lineN))
365 highlights := make(LineMatch)
369 h.lastRegion = input.State(lineN - 1)
373 if lineN == 0 || h.lastRegion == nil {
374 match = h.highlightEmptyRegion(highlights, 0, true, lineN, line, false)
376 match = h.highlightRegion(highlights, 0, true, lineN, line, h.lastRegion, false)
378 curState := h.lastRegion
380 input.SetMatch(lineN, match)
381 input.SetState(lineN, curState)