From e1506e300d8f5b01947358ba98f01a5339a4a780 Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Fri, 25 Mar 2016 14:32:35 -0400 Subject: [PATCH] Much improved highlighting (turned off for now) --- src/highlighter.go | 119 ++++++++++++++++++++++++++++++++++++--------- src/messenger.go | 7 +++ src/micro.go | 3 ++ src/statusline.go | 3 ++ src/view.go | 60 ++++++++++++++++++++--- 5 files changed, 160 insertions(+), 32 deletions(-) diff --git a/src/highlighter.go b/src/highlighter.go index 8ef9ad4a..ef91f5c5 100644 --- a/src/highlighter.go +++ b/src/highlighter.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "github.com/gdamore/tcell" "io/ioutil" "os/user" @@ -20,6 +19,10 @@ type FileTypeRules struct { type SyntaxRule struct { // What to highlight regex *regexp.Regexp + // Any flags + flags string + // Whether this regex is a start=... end=... regex + startend bool // How to highlight it style tcell.Style } @@ -54,16 +57,19 @@ func LoadSyntaxFilesFromDir(dir string) { for _, f := range files { if filepath.Ext(f.Name()) == ".micro" { text, err := ioutil.ReadFile(dir + "/" + f.Name()) + filename := dir + "/" + f.Name() if err != nil { - fmt.Println("Error loading syntax files:", err) + TermMessage("Error loading syntax files: " + err.Error()) continue } lines := strings.Split(string(text), "\n") syntaxParser := regexp.MustCompile(`syntax "(.*?)"\s+"(.*)"+`) headerParser := regexp.MustCompile(`header "(.*)"`) + ruleParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?"(.*)"`) + ruleStartEndParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?start="(.*?)"\s+end="(.*?)"`) var syntaxRegex *regexp.Regexp var headerRegex *regexp.Regexp @@ -90,11 +96,11 @@ func LoadSyntaxFilesFromDir(dir string) { syntaxRegex, err = regexp.Compile(extensions) if err != nil { - fmt.Println("Regex error:", err) + TermError(filename, lineNum, err.Error()) continue } } else { - fmt.Println("Syntax statement is not valid:", line) + TermError(filename, lineNum, "Syntax statement is not valid: "+line) continue } } else if strings.HasPrefix(line, "header") { @@ -104,26 +110,32 @@ func LoadSyntaxFilesFromDir(dir string) { headerRegex, err = regexp.Compile(header) if err != nil { - fmt.Println("Regex error:", err) + TermError(filename, lineNum, "Regex error: "+err.Error()) continue } } else { - fmt.Println("Header statement is not valid:", line) + TermError(filename, lineNum, "Header statement is not valid: "+line) continue } } else { if ruleParser.MatchString(line) { submatch := ruleParser.FindSubmatch([]byte(line)) - color := string(submatch[1]) + var color string var regexStr string + var flags string if len(submatch) == 4 { - regexStr = "(?m" + string(submatch[2]) + ")" + JoinRule(string(submatch[3])) + color = string(submatch[1]) + flags = string(submatch[2]) + regexStr = "(?" + flags + ")" + JoinRule(string(submatch[3])) } else if len(submatch) == 3 { - regexStr = "(?m)" + JoinRule(string(submatch[2])) + color = string(submatch[1]) + regexStr = JoinRule(string(submatch[2])) + } else { + TermError(filename, lineNum, "Invalid statement: "+line) } regex, err := regexp.Compile(regexStr) if err != nil { - fmt.Println(f.Name(), lineNum, err) + TermError(filename, lineNum, err.Error()) continue } @@ -133,7 +145,40 @@ func LoadSyntaxFilesFromDir(dir string) { } else { st = StringToStyle(color) } - rules = append(rules, SyntaxRule{regex, st}) + rules = append(rules, SyntaxRule{regex, flags, false, st}) + } else if ruleStartEndParser.MatchString(line) { + submatch := ruleStartEndParser.FindSubmatch([]byte(line)) + var color string + var start string + var end string + // Use m and s flags by default + flags := "ms" + if len(submatch) == 5 { + color = string(submatch[1]) + flags = string(submatch[2]) + start = string(submatch[3]) + end = string(submatch[4]) + } else if len(submatch) == 4 { + color = string(submatch[1]) + start = string(submatch[2]) + end = string(submatch[3]) + } else { + TermError(filename, lineNum, "Invalid statement: "+line) + } + + regex, err := regexp.Compile("(?" + flags + ")" + "(" + start + ").*?(" + end + ")") + if err != nil { + TermError(filename, lineNum, err.Error()) + continue + } + + st := tcell.StyleDefault + if _, ok := colorscheme[color]; ok { + st = colorscheme[color] + } else { + st = StringToStyle(color) + } + rules = append(rules, SyntaxRule{regex, flags, true, st}) } } } @@ -164,21 +209,34 @@ type SyntaxMatches map[int]tcell.Style // Match takes a buffer and returns the syntax matches a map specifying how it should be syntax highlighted func Match(rules []SyntaxRule, buf *Buffer, v *View) SyntaxMatches { - start := v.topline - synLinesUp - end := v.topline + v.height + synLinesDown - if start < 0 { - start = 0 - } - if end > len(buf.lines) { - end = len(buf.lines) - } - str := strings.Join(buf.lines[start:end], "\n") - startNum := v.cursor.loc + v.cursor.Distance(0, start) - toplineNum := v.cursor.loc + v.cursor.Distance(0, v.topline) + m := make(SyntaxMatches) - m := make(map[int]tcell.Style) + lineStart := v.updateLines[0] + lineEnd := v.updateLines[1] + 1 + if lineStart < 0 { + // Don't need to update syntax highlighting + return m + } + + totalStart := v.topline - synLinesUp + totalEnd := v.topline + v.height + synLinesDown + if totalStart < 0 { + totalStart = 0 + } + if totalEnd > len(buf.lines) { + totalEnd = len(buf.lines) + } + + if lineEnd > len(buf.lines) { + lineEnd = len(buf.lines) + } + + lines := buf.lines[lineStart:lineEnd] + str := strings.Join(buf.lines[totalStart:totalEnd], "\n") + startNum := v.cursor.loc + v.cursor.Distance(0, totalStart) + toplineNum := v.cursor.loc + v.cursor.Distance(0, v.topline) for _, rule := range rules { - if rule.regex.MatchString(str) { + if rule.startend && rule.regex.MatchString(str) { indicies := rule.regex.FindAllStringIndex(str, -1) for _, value := range indicies { value[0] += startNum @@ -189,6 +247,19 @@ func Match(rules []SyntaxRule, buf *Buffer, v *View) SyntaxMatches { } } } + } else { + for _, line := range lines { + if rule.regex.MatchString(line) { + indicies := rule.regex.FindAllStringIndex(str, -1) + for _, value := range indicies { + value[0] += toplineNum + value[1] += toplineNum + for i := value[0]; i < value[1]; i++ { + m[i] = rule.style + } + } + } + } } } diff --git a/src/messenger.go b/src/messenger.go index ca8eba2e..0a90ca74 100644 --- a/src/messenger.go +++ b/src/messenger.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/gdamore/tcell" "os" + "strconv" ) // TermMessage sends a message to the user in the terminal. This usually occurs before @@ -21,6 +22,12 @@ func TermMessage(msg string) { reader.ReadString('\n') } +// TermError sends an error to the user in the terminal. Like TermMessage except formatted +// as an error +func TermError(filename string, lineNum int, err string) { + TermMessage(filename + ", " + strconv.Itoa(lineNum) + ": " + err) +} + // Messenger is an object that makes it easy to send messages to the user // and get input from the user type Messenger struct { diff --git a/src/micro.go b/src/micro.go index 7f460908..58131625 100644 --- a/src/micro.go +++ b/src/micro.go @@ -71,6 +71,9 @@ func main() { os.Setenv("TERM", "xterm-truecolor") } + // Load the syntax files, including the colorscheme + LoadSyntaxFiles() + // Initilize tcell screen, err = tcell.NewScreen() if err != nil { diff --git a/src/statusline.go b/src/statusline.go index 0374b4f9..e3b1d87e 100644 --- a/src/statusline.go +++ b/src/statusline.go @@ -42,6 +42,9 @@ func (sline *Statusline) Display() { file += " " + sline.view.buf.filetype statusLineStyle := tcell.StyleDefault.Reverse(true) + if style, ok := colorscheme["statusline"]; ok { + statusLineStyle = style + } // Maybe there is a unicode filename? fileRunes := []rune(file) diff --git a/src/view.go b/src/view.go index c80cc026..c59132bb 100644 --- a/src/view.go +++ b/src/view.go @@ -46,6 +46,11 @@ type View struct { // Syntax higlighting matches matches SyntaxMatches + // The matches from the last frame + lastMatches SyntaxMatches + + // This is the range of lines that should have their syntax highlighting updated + updateLines [2]int // The messenger so we can send messages to the user and get input from them m *Messenger @@ -84,6 +89,10 @@ func NewViewWidthHeight(buf *Buffer, m *Messenger, w, h int) *View { view: v, } + // Update the syntax highlighting for the entire buffer at the start + v.UpdateLines(v.topline, v.topline+v.height) + // v.matches = Match(v.buf.rules, v.buf, v) + // Set mouseReleased to true because we assume the mouse is not being pressed when // the editor is opened v.mouseReleased = true @@ -91,6 +100,12 @@ func NewViewWidthHeight(buf *Buffer, m *Messenger, w, h int) *View { return v } +// UpdateLines sets the values for v.updateLines +func (v *View) UpdateLines(start, end int) { + v.updateLines[0] = start + v.updateLines[1] = end +} + // Resize recalculates the actual width and height of the view from the width and height // percentages // This is usually called when the window is resized, or when a split has been added and @@ -307,6 +322,8 @@ func (v *View) HandleEvent(event tcell.Event) { // This bool determines whether the view is relocated at the end of the function // By default it's true because most events should cause a relocate relocate := true + // By default we don't update and syntax highlighting + v.UpdateLines(-2, 0) switch e := event.(type) { case *tcell.EventResize: // Window resized @@ -329,15 +346,19 @@ func (v *View) HandleEvent(event tcell.Event) { // Insert a newline v.eh.Insert(v.cursor.loc, "\n") v.cursor.Right() + v.UpdateLines(v.cursor.y-1, v.cursor.y) case tcell.KeySpace: // Insert a space v.eh.Insert(v.cursor.loc, " ") v.cursor.Right() + v.UpdateLines(v.cursor.y, v.cursor.y) case tcell.KeyBackspace2: // Delete a character if v.cursor.HasSelection() { v.cursor.DeleteSelection() v.cursor.ResetSelection() + // Rehighlight the entire buffer + v.UpdateLines(v.topline, v.topline+v.height) } else if v.cursor.loc > 0 { // We have to do something a bit hacky here because we want to // delete the line by first moving left and then deleting backwards @@ -349,27 +370,41 @@ func (v *View) HandleEvent(event tcell.Event) { v.cursor.Right() v.eh.Remove(v.cursor.loc-1, v.cursor.loc) v.cursor.x, v.cursor.y, v.cursor.loc = cx, cy, cloc + v.UpdateLines(v.cursor.y, v.cursor.y+1) } case tcell.KeyTab: // Insert a tab v.eh.Insert(v.cursor.loc, "\t") v.cursor.Right() + v.UpdateLines(v.cursor.y, v.cursor.y) case tcell.KeyCtrlS: v.Save() case tcell.KeyCtrlZ: v.eh.Undo() + // Rehighlight the entire buffer + v.UpdateLines(v.topline, v.topline+v.height) case tcell.KeyCtrlY: v.eh.Redo() + // Rehighlight the entire buffer + v.UpdateLines(v.topline, v.topline+v.height) case tcell.KeyCtrlC: v.Copy() + // Rehighlight the entire buffer + v.UpdateLines(v.topline, v.topline+v.height) case tcell.KeyCtrlX: v.Cut() + // Rehighlight the entire buffer + v.UpdateLines(v.topline, v.topline+v.height) case tcell.KeyCtrlV: v.Paste() + // Rehighlight the entire buffer + v.UpdateLines(v.topline, v.topline+v.height) case tcell.KeyCtrlA: v.SelectAll() case tcell.KeyCtrlO: v.OpenFile() + // Rehighlight the entire buffer + v.UpdateLines(v.topline, v.topline+v.height) case tcell.KeyPgUp: v.PageUp() case tcell.KeyPgDn: @@ -386,6 +421,7 @@ func (v *View) HandleEvent(event tcell.Event) { } v.eh.Insert(v.cursor.loc, string(e.Rune())) v.cursor.Right() + v.UpdateLines(v.cursor.y, v.cursor.y) } case *tcell.EventMouse: x, y := e.Position() @@ -442,10 +478,14 @@ func (v *View) HandleEvent(event tcell.Event) { if relocate { v.Relocate() } + + // v.matches = Match(v.buf.rules, v.buf, v) } // DisplayView renders the view to the screen func (v *View) DisplayView() { + matches := make(SyntaxMatches) + // The character number of the character in the top left of the screen charNum := v.cursor.loc + v.cursor.Distance(0, v.topline) @@ -468,8 +508,8 @@ func (v *View) DisplayView() { // Write the line number lineNumStyle := tcell.StyleDefault - if _, ok := colorscheme["line-number"]; ok { - lineNumStyle = colorscheme["line-number"] + if style, ok := colorscheme["line-number"]; ok { + lineNumStyle = style } // Write the spaces before the line number if necessary lineNum := strconv.Itoa(lineN + v.topline + 1) @@ -491,12 +531,14 @@ func (v *View) DisplayView() { for _, ch := range line { var lineStyle tcell.Style // Does the current character need to be syntax highlighted? - st, ok := v.matches[charNum] - if ok { + if st, ok := v.matches[charNum]; ok { + highlightStyle = st + } else if st, ok := v.lastMatches[charNum]; ok { highlightStyle = st } else { highlightStyle = tcell.StyleDefault } + matches[charNum] = highlightStyle if v.cursor.HasSelection() && (charNum >= v.cursor.selectionStart && charNum <= v.cursor.selectionEnd || @@ -504,8 +546,8 @@ func (v *View) DisplayView() { lineStyle = tcell.StyleDefault.Reverse(true) - if _, ok := colorscheme["selection"]; ok { - lineStyle = colorscheme["selection"] + if style, ok := colorscheme["selection"]; ok { + lineStyle = style } } else { lineStyle = highlightStyle @@ -533,8 +575,8 @@ func (v *View) DisplayView() { selectStyle := tcell.StyleDefault.Reverse(true) - if _, ok := colorscheme["selection"]; ok { - selectStyle = colorscheme["selection"] + if style, ok := colorscheme["selection"]; ok { + selectStyle = style } screen.SetContent(x+tabchars, lineN, ' ', nil, selectStyle) } @@ -542,6 +584,8 @@ func (v *View) DisplayView() { x = 0 charNum++ } + + v.lastMatches = matches } // Display renders the view, the cursor, and statusline