qedit/ui/buffer/highlighter.go
Luke I. Wilson f829b37d0c TextEdit & Highlighting: performance improvements & changes to architecture
I ran pprof to find what was causing stuttering, and found it to be the
getTabCountInLineAtCol function in TextEdit, because it was iterating
many bytes of the buffer, for each rune rendered. Replaced it with a more
optimal system. Also changed the architecture of the highlighting system
to use a single RegexpRange structure for all regular expressions. This
allows for optimizations and multiline matches in the future.
2021-04-01 12:05:17 -05:00

171 lines
4.9 KiB
Go
Executable File

package buffer
import (
"regexp"
"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 RegexpRegion struct {
Start *regexp.Regexp
End *regexp.Regexp // Should be "$" by default
Skip *regexp.Regexp // Optional
Error *regexp.Regexp // Optional
Specials []*regexp.Regexp // Optional (nil or zero len)
}
type Match struct {
Col int
EndLine int // Inclusive
EndCol int // Inclusive
Syntax Syntax
}
// ByCol implements sort.Interface for []Match based on the Col field.
type ByCol []Match
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
lineMatches [][]Match
}
func NewHighlighter(buffer Buffer, lang *Language, colorscheme *Colorscheme) *Highlighter {
return &Highlighter{
buffer,
lang,
colorscheme,
make([][]Match, buffer.Lines()),
}
}
// UpdateLines forces the highlighting matches for lines between startLine to
// endLine, inclusively, to be updated. It is more efficient to mark lines as
// invalidated when changes occur and call UpdateInvalidatedLines(...).
func (h *Highlighter) UpdateLines(startLine, endLine int) {
if lines := h.Buffer.Lines(); len(h.lineMatches) < lines {
h.lineMatches = append(h.lineMatches, make([][]Match, lines)...) // Extend
}
for i := startLine; i <= endLine && i < len(h.lineMatches); i++ {
if h.lineMatches[i] != nil {
h.lineMatches[i] = h.lineMatches[i][:0] // Shrink slice to zero (hopefully save allocs)
}
}
// If the rule k does not have an End, then it can be optimized that we search from the start
// of view until the end of view. For any k that has an End, we search for starts from start
// of buffer, until end of view.
endLine, endCol := h.Buffer.ClampLineCol(endLine, (h.Buffer).RunesInLineWithDelim(endLine)-1)
bytes := (h.Buffer).Slice(0, 0, endLine, endCol) // Allocates size of the buffer
for k, v := range h.Language.Rules {
indexes := k.Start.FindAllIndex(bytes, -1) // Attempt to find the start match
if indexes != nil {
for i := range indexes {
// if k.End != nil && k.End.String() != "$" { // If this match has a defined end...
// }
endPos := indexes[i][1] - 1
startLine, startCol := h.Buffer.PosToLineCol(indexes[i][0])
endLine, endCol := h.Buffer.PosToLineCol(endPos)
match := Match { startCol, endLine, endCol, v }
h.lineMatches[startLine] = append(h.lineMatches[startLine], match) // Unsorted
}
}
}
h.validateLines(startLine, endLine) // Marks any "unvalidated" or nil lines as valued
}
// UpdateInvalidatedLines only updates the highlighting for lines that are invalidated
// between lines startLine and endLine, inclusively.
func (h *Highlighter) UpdateInvalidatedLines(startLine, endLine int) {
// Move startLine to first line with invalidated changes
for startLine <= endLine && startLine < len(h.lineMatches)-1 {
if h.lineMatches[startLine] == nil {
break
}
startLine++
}
// Move endLine back to first line at or before endLine with invalidated changes
for endLine >= startLine && endLine > 0 {
if h.lineMatches[endLine] == nil {
break
}
endLine--
}
if startLine > endLine {
return // Do nothing; no invalidated lines
}
h.UpdateLines(startLine, endLine)
}
func (h *Highlighter) HasInvalidatedLines(startLine, endLine int) bool {
for i := startLine; i <= endLine && i < len(h.lineMatches); i++ {
if h.lineMatches[i] == nil {
return true
}
}
return false
}
func (h *Highlighter) validateLines(startLine, endLine int) {
for i := startLine; i <= endLine && i < len(h.lineMatches); i++ {
if h.lineMatches[i] == nil {
h.lineMatches[i] = make([]Match, 0)
}
}
}
func (h *Highlighter) InvalidateLines(startLine, endLine int) {
for i := startLine; i <= endLine && i < len(h.lineMatches); i++ {
h.lineMatches[i] = nil
}
}
func (h *Highlighter) GetLineMatches(line int) []Match {
if line < 0 || line >= len(h.lineMatches) {
return nil
}
data := h.lineMatches[line]
sort.Sort(ByCol(data))
return data
}
func (h *Highlighter) GetStyle(match Match) tcell.Style {
return h.Colorscheme.GetStyle(match.Syntax)
}