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