Refactored cursor architecture into buffer module

This commit is contained in:
Luke Wilson 2021-04-12 17:38:29 -05:00
parent 59856c5e41
commit d96c2f6f03
6 changed files with 352 additions and 242 deletions

View File

@ -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
View 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
View 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
View 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
View 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
View 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: