]> git.lizzy.rs Git - micro.git/blob - cmd/micro/highlighter.go
shell command output is held in buffer until completion
[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 map specifying how it should be syntax highlighted
381 // We need to check the start-end regexes for the entire buffer every time Match is called, but for the
382 // non start-end rules, we only have to update the updateLines provided by the view
383 func Match(v *View) SyntaxMatches {
384         buf := v.buf
385         rules := v.buf.rules
386
387         viewStart := v.topline
388         viewEnd := v.topline + v.height
389         if viewEnd > len(buf.lines) {
390                 viewEnd = len(buf.lines)
391         }
392
393         // updateStart := v.updateLines[0]
394         // updateEnd := v.updateLines[1]
395         //
396         // if updateEnd > len(buf.lines) {
397         //      updateEnd = len(buf.lines)
398         // }
399         // if updateStart < 0 {
400         //      updateStart = 0
401         // }
402         lines := buf.lines[viewStart:viewEnd]
403         // updateLines := buf.lines[updateStart:updateEnd]
404         matches := make(SyntaxMatches, len(lines))
405
406         for i, line := range lines {
407                 matches[i] = make([]tcell.Style, len(line)+1)
408         }
409
410         // We don't actually check the entire buffer, just from synLinesUp to synLinesDown
411         totalStart := v.topline - synLinesUp
412         totalEnd := v.topline + v.height + synLinesDown
413         if totalStart < 0 {
414                 totalStart = 0
415         }
416         if totalEnd > len(buf.lines) {
417                 totalEnd = len(buf.lines)
418         }
419
420         str := strings.Join(buf.lines[totalStart:totalEnd], "\n")
421         startNum := ToCharPos(0, totalStart, v.buf)
422
423         toplineNum := ToCharPos(0, v.topline, v.buf)
424
425         for _, rule := range rules {
426                 if rule.startend {
427                         if indicies := rule.regex.FindAllStringIndex(str, -1); indicies != nil {
428                                 for _, value := range indicies {
429                                         value[0] += startNum
430                                         value[1] += startNum
431                                         for i := value[0]; i < value[1]; i++ {
432                                                 if i < toplineNum {
433                                                         continue
434                                                 }
435                                                 colNum, lineNum := FromCharPosStart(toplineNum, 0, v.topline, i, buf)
436                                                 if lineNum == -1 || colNum == -1 {
437                                                         continue
438                                                 }
439                                                 lineNum -= viewStart
440                                                 if lineNum >= 0 && lineNum < v.height {
441                                                         matches[lineNum][colNum] = rule.style
442                                                 }
443                                         }
444                                 }
445                         }
446                 } else {
447                         for lineN, line := range lines {
448                                 if indicies := rule.regex.FindAllStringIndex(line, -1); indicies != nil {
449                                         for _, value := range indicies {
450                                                 for i := value[0]; i < value[1]; i++ {
451                                                         // matches[lineN+updateStart][i] = rule.style
452                                                         matches[lineN][i] = rule.style
453                                                 }
454                                         }
455                                 }
456                         }
457                 }
458         }
459
460         return matches
461 }