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