]> git.lizzy.rs Git - micro.git/blob - src/highlighter.go
Much improved highlighting (turned off for now)
[micro.git] / src / highlighter.go
1 package main
2
3 import (
4         "github.com/gdamore/tcell"
5         "io/ioutil"
6         "os/user"
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         usr, _ := user.Current()
35         dir := usr.HomeDir
36         LoadSyntaxFilesFromDir(dir + "/.micro/syntax")
37 }
38
39 // JoinRule takes a syntax rule (which can be multiple regular expressions)
40 // and joins it into one regular expression by ORing everything together
41 func JoinRule(rule string) string {
42         split := strings.Split(rule, `" "`)
43         joined := strings.Join(split, ")|(")
44         joined = "(" + joined + ")"
45         return joined
46 }
47
48 // LoadSyntaxFilesFromDir loads the syntax files from a specified directory
49 // To load the syntax files, we must fill the `syntaxFiles` map
50 // This involves finding the regex for syntax and if it exists, the regex
51 // for the header. Then we must get the text for the file and the filetype.
52 func LoadSyntaxFilesFromDir(dir string) {
53         InitColorscheme()
54
55         syntaxFiles = make(map[[2]*regexp.Regexp]FileTypeRules)
56         files, _ := ioutil.ReadDir(dir)
57         for _, f := range files {
58                 if filepath.Ext(f.Name()) == ".micro" {
59                         text, err := ioutil.ReadFile(dir + "/" + f.Name())
60                         filename := dir + "/" + f.Name()
61
62                         if err != nil {
63                                 TermMessage("Error loading syntax files: " + err.Error())
64                                 continue
65                         }
66                         lines := strings.Split(string(text), "\n")
67
68                         syntaxParser := regexp.MustCompile(`syntax "(.*?)"\s+"(.*)"+`)
69                         headerParser := regexp.MustCompile(`header "(.*)"`)
70
71                         ruleParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?"(.*)"`)
72                         ruleStartEndParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?start="(.*?)"\s+end="(.*?)"`)
73
74                         var syntaxRegex *regexp.Regexp
75                         var headerRegex *regexp.Regexp
76                         var filetype string
77                         var rules []SyntaxRule
78                         for lineNum, line := range lines {
79                                 if strings.TrimSpace(line) == "" ||
80                                         strings.TrimSpace(line)[0] == '#' {
81                                         // Ignore this line
82                                         continue
83                                 }
84
85                                 if strings.HasPrefix(line, "syntax") {
86                                         syntaxMatches := syntaxParser.FindSubmatch([]byte(line))
87                                         if len(syntaxMatches) == 3 {
88                                                 if syntaxRegex != nil {
89                                                         regexes := [2]*regexp.Regexp{syntaxRegex, headerRegex}
90                                                         syntaxFiles[regexes] = FileTypeRules{filetype, rules}
91                                                 }
92                                                 rules = rules[:0]
93
94                                                 filetype = string(syntaxMatches[1])
95                                                 extensions := JoinRule(string(syntaxMatches[2]))
96
97                                                 syntaxRegex, err = regexp.Compile(extensions)
98                                                 if err != nil {
99                                                         TermError(filename, lineNum, err.Error())
100                                                         continue
101                                                 }
102                                         } else {
103                                                 TermError(filename, lineNum, "Syntax statement is not valid: "+line)
104                                                 continue
105                                         }
106                                 } else if strings.HasPrefix(line, "header") {
107                                         headerMatches := headerParser.FindSubmatch([]byte(line))
108                                         if len(headerMatches) == 2 {
109                                                 header := JoinRule(string(headerMatches[1]))
110
111                                                 headerRegex, err = regexp.Compile(header)
112                                                 if err != nil {
113                                                         TermError(filename, lineNum, "Regex error: "+err.Error())
114                                                         continue
115                                                 }
116                                         } else {
117                                                 TermError(filename, lineNum, "Header statement is not valid: "+line)
118                                                 continue
119                                         }
120                                 } else {
121                                         if ruleParser.MatchString(line) {
122                                                 submatch := ruleParser.FindSubmatch([]byte(line))
123                                                 var color string
124                                                 var regexStr string
125                                                 var flags string
126                                                 if len(submatch) == 4 {
127                                                         color = string(submatch[1])
128                                                         flags = string(submatch[2])
129                                                         regexStr = "(?" + flags + ")" + JoinRule(string(submatch[3]))
130                                                 } else if len(submatch) == 3 {
131                                                         color = string(submatch[1])
132                                                         regexStr = JoinRule(string(submatch[2]))
133                                                 } else {
134                                                         TermError(filename, lineNum, "Invalid statement: "+line)
135                                                 }
136                                                 regex, err := regexp.Compile(regexStr)
137                                                 if err != nil {
138                                                         TermError(filename, lineNum, err.Error())
139                                                         continue
140                                                 }
141
142                                                 st := tcell.StyleDefault
143                                                 if _, ok := colorscheme[color]; ok {
144                                                         st = colorscheme[color]
145                                                 } else {
146                                                         st = StringToStyle(color)
147                                                 }
148                                                 rules = append(rules, SyntaxRule{regex, flags, false, st})
149                                         } else if ruleStartEndParser.MatchString(line) {
150                                                 submatch := ruleStartEndParser.FindSubmatch([]byte(line))
151                                                 var color string
152                                                 var start string
153                                                 var end string
154                                                 // Use m and s flags by default
155                                                 flags := "ms"
156                                                 if len(submatch) == 5 {
157                                                         color = string(submatch[1])
158                                                         flags = string(submatch[2])
159                                                         start = string(submatch[3])
160                                                         end = string(submatch[4])
161                                                 } else if len(submatch) == 4 {
162                                                         color = string(submatch[1])
163                                                         start = string(submatch[2])
164                                                         end = string(submatch[3])
165                                                 } else {
166                                                         TermError(filename, lineNum, "Invalid statement: "+line)
167                                                 }
168
169                                                 regex, err := regexp.Compile("(?" + flags + ")" + "(" + start + ").*?(" + end + ")")
170                                                 if err != nil {
171                                                         TermError(filename, lineNum, err.Error())
172                                                         continue
173                                                 }
174
175                                                 st := tcell.StyleDefault
176                                                 if _, ok := colorscheme[color]; ok {
177                                                         st = colorscheme[color]
178                                                 } else {
179                                                         st = StringToStyle(color)
180                                                 }
181                                                 rules = append(rules, SyntaxRule{regex, flags, true, st})
182                                         }
183                                 }
184                         }
185                         if syntaxRegex != nil {
186                                 regexes := [2]*regexp.Regexp{syntaxRegex, headerRegex}
187                                 syntaxFiles[regexes] = FileTypeRules{filetype, rules}
188                         }
189                 }
190         }
191 }
192
193 // GetRules finds the syntax rules that should be used for the buffer
194 // and returns them. It also returns the filetype of the file
195 func GetRules(buf *Buffer) ([]SyntaxRule, string) {
196         for r := range syntaxFiles {
197                 if r[0] != nil && r[0].MatchString(buf.path) {
198                         return syntaxFiles[r].rules, syntaxFiles[r].filetype
199                 } else if r[1] != nil && r[1].MatchString(buf.lines[0]) {
200                         return syntaxFiles[r].rules, syntaxFiles[r].filetype
201                 }
202         }
203         return nil, "Unknown"
204 }
205
206 // SyntaxMatches is an alias to a map from character numbers to styles,
207 // so map[3] represents the style of the third character
208 type SyntaxMatches map[int]tcell.Style
209
210 // Match takes a buffer and returns the syntax matches a map specifying how it should be syntax highlighted
211 func Match(rules []SyntaxRule, buf *Buffer, v *View) SyntaxMatches {
212         m := make(SyntaxMatches)
213
214         lineStart := v.updateLines[0]
215         lineEnd := v.updateLines[1] + 1
216         if lineStart < 0 {
217                 // Don't need to update syntax highlighting
218                 return m
219         }
220
221         totalStart := v.topline - synLinesUp
222         totalEnd := v.topline + v.height + synLinesDown
223         if totalStart < 0 {
224                 totalStart = 0
225         }
226         if totalEnd > len(buf.lines) {
227                 totalEnd = len(buf.lines)
228         }
229
230         if lineEnd > len(buf.lines) {
231                 lineEnd = len(buf.lines)
232         }
233
234         lines := buf.lines[lineStart:lineEnd]
235         str := strings.Join(buf.lines[totalStart:totalEnd], "\n")
236         startNum := v.cursor.loc + v.cursor.Distance(0, totalStart)
237         toplineNum := v.cursor.loc + v.cursor.Distance(0, v.topline)
238         for _, rule := range rules {
239                 if rule.startend && rule.regex.MatchString(str) {
240                         indicies := rule.regex.FindAllStringIndex(str, -1)
241                         for _, value := range indicies {
242                                 value[0] += startNum
243                                 value[1] += startNum
244                                 for i := value[0]; i < value[1]; i++ {
245                                         if i >= toplineNum {
246                                                 m[i] = rule.style
247                                         }
248                                 }
249                         }
250                 } else {
251                         for _, line := range lines {
252                                 if rule.regex.MatchString(line) {
253                                         indicies := rule.regex.FindAllStringIndex(str, -1)
254                                         for _, value := range indicies {
255                                                 value[0] += toplineNum
256                                                 value[1] += toplineNum
257                                                 for i := value[0]; i < value[1]; i++ {
258                                                         m[i] = rule.style
259                                                 }
260                                         }
261                                 }
262                         }
263                 }
264         }
265
266         return m
267 }