From 9a85e8efefda7cfd86c0a6a1698c741c622fa73c Mon Sep 17 00:00:00 2001 From: "Luke I. Wilson" Date: Sat, 27 Mar 2021 13:42:11 -0500 Subject: [PATCH] Create and initialize Highlighter in TextEdit --- ui/buffer/buffer.go | 7 ++- ui/buffer/highlighter.go | 103 +++++++++++++++++++++++++++++++++++++++ ui/buffer/language.go | 23 +++++++++ ui/buffer/rope.go | 36 ++++++++++++++ ui/container.go | 4 +- ui/textedit.go | 22 ++++++++- 6 files changed, 191 insertions(+), 4 deletions(-) create mode 100755 ui/buffer/highlighter.go create mode 100755 ui/buffer/language.go diff --git a/ui/buffer/buffer.go b/ui/buffer/buffer.go index cdb2692..b253c3c 100644 --- a/ui/buffer/buffer.go +++ b/ui/buffer/buffer.go @@ -15,7 +15,7 @@ import ( type Buffer interface { // Line returns a slice of the data at the given line, including the ending line- // delimiter. line starts from zero. Data returned may or may not be a copy: do not - // write it. + // write to it. Line(line int) []byte // Returns a slice of the buffer from startLine, startCol, to endLine, endCol, @@ -62,5 +62,10 @@ type Buffer interface { // the last rune before the line delimiter. ClampLineCol(line, col int) (int, int) + // PosToLineCol converts a byte offset (position) of the buffer's bytes, into + // a line and column. Unless you are working with the Bytes() function, this + // is unlikely to be useful to you. Position will be clamped. + PosToLineCol(pos int) (int, int) + WriteTo(w io.Writer) (int64, error) } diff --git a/ui/buffer/highlighter.go b/ui/buffer/highlighter.go new file mode 100755 index 0000000..5a90974 --- /dev/null +++ b/ui/buffer/highlighter.go @@ -0,0 +1,103 @@ +package buffer + +import ( + "sort" + + "github.com/gdamore/tcell/v2" +) + +type Colorscheme map[Syntax]tcell.Style + +// Gets the tcell.Style from the Colorscheme map for the given Syntax. +// If the Syntax cannot be found in the map, either the `Default` Syntax +// is used, or `tcell.DefaultStyle` is returned if the Default is not assigned. +func (c *Colorscheme) GetStyle(s Syntax) tcell.Style { + if c != nil { + if val, ok := (*c)[s]; ok { + return val // Try to return the requested value + } else if s != Default { + if val, ok := (*c)[Default]; ok { + return val // Use default colorscheme value, instead + } + } + } + + return tcell.StyleDefault; // No value for Default; use default style. +} + +type SyntaxData struct { + Col int + EndLine int + EndCol int + Syntax Syntax +} + +// ByCol implements sort.Interface for []SyntaxData based on the Col field. +type ByCol []SyntaxData + +func (c ByCol) Len() int { return len(c) } +func (c ByCol) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +func (c ByCol) Less(i, j int) bool { return c[i].Col < c[j].Col } + +// A Highlighter can answer how to color any part of a provided Buffer. It does so +// by applying regular expressions over a region of the buffer. +type Highlighter struct { + Buffer Buffer + Language *Language + Colorscheme *Colorscheme + + lineData [][]SyntaxData +} + +func NewHighlighter(buffer Buffer, lang *Language, colorscheme *Colorscheme) *Highlighter { + return &Highlighter{ + buffer, + lang, + colorscheme, + make([][]SyntaxData, buffer.Lines()), + } +} + +func (h *Highlighter) Update() { + if lines := h.Buffer.Lines(); len(h.lineData) < lines { + h.lineData = append(h.lineData, make([][]SyntaxData, lines)...) // Extend + } + for i := range h.lineData { // Invalidate all line data + h.lineData[i] = nil + } + + // For each compiled syntax regex: + // Use FindAllIndex to get all instances of a single match, then for each match found: + // use Find to get the bytes of the match and get the length. Calculate to what line + // and column the bytes span and its syntax. Append a SyntaxData to the output. + + bytes := (h.Buffer).Bytes() // Allocates size of the buffer + + for k, v := range h.Language.Rules { + indexes := k.FindAllIndex(bytes, -1) + if indexes != nil { + for i := range indexes { + endPos := indexes[i][1] - 1 + startLine, startCol := h.Buffer.PosToLineCol(indexes[i][0]) + endLine, endCol := h.Buffer.PosToLineCol(endPos) + + syntaxData := SyntaxData { startCol, endLine, endCol, v } + + h.lineData[startLine] = append(h.lineData[startLine], syntaxData) // Not sorted + } + } + } +} + +func (h *Highlighter) GetLine(line int) []SyntaxData { + if line < 0 || line >= len(h.lineData) { + return nil + } + data := h.lineData[line] + sort.Sort(ByCol(data)) + return data +} + +func (h *Highlighter) GetStyle(syn SyntaxData) tcell.Style { + return h.Colorscheme.GetStyle(syn.Syntax) +} diff --git a/ui/buffer/language.go b/ui/buffer/language.go new file mode 100755 index 0000000..8a58afc --- /dev/null +++ b/ui/buffer/language.go @@ -0,0 +1,23 @@ +package buffer + +import "regexp" + +type Syntax uint8 + +const ( + Default Syntax = iota + Keyword + Special + Type + Number + Builtin + Comment + DocComment +) + +type Language struct { + Name string + Filetypes []string // .go, .c, etc. + Rules map[*regexp.Regexp]Syntax + // TODO: add other language details +} diff --git a/ui/buffer/rope.go b/ui/buffer/rope.go index 9d9a2f2..1fbdafd 100644 --- a/ui/buffer/rope.go +++ b/ui/buffer/rope.go @@ -283,6 +283,42 @@ func (b *RopeBuffer) ClampLineCol(line, col int) (int, int) { return line, col } +// PosToLineCol converts a byte offset (position) of the buffer's bytes, into +// a line and column. Unless you are working with the Bytes() function, this +// is unlikely to be useful to you. Position will be clamped. +func (b *RopeBuffer) PosToLineCol(pos int) (int, int) { + var line, col int + var wasAtNewline bool + + _rope := (*rope.Node)(b) + _rope.EachLeaf(func(n *rope.Node) bool { + data := n.Value() + var i int + for i < len(data) { + if pos <= 0 { + return true + } + + if data[i] == '\n' { // End of line + wasAtNewline = true + col++ + } else if wasAtNewline { // Start of line + wasAtNewline = false + line, col = line+1, 0 + } else { + col++ // Normal byte + } + + _, size := utf8.DecodeRune(data[i:]) + i += size + pos -= size + } + return false + }) + + return line, col +} + func (b *RopeBuffer) WriteTo(w io.Writer) (int64, error) { return (*rope.Node)(b).WriteTo(w) } diff --git a/ui/container.go b/ui/container.go index bbd755d..9438849 100644 --- a/ui/container.go +++ b/ui/container.go @@ -192,7 +192,7 @@ func (c *TabContainer) Draw(s tcell.Screen) { combinedTabLength += len(c.children) - 1 // add for spacing between tabs // Draw tabs - col := c.x + c.width/2 - combinedTabLength/2 // Starting column + col := c.x + c.width/2 - combinedTabLength/2 - 1 // Starting column for i, tab := range c.children { var sty tcell.Style if c.selected == i { @@ -213,7 +213,7 @@ func (c *TabContainer) Draw(s tcell.Screen) { } str := fmt.Sprintf(" %s ", name) - //DrawStr(s, c.x+c.width/2-len(str)/2, c.y, str, sty) + DrawStr(s, c.x+col, c.y, str, sty) col += len(str) + 1 // Add one for spacing between tabs } diff --git a/ui/textedit.go b/ui/textedit.go index 867b9d3..4152dc9 100644 --- a/ui/textedit.go +++ b/ui/textedit.go @@ -3,6 +3,7 @@ package ui import ( "fmt" "math" + "regexp" "strconv" "strings" "unicode/utf8" @@ -27,6 +28,7 @@ type Region struct { // content being edited. type TextEdit struct { Buffer buffer.Buffer + Highlighter *buffer.Highlighter LineNumbers bool // Whether to render line numbers (and therefore the column) Dirty bool // Whether the buffer has been edited UseHardTabs bool // When true, tabs are '\t' @@ -52,7 +54,8 @@ type TextEdit struct { // it can be assumed that the TextEdit has no file association, or it is unsaved. func NewTextEdit(screen *tcell.Screen, filePath string, contents []byte, theme *Theme) *TextEdit { te := &TextEdit{ - Buffer: nil, + Buffer: nil, // Set in SetContents + Highlighter: nil, // Set in SetContents LineNumbers: true, UseHardTabs: true, TabSize: 4, @@ -84,6 +87,23 @@ loop: } t.Buffer = buffer.NewRopeBuffer(contents) + + // TODO: replace with automatic determination of language via filetype + lang := &buffer.Language { + Name: "Go", + Filetypes: []string{".go"}, + Rules: map[*regexp.Regexp]buffer.Syntax { + regexp.MustCompile("(if|for|func|switch)"): buffer.Keyword, + }, + } + + colorscheme := &buffer.Colorscheme { + buffer.Default: tcell.Style{}.Foreground(tcell.ColorLightGray).Background(tcell.ColorBlack), + buffer.Keyword: tcell.Style{}.Foreground(tcell.ColorBlue).Background(tcell.ColorBlack), + } + + t.Highlighter = buffer.NewHighlighter(t.Buffer, lang, colorscheme) + t.Highlighter.Update() } // GetLineDelimiter returns "\r\n" for a CRLF buffer, or "\n" for an LF buffer.