Refactored cursor architecture into buffer module
This commit is contained in:
12
ui/buffer/buffer.go
Normal file → Executable file
12
ui/buffer/buffer.go
Normal file → Executable file
@@ -75,4 +75,16 @@ type Buffer interface {
|
||||
PosToLineCol(pos int) (int, int)
|
||||
|
||||
WriteTo(w io.Writer) (int64, error)
|
||||
|
||||
// RegisterCursor adds the Cursor to a slice which the Buffer uses to update
|
||||
// each Cursor based on changes that occur in the Buffer. Various functions are
|
||||
// called on the Cursor depending upon where the edits occurred and how it should
|
||||
// modify the Cursor's position. Unregister a Cursor before deleting it from
|
||||
// memory, or forgetting it, with UnregisterPosition.
|
||||
RegisterCursor(cursor *Cursor)
|
||||
|
||||
// UnregisterCursor will remove the cursor from the list of watched Cursors.
|
||||
// It is mandatory that a Cursor be unregistered before being freed from memory,
|
||||
// or otherwise being forgotten.
|
||||
UnregisterCursor(cursor *Cursor)
|
||||
}
|
||||
|
107
ui/buffer/cursor.go
Executable file
107
ui/buffer/cursor.go
Executable file
@@ -0,0 +1,107 @@
|
||||
package buffer
|
||||
|
||||
import "math"
|
||||
|
||||
// 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, as well.
|
||||
type Region struct {
|
||||
buffer *Buffer
|
||||
start position
|
||||
end position
|
||||
}
|
||||
|
||||
func NewRegion(in *Buffer) Region {
|
||||
return Region{
|
||||
buffer: in,
|
||||
}
|
||||
}
|
||||
|
||||
func (r Region) Start() (line, col int) {
|
||||
return r.start.line, r.start.col
|
||||
}
|
||||
|
||||
func (r Region) End() (line, col int) {
|
||||
return r.end.line, r.end.col
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
105
ui/buffer/rope.go
Normal file → Executable file
105
ui/buffer/rope.go
Normal file → Executable file
@@ -4,13 +4,19 @@ import (
|
||||
"io"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/rope"
|
||||
ropes "github.com/zyedidia/rope"
|
||||
)
|
||||
|
||||
type RopeBuffer rope.Node
|
||||
type RopeBuffer struct {
|
||||
rope *ropes.Node
|
||||
anchors []*Cursor
|
||||
}
|
||||
|
||||
func NewRopeBuffer(contents []byte) *RopeBuffer {
|
||||
return (*RopeBuffer)(rope.New(contents))
|
||||
return &RopeBuffer{
|
||||
ropes.New(contents),
|
||||
nil,
|
||||
}
|
||||
}
|
||||
|
||||
// LineColToPos returns the index of the byte at line, col. If line is less than
|
||||
@@ -19,20 +25,16 @@ func NewRopeBuffer(contents []byte) *RopeBuffer {
|
||||
// length of the line, the position of the last byte of the line is returned,
|
||||
// instead.
|
||||
func (b *RopeBuffer) LineColToPos(line, col int) int {
|
||||
var pos int
|
||||
|
||||
_rope := (*rope.Node)(b)
|
||||
|
||||
pos = b.getLineStartPos(line)
|
||||
pos := b.getLineStartPos(line)
|
||||
|
||||
// Have to do this algorithm for safety. If this function was declared to panic
|
||||
// or index out of bounds memory, if col > the given line length, it would be
|
||||
// more efficient and simpler. But unfortunately, I believe it is necessary.
|
||||
if col > 0 {
|
||||
_, r := _rope.SplitAt(pos)
|
||||
l, _ := r.SplitAt(_rope.Len() - pos)
|
||||
_, r := b.rope.SplitAt(pos)
|
||||
l, _ := r.SplitAt(b.rope.Len() - pos)
|
||||
|
||||
l.EachLeaf(func(n *rope.Node) bool {
|
||||
l.EachLeaf(func(n *ropes.Node) bool {
|
||||
data := n.Value() // Reference; not a copy.
|
||||
var i int
|
||||
for i < len(data) {
|
||||
@@ -60,12 +62,11 @@ func (b *RopeBuffer) Line(line int) []byte {
|
||||
pos := b.getLineStartPos(line)
|
||||
bytes := 0
|
||||
|
||||
_rope := (*rope.Node)(b)
|
||||
_, r := _rope.SplitAt(pos)
|
||||
l, _ := r.SplitAt(_rope.Len() - pos)
|
||||
_, r := b.rope.SplitAt(pos)
|
||||
l, _ := r.SplitAt(b.rope.Len() - pos)
|
||||
|
||||
var isCRLF bool // true if the last byte was '\r'
|
||||
l.EachLeaf(func(n *rope.Node) bool {
|
||||
l.EachLeaf(func(n *ropes.Node) bool {
|
||||
data := n.Value() // Reference; not a copy.
|
||||
var i int
|
||||
for i < len(data) {
|
||||
@@ -90,7 +91,7 @@ func (b *RopeBuffer) Line(line int) []byte {
|
||||
return false // Have not read the whole line, yet
|
||||
})
|
||||
|
||||
return _rope.Slice(pos, pos+bytes) // NOTE: may be faster to do it ourselves
|
||||
return b.rope.Slice(pos, pos+bytes) // NOTE: may be faster to do it ourselves
|
||||
}
|
||||
|
||||
// Returns a slice of the buffer from startLine, startCol, to endLine, endCol,
|
||||
@@ -98,22 +99,22 @@ func (b *RopeBuffer) Line(line int) []byte {
|
||||
// so do not write to it.
|
||||
func (b *RopeBuffer) Slice(startLine, startCol, endLine, endCol int) []byte {
|
||||
endPos := b.LineColToPos(endLine, endCol)
|
||||
if length := (*rope.Node)(b).Len(); endPos >= length {
|
||||
if length := b.rope.Len(); endPos >= length {
|
||||
endPos = length - 1
|
||||
}
|
||||
return (*rope.Node)(b).Slice(b.LineColToPos(startLine, startCol), endPos+1)
|
||||
return b.rope.Slice(b.LineColToPos(startLine, startCol), endPos+1)
|
||||
}
|
||||
|
||||
// Bytes returns all of the bytes in the buffer. This function is very likely
|
||||
// to copy all of the data in the buffer. Use sparingly. Try using other methods,
|
||||
// where possible.
|
||||
func (b *RopeBuffer) Bytes() []byte {
|
||||
return (*rope.Node)(b).Value()
|
||||
return b.rope.Value()
|
||||
}
|
||||
|
||||
// Insert copies a byte slice (inserting it) into the position at line, col.
|
||||
func (b *RopeBuffer) Insert(line, col int, value []byte) {
|
||||
(*rope.Node)(b).Insert(b.LineColToPos(line, col), value)
|
||||
b.rope.Insert(b.LineColToPos(line, col), value)
|
||||
}
|
||||
|
||||
// Remove deletes any characters between startLine, startCol, and endLine,
|
||||
@@ -122,14 +123,14 @@ func (b *RopeBuffer) Remove(startLine, startCol, endLine, endCol int) {
|
||||
start := b.LineColToPos(startLine, startCol)
|
||||
end := b.LineColToPos(endLine, endCol) + 1
|
||||
|
||||
if len := (*rope.Node)(b).Len(); end >= len {
|
||||
if len := b.rope.Len(); end >= len {
|
||||
end = len
|
||||
if start > end {
|
||||
start = end
|
||||
}
|
||||
}
|
||||
|
||||
(*rope.Node)(b).Remove(start, end)
|
||||
b.rope.Remove(start, end)
|
||||
}
|
||||
|
||||
// Returns the number of occurrences of 'sequence' in the buffer, within the range
|
||||
@@ -137,19 +138,19 @@ func (b *RopeBuffer) Remove(startLine, startCol, endLine, endCol int) {
|
||||
func (b *RopeBuffer) Count(startLine, startCol, endLine, endCol int, sequence []byte) int {
|
||||
startPos := b.LineColToPos(startLine, startCol)
|
||||
endPos := b.LineColToPos(endLine, endCol)
|
||||
return (*rope.Node)(b).Count(startPos, endPos, sequence)
|
||||
return b.rope.Count(startPos, endPos, sequence)
|
||||
}
|
||||
|
||||
// Len returns the number of bytes in the buffer.
|
||||
func (b *RopeBuffer) Len() int {
|
||||
return (*rope.Node)(b).Len()
|
||||
return b.rope.Len()
|
||||
}
|
||||
|
||||
// Lines returns the number of lines in the buffer. If the buffer is empty,
|
||||
// 1 is returned, because there is always at least one line. This function
|
||||
// basically counts the number of newline ('\n') characters in a buffer.
|
||||
func (b *RopeBuffer) Lines() int {
|
||||
rope := (*rope.Node)(b)
|
||||
rope := b.rope
|
||||
return rope.Count(0, rope.Len(), []byte{'\n'}) + 1
|
||||
}
|
||||
|
||||
@@ -158,17 +159,13 @@ func (b *RopeBuffer) Lines() int {
|
||||
// which means the byte is on the last, and empty, line of the buffer. If line is greater
|
||||
// than or equal to the number of lines in the buffer, a panic is issued.
|
||||
func (b *RopeBuffer) getLineStartPos(line int) int {
|
||||
_rope := (*rope.Node)(b)
|
||||
var pos int
|
||||
|
||||
if line > 0 {
|
||||
_rope.IndexAllFunc(0, _rope.Len(), []byte{'\n'}, func(idx int) bool {
|
||||
b.rope.IndexAllFunc(0, b.rope.Len(), []byte{'\n'}, func(idx int) bool {
|
||||
line--
|
||||
pos = idx + 1 // idx+1 = start of line after delimiter
|
||||
if line <= 0 { // If pos is now the start of the line we're searching for...
|
||||
return true // Stop indexing
|
||||
}
|
||||
return false
|
||||
pos = idx + 1 // idx+1 = start of line after delimiter
|
||||
return line <= 0 // If pos is now the start of the line we're searching for
|
||||
})
|
||||
}
|
||||
|
||||
@@ -185,8 +182,7 @@ func (b *RopeBuffer) getLineStartPos(line int) int {
|
||||
func (b *RopeBuffer) RunesInLineWithDelim(line int) int {
|
||||
linePos := b.getLineStartPos(line)
|
||||
|
||||
_rope := (*rope.Node)(b)
|
||||
ropeLen := _rope.Len()
|
||||
ropeLen := b.rope.Len()
|
||||
|
||||
if linePos >= ropeLen {
|
||||
return 0
|
||||
@@ -194,11 +190,11 @@ func (b *RopeBuffer) RunesInLineWithDelim(line int) int {
|
||||
|
||||
var count int
|
||||
|
||||
_, r := _rope.SplitAt(linePos)
|
||||
_, r := b.rope.SplitAt(linePos)
|
||||
l, _ := r.SplitAt(ropeLen - linePos)
|
||||
|
||||
var isCRLF bool
|
||||
l.EachLeaf(func(n *rope.Node) bool {
|
||||
l.EachLeaf(func(n *ropes.Node) bool {
|
||||
data := n.Value() // Reference; not a copy.
|
||||
var i int
|
||||
for i < len(data) {
|
||||
@@ -229,8 +225,7 @@ func (b *RopeBuffer) RunesInLineWithDelim(line int) int {
|
||||
func (b *RopeBuffer) RunesInLine(line int) int {
|
||||
linePos := b.getLineStartPos(line)
|
||||
|
||||
_rope := (*rope.Node)(b)
|
||||
ropeLen := _rope.Len()
|
||||
ropeLen := b.rope.Len()
|
||||
|
||||
if linePos >= ropeLen {
|
||||
return 0
|
||||
@@ -238,11 +233,11 @@ func (b *RopeBuffer) RunesInLine(line int) int {
|
||||
|
||||
var count int
|
||||
|
||||
_, r := _rope.SplitAt(linePos)
|
||||
_, r := b.rope.SplitAt(linePos)
|
||||
l, _ := r.SplitAt(ropeLen - linePos)
|
||||
|
||||
var isCRLF bool
|
||||
l.EachLeaf(func(n *rope.Node) bool {
|
||||
l.EachLeaf(func(n *ropes.Node) bool {
|
||||
data := n.Value() // Reference; not a copy.
|
||||
var i int
|
||||
for i < len(data) {
|
||||
@@ -299,7 +294,7 @@ func (b *RopeBuffer) PosToLineCol(pos int) (int, int) {
|
||||
return line, col
|
||||
}
|
||||
|
||||
(*rope.Node)(b).EachLeaf(func(n *rope.Node) bool {
|
||||
b.rope.EachLeaf(func(n *ropes.Node) bool {
|
||||
data := n.Value()
|
||||
var i int
|
||||
for i < len(data) {
|
||||
@@ -330,5 +325,31 @@ func (b *RopeBuffer) PosToLineCol(pos int) (int, int) {
|
||||
}
|
||||
|
||||
func (b *RopeBuffer) WriteTo(w io.Writer) (int64, error) {
|
||||
return (*rope.Node)(b).WriteTo(w)
|
||||
return b.rope.WriteTo(w)
|
||||
}
|
||||
|
||||
// RegisterCursor adds the Cursor to a slice which the Buffer uses to update
|
||||
// each Cursor based on changes that occur in the Buffer. Various functions are
|
||||
// called on the Cursor depending upon where the edits occurred and how it should
|
||||
// modify the Cursor's position. Unregister a Cursor before deleting it from
|
||||
// memory, or forgetting it, with UnregisterPosition.
|
||||
func (b *RopeBuffer) RegisterCursor(cursor *Cursor) {
|
||||
if cursor == nil {
|
||||
return
|
||||
}
|
||||
b.anchors = append(b.anchors, cursor)
|
||||
}
|
||||
|
||||
// UnregisterCursor will remove the cursor from the list of watched Cursors.
|
||||
// It is mandatory that a Cursor be unregistered before being freed from memory,
|
||||
// or otherwise being forgotten.
|
||||
func (b *RopeBuffer) UnregisterCursor(cursor *Cursor) {
|
||||
for i, v := range b.anchors {
|
||||
if cursor == v {
|
||||
// Delete item at i without preserving order
|
||||
b.anchors[i] = b.anchors[len(b.anchors)-1]
|
||||
b.anchors[len(b.anchors)-1] = nil
|
||||
b.anchors = b.anchors[:len(b.anchors)-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
23
ui/buffer/util.go
Normal file
23
ui/buffer/util.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package buffer
|
||||
|
||||
// Max returns the larger integer.
|
||||
func Max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Min returns the smaller integer.
|
||||
func Min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Clamp keeps `v` within `a` and `b` numerically. `a` must be smaller than `b`.
|
||||
// Returns clamped `v`.
|
||||
func Clamp(v, a, b int) int {
|
||||
return Max(a, Min(v, b))
|
||||
}
|
Reference in New Issue
Block a user