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