Refactored cursor architecture into buffer module
This commit is contained in:
parent
59856c5e41
commit
d96c2f6f03
6
main.go
6
main.go
@ -301,7 +301,7 @@ func main() {
|
|||||||
panelContainer.SelectPrev()
|
panelContainer.SelectPrev()
|
||||||
changeFocus(panelContainer)
|
changeFocus(panelContainer)
|
||||||
}}, &ui.ItemEntry{Name: "Focus Up", QuickChar: -1, Shortcut: "Alt+Up", Callback: func() {
|
}}, &ui.ItemEntry{Name: "Focus Up", QuickChar: -1, Shortcut: "Alt+Up", Callback: func() {
|
||||||
|
|
||||||
}}, &ui.ItemEntry{Name: "Focus Down", QuickChar: -1, Shortcut: "Alt+Down", Callback: func() {
|
}}, &ui.ItemEntry{Name: "Focus Down", QuickChar: -1, Shortcut: "Alt+Down", Callback: func() {
|
||||||
|
|
||||||
}}, &ui.ItemEntry{Name: "Focus Left", QuickChar: -1, Shortcut: "Alt+Left", Callback: func() {
|
}}, &ui.ItemEntry{Name: "Focus Left", QuickChar: -1, Shortcut: "Alt+Left", Callback: func() {
|
||||||
@ -399,7 +399,7 @@ func main() {
|
|||||||
if te != nil {
|
if te != nil {
|
||||||
callback := func(line int) {
|
callback := func(line int) {
|
||||||
te := getActiveTextEdit()
|
te := getActiveTextEdit()
|
||||||
te.SetLineCol(line-1, 0)
|
te.SetCursor(te.GetCursor().SetLineCol(line-1, 0))
|
||||||
// Hide dialog
|
// Hide dialog
|
||||||
dialog = nil
|
dialog = nil
|
||||||
changeFocus(panelContainer)
|
changeFocus(panelContainer)
|
||||||
@ -448,7 +448,7 @@ func main() {
|
|||||||
delim = "LF"
|
delim = "LF"
|
||||||
}
|
}
|
||||||
|
|
||||||
line, col := te.GetLineCol()
|
line, col := te.GetCursor().GetLineCol()
|
||||||
|
|
||||||
var tabs string
|
var tabs string
|
||||||
if te.UseHardTabs {
|
if te.UseHardTabs {
|
||||||
|
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)
|
PosToLineCol(pos int) (int, int)
|
||||||
|
|
||||||
WriteTo(w io.Writer) (int64, error)
|
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"
|
"io"
|
||||||
"unicode/utf8"
|
"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 {
|
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
|
// 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,
|
// length of the line, the position of the last byte of the line is returned,
|
||||||
// instead.
|
// instead.
|
||||||
func (b *RopeBuffer) LineColToPos(line, col int) int {
|
func (b *RopeBuffer) LineColToPos(line, col int) int {
|
||||||
var pos int
|
pos := b.getLineStartPos(line)
|
||||||
|
|
||||||
_rope := (*rope.Node)(b)
|
|
||||||
|
|
||||||
pos = b.getLineStartPos(line)
|
|
||||||
|
|
||||||
// Have to do this algorithm for safety. If this function was declared to panic
|
// 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
|
// 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.
|
// more efficient and simpler. But unfortunately, I believe it is necessary.
|
||||||
if col > 0 {
|
if col > 0 {
|
||||||
_, r := _rope.SplitAt(pos)
|
_, r := b.rope.SplitAt(pos)
|
||||||
l, _ := r.SplitAt(_rope.Len() - 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.
|
data := n.Value() // Reference; not a copy.
|
||||||
var i int
|
var i int
|
||||||
for i < len(data) {
|
for i < len(data) {
|
||||||
@ -60,12 +62,11 @@ func (b *RopeBuffer) Line(line int) []byte {
|
|||||||
pos := b.getLineStartPos(line)
|
pos := b.getLineStartPos(line)
|
||||||
bytes := 0
|
bytes := 0
|
||||||
|
|
||||||
_rope := (*rope.Node)(b)
|
_, r := b.rope.SplitAt(pos)
|
||||||
_, r := _rope.SplitAt(pos)
|
l, _ := r.SplitAt(b.rope.Len() - pos)
|
||||||
l, _ := r.SplitAt(_rope.Len() - pos)
|
|
||||||
|
|
||||||
var isCRLF bool // true if the last byte was '\r'
|
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.
|
data := n.Value() // Reference; not a copy.
|
||||||
var i int
|
var i int
|
||||||
for i < len(data) {
|
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 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,
|
// 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.
|
// so do not write to it.
|
||||||
func (b *RopeBuffer) Slice(startLine, startCol, endLine, endCol int) []byte {
|
func (b *RopeBuffer) Slice(startLine, startCol, endLine, endCol int) []byte {
|
||||||
endPos := b.LineColToPos(endLine, endCol)
|
endPos := b.LineColToPos(endLine, endCol)
|
||||||
if length := (*rope.Node)(b).Len(); endPos >= length {
|
if length := b.rope.Len(); endPos >= length {
|
||||||
endPos = length - 1
|
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
|
// 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,
|
// to copy all of the data in the buffer. Use sparingly. Try using other methods,
|
||||||
// where possible.
|
// where possible.
|
||||||
func (b *RopeBuffer) Bytes() []byte {
|
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.
|
// Insert copies a byte slice (inserting it) into the position at line, col.
|
||||||
func (b *RopeBuffer) Insert(line, col int, value []byte) {
|
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,
|
// 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)
|
start := b.LineColToPos(startLine, startCol)
|
||||||
end := b.LineColToPos(endLine, endCol) + 1
|
end := b.LineColToPos(endLine, endCol) + 1
|
||||||
|
|
||||||
if len := (*rope.Node)(b).Len(); end >= len {
|
if len := b.rope.Len(); end >= len {
|
||||||
end = len
|
end = len
|
||||||
if start > end {
|
if start > end {
|
||||||
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
|
// 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 {
|
func (b *RopeBuffer) Count(startLine, startCol, endLine, endCol int, sequence []byte) int {
|
||||||
startPos := b.LineColToPos(startLine, startCol)
|
startPos := b.LineColToPos(startLine, startCol)
|
||||||
endPos := b.LineColToPos(endLine, endCol)
|
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.
|
// Len returns the number of bytes in the buffer.
|
||||||
func (b *RopeBuffer) Len() int {
|
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,
|
// 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
|
// 1 is returned, because there is always at least one line. This function
|
||||||
// basically counts the number of newline ('\n') characters in a buffer.
|
// basically counts the number of newline ('\n') characters in a buffer.
|
||||||
func (b *RopeBuffer) Lines() int {
|
func (b *RopeBuffer) Lines() int {
|
||||||
rope := (*rope.Node)(b)
|
rope := b.rope
|
||||||
return rope.Count(0, rope.Len(), []byte{'\n'}) + 1
|
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
|
// 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.
|
// than or equal to the number of lines in the buffer, a panic is issued.
|
||||||
func (b *RopeBuffer) getLineStartPos(line int) int {
|
func (b *RopeBuffer) getLineStartPos(line int) int {
|
||||||
_rope := (*rope.Node)(b)
|
|
||||||
var pos int
|
var pos int
|
||||||
|
|
||||||
if line > 0 {
|
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--
|
line--
|
||||||
pos = idx + 1 // idx+1 = start of line after delimiter
|
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 line <= 0 // If pos is now the start of the line we're searching for
|
||||||
return true // Stop indexing
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,8 +182,7 @@ func (b *RopeBuffer) getLineStartPos(line int) int {
|
|||||||
func (b *RopeBuffer) RunesInLineWithDelim(line int) int {
|
func (b *RopeBuffer) RunesInLineWithDelim(line int) int {
|
||||||
linePos := b.getLineStartPos(line)
|
linePos := b.getLineStartPos(line)
|
||||||
|
|
||||||
_rope := (*rope.Node)(b)
|
ropeLen := b.rope.Len()
|
||||||
ropeLen := _rope.Len()
|
|
||||||
|
|
||||||
if linePos >= ropeLen {
|
if linePos >= ropeLen {
|
||||||
return 0
|
return 0
|
||||||
@ -194,11 +190,11 @@ func (b *RopeBuffer) RunesInLineWithDelim(line int) int {
|
|||||||
|
|
||||||
var count int
|
var count int
|
||||||
|
|
||||||
_, r := _rope.SplitAt(linePos)
|
_, r := b.rope.SplitAt(linePos)
|
||||||
l, _ := r.SplitAt(ropeLen - linePos)
|
l, _ := r.SplitAt(ropeLen - linePos)
|
||||||
|
|
||||||
var isCRLF bool
|
var isCRLF bool
|
||||||
l.EachLeaf(func(n *rope.Node) bool {
|
l.EachLeaf(func(n *ropes.Node) bool {
|
||||||
data := n.Value() // Reference; not a copy.
|
data := n.Value() // Reference; not a copy.
|
||||||
var i int
|
var i int
|
||||||
for i < len(data) {
|
for i < len(data) {
|
||||||
@ -229,8 +225,7 @@ func (b *RopeBuffer) RunesInLineWithDelim(line int) int {
|
|||||||
func (b *RopeBuffer) RunesInLine(line int) int {
|
func (b *RopeBuffer) RunesInLine(line int) int {
|
||||||
linePos := b.getLineStartPos(line)
|
linePos := b.getLineStartPos(line)
|
||||||
|
|
||||||
_rope := (*rope.Node)(b)
|
ropeLen := b.rope.Len()
|
||||||
ropeLen := _rope.Len()
|
|
||||||
|
|
||||||
if linePos >= ropeLen {
|
if linePos >= ropeLen {
|
||||||
return 0
|
return 0
|
||||||
@ -238,11 +233,11 @@ func (b *RopeBuffer) RunesInLine(line int) int {
|
|||||||
|
|
||||||
var count int
|
var count int
|
||||||
|
|
||||||
_, r := _rope.SplitAt(linePos)
|
_, r := b.rope.SplitAt(linePos)
|
||||||
l, _ := r.SplitAt(ropeLen - linePos)
|
l, _ := r.SplitAt(ropeLen - linePos)
|
||||||
|
|
||||||
var isCRLF bool
|
var isCRLF bool
|
||||||
l.EachLeaf(func(n *rope.Node) bool {
|
l.EachLeaf(func(n *ropes.Node) bool {
|
||||||
data := n.Value() // Reference; not a copy.
|
data := n.Value() // Reference; not a copy.
|
||||||
var i int
|
var i int
|
||||||
for i < len(data) {
|
for i < len(data) {
|
||||||
@ -299,7 +294,7 @@ func (b *RopeBuffer) PosToLineCol(pos int) (int, int) {
|
|||||||
return line, col
|
return line, col
|
||||||
}
|
}
|
||||||
|
|
||||||
(*rope.Node)(b).EachLeaf(func(n *rope.Node) bool {
|
b.rope.EachLeaf(func(n *ropes.Node) bool {
|
||||||
data := n.Value()
|
data := n.Value()
|
||||||
var i int
|
var i int
|
||||||
for i < len(data) {
|
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) {
|
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))
|
||||||
|
}
|
341
ui/textedit.go
Normal file → Executable file
341
ui/textedit.go
Normal file → Executable file
@ -14,17 +14,6 @@ import (
|
|||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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 {
|
|
||||||
StartLine, StartCol int
|
|
||||||
EndLine, EndCol int
|
|
||||||
}
|
|
||||||
|
|
||||||
// TextEdit is a field for line-based editing. It features syntax highlighting
|
// TextEdit is a field for line-based editing. It features syntax highlighting
|
||||||
// tools, is autocomplete ready, and contains the various information about
|
// tools, is autocomplete ready, and contains the various information about
|
||||||
// content being edited.
|
// content being edited.
|
||||||
@ -39,13 +28,12 @@ type TextEdit struct {
|
|||||||
FilePath string // Will be empty if the file has not been saved yet
|
FilePath string // Will be empty if the file has not been saved yet
|
||||||
|
|
||||||
screen *tcell.Screen // We keep our own reference to the screen for cursor purposes.
|
screen *tcell.Screen // We keep our own reference to the screen for cursor purposes.
|
||||||
curx, cury int // Zero-based: cursor points before the character at that position.
|
cursor buffer.Cursor
|
||||||
prevCurCol int // Previous maximum column the cursor was at, when the user pressed left or right
|
|
||||||
scrollx, scrolly int // X and Y offset of view, known as scroll
|
scrollx, scrolly int // X and Y offset of view, known as scroll
|
||||||
theme *Theme
|
theme *Theme
|
||||||
|
|
||||||
selection Region // Selection: selectMode determines if it should be used
|
selection buffer.Region // Selection: selectMode determines if it should be used
|
||||||
selectMode bool // Whether the user is actively selecting text
|
selectMode bool // Whether the user is actively selecting text
|
||||||
|
|
||||||
baseComponent
|
baseComponent
|
||||||
}
|
}
|
||||||
@ -88,6 +76,7 @@ loop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Buffer = buffer.NewRopeBuffer(contents)
|
t.Buffer = buffer.NewRopeBuffer(contents)
|
||||||
|
t.cursor = buffer.NewCursor(&t.Buffer)
|
||||||
|
|
||||||
// TODO: replace with automatic determination of language via filetype
|
// TODO: replace with automatic determination of language via filetype
|
||||||
lang := &buffer.Language{
|
lang := &buffer.Language{
|
||||||
@ -156,38 +145,45 @@ func (t *TextEdit) Delete(forwards bool) {
|
|||||||
t.Dirty = true
|
t.Dirty = true
|
||||||
|
|
||||||
var deletedLine bool // Whether any whole line has been deleted (changing the # of lines)
|
var deletedLine bool // Whether any whole line has been deleted (changing the # of lines)
|
||||||
startingLine := t.cury
|
cursLine, cursCol := t.cursor.GetLineCol()
|
||||||
|
startingLine := cursLine
|
||||||
|
|
||||||
if t.selectMode { // If text is selected, delete the whole selection
|
if t.selectMode { // If text is selected, delete the whole selection
|
||||||
t.selectMode = false // Disable selection and prevent infinite loop
|
t.selectMode = false // Disable selection and prevent infinite loop
|
||||||
|
|
||||||
// Delete the region
|
startLine, startCol := t.selection.Start()
|
||||||
t.Buffer.Remove(t.selection.StartLine, t.selection.StartCol, t.selection.EndLine, t.selection.EndCol)
|
endLine, endCol := t.selection.End()
|
||||||
t.SetLineCol(t.selection.StartLine, t.selection.StartCol) // Set cursor to start of region
|
|
||||||
|
|
||||||
deletedLine = t.selection.StartLine != t.selection.EndLine
|
// Delete the region
|
||||||
|
t.Buffer.Remove(startLine, startCol, endLine, endCol)
|
||||||
|
t.cursor.SetLineCol(startLine, startCol) // Set cursor to start of region
|
||||||
|
|
||||||
|
deletedLine = startLine != endLine
|
||||||
} else { // Not deleting selection
|
} else { // Not deleting selection
|
||||||
if forwards { // Delete the character after the cursor
|
if forwards { // Delete the character after the cursor
|
||||||
// If the cursor is not at the end of the last line...
|
// If the cursor is not at the end of the last line...
|
||||||
if t.cury < t.Buffer.Lines()-1 || t.curx < t.Buffer.RunesInLine(t.cury) {
|
if cursLine < t.Buffer.Lines()-1 || cursCol < t.Buffer.RunesInLine(cursLine) {
|
||||||
bytes := t.Buffer.Slice(t.cury, t.curx, t.cury, t.curx) // Get the character at cursor
|
bytes := t.Buffer.Slice(cursLine, cursCol, cursLine, cursCol) // Get the character at cursor
|
||||||
deletedLine = bytes[0] == '\n'
|
deletedLine = bytes[0] == '\n'
|
||||||
|
|
||||||
t.Buffer.Remove(t.cury, t.curx, t.cury, t.curx) // Remove character at cursor
|
t.Buffer.Remove(cursLine, cursCol, cursLine, cursCol) // Remove character at cursor
|
||||||
}
|
}
|
||||||
} else { // Delete the character before the cursor
|
} else { // Delete the character before the cursor
|
||||||
// If the cursor is not at the first column of the first line...
|
// If the cursor is not at the first column of the first line...
|
||||||
if t.cury > 0 || t.curx > 0 {
|
if cursLine > 0 || cursCol > 0 {
|
||||||
t.CursorLeft() // Back up to that character
|
t.cursor.Left() // Back up to that character
|
||||||
|
|
||||||
bytes := t.Buffer.Slice(t.cury, t.curx, t.cury, t.curx) // Get the char at cursor
|
bytes := t.Buffer.Slice(cursLine, cursCol, cursLine, cursCol) // Get the char at cursor
|
||||||
deletedLine = bytes[0] == '\n'
|
deletedLine = bytes[0] == '\n'
|
||||||
|
|
||||||
t.Buffer.Remove(t.cury, t.curx, t.cury, t.curx) // Remove character at cursor
|
t.Buffer.Remove(cursLine, cursCol, cursLine, cursCol) // Remove character at cursor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.ScrollToCursor()
|
||||||
|
t.updateCursorVisibility()
|
||||||
|
|
||||||
if deletedLine {
|
if deletedLine {
|
||||||
t.Highlighter.InvalidateLines(startingLine, t.Buffer.Lines()-1)
|
t.Highlighter.InvalidateLines(startingLine, t.Buffer.Lines()-1)
|
||||||
} else {
|
} else {
|
||||||
@ -206,7 +202,8 @@ func (t *TextEdit) Insert(contents string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var lineInserted bool // True if contents contains a '\n'
|
var lineInserted bool // True if contents contains a '\n'
|
||||||
startingLine := t.cury
|
cursLine, cursCol := t.cursor.GetLineCol()
|
||||||
|
startingLine := cursLine
|
||||||
|
|
||||||
runes := []rune(contents)
|
runes := []rune(contents)
|
||||||
for i := 0; i < len(runes); i++ {
|
for i := 0; i < len(runes); i++ {
|
||||||
@ -216,13 +213,11 @@ func (t *TextEdit) Insert(contents string) {
|
|||||||
// If the character after is a \n, then it is a CRLF
|
// If the character after is a \n, then it is a CRLF
|
||||||
if i+1 < len(runes) && runes[i+1] == '\n' {
|
if i+1 < len(runes) && runes[i+1] == '\n' {
|
||||||
i++ // Consume '\n' after
|
i++ // Consume '\n' after
|
||||||
t.Buffer.Insert(t.cury, t.curx, []byte{'\n'})
|
t.Buffer.Insert(cursLine, cursCol, []byte{'\n'})
|
||||||
t.SetLineCol(t.cury+1, 0) // Go to the start of that new line
|
|
||||||
lineInserted = true
|
lineInserted = true
|
||||||
}
|
}
|
||||||
case '\n':
|
case '\n':
|
||||||
t.Buffer.Insert(t.cury, t.curx, []byte{'\n'})
|
t.Buffer.Insert(cursLine, cursCol, []byte{'\n'})
|
||||||
t.SetLineCol(t.cury+1, 0) // Go to the start of that new line
|
|
||||||
lineInserted = true
|
lineInserted = true
|
||||||
case '\b':
|
case '\b':
|
||||||
t.Delete(false) // Delete the character before the cursor
|
t.Delete(false) // Delete the character before the cursor
|
||||||
@ -230,18 +225,19 @@ func (t *TextEdit) Insert(contents string) {
|
|||||||
if !t.UseHardTabs { // If this file does not use hard tabs...
|
if !t.UseHardTabs { // If this file does not use hard tabs...
|
||||||
// Insert spaces
|
// Insert spaces
|
||||||
spaces := strings.Repeat(" ", t.TabSize)
|
spaces := strings.Repeat(" ", t.TabSize)
|
||||||
t.Buffer.Insert(t.cury, t.curx, []byte(spaces))
|
t.Buffer.Insert(cursLine, cursCol, []byte(spaces))
|
||||||
t.SetLineCol(t.cury, t.curx+len(spaces)) // Advance the cursor
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
fallthrough // Append the \t character
|
fallthrough // Append the \t character
|
||||||
default:
|
default:
|
||||||
// Insert character into line
|
// Insert character into line
|
||||||
t.Buffer.Insert(t.cury, t.curx, []byte(string(ch)))
|
t.Buffer.Insert(cursLine, cursCol, []byte(string(ch)))
|
||||||
t.SetLineCol(t.cury, t.curx+1) // Advance the cursor
|
// t.SetLineCol(t.cury, t.curx+1) // Advance the cursor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.prevCurCol = t.curx
|
|
||||||
|
t.ScrollToCursor()
|
||||||
|
t.updateCursorVisibility()
|
||||||
|
|
||||||
if lineInserted {
|
if lineInserted {
|
||||||
t.Highlighter.InvalidateLines(startingLine, t.Buffer.Lines()-1)
|
t.Highlighter.InvalidateLines(startingLine, t.Buffer.Lines()-1)
|
||||||
@ -262,34 +258,24 @@ func (t *TextEdit) getTabCountInLineAtCol(line, col int) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLineCol returns (line, col) of the cursor. Zero is origin for both.
|
// updateCursorVisibility sets the position of the terminal's cursor with the
|
||||||
func (t *TextEdit) GetLineCol() (int, int) {
|
// cursor of the TextEdit. Sends a signal to show the cursor if the TextEdit
|
||||||
return t.cury, t.curx
|
// is focused and not in select mode.
|
||||||
|
func (t *TextEdit) updateCursorVisibility() {
|
||||||
|
if t.focused && !t.selectMode {
|
||||||
|
columnWidth := t.getColumnWidth()
|
||||||
|
line, col := t.cursor.GetLineCol()
|
||||||
|
tabOffset := t.getTabCountInLineAtCol(line, col) * (t.TabSize - 1)
|
||||||
|
(*t.screen).ShowCursor(t.x+columnWidth+col+tabOffset-t.scrollx, t.y+line-t.scrolly)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The same as updateTerminalCursor but the caller can provide the tabOffset to
|
// Scroll the screen if the cursor is out of view.
|
||||||
// save the original function a calculation.
|
func (t *TextEdit) ScrollToCursor() {
|
||||||
func (t *TextEdit) updateTerminalCursorNoHelper(columnWidth, tabOffset int) {
|
line, col := t.cursor.GetLineCol()
|
||||||
(*t.screen).ShowCursor(t.x+columnWidth+t.curx+tabOffset-t.scrollx, t.y+t.cury-t.scrolly)
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateTerminalCursor sets the position of the cursor with the cursor position
|
|
||||||
// properties of the TextEdit. Always sends a signal to *show* the cursor.
|
|
||||||
func (t *TextEdit) updateTerminalCursor() {
|
|
||||||
columnWidth := t.getColumnWidth()
|
|
||||||
tabOffset := t.getTabCountInLineAtCol(t.cury, t.curx) * (t.TabSize - 1)
|
|
||||||
t.updateTerminalCursorNoHelper(columnWidth, tabOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLineCol sets the cursor line and column position. Zero is origin for both.
|
|
||||||
// If `line` is out of bounds, `line` will be clamped to the closest available line.
|
|
||||||
// If `col` is out of bounds, `col` will be clamped to the closest column available for the line.
|
|
||||||
// Will scroll the TextEdit just enough to see the line the cursor is at.
|
|
||||||
func (t *TextEdit) SetLineCol(line, col int) {
|
|
||||||
line, col = t.Buffer.ClampLineCol(line, col)
|
|
||||||
|
|
||||||
// Handle hard tabs
|
// Handle hard tabs
|
||||||
tabOffset := t.getTabCountInLineAtCol(line, col) * (t.TabSize - 1) // Offset for the current line from hard tabs (temporary; purely visual)
|
tabOffset := t.getTabCountInLineAtCol(line, col) * (t.TabSize - 1) // Offset for the current line from hard tabs
|
||||||
|
|
||||||
// Scroll the screen when going to lines out of view
|
// Scroll the screen when going to lines out of view
|
||||||
if line >= t.scrolly+t.height-1 { // If the new line is below view...
|
if line >= t.scrolly+t.height-1 { // If the new line is below view...
|
||||||
@ -306,68 +292,15 @@ func (t *TextEdit) SetLineCol(line, col int) {
|
|||||||
} else if col+tabOffset < t.scrollx { // If the new column is left of view
|
} else if col+tabOffset < t.scrollx { // If the new column is left of view
|
||||||
t.scrollx = col + tabOffset // Scroll left enough to view that column
|
t.scrollx = col + tabOffset // Scroll left enough to view that column
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.scrollx < 0 {
|
|
||||||
panic("oops")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.cury, t.curx = line, col
|
|
||||||
if t.focused && !t.selectMode {
|
|
||||||
// Update terminal cursor position
|
|
||||||
t.updateTerminalCursorNoHelper(columnWidth, tabOffset)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CursorUp moves the cursor up a line.
|
func (t *TextEdit) GetCursor() buffer.Cursor {
|
||||||
func (t *TextEdit) CursorUp() {
|
return t.cursor
|
||||||
if t.cury <= 0 { // If the cursor is at the first line...
|
|
||||||
t.SetLineCol(t.cury, 0) // Go to beginning
|
|
||||||
} else {
|
|
||||||
line, col := t.Buffer.ClampLineCol(t.cury-1, t.prevCurCol)
|
|
||||||
if t.UseHardTabs { // When using hard tabs, subtract offsets produced by tabs
|
|
||||||
tabOffset := t.getTabCountInLineAtCol(line, col) * (t.TabSize - 1)
|
|
||||||
col -= tabOffset // We still count each \t in the col
|
|
||||||
}
|
|
||||||
t.SetLineCol(line, col)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CursorDown moves the cursor down a line.
|
func (t *TextEdit) SetCursor(newCursor buffer.Cursor) {
|
||||||
func (t *TextEdit) CursorDown() {
|
t.cursor = newCursor
|
||||||
if t.cury >= t.Buffer.Lines()-1 { // If the cursor is at the last line...
|
t.updateCursorVisibility()
|
||||||
t.SetLineCol(t.cury, math.MaxInt32) // Go to end of current line
|
|
||||||
} else {
|
|
||||||
line, col := t.Buffer.ClampLineCol(t.cury+1, t.prevCurCol)
|
|
||||||
if t.UseHardTabs {
|
|
||||||
tabOffset := t.getTabCountInLineAtCol(line, col) * (t.TabSize - 1)
|
|
||||||
col -= tabOffset // We still count each \t in the col
|
|
||||||
}
|
|
||||||
t.SetLineCol(line, col) // Go to line below
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CursorLeft moves the cursor left a column.
|
|
||||||
func (t *TextEdit) CursorLeft() {
|
|
||||||
if t.curx <= 0 && t.cury != 0 { // If we are at the beginning of the current line...
|
|
||||||
t.SetLineCol(t.cury-1, math.MaxInt32) // Go to end of line above
|
|
||||||
} else {
|
|
||||||
t.SetLineCol(t.cury, t.curx-1)
|
|
||||||
}
|
|
||||||
tabOffset := t.getTabCountInLineAtCol(t.cury, t.curx) * (t.TabSize - 1)
|
|
||||||
t.prevCurCol = t.curx + tabOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
// CursorRight moves the cursor right a column.
|
|
||||||
func (t *TextEdit) CursorRight() {
|
|
||||||
// If we are at the end of the current line,
|
|
||||||
// and not at the last line...
|
|
||||||
if t.curx >= t.Buffer.RunesInLine(t.cury) && t.cury < t.Buffer.Lines()-1 {
|
|
||||||
t.SetLineCol(t.cury+1, 0) // Go to beginning of line below
|
|
||||||
} else {
|
|
||||||
t.SetLineCol(t.cury, t.curx+1)
|
|
||||||
}
|
|
||||||
tabOffset := t.getTabCountInLineAtCol(t.cury, t.curx) * (t.TabSize - 1)
|
|
||||||
t.prevCurCol = t.curx + tabOffset
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getColumnWidth returns the width of the line numbers column if it is present.
|
// getColumnWidth returns the width of the line numbers column if it is present.
|
||||||
@ -386,7 +319,9 @@ func (t *TextEdit) getColumnWidth() int {
|
|||||||
func (t *TextEdit) GetSelectedBytes() []byte {
|
func (t *TextEdit) GetSelectedBytes() []byte {
|
||||||
// TODO: there's a bug with copying text
|
// TODO: there's a bug with copying text
|
||||||
if t.selectMode {
|
if t.selectMode {
|
||||||
return t.Buffer.Slice(t.selection.StartLine, t.selection.StartCol, t.selection.EndLine, t.selection.EndCol)
|
startLine, startCol := t.selection.Start()
|
||||||
|
endLine, endCol := t.selection.End()
|
||||||
|
return t.Buffer.Slice(startLine, startCol, endLine, endCol)
|
||||||
}
|
}
|
||||||
return []byte{}
|
return []byte{}
|
||||||
}
|
}
|
||||||
@ -486,23 +421,26 @@ func (t *TextEdit) Draw(s tcell.Screen) {
|
|||||||
r = ' '
|
r = ' '
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startLine, startCol := t.selection.Start()
|
||||||
|
endLine, endCol := t.selection.End()
|
||||||
|
|
||||||
// Determine whether we select the current rune. Also only select runes within
|
// Determine whether we select the current rune. Also only select runes within
|
||||||
// the line bytes range.
|
// the line bytes range.
|
||||||
if t.selectMode && line >= t.selection.StartLine && line <= t.selection.EndLine { // If we're part of a selection...
|
if t.selectMode && line >= startLine && line <= endLine { // If we're part of a selection...
|
||||||
_origRuneIdx := origRuneIdx(runeIdx)
|
_origRuneIdx := origRuneIdx(runeIdx)
|
||||||
if line == t.selection.StartLine { // If selection starts at this line...
|
if line == startLine { // If selection starts at this line...
|
||||||
if _origRuneIdx >= t.selection.StartCol { // And we're at or past the start col...
|
if _origRuneIdx >= startCol { // And we're at or past the start col...
|
||||||
// If the start line is also the end line...
|
// If the start line is also the end line...
|
||||||
if line == t.selection.EndLine {
|
if line == endLine {
|
||||||
if _origRuneIdx <= t.selection.EndCol { // And we're before the end of that...
|
if _origRuneIdx <= endCol { // And we're before the end of that...
|
||||||
selected = true
|
selected = true
|
||||||
}
|
}
|
||||||
} else { // Definitely highlight
|
} else { // Definitely highlight
|
||||||
selected = true
|
selected = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if line == t.selection.EndLine { // If selection ends at this line...
|
} else if line == endLine { // If selection ends at this line...
|
||||||
if _origRuneIdx <= t.selection.EndCol { // And we're at or before the end col...
|
if _origRuneIdx <= endCol { // And we're at or before the end col...
|
||||||
selected = true
|
selected = true
|
||||||
}
|
}
|
||||||
} else { // We're between the start and the end lines, definitely highlight.
|
} else { // We're between the start and the end lines, definitely highlight.
|
||||||
@ -547,8 +485,7 @@ func (t *TextEdit) Draw(s tcell.Screen) {
|
|||||||
DrawStr(s, t.x, lineY, columnStr, columnStyle) // Draw column
|
DrawStr(s, t.x, lineY, columnStr, columnStyle) // Draw column
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cursor
|
t.updateCursorVisibility()
|
||||||
t.SetLineCol(t.cury, t.curx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetFocused sets whether the TextEdit is focused. When focused, the cursor is set visible
|
// SetFocused sets whether the TextEdit is focused. When focused, the cursor is set visible
|
||||||
@ -556,7 +493,7 @@ func (t *TextEdit) Draw(s tcell.Screen) {
|
|||||||
func (t *TextEdit) SetFocused(v bool) {
|
func (t *TextEdit) SetFocused(v bool) {
|
||||||
t.focused = v
|
t.focused = v
|
||||||
if v {
|
if v {
|
||||||
t.updateTerminalCursor()
|
t.updateCursorVisibility()
|
||||||
} else {
|
} else {
|
||||||
(*t.screen).HideCursor()
|
(*t.screen).HideCursor()
|
||||||
}
|
}
|
||||||
@ -571,95 +508,105 @@ func (t *TextEdit) HandleEvent(event tcell.Event) bool {
|
|||||||
// Cursor movement
|
// Cursor movement
|
||||||
case tcell.KeyUp:
|
case tcell.KeyUp:
|
||||||
if ev.Modifiers() == tcell.ModShift {
|
if ev.Modifiers() == tcell.ModShift {
|
||||||
if !t.selectMode {
|
// if !t.selectMode {
|
||||||
t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
// t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
||||||
t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
// t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
||||||
t.selectMode = true
|
// t.selectMode = true
|
||||||
} else {
|
// } else {
|
||||||
prevCurX, prevCurY := t.curx, t.cury
|
// prevCurX, prevCurY := t.curx, t.cury
|
||||||
t.CursorUp()
|
// t.CursorUp()
|
||||||
// Grow the selection in the correct direction
|
// // Grow the selection in the correct direction
|
||||||
if prevCurY <= t.selection.StartLine && prevCurX <= t.selection.StartCol {
|
// if prevCurY <= t.selection.StartLine && prevCurX <= t.selection.StartCol {
|
||||||
t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
// t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
||||||
} else {
|
// } else {
|
||||||
t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
// t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} else {
|
} else {
|
||||||
t.selectMode = false
|
t.selectMode = false
|
||||||
t.CursorUp()
|
t.SetCursor(t.cursor.Up())
|
||||||
|
t.ScrollToCursor()
|
||||||
}
|
}
|
||||||
case tcell.KeyDown:
|
case tcell.KeyDown:
|
||||||
if ev.Modifiers() == tcell.ModShift {
|
if ev.Modifiers() == tcell.ModShift {
|
||||||
if !t.selectMode {
|
// if !t.selectMode {
|
||||||
t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
// t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
||||||
t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
// t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
||||||
t.selectMode = true
|
// t.selectMode = true
|
||||||
} else {
|
// } else {
|
||||||
prevCurX, prevCurY := t.curx, t.cury
|
// prevCurX, prevCurY := t.curx, t.cury
|
||||||
t.CursorDown()
|
// t.CursorDown()
|
||||||
if prevCurY >= t.selection.EndLine && prevCurX >= t.selection.EndCol {
|
// if prevCurY >= t.selection.EndLine && prevCurX >= t.selection.EndCol {
|
||||||
t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
// t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
||||||
} else {
|
// } else {
|
||||||
t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
// t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} else {
|
} else {
|
||||||
t.selectMode = false
|
t.selectMode = false
|
||||||
t.CursorDown()
|
t.SetCursor(t.cursor.Down())
|
||||||
|
t.ScrollToCursor()
|
||||||
}
|
}
|
||||||
case tcell.KeyLeft:
|
case tcell.KeyLeft:
|
||||||
if ev.Modifiers() == tcell.ModShift {
|
if ev.Modifiers() == tcell.ModShift {
|
||||||
if !t.selectMode {
|
// if !t.selectMode {
|
||||||
t.CursorLeft() // We want the character to the left to be selected only (think insert)
|
// t.CursorLeft() // We want the character to the left to be selected only (think insert)
|
||||||
t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
// t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
||||||
t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
// t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
||||||
t.selectMode = true
|
// t.selectMode = true
|
||||||
} else {
|
// } else {
|
||||||
prevCurX, prevCurY := t.curx, t.cury
|
// prevCurX, prevCurY := t.curx, t.cury
|
||||||
t.CursorLeft()
|
// t.CursorLeft()
|
||||||
if prevCurY == t.selection.StartLine && prevCurX == t.selection.StartCol { // We are moving the start...
|
// if prevCurY == t.selection.StartLine && prevCurX == t.selection.StartCol { // We are moving the start...
|
||||||
t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
// t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
||||||
} else {
|
// } else {
|
||||||
t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
// t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} else {
|
} else {
|
||||||
t.selectMode = false
|
t.selectMode = false
|
||||||
t.CursorLeft()
|
t.SetCursor(t.cursor.Left())
|
||||||
|
t.ScrollToCursor()
|
||||||
}
|
}
|
||||||
case tcell.KeyRight:
|
case tcell.KeyRight:
|
||||||
if ev.Modifiers() == tcell.ModShift {
|
if ev.Modifiers() == tcell.ModShift {
|
||||||
if !t.selectMode { // If we are not already selecting...
|
// if !t.selectMode { // If we are not already selecting...
|
||||||
// Reset the selection to cursor pos
|
// // Reset the selection to cursor pos
|
||||||
t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
// t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
||||||
t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
// t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
||||||
t.selectMode = true
|
// t.selectMode = true
|
||||||
} else {
|
// } else {
|
||||||
prevCurX, prevCurY := t.curx, t.cury
|
// prevCurX, prevCurY := t.curx, t.cury
|
||||||
t.CursorRight() // Advance the cursor
|
// t.CursorRight() // Advance the cursor
|
||||||
if prevCurY == t.selection.EndLine && prevCurX == t.selection.EndCol {
|
// if prevCurY == t.selection.EndLine && prevCurX == t.selection.EndCol {
|
||||||
t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
// t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
|
||||||
} else {
|
// } else {
|
||||||
t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
// t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} else {
|
} else {
|
||||||
t.selectMode = false
|
t.selectMode = false
|
||||||
t.CursorRight()
|
t.SetCursor(t.cursor.Right())
|
||||||
|
t.ScrollToCursor()
|
||||||
}
|
}
|
||||||
case tcell.KeyHome:
|
case tcell.KeyHome:
|
||||||
t.SetLineCol(t.cury, 0)
|
cursLine, _ := t.cursor.GetLineCol()
|
||||||
t.prevCurCol = t.curx
|
// TODO: go to first (non-whitespace) character on current line, if we are not already there
|
||||||
|
// otherwise actually go to first (0) character of the line
|
||||||
|
t.SetCursor(t.cursor.SetLineCol(cursLine, 0))
|
||||||
|
t.ScrollToCursor()
|
||||||
case tcell.KeyEnd:
|
case tcell.KeyEnd:
|
||||||
t.SetLineCol(t.cury, math.MaxInt32) // Max column
|
cursLine, _ := t.cursor.GetLineCol()
|
||||||
t.prevCurCol = t.curx
|
t.SetCursor(t.cursor.SetLineCol(cursLine, math.MaxInt32)) // Max column
|
||||||
|
t.ScrollToCursor()
|
||||||
case tcell.KeyPgUp:
|
case tcell.KeyPgUp:
|
||||||
t.SetLineCol(t.scrolly-t.height, t.curx) // Go a page up
|
_, cursCol := t.cursor.GetLineCol()
|
||||||
t.prevCurCol = t.curx
|
t.SetCursor(t.cursor.SetLineCol(t.scrolly-t.height, cursCol)) // Go a page up
|
||||||
|
t.ScrollToCursor()
|
||||||
case tcell.KeyPgDn:
|
case tcell.KeyPgDn:
|
||||||
t.SetLineCol(t.scrolly+t.height*2-1, t.curx) // Go a page down
|
_, cursCol := t.cursor.GetLineCol()
|
||||||
t.prevCurCol = t.curx
|
t.SetCursor(t.cursor.SetLineCol(t.scrolly+t.height*2-1, cursCol)) // Go a page down
|
||||||
|
t.ScrollToCursor()
|
||||||
|
|
||||||
// Deleting
|
// Deleting
|
||||||
case tcell.KeyBackspace:
|
case tcell.KeyBackspace:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user