160 lines
4.4 KiB
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
|
|
}
|
|
}
|