gilo/editor/document/row.go
Timothy Warren 69709a1cb4
All checks were successful
timw4mail/gilo/pipeline/head This commit looks good
Highlight keywords, and fix emoji rendering issue
2023-10-05 11:03:08 -04:00

284 lines
5.6 KiB
Go

package document
import (
"strings"
"timshome.page/gilo/editor/highlight"
"timshome.page/gilo/internal/gilo"
"timshome.page/gilo/key"
"unicode"
)
type Row struct {
parent *Document
chars []rune
render []rune
Hl []int
}
func newRow(parent *Document, s string) *Row {
var chars []rune
var render []rune
for _, ch := range s {
chars = append(chars, ch)
render = append(render, ch)
}
return &Row{parent, chars, render, []int{}}
}
func (r *Row) Size() int {
return len(r.chars)
}
// RenderSize is a convenient equivalent of row->rsize in kilo
func (r *Row) RenderSize() int {
return len(r.render)
}
func (r *Row) Render(at *gilo.Point) string {
return string(r.render[at.X:])
}
// RenderRune returns the array of runes in the current row. Unlike a string
// this will index how you expect with multi-byte characters
func (r *Row) RenderRune(at *gilo.Point) []rune {
return r.render[at.X:]
}
func (r *Row) Search(query string) int {
return strings.Index(string(r.render), query)
}
func (r *Row) insertRune(ch rune, at int) {
// If insertion index is invalid, just
// append the rune to the end of the array
if at < 0 || at >= r.Size() {
r.chars = append(r.chars, ch)
r.update()
return
}
var newSlice []rune
// Split the character array at the insertion point
start := r.chars[0:at]
end := r.chars[at:r.Size()]
// Splice it back together
newSlice = append(newSlice, start...)
newSlice = append(newSlice, ch)
newSlice = append(newSlice, end...)
r.chars = newSlice
r.update()
}
func (r *Row) appendString(str string) {
for _, ch := range str {
r.chars = append(r.chars, ch)
}
r.update()
}
func (r *Row) deleteRune(at int) {
if at < 0 || at >= r.Size() {
return
}
var newSlice []rune
// Split the character array at the insertion point
start := r.chars[0:at]
end := r.chars[at+1 : r.Size()] // Skip the index in question
// Splice it back together
newSlice = append(newSlice, start...)
newSlice = append(newSlice, end...)
r.chars = newSlice
r.update()
}
func (r *Row) update() {
r.render = r.render[:0]
replacement := strings.Repeat(" ", gilo.TabSize)
str := strings.ReplaceAll(string(r.chars), "\t", replacement)
for _, ch := range str {
r.render = append(r.render, ch)
}
r.updateSyntax()
}
// updateSyntax is the equivalent of editorUpdateSyntax in kilo
func (r *Row) updateSyntax() {
i := 0
s := r.parent.Syntax
r.Hl = make([]int, r.RenderSize())
for x := range r.Hl {
r.Hl[x] = highlight.Normal
}
// Don't bother updating the syntax if there isn't any
if s == nil {
return
}
renderStr := string(r.render)
keywords1 := s.Keywords1
keywords2 := s.Keywords2
var scsIndex int = -1
scs := s.LineCommentStart
if len(scs) > 0 {
scsIndex = strings.Index(renderStr, scs)
}
prevSep := true
inString := '0'
for i < r.RenderSize() {
ch := r.render[i]
prevHl := highlight.Normal
if i > 0 {
prevHl = r.Hl[i-1]
}
ip1 := i + 1
// Single line comments
if inString == '0' && scsIndex == i {
for j := scsIndex; j < r.RenderSize(); j++ {
r.Hl[j] = highlight.Comment
}
break
}
// String literals
if s.Flags&highlight.DoStrings == highlight.DoStrings {
// At the start of a string literal
if inString == '0' && (ch == '"' || ch == '\'') {
inString = ch
r.Hl[i] = highlight.String
i++
continue
}
// In an existing string
if inString != '0' {
r.Hl[i] = highlight.String
// Handle when a quote is escaped inside a string
if ch == '\\' && ip1 < r.RenderSize() {
r.Hl[ip1] = highlight.String
i += 2
continue
}
// This quote mark matches the beginning of the string
// so now the string is completed
if ch == inString {
inString = '0'
}
i++
prevSep = true
continue
}
}
// Numeric literals
if s.Flags&highlight.DoNumbers == highlight.DoNumbers {
if (unicode.IsDigit(ch) && (prevSep || prevHl == highlight.Number)) ||
(ch == '.' && prevHl == highlight.Number) {
r.Hl[i] = highlight.Number
i += 1
prevSep = false
continue
}
}
// Keywords
if prevSep {
renderLen := r.RenderSize()
for _, word := range keywords1 {
wordLen := len(word)
nextInd := i + wordLen
if nextInd >= renderLen || renderStr[i:nextInd] != word {
continue
}
if renderStr[i:renderLen] == word || key.IsSeparator(r.render[nextInd]) {
for k := i; k < nextInd; k++ {
r.Hl[k] = highlight.Keyword1
}
i += wordLen
break
}
}
for _, word := range keywords2 {
wordLen := len(word)
nextInd := i + wordLen
if nextInd >= renderLen || renderStr[i:nextInd] != word {
continue
}
if renderStr[i:renderLen] == word || key.IsSeparator(r.render[nextInd]) {
for k := i; k < nextInd; k++ {
r.Hl[k] = highlight.Keyword2
}
i += wordLen
break
}
}
}
prevSep = key.IsSeparator(ch)
i++
}
}
func (r *Row) toString() string {
return string(r.chars)
}
// CursorXToRenderX is the equivalent of editorRowCxToRx in kilo
func (r *Row) CursorXToRenderX(cursorX int) (renderX int) {
renderX = 0
for i := 0; i < cursorX; i++ {
if r.chars[i] == '\t' {
renderX += (gilo.TabSize - 1) - (renderX % gilo.TabSize)
}
renderX += 1
}
return renderX
}
// RenderXtoCursorX is the equivalent of editorRowRxToCx in kilo
func (r *Row) RenderXtoCursorX(renderX int) (cursorX int) {
currentRenderX := 0
cursorX = 0
for cursorX = 0; cursorX < r.Size(); cursorX++ {
if r.chars[cursorX] == '\t' {
currentRenderX += (gilo.TabSize - 1) - (currentRenderX % gilo.TabSize)
} else {
currentRenderX += 1
}
if currentRenderX > renderX {
return cursorX
}
}
return cursorX
}