]> git.lizzy.rs Git - micro.git/blob - cmd/micro/highlighter.go
change pluginmanager json to json5
[micro.git] / cmd / micro / highlighter.go
1 package main
2
3 import (
4         "regexp"
5         "strings"
6
7         "github.com/zyedidia/tcell"
8 )
9
10 // FileTypeRules represents a complete set of syntax rules for a filetype
11 type FileTypeRules struct {
12         filetype string
13         filename string
14         text     string
15 }
16
17 // SyntaxRule represents a regex to highlight in a certain style
18 type SyntaxRule struct {
19         // What to highlight
20         regex *regexp.Regexp
21         // Any flags
22         flags string
23         // Whether this regex is a start=... end=... regex
24         startend bool
25         // How to highlight it
26         style tcell.Style
27 }
28
29 var syntaxFiles map[[2]*regexp.Regexp]FileTypeRules
30
31 // LoadSyntaxFiles loads the syntax files from the default directory (configDir)
32 func LoadSyntaxFiles() {
33         InitColorscheme()
34         syntaxFiles = make(map[[2]*regexp.Regexp]FileTypeRules)
35         for _, f := range ListRuntimeFiles(RTSyntax) {
36                 data, err := f.Data()
37                 if err != nil {
38                         TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
39                 } else {
40                         LoadSyntaxFile(string(data), f.Name())
41                 }
42         }
43 }
44
45 // JoinRule takes a syntax rule (which can be multiple regular expressions)
46 // and joins it into one regular expression by ORing everything together
47 func JoinRule(rule string) string {
48         split := strings.Split(rule, `" "`)
49         joined := strings.Join(split, ")|(")
50         joined = "(" + joined + ")"
51         return joined
52 }
53
54 // LoadSyntaxFile simply gets the filetype of a the syntax file and the source for the
55 // file and creates FileTypeRules out of it. If this filetype is the one opened by the user
56 // the rules will be loaded and compiled later
57 // In this function we are only concerned with loading the syntax and header regexes
58 func LoadSyntaxFile(text, filename string) {
59         var err error
60         lines := strings.Split(string(text), "\n")
61
62         // Regex for parsing syntax statements
63         syntaxParser := regexp.MustCompile(`syntax "(.*?)"\s+"(.*)"+`)
64         // Regex for parsing header statements
65         headerParser := regexp.MustCompile(`header "(.*)"`)
66
67         // Is there a syntax definition in this file?
68         hasSyntax := syntaxParser.MatchString(text)
69         // Is there a header definition in this file?
70         hasHeader := headerParser.MatchString(text)
71
72         var syntaxRegex *regexp.Regexp
73         var headerRegex *regexp.Regexp
74         var filetype string
75         for lineNum, line := range lines {
76                 if (hasSyntax == (syntaxRegex != nil)) && (hasHeader == (headerRegex != nil)) {
77                         // We found what we we're supposed to find
78                         break
79                 }
80
81                 if strings.TrimSpace(line) == "" ||
82                         strings.TrimSpace(line)[0] == '#' {
83                         // Ignore this line
84                         continue
85                 }
86
87                 if strings.HasPrefix(line, "syntax") {
88                         // Syntax statement
89                         syntaxMatches := syntaxParser.FindSubmatch([]byte(line))
90                         if len(syntaxMatches) == 3 {
91                                 if syntaxRegex != nil {
92                                         TermError(filename, lineNum, "Syntax statement redeclaration")
93                                 }
94
95                                 filetype = string(syntaxMatches[1])
96                                 extensions := JoinRule(string(syntaxMatches[2]))
97
98                                 syntaxRegex, err = regexp.Compile(extensions)
99                                 if err != nil {
100                                         TermError(filename, lineNum, err.Error())
101                                         continue
102                                 }
103                         } else {
104                                 TermError(filename, lineNum, "Syntax statement is not valid: "+line)
105                                 continue
106                         }
107                 } else if strings.HasPrefix(line, "header") {
108                         // Header statement
109                         headerMatches := headerParser.FindSubmatch([]byte(line))
110                         if len(headerMatches) == 2 {
111                                 header := JoinRule(string(headerMatches[1]))
112
113                                 headerRegex, err = regexp.Compile(header)
114                                 if err != nil {
115                                         TermError(filename, lineNum, "Regex error: "+err.Error())
116                                         continue
117                                 }
118                         } else {
119                                 TermError(filename, lineNum, "Header statement is not valid: "+line)
120                                 continue
121                         }
122                 }
123         }
124         if syntaxRegex != nil {
125                 // Add the current rules to the syntaxFiles variable
126                 regexes := [2]*regexp.Regexp{syntaxRegex, headerRegex}
127                 syntaxFiles[regexes] = FileTypeRules{filetype, filename, text}
128         }
129 }
130
131 // LoadRulesFromFile loads just the syntax rules from a given file
132 // Only the necessary rules are loaded when the buffer is opened.
133 // If we load all the rules for every filetype when micro starts, there's a bit of lag
134 // A rule just explains how to color certain regular expressions
135 // Example: color comment "//.*"
136 // This would color all strings that match the regex "//.*" in the comment color defined
137 // by the colorscheme
138 func LoadRulesFromFile(text, filename string) []SyntaxRule {
139         lines := strings.Split(string(text), "\n")
140
141         // Regex for parsing standard syntax rules
142         ruleParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?"(.*)"`)
143         // Regex for parsing syntax rules with start="..." end="..."
144         ruleStartEndParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?start="(.*)"\s+end="(.*)"`)
145
146         var rules []SyntaxRule
147         for lineNum, line := range lines {
148                 if strings.TrimSpace(line) == "" ||
149                         strings.TrimSpace(line)[0] == '#' ||
150                         strings.HasPrefix(line, "syntax") ||
151                         strings.HasPrefix(line, "header") {
152                         // Ignore this line
153                         continue
154                 }
155
156                 // Syntax rule, but it could be standard or start-end
157                 if ruleParser.MatchString(line) {
158                         // Standard syntax rule
159                         // Parse the line
160                         submatch := ruleParser.FindSubmatch([]byte(line))
161                         var color string
162                         var regexStr string
163                         var flags string
164                         if len(submatch) == 4 {
165                                 // If len is 4 then the user specified some additional flags to use
166                                 color = string(submatch[1])
167                                 flags = string(submatch[2])
168                                 regexStr = "(?" + flags + ")" + JoinRule(string(submatch[3]))
169                         } else if len(submatch) == 3 {
170                                 // If len is 3, no additional flags were given
171                                 color = string(submatch[1])
172                                 regexStr = JoinRule(string(submatch[2]))
173                         } else {
174                                 // If len is not 3 or 4 there is a problem
175                                 TermError(filename, lineNum, "Invalid statement: "+line)
176                                 continue
177                         }
178                         // Compile the regex
179                         regex, err := regexp.Compile(regexStr)
180                         if err != nil {
181                                 TermError(filename, lineNum, err.Error())
182                                 continue
183                         }
184
185                         // Get the style
186                         // The user could give us a "color" that is really a part of the colorscheme
187                         // in which case we should look that up in the colorscheme
188                         // They can also just give us a straight up color
189                         st := defStyle
190                         groups := strings.Split(color, ".")
191                         if len(groups) > 1 {
192                                 curGroup := ""
193                                 for i, g := range groups {
194                                         if i != 0 {
195                                                 curGroup += "."
196                                         }
197                                         curGroup += g
198                                         if style, ok := colorscheme[curGroup]; ok {
199                                                 st = style
200                                         }
201                                 }
202                         } else if style, ok := colorscheme[color]; ok {
203                                 st = style
204                         } else {
205                                 st = StringToStyle(color)
206                         }
207                         // Add the regex, flags, and style
208                         // False because this is not start-end
209                         rules = append(rules, SyntaxRule{regex, flags, false, st})
210                 } else if ruleStartEndParser.MatchString(line) {
211                         // Start-end syntax rule
212                         submatch := ruleStartEndParser.FindSubmatch([]byte(line))
213                         var color string
214                         var start string
215                         var end string
216                         // Use m and s flags by default
217                         flags := "ms"
218                         if len(submatch) == 5 {
219                                 // If len is 5 the user provided some additional flags
220                                 color = string(submatch[1])
221                                 flags += string(submatch[2])
222                                 start = string(submatch[3])
223                                 end = string(submatch[4])
224                         } else if len(submatch) == 4 {
225                                 // If len is 4 the user did not provide additional flags
226                                 color = string(submatch[1])
227                                 start = string(submatch[2])
228                                 end = string(submatch[3])
229                         } else {
230                                 // If len is not 4 or 5 there is a problem
231                                 TermError(filename, lineNum, "Invalid statement: "+line)
232                                 continue
233                         }
234
235                         // Compile the regex
236                         regex, err := regexp.Compile("(?" + flags + ")" + "(" + start + ").*?(" + end + ")")
237                         if err != nil {
238                                 TermError(filename, lineNum, err.Error())
239                                 continue
240                         }
241
242                         // Get the style
243                         // The user could give us a "color" that is really a part of the colorscheme
244                         // in which case we should look that up in the colorscheme
245                         // They can also just give us a straight up color
246                         st := defStyle
247                         if _, ok := colorscheme[color]; ok {
248                                 st = colorscheme[color]
249                         } else {
250                                 st = StringToStyle(color)
251                         }
252                         // Add the regex, flags, and style
253                         // True because this is start-end
254                         rules = append(rules, SyntaxRule{regex, flags, true, st})
255                 }
256         }
257         return rules
258 }
259
260 // FindFileType finds the filetype for the given buffer
261 func FindFileType(buf *Buffer) string {
262         for r := range syntaxFiles {
263                 if r[0] != nil && r[0].MatchString(buf.Path) {
264                         // The syntax statement matches the extension
265                         return syntaxFiles[r].filetype
266                 } else if r[1] != nil && r[1].MatchString(buf.Line(0)) {
267                         // The header statement matches the first line
268                         return syntaxFiles[r].filetype
269                 }
270         }
271         return "Unknown"
272 }
273
274 // GetRules finds the syntax rules that should be used for the buffer
275 // and returns them. It also returns the filetype of the file
276 func GetRules(buf *Buffer) []SyntaxRule {
277         for r := range syntaxFiles {
278                 if syntaxFiles[r].filetype == buf.FileType() {
279                         return LoadRulesFromFile(syntaxFiles[r].text, syntaxFiles[r].filename)
280                 }
281         }
282         return nil
283 }
284
285 // SyntaxMatches is an alias to a map from character numbers to styles,
286 // so map[3] represents the style of the third character
287 type SyntaxMatches [][]tcell.Style
288
289 // Match takes a buffer and returns the syntax matches: a 2d array specifying how it should be syntax highlighted
290 // We match the rules from up `synLinesUp` lines and down `synLinesDown` lines
291 func Match(v *View) SyntaxMatches {
292         buf := v.Buf
293         rules := v.Buf.rules
294
295         viewStart := v.Topline
296         viewEnd := v.Topline + v.height
297         if viewEnd > buf.NumLines {
298                 viewEnd = buf.NumLines
299         }
300
301         lines := buf.Lines(viewStart, viewEnd)
302         matches := make(SyntaxMatches, len(lines))
303
304         for i, line := range lines {
305                 matches[i] = make([]tcell.Style, len(line)+1)
306                 for j := range matches[i] {
307                         matches[i][j] = defStyle
308                 }
309         }
310
311         // We don't actually check the entire buffer, just from synLinesUp to synLinesDown
312         totalStart := v.Topline - synLinesUp
313         totalEnd := v.Topline + v.height + synLinesDown
314         if totalStart < 0 {
315                 totalStart = 0
316         }
317         if totalEnd > buf.NumLines {
318                 totalEnd = buf.NumLines
319         }
320
321         str := strings.Join(buf.Lines(totalStart, totalEnd), "\n")
322         startNum := ToCharPos(Loc{0, totalStart}, v.Buf)
323
324         for _, rule := range rules {
325                 if rule.startend {
326                         if indicies := rule.regex.FindAllStringIndex(str, -1); indicies != nil {
327                                 for _, value := range indicies {
328                                         value[0] = runePos(value[0], str) + startNum
329                                         value[1] = runePos(value[1], str) + startNum
330                                         startLoc := FromCharPos(value[0], buf)
331                                         endLoc := FromCharPos(value[1], buf)
332                                         for curLoc := startLoc; curLoc.LessThan(endLoc); curLoc = curLoc.Move(1, buf) {
333                                                 if curLoc.Y < v.Topline {
334                                                         continue
335                                                 }
336                                                 colNum, lineNum := curLoc.X, curLoc.Y
337                                                 if lineNum == -1 || colNum == -1 {
338                                                         continue
339                                                 }
340                                                 lineNum -= viewStart
341                                                 if lineNum >= 0 && lineNum < v.height {
342                                                         matches[lineNum][colNum] = rule.style
343                                                 }
344                                         }
345                                 }
346                         }
347                 } else {
348                         for lineN, line := range lines {
349                                 if indicies := rule.regex.FindAllStringIndex(line, -1); indicies != nil {
350                                         for _, value := range indicies {
351                                                 start := runePos(value[0], line)
352                                                 end := runePos(value[1], line)
353                                                 for i := start; i < end; i++ {
354                                                         matches[lineN][i] = rule.style
355                                                 }
356                                         }
357                                 }
358                         }
359                 }
360         }
361
362         return matches
363 }