]> git.lizzy.rs Git - micro.git/blob - cmd/micro/highlighter.go
Rewrite gofmt and goimports as plugins
[micro.git] / cmd / micro / highlighter.go
1 package main
2
3 import (
4         "github.com/zyedidia/tcell"
5         "io/ioutil"
6         "path/filepath"
7         "regexp"
8         "strings"
9 )
10
11 // FileTypeRules represents a complete set of syntax rules for a filetype
12 type FileTypeRules struct {
13         filetype string
14         filename string
15         text     string
16 }
17
18 // SyntaxRule represents a regex to highlight in a certain style
19 type SyntaxRule struct {
20         // What to highlight
21         regex *regexp.Regexp
22         // Any flags
23         flags string
24         // Whether this regex is a start=... end=... regex
25         startend bool
26         // How to highlight it
27         style tcell.Style
28 }
29
30 var syntaxFiles map[[2]*regexp.Regexp]FileTypeRules
31
32 // These syntax files are pre installed and embedded in the resulting binary by go-bindata
33 var preInstalledSynFiles = []string{
34         "Dockerfile",
35         "apacheconf",
36         "arduino",
37         "asciidoc",
38         "asm",
39         "awk",
40         "c",
41         "cmake",
42         "coffeescript",
43         "colortest",
44         "conf",
45         "conky",
46         "csharp",
47         "css",
48         "cython",
49         "d",
50         "dot",
51         "erb",
52         "fish",
53         "fortran",
54         "gentoo-ebuild",
55         "gentoo-etc-portage",
56         "git-commit",
57         "git-config",
58         "git-rebase-todo",
59         "glsl",
60         "go",
61         "groff",
62         "haml",
63         "haskell",
64         "html",
65         "ini",
66         "inputrc",
67         "java",
68         "javascript",
69         "json",
70         "keymap",
71         "kickstart",
72         "ledger",
73         "lisp",
74         "lua",
75         "makefile",
76         "man",
77         "markdown",
78         "mpdconf",
79         "nanorc",
80         "nginx",
81         "ocaml",
82         "patch",
83         "peg",
84         "perl",
85         "perl6",
86         "php",
87         "pkg-config",
88         "pkgbuild",
89         "po",
90         "pov",
91         "privoxy-action",
92         "privoxy-config",
93         "privoxy-filter",
94         "puppet",
95         "python",
96         "r",
97         "reST",
98         "rpmspec",
99         "ruby",
100         "rust",
101         "scala",
102         "sed",
103         "sh",
104         "sls",
105         "sql",
106         "swift",
107         "systemd",
108         "tcl",
109         "tex",
110         "vala",
111         "vi",
112         "xml",
113         "xresources",
114         "yaml",
115         "yum",
116         "zsh",
117 }
118
119 // LoadSyntaxFiles loads the syntax files from the default directory (configDir)
120 func LoadSyntaxFiles() {
121         // Load the user's custom syntax files, if there are any
122         LoadSyntaxFilesFromDir(configDir + "/syntax")
123
124         // Load the pre-installed syntax files from inside the binary
125         for _, filetype := range preInstalledSynFiles {
126                 data, err := Asset("runtime/syntax/" + filetype + ".micro")
127                 if err != nil {
128                         TermMessage("Unable to load pre-installed syntax file " + filetype)
129                         continue
130                 }
131
132                 LoadSyntaxFile(string(data), filetype+".micro")
133         }
134 }
135
136 // LoadSyntaxFilesFromDir loads the syntax files from a specified directory
137 // To load the syntax files, we must fill the `syntaxFiles` map
138 // This involves finding the regex for syntax and if it exists, the regex
139 // for the header. Then we must get the text for the file and the filetype.
140 func LoadSyntaxFilesFromDir(dir string) {
141         InitColorscheme()
142
143         syntaxFiles = make(map[[2]*regexp.Regexp]FileTypeRules)
144         files, _ := ioutil.ReadDir(dir)
145         for _, f := range files {
146                 if filepath.Ext(f.Name()) == ".micro" {
147                         filename := dir + "/" + f.Name()
148                         text, err := ioutil.ReadFile(filename)
149
150                         if err != nil {
151                                 TermMessage("Error loading syntax file " + filename + ": " + err.Error())
152                                 return
153                         }
154                         LoadSyntaxFile(string(text), filename)
155                 }
156         }
157 }
158
159 // JoinRule takes a syntax rule (which can be multiple regular expressions)
160 // and joins it into one regular expression by ORing everything together
161 func JoinRule(rule string) string {
162         split := strings.Split(rule, `" "`)
163         joined := strings.Join(split, ")|(")
164         joined = "(" + joined + ")"
165         return joined
166 }
167
168 // LoadSyntaxFile simply gets the filetype of a the syntax file and the source for the
169 // file and creates FileTypeRules out of it. If this filetype is the one opened by the user
170 // the rules will be loaded and compiled later
171 // In this function we are only concerned with loading the syntax and header regexes
172 func LoadSyntaxFile(text, filename string) {
173         var err error
174         lines := strings.Split(string(text), "\n")
175
176         // Regex for parsing syntax statements
177         syntaxParser := regexp.MustCompile(`syntax "(.*?)"\s+"(.*)"+`)
178         // Regex for parsing header statements
179         headerParser := regexp.MustCompile(`header "(.*)"`)
180
181         // Is there a syntax definition in this file?
182         hasSyntax := syntaxParser.MatchString(text)
183         // Is there a header definition in this file?
184         hasHeader := headerParser.MatchString(text)
185
186         var syntaxRegex *regexp.Regexp
187         var headerRegex *regexp.Regexp
188         var filetype string
189         for lineNum, line := range lines {
190                 if (hasSyntax == (syntaxRegex != nil)) && (hasHeader == (headerRegex != nil)) {
191                         // We found what we we're supposed to find
192                         break
193                 }
194
195                 if strings.TrimSpace(line) == "" ||
196                         strings.TrimSpace(line)[0] == '#' {
197                         // Ignore this line
198                         continue
199                 }
200
201                 if strings.HasPrefix(line, "syntax") {
202                         // Syntax statement
203                         syntaxMatches := syntaxParser.FindSubmatch([]byte(line))
204                         if len(syntaxMatches) == 3 {
205                                 if syntaxRegex != nil {
206                                         TermError(filename, lineNum, "Syntax statement redeclaration")
207                                 }
208
209                                 filetype = string(syntaxMatches[1])
210                                 extensions := JoinRule(string(syntaxMatches[2]))
211
212                                 syntaxRegex, err = regexp.Compile(extensions)
213                                 if err != nil {
214                                         TermError(filename, lineNum, err.Error())
215                                         continue
216                                 }
217                         } else {
218                                 TermError(filename, lineNum, "Syntax statement is not valid: "+line)
219                                 continue
220                         }
221                 } else if strings.HasPrefix(line, "header") {
222                         // Header statement
223                         headerMatches := headerParser.FindSubmatch([]byte(line))
224                         if len(headerMatches) == 2 {
225                                 header := JoinRule(string(headerMatches[1]))
226
227                                 headerRegex, err = regexp.Compile(header)
228                                 if err != nil {
229                                         TermError(filename, lineNum, "Regex error: "+err.Error())
230                                         continue
231                                 }
232                         } else {
233                                 TermError(filename, lineNum, "Header statement is not valid: "+line)
234                                 continue
235                         }
236                 }
237         }
238         if syntaxRegex != nil {
239                 // Add the current rules to the syntaxFiles variable
240                 regexes := [2]*regexp.Regexp{syntaxRegex, headerRegex}
241                 syntaxFiles[regexes] = FileTypeRules{filetype, filename, text}
242         }
243 }
244
245 // LoadRulesFromFile loads just the syntax rules from a given file
246 // Only the necessary rules are loaded when the buffer is opened.
247 // If we load all the rules for every filetype when micro starts, there's a bit of lag
248 // A rule just explains how to color certain regular expressions
249 // Example: color comment "//.*"
250 // This would color all strings that match the regex "//.*" in the comment color defined
251 // by the colorscheme
252 func LoadRulesFromFile(text, filename string) []SyntaxRule {
253         lines := strings.Split(string(text), "\n")
254
255         // Regex for parsing standard syntax rules
256         ruleParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?"(.*)"`)
257         // Regex for parsing syntax rules with start="..." end="..."
258         ruleStartEndParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?start="(.*)"\s+end="(.*)"`)
259
260         var rules []SyntaxRule
261         for lineNum, line := range lines {
262                 if strings.TrimSpace(line) == "" ||
263                         strings.TrimSpace(line)[0] == '#' ||
264                         strings.HasPrefix(line, "syntax") ||
265                         strings.HasPrefix(line, "header") {
266                         // Ignore this line
267                         continue
268                 }
269
270                 // Syntax rule, but it could be standard or start-end
271                 if ruleParser.MatchString(line) {
272                         // Standard syntax rule
273                         // Parse the line
274                         submatch := ruleParser.FindSubmatch([]byte(line))
275                         var color string
276                         var regexStr string
277                         var flags string
278                         if len(submatch) == 4 {
279                                 // If len is 4 then the user specified some additional flags to use
280                                 color = string(submatch[1])
281                                 flags = string(submatch[2])
282                                 regexStr = "(?" + flags + ")" + JoinRule(string(submatch[3]))
283                         } else if len(submatch) == 3 {
284                                 // If len is 3, no additional flags were given
285                                 color = string(submatch[1])
286                                 regexStr = JoinRule(string(submatch[2]))
287                         } else {
288                                 // If len is not 3 or 4 there is a problem
289                                 TermError(filename, lineNum, "Invalid statement: "+line)
290                                 continue
291                         }
292                         // Compile the regex
293                         regex, err := regexp.Compile(regexStr)
294                         if err != nil {
295                                 TermError(filename, lineNum, err.Error())
296                                 continue
297                         }
298
299                         // Get the style
300                         // The user could give us a "color" that is really a part of the colorscheme
301                         // in which case we should look that up in the colorscheme
302                         // They can also just give us a straight up color
303                         st := defStyle
304                         if _, ok := colorscheme[color]; ok {
305                                 st = colorscheme[color]
306                         } else {
307                                 st = StringToStyle(color)
308                         }
309                         // Add the regex, flags, and style
310                         // False because this is not start-end
311                         rules = append(rules, SyntaxRule{regex, flags, false, st})
312                 } else if ruleStartEndParser.MatchString(line) {
313                         // Start-end syntax rule
314                         submatch := ruleStartEndParser.FindSubmatch([]byte(line))
315                         var color string
316                         var start string
317                         var end string
318                         // Use m and s flags by default
319                         flags := "ms"
320                         if len(submatch) == 5 {
321                                 // If len is 5 the user provided some additional flags
322                                 color = string(submatch[1])
323                                 flags += string(submatch[2])
324                                 start = string(submatch[3])
325                                 end = string(submatch[4])
326                         } else if len(submatch) == 4 {
327                                 // If len is 4 the user did not provide additional flags
328                                 color = string(submatch[1])
329                                 start = string(submatch[2])
330                                 end = string(submatch[3])
331                         } else {
332                                 // If len is not 4 or 5 there is a problem
333                                 TermError(filename, lineNum, "Invalid statement: "+line)
334                                 continue
335                         }
336
337                         // Compile the regex
338                         regex, err := regexp.Compile("(?" + flags + ")" + "(" + start + ").*?(" + end + ")")
339                         if err != nil {
340                                 TermError(filename, lineNum, err.Error())
341                                 continue
342                         }
343
344                         // Get the style
345                         // The user could give us a "color" that is really a part of the colorscheme
346                         // in which case we should look that up in the colorscheme
347                         // They can also just give us a straight up color
348                         st := defStyle
349                         if _, ok := colorscheme[color]; ok {
350                                 st = colorscheme[color]
351                         } else {
352                                 st = StringToStyle(color)
353                         }
354                         // Add the regex, flags, and style
355                         // True because this is start-end
356                         rules = append(rules, SyntaxRule{regex, flags, true, st})
357                 }
358         }
359         return rules
360 }
361
362 // GetRules finds the syntax rules that should be used for the buffer
363 // and returns them. It also returns the filetype of the file
364 func GetRules(buf *Buffer) ([]SyntaxRule, string) {
365         for r := range syntaxFiles {
366                 if r[0] != nil && r[0].MatchString(buf.Path) {
367                         // Check if the syntax statement matches the extension
368                         return LoadRulesFromFile(syntaxFiles[r].text, syntaxFiles[r].filename), syntaxFiles[r].filetype
369                 } else if r[1] != nil && r[1].MatchString(buf.Lines[0]) {
370                         // Check if the header statement matches the first line
371                         return LoadRulesFromFile(syntaxFiles[r].text, syntaxFiles[r].filename), syntaxFiles[r].filetype
372                 }
373         }
374         return nil, "Unknown"
375 }
376
377 // SyntaxMatches is an alias to a map from character numbers to styles,
378 // so map[3] represents the style of the third character
379 type SyntaxMatches [][]tcell.Style
380
381 // Match takes a buffer and returns the syntax matches: a 2d array specifying how it should be syntax highlighted
382 // We match the rules from up `synLinesUp` lines and down `synLinesDown` lines
383 func Match(v *View) SyntaxMatches {
384         buf := v.Buf
385         rules := v.Buf.rules
386
387         viewStart := v.Topline
388         viewEnd := v.Topline + v.height
389         if viewEnd > buf.NumLines {
390                 viewEnd = buf.NumLines
391         }
392
393         lines := buf.Lines[viewStart:viewEnd]
394         matches := make(SyntaxMatches, len(lines))
395
396         for i, line := range lines {
397                 matches[i] = make([]tcell.Style, len(line)+1)
398         }
399
400         // We don't actually check the entire buffer, just from synLinesUp to synLinesDown
401         totalStart := v.Topline - synLinesUp
402         totalEnd := v.Topline + v.height + synLinesDown
403         if totalStart < 0 {
404                 totalStart = 0
405         }
406         if totalEnd > buf.NumLines {
407                 totalEnd = buf.NumLines
408         }
409
410         str := strings.Join(buf.Lines[totalStart:totalEnd], "\n")
411         startNum := ToCharPos(0, totalStart, v.Buf)
412
413         toplineNum := ToCharPos(0, v.Topline, v.Buf)
414
415         for _, rule := range rules {
416                 if rule.startend {
417                         if indicies := rule.regex.FindAllStringIndex(str, -1); indicies != nil {
418                                 for _, value := range indicies {
419                                         value[0] += startNum
420                                         value[1] += startNum
421                                         for i := value[0]; i < value[1]; i++ {
422                                                 if i < toplineNum {
423                                                         continue
424                                                 }
425                                                 colNum, lineNum := FromCharPosStart(toplineNum, 0, v.Topline, i, buf)
426                                                 if lineNum == -1 || colNum == -1 {
427                                                         continue
428                                                 }
429                                                 lineNum -= viewStart
430                                                 if lineNum >= 0 && lineNum < v.height {
431                                                         matches[lineNum][colNum] = rule.style
432                                                 }
433                                         }
434                                 }
435                         }
436                 } else {
437                         for lineN, line := range lines {
438                                 if indicies := rule.regex.FindAllStringIndex(line, -1); indicies != nil {
439                                         for _, value := range indicies {
440                                                 for i := value[0]; i < value[1]; i++ {
441                                                         matches[lineN][i] = rule.style
442                                                 }
443                                         }
444                                 }
445                         }
446                 }
447         }
448
449         return matches
450 }