From d96c2f6f030e0774f861953b71e7ffebf5d27281 Mon Sep 17 00:00:00 2001 From: Luke Wilson Date: Mon, 12 Apr 2021 17:38:29 -0500 Subject: [PATCH] Refactored cursor architecture into buffer module --- main.go | 6 +- ui/buffer/buffer.go | 12 ++ ui/buffer/cursor.go | 107 ++++++++++++++ ui/buffer/rope.go | 105 ++++++++------ ui/buffer/util.go | 23 +++ ui/textedit.go | 341 +++++++++++++++++++------------------------- 6 files changed, 352 insertions(+), 242 deletions(-) mode change 100644 => 100755 ui/buffer/buffer.go create mode 100755 ui/buffer/cursor.go mode change 100644 => 100755 ui/buffer/rope.go create mode 100644 ui/buffer/util.go mode change 100644 => 100755 ui/textedit.go diff --git a/main.go b/main.go index e716e79..eb5f012 100644 --- a/main.go +++ b/main.go @@ -301,7 +301,7 @@ func main() { panelContainer.SelectPrev() changeFocus(panelContainer) }}, &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 Left", QuickChar: -1, Shortcut: "Alt+Left", Callback: func() { @@ -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 { diff --git a/ui/buffer/buffer.go b/ui/buffer/buffer.go old mode 100644 new mode 100755 index 8a6998d..284b2ab --- a/ui/buffer/buffer.go +++ b/ui/buffer/buffer.go @@ -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) } diff --git a/ui/buffer/cursor.go b/ui/buffer/cursor.go new file mode 100755 index 0000000..5ed20e3 --- /dev/null +++ b/ui/buffer/cursor.go @@ -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 +} diff --git a/ui/buffer/rope.go b/ui/buffer/rope.go old mode 100644 new mode 100755 index d92978d..bb0238b --- a/ui/buffer/rope.go +++ b/ui/buffer/rope.go @@ -4,13 +4,19 @@ import ( "io" "unicode/utf8" - "github.com/zyedidia/rope" + ropes "github.com/zyedidia/rope" ) -type RopeBuffer rope.Node +type RopeBuffer struct { + rope *ropes.Node + anchors []*Cursor +} func NewRopeBuffer(contents []byte) *RopeBuffer { - return (*RopeBuffer)(rope.New(contents)) + return &RopeBuffer{ + ropes.New(contents), + nil, + } } // LineColToPos returns the index of the byte at line, col. If line is less than @@ -19,20 +25,16 @@ func NewRopeBuffer(contents []byte) *RopeBuffer { // length of the line, the position of the last byte of the line is returned, // instead. func (b *RopeBuffer) LineColToPos(line, col int) int { - var pos int - - _rope := (*rope.Node)(b) - - pos = b.getLineStartPos(line) + pos := b.getLineStartPos(line) // Have to do this algorithm for safety. If this function was declared to panic // or index out of bounds memory, if col > the given line length, it would be // more efficient and simpler. But unfortunately, I believe it is necessary. if col > 0 { - _, r := _rope.SplitAt(pos) - l, _ := r.SplitAt(_rope.Len() - pos) + _, r := b.rope.SplitAt(pos) + l, _ := r.SplitAt(b.rope.Len() - pos) - l.EachLeaf(func(n *rope.Node) bool { + l.EachLeaf(func(n *ropes.Node) bool { data := n.Value() // Reference; not a copy. var i int for i < len(data) { @@ -60,12 +62,11 @@ func (b *RopeBuffer) Line(line int) []byte { pos := b.getLineStartPos(line) bytes := 0 - _rope := (*rope.Node)(b) - _, r := _rope.SplitAt(pos) - l, _ := r.SplitAt(_rope.Len() - pos) + _, r := b.rope.SplitAt(pos) + l, _ := r.SplitAt(b.rope.Len() - pos) var isCRLF bool // true if the last byte was '\r' - l.EachLeaf(func(n *rope.Node) bool { + l.EachLeaf(func(n *ropes.Node) bool { data := n.Value() // Reference; not a copy. var i int for i < len(data) { @@ -90,7 +91,7 @@ func (b *RopeBuffer) Line(line int) []byte { return false // Have not read the whole line, yet }) - return _rope.Slice(pos, pos+bytes) // NOTE: may be faster to do it ourselves + return b.rope.Slice(pos, pos+bytes) // NOTE: may be faster to do it ourselves } // Returns a slice of the buffer from startLine, startCol, to endLine, endCol, @@ -98,22 +99,22 @@ func (b *RopeBuffer) Line(line int) []byte { // so do not write to it. func (b *RopeBuffer) Slice(startLine, startCol, endLine, endCol int) []byte { endPos := b.LineColToPos(endLine, endCol) - if length := (*rope.Node)(b).Len(); endPos >= length { + if length := b.rope.Len(); endPos >= length { endPos = length - 1 } - return (*rope.Node)(b).Slice(b.LineColToPos(startLine, startCol), endPos+1) + return b.rope.Slice(b.LineColToPos(startLine, startCol), endPos+1) } // Bytes returns all of the bytes in the buffer. This function is very likely // to copy all of the data in the buffer. Use sparingly. Try using other methods, // where possible. func (b *RopeBuffer) Bytes() []byte { - return (*rope.Node)(b).Value() + return b.rope.Value() } // Insert copies a byte slice (inserting it) into the position at line, col. func (b *RopeBuffer) Insert(line, col int, value []byte) { - (*rope.Node)(b).Insert(b.LineColToPos(line, col), value) + b.rope.Insert(b.LineColToPos(line, col), value) } // Remove deletes any characters between startLine, startCol, and endLine, @@ -122,14 +123,14 @@ func (b *RopeBuffer) Remove(startLine, startCol, endLine, endCol int) { start := b.LineColToPos(startLine, startCol) end := b.LineColToPos(endLine, endCol) + 1 - if len := (*rope.Node)(b).Len(); end >= len { + if len := b.rope.Len(); end >= len { end = len if start > end { start = end } } - (*rope.Node)(b).Remove(start, end) + b.rope.Remove(start, end) } // Returns the number of occurrences of 'sequence' in the buffer, within the range @@ -137,19 +138,19 @@ func (b *RopeBuffer) Remove(startLine, startCol, endLine, endCol int) { func (b *RopeBuffer) Count(startLine, startCol, endLine, endCol int, sequence []byte) int { startPos := b.LineColToPos(startLine, startCol) endPos := b.LineColToPos(endLine, endCol) - return (*rope.Node)(b).Count(startPos, endPos, sequence) + return b.rope.Count(startPos, endPos, sequence) } // Len returns the number of bytes in the buffer. func (b *RopeBuffer) Len() int { - return (*rope.Node)(b).Len() + return b.rope.Len() } // Lines returns the number of lines in the buffer. If the buffer is empty, // 1 is returned, because there is always at least one line. This function // basically counts the number of newline ('\n') characters in a buffer. func (b *RopeBuffer) Lines() int { - rope := (*rope.Node)(b) + rope := b.rope return rope.Count(0, rope.Len(), []byte{'\n'}) + 1 } @@ -158,17 +159,13 @@ func (b *RopeBuffer) Lines() int { // which means the byte is on the last, and empty, line of the buffer. If line is greater // than or equal to the number of lines in the buffer, a panic is issued. func (b *RopeBuffer) getLineStartPos(line int) int { - _rope := (*rope.Node)(b) var pos int if line > 0 { - _rope.IndexAllFunc(0, _rope.Len(), []byte{'\n'}, func(idx int) bool { + b.rope.IndexAllFunc(0, b.rope.Len(), []byte{'\n'}, func(idx int) bool { line-- - pos = idx + 1 // idx+1 = start of line after delimiter - if line <= 0 { // If pos is now the start of the line we're searching for... - return true // Stop indexing - } - return false + pos = idx + 1 // idx+1 = start of line after delimiter + return line <= 0 // If pos is now the start of the line we're searching for }) } @@ -185,8 +182,7 @@ func (b *RopeBuffer) getLineStartPos(line int) int { func (b *RopeBuffer) RunesInLineWithDelim(line int) int { linePos := b.getLineStartPos(line) - _rope := (*rope.Node)(b) - ropeLen := _rope.Len() + ropeLen := b.rope.Len() if linePos >= ropeLen { return 0 @@ -194,11 +190,11 @@ func (b *RopeBuffer) RunesInLineWithDelim(line int) int { var count int - _, r := _rope.SplitAt(linePos) + _, r := b.rope.SplitAt(linePos) l, _ := r.SplitAt(ropeLen - linePos) var isCRLF bool - l.EachLeaf(func(n *rope.Node) bool { + l.EachLeaf(func(n *ropes.Node) bool { data := n.Value() // Reference; not a copy. var i int for i < len(data) { @@ -229,8 +225,7 @@ func (b *RopeBuffer) RunesInLineWithDelim(line int) int { func (b *RopeBuffer) RunesInLine(line int) int { linePos := b.getLineStartPos(line) - _rope := (*rope.Node)(b) - ropeLen := _rope.Len() + ropeLen := b.rope.Len() if linePos >= ropeLen { return 0 @@ -238,11 +233,11 @@ func (b *RopeBuffer) RunesInLine(line int) int { var count int - _, r := _rope.SplitAt(linePos) + _, r := b.rope.SplitAt(linePos) l, _ := r.SplitAt(ropeLen - linePos) var isCRLF bool - l.EachLeaf(func(n *rope.Node) bool { + l.EachLeaf(func(n *ropes.Node) bool { data := n.Value() // Reference; not a copy. var i int for i < len(data) { @@ -299,7 +294,7 @@ func (b *RopeBuffer) PosToLineCol(pos int) (int, int) { return line, col } - (*rope.Node)(b).EachLeaf(func(n *rope.Node) bool { + b.rope.EachLeaf(func(n *ropes.Node) bool { data := n.Value() var i int for i < len(data) { @@ -330,5 +325,31 @@ func (b *RopeBuffer) PosToLineCol(pos int) (int, int) { } func (b *RopeBuffer) WriteTo(w io.Writer) (int64, error) { - return (*rope.Node)(b).WriteTo(w) + return b.rope.WriteTo(w) +} + +// RegisterCursor adds the Cursor to a slice which the Buffer uses to update +// each Cursor based on changes that occur in the Buffer. Various functions are +// called on the Cursor depending upon where the edits occurred and how it should +// modify the Cursor's position. Unregister a Cursor before deleting it from +// memory, or forgetting it, with UnregisterPosition. +func (b *RopeBuffer) RegisterCursor(cursor *Cursor) { + if cursor == nil { + return + } + b.anchors = append(b.anchors, cursor) +} + +// UnregisterCursor will remove the cursor from the list of watched Cursors. +// It is mandatory that a Cursor be unregistered before being freed from memory, +// or otherwise being forgotten. +func (b *RopeBuffer) UnregisterCursor(cursor *Cursor) { + for i, v := range b.anchors { + if cursor == v { + // Delete item at i without preserving order + b.anchors[i] = b.anchors[len(b.anchors)-1] + b.anchors[len(b.anchors)-1] = nil + b.anchors = b.anchors[:len(b.anchors)-1] + } + } } diff --git a/ui/buffer/util.go b/ui/buffer/util.go new file mode 100644 index 0000000..454f4c1 --- /dev/null +++ b/ui/buffer/util.go @@ -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)) +} diff --git a/ui/textedit.go b/ui/textedit.go old mode 100644 new mode 100755 index c58efce..75a2584 --- a/ui/textedit.go +++ b/ui/textedit.go @@ -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,13 +28,12 @@ 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 - selectMode bool // Whether the user is actively selecting text + 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 +// 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() + 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 -// 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() { - 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) +// 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) - } } -// 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) - } +func (t *TextEdit) GetCursor() buffer.Cursor { + return t.cursor } -// 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: