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:
parent
980ae61d2a
commit
eb795cd8e5
@ -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
1
doc.go
@ -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
|
||||||
|
464
textarea.go
464
textarea.go
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user