Create and initialize Highlighter in TextEdit
This commit is contained in:
parent
1161888660
commit
9a85e8efef
@ -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
103
ui/buffer/highlighter.go
Executable 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
23
ui/buffer/language.go
Executable 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
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user