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