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