qedit/pkg/buffer/cursor.go

160 lines
4.4 KiB
Go

package buffer
import (
"math"
"unicode"
)
// So why is the code for moving the cursor in the buffer package, and not in the
// TextEdit component? Well, it used to be, but it sucked that way. The cursor
// needs to have a reference to the buffer to know where lines end and how it can
// move. The buffer is the city, and the Cursor is the car.
type position struct {
line int
col int
}
// A Selection represents a region of the buffer to be selected for text editing
// purposes. It is asserted that the start position is less than the end position.
// The start and end are inclusive. If the EndCol of a Region is one more than the
// last column of a line, then it points to the line delimiter at the end of that
// line. It is understood that as a Region spans multiple lines, those connecting
// line-delimiters are included in the selection, as well.
type Region struct {
Start Cursor
End Cursor
}
func NewRegion(in *Buffer) Region {
return Region{
NewCursor(in),
NewCursor(in),
}
}
// A Cursor's functions emulate common cursor actions. To have a Cursor be
// automatically updated when the buffer has text prepended or appended -- one
// should register the Cursor with the Buffer's function `RegisterCursor()`
// which makes the Cursor "anchored" to the Buffer.
type Cursor struct {
buffer *Buffer
prevCol int
position
}
func NewCursor(in *Buffer) Cursor {
return Cursor{
buffer: in,
}
}
func (c Cursor) Left() Cursor {
if c.col == 0 && c.line != 0 { // If we are at the beginning of the current line...
// Go to the end of the above line
c.line--
c.col = (*c.buffer).RunesInLine(c.line)
} else {
c.col = Max(c.col-1, 0)
}
return c
}
func (c Cursor) Right() Cursor {
// If we are at the end of the current line,
// and not at the last line...
if c.col >= (*c.buffer).RunesInLine(c.line) && c.line < (*c.buffer).Lines()-1 {
c.line, c.col = (*c.buffer).ClampLineCol(c.line+1, 0) // Go to beginning of line below
} else {
c.line, c.col = (*c.buffer).ClampLineCol(c.line, c.col+1)
}
return c
}
func (c Cursor) Up() Cursor {
if c.line == 0 { // If the cursor is at the first line...
c.line, c.col = 0, 0 // Go to beginning
} else {
c.line, c.col = (*c.buffer).ClampLineCol(c.line-1, c.col)
}
return c
}
func (c Cursor) Down() Cursor {
if c.line == (*c.buffer).Lines()-1 { // If the cursor is at the last line...
c.line, c.col = (*c.buffer).ClampLineCol(c.line, math.MaxInt32) // Go to end of current line
} else {
c.line, c.col = (*c.buffer).ClampLineCol(c.line+1, c.col)
}
return c
}
// NextWordBoundaryEnd proceeds to the position after the last character of the
// next word boundary to the right of the Cursor. A word boundary is the
// beginning or end of any sequence of similar or same-classed characters.
// Whitespace is skipped.
func (c Cursor) NextWordBoundaryEnd() Cursor {
// Get position of cursor in buffer as pos
// get classification of character at pos or assume none if whitespace
// for each pos until end of buffer: pos + 1 (at end)
// if pos char is not of previous pos char class:
// set cursor position as pos
//
// only skip contiguous characters for word characters
// jump to position *after* any symbols
pos := (*c.buffer).LineColToPos(c.line, c.col)
startClass := getRuneCharclass((*c.buffer).RuneAtPos(pos))
pos++
(*c.buffer).EachRuneAtPos(pos, func(rpos int, r rune) bool {
class := getRuneCharclass(r)
if class != startClass && class != charwhitespace {
return true
}
return false
})
c.line, c.col = (*c.buffer).PosToLineCol(pos)
return c
}
func (c Cursor) PrevWordBoundaryStart() Cursor {
return c
}
func (c Cursor) GetLineCol() (line, col int) {
return c.line, c.col
}
// SetLineCol sets the line and col of the Cursor to those provided. `line` is
// clamped within the range (0, lines in buffer). `col` is then clamped within
// the range (0, line length in runes).
func (c Cursor) SetLineCol(line, col int) Cursor {
c.line, c.col = (*c.buffer).ClampLineCol(line, col)
return c
}
func (c Cursor) Eq(other Cursor) bool {
return c.buffer == other.buffer && c.line == other.line && c.col == other.col
}
type charclass uint8
const (
charwhitespace charclass = iota
charword
charsymbol
)
func getRuneCharclass(r rune) charclass {
if unicode.IsSpace(r) {
return charwhitespace
} else if r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r) {
return charword
} else {
return charsymbol
}
}