forked from rivo/tview
-
Notifications
You must be signed in to change notification settings - Fork 0
/
textarea.go
2289 lines (2116 loc) · 72.8 KB
/
textarea.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
863
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package tview
import (
"strings"
"unicode"
"unicode/utf8"
"github.com/gdamore/tcell/v2"
"github.com/rivo/uniseg"
)
const (
// The minimum capacity of the text area's piece chain slice.
pieceChainMinCap = 10
// The minimum capacity of the text area's edit buffer.
editBufferMinCap = 200
// The maximum number of bytes making up a grapheme cluster. In theory, this
// could be longer but it would be highly unusual.
maxGraphemeClusterSize = 40
// The minimum width of text (if available) to be shown left of the cursor.
minCursorPrefix = 5
// The minimum width of text (if available) to be shown right of the cursor.
minCursorSuffix = 3
)
// 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 outlined in
// http://www.catch22.net/tuts/neatpad/piece-chains with some modifications.
// This type represents a "span" (or "piece") and thus refers to a subset of the
// text in the editor as part of a doubly-linked list.
//
// In most places where we reference a position in the text, we use a
// three-element int array. The first element is the index of the referenced
// span in the piece chain. The second element is the offset into the span's
// referenced text (relative to the span's start), its value is always >= 0 and
// < span.length. The third element is the state of the text parser at that
// position.
//
// A range of text is represented by a span range which is a starting position
// (3-int array) and an ending position (3-int array). The starting position
// references the first character of the range, the ending position references
// the position after the last character of the range. The end of the text is
// therefore always [3]int{1, 0, 0}, position 0 of the ending sentinel.
//
// Sentinel spans are dummy spans not referring to any text. There are always
// two sentinel spans: the starting span at index 0 of the [TextArea.spans]
// slice and the ending span at index 1.
type textAreaSpan struct {
// Links to the previous and next textAreaSpan objects as indices into the
// [TextArea.spans] slice. The sentinel spans (index 0 and 1) have -1 as
// their previous or next links, respectively.
previous, next int
// The start index and the length of the text segment this span represents.
// If "length" is negative, the span represents a substring of
// [TextArea.initialText] and the actual length is 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. Others will
// never have a zero length.
offset, length int
}
// textAreaUndoItem represents an undoable edit to the text area. It describes
// the two spans wrapping a text change.
type textAreaUndoItem struct {
before, after int // The index of the copied "before" and "after" spans into the "spans" slice.
originalBefore, originalAfter int // The original indices of the "before" and "after" spans.
pos [3]int // The cursor position to be assumed after applying an undo.
length int // The total text length at the time the undo item was created.
continuation bool // If true, this item is a continuation of the previous undo item. It is handled together with all other undo items in the same continuation sequence.
}
// TextArea implements a simple text editor for multi-line text. Multi-color
// text is not supported. Word-wrapping is enabled by default but can be turned
// off or be changed to character-wrapping.
//
// At this point, a text area cannot be added to a [Form]. This will be added in
// the future.
//
// # 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 (subject to what the user's terminal
// supports and how it is configured):
//
// - Left arrow: Move left.
// - Right arrow: Move right.
// - Down arrow: Move down.
// - Up arrow: Move up.
// - Ctrl-A, Home: Move to the beginning of the current line.
// - Ctrl-E, End: Move to the end of the current line.
// - Ctrl-F, page down: Move down by one page.
// - Ctrl-B, page up: Move up by one page.
// - Alt-Up arrow: Scroll the page up, leaving the cursor in its position.
// - Alt-Down arrow: Scroll the page down, leaving the cursor in its position.
// - Alt-Left arrow: Scroll the page to the left, leaving the cursor in its
// position. Ignored if wrapping is enabled.
// - Alt-Right arrow: Scroll the page to the right, leaving the cursor in its
// position. Ignored if wrapping is enabled.
// - Alt-B, Ctrl-Left arrow: Jump to the beginning of the current or previous
// word.
// - Alt-F, Ctrl-Right arrow: Jump to the end of the current or next word.
//
// Words are defined according to [Unicode Standard Annex #29]. We skip any
// words that contain only spaces or punctuation.
//
// Entering a character will insert it at the current cursor location.
// Subsequent characters are shifted 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:
//
// - Enter: Insert a newline character (see [NewLine]).
// - Tab: Insert a tab character (\t). It will be rendered like [TabSize]
// spaces. (This may eventually be changed to behave like regular tabs.)
// - Ctrl-H, Backspace: Delete one character to the left of the cursor.
// - Ctrl-D, Delete: Delete the character under the cursor (or the first
// character on the next line if the cursor is at the end of a line).
// - Alt-Backspace: Delete the word to the left of the cursor.
// - Ctrl-K: Delete everything under and to the right of the cursor until the
// next newline character.
// - Ctrl-W: Delete from the start of the current word to the left of the
// cursor.
// - Ctrl-U: Delete the current line, i.e. everything after the last newline
// character before the cursor up until the next newline character. This may
// span multiple visible rows if wrapping is enabled.
//
// Text can be selected by moving the cursor while holding the Shift key, to the
// extent that this is supported by the user's terminal. The Ctrl-L key can be
// used to select the entire text. (Ctrl-A already binds to the "Home" key.)
//
// When text is selected:
//
// - Entering a character will replace the selected text with the new
// character.
// - Backspace, delete, Ctrl-H, Ctrl-D: Delete the selected text.
// - Ctrl-Q: Copy the selected text into the clipboard, unselect the text.
// - Ctrl-X: Copy the selected text into the clipboard and delete it.
// - 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 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. Pasting text
// using your operating system's or terminal's own methods may be very slow as
// each character will be pasted individually.
//
// 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.
//
// Undo does not affect the clipboard.
//
// If the mouse is enabled, the following actions are available:
//
// - Left click: Move the cursor to the clicked position or to the end of the
// line if past the last character.
// - Left double-click: Select the word under the cursor.
// - Left click while holding the Shift key: Select text.
// - Scroll wheel: Scroll the text.
//
// [Unicode Standard Annex #29]: https://unicode.org/reports/tr29/
type TextArea struct {
*Box
// The size of the text area. If set to 0, the text area will use the entire
// available space.
width, height int
// The text to be shown in the text area when it is empty.
placeholder string
// The label text shown, usually when part of a form.
label string
// The width of the text area's label.
labelWidth int
// Styles:
// The label style.
labelStyle tcell.Style
// The style of the text. Background colors different from the Box's
// background color may lead to unwanted artefacts.
textStyle tcell.Style
// The style of the selected text.
selectedStyle tcell.Style
// The style of the placeholder text.
placeholderStyle tcell.Style
// Text manipulation related fields:
// The text area's text prior to any editing. It is referenced by spans with
// a negative length.
initialText string
// Any text that's been added by the user at some point. We only ever append
// to this buffer. It is referenced by spans with a positive length.
editText strings.Builder
// The total length of all text in the text area.
length int
// The maximum number of bytes allowed in the text area. If 0, there is no
// limit.
maxLength int
// The piece chain. The first two spans are sentinel spans which don't
// reference anything and always remain in the same place. Spans are never
// deleted from this slice.
spans []textAreaSpan
// Display, navigation, and cursor related fields:
// If set to true, lines that are longer than the available width are
// wrapped onto the next line. If set to false, any characters beyond the
// available width are discarded.
wrap bool
// If set to true and if wrap is also true, lines are split at spaces or
// after punctuation characters.
wordWrap bool
// The index of the first line shown in the text area.
rowOffset int
// The number of cells to be skipped on each line (not used in wrap mode).
columnOffset int
// The inner height and width of the text area the last time it was drawn.
lastHeight, lastWidth int
// The width of the currently known widest line, as determined by
// [TextArea.extendLines].
widestLine int
// Text positions and states of the start of lines. Each element is a span
// position (see [textAreaSpan]). Not all lines of the text may be contained
// at any time, extend as needed with the [TextArea.extendLines] function.
lineStarts [][3]int
// The cursor always points to the next position where a new character would
// be placed. The selection start is the same as cursor as long as there is
// no selection. When there is one, the selection is between selectionStart
// and cursor.
cursor, selectionStart struct {
// The row and column in screen space but relative to the start of the
// text which may be outside the text area's box. The column value may
// be larger than where the cursor actually is if the line the cursor
// is on is shorter. The actualColumn is the position as it is seen on
// screen. These three values may not be determined yet, in which case
// the row is negative.
row, column, actualColumn int
// The textAreaSpan position with state for the actual next character.
pos [3]int
}
// Set to true when the mouse is dragging to select text.
dragging bool
// Clipboard related fields:
// The internal clipboard.
clipboard string
// The function to call when the user copies/cuts a text selection to the
// clipboard.
copyToClipboard func(string)
// 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. Each item is a copy of the span before the
// modified span range and a copy of the span after the modified span range.
// To undo an action, the two referenced 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 []textAreaUndoItem
// 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
// Event handlers:
// An optional function which is called when the input has changed.
changed func()
// An optional function which is called when the position of the cursor or
// the selection has changed.
moved func()
// A callback function set by the Form class and called when the user leaves
// this form item.
finished func(tcell.Key)
}
// NewTextArea returns a new text area. Use [TextArea.SetText] to set the
// initial text.
func NewTextArea() *TextArea {
t := &TextArea{
Box: NewBox(),
wrap: true,
wordWrap: true,
placeholderStyle: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.TertiaryTextColor),
labelStyle: tcell.StyleDefault.Foreground(Styles.SecondaryTextColor),
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}
t.spans[1] = textAreaSpan{previous: 0, next: -1}
t.cursor.pos = [3]int{1, 0, -1}
t.selectionStart = t.cursor
t.SetClipboard(nil, nil)
return t
}
// SetText sets the text of the text area. All existing text is deleted and
// replaced with the new text. Any edits are discarded, no undos are available.
// This function is typically only used to initialize the text area with a text
// after it has been created. To clear the text area's text (again, no undos),
// provide an empty string.
//
// If cursorAtTheEnd is false, the cursor is placed at the start of the text. If
// it is true, it is placed at the end of the text. For very long texts, placing
// the cursor at the end can be an expensive operation because the entire text
// needs to be parsed and laid out.
//
// If you want to set text and preserve undo functionality, use
// [TextArea.Replace] instead.
func (t *TextArea) SetText(text string, cursorAtTheEnd bool) *TextArea {
t.spans = t.spans[:2]
t.initialText = text
t.editText.Reset()
t.lineStarts = nil
t.length = len(text)
t.rowOffset = 0
t.columnOffset = 0
t.reset()
t.cursor.row, t.cursor.actualColumn, t.cursor.column = 0, 0, 0
t.cursor.pos = [3]int{1, 0, -1}
t.undoStack = t.undoStack[:0]
if len(text) > 0 {
t.spans = append(t.spans, textAreaSpan{
previous: 0,
next: 1,
offset: 0,
length: -len(text),
})
t.spans[0].next = 2
t.spans[1].previous = 2
if cursorAtTheEnd {
t.cursor.row = -1
if t.lastWidth > 0 {
t.findCursor(true, 0)
}
} else {
t.cursor.pos = [3]int{2, 0, -1}
}
} else {
t.spans[0].next = 1
t.spans[1].previous = 0
}
t.selectionStart = t.cursor
if t.changed != nil {
t.changed()
}
if t.lastWidth > 0 && t.moved != nil {
t.moved()
}
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()
}
// HasSelection returns whether the selected text is non-empty.
func (t *TextArea) HasSelection() bool {
return t.selectionStart != t.cursor
}
// GetSelection returns the currently selected text and its start and end
// positions within the entire text as a half-open interval. If the returned
// text is an empty string, the start and end positions are the same and can be
// interpreted as the cursor position.
//
// Calling this function will result in string allocations as well as a search
// for text positions. This is expensive if the text has been edited extensively
// already. Use [TextArea.HasSelection] first if you are only interested in
// selected text.
func (t *TextArea) GetSelection() (text string, start int, end int) {
from, to := t.selectionStart.pos, t.cursor.pos
if t.cursor.row < t.selectionStart.row || (t.cursor.row == t.selectionStart.row && t.cursor.actualColumn < t.selectionStart.actualColumn) {
from, to = to, from
}
if from[0] == 1 {
start = t.length
}
if to[0] == 1 {
end = t.length
}
var (
index int
selection strings.Builder
inside bool
)
for span := t.spans[0].next; span != 1; span = t.spans[span].next {
var spanText string
length := t.spans[span].length
if length < 0 {
length = -length
spanText = t.initialText
} else {
spanText = t.editText.String()
}
spanText = spanText[t.spans[span].offset : t.spans[span].offset length]
if from[0] == span && to[0] == span {
if from != to {
selection.WriteString(spanText[from[1]:to[1]])
}
start = index from[1]
end = index to[1]
break
} else if from[0] == span {
if from != to {
selection.WriteString(spanText[from[1]:])
}
start = index from[1]
inside = true
} else if to[0] == span {
if from != to {
selection.WriteString(spanText[:to[1]])
}
end = index to[1]
break
} else if inside && from != to {
selection.WriteString(spanText)
}
index = length
}
if selection.Len() != 0 {
text = selection.String()
}
return
}
// GetCursor returns the current cursor position where the first character of
// the entire text is in row 0, column 0. If the user has selected text, the
// "from" values will refer to the beginning of the selection and the "to"
// values to the end of the selection (exclusive). They are the same if there
// is no selection.
func (t *TextArea) GetCursor() (fromRow, fromColumn, toRow, toColumn int) {
fromRow, fromColumn = t.selectionStart.row, t.selectionStart.actualColumn
toRow, toColumn = t.cursor.row, t.cursor.actualColumn
if toRow < fromRow || (toRow == fromRow && toColumn < fromColumn) {
fromRow, fromColumn, toRow, toColumn = toRow, toColumn, fromRow, fromColumn
}
if t.length > 0 && t.wrap && fromColumn >= t.lastWidth { // This happens when a row has text all the way until the end, pushing the cursor outside the viewport.
fromRow
fromColumn = 0
}
if t.length > 0 && t.wrap && toColumn >= t.lastWidth {
toRow
toColumn = 0
}
return
}
// GetTextLength returns the string length of the text in the text area.
func (t *TextArea) GetTextLength() int {
return t.length
}
// Replace replaces a section of the text with new text. The start and end
// positions refer to index positions within the entire text string (as a
// half-open interval). They may be the same, in which case text is inserted at
// the given position. If the text is an empty string, text between start and
// end is deleted. Index positions will be shifted to line up with character
// boundaries.
//
// Previous selections are cleared. The cursor will be located at the end of the
// replaced text. Scroll offsets will not be changed.
//
// The effects of this function can be undone (and redone) by the user.
func (t *TextArea) Replace(start, end int, text string) *TextArea {
t.Select(start, end)
row := t.selectionStart.row
t.cursor.pos = t.replace(t.selectionStart.pos, t.cursor.pos, text, false)
t.cursor.row = -1
t.truncateLines(row - 1)
t.findCursor(false, row)
t.selectionStart = t.cursor
if t.changed != nil {
t.changed()
}
if t.moved != nil {
t.moved()
}
return t
}
// Select selects a section of the text. The start and end positions refer to
// index positions within the entire text string (as a half-open interval). They
// may be the same, in which case the cursor is placed at the given position.
// Any previous selection is removed. Scroll offsets will be preserved.
//
// Index positions will be shifted to line up with character boundaries.
func (t *TextArea) Select(start, end int) *TextArea {
oldFrom, oldTo := t.selectionStart, t.cursor
defer func() {
if (oldFrom != t.selectionStart || oldTo != t.cursor) && t.moved != nil {
t.moved()
}
}()
// Clamp input values.
if start < 0 {
start = 0
}
if start > t.length {
start = t.length
}
if end < 0 {
end = 0
}
if end > t.length {
end = t.length
}
if end < start {
start, end = end, start
}
// Find the cursor positions.
var row, index int
t.cursor.row, t.cursor.pos = -1, [3]int{1, 0, -1}
t.selectionStart = t.cursor
RowLoop:
for {
if row >= len(t.lineStarts) {
t.extendLines(t.lastWidth, row)
if row >= len(t.lineStarts) {
break
}
}
// Check the spans of this row.
pos := t.lineStarts[row]
var (
next [3]int
lineIndex int
)
if row 1 < len(t.lineStarts) {
next = t.lineStarts[row 1]
} else {
next = [3]int{1, 0, -1}
}
for {
if pos[0] == next[0] {
if start >= index lineIndex && start < index lineIndex next[1]-pos[1] ||
end >= index lineIndex && end < index lineIndex next[1]-pos[1] {
break
}
index = lineIndex next[1] - pos[1]
row
continue RowLoop // Move on to the next row.
} else {
length := t.spans[pos[0]].length
if length < 0 {
length = -length
}
if start >= index lineIndex && start < index lineIndex length-pos[1] ||
end >= index lineIndex && end < index lineIndex length-pos[1] {
break
}
lineIndex = length - pos[1]
pos[0], pos[1] = t.spans[pos[0]].next, 0
}
}
// One of the indices is in this row. Step through it.
pos = t.lineStarts[row]
endPos := pos
var (
cluster, text string
column, width int
)
for pos != next {
if t.selectionStart.row < 0 && start <= index {
t.selectionStart.row, t.selectionStart.column, t.selectionStart.actualColumn = row, column, column
t.selectionStart.pos = pos
}
if t.cursor.row < 0 && end <= index {
t.cursor.row, t.cursor.column, t.cursor.actualColumn = row, column, column
t.cursor.pos = pos
break RowLoop
}
cluster, text, _, width, pos, endPos = t.step(text, pos, endPos)
index = len(cluster)
column = width
}
}
if t.cursor.row < 0 {
t.findCursor(false, 0) // This only happens if we couldn't find the locations above.
t.selectionStart = t.cursor
}
return t
}
// 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.
func (t *TextArea) SetWrap(wrap bool) *TextArea {
if t.wrap != wrap {
t.wrap = wrap
t.reset()
}
return t
}
// SetWordWrap sets the flag that causes lines that are longer than the
// available width to be wrapped onto the next line at spaces or after
// punctuation marks (according to [Unicode Standard Annex #14]). This flag is
// ignored if the flag set with [TextArea.SetWrap] is false. The text area's
// default is word-wrapping.
//
// [Unicode Standard Annex #14]: https://www.unicode.org/reports/tr14/
func (t *TextArea) SetWordWrap(wrapOnWords bool) *TextArea {
if t.wordWrap != wrapOnWords {
t.wordWrap = wrapOnWords
t.reset()
}
return t
}
// SetPlaceholder sets the text to be displayed when the text area is empty.
func (t *TextArea) SetPlaceholder(placeholder string) *TextArea {
t.placeholder = placeholder
return t
}
// SetLabel sets the text to be displayed before the text area.
func (t *TextArea) SetLabel(label string) *TextArea {
t.label = label
return t
}
// GetLabel returns the text to be displayed before the text area.
func (t *TextArea) GetLabel() string {
return t.label
}
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
// primitive to use the width of the label string.
func (t *TextArea) SetLabelWidth(width int) *TextArea {
t.labelWidth = width
return t
}
// SetSize sets the screen size of the input element of the text area. The input
// element is always located next to the label which is always located in the
// top left corner. If any of the values are 0 or larger than the available
// space, the available space will be used.
func (t *TextArea) SetSize(rows, columns int) *TextArea {
t.width = columns
t.height = rows
return t
}
// GetFieldWidth returns this primitive's field width.
func (t *TextArea) GetFieldWidth() int {
return t.width
}
// GetFieldHeight returns this primitive's field height.
func (t *TextArea) GetFieldHeight() int {
return t.height
}
// SetMaxLength sets the maximum number of bytes allowed in the text area. A
// value of 0 means there is no limit. If the text area currently contains more
// bytes than this, it may violate this constraint.
func (t *TextArea) SetMaxLength(maxLength int) *TextArea {
t.maxLength = maxLength
return t
}
// SetLabelStyle sets the style of the label.
func (t *TextArea) SetLabelStyle(style tcell.Style) *TextArea {
t.labelStyle = style
return t
}
// GetLabelStyle returns the style of the label.
func (t *TextArea) GetLabelStyle() tcell.Style {
return t.labelStyle
}
// SetTextStyle sets the style of the text. Background colors different from the
// Box's background color may lead to unwanted artefacts.
func (t *TextArea) SetTextStyle(style tcell.Style) *TextArea {
t.textStyle = style
return t
}
// SetSelectedStyle sets the style of the selected text.
func (t *TextArea) SetSelectedStyle(style tcell.Style) *TextArea {
t.selectedStyle = style
return t
}
// SetPlaceholderStyle sets the style of the placeholder text.
func (t *TextArea) SetPlaceholderStyle(style tcell.Style) *TextArea {
t.placeholderStyle = style
return t
}
// GetOffset returns the text's offset, that is, the number of rows and columns
// skipped during drawing at the top or on the left, respectively. Note that the
// column offset is ignored if wrapping is enabled.
func (t *TextArea) GetOffset() (row, column int) {
return t.rowOffset, t.columnOffset
}
// SetOffset sets the text's offset, that is, the number of rows and columns
// skipped during drawing at the top or on the left, respectively. If wrapping
// is enabled, the column offset is ignored. These values may get adjusted
// automatically to ensure that some text is always visible.
func (t *TextArea) SetOffset(row, column int) *TextArea {
t.rowOffset, t.columnOffset = row, column
return t
}
// SetClipboard allows you to implement your own clipboard by providing a
// function that is called when the user wishes to store text in the clipboard
// (copyToClipboard) and a function that is called when the user wishes to
// retrieve text from the clipboard (pasteFromClipboard).
//
// Providing nil values will cause the default clipboard implementation to be
// used.
func (t *TextArea) SetClipboard(copyToClipboard func(string), pasteFromClipboard func() string) *TextArea {
t.copyToClipboard = copyToClipboard
if t.copyToClipboard == nil {
t.copyToClipboard = func(text string) {
t.clipboard = text
}
}
t.pasteFromClipboard = pasteFromClipboard
if t.pasteFromClipboard == nil {
t.pasteFromClipboard = func() string {
return t.clipboard
}
}
return t
}
// SetChangedFunc sets a handler which is called whenever the text of the text
// area has changed.
func (t *TextArea) SetChangedFunc(handler func()) *TextArea {
t.changed = handler
return t
}
// SetMovedFunc sets a handler which is called whenever the cursor position or
// the text selection has changed.
func (t *TextArea) SetMovedFunc(handler func()) *TextArea {
t.moved = handler
return t
}
// SetFinishedFunc sets a callback invoked when the user leaves this form item.
func (t *TextArea) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
t.finished = handler
return t
}
// SetFormAttributes sets attributes shared by all form items.
func (t *TextArea) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
t.labelWidth = labelWidth
t.backgroundColor = bgColor
t.labelStyle = t.labelStyle.Foreground(labelColor)
t.textStyle = tcell.StyleDefault.Foreground(fieldTextColor).Background(fieldBgColor)
return t
}
// 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 end position of the deleted/inserted
// range. The provided row is the row of the deleted range start.
//
// The function can hang if "deleteStart" is located after "deleteEnd".
//
// 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 && len(insert) > 0 && t.length len(insert) >= t.maxLength {
return deleteEnd
}
// Notify at the end.
if t.changed != nil {
defer t.changed()
}
// Handle a few cases where we don't put anything onto the undo stack for
// increased efficiency.
if continuation {
// Same action as the one before. An undo item was already generated for
// this block of (same) actions. We're also only changing one character.
switch {
case insert == "" && deleteStart[1] != 0 && deleteEnd[1] == 0:
// Simple backspace. Just shorten this span.
length := t.spans[deleteStart[0]].length
if length < 0 {
t.length -= -length - deleteStart[1]
length = -deleteStart[1]
} else {
t.length -= length - deleteStart[1]
length = deleteStart[1]
}
t.spans[deleteStart[0]].length = length
return deleteEnd
case insert == "" && deleteStart[1] == 0 && deleteEnd[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]
deleteEnd[1] = 0
return deleteEnd
case 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
}
}
}
// 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
}
t.undoStack = t.undoStack[:t.nextUndo]
t.undoStack = append(t.undoStack, textAreaUndoItem{
before: len(t.spans),
after: len(t.spans) 1,
originalBefore: before,
originalAfter: after,
length: t.length,
pos: t.cursor.pos,
continuation: continuation,
})
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
}
}
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)
}
// 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)
}