]> git.lizzy.rs Git - micro.git/blob - cmd/micro/highlighter.go
Create ~/.micro if it does not exist
[micro.git] / cmd / micro / highlighter.go
1 package main
2
3 import (
4         "github.com/gdamore/tcell"
5         "github.com/mitchellh/go-homedir"
6         "io/ioutil"
7         "path/filepath"
8         "regexp"
9         "strings"
10 )
11
12 // FileTypeRules represents a complete set of syntax rules for a filetype
13 type FileTypeRules struct {
14         filetype string
15         rules    []SyntaxRule
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",
54         "git",
55         "glsl",
56         "go",
57         "groff",
58         "haml",
59         "haskell",
60         "html",
61         "ini",
62         "inputrc",
63         "java",
64         "javascript",
65         "json",
66         "keymap",
67         "kickstart",
68         "ledger",
69         "lisp",
70         "lua",
71         "makefile",
72         "man",
73         "markdown",
74         "mpdconf",
75         "nanorc",
76         "nginx",
77         "ocaml",
78         "patch",
79         "peg",
80         "perl",
81         "perl6",
82         "php",
83         "pkg-config",
84         "pkgbuild",
85         "po",
86         "pov",
87         "privoxy",
88         "puppet",
89         "python",
90         "reST",
91         "rpmspec",
92         "ruby",
93         "rust",
94         "scala",
95         "sed",
96         "sh",
97         "sls",
98         "sql",
99         "swift",
100         "systemd",
101         "tcl",
102         "tex",
103         "vala",
104         "vi",
105         "xml",
106         "xresources",
107         "yaml",
108         "yum",
109         "zsh",
110 }
111
112 // LoadSyntaxFiles loads the syntax files from the default directory ~/.micro
113 func LoadSyntaxFiles() {
114         home, err := homedir.Dir()
115         if err != nil {
116                 TermMessage("Error finding your home directory\nCan't load syntax files")
117                 return
118         }
119         LoadSyntaxFilesFromDir(home + "/.micro/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         // Regex for parsing standard syntax rules
180         ruleParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?"(.*)"`)
181         // Regex for parsing syntax rules with start="..." end="..."
182         ruleStartEndParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?start="(.*)"\s+end="(.*)"`)
183
184         var syntaxRegex *regexp.Regexp
185         var headerRegex *regexp.Regexp
186         var filetype string
187         var rules []SyntaxRule
188         for lineNum, line := range lines {
189                 if strings.TrimSpace(line) == "" ||
190                         strings.TrimSpace(line)[0] == '#' {
191                         // Ignore this line
192                         continue
193                 }
194
195                 if strings.HasPrefix(line, "syntax") {
196                         // Syntax statement
197                         syntaxMatches := syntaxParser.FindSubmatch([]byte(line))
198                         if len(syntaxMatches) == 3 {
199                                 if syntaxRegex != nil {
200                                         // Add the current rules to the syntaxFiles variable
201                                         regexes := [2]*regexp.Regexp{syntaxRegex, headerRegex}
202                                         syntaxFiles[regexes] = FileTypeRules{filetype, rules}
203                                 }
204                                 rules = rules[:0]
205
206                                 filetype = string(syntaxMatches[1])
207                                 extensions := JoinRule(string(syntaxMatches[2]))
208
209                                 syntaxRegex, err = regexp.Compile(extensions)
210                                 if err != nil {
211                                         TermError(filename, lineNum, err.Error())
212                                         continue
213                                 }
214                         } else {
215                                 TermError(filename, lineNum, "Syntax statement is not valid: "+line)
216                                 continue
217                         }
218                 } else if strings.HasPrefix(line, "header") {
219                         // Header statement
220                         headerMatches := headerParser.FindSubmatch([]byte(line))
221                         if len(headerMatches) == 2 {
222                                 header := JoinRule(string(headerMatches[1]))
223
224                                 headerRegex, err = regexp.Compile(header)
225                                 if err != nil {
226                                         TermError(filename, lineNum, "Regex error: "+err.Error())
227                                         continue
228                                 }
229                         } else {
230                                 TermError(filename, lineNum, "Header statement is not valid: "+line)
231                                 continue
232                         }
233                 } else {
234                         // Syntax rule, but it could be standard or start-end
235                         if ruleParser.MatchString(line) {
236                                 // Standard syntax rule
237                                 // Parse the line
238                                 submatch := ruleParser.FindSubmatch([]byte(line))
239                                 var color string
240                                 var regexStr string
241                                 var flags string
242                                 if len(submatch) == 4 {
243                                         // If len is 4 then the user specified some additional flags to use
244                                         color = string(submatch[1])
245                                         flags = string(submatch[2])
246                                         regexStr = "(?" + flags + ")" + JoinRule(string(submatch[3]))
247                                 } else if len(submatch) == 3 {
248                                         // If len is 3, no additional flags were given
249                                         color = string(submatch[1])
250                                         regexStr = JoinRule(string(submatch[2]))
251                                 } else {
252                                         // If len is not 3 or 4 there is a problem
253                                         TermError(filename, lineNum, "Invalid statement: "+line)
254                                         continue
255                                 }
256                                 // Compile the regex
257                                 regex, err := regexp.Compile(regexStr)
258                                 if err != nil {
259                                         TermError(filename, lineNum, err.Error())
260                                         continue
261                                 }
262
263                                 // Get the style
264                                 // The user could give us a "color" that is really a part of the colorscheme
265                                 // in which case we should look that up in the colorscheme
266                                 // They can also just give us a straight up color
267                                 st := defStyle
268                                 if _, ok := colorscheme[color]; ok {
269                                         st = colorscheme[color]
270                                 } else {
271                                         st = StringToStyle(color)
272                                 }
273                                 // Add the regex, flags, and style
274                                 // False because this is not start-end
275                                 rules = append(rules, SyntaxRule{regex, flags, false, st})
276                         } else if ruleStartEndParser.MatchString(line) {
277                                 // Start-end syntax rule
278                                 submatch := ruleStartEndParser.FindSubmatch([]byte(line))
279                                 var color string
280                                 var start string
281                                 var end string
282                                 // Use m and s flags by default
283                                 flags := "ms"
284                                 if len(submatch) == 5 {
285                                         // If len is 5 the user provided some additional flags
286                                         color = string(submatch[1])
287                                         flags += string(submatch[2])
288                                         start = string(submatch[3])
289                                         end = string(submatch[4])
290                                 } else if len(submatch) == 4 {
291                                         // If len is 4 the user did not provide additional flags
292                                         color = string(submatch[1])
293                                         start = string(submatch[2])
294                                         end = string(submatch[3])
295                                 } else {
296                                         // If len is not 4 or 5 there is a problem
297                                         TermError(filename, lineNum, "Invalid statement: "+line)
298                                         continue
299                                 }
300
301                                 // Compile the regex
302                                 regex, err := regexp.Compile("(?" + flags + ")" + "(" + start + ").*?(" + end + ")")
303                                 if err != nil {
304                                         TermError(filename, lineNum, err.Error())
305                                         continue
306                                 }
307
308                                 // Get the style
309                                 // The user could give us a "color" that is really a part of the colorscheme
310                                 // in which case we should look that up in the colorscheme
311                                 // They can also just give us a straight up color
312                                 st := defStyle
313                                 if _, ok := colorscheme[color]; ok {
314                                         st = colorscheme[color]
315                                 } else {
316                                         st = StringToStyle(color)
317                                 }
318                                 // Add the regex, flags, and style
319                                 // True because this is start-end
320                                 rules = append(rules, SyntaxRule{regex, flags, true, st})
321                         }
322                 }
323         }
324         if syntaxRegex != nil {
325                 // Add the current rules to the syntaxFiles variable
326                 regexes := [2]*regexp.Regexp{syntaxRegex, headerRegex}
327                 syntaxFiles[regexes] = FileTypeRules{filetype, rules}
328         }
329 }
330
331 // GetRules finds the syntax rules that should be used for the buffer
332 // and returns them. It also returns the filetype of the file
333 func GetRules(buf *Buffer) ([]SyntaxRule, string) {
334         for r := range syntaxFiles {
335                 if r[0] != nil && r[0].MatchString(buf.path) {
336                         return syntaxFiles[r].rules, syntaxFiles[r].filetype
337                 } else if r[1] != nil && r[1].MatchString(buf.lines[0]) {
338                         return syntaxFiles[r].rules, syntaxFiles[r].filetype
339                 }
340         }
341         return nil, "Unknown"
342 }
343
344 // SyntaxMatches is an alias to a map from character numbers to styles,
345 // so map[3] represents the style of the third character
346 type SyntaxMatches [][]tcell.Style
347
348 // Match takes a buffer and returns the syntax matches a map specifying how it should be syntax highlighted
349 // We need to check the start-end regexes for the entire buffer every time Match is called, but for the
350 // non start-end rules, we only have to update the updateLines provided by the view
351 func Match(v *View) SyntaxMatches {
352         buf := v.buf
353         rules := v.buf.rules
354
355         viewStart := v.topline
356         viewEnd := v.topline + v.height
357         if viewEnd > len(buf.lines) {
358                 viewEnd = len(buf.lines)
359         }
360
361         // updateStart := v.updateLines[0]
362         // updateEnd := v.updateLines[1]
363         //
364         // if updateEnd > len(buf.lines) {
365         //      updateEnd = len(buf.lines)
366         // }
367         // if updateStart < 0 {
368         //      updateStart = 0
369         // }
370         lines := buf.lines[viewStart:viewEnd]
371         // updateLines := buf.lines[updateStart:updateEnd]
372         matches := make(SyntaxMatches, len(lines))
373
374         for i, line := range lines {
375                 matches[i] = make([]tcell.Style, len(line)+1)
376         }
377
378         // We don't actually check the entire buffer, just from synLinesUp to synLinesDown
379         totalStart := v.topline - synLinesUp
380         totalEnd := v.topline + v.height + synLinesDown
381         if totalStart < 0 {
382                 totalStart = 0
383         }
384         if totalEnd > len(buf.lines) {
385                 totalEnd = len(buf.lines)
386         }
387
388         str := strings.Join(buf.lines[totalStart:totalEnd], "\n")
389         startNum := ToCharPos(0, totalStart, v.buf)
390
391         toplineNum := ToCharPos(0, v.topline, v.buf)
392
393         for _, rule := range rules {
394                 if rule.startend {
395                         if indicies := rule.regex.FindAllStringIndex(str, -1); indicies != nil {
396                                 for _, value := range indicies {
397                                         value[0] += startNum
398                                         value[1] += startNum
399                                         for i := value[0]; i < value[1]; i++ {
400                                                 if i < toplineNum {
401                                                         continue
402                                                 }
403                                                 colNum, lineNum := FromCharPosStart(toplineNum, 0, v.topline, i, buf)
404                                                 if lineNum == -1 || colNum == -1 {
405                                                         continue
406                                                 }
407                                                 lineNum -= viewStart
408                                                 if lineNum >= 0 && lineNum < v.height {
409                                                         matches[lineNum][colNum] = rule.style
410                                                 }
411                                         }
412                                 }
413                         }
414                 } else {
415                         for lineN, line := range lines {
416                                 if indicies := rule.regex.FindAllStringIndex(line, -1); indicies != nil {
417                                         for _, value := range indicies {
418                                                 for i := value[0]; i < value[1]; i++ {
419                                                         // matches[lineN+updateStart][i] = rule.style
420                                                         matches[lineN][i] = rule.style
421                                                 }
422                                         }
423                                 }
424                         }
425                 }
426         }
427
428         return matches
429 }