package document import ( "strings" "timshome.page/gilo/char" "timshome.page/gilo/editor/highlight" "timshome.page/gilo/internal/gilo" ) type Row struct { index int parent *Document chars []rune render []rune Hl []int startsComment bool } func newRow(at int, 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{ at, parent, chars, render, []int{}, false, } } 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 // this is basically the core syntax highlighting algorithm 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) renderLen := r.RenderSize() keywords1 := s.Keywords1 keywords2 := s.Keywords2 // Scan for comments var scsIndex int = -1 var mcsIndex int = -1 var mceIndex int = -1 scs := s.LineCommentStart mcs := s.MultilineCommentStart mce := s.MultilineCommentEnd hasMcs := len(mcs) > 0 hasMce := len(mce) > 0 if len(scs) > 0 { scsIndex = strings.Index(renderStr, scs) } if hasMcs { mcsIndex = strings.Index(renderStr, mcs) } if hasMce { mceIndex = strings.Index(renderStr, mce) } prevSep := true inString := '0' inComment := r.index > 0 && r.parent.rows[r.index-1].startsComment 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' && !inComment && scsIndex == i { for j := scsIndex; j < renderLen; j++ { r.Hl[j] = highlight.Comment } break } // Multiline comments if hasMcs && hasMce && inString == '0' { if inComment { r.Hl[i] = highlight.MLComment // We found the end of the multiline comment if mceIndex == i { for j := i; j < (i + len(mce)); j++ { r.Hl[j] = highlight.MLComment } i += len(mce) inComment = false prevSep = true continue } i++ continue } // Found the start of the multiline comment if mcsIndex == i { for j := i; j < (i + len(mce)); j++ { r.Hl[j] = highlight.MLComment } i += len(mcs) inComment = true continue } } // String literals if s.Flags&highlight.DoStrings == highlight.DoStrings { // At the start of a string literal if inString == '0' && (ch == char.DoubleQuote || ch == char.SingleQuote) { 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 == char.Backslash && ip1 < renderLen { 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 (char.IsDigit(ch) && prevSep) || (char.IsNumeric(ch) && prevHl == highlight.Number) { r.Hl[i] = highlight.Number i += 1 prevSep = false continue } } // Keywords if prevSep { matched := false keywordLoop: for n, list := range [][]string{keywords1, keywords2} { hlType := highlight.Keyword1 if n == 1 { hlType = highlight.Keyword2 } for _, word := range list { wordLen := len(word) nextInd := i + wordLen endMatch := renderStr[i:renderLen] == word goodMatch := nextInd < renderLen && renderStr[i:nextInd] == word if endMatch || (goodMatch && char.IsSeparator(r.render[nextInd])) { for k := i; k < nextInd; k++ { r.Hl[k] = hlType } i += wordLen matched = true break keywordLoop } } } if matched { prevSep = false continue } } prevSep = char.IsSeparator(ch) i++ } changed := r.startsComment != inComment r.startsComment = inComment if changed && (r.index+1) < r.parent.RowCount() { r.parent.rows[r.index+1].updateSyntax() } } 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 }