Add and replace []string buffer with ropes

This commit is contained in:
Luke I. Wilson 2021-03-23 15:14:23 -05:00
parent bd6f1cf79e
commit 40e1b40672
5 changed files with 228 additions and 217 deletions

24
main.go
View File

@ -77,7 +77,7 @@ func main() {
} }
} }
textEdit := ui.NewTextEdit(&s, os.Args[i], string(bytes), &theme) textEdit := ui.NewTextEdit(&s, os.Args[i], bytes, &theme)
textEdit.Dirty = dirty textEdit.Dirty = dirty
tabContainer.AddTab(os.Args[i], textEdit) tabContainer.AddTab(os.Args[i], textEdit)
} }
@ -88,7 +88,7 @@ func main() {
fileMenu := ui.NewMenu("_File", &theme) fileMenu := ui.NewMenu("_File", &theme)
fileMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "_New File", Shortcut: "Ctrl+N", Callback: func() { fileMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "_New File", Shortcut: "Ctrl+N", Callback: func() {
textEdit := ui.NewTextEdit(&s, "", "", &theme) // No file path, no contents textEdit := ui.NewTextEdit(&s, "", []byte{}, &theme) // No file path, no contents
tabContainer.AddTab("noname", textEdit) tabContainer.AddTab("noname", textEdit)
}}, &ui.ItemEntry{Name: "_Open...", Shortcut: "Ctrl+O", Callback: func() { }}, &ui.ItemEntry{Name: "_Open...", Shortcut: "Ctrl+O", Callback: func() {
callback := func(filePaths []string) { callback := func(filePaths []string) {
@ -104,7 +104,7 @@ func main() {
panic("Could not read all of file") panic("Could not read all of file")
} }
textEdit := ui.NewTextEdit(&s, path, string(bytes), &theme) textEdit := ui.NewTextEdit(&s, path, bytes, &theme)
tabContainer.AddTab(path, textEdit) tabContainer.AddTab(path, textEdit)
} }
// TODO: free the dialog instead? // TODO: free the dialog instead?
@ -129,14 +129,12 @@ func main() {
tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
te := tab.Child.(*ui.TextEdit) te := tab.Child.(*ui.TextEdit)
if len(te.FilePath) > 0 { if len(te.FilePath) > 0 {
contents := te.String()
// Write the contents into the file, creating one if it does // Write the contents into the file, creating one if it does
// not exist. // not exist.
err := ioutil.WriteFile(te.FilePath, []byte(contents), fs.ModePerm) err := ioutil.WriteFile(te.FilePath, te.Buffer.Bytes(), fs.ModePerm)
if err != nil { if err != nil {
panic("Could not write file at path " + te.FilePath) panic("Could not write file at path " + te.FilePath)
} } // TODO: Replace with io.Writer method
te.Dirty = false te.Dirty = false
} }
@ -202,11 +200,11 @@ func main() {
if tabContainer.GetTabCount() > 0 { if tabContainer.GetTabCount() > 0 {
tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
te := tab.Child.(*ui.TextEdit) te := tab.Child.(*ui.TextEdit)
selectedStr := te.GetSelectedString() bytes := te.GetSelectedBytes()
if selectedStr != "" { // If something is selected... if len(bytes) > 0 { // If something is selected...
te.Delete(false) // Delete the selection te.Delete(false) // Delete the selection
// TODO: better error handling within editor // TODO: better error handling within editor
_ = ClipWrite(selectedStr) // Add the selectedStr to clipboard _ = ClipWrite(string(bytes)) // Add the selectedStr to clipboard
} }
changeFocus(tabContainer) changeFocus(tabContainer)
} }
@ -214,9 +212,9 @@ func main() {
if tabContainer.GetTabCount() > 0 { if tabContainer.GetTabCount() > 0 {
tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
te := tab.Child.(*ui.TextEdit) te := tab.Child.(*ui.TextEdit)
selectedStr := te.GetSelectedString() bytes := te.GetSelectedBytes()
if selectedStr != "" { // If there is something selected... if len(bytes) > 0 { // If there is something selected...
_ = ClipWrite(selectedStr) // Add selectedStr to clipboard _ = ClipWrite(string(bytes)) // Add selectedStr to clipboard
} }
changeFocus(tabContainer) changeFocus(tabContainer)
} }

View File

@ -35,6 +35,10 @@ type Buffer interface {
// endCol, inclusive bounds. // endCol, inclusive bounds.
Remove(startLine, startCol, endLine, endCol int) Remove(startLine, startCol, endLine, endCol int)
// Returns the number of occurrences of 'sequence' in the buffer, within the range
// of start line and col, to end line and col. [start, end) (exclusive end).
Count(startLine, startCol, endLine, endCol int, sequence []byte) int
// Len returns the number of bytes in the buffer. // Len returns the number of bytes in the buffer.
Len() int Len() int
@ -43,13 +47,19 @@ type Buffer interface {
// basically counts the number of newline ('\n') characters in a buffer. // basically counts the number of newline ('\n') characters in a buffer.
Lines() int Lines() int
// ColsInLine returns the number of columns in the given line. That is, the // RunesInLine returns the number of runes in the given line. That is, the
// number of Utf-8 codepoints (or runes) in line, not bytes. // number of Utf-8 codepoints in the line, not bytes. Includes the line delimiter
// in the count. If that line delimiter is CRLF ('\r\n'), then it adds two.
RunesInLineWithDelim(line int) int
// RunesInLine returns the number of runes in the given line. That is, the
// number of Utf-8 codepoints in the line, not bytes. Excludes line delimiters.
RunesInLine(line int) int RunesInLine(line int) int
// ClampLineCol is a utility function to clamp any provided line and col to // ClampLineCol is a utility function to clamp any provided line and col to
// only possible values within the buffer. It first clamps the line, then clamps // only possible values within the buffer, pointing to runes. It first clamps
// the column within that line. // the line, then clamps the column. The column is clamped between zero and
// the last rune before the line delimiter.
ClampLineCol(line, col int) (int, int) ClampLineCol(line, col int) (int, int)
WriteTo(w io.Writer) (int64, error) WriteTo(w io.Writer) (int64, error)

View File

@ -63,13 +63,22 @@ func (b *RopeBuffer) Line(line int) []byte {
_, r := _rope.SplitAt(pos) _, r := _rope.SplitAt(pos)
l, _ := r.SplitAt(_rope.Len() - pos) l, _ := r.SplitAt(_rope.Len() - pos)
var isCRLF bool // true if the last byte was '\r'
l.EachLeaf(func(n *rope.Node) bool { l.EachLeaf(func(n *rope.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) {
if data[i] == '\n' { if data[i] == '\r' {
bytes++ // Add the newline byte isCRLF = true
} else if data[i] == '\n' {
if isCRLF {
bytes += 2 // Add the CRLF bytes
} else {
bytes += 1 // Add LF byte
}
return true // Read (past-tense) the whole line return true // Read (past-tense) the whole line
} else {
isCRLF = false
} }
// Respect Utf-8 codepoint boundaries // Respect Utf-8 codepoint boundaries
@ -108,6 +117,14 @@ func (b *RopeBuffer) Remove(startLine, startCol, endLine, endCol int) {
(*rope.Node)(b).Remove(b.pos(startLine, startCol), b.pos(endLine, endCol)+1) (*rope.Node)(b).Remove(b.pos(startLine, startCol), b.pos(endLine, endCol)+1)
} }
// Returns the number of occurrences of 'sequence' in the buffer, within the range
// of start line and col, to end line and col. End is exclusive.
func (b *RopeBuffer) Count(startLine, startCol, endLine, endCol int, sequence []byte) int {
startPos := b.pos(startLine, startCol)
endPos := b.pos(endLine, endCol)
return (*rope.Node)(b).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 (*rope.Node)(b).Len()
@ -121,41 +138,6 @@ func (b *RopeBuffer) Lines() int {
return rope.Count(0, rope.Len(), []byte{'\n'}) + 1 return rope.Count(0, rope.Len(), []byte{'\n'}) + 1
} }
// pos is the first byte index in the line we want to count runes/cols. pos can point to
// a newline or be greater than or equal to the number of bytes in the buffer, and will
// return 0 for both cases.
func (b *RopeBuffer) runesInLine(pos int) int {
_rope := (*rope.Node)(b)
ropeLen := _rope.Len()
if pos >= ropeLen {
return 0
}
var count int
_, r := _rope.SplitAt(pos)
l, _ := r.SplitAt(ropeLen - pos)
l.EachLeaf(func(n *rope.Node) bool {
data := n.Value() // Reference; not a copy.
var i int
for i < len(data) {
if data[i] == '\n' {
return true // Read (past-tense) the whole line
}
count++
// Respect Utf-8 codepoint boundaries
_, size := utf8.DecodeRune(data[i:])
i += size
}
return false // Have not read the whole line, yet
})
return count
}
// getLineStartPos returns the first byte index of the given line (starting from zero). // getLineStartPos returns the first byte index of the given line (starting from zero).
// The returned index can be equal to the length of the buffer, not pointing to any byte, // The returned index can be equal to the length of the buffer, not pointing to any byte,
// 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
@ -182,16 +164,99 @@ func (b *RopeBuffer) getLineStartPos(line int) int {
return pos return pos
} }
// RunesInLineWithDelim returns the number of runes in the given line. That is, the
// number of Utf-8 codepoints in the line, not bytes. Includes the line delimiter
// in the count. If that line delimiter is CRLF ('\r\n'), then it adds two.
func (b *RopeBuffer) RunesInLineWithDelim(line int) int {
linePos := b.getLineStartPos(line)
_rope := (*rope.Node)(b)
ropeLen := _rope.Len()
if linePos >= ropeLen {
return 0
}
var count int
_, r := _rope.SplitAt(linePos)
l, _ := r.SplitAt(ropeLen - linePos)
var isCRLF bool
l.EachLeaf(func(n *rope.Node) bool {
data := n.Value() // Reference; not a copy.
var i int
for i < len(data) {
count++ // Before: we count the line delimiter
if data[i] == '\r' {
isCRLF = true
} else if data[i] == '\n' {
return true // Read (past-tense) the whole line
} else {
if isCRLF {
isCRLF = false
count++ // Add the '\r' we previously thought was part of the delim.
}
}
// Respect Utf-8 codepoint boundaries
_, size := utf8.DecodeRune(data[i:])
i += size
}
return false // Have not read the whole line, yet
})
return count
}
// RunesInLine returns the number of runes in the given line. That is, the // RunesInLine returns the number of runes in the given line. That is, the
// number of Utf-8 codepoints in the line, not bytes. // number of Utf-8 codepoints in the line, not bytes. Excludes line delimiters.
func (b *RopeBuffer) RunesInLine(line int) int { func (b *RopeBuffer) RunesInLine(line int) int {
linePos := b.getLineStartPos(line) linePos := b.getLineStartPos(line)
return b.runesInLine(linePos)
_rope := (*rope.Node)(b)
ropeLen := _rope.Len()
if linePos >= ropeLen {
return 0
}
var count int
_, r := _rope.SplitAt(linePos)
l, _ := r.SplitAt(ropeLen - linePos)
var isCRLF bool
l.EachLeaf(func(n *rope.Node) bool {
data := n.Value() // Reference; not a copy.
var i int
for i < len(data) {
if data[i] == '\r' {
isCRLF = true
} else if data[i] == '\n' {
return true // Read (past-tense) the whole line
} else {
if isCRLF {
isCRLF = false
count++ // Add the '\r' we previously thought was part of the delim.
}
}
count++
// Respect Utf-8 codepoint boundaries
_, size := utf8.DecodeRune(data[i:])
i += size
}
return false // Have not read the whole line, yet
})
return count
} }
// ClampLineCol is a utility function to clamp any provided line and col to // ClampLineCol is a utility function to clamp any provided line and col to
// only possible values within the buffer, pointing to runes. It first clamps // only possible values within the buffer, pointing to runes. It first clamps
// the line, then clamps the column. // the line, then clamps the column. The column is clamped between zero and
// the last rune before the line delimiter.
func (b *RopeBuffer) ClampLineCol(line, col int) (int, int) { func (b *RopeBuffer) ClampLineCol(line, col int) (int, int) {
if line < 0 { if line < 0 {
line = 0 line = 0
@ -201,8 +266,8 @@ func (b *RopeBuffer) ClampLineCol(line, col int) (int, int) {
if col < 0 { if col < 0 {
col = 0 col = 0
} else if cols := b.RunesInLine(line)-1; col > cols { } else if runes := b.RunesInLine(line); col > runes {
col = cols col = runes
} }
return line, col return line, col

View File

@ -34,14 +34,52 @@ func TestRopeBounds(t *testing.T) {
t.Fail() t.Fail()
} }
if len := buf.RunesInLineWithDelim(4); len != 0 {
t.Errorf("Expected 0 runes in line 5, found %v", len)
t.Fail()
}
line, col := buf.ClampLineCol(15, 5) // Should become last line, first column line, col := buf.ClampLineCol(15, 5) // Should become last line, first column
if line != 4 && col != 0 { if line != 4 && col != 0 {
t.Errorf("Expected to clamp line col to 4,0 got %v,%v", line, col) t.Errorf("Expected to clamp line col to 4,0 got %v,%v", line, col)
t.Fail() t.Fail()
} }
line, col = buf.ClampLineCol(4, -1)
if line != 4 && col != 0 {
t.Errorf("Expected to clamp line col to 4,0 got %v,%v", line, col)
t.Fail()
}
line, col = buf.ClampLineCol(2, 5) // Should be third line, pointing at the newline char
if line != 2 && col != 5 {
t.Errorf("Expected to clamp line, col to 2,5 got %v,%v", line, col)
t.Fail()
}
if line := string(buf.Line(2)); line != "\tsome\n" { if line := string(buf.Line(2)); line != "\tsome\n" {
t.Errorf("Expected line 3 to equal \"\\tsome\", got %#v", line) t.Errorf("Expected line 3 to equal \"\\tsome\", got %#v", line)
t.Fail() t.Fail()
} }
if line := string(buf.Line(4)); line != "" {
t.Errorf("Got %#v", line)
t.Fail()
}
}
func TestRopeCount(t *testing.T) {
var buf Buffer = NewRopeBuffer([]byte("\t\tlot of\n\ttabs"))
tabsAtOf := buf.Count(0, 0, 0, 7, []byte{'\t'})
if tabsAtOf != 2 {
t.Errorf("Expected 2 tabs before 'of', got %#v", tabsAtOf)
t.Fail()
}
tabs := buf.Count(0, 0, 0, 0, []byte{'\t'})
if tabs != 0 {
t.Errorf("Expected no tabs at column zero, got %v", tabs)
t.Fail()
}
} }

View File

@ -48,9 +48,9 @@ type TextEdit struct {
Theme *Theme Theme *Theme
} }
// New will initialize the buffer using the given string `contents`. If the `filePath` or `FilePath` is empty, // New will initialize the buffer using the given 'contents'. If the 'filePath' or 'FilePath' is empty,
// it can be assumed that the TextEdit has no file association, or it is unsaved. // it can be assumed that the TextEdit has no file association, or it is unsaved.
func NewTextEdit(screen *tcell.Screen, filePath, contents string, theme *Theme) *TextEdit { func NewTextEdit(screen *tcell.Screen, filePath string, contents []byte, theme *Theme) *TextEdit {
te := &TextEdit{ te := &TextEdit{
Buffer: nil, Buffer: nil,
LineNumbers: true, LineNumbers: true,
@ -66,11 +66,11 @@ func NewTextEdit(screen *tcell.Screen, filePath, contents string, theme *Theme)
// SetContents applies the string to the internal buffer of the TextEdit component. // SetContents applies the string to the internal buffer of the TextEdit component.
// The string is determined to be either CRLF or LF based on line-endings. // The string is determined to be either CRLF or LF based on line-endings.
func (t *TextEdit) SetContents(contents string) { func (t *TextEdit) SetContents(contents []byte) {
var i int var i int
loop: loop:
for i < len(contents) { for i < len(contents) {
switch r { switch contents[i] {
case '\n': case '\n':
t.IsCRLF = false t.IsCRLF = false
break loop break loop
@ -79,7 +79,8 @@ loop:
t.IsCRLF = true t.IsCRLF = true
break loop break loop
} }
i += utf8.DecodeRune(contents[i:]) _, size := utf8.DecodeRune(contents[i:])
i += size
} }
t.Buffer = buffer.NewRopeBuffer(contents) t.Buffer = buffer.NewRopeBuffer(contents)
@ -94,10 +95,6 @@ func (t *TextEdit) GetLineDelimiter() string {
} }
} }
func (t *TextEdit) String() string {
return strings.Join(t.buffer, t.GetLineDelimiter())
}
// Changes a file's line delimiters. If `crlf` is true, then line delimiters are replaced // Changes a file's line delimiters. If `crlf` is true, then line delimiters are replaced
// with Windows CRLF (\r\n). If `crlf` is false, then line delimtiers are replaced with Unix // with Windows CRLF (\r\n). If `crlf` is false, then line delimtiers are replaced with Unix
// LF (\n). The TextEdit `IsCRLF` variable is updated with the new value. // LF (\n). The TextEdit `IsCRLF` variable is updated with the new value.
@ -105,6 +102,8 @@ func (t *TextEdit) ChangeLineDelimiters(crlf bool) {
t.IsCRLF = crlf t.IsCRLF = crlf
t.Dirty = true t.Dirty = true
// line delimiters are constructed with String() function // line delimiters are constructed with String() function
// TODO: ^ not true anymore ^
panic("Cannot ChangeLineDelimiters")
} }
// Delete with `forwards` false will backspace, destroying the character before the cursor, // Delete with `forwards` false will backspace, destroying the character before the cursor,
@ -114,59 +113,26 @@ func (t *TextEdit) Delete(forwards bool) {
t.Dirty = true t.Dirty = true
if t.selectMode { // If text is selected, delete the whole selection if t.selectMode { // If text is selected, delete the whole selection
t.cury, t.curx = t.clampLineCol(t.selection.EndLine, t.selection.EndCol) t.cury, t.curx = t.Buffer.ClampLineCol(t.selection.EndLine, t.selection.EndCol)
t.selectMode = false // Disable selection and prevent infinite loop t.selectMode = false // Disable selection and prevent infinite loop
t.Delete(true) // Delete last character of selection first // Delete the region
// Delete from end, backwards, until we are at the start of the selection t.Buffer.Remove(t.selection.StartLine, t.selection.StartCol, t.selection.EndLine, t.selection.EndCol)
for { // TODO: inefficient t.SetLineCol(t.selection.StartLine, t.selection.StartCol) // Set cursor to start of region
if t.cury == t.selection.StartLine && t.curx == t.selection.StartCol {
break
}
t.Delete(false) // NOTE: we want to delete start column as well.
}
return return
} }
// TODO: deleting through lines
if forwards { // Delete the character after the cursor if forwards { // Delete the character after the cursor
if t.curx < len(t.buffer[t.cury]) { // If the cursor is not at the end of the line... // If the cursor is not at the end of the last line...
lineRunes := []rune(t.buffer[t.cury]) if t.cury < t.Buffer.Lines()-1 || t.curx < t.Buffer.RunesInLine(t.cury) {
copy(lineRunes[t.curx:], lineRunes[t.curx+1:]) // Shift runes at cursor + 1 left t.Buffer.Remove(t.cury, t.curx, t.cury, t.curx) // Remove character at cursor
lineRunes = lineRunes[:len(lineRunes)-1] // Shrink line length
t.buffer[t.cury] = string(lineRunes) // Reassign line
} else { // If the cursor is at the end of the line...
if t.cury < len(t.buffer)-1 { // And the cursor is not at the last line...
oldLineIdx := t.cury + 1
curLineRunes := []rune(t.buffer[t.cury])
oldLineRunes := []rune(t.buffer[oldLineIdx])
curLineRunes = append(curLineRunes, oldLineRunes...) // Append runes from deleted line to current line
t.buffer[t.cury] = string(curLineRunes) // Update the current line with the new runes
copy(t.buffer[oldLineIdx:], t.buffer[oldLineIdx+1:]) // Shift lines below the old line up
t.buffer = t.buffer[:len(t.buffer)-1] // Shrink buffer by one line
}
} }
} else { // Delete the character before the cursor } else { // Delete the character before the cursor
if t.curx > 0 { // If the cursor is not at the beginning of the line... // If the cursor is not at the first column of the first line...
lineRunes := []rune(t.buffer[t.cury]) if t.cury > 0 || t.curx > 0 {
copy(lineRunes[t.curx-1:], lineRunes[t.curx:]) // Shift runes at cursor left t.CursorLeft() // Back up to that character
lineRunes = lineRunes[:len(lineRunes)-1] // Shrink line length t.Buffer.Remove(t.cury, t.curx, t.cury, t.curx) // Remove character at cursor
t.buffer[t.cury] = string(lineRunes) // Reassign line
t.SetLineCol(t.cury, t.curx-1) // Shift cursor left
} else { // If the cursor is at the beginning of the line...
if t.cury > 0 { // And the cursor is not at the first line...
oldLineIdx := t.cury
t.SetLineCol(t.cury-1, len(t.buffer[t.cury-1])) // Cursor goes to the end of the above line
curLineRunes := []rune(t.buffer[t.cury])
oldLineRunes := []rune(t.buffer[oldLineIdx])
curLineRunes = append(curLineRunes, oldLineRunes...) // Append the old line to the current line
t.buffer[t.cury] = string(curLineRunes) // Update the current line to the new runes
copy(t.buffer[oldLineIdx:], t.buffer[oldLineIdx+1:]) // Shift lines below the old line up
t.buffer = t.buffer[:len(t.buffer)-1] // Shrink buffer by one line
}
} }
} }
} }
@ -188,99 +154,45 @@ func (t *TextEdit) Insert(contents string) {
case '\r': case '\r':
// 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++ i++ // Consume '\n' after
t.insertNewLine() t.Buffer.Insert(t.cury, t.curx, []byte{'\n'})
t.SetLineCol(t.cury+1, 0) // Go to the start of that new line
} }
case '\n': case '\n':
t.insertNewLine() t.Buffer.Insert(t.cury, t.curx, []byte{'\n'})
t.SetLineCol(t.cury+1, 0) // Go to the start of that new line
case '\b': case '\b':
t.Delete(false) // Delete the character before the cursor t.Delete(false) // Delete the character before the cursor
case '\t': case '\t':
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 := []rune(strings.Repeat(" ", t.TabSize)) spaces := strings.Repeat(" ", t.TabSize)
spacesLen := len(spaces) t.Buffer.Insert(t.cury, t.curx, []byte(spaces))
t.SetLineCol(t.cury, t.curx+len(spaces)) // Advance the cursor
lineRunes := []rune(t.buffer[t.cury])
lineRunes = append(lineRunes, spaces...)
copy(lineRunes[t.curx+spacesLen:], lineRunes[t.curx:]) // Shift runes at cursor to the right
copy(lineRunes[t.curx:], spaces) // Copy spaces into the gap
t.buffer[t.cury] = string(lineRunes) // Reassign the line
t.SetLineCol(t.cury, t.curx+spacesLen) // 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
lineRunes := []rune(t.buffer[t.cury]) t.Buffer.Insert(t.cury, t.curx, []byte(string(ch)))
lineRunes = append(lineRunes, ch) // Extend the length of the string
copy(lineRunes[t.curx+1:], lineRunes[t.curx:]) // Shift runes at cursor to the right
lineRunes[t.curx] = ch
t.buffer[t.cury] = string(lineRunes) // Reassign the line
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.prevCurCol = t.curx
} }
// insertNewLine inserts a line break at the cursor and sets the cursor position to the first // getTabCountInLineAtCol returns tabs in the given line, before the column position,
// column of that new line. Text before the cursor on the current line remains on that line,
// text at or after the cursor on the current line is moved to the new line.
func (t *TextEdit) insertNewLine() {
t.Dirty = true
lineRunes := []rune(t.buffer[t.cury]) // A slice of runes of the old line
movedRunes := lineRunes[t.curx:] // A slice of the old line containing runes to be moved
newLineRunes := make([]rune, len(movedRunes))
copy(newLineRunes, movedRunes) // Copy old runes to new line
t.buffer[t.cury] = string(lineRunes[:t.curx]) // Shrink old line's length
t.buffer = append(t.buffer, "") // Increment buffer length
copy(t.buffer[t.cury+2:], t.buffer[t.cury+1:]) // Shift lines after current line down
t.buffer[t.cury+1] = string(newLineRunes) // Assign the new line
t.SetLineCol(t.cury+1, 0) // Go to start of new line
t.prevCurCol = t.curx
}
// getTabCountInLineAtCol returns tabs in the given line, at or before that column position,
// if hard tabs are enabled. If hard tabs are not enabled, the function returns zero. // if hard tabs are enabled. If hard tabs are not enabled, the function returns zero.
// Multiply returned tab count by TabSize to get the offset produced by tabs. // Multiply returned tab count by TabSize to get the offset produced by tabs.
// Col must be a valid column position in the given line. Maybe call clampLineCol before // Col must be a valid column position in the given line. Maybe call clampLineCol before
// this function. // this function.
func (t *TextEdit) getTabCountInLineAtCol(line, col int) int { func (t *TextEdit) getTabCountInLineAtCol(line, col int) int {
if t.UseHardTabs { if t.UseHardTabs {
lineRunes := []rune(t.buffer[line]) return t.Buffer.Count(line, 0, line, col, []byte{'\t'})
return strings.Count(string(lineRunes[:col]), "\t")
} }
return 0 return 0
} }
// clampLineCol clamps the line and col inputs to only valid values within the buffer.
func (t *TextEdit) clampLineCol(line, col int) (int, int) {
// Clamp the line input
if line < 0 {
line = 0
} else if len := len(t.buffer); line >= len { // If new line is beyond the length of the buffer...
line = len - 1 // Change that line to be the end of the buffer, instead
}
lineRunes := []rune(t.buffer[line])
// Clamp the column input
if col < 0 {
col = 0
} else if len := len(lineRunes); col > len {
col = len
}
return line, col
}
// GetLineCol returns (line, col) of the cursor. Zero is origin for both. // GetLineCol returns (line, col) of the cursor. Zero is origin for both.
func (t *TextEdit) GetLineCol() (int, int) { func (t *TextEdit) GetLineCol() (int, int) {
return t.cury, t.curx return t.cury, t.curx
@ -291,10 +203,10 @@ func (t *TextEdit) GetLineCol() (int, int) {
// If `col` is out of bounds, `col` will be clamped to the closest column available for the 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. // Will scroll the TextEdit just enough to see the line the cursor is at.
func (t *TextEdit) SetLineCol(line, col int) { func (t *TextEdit) SetLineCol(line, col int) {
line, col = t.clampLineCol(line, col) 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 (temporary; purely visual)
// 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...
@ -312,6 +224,10 @@ func (t *TextEdit) SetLineCol(line, col int) {
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 t.cury, t.curx = line, col
if t.focused && !t.selectMode { if t.focused && !t.selectMode {
(*t.screen).ShowCursor(t.x+columnWidth+col+tabOffset-t.scrollx, t.y+line-t.scrolly) (*t.screen).ShowCursor(t.x+columnWidth+col+tabOffset-t.scrollx, t.y+line-t.scrolly)
@ -325,7 +241,7 @@ func (t *TextEdit) CursorUp() {
if t.cury <= 0 { // If the cursor is at the first line... if t.cury <= 0 { // If the cursor is at the first line...
t.SetLineCol(t.cury, 0) // Go to beginning t.SetLineCol(t.cury, 0) // Go to beginning
} else { } else {
line, col := t.clampLineCol(t.cury-1, t.prevCurCol) line, col := t.Buffer.ClampLineCol(t.cury-1, t.prevCurCol)
if t.UseHardTabs { // When using hard tabs, subtract offsets produced by tabs if t.UseHardTabs { // When using hard tabs, subtract offsets produced by tabs
tabOffset := t.getTabCountInLineAtCol(line, col) * (t.TabSize - 1) tabOffset := t.getTabCountInLineAtCol(line, col) * (t.TabSize - 1)
col -= tabOffset // We still count each \t in the col col -= tabOffset // We still count each \t in the col
@ -336,10 +252,10 @@ func (t *TextEdit) CursorUp() {
// CursorDown moves the cursor down a line. // CursorDown moves the cursor down a line.
func (t *TextEdit) CursorDown() { func (t *TextEdit) CursorDown() {
if t.cury >= len(t.buffer)-1 { // If the cursor is at the last line... if t.cury >= t.Buffer.Lines()-1 { // If the cursor is at the last line...
t.SetLineCol(t.cury, len(t.buffer[t.cury])) // Go to end of current line t.SetLineCol(t.cury, math.MaxInt32) // Go to end of current line
} else { } else {
line, col := t.clampLineCol(t.cury+1, t.prevCurCol) line, col := t.Buffer.ClampLineCol(t.cury+1, t.prevCurCol)
if t.UseHardTabs { if t.UseHardTabs {
tabOffset := t.getTabCountInLineAtCol(line, col) * (t.TabSize - 1) tabOffset := t.getTabCountInLineAtCol(line, col) * (t.TabSize - 1)
col -= tabOffset // We still count each \t in the col col -= tabOffset // We still count each \t in the col
@ -363,7 +279,7 @@ func (t *TextEdit) CursorLeft() {
func (t *TextEdit) CursorRight() { func (t *TextEdit) CursorRight() {
// If we are at the end of the current line, // If we are at the end of the current line,
// and not at the last line... // and not at the last line...
if t.curx >= len([]rune(t.buffer[t.cury])) && t.cury < len(t.buffer)-1 { 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 t.SetLineCol(t.cury+1, 0) // Go to beginning of line below
} else { } else {
t.SetLineCol(t.cury, t.curx+1) t.SetLineCol(t.cury, t.curx+1)
@ -377,39 +293,25 @@ func (t *TextEdit) getColumnWidth() int {
columnWidth := 0 columnWidth := 0
if t.LineNumbers { if t.LineNumbers {
// Set columnWidth to max count of line number digits // Set columnWidth to max count of line number digits
columnWidth = Max(2, len(strconv.Itoa(len(t.buffer)))) // Column has minimum width of 2 columnWidth = Max(2, len(strconv.Itoa(t.Buffer.Lines()))) // Column has minimum width of 2
} }
return columnWidth return columnWidth
} }
// GetSelectedString returns a string of the region of the buffer that is currently selected. // GetSelectedBytes returns a byte slice of the region of the buffer that is currently selected.
// If the returned string is empty, then nothing was selected. // If the returned string is empty, then nothing was selected. The slice returned may or may not
func (t *TextEdit) GetSelectedString() string { // be a copy of the buffer, so do not write to it.
func (t *TextEdit) GetSelectedBytes() []byte {
if t.selectMode { if t.selectMode {
lines := make([]string, t.selection.EndLine-t.selection.StartLine+1) return t.Buffer.Slice(t.selection.StartLine, t.selection.StartCol, t.selection.EndLine, t.selection.EndCol)
copy(lines, t.buffer[t.selection.StartLine:t.selection.EndLine+1])
// Start last line at end col
lastLine := lines[len(lines)-1]
if t.selection.EndCol >= len(lastLine) { // If the line delimiter of the last line is selected...
// Don't access out-of-bounds and include the line delimiter
lastLine = string([]rune(lastLine)[:t.selection.EndCol]) + t.GetLineDelimiter()
} else { // Normal access
lastLine = string([]rune(lastLine)[:t.selection.EndCol+1])
}
lines[len(lines)-1] = lastLine
lines[0] = string([]rune(lines[0])[t.selection.StartCol:]) // Start first line at start col
return strings.Join(lines, t.GetLineDelimiter())
} }
return "" return []byte{}
} }
// Draw renders the TextEdit component. // Draw renders the TextEdit component.
func (t *TextEdit) Draw(s tcell.Screen) { func (t *TextEdit) Draw(s tcell.Screen) {
columnWidth := t.getColumnWidth() columnWidth := t.getColumnWidth()
bufferLen := len(t.buffer) bufferLines := t.Buffer.Lines()
textEditStyle := t.Theme.GetOrDefault("TextEdit") textEditStyle := t.Theme.GetOrDefault("TextEdit")
selectedStyle := t.Theme.GetOrDefault("TextEditSelected") selectedStyle := t.Theme.GetOrDefault("TextEditSelected")
@ -428,17 +330,15 @@ func (t *TextEdit) Draw(s tcell.Screen) {
lineNumStr := "" lineNumStr := ""
if line < bufferLen { // Only index buffer if we are within it... if line < bufferLines { // Only index buffer if we are within it...
lineNumStr = strconv.Itoa(line + 1) // Line number as a string lineNumStr = strconv.Itoa(line + 1) // Line number as a string
var lineStr string // Line to be drawn lineStr := string(t.Buffer.Line(line)) // Line to be drawn
if t.UseHardTabs { if t.UseHardTabs {
lineStr = strings.ReplaceAll(t.buffer[line], "\t", tabStr) lineStr = strings.ReplaceAll(lineStr, "\t", tabStr)
} else {
lineStr = t.buffer[line]
} }
lineRunes := []rune(lineStr) lineRunes := []rune(lineStr) // TODO: something more efficient here
if len(lineRunes) >= t.scrollx { // If some of the line is visible at our horizontal scroll... if len(lineRunes) >= t.scrollx { // If some of the line is visible at our horizontal scroll...
lineRunes = lineRunes[t.scrollx:] // Trim left side of string we cannot see lineRunes = lineRunes[t.scrollx:] // Trim left side of string we cannot see
@ -454,10 +354,10 @@ func (t *TextEdit) Draw(s tcell.Screen) {
tabCount := t.getTabCountInLineAtCol(line, t.selection.StartCol) tabCount := t.getTabCountInLineAtCol(line, t.selection.StartCol)
selStartIdx = t.selection.StartCol + tabCount*(t.TabSize-1) - t.scrollx selStartIdx = t.selection.StartCol + tabCount*(t.TabSize-1) - t.scrollx
} }
selEndIdx := len(lineRunes) - t.scrollx // used inclusively selEndIdx := len(lineRunes) - t.scrollx - 1 // used inclusively
if line == t.selection.EndLine { // If the selection ends somewhere in the line... if line == t.selection.EndLine { // If the selection ends somewhere in the line...
tabCount := t.getTabCountInLineAtCol(line, t.selection.EndCol) tabCount := t.getTabCountInLineAtCol(line, t.selection.EndCol)
selEndIdx = t.selection.EndCol + tabCount*(t.TabSize-1) - t.scrollx selEndIdx = t.selection.EndCol - 1 + tabCount*(t.TabSize-1) - t.scrollx
} }
// NOTE: a special draw function just for selections. Should combine this with ordinary draw // NOTE: a special draw function just for selections. Should combine this with ordinary draw
@ -620,7 +520,7 @@ func (t *TextEdit) HandleEvent(event tcell.Event) bool {
t.SetLineCol(t.cury, 0) t.SetLineCol(t.cury, 0)
t.prevCurCol = t.curx t.prevCurCol = t.curx
case tcell.KeyEnd: case tcell.KeyEnd:
t.SetLineCol(t.cury, len(t.buffer[t.cury])) t.SetLineCol(t.cury, math.MaxInt32) // Max column
t.prevCurCol = t.curx t.prevCurCol = t.curx
case tcell.KeyPgUp: case tcell.KeyPgUp:
t.SetLineCol(t.scrolly-t.height, t.curx) // Go a page up t.SetLineCol(t.scrolly-t.height, t.curx) // Go a page up
@ -641,7 +541,7 @@ func (t *TextEdit) HandleEvent(event tcell.Event) bool {
case tcell.KeyTab: case tcell.KeyTab:
t.Insert("\t") // (can translate to four spaces) t.Insert("\t") // (can translate to four spaces)
case tcell.KeyEnter: case tcell.KeyEnter:
t.insertNewLine() t.Insert("\n")
// Inserting // Inserting
case tcell.KeyRune: case tcell.KeyRune: