1
0
mirror of https://github.com/rivo/tview.git synced 2025-04-26 13:49:06 +08:00

Reimplemented TextArea.replace() function, considering undo handling now.

This commit is contained in:
Oliver 2022-08-19 20:30:40 +02:00
parent 980ae61d2a
commit eb795cd8e5
3 changed files with 284 additions and 206 deletions

View File

@ -11,6 +11,7 @@ Among these components are:
- __Input forms__ (include __input/password fields__, __drop-down selections__, __checkboxes__, and __buttons__) - __Input forms__ (include __input/password fields__, __drop-down selections__, __checkboxes__, and __buttons__)
- Navigable multi-color __text views__ - Navigable multi-color __text views__
- Editable multi-line __text areas__
- Sophisticated navigable __table views__ - Sophisticated navigable __table views__
- Flexible __tree views__ - Flexible __tree views__
- Selectable __lists__ - Selectable __lists__

1
doc.go
View File

@ -9,6 +9,7 @@ The package implements the following widgets:
- [TextView]: A scrollable window that display multi-colored text. Text may - [TextView]: A scrollable window that display multi-colored text. Text may
also be highlighted. also be highlighted.
- [TextArea]: An editable multi-line text area.
- [Table]: A scrollable display of tabular data. Table cells, rows, or columns - [Table]: A scrollable display of tabular data. Table cells, rows, or columns
may also be highlighted. may also be highlighted.
- [TreeView]: A scrollable display for hierarchical data. Tree nodes can be - [TreeView]: A scrollable display for hierarchical data. Tree nodes can be

View File

@ -27,13 +27,21 @@ const (
minCursorSuffix = 3 minCursorSuffix = 3
) )
var ( // Types of user actions on a text area.
// NewLine is the string sequence to be inserted when hitting the Enter key type taAction int
// in a TextArea. The default is "\n" but you may change it to "\r\n" if
// required. const (
NewLine = "\n" taActionOther taAction = iota
taActionTypeSpace // Typing a space character.
taActionTypeNonSpace // Typing a non-space character.
taActionBackspace // Deleting the previous character.
taActionDelete // Deleting the next character.
) )
// NewLine is the string sequence to be inserted when hitting the Enter key in a
// TextArea. The default is "\n" but you may change it to "\r\n" if required.
var NewLine = "\n"
// textAreaSpan represents a range of text in a text area. The text area widget // textAreaSpan represents a range of text in a text area. The text area widget
// roughly follows the concept of Piece Chains outline in // roughly follows the concept of Piece Chains outline in
// http://www.catch22.net/tuts/neatpad/piece-chains with some modifications. // http://www.catch22.net/tuts/neatpad/piece-chains with some modifications.
@ -61,7 +69,8 @@ type textAreaSpan struct {
// If "length" is negative, the span represents a substring of // If "length" is negative, the span represents a substring of
// TextArea.initialText and the actual length must be its absolute value. If // TextArea.initialText and the actual length must be its absolute value. If
// it is positive, the span represents a substring of TextArea.editText. For // it is positive, the span represents a substring of TextArea.editText. For
// the sentinel spans (index 0 and 1), both values will be 0. // the sentinel spans (index 0 and 1), both values will be 0. Others will
// never have a zero length.
offset, length int offset, length int
} }
@ -69,7 +78,7 @@ type textAreaSpan struct {
// text is not supported. Word-wrapping is enabled by default but can be turned // text is not supported. Word-wrapping is enabled by default but can be turned
// off or be changed to character-wrapping. // off or be changed to character-wrapping.
// //
// Navigation and Editing // # Navigation and Editing
// //
// A text area is always in editing mode and no other mode exists. The following // A text area is always in editing mode and no other mode exists. The following
// keys can be used to move the cursor: // keys can be used to move the cursor:
@ -94,7 +103,7 @@ type textAreaSpan struct {
// Words are defined according to Unicode Standard Annex #29. We skip any words // Words are defined according to Unicode Standard Annex #29. We skip any words
// that contain only spaces or punctuation. // that contain only spaces or punctuation.
// //
// Entering a character (rune) will insert it at the current cursor location. // Entering a character will insert it at the current cursor location.
// Subsequent characters are moved accordingly. If the cursor is outside the // Subsequent characters are moved accordingly. If the cursor is outside the
// visible area, any changes to the text will move it into the visible area. The // visible area, any changes to the text will move it into the visible area. The
// following keys can also be used to modify the text: // following keys can also be used to modify the text:
@ -123,19 +132,19 @@ type textAreaSpan struct {
// - Ctrl-V: Replace the selected text with the clipboard text. If no text is // - Ctrl-V: Replace the selected text with the clipboard text. If no text is
// selected, the clipboard text will be inserted at the cursor location. // selected, the clipboard text will be inserted at the cursor location.
// //
// The default clipboard is an internal text buffer, i.e. the operating system's // The Ctrl-Q key was chosen for the "copy" function because the Ctrl-C key is
// clipboard is not used. The Ctrl-Q key was chosen for the "copy" function // the default key to stop the application. If your application frees up the
// because the Ctrl-C key is the default key to stop the application. If your // global Ctrl-C key and you want to bind it to the "copy to clipboard"
// application frees up the global Ctrl-C key and you want to bind it to the // function, you may use [Box.SetInputCapture] to override the Ctrl-Q key to
// "copy to clipboard" function, you may use [Box.SetInputCapture] to override // implement copying to the clipboard. Note that using your terminal's /
// the Ctrl-Q key to implement copying to the clipboard. Note that using your // operating system's key bindings for copy+paste functionality may not have the
// terminal's / operating system's key bindings for copy+paste functionality may // expected effect as tview will not be able to handle these keys.
// not have the expected effect as tview will not be able to handle these keys.
// //
// Similarly, if you want to implement your own clipboard (or make use of your // The default clipboard is an internal text buffer, i.e. the operating system's
// operating system's clipboard), you can use [Box.SetInputCapture] to override // clipboard is not used. If you want to implement your own clipboard (or make
// the key binds for copy, cut, and paste. [TextArea.SetClipboard] provides all // use of your operating system's clipboard), you can use
// the functionality needed to implement your own clipboard. // [TextArea.SetClipboard] which provides all the functionality needed to
// implement your own clipboard.
// //
// The text area also supports Undo: // The text area also supports Undo:
// //
@ -147,7 +156,6 @@ type textAreaSpan struct {
// the scroll wheel will scroll the text. Text can also be selected by moving // the scroll wheel will scroll the text. Text can also be selected by moving
// the mouse while pressing the left mouse button (see below for details). The // the mouse while pressing the left mouse button (see below for details). The
// word underneath the mouse cursor can be selected by double-clicking. // word underneath the mouse cursor can be selected by double-clicking.
type TextArea struct { type TextArea struct {
*Box *Box
@ -188,12 +196,6 @@ type TextArea struct {
// deleted. // deleted.
spans []textAreaSpan spans []textAreaSpan
// The undo stack's items are the first of two consecutive indices into the
// spans slice. The first referenced span is a copy of the one before the
// modified span range, thse second referenced span is a copy of the one
// after the modified span range.
undoStack []int
// Display, navigation, and cursor related fields: // Display, navigation, and cursor related fields:
// If set to true, lines that are longer than the available width are // If set to true, lines that are longer than the available width are
@ -252,6 +254,23 @@ type TextArea struct {
// The function to call when the user pastes text from the clipboard. // The function to call when the user pastes text from the clipboard.
pasteFromClipboard func() string pasteFromClipboard func() string
// Undo/redo related fields:
// The last action performed by the user.
lastAction taAction
// The undo stack's items are the first of two consecutive indices into the
// spans slice. The first referenced span is a copy of the one before the
// modified span range, the second referenced span is a copy of the one
// after the modified span range. To undo an action, these two spans are put
// back into their original place. Undos and redos decrease or increase the
// nextUndo value. Thus, the next undo action is not always the last item.
undoStack []int
// The current undo/redo position on the undo stack. If no undo or redo has
// been performed yet, this is the same as len(undoStack).
nextUndo int
} }
// NewTextArea returns a new text area. Use [TextArea.SetText] to set the // NewTextArea returns a new text area. Use [TextArea.SetText] to set the
@ -265,6 +284,7 @@ func NewTextArea() *TextArea {
textStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.PrimaryTextColor), textStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.PrimaryTextColor),
selectedStyle: tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.PrimitiveBackgroundColor), selectedStyle: tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.PrimitiveBackgroundColor),
spans: make([]textAreaSpan, 2, pieceChainMinCap), // We reserve some space to avoid reallocations right when editing starts. spans: make([]textAreaSpan, 2, pieceChainMinCap), // We reserve some space to avoid reallocations right when editing starts.
lastAction: taActionOther,
} }
t.editText.Grow(editBufferMinCap) t.editText.Grow(editBufferMinCap)
t.spans[0] = textAreaSpan{previous: -1, next: 1} t.spans[0] = textAreaSpan{previous: -1, next: 1}
@ -321,6 +341,30 @@ func (t *TextArea) SetText(text string, cursorAtTheEnd bool) *TextArea {
return t return t
} }
// GetText returns the entire text of the text area. Note that this will newly
// allocate the entire text.
func (t *TextArea) GetText() string {
if t.length == 0 {
return ""
}
var text strings.Builder
text.Grow(t.length)
spanIndex := t.spans[0].next
for spanIndex != 1 {
span := &t.spans[spanIndex]
if span.length < 0 {
text.WriteString(t.initialText[span.offset : span.offset-span.length])
} else {
text.WriteString(t.editText.String()[span.offset : span.offset+span.length])
}
spanIndex = t.spans[spanIndex].next
}
return text.String()
}
// SetWrap sets the flag that, if true, leads to lines that are longer than the // SetWrap sets the flag that, if true, leads to lines that are longer than the
// available width being wrapped onto the next line. If false, any characters // available width being wrapped onto the next line. If false, any characters
// beyond the available width are not displayed. // beyond the available width are not displayed.
@ -419,185 +463,199 @@ func (t *TextArea) SetClipboard(copyToClipboard func(string), pasteFromClipboard
// replace deletes a range of text and inserts the given text at that position. // replace deletes a range of text and inserts the given text at that position.
// If the resulting text would exceed the maximum length, the function does not // If the resulting text would exceed the maximum length, the function does not
// do anything. The function returns the new position of the deleted/inserted // do anything. The function returns the end position of the deleted/inserted
// range (with an undefined state). // range.
// //
// The function can hang if "deleteStart" is located after "deleteEnd". // The function can hang if "deleteStart" is located after "deleteEnd".
// //
// This function does not generate Undo events. Undo events are generated // Undo events are always generated unless continuation is true and text is
// elsewhere, when the user changes their type of edit. It also does not modify // either appended to the end of a span or a span is shortened at the beginning
// [TextArea.lineStarts]. // or the end (and nothing else).
func (t *TextArea) replace(deleteStart, deleteEnd [3]int, insert string) (end [3]int) { //
end = deleteEnd // This function does not modify [TextArea.lineStarts].
func (t *TextArea) replace(deleteStart, deleteEnd [3]int, insert string, continuation bool) [3]int {
// Check max length. // Maybe nothing needs to be done?
if t.maxLength > 0 && t.length+len(insert) > t.maxLength { if deleteStart == deleteEnd && insert == "" || t.maxLength > 0 && t.length+len(insert) >= t.maxLength {
return return deleteEnd
} }
// Delete. // Handle the few cases where we don't put anything onto the undo stack.
for deleteStart[0] != deleteEnd[0] { if continuation {
if deleteStart[1] == 0 { // Same action as the one before. An undo item was already generated for
// Delete this entire span. // this block of (same) actions.
deleteStart[0] = t.deleteSpan(deleteStart[0]) if insert == "" {
deleteStart[1] = 0 if deleteStart[1] == 0 && deleteEnd[1] == 0 {
} else { // We're deleting an entire span (only one because we delete at
// Delete a partial span at the end. // most one grapheme). Where does it connect to the last undo
if t.spans[deleteStart[0]].length < 0 { // item?
// Initial text span. Has negative length. if t.spans[t.undoStack[t.nextUndo-1]].next == deleteStart[0] {
t.length -= -t.spans[deleteStart[0]].length - deleteStart[1] // Backspace. Extend the "before" span of the undo stack's
t.spans[deleteStart[0]].length = -deleteStart[1] // last item.
} else { previous := t.spans[deleteStart[0]].previous
// Edit buffer span. Has positive length. if previous != 0 {
t.length -= t.spans[deleteStart[0]].length - deleteStart[1] undoPrevious := t.undoStack[t.nextUndo-1]
t.spans[deleteStart[0]].length = deleteStart[1] spansLength := len(t.spans) // We make two new entries for the modified undo item.
t.spans = append(t.spans, t.spans[previous])
t.spans = append(t.spans, t.spans[undoPrevious+1])
t.spans[undoPrevious].previous = spansLength
t.spans[spansLength].next = undoPrevious
t.undoStack[t.nextUndo-1] = spansLength
} }
deleteStart[0] = t.spans[deleteStart[0]].next t.spans[previous].next = deleteEnd[0]
deleteStart[1] = 0 t.spans[deleteEnd[0]].previous = previous
} length := t.spans[deleteStart[0]].length
} // At this point, deleteStart[0] == deleteEnd[0]. if length < 0 {
if deleteEnd[1] > deleteStart[1] {
if deleteStart[1] != 0 {
// Delete in the middle by splitting the span.
deleteEnd[1] -= deleteStart[1]
deleteEnd[0] = t.splitSpan(deleteStart[0], deleteStart[1])
deleteStart[0] = deleteEnd[0]
deleteStart[1] = 0
}
// Delete a partial span at the beginning.
t.length -= deleteEnd[1]
if t.spans[deleteEnd[0]].length < 0 {
// Initial text span. Has negative length.
t.spans[deleteEnd[0]].length += deleteEnd[1]
} else {
// Edit buffer span. Has positive length.
t.spans[deleteEnd[0]].length -= deleteEnd[1]
}
t.spans[deleteEnd[0]].offset += deleteEnd[1]
deleteEnd[1] = 0
end = deleteEnd
}
// Insert.
if len(insert) > 0 {
spanIndex, offset := deleteStart[0], deleteStart[1]
span := t.spans[spanIndex]
if offset == 0 {
previousSpan := t.spans[span.previous]
if previousSpan.length > 0 && previousSpan.offset+previousSpan.length == t.editText.Len() {
// We can simply append to the edit buffer.
length, _ := t.editText.WriteString(insert)
t.spans[span.previous].length += length
t.length += length t.length += length
} else { } else {
// Insert a new span.
t.insertSpan(insert, spanIndex)
}
} else {
// Split and insert.
spanIndex = t.splitSpan(spanIndex, offset)
t.insertSpan(insert, spanIndex)
end = [3]int{spanIndex, 0, 0}
}
}
return
}
// deleteSpan removes the span with the given index from the piece chain. It
// returns the index of the span after the deleted span (or the provided index
// if no span was deleted due to an invalid span index).
//
// This function also adjusts [TextArea.length].
func (t *TextArea) deleteSpan(index int) int {
if index < 2 || index >= len(t.spans) {
return index
}
// Remove from piece chain.
previous := t.spans[index].previous
next := t.spans[index].next
t.spans[previous].next = next
t.spans[next].previous = previous
// Adjust total length.
length := t.spans[index].length
if length < 0 {
length = -length
}
t.length -= length t.length -= length
return next
} }
return deleteEnd
// splitSpan splits the span with the given index at the given offset into two
// spans. It returns the index of the span after the split or the provided
// index if no span was split due to an invalid span index or an invalid
// offset.
func (t *TextArea) splitSpan(index, offset int) int {
if index < 2 || index >= len(t.spans) || offset <= 0 ||
(t.spans[index].length < 0 && offset >= -t.spans[index].length) ||
(t.spans[index].length >= 0 && offset >= t.spans[index].length) {
return index
} }
// Delete. Extend the "after" span of the undo stack's last
// Make a new trailing span. // item.
span := t.spans[index] if deleteEnd[0] != 1 {
newSpan := textAreaSpan{ undoNext := t.undoStack[t.nextUndo-1] + 1
previous: index, spansLength := len(t.spans) // We make two new entries for the modified undo item.
next: span.next, t.spans = append(t.spans, t.spans[undoNext-1])
offset: span.offset + offset, t.spans = append(t.spans, t.spans[undoNext])
t.spans[undoNext].next = spansLength + 1
t.spans[spansLength+1].previous = undoNext
t.undoStack[t.nextUndo-1] = spansLength
} }
previous := t.spans[deleteStart[0]].previous
// Adjust lengths. t.spans[deleteEnd[0]].previous = previous
if span.length < 0 { t.spans[previous].next = deleteEnd[0]
// Initial text span. Has negative length. length := t.spans[deleteStart[0]].length
newSpan.length = span.length + offset if length < 0 {
t.spans[index].length = -offset t.length += length
} else { } else {
// Edit buffer span. Has positive length. t.length -= length
newSpan.length = span.length - offset }
t.spans[index].length = offset return deleteEnd
} else if deleteEnd[1] == 0 {
// Simple backspace. Just shorten this span.
length := t.spans[deleteStart[0]].length
if length < 0 {
length = -deleteStart[1]
}
t.spans[deleteStart[0]].length = length
t.length -= deleteStart[1]
return deleteEnd
} else if deleteStart[1] == 0 {
// Simple delete. Just clip the beginning of this span.
t.spans[deleteEnd[0]].offset += deleteEnd[1]
if t.spans[deleteEnd[0]].length < 0 {
t.spans[deleteEnd[0]].length += deleteEnd[1]
} else {
t.spans[deleteEnd[0]].length -= deleteEnd[1]
}
t.length -= deleteEnd[1]
return deleteEnd
}
} else if insert != "" && deleteStart == deleteEnd && deleteEnd[1] == 0 {
previous := t.spans[deleteStart[0]].previous
bufferSpan := t.spans[previous]
if bufferSpan.length > 0 && bufferSpan.offset+bufferSpan.length == t.editText.Len() {
// Typing individual characters. Simply extend the edit buffer.
length, _ := t.editText.WriteString(insert)
t.spans[previous].length += length
t.length += length
return deleteEnd
}
}
} }
// Insert the modified and new spans. // All other cases generate an undo item.
newIndex := len(t.spans) before := t.spans[deleteStart[0]].previous
t.spans = append(t.spans, newSpan) after := deleteEnd[0]
t.spans[span.next].previous = newIndex if deleteEnd[1] > 0 {
t.spans[index].next = newIndex after = t.spans[deleteEnd[0]].next
return newIndex
} }
t.undoStack = t.undoStack[:t.nextUndo]
t.undoStack = append(t.undoStack, len(t.spans))
t.spans = append(t.spans, t.spans[before])
t.spans = append(t.spans, t.spans[after])
t.nextUndo++
// insertSpan inserts the a span with the given text into the piece chain before // Adjust total text length by subtracting everything between "before" and
// the span with the given index and returns the index of the newly inserted // "after". Inserted spans will be added back.
// span. If index <= 0, nothing happens and 1 is returned. The text is appended for index := deleteStart[0]; index != after; index = t.spans[index].next {
// to the edit buffer. The length of the text is added to TextArea.length. if t.spans[index].length < 0 {
func (t *TextArea) insertSpan(text string, index int) int { t.length += t.spans[index].length
if index < 1 || index >= len(t.spans) { } else {
return 1 t.length -= t.spans[index].length
} }
}
t.spans[before].next = after
t.spans[after].previous = before
// Make a new span. // We go from left to right, connecting new spans as needed. We update
nextSpan := t.spans[index] // "before" as the span to connect new spans to.
// If we start deleting in the middle of a span, connect a partial span.
if deleteStart[1] != 0 {
span := textAreaSpan{ span := textAreaSpan{
previous: nextSpan.previous, previous: before,
next: index, next: after,
offset: t.spans[deleteStart[0]].offset,
length: deleteStart[1],
}
if t.spans[deleteStart[0]].length < 0 {
span.length = -span.length
}
t.length += deleteStart[1] // This was previously subtracted.
t.spans[before].next = len(t.spans)
t.spans[after].previous = len(t.spans)
before = len(t.spans)
for row, lineStart := range t.lineStarts { // Also redirect line starts until the end of this new span.
if lineStart[0] == deleteStart[0] {
if lineStart[1] >= deleteStart[1] {
t.lineStarts = t.lineStarts[:row] // Everything else is unknown at this point.
break
}
t.lineStarts[row][0] = len(t.spans)
}
}
t.spans = append(t.spans, span)
}
// If we insert text, connect a new span.
if insert != "" {
span := textAreaSpan{
previous: before,
next: after,
offset: t.editText.Len(), offset: t.editText.Len(),
} }
span.length, _ = t.editText.WriteString(text) span.length, _ = t.editText.WriteString(insert)
// Insert into piece chain.
newIndex := len(t.spans)
t.spans[nextSpan.previous].next = newIndex
t.spans[index].previous = newIndex
t.spans = append(t.spans, span)
// Adjust text area length.
t.length += span.length t.length += span.length
t.spans[before].next = len(t.spans)
t.spans[after].previous = len(t.spans)
before = len(t.spans)
t.spans = append(t.spans, span)
}
return newIndex // If we stop deleting in the middle of a span, connect a partial span.
if deleteEnd[1] != 0 {
span := textAreaSpan{
previous: before,
next: after,
offset: t.spans[deleteEnd[0]].offset + deleteEnd[1],
}
length := t.spans[deleteEnd[0]].length
if length < 0 {
span.length = length + deleteEnd[1]
t.length -= span.length // This was previously subtracted.
} else {
span.length = length - deleteEnd[1]
t.length += span.length // This was previously subtracted.
}
t.spans[before].next = len(t.spans)
t.spans[after].previous = len(t.spans)
deleteEnd[0], deleteEnd[1] = len(t.spans), 0
t.spans = append(t.spans, span)
}
return deleteEnd
} }
// Draw draws this primitive onto the screen. // Draw draws this primitive onto the screen.
@ -1253,7 +1311,7 @@ func (t *TextArea) deleteLine() {
} }
// Delete the text. // Delete the text.
t.cursor.pos = t.replace(t.lineStarts[startRow], pos, "") t.cursor.pos = t.replace(t.lineStarts[startRow], pos, "", false)
t.cursor.row = -1 t.cursor.row = -1
t.truncateLines(startRow) t.truncateLines(startRow)
t.clampToCursor(startRow) t.clampToCursor(startRow)
@ -1304,6 +1362,13 @@ func (t *TextArea) getSelectedText() string {
// InputHandler returns the handler for this primitive. // InputHandler returns the handler for this primitive.
func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) { func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) { return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
// All actions except a few specific ones are "other" actions.
newLastAction := taActionOther
defer func() {
t.lastAction = newLastAction
}()
// Process the different key events.
switch key := event.Key(); key { switch key := event.Key(); key {
case tcell.KeyLeft: // Move one grapheme cluster to the left. case tcell.KeyLeft: // Move one grapheme cluster to the left.
if event.Modifiers()&tcell.ModAlt == 0 { if event.Modifiers()&tcell.ModAlt == 0 {
@ -1416,18 +1481,20 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
} }
case tcell.KeyEnter: // Insert a newline. case tcell.KeyEnter: // Insert a newline.
from, to, row := t.getSelection() from, to, row := t.getSelection()
t.cursor.pos = t.replace(from, to, NewLine) t.cursor.pos = t.replace(from, to, NewLine, t.lastAction != taActionTypeSpace)
t.cursor.row = -1 t.cursor.row = -1
t.truncateLines(row - 1) t.truncateLines(row - 1)
t.clampToCursor(row) t.clampToCursor(row)
t.selectionStart = t.cursor t.selectionStart = t.cursor
newLastAction = taActionTypeSpace
case tcell.KeyTab: // Insert TabSize spaces. case tcell.KeyTab: // Insert TabSize spaces.
from, to, row := t.getSelection() from, to, row := t.getSelection()
t.cursor.pos = t.replace(from, to, strings.Repeat(" ", TabSize)) t.cursor.pos = t.replace(from, to, strings.Repeat(" ", TabSize), t.lastAction != taActionTypeSpace)
t.cursor.row = -1 t.cursor.row = -1
t.truncateLines(row - 1) t.truncateLines(row - 1)
t.clampToCursor(row) t.clampToCursor(row)
t.selectionStart = t.cursor t.selectionStart = t.cursor
newLastAction = taActionTypeSpace
case tcell.KeyRune: case tcell.KeyRune:
if event.Modifiers()&tcell.ModAlt > 0 { if event.Modifiers()&tcell.ModAlt > 0 {
// We accept some Alt- key combinations. // We accept some Alt- key combinations.
@ -1445,8 +1512,13 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
} }
} else { } else {
// Other keys are simply accepted as regular characters. // Other keys are simply accepted as regular characters.
r := event.Rune()
from, to, row := t.getSelection() from, to, row := t.getSelection()
t.cursor.pos = t.replace(from, to, string(event.Rune())) newLastAction = taActionTypeNonSpace
if unicode.IsSpace(r) {
newLastAction = taActionTypeSpace
}
t.cursor.pos = t.replace(from, to, string(r), newLastAction == t.lastAction || t.lastAction == taActionTypeNonSpace && newLastAction == taActionTypeSpace)
t.cursor.row = -1 t.cursor.row = -1
t.truncateLines(row - 1) t.truncateLines(row - 1)
t.clampToCursor(row) t.clampToCursor(row)
@ -1456,7 +1528,7 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
from, to, row := t.getSelection() from, to, row := t.getSelection()
if from != to { if from != to {
// Simply delete the current selection. // Simply delete the current selection.
t.cursor.pos = t.replace(from, to, "") t.cursor.pos = t.replace(from, to, "", false)
t.cursor.row = -1 t.cursor.row = -1
t.truncateLines(row - 1) t.truncateLines(row - 1)
t.clampToCursor(row) t.clampToCursor(row)
@ -1475,18 +1547,21 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
// Move one grapheme cluster to the left. // Move one grapheme cluster to the left.
t.moveCursor(t.cursor.row, t.cursor.actualColumn-1) t.moveCursor(t.cursor.row, t.cursor.actualColumn-1)
} }
// Remove that last grapheme cluster.
if t.cursor.pos != endPos { if t.cursor.pos != endPos {
t.cursor.pos = t.replace(t.cursor.pos, endPos, "") // Delete the character. t.cursor.pos = t.replace(t.cursor.pos, endPos, "", t.lastAction != taActionBackspace) // Delete the character.
t.cursor.pos[2] = endPos[2] t.cursor.pos[2] = endPos[2]
t.truncateLines(t.cursor.row - 1) t.truncateLines(t.cursor.row - 1)
t.clampToCursor(t.cursor.row) t.clampToCursor(t.cursor.row)
newLastAction = taActionBackspace
} }
t.selectionStart = t.cursor t.selectionStart = t.cursor
case tcell.KeyDelete, tcell.KeyCtrlD: // Delete forward. case tcell.KeyDelete, tcell.KeyCtrlD: // Delete forward.
from, to, row := t.getSelection() from, to, row := t.getSelection()
if from != to { if from != to {
// Simply delete the current selection. // Simply delete the current selection.
t.cursor.pos = t.replace(from, to, "") t.cursor.pos = t.replace(from, to, "", false)
t.cursor.row = -1 t.cursor.row = -1
t.truncateLines(row - 1) t.truncateLines(row - 1)
t.clampToCursor(row) t.clampToCursor(row)
@ -1496,10 +1571,11 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
if t.cursor.pos[0] != 1 { if t.cursor.pos[0] != 1 {
_, _, _, endPos, _ := t.step("", t.cursor.pos, t.cursor.pos) _, _, _, endPos, _ := t.step("", t.cursor.pos, t.cursor.pos)
t.cursor.pos = t.replace(t.cursor.pos, endPos, "") t.cursor.pos = t.replace(t.cursor.pos, endPos, "", t.lastAction != taActionDelete) // Delete the character.
t.cursor.pos[2] = endPos[2] t.cursor.pos[2] = endPos[2]
t.truncateLines(t.cursor.row - 1) t.truncateLines(t.cursor.row - 1)
t.clampToCursor(t.cursor.row) t.clampToCursor(t.cursor.row)
newLastAction = taActionDelete
} }
t.selectionStart = t.cursor t.selectionStart = t.cursor
case tcell.KeyCtrlK: // Delete everything under and to the right of the cursor until before the next newline character. case tcell.KeyCtrlK: // Delete everything under and to the right of the cursor until before the next newline character.
@ -1517,7 +1593,7 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
break break
} }
} }
t.cursor.pos = t.replace(t.cursor.pos, pos, "") t.cursor.pos = t.replace(t.cursor.pos, pos, "", false)
row := t.cursor.row row := t.cursor.row
t.cursor.row = -1 t.cursor.row = -1
t.truncateLines(row - 1) t.truncateLines(row - 1)
@ -1526,7 +1602,7 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
case tcell.KeyCtrlW: // Delete from the start of the current word to the left of the cursor. case tcell.KeyCtrlW: // Delete from the start of the current word to the left of the cursor.
pos := t.cursor.pos pos := t.cursor.pos
t.moveWordLeft() t.moveWordLeft()
t.cursor.pos = t.replace(t.cursor.pos, pos, "") t.cursor.pos = t.replace(t.cursor.pos, pos, "", false)
row := t.cursor.row - 1 row := t.cursor.row - 1
t.cursor.row = -1 t.cursor.row = -1
t.truncateLines(row) t.truncateLines(row)
@ -1544,7 +1620,7 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
if t.cursor != t.selectionStart { if t.cursor != t.selectionStart {
t.copyToClipboard(t.getSelectedText()) t.copyToClipboard(t.getSelectedText())
from, to, row := t.getSelection() from, to, row := t.getSelection()
t.cursor.pos = t.replace(from, to, "") t.cursor.pos = t.replace(from, to, "", false)
t.cursor.row = -1 t.cursor.row = -1
t.truncateLines(row - 1) t.truncateLines(row - 1)
t.clampToCursor(row) t.clampToCursor(row)
@ -1552,7 +1628,7 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
} }
case tcell.KeyCtrlV: // Paste from clipboard. case tcell.KeyCtrlV: // Paste from clipboard.
from, to, row := t.getSelection() from, to, row := t.getSelection()
t.cursor.pos = t.replace(from, to, t.pasteFromClipboard()) t.cursor.pos = t.replace(from, to, t.pasteFromClipboard(), false)
t.cursor.row = -1 t.cursor.row = -1
t.truncateLines(row - 1) t.truncateLines(row - 1)
t.clampToCursor(row) t.clampToCursor(row)