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