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