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

@ -399,7 +399,7 @@ func main() {
if te != nil {
callback := func(line int) {
te := getActiveTextEdit()
te.SetLineCol(line-1, 0)
te.SetCursor(te.GetCursor().SetLineCol(line-1, 0))
// Hide dialog
dialog = nil
changeFocus(panelContainer)
@ -448,7 +448,7 @@ func main() {
delim = "LF"
}
line, col := te.GetLineCol()
line, col := te.GetCursor().GetLineCol()
var tabs string
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)
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
}

103
ui/buffer/rope.go Normal file → Executable file
View 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
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
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))
}

337
ui/textedit.go Normal file → Executable file
View File

@ -14,17 +14,6 @@ import (
"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
// tools, is autocomplete ready, and contains the various information about
// content being edited.
@ -39,12 +28,11 @@ type TextEdit struct {
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.
curx, cury int // Zero-based: cursor points before the character at that position.
prevCurCol int // Previous maximum column the cursor was at, when the user pressed left or right
cursor buffer.Cursor
scrollx, scrolly int // X and Y offset of view, known as scroll
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
baseComponent
@ -88,6 +76,7 @@ loop:
}
t.Buffer = buffer.NewRopeBuffer(contents)
t.cursor = buffer.NewCursor(&t.Buffer)
// TODO: replace with automatic determination of language via filetype
lang := &buffer.Language{
@ -156,38 +145,45 @@ func (t *TextEdit) Delete(forwards bool) {
t.Dirty = true
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
t.selectMode = false // Disable selection and prevent infinite loop
// Delete the region
t.Buffer.Remove(t.selection.StartLine, t.selection.StartCol, t.selection.EndLine, t.selection.EndCol)
t.SetLineCol(t.selection.StartLine, t.selection.StartCol) // Set cursor to start of region
startLine, startCol := t.selection.Start()
endLine, endCol := t.selection.End()
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
if forwards { // Delete the character after the cursor
// 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) {
bytes := t.Buffer.Slice(t.cury, t.curx, t.cury, t.curx) // Get the character at cursor
if cursLine < t.Buffer.Lines()-1 || cursCol < t.Buffer.RunesInLine(cursLine) {
bytes := t.Buffer.Slice(cursLine, cursCol, cursLine, cursCol) // Get the character at cursor
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
// If the cursor is not at the first column of the first line...
if t.cury > 0 || t.curx > 0 {
t.CursorLeft() // Back up to that character
if cursLine > 0 || cursCol > 0 {
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'
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 {
t.Highlighter.InvalidateLines(startingLine, t.Buffer.Lines()-1)
} else {
@ -206,7 +202,8 @@ func (t *TextEdit) Insert(contents string) {
}
var lineInserted bool // True if contents contains a '\n'
startingLine := t.cury
cursLine, cursCol := t.cursor.GetLineCol()
startingLine := cursLine
runes := []rune(contents)
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 i+1 < len(runes) && runes[i+1] == '\n' {
i++ // Consume '\n' after
t.Buffer.Insert(t.cury, t.curx, []byte{'\n'})
t.SetLineCol(t.cury+1, 0) // Go to the start of that new line
t.Buffer.Insert(cursLine, cursCol, []byte{'\n'})
lineInserted = true
}
case '\n':
t.Buffer.Insert(t.cury, t.curx, []byte{'\n'})
t.SetLineCol(t.cury+1, 0) // Go to the start of that new line
t.Buffer.Insert(cursLine, cursCol, []byte{'\n'})
lineInserted = true
case '\b':
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...
// Insert spaces
spaces := strings.Repeat(" ", t.TabSize)
t.Buffer.Insert(t.cury, t.curx, []byte(spaces))
t.SetLineCol(t.cury, t.curx+len(spaces)) // Advance the cursor
t.Buffer.Insert(cursLine, cursCol, []byte(spaces))
break
}
fallthrough // Append the \t character
default:
// Insert character into line
t.Buffer.Insert(t.cury, t.curx, []byte(string(ch)))
t.SetLineCol(t.cury, t.curx+1) // Advance the cursor
t.Buffer.Insert(cursLine, cursCol, []byte(string(ch)))
// t.SetLineCol(t.cury, t.curx+1) // Advance the cursor
}
}
t.prevCurCol = t.curx
t.ScrollToCursor()
t.updateCursorVisibility()
if lineInserted {
t.Highlighter.InvalidateLines(startingLine, t.Buffer.Lines()-1)
@ -262,34 +258,24 @@ func (t *TextEdit) getTabCountInLineAtCol(line, col int) int {
return 0
}
// GetLineCol returns (line, col) of the cursor. Zero is origin for both.
func (t *TextEdit) GetLineCol() (int, int) {
return t.cury, t.curx
}
// The same as updateTerminalCursor but the caller can provide the tabOffset to
// save the original function a calculation.
func (t *TextEdit) updateTerminalCursorNoHelper(columnWidth, tabOffset int) {
(*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() {
// updateCursorVisibility sets the position of the terminal's cursor with the
// cursor of the TextEdit. Sends a signal to show the cursor if the TextEdit
// is focused and not in select mode.
func (t *TextEdit) updateCursorVisibility() {
if t.focused && !t.selectMode {
columnWidth := t.getColumnWidth()
tabOffset := t.getTabCountInLineAtCol(t.cury, t.curx) * (t.TabSize - 1)
t.updateTerminalCursorNoHelper(columnWidth, tabOffset)
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)
}
}
// 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)
// Scroll the screen if the cursor is out of view.
func (t *TextEdit) ScrollToCursor() {
line, col := t.cursor.GetLineCol()
// 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
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
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)
}
func (t *TextEdit) GetCursor() buffer.Cursor {
return t.cursor
}
// CursorUp moves the cursor up a line.
func (t *TextEdit) CursorUp() {
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) CursorDown() {
if t.cury >= t.Buffer.Lines()-1 { // If the cursor is at the last line...
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
func (t *TextEdit) SetCursor(newCursor buffer.Cursor) {
t.cursor = newCursor
t.updateCursorVisibility()
}
// 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 {
// TODO: there's a bug with copying text
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{}
}
@ -486,23 +421,26 @@ func (t *TextEdit) Draw(s tcell.Screen) {
r = ' '
}
startLine, startCol := t.selection.Start()
endLine, endCol := t.selection.End()
// Determine whether we select the current rune. Also only select runes within
// 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)
if line == t.selection.StartLine { // If selection starts at this line...
if _origRuneIdx >= t.selection.StartCol { // And we're at or past the start col...
if line == startLine { // If selection starts at this line...
if _origRuneIdx >= startCol { // And we're at or past the start col...
// If the start line is also the end line...
if line == t.selection.EndLine {
if _origRuneIdx <= t.selection.EndCol { // And we're before the end of that...
if line == endLine {
if _origRuneIdx <= endCol { // And we're before the end of that...
selected = true
}
} else { // Definitely highlight
selected = true
}
}
} else if line == t.selection.EndLine { // If selection ends at this line...
if _origRuneIdx <= t.selection.EndCol { // And we're at or before the end col...
} else if line == endLine { // If selection ends at this line...
if _origRuneIdx <= endCol { // And we're at or before the end col...
selected = true
}
} 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
}
// Update cursor
t.SetLineCol(t.cury, t.curx)
t.updateCursorVisibility()
}
// 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) {
t.focused = v
if v {
t.updateTerminalCursor()
t.updateCursorVisibility()
} else {
(*t.screen).HideCursor()
}
@ -571,95 +508,105 @@ func (t *TextEdit) HandleEvent(event tcell.Event) bool {
// Cursor movement
case tcell.KeyUp:
if ev.Modifiers() == tcell.ModShift {
if !t.selectMode {
t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
t.selectMode = true
} else {
prevCurX, prevCurY := t.curx, t.cury
t.CursorUp()
// Grow the selection in the correct direction
if prevCurY <= t.selection.StartLine && prevCurX <= t.selection.StartCol {
t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
} else {
t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
}
}
// if !t.selectMode {
// t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
// t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
// t.selectMode = true
// } else {
// prevCurX, prevCurY := t.curx, t.cury
// t.CursorUp()
// // Grow the selection in the correct direction
// if prevCurY <= t.selection.StartLine && prevCurX <= t.selection.StartCol {
// t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
// } else {
// t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
// }
// }
} else {
t.selectMode = false
t.CursorUp()
t.SetCursor(t.cursor.Up())
t.ScrollToCursor()
}
case tcell.KeyDown:
if ev.Modifiers() == tcell.ModShift {
if !t.selectMode {
t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
t.selectMode = true
} else {
prevCurX, prevCurY := t.curx, t.cury
t.CursorDown()
if prevCurY >= t.selection.EndLine && prevCurX >= t.selection.EndCol {
t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
} else {
t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
}
}
// if !t.selectMode {
// t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
// t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
// t.selectMode = true
// } else {
// prevCurX, prevCurY := t.curx, t.cury
// t.CursorDown()
// if prevCurY >= t.selection.EndLine && prevCurX >= t.selection.EndCol {
// t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
// } else {
// t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
// }
// }
} else {
t.selectMode = false
t.CursorDown()
t.SetCursor(t.cursor.Down())
t.ScrollToCursor()
}
case tcell.KeyLeft:
if ev.Modifiers() == tcell.ModShift {
if !t.selectMode {
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.EndLine, t.selection.EndCol = t.cury, t.curx
t.selectMode = true
} else {
prevCurX, prevCurY := t.curx, t.cury
t.CursorLeft()
if prevCurY == t.selection.StartLine && prevCurX == t.selection.StartCol { // We are moving the start...
t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
} else {
t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
}
}
// if !t.selectMode {
// 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.EndLine, t.selection.EndCol = t.cury, t.curx
// t.selectMode = true
// } else {
// prevCurX, prevCurY := t.curx, t.cury
// t.CursorLeft()
// if prevCurY == t.selection.StartLine && prevCurX == t.selection.StartCol { // We are moving the start...
// t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
// } else {
// t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
// }
// }
} else {
t.selectMode = false
t.CursorLeft()
t.SetCursor(t.cursor.Left())
t.ScrollToCursor()
}
case tcell.KeyRight:
if ev.Modifiers() == tcell.ModShift {
if !t.selectMode { // If we are not already selecting...
// Reset the selection to cursor pos
t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
t.selectMode = true
} else {
prevCurX, prevCurY := t.curx, t.cury
t.CursorRight() // Advance the cursor
if prevCurY == t.selection.EndLine && prevCurX == t.selection.EndCol {
t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
} else {
t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
}
}
// if !t.selectMode { // If we are not already selecting...
// // Reset the selection to cursor pos
// t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
// t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
// t.selectMode = true
// } else {
// prevCurX, prevCurY := t.curx, t.cury
// t.CursorRight() // Advance the cursor
// if prevCurY == t.selection.EndLine && prevCurX == t.selection.EndCol {
// t.selection.EndLine, t.selection.EndCol = t.cury, t.curx
// } else {
// t.selection.StartLine, t.selection.StartCol = t.cury, t.curx
// }
// }
} else {
t.selectMode = false
t.CursorRight()
t.SetCursor(t.cursor.Right())
t.ScrollToCursor()
}
case tcell.KeyHome:
t.SetLineCol(t.cury, 0)
t.prevCurCol = t.curx
cursLine, _ := t.cursor.GetLineCol()
// 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:
t.SetLineCol(t.cury, math.MaxInt32) // Max column
t.prevCurCol = t.curx
cursLine, _ := t.cursor.GetLineCol()
t.SetCursor(t.cursor.SetLineCol(cursLine, math.MaxInt32)) // Max column
t.ScrollToCursor()
case tcell.KeyPgUp:
t.SetLineCol(t.scrolly-t.height, t.curx) // Go a page up
t.prevCurCol = t.curx
_, cursCol := t.cursor.GetLineCol()
t.SetCursor(t.cursor.SetLineCol(t.scrolly-t.height, cursCol)) // Go a page up
t.ScrollToCursor()
case tcell.KeyPgDn:
t.SetLineCol(t.scrolly+t.height*2-1, t.curx) // Go a page down
t.prevCurCol = t.curx
_, cursCol := t.cursor.GetLineCol()
t.SetCursor(t.cursor.SetLineCol(t.scrolly+t.height*2-1, cursCol)) // Go a page down
t.ScrollToCursor()
// Deleting
case tcell.KeyBackspace: