]> git.lizzy.rs Git - micro.git/blob - cmd/micro/highlighter.go
Fix draw ordering
[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         if screen != nil {
156                 screen.SetStyle(defStyle)
157         }
158
159         syntaxFiles = make(map[[2]*regexp.Regexp]FileTypeRules)
160         files, _ := ioutil.ReadDir(dir)
161         for _, f := range files {
162                 if filepath.Ext(f.Name()) == ".micro" {
163                         filename := dir + "/" + f.Name()
164                         text, err := ioutil.ReadFile(filename)
165
166                         if err != nil {
167                                 TermMessage("Error loading syntax file " + filename + ": " + err.Error())
168                                 return
169                         }
170                         LoadSyntaxFile(string(text), filename)
171                 }
172         }
173 }
174
175 // JoinRule takes a syntax rule (which can be multiple regular expressions)
176 // and joins it into one regular expression by ORing everything together
177 func JoinRule(rule string) string {
178         split := strings.Split(rule, `" "`)
179         joined := strings.Join(split, ")|(")
180         joined = "(" + joined + ")"
181         return joined
182 }
183
184 // LoadSyntaxFile simply gets the filetype of a the syntax file and the source for the
185 // file and creates FileTypeRules out of it. If this filetype is the one opened by the user
186 // the rules will be loaded and compiled later
187 // In this function we are only concerned with loading the syntax and header regexes
188 func LoadSyntaxFile(text, filename string) {
189         var err error
190         lines := strings.Split(string(text), "\n")
191
192         // Regex for parsing syntax statements
193         syntaxParser := regexp.MustCompile(`syntax "(.*?)"\s+"(.*)"+`)
194         // Regex for parsing header statements
195         headerParser := regexp.MustCompile(`header "(.*)"`)
196
197         // Is there a syntax definition in this file?
198         hasSyntax := syntaxParser.MatchString(text)
199         // Is there a header definition in this file?
200         hasHeader := headerParser.MatchString(text)
201
202         var syntaxRegex *regexp.Regexp
203         var headerRegex *regexp.Regexp
204         var filetype string
205         for lineNum, line := range lines {
206                 if (hasSyntax == (syntaxRegex != nil)) && (hasHeader == (headerRegex != nil)) {
207                         // We found what we we're supposed to find
208                         break
209                 }
210
211                 if strings.TrimSpace(line) == "" ||
212                         strings.TrimSpace(line)[0] == '#' {
213                         // Ignore this line
214                         continue
215                 }
216
217                 if strings.HasPrefix(line, "syntax") {
218                         // Syntax statement
219                         syntaxMatches := syntaxParser.FindSubmatch([]byte(line))
220                         if len(syntaxMatches) == 3 {
221                                 if syntaxRegex != nil {
222                                         TermError(filename, lineNum, "Syntax statement redeclaration")
223                                 }
224
225                                 filetype = string(syntaxMatches[1])
226                                 extensions := JoinRule(string(syntaxMatches[2]))
227
228                                 syntaxRegex, err = regexp.Compile(extensions)
229                                 if err != nil {
230                                         TermError(filename, lineNum, err.Error())
231                                         continue
232                                 }
233                         } else {
234                                 TermError(filename, lineNum, "Syntax statement is not valid: "+line)
235                                 continue
236                         }
237                 } else if strings.HasPrefix(line, "header") {
238                         // Header statement
239                         headerMatches := headerParser.FindSubmatch([]byte(line))
240                         if len(headerMatches) == 2 {
241                                 header := JoinRule(string(headerMatches[1]))
242
243                                 headerRegex, err = regexp.Compile(header)
244                                 if err != nil {
245                                         TermError(filename, lineNum, "Regex error: "+err.Error())
246                                         continue
247                                 }
248                         } else {
249                                 TermError(filename, lineNum, "Header statement is not valid: "+line)
250                                 continue
251                         }
252                 }
253         }
254         if syntaxRegex != nil {
255                 // Add the current rules to the syntaxFiles variable
256                 regexes := [2]*regexp.Regexp{syntaxRegex, headerRegex}
257                 syntaxFiles[regexes] = FileTypeRules{filetype, filename, text}
258         }
259 }
260
261 // LoadRulesFromFile loads just the syntax rules from a given file
262 // Only the necessary rules are loaded when the buffer is opened.
263 // If we load all the rules for every filetype when micro starts, there's a bit of lag
264 // A rule just explains how to color certain regular expressions
265 // Example: color comment "//.*"
266 // This would color all strings that match the regex "//.*" in the comment color defined
267 // by the colorscheme
268 func LoadRulesFromFile(text, filename string) []SyntaxRule {
269         lines := strings.Split(string(text), "\n")
270
271         // Regex for parsing standard syntax rules
272         ruleParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?"(.*)"`)
273         // Regex for parsing syntax rules with start="..." end="..."
274         ruleStartEndParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?start="(.*)"\s+end="(.*)"`)
275
276         var rules []SyntaxRule
277         for lineNum, line := range lines {
278                 if strings.TrimSpace(line) == "" ||
279                         strings.TrimSpace(line)[0] == '#' ||
280                         strings.HasPrefix(line, "syntax") ||
281                         strings.HasPrefix(line, "header") {
282                         // Ignore this line
283                         continue
284                 }
285
286                 // Syntax rule, but it could be standard or start-end
287                 if ruleParser.MatchString(line) {
288                         // Standard syntax rule
289                         // Parse the line
290                         submatch := ruleParser.FindSubmatch([]byte(line))
291                         var color string
292                         var regexStr string
293                         var flags string
294                         if len(submatch) == 4 {
295                                 // If len is 4 then the user specified some additional flags to use
296                                 color = string(submatch[1])
297                                 flags = string(submatch[2])
298                                 regexStr = "(?" + flags + ")" + JoinRule(string(submatch[3]))
299                         } else if len(submatch) == 3 {
300                                 // If len is 3, no additional flags were given
301                                 color = string(submatch[1])
302                                 regexStr = JoinRule(string(submatch[2]))
303                         } else {
304                                 // If len is not 3 or 4 there is a problem
305                                 TermError(filename, lineNum, "Invalid statement: "+line)
306                                 continue
307                         }
308                         // Compile the regex
309                         regex, err := regexp.Compile(regexStr)
310                         if err != nil {
311                                 TermError(filename, lineNum, err.Error())
312                                 continue
313                         }
314
315                         // Get the style
316                         // The user could give us a "color" that is really a part of the colorscheme
317                         // in which case we should look that up in the colorscheme
318                         // They can also just give us a straight up color
319                         st := defStyle
320                         groups := strings.Split(color, ".")
321                         if len(groups) > 1 {
322                                 curGroup := ""
323                                 for i, g := range groups {
324                                         if i != 0 {
325                                                 curGroup += "."
326                                         }
327                                         curGroup += g
328                                         if style, ok := colorscheme[curGroup]; ok {
329                                                 st = style
330                                         }
331                                 }
332                         } else if style, ok := colorscheme[color]; ok {
333                                 st = style
334                         } else {
335                                 st = StringToStyle(color)
336                         }
337                         // Add the regex, flags, and style
338                         // False because this is not start-end
339                         rules = append(rules, SyntaxRule{regex, flags, false, st})
340                 } else if ruleStartEndParser.MatchString(line) {
341                         // Start-end syntax rule
342                         submatch := ruleStartEndParser.FindSubmatch([]byte(line))
343                         var color string
344                         var start string
345                         var end string
346                         // Use m and s flags by default
347                         flags := "ms"
348                         if len(submatch) == 5 {
349                                 // If len is 5 the user provided some additional flags
350                                 color = string(submatch[1])
351                                 flags += string(submatch[2])
352                                 start = string(submatch[3])
353                                 end = string(submatch[4])
354                         } else if len(submatch) == 4 {
355                                 // If len is 4 the user did not provide additional flags
356                                 color = string(submatch[1])
357                                 start = string(submatch[2])
358                                 end = string(submatch[3])
359                         } else {
360                                 // If len is not 4 or 5 there is a problem
361                                 TermError(filename, lineNum, "Invalid statement: "+line)
362                                 continue
363                         }
364
365                         // Compile the regex
366                         regex, err := regexp.Compile("(?" + flags + ")" + "(" + start + ").*?(" + end + ")")
367                         if err != nil {
368                                 TermError(filename, lineNum, err.Error())
369                                 continue
370                         }
371
372                         // Get the style
373                         // The user could give us a "color" that is really a part of the colorscheme
374                         // in which case we should look that up in the colorscheme
375                         // They can also just give us a straight up color
376                         st := defStyle
377                         if _, ok := colorscheme[color]; ok {
378                                 st = colorscheme[color]
379                         } else {
380                                 st = StringToStyle(color)
381                         }
382                         // Add the regex, flags, and style
383                         // True because this is start-end
384                         rules = append(rules, SyntaxRule{regex, flags, true, st})
385                 }
386         }
387         return rules
388 }
389
390 // GetRules finds the syntax rules that should be used for the buffer
391 // and returns them. It also returns the filetype of the file
392 func GetRules(buf *Buffer) ([]SyntaxRule, string) {
393         for r := range syntaxFiles {
394                 if r[0] != nil && r[0].MatchString(buf.Path) {
395                         // Check if the syntax statement matches the extension
396                         return LoadRulesFromFile(syntaxFiles[r].text, syntaxFiles[r].filename), syntaxFiles[r].filetype
397                 } else if r[1] != nil && r[1].MatchString(buf.Line(0)) {
398                         // Check if the header statement matches the first line
399                         return LoadRulesFromFile(syntaxFiles[r].text, syntaxFiles[r].filename), syntaxFiles[r].filetype
400                 }
401         }
402         return nil, "Unknown"
403 }
404
405 // SyntaxMatches is an alias to a map from character numbers to styles,
406 // so map[3] represents the style of the third character
407 type SyntaxMatches [][]tcell.Style
408
409 // Match takes a buffer and returns the syntax matches: a 2d array specifying how it should be syntax highlighted
410 // We match the rules from up `synLinesUp` lines and down `synLinesDown` lines
411 func Match(v *View) SyntaxMatches {
412         buf := v.Buf
413         rules := v.Buf.rules
414
415         viewStart := v.Topline
416         viewEnd := v.Topline + v.height
417         if viewEnd > buf.NumLines {
418                 viewEnd = buf.NumLines
419         }
420
421         lines := buf.Lines(viewStart, viewEnd)
422         matches := make(SyntaxMatches, len(lines))
423
424         for i, line := range lines {
425                 matches[i] = make([]tcell.Style, len(line)+1)
426                 for j, _ := range matches[i] {
427                         matches[i][j] = defStyle
428                 }
429         }
430
431         // We don't actually check the entire buffer, just from synLinesUp to synLinesDown
432         totalStart := v.Topline - synLinesUp
433         totalEnd := v.Topline + v.height + synLinesDown
434         if totalStart < 0 {
435                 totalStart = 0
436         }
437         if totalEnd > buf.NumLines {
438                 totalEnd = buf.NumLines
439         }
440
441         str := strings.Join(buf.Lines(totalStart, totalEnd), "\n")
442         startNum := ToCharPos(Loc{0, totalStart}, v.Buf)
443
444         toplineNum := ToCharPos(Loc{0, v.Topline}, v.Buf)
445
446         for _, rule := range rules {
447                 if rule.startend {
448                         if indicies := rule.regex.FindAllStringIndex(str, -1); indicies != nil {
449                                 for _, value := range indicies {
450                                         value[0] = runePos(value[0], str) + startNum
451                                         value[1] = runePos(value[1], str) + startNum
452                                         for i := value[0]; i < value[1]; i++ {
453                                                 if i < toplineNum {
454                                                         continue
455                                                 }
456                                                 loc := FromCharPos(i, buf)
457                                                 colNum, lineNum := loc.X, loc.Y
458                                                 if lineNum == -1 || colNum == -1 {
459                                                         continue
460                                                 }
461                                                 lineNum -= viewStart
462                                                 if lineNum >= 0 && lineNum < v.height {
463                                                         matches[lineNum][colNum] = rule.style
464                                                 }
465                                         }
466                                 }
467                         }
468                 } else {
469                         for lineN, line := range lines {
470                                 if indicies := rule.regex.FindAllStringIndex(line, -1); indicies != nil {
471                                         for _, value := range indicies {
472                                                 start := runePos(value[0], line)
473                                                 end := runePos(value[1], line)
474                                                 for i := start; i < end; i++ {
475                                                         matches[lineN][i] = rule.style
476                                                 }
477                                         }
478                                 }
479                         }
480                 }
481         }
482
483         return matches
484 }