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 }