diff --git a/README.md b/README.md index 816ffae..f6d0b8b 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Among these components are: - __Input forms__ (include __input/password fields__, __drop-down selections__, __checkboxes__, and __buttons__) - Navigable multi-color __text views__ +- Editable multi-line __text areas__ - Sophisticated navigable __table views__ - Flexible __tree views__ - Selectable __lists__ diff --git a/doc.go b/doc.go index de563e3..2ad024d 100644 --- a/doc.go +++ b/doc.go @@ -9,6 +9,7 @@ The package implements the following widgets: - [TextView]: A scrollable window that display multi-colored text. Text may also be highlighted. + - [TextArea]: An editable multi-line text area. - [Table]: A scrollable display of tabular data. Table cells, rows, or columns may also be highlighted. - [TreeView]: A scrollable display for hierarchical data. Tree nodes can be diff --git a/textarea.go b/textarea.go index c52ede7..796b109 100644 --- a/textarea.go +++ b/textarea.go @@ -27,13 +27,21 @@ const ( minCursorSuffix = 3 ) -var ( - // 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. - NewLine = "\n" +// Types of user actions on a text area. +type taAction int + +const ( + 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 // roughly follows the concept of Piece Chains outline in // 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 // TextArea.initialText and the actual length must be its absolute value. If // 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 } @@ -69,7 +78,7 @@ type textAreaSpan struct { // text is not supported. Word-wrapping is enabled by default but can be turned // 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 // 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 // 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 // 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: @@ -123,31 +132,30 @@ type textAreaSpan struct { // - 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. // -// The default clipboard is an internal text buffer, i.e. the operating system's -// clipboard is not used. The Ctrl-Q key was chosen for the "copy" function -// because the Ctrl-C key is the default key to stop the application. If your -// application frees up the global Ctrl-C key and you want to bind it to the -// "copy to clipboard" function, you may use [Box.SetInputCapture] to override -// the Ctrl-Q key to implement copying to the clipboard. Note that using your -// terminal's / operating system's key bindings for copy+paste functionality may -// not have the expected effect as tview will not be able to handle these keys. +// The Ctrl-Q key was chosen for the "copy" function because the Ctrl-C key is +// the default key to stop the application. If your application frees up the +// global Ctrl-C key and you want to bind it to the "copy to clipboard" +// function, you may use [Box.SetInputCapture] to override the Ctrl-Q key to +// implement copying to the clipboard. Note that using your terminal's / +// operating system's key bindings for copy+paste functionality may 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 -// operating system's clipboard), you can use [Box.SetInputCapture] to override -// the key binds for copy, cut, and paste. [TextArea.SetClipboard] provides all -// the functionality needed to implement your own clipboard. +// The default clipboard is an internal text buffer, i.e. the operating system's +// clipboard is not used. If you want to implement your own clipboard (or make +// use of your operating system's clipboard), you can use +// [TextArea.SetClipboard] which provides all the functionality needed to +// implement your own clipboard. // // The text area also supports Undo: // -// - Ctrl-Z: Undo the last change. -// - Ctrl-Y: Redo the last Undo change. +// - Ctrl-Z: Undo the last change. +// - Ctrl-Y: Redo the last Undo change. // // If the mouse is enabled, clicking on a screen cell will move the cursor to // that location or to the end of the line if past the last character. Turning // 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 // word underneath the mouse cursor can be selected by double-clicking. - type TextArea struct { *Box @@ -188,12 +196,6 @@ type TextArea struct { // deleted. 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: // 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. 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 @@ -265,6 +284,7 @@ func NewTextArea() *TextArea { textStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.PrimaryTextColor), 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. + lastAction: taActionOther, } t.editText.Grow(editBufferMinCap) t.spans[0] = textAreaSpan{previous: -1, next: 1} @@ -321,6 +341,30 @@ func (t *TextArea) SetText(text string, cursorAtTheEnd bool) *TextArea { 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 // available width being wrapped onto the next line. If false, any characters // 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. // 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 -// range (with an undefined state). +// do anything. The function returns the end position of the deleted/inserted +// range. // // The function can hang if "deleteStart" is located after "deleteEnd". // -// This function does not generate Undo events. Undo events are generated -// elsewhere, when the user changes their type of edit. It also does not modify -// [TextArea.lineStarts]. -func (t *TextArea) replace(deleteStart, deleteEnd [3]int, insert string) (end [3]int) { - end = deleteEnd - - // Check max length. - if t.maxLength > 0 && t.length+len(insert) > t.maxLength { - return +// Undo events are always generated unless continuation is true and text is +// either appended to the end of a span or a span is shortened at the beginning +// or the end (and nothing else). +// +// This function does not modify [TextArea.lineStarts]. +func (t *TextArea) replace(deleteStart, deleteEnd [3]int, insert string, continuation bool) [3]int { + // Maybe nothing needs to be done? + if deleteStart == deleteEnd && insert == "" || t.maxLength > 0 && t.length+len(insert) >= t.maxLength { + return deleteEnd } - // Delete. - for deleteStart[0] != deleteEnd[0] { - if deleteStart[1] == 0 { - // Delete this entire span. - deleteStart[0] = t.deleteSpan(deleteStart[0]) - deleteStart[1] = 0 - } else { - // Delete a partial span at the end. - if t.spans[deleteStart[0]].length < 0 { - // Initial text span. Has negative length. - t.length -= -t.spans[deleteStart[0]].length - deleteStart[1] - t.spans[deleteStart[0]].length = -deleteStart[1] - } else { - // Edit buffer span. Has positive length. - t.length -= t.spans[deleteStart[0]].length - deleteStart[1] - t.spans[deleteStart[0]].length = deleteStart[1] + // Handle the few cases where we don't put anything onto the undo stack. + if continuation { + // Same action as the one before. An undo item was already generated for + // this block of (same) actions. + if insert == "" { + if deleteStart[1] == 0 && deleteEnd[1] == 0 { + // We're deleting an entire span (only one because we delete at + // most one grapheme). Where does it connect to the last undo + // item? + if t.spans[t.undoStack[t.nextUndo-1]].next == deleteStart[0] { + // Backspace. Extend the "before" span of the undo stack's + // last item. + previous := t.spans[deleteStart[0]].previous + if previous != 0 { + undoPrevious := t.undoStack[t.nextUndo-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 + } + t.spans[previous].next = deleteEnd[0] + t.spans[deleteEnd[0]].previous = previous + length := t.spans[deleteStart[0]].length + if length < 0 { + t.length += length + } else { + t.length -= length + } + return deleteEnd + } + // Delete. Extend the "after" span of the undo stack's last + // item. + if deleteEnd[0] != 1 { + undoNext := t.undoStack[t.nextUndo-1] + 1 + spansLength := len(t.spans) // We make two new entries for the modified undo item. + t.spans = append(t.spans, t.spans[undoNext-1]) + 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 + t.spans[deleteEnd[0]].previous = previous + t.spans[previous].next = deleteEnd[0] + length := t.spans[deleteStart[0]].length + if length < 0 { + t.length += length + } else { + t.length -= length + } + 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 } - deleteStart[0] = t.spans[deleteStart[0]].next - deleteStart[1] = 0 - } - } // At this point, deleteStart[0] == deleteEnd[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. + } 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[span.previous].length += length + t.spans[previous].length += length t.length += length - } else { - // Insert a new span. - t.insertSpan(insert, spanIndex) + return deleteEnd } - } 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 - - return next -} - -// 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 + // All other cases generate an undo item. + before := t.spans[deleteStart[0]].previous + after := deleteEnd[0] + if deleteEnd[1] > 0 { + after = t.spans[deleteEnd[0]].next } - - // Make a new trailing span. - span := t.spans[index] - newSpan := textAreaSpan{ - previous: index, - next: span.next, - offset: span.offset + offset, + 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++ + + // Adjust total text length by subtracting everything between "before" and + // "after". Inserted spans will be added back. + for index := deleteStart[0]; index != after; index = t.spans[index].next { + if t.spans[index].length < 0 { + t.length += t.spans[index].length + } else { + t.length -= t.spans[index].length + } } - - // Adjust lengths. - if span.length < 0 { - // Initial text span. Has negative length. - newSpan.length = span.length + offset - t.spans[index].length = -offset - } else { - // Edit buffer span. Has positive length. - newSpan.length = span.length - offset - t.spans[index].length = offset + t.spans[before].next = after + t.spans[after].previous = before + + // We go from left to right, connecting new spans as needed. We update + // "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{ + previous: before, + 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) } - // Insert the modified and new spans. - newIndex := len(t.spans) - t.spans = append(t.spans, newSpan) - t.spans[span.next].previous = newIndex - t.spans[index].next = newIndex - - return newIndex -} - -// insertSpan inserts the a span with the given text into the piece chain before -// the span with the given index and returns the index of the newly inserted -// span. If index <= 0, nothing happens and 1 is returned. The text is appended -// to the edit buffer. The length of the text is added to TextArea.length. -func (t *TextArea) insertSpan(text string, index int) int { - if index < 1 || index >= len(t.spans) { - return 1 + // If we insert text, connect a new span. + if insert != "" { + span := textAreaSpan{ + previous: before, + next: after, + offset: t.editText.Len(), + } + span.length, _ = t.editText.WriteString(insert) + 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) } - // Make a new span. - nextSpan := t.spans[index] - span := textAreaSpan{ - previous: nextSpan.previous, - next: index, - offset: t.editText.Len(), + // 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) } - span.length, _ = t.editText.WriteString(text) - // 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 - - return newIndex + return deleteEnd } // Draw draws this primitive onto the screen. @@ -1253,7 +1311,7 @@ func (t *TextArea) deleteLine() { } // 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.truncateLines(startRow) t.clampToCursor(startRow) @@ -1304,6 +1362,13 @@ func (t *TextArea) getSelectedText() string { // InputHandler returns the handler for this primitive. func (t *TextArea) InputHandler() 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 { case tcell.KeyLeft: // Move one grapheme cluster to the left. 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. 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.truncateLines(row - 1) t.clampToCursor(row) t.selectionStart = t.cursor + newLastAction = taActionTypeSpace case tcell.KeyTab: // Insert TabSize spaces. 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.truncateLines(row - 1) t.clampToCursor(row) t.selectionStart = t.cursor + newLastAction = taActionTypeSpace case tcell.KeyRune: if event.Modifiers()&tcell.ModAlt > 0 { // We accept some Alt- key combinations. @@ -1445,8 +1512,13 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr } } else { // Other keys are simply accepted as regular characters. + r := event.Rune() 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.truncateLines(row - 1) t.clampToCursor(row) @@ -1456,7 +1528,7 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr from, to, row := t.getSelection() if from != to { // 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.truncateLines(row - 1) 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. t.moveCursor(t.cursor.row, t.cursor.actualColumn-1) } + + // Remove that last grapheme cluster. 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.truncateLines(t.cursor.row - 1) t.clampToCursor(t.cursor.row) + newLastAction = taActionBackspace } t.selectionStart = t.cursor case tcell.KeyDelete, tcell.KeyCtrlD: // Delete forward. from, to, row := t.getSelection() if from != to { // 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.truncateLines(row - 1) 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 { _, _, _, 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.truncateLines(t.cursor.row - 1) t.clampToCursor(t.cursor.row) + newLastAction = taActionDelete } t.selectionStart = t.cursor 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 } } - t.cursor.pos = t.replace(t.cursor.pos, pos, "") + t.cursor.pos = t.replace(t.cursor.pos, pos, "", false) row := t.cursor.row t.cursor.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. pos := t.cursor.pos 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 t.cursor.row = -1 t.truncateLines(row) @@ -1544,7 +1620,7 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr if t.cursor != t.selectionStart { t.copyToClipboard(t.getSelectedText()) 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.truncateLines(row - 1) 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. 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.truncateLines(row - 1) t.clampToCursor(row)