]> git.lizzy.rs Git - micro.git/blob - cmd/micro/highlighter.go
00f8b5a2c75191570bfea7c2ce833ed5ef5b3c72
[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 // LoadSyntaxFiles loads the syntax files from the default directory ~/.micro
33 func LoadSyntaxFiles() {
34         home, err := homedir.Dir()
35         if err != nil {
36                 TermMessage("Error finding your home directory\nCan't load syntax files")
37                 return
38         }
39         LoadSyntaxFilesFromDir(home + "/.micro/syntax")
40 }
41
42 // JoinRule takes a syntax rule (which can be multiple regular expressions)
43 // and joins it into one regular expression by ORing everything together
44 func JoinRule(rule string) string {
45         split := strings.Split(rule, `" "`)
46         joined := strings.Join(split, ")|(")
47         joined = "(" + joined + ")"
48         return joined
49 }
50
51 // LoadSyntaxFile loads the specified syntax file
52 // A syntax file is a list of syntax rules, explaining how to color certain
53 // regular expressions
54 // Example: color comment "//.*"
55 // This would color all strings that match the regex "//.*" in the comment color defined
56 // by the colorscheme
57 func LoadSyntaxFile(filename string) {
58         text, err := ioutil.ReadFile(filename)
59
60         if err != nil {
61                 TermMessage("Error loading syntax file " + filename + ": " + err.Error())
62                 return
63         }
64         lines := strings.Split(string(text), "\n")
65
66         // Regex for parsing syntax statements
67         syntaxParser := regexp.MustCompile(`syntax "(.*?)"\s+"(.*)"+`)
68         // Regex for parsing header statements
69         headerParser := regexp.MustCompile(`header "(.*)"`)
70
71         // Regex for parsing standard syntax rules
72         ruleParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?"(.*)"`)
73         // Regex for parsing syntax rules with start="..." end="..."
74         ruleStartEndParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?start="(.*)"\s+end="(.*)"`)
75
76         var syntaxRegex *regexp.Regexp
77         var headerRegex *regexp.Regexp
78         var filetype string
79         var rules []SyntaxRule
80         for lineNum, line := range lines {
81                 if strings.TrimSpace(line) == "" ||
82                         strings.TrimSpace(line)[0] == '#' {
83                         // Ignore this line
84                         continue
85                 }
86
87                 if strings.HasPrefix(line, "syntax") {
88                         // Syntax statement
89                         syntaxMatches := syntaxParser.FindSubmatch([]byte(line))
90                         if len(syntaxMatches) == 3 {
91                                 if syntaxRegex != nil {
92                                         // Add the current rules to the syntaxFiles variable
93                                         regexes := [2]*regexp.Regexp{syntaxRegex, headerRegex}
94                                         syntaxFiles[regexes] = FileTypeRules{filetype, rules}
95                                 }
96                                 rules = rules[:0]
97
98                                 filetype = string(syntaxMatches[1])
99                                 extensions := JoinRule(string(syntaxMatches[2]))
100
101                                 syntaxRegex, err = regexp.Compile(extensions)
102                                 if err != nil {
103                                         TermError(filename, lineNum, err.Error())
104                                         continue
105                                 }
106                         } else {
107                                 TermError(filename, lineNum, "Syntax statement is not valid: "+line)
108                                 continue
109                         }
110                 } else if strings.HasPrefix(line, "header") {
111                         // Header statement
112                         headerMatches := headerParser.FindSubmatch([]byte(line))
113                         if len(headerMatches) == 2 {
114                                 header := JoinRule(string(headerMatches[1]))
115
116                                 headerRegex, err = regexp.Compile(header)
117                                 if err != nil {
118                                         TermError(filename, lineNum, "Regex error: "+err.Error())
119                                         continue
120                                 }
121                         } else {
122                                 TermError(filename, lineNum, "Header statement is not valid: "+line)
123                                 continue
124                         }
125                 } else {
126                         // Syntax rule, but it could be standard or start-end
127                         if ruleParser.MatchString(line) {
128                                 // Standard syntax rule
129                                 // Parse the line
130                                 submatch := ruleParser.FindSubmatch([]byte(line))
131                                 var color string
132                                 var regexStr string
133                                 var flags string
134                                 if len(submatch) == 4 {
135                                         // If len is 4 then the user specified some additional flags to use
136                                         color = string(submatch[1])
137                                         flags = string(submatch[2])
138                                         regexStr = "(?" + flags + ")" + JoinRule(string(submatch[3]))
139                                 } else if len(submatch) == 3 {
140                                         // If len is 3, no additional flags were given
141                                         color = string(submatch[1])
142                                         regexStr = JoinRule(string(submatch[2]))
143                                 } else {
144                                         // If len is not 3 or 4 there is a problem
145                                         TermError(filename, lineNum, "Invalid statement: "+line)
146                                         continue
147                                 }
148                                 // Compile the regex
149                                 regex, err := regexp.Compile(regexStr)
150                                 if err != nil {
151                                         TermError(filename, lineNum, err.Error())
152                                         continue
153                                 }
154
155                                 // Get the style
156                                 // The user could give us a "color" that is really a part of the colorscheme
157                                 // in which case we should look that up in the colorscheme
158                                 // They can also just give us a straight up color
159                                 st := defStyle
160                                 if _, ok := colorscheme[color]; ok {
161                                         st = colorscheme[color]
162                                 } else {
163                                         st = StringToStyle(color)
164                                 }
165                                 // Add the regex, flags, and style
166                                 // False because this is not start-end
167                                 rules = append(rules, SyntaxRule{regex, flags, false, st})
168                         } else if ruleStartEndParser.MatchString(line) {
169                                 // Start-end syntax rule
170                                 submatch := ruleStartEndParser.FindSubmatch([]byte(line))
171                                 var color string
172                                 var start string
173                                 var end string
174                                 // Use m and s flags by default
175                                 flags := "ms"
176                                 if len(submatch) == 5 {
177                                         // If len is 5 the user provided some additional flags
178                                         color = string(submatch[1])
179                                         flags += string(submatch[2])
180                                         start = string(submatch[3])
181                                         end = string(submatch[4])
182                                 } else if len(submatch) == 4 {
183                                         // If len is 4 the user did not provide additional flags
184                                         color = string(submatch[1])
185                                         start = string(submatch[2])
186                                         end = string(submatch[3])
187                                 } else {
188                                         // If len is not 4 or 5 there is a problem
189                                         TermError(filename, lineNum, "Invalid statement: "+line)
190                                         continue
191                                 }
192
193                                 // Compile the regex
194                                 regex, err := regexp.Compile("(?" + flags + ")" + "(" + start + ").*?(" + end + ")")
195                                 if err != nil {
196                                         TermError(filename, lineNum, err.Error())
197                                         continue
198                                 }
199
200                                 // Get the style
201                                 // The user could give us a "color" that is really a part of the colorscheme
202                                 // in which case we should look that up in the colorscheme
203                                 // They can also just give us a straight up color
204                                 st := defStyle
205                                 if _, ok := colorscheme[color]; ok {
206                                         st = colorscheme[color]
207                                 } else {
208                                         st = StringToStyle(color)
209                                 }
210                                 // Add the regex, flags, and style
211                                 // True because this is start-end
212                                 rules = append(rules, SyntaxRule{regex, flags, true, st})
213                         }
214                 }
215         }
216         if syntaxRegex != nil {
217                 // Add the current rules to the syntaxFiles variable
218                 regexes := [2]*regexp.Regexp{syntaxRegex, headerRegex}
219                 syntaxFiles[regexes] = FileTypeRules{filetype, rules}
220         }
221 }
222
223 // LoadSyntaxFilesFromDir loads the syntax files from a specified directory
224 // To load the syntax files, we must fill the `syntaxFiles` map
225 // This involves finding the regex for syntax and if it exists, the regex
226 // for the header. Then we must get the text for the file and the filetype.
227 func LoadSyntaxFilesFromDir(dir string) {
228         InitColorscheme()
229
230         syntaxFiles = make(map[[2]*regexp.Regexp]FileTypeRules)
231         files, _ := ioutil.ReadDir(dir)
232         for _, f := range files {
233                 if filepath.Ext(f.Name()) == ".micro" {
234                         LoadSyntaxFile(dir + "/" + f.Name())
235                 }
236         }
237 }
238
239 // GetRules finds the syntax rules that should be used for the buffer
240 // and returns them. It also returns the filetype of the file
241 func GetRules(buf *Buffer) ([]SyntaxRule, string) {
242         for r := range syntaxFiles {
243                 if r[0] != nil && r[0].MatchString(buf.path) {
244                         return syntaxFiles[r].rules, syntaxFiles[r].filetype
245                 } else if r[1] != nil && r[1].MatchString(buf.lines[0]) {
246                         return syntaxFiles[r].rules, syntaxFiles[r].filetype
247                 }
248         }
249         return nil, "Unknown"
250 }
251
252 // SyntaxMatches is an alias to a map from character numbers to styles,
253 // so map[3] represents the style of the third character
254 type SyntaxMatches [][]tcell.Style
255
256 // Match takes a buffer and returns the syntax matches a map specifying how it should be syntax highlighted
257 // We need to check the start-end regexes for the entire buffer every time Match is called, but for the
258 // non start-end rules, we only have to update the updateLines provided by the view
259 func Match(v *View) SyntaxMatches {
260         buf := v.buf
261         rules := v.buf.rules
262
263         viewStart := v.topline
264         viewEnd := v.topline + v.height
265         if viewEnd > len(buf.lines) {
266                 viewEnd = len(buf.lines)
267         }
268
269         // updateStart := v.updateLines[0]
270         // updateEnd := v.updateLines[1]
271         //
272         // if updateEnd > len(buf.lines) {
273         //      updateEnd = len(buf.lines)
274         // }
275         // if updateStart < 0 {
276         //      updateStart = 0
277         // }
278         lines := buf.lines[viewStart:viewEnd]
279         // updateLines := buf.lines[updateStart:updateEnd]
280         matches := make(SyntaxMatches, len(lines))
281
282         for i, line := range lines {
283                 matches[i] = make([]tcell.Style, len(line)+1)
284         }
285
286         // We don't actually check the entire buffer, just from synLinesUp to synLinesDown
287         totalStart := v.topline - synLinesUp
288         totalEnd := v.topline + v.height + synLinesDown
289         if totalStart < 0 {
290                 totalStart = 0
291         }
292         if totalEnd > len(buf.lines) {
293                 totalEnd = len(buf.lines)
294         }
295
296         str := strings.Join(buf.lines[totalStart:totalEnd], "\n")
297         startNum := ToCharPos(0, totalStart, v.buf)
298
299         toplineNum := ToCharPos(0, v.topline, v.buf)
300
301         for _, rule := range rules {
302                 if rule.startend {
303                         if indicies := rule.regex.FindAllStringIndex(str, -1); indicies != nil {
304                                 for _, value := range indicies {
305                                         value[0] += startNum
306                                         value[1] += startNum
307                                         for i := value[0]; i < value[1]; i++ {
308                                                 if i < toplineNum {
309                                                         continue
310                                                 }
311                                                 colNum, lineNum := FromCharPosStart(toplineNum, 0, v.topline, i, buf)
312                                                 if lineNum == -1 || colNum == -1 {
313                                                         continue
314                                                 }
315                                                 lineNum -= viewStart
316                                                 if lineNum >= 0 && lineNum < v.height {
317                                                         matches[lineNum][colNum] = rule.style
318                                                 }
319                                         }
320                                 }
321                         }
322                 } else {
323                         for lineN, line := range lines {
324                                 if indicies := rule.regex.FindAllStringIndex(line, -1); indicies != nil {
325                                         for _, value := range indicies {
326                                                 for i := value[0]; i < value[1]; i++ {
327                                                         // matches[lineN+updateStart][i] = rule.style
328                                                         matches[lineN][i] = rule.style
329                                                 }
330                                         }
331                                 }
332                         }
333                 }
334         }
335
336         return matches
337 }