Create and initialize Highlighter in TextEdit

This commit is contained in:
Luke I. Wilson 2021-03-27 13:42:11 -05:00
parent 1161888660
commit 9a85e8efef
6 changed files with 191 additions and 4 deletions

View File

@ -15,7 +15,7 @@ import (
type Buffer interface { type Buffer interface {
// Line returns a slice of the data at the given line, including the ending line- // 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 // 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 Line(line int) []byte
// Returns a slice of the buffer from startLine, startCol, to endLine, endCol, // 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. // the last rune before the line delimiter.
ClampLineCol(line, col int) (int, int) 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) WriteTo(w io.Writer) (int64, error)
} }

103
ui/buffer/highlighter.go Executable file
View File

@ -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)
}

23
ui/buffer/language.go Executable file
View File

@ -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
}

View File

@ -283,6 +283,42 @@ func (b *RopeBuffer) ClampLineCol(line, col int) (int, int) {
return line, col 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) { func (b *RopeBuffer) WriteTo(w io.Writer) (int64, error) {
return (*rope.Node)(b).WriteTo(w) return (*rope.Node)(b).WriteTo(w)
} }

View File

@ -192,7 +192,7 @@ func (c *TabContainer) Draw(s tcell.Screen) {
combinedTabLength += len(c.children) - 1 // add for spacing between tabs combinedTabLength += len(c.children) - 1 // add for spacing between tabs
// Draw 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 { for i, tab := range c.children {
var sty tcell.Style var sty tcell.Style
if c.selected == i { if c.selected == i {
@ -213,7 +213,7 @@ func (c *TabContainer) Draw(s tcell.Screen) {
} }
str := fmt.Sprintf(" %s ", name) 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) DrawStr(s, c.x+col, c.y, str, sty)
col += len(str) + 1 // Add one for spacing between tabs col += len(str) + 1 // Add one for spacing between tabs
} }

View File

@ -3,6 +3,7 @@ package ui
import ( import (
"fmt" "fmt"
"math" "math"
"regexp"
"strconv" "strconv"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
@ -27,6 +28,7 @@ type Region struct {
// content being edited. // content being edited.
type TextEdit struct { type TextEdit struct {
Buffer buffer.Buffer Buffer buffer.Buffer
Highlighter *buffer.Highlighter
LineNumbers bool // Whether to render line numbers (and therefore the column) LineNumbers bool // Whether to render line numbers (and therefore the column)
Dirty bool // Whether the buffer has been edited Dirty bool // Whether the buffer has been edited
UseHardTabs bool // When true, tabs are '\t' 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. // 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 { func NewTextEdit(screen *tcell.Screen, filePath string, contents []byte, theme *Theme) *TextEdit {
te := &TextEdit{ te := &TextEdit{
Buffer: nil, Buffer: nil, // Set in SetContents
Highlighter: nil, // Set in SetContents
LineNumbers: true, LineNumbers: true,
UseHardTabs: true, UseHardTabs: true,
TabSize: 4, TabSize: 4,
@ -84,6 +87,23 @@ loop:
} }
t.Buffer = buffer.NewRopeBuffer(contents) 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. // GetLineDelimiter returns "\r\n" for a CRLF buffer, or "\n" for an LF buffer.