1// Package textarea provides a multi-line text input component for Bubble Tea
2// applications.
3package textarea
4
5import (
6 "crypto/sha256"
7 "fmt"
8 "image/color"
9 "slices"
10 "strconv"
11 "strings"
12 "time"
13 "unicode"
14
15 "github.com/atotto/clipboard"
16 "github.com/charmbracelet/bubbles/v2/cursor"
17 "github.com/charmbracelet/bubbles/v2/internal/memoization"
18 "github.com/charmbracelet/bubbles/v2/internal/runeutil"
19 "github.com/charmbracelet/bubbles/v2/key"
20 "github.com/charmbracelet/bubbles/v2/viewport"
21 tea "github.com/charmbracelet/bubbletea/v2"
22 "github.com/charmbracelet/lipgloss/v2"
23 "github.com/charmbracelet/x/ansi"
24 rw "github.com/mattn/go-runewidth"
25 "github.com/rivo/uniseg"
26)
27
28const (
29 minHeight = 1
30 defaultHeight = 6
31 defaultWidth = 40
32 defaultCharLimit = 0 // no limit
33 defaultMaxHeight = 99
34 defaultMaxWidth = 500
35
36 // XXX: in v2, make max lines dynamic and default max lines configurable.
37 maxLines = 10000
38)
39
40// Internal messages for clipboard operations.
41type (
42 pasteMsg string
43 pasteErrMsg struct{ error }
44)
45
46// KeyMap is the key bindings for different actions within the textarea.
47type KeyMap struct {
48 CharacterBackward key.Binding
49 CharacterForward key.Binding
50 DeleteAfterCursor key.Binding
51 DeleteBeforeCursor key.Binding
52 DeleteCharacterBackward key.Binding
53 DeleteCharacterForward key.Binding
54 DeleteWordBackward key.Binding
55 DeleteWordForward key.Binding
56 InsertNewline key.Binding
57 LineEnd key.Binding
58 LineNext key.Binding
59 LinePrevious key.Binding
60 LineStart key.Binding
61 Paste key.Binding
62 WordBackward key.Binding
63 WordForward key.Binding
64 InputBegin key.Binding
65 InputEnd key.Binding
66
67 UppercaseWordForward key.Binding
68 LowercaseWordForward key.Binding
69 CapitalizeWordForward key.Binding
70
71 TransposeCharacterBackward key.Binding
72}
73
74// DefaultKeyMap returns the default set of key bindings for navigating and acting
75// upon the textarea.
76func DefaultKeyMap() KeyMap {
77 return KeyMap{
78 CharacterForward: key.NewBinding(key.WithKeys("right", "ctrl+f"), key.WithHelp("right", "character forward")),
79 CharacterBackward: key.NewBinding(key.WithKeys("left", "ctrl+b"), key.WithHelp("left", "character backward")),
80 WordForward: key.NewBinding(key.WithKeys("alt+right", "alt+f"), key.WithHelp("alt+right", "word forward")),
81 WordBackward: key.NewBinding(key.WithKeys("alt+left", "alt+b"), key.WithHelp("alt+left", "word backward")),
82 LineNext: key.NewBinding(key.WithKeys("down", "ctrl+n"), key.WithHelp("down", "next line")),
83 LinePrevious: key.NewBinding(key.WithKeys("up", "ctrl+p"), key.WithHelp("up", "previous line")),
84 DeleteWordBackward: key.NewBinding(key.WithKeys("alt+backspace", "ctrl+w"), key.WithHelp("alt+backspace", "delete word backward")),
85 DeleteWordForward: key.NewBinding(key.WithKeys("alt+delete", "alt+d"), key.WithHelp("alt+delete", "delete word forward")),
86 DeleteAfterCursor: key.NewBinding(key.WithKeys("ctrl+k"), key.WithHelp("ctrl+k", "delete after cursor")),
87 DeleteBeforeCursor: key.NewBinding(key.WithKeys("ctrl+u"), key.WithHelp("ctrl+u", "delete before cursor")),
88 InsertNewline: key.NewBinding(key.WithKeys("enter", "ctrl+m"), key.WithHelp("enter", "insert newline")),
89 DeleteCharacterBackward: key.NewBinding(key.WithKeys("backspace", "ctrl+h"), key.WithHelp("backspace", "delete character backward")),
90 DeleteCharacterForward: key.NewBinding(key.WithKeys("delete", "ctrl+d"), key.WithHelp("delete", "delete character forward")),
91 LineStart: key.NewBinding(key.WithKeys("home", "ctrl+a"), key.WithHelp("home", "line start")),
92 LineEnd: key.NewBinding(key.WithKeys("end", "ctrl+e"), key.WithHelp("end", "line end")),
93 Paste: key.NewBinding(key.WithKeys("ctrl+v"), key.WithHelp("ctrl+v", "paste")),
94 InputBegin: key.NewBinding(key.WithKeys("alt+<", "ctrl+home"), key.WithHelp("alt+<", "input begin")),
95 InputEnd: key.NewBinding(key.WithKeys("alt+>", "ctrl+end"), key.WithHelp("alt+>", "input end")),
96
97 CapitalizeWordForward: key.NewBinding(key.WithKeys("alt+c"), key.WithHelp("alt+c", "capitalize word forward")),
98 LowercaseWordForward: key.NewBinding(key.WithKeys("alt+l"), key.WithHelp("alt+l", "lowercase word forward")),
99 UppercaseWordForward: key.NewBinding(key.WithKeys("alt+u"), key.WithHelp("alt+u", "uppercase word forward")),
100
101 TransposeCharacterBackward: key.NewBinding(key.WithKeys("ctrl+t"), key.WithHelp("ctrl+t", "transpose character backward")),
102 }
103}
104
105// LineInfo is a helper for keeping track of line information regarding
106// soft-wrapped lines.
107type LineInfo struct {
108 // Width is the number of columns in the line.
109 Width int
110
111 // CharWidth is the number of characters in the line to account for
112 // double-width runes.
113 CharWidth int
114
115 // Height is the number of rows in the line.
116 Height int
117
118 // StartColumn is the index of the first column of the line.
119 StartColumn int
120
121 // ColumnOffset is the number of columns that the cursor is offset from the
122 // start of the line.
123 ColumnOffset int
124
125 // RowOffset is the number of rows that the cursor is offset from the start
126 // of the line.
127 RowOffset int
128
129 // CharOffset is the number of characters that the cursor is offset
130 // from the start of the line. This will generally be equivalent to
131 // ColumnOffset, but will be different there are double-width runes before
132 // the cursor.
133 CharOffset int
134}
135
136// CursorStyle is the style for real and virtual cursors.
137type CursorStyle struct {
138 // Style styles the cursor block.
139 //
140 // For real cursors, the foreground color set here will be used as the
141 // cursor color.
142 Color color.Color
143
144 // Shape is the cursor shape. The following shapes are available:
145 //
146 // - tea.CursorBlock
147 // - tea.CursorUnderline
148 // - tea.CursorBar
149 //
150 // This is only used for real cursors.
151 Shape tea.CursorShape
152
153 // CursorBlink determines whether or not the cursor should blink.
154 Blink bool
155
156 // BlinkSpeed is the speed at which the virtual cursor blinks. This has no
157 // effect on real cursors as well as no effect if the cursor is set not to
158 // [CursorBlink].
159 //
160 // By default, the blink speed is set to about 500ms.
161 BlinkSpeed time.Duration
162}
163
164// Styles are the styles for the textarea, separated into focused and blurred
165// states. The appropriate styles will be chosen based on the focus state of
166// the textarea.
167type Styles struct {
168 Focused StyleState
169 Blurred StyleState
170 Cursor CursorStyle
171}
172
173// StyleState that will be applied to the text area.
174//
175// StyleState can be applied to focused and unfocused states to change the styles
176// depending on the focus state.
177//
178// For an introduction to styling with Lip Gloss see:
179// https://github.com/charmbracelet/lipgloss
180type StyleState struct {
181 Base lipgloss.Style
182 Text lipgloss.Style
183 LineNumber lipgloss.Style
184 CursorLineNumber lipgloss.Style
185 CursorLine lipgloss.Style
186 EndOfBuffer lipgloss.Style
187 Placeholder lipgloss.Style
188 Prompt lipgloss.Style
189}
190
191func (s StyleState) computedCursorLine() lipgloss.Style {
192 return s.CursorLine.Inherit(s.Base).Inline(true)
193}
194
195func (s StyleState) computedCursorLineNumber() lipgloss.Style {
196 return s.CursorLineNumber.
197 Inherit(s.CursorLine).
198 Inherit(s.Base).
199 Inline(true)
200}
201
202func (s StyleState) computedEndOfBuffer() lipgloss.Style {
203 return s.EndOfBuffer.Inherit(s.Base).Inline(true)
204}
205
206func (s StyleState) computedLineNumber() lipgloss.Style {
207 return s.LineNumber.Inherit(s.Base).Inline(true)
208}
209
210func (s StyleState) computedPlaceholder() lipgloss.Style {
211 return s.Placeholder.Inherit(s.Base).Inline(true)
212}
213
214func (s StyleState) computedPrompt() lipgloss.Style {
215 return s.Prompt.Inherit(s.Base).Inline(true)
216}
217
218func (s StyleState) computedText() lipgloss.Style {
219 return s.Text.Inherit(s.Base).Inline(true)
220}
221
222// line is the input to the text wrapping function. This is stored in a struct
223// so that it can be hashed and memoized.
224type line struct {
225 runes []rune
226 width int
227}
228
229// Hash returns a hash of the line.
230func (w line) Hash() string {
231 v := fmt.Sprintf("%s:%d", string(w.runes), w.width)
232 return fmt.Sprintf("%x", sha256.Sum256([]byte(v)))
233}
234
235// Model is the Bubble Tea model for this text area element.
236type Model struct {
237 Err error
238
239 // General settings.
240 cache *memoization.MemoCache[line, [][]rune]
241
242 // Prompt is printed at the beginning of each line.
243 //
244 // When changing the value of Prompt after the model has been
245 // initialized, ensure that SetWidth() gets called afterwards.
246 //
247 // See also [SetPromptFunc] for a dynamic prompt.
248 Prompt string
249
250 // Placeholder is the text displayed when the user
251 // hasn't entered anything yet.
252 Placeholder string
253
254 // ShowLineNumbers, if enabled, causes line numbers to be printed
255 // after the prompt.
256 ShowLineNumbers bool
257
258 // EndOfBufferCharacter is displayed at the end of the input.
259 EndOfBufferCharacter rune
260
261 // KeyMap encodes the keybindings recognized by the widget.
262 KeyMap KeyMap
263
264 // virtualCursor manages the virtual cursor.
265 virtualCursor cursor.Model
266
267 // CharLimit is the maximum number of characters this input element will
268 // accept. If 0 or less, there's no limit.
269 CharLimit int
270
271 // MaxHeight is the maximum height of the text area in rows. If 0 or less,
272 // there's no limit.
273 MaxHeight int
274
275 // MaxWidth is the maximum width of the text area in columns. If 0 or less,
276 // there's no limit.
277 MaxWidth int
278
279 // Styling. Styles are defined in [Styles]. Use [SetStyles] and [GetStyles]
280 // to work with this value publicly.
281 styles Styles
282
283 // useVirtualCursor determines whether or not to use the virtual cursor.
284 // Use [SetVirtualCursor] and [VirtualCursor] to work with this this
285 // value publicly.
286 useVirtualCursor bool
287
288 // If promptFunc is set, it replaces Prompt as a generator for
289 // prompt strings at the beginning of each line.
290 promptFunc func(line int, focused bool) string
291
292 // promptWidth is the width of the prompt.
293 promptWidth int
294
295 // width is the maximum number of characters that can be displayed at once.
296 // If 0 or less this setting is ignored.
297 width int
298
299 // height is the maximum number of lines that can be displayed at once. It
300 // essentially treats the text field like a vertically scrolling viewport
301 // if there are more lines than the permitted height.
302 height int
303
304 // Underlying text value.
305 value [][]rune
306
307 // focus indicates whether user input focus should be on this input
308 // component. When false, ignore keyboard input and hide the cursor.
309 focus bool
310
311 // Cursor column.
312 col int
313
314 // Cursor row.
315 row int
316
317 // Last character offset, used to maintain state when the cursor is moved
318 // vertically such that we can maintain the same navigating position.
319 lastCharOffset int
320
321 // viewport is the vertically-scrollable viewport of the multi-line text
322 // input.
323 viewport *viewport.Model
324
325 // rune sanitizer for input.
326 rsan runeutil.Sanitizer
327}
328
329// New creates a new model with default settings.
330func New() Model {
331 vp := viewport.New()
332 vp.KeyMap = viewport.KeyMap{}
333 cur := cursor.New()
334
335 styles := DefaultDarkStyles()
336
337 m := Model{
338 CharLimit: defaultCharLimit,
339 MaxHeight: defaultMaxHeight,
340 MaxWidth: defaultMaxWidth,
341 Prompt: lipgloss.ThickBorder().Left + " ",
342 styles: styles,
343 cache: memoization.NewMemoCache[line, [][]rune](maxLines),
344 EndOfBufferCharacter: ' ',
345 ShowLineNumbers: true,
346 useVirtualCursor: true,
347 virtualCursor: cur,
348 KeyMap: DefaultKeyMap(),
349
350 value: make([][]rune, minHeight, maxLines),
351 focus: false,
352 col: 0,
353 row: 0,
354
355 viewport: &vp,
356 }
357
358 m.SetHeight(defaultHeight)
359 m.SetWidth(defaultWidth)
360
361 return m
362}
363
364// DefaultStyles returns the default styles for focused and blurred states for
365// the textarea.
366func DefaultStyles(isDark bool) Styles {
367 lightDark := lipgloss.LightDark(isDark)
368
369 var s Styles
370 s.Focused = StyleState{
371 Base: lipgloss.NewStyle(),
372 CursorLine: lipgloss.NewStyle().Background(lightDark(lipgloss.Color("255"), lipgloss.Color("0"))),
373 CursorLineNumber: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("240"), lipgloss.Color("240"))),
374 EndOfBuffer: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("254"), lipgloss.Color("0"))),
375 LineNumber: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("249"), lipgloss.Color("7"))),
376 Placeholder: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
377 Prompt: lipgloss.NewStyle().Foreground(lipgloss.Color("7")),
378 Text: lipgloss.NewStyle(),
379 }
380 s.Blurred = StyleState{
381 Base: lipgloss.NewStyle(),
382 CursorLine: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("245"), lipgloss.Color("7"))),
383 CursorLineNumber: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("249"), lipgloss.Color("7"))),
384 EndOfBuffer: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("254"), lipgloss.Color("0"))),
385 LineNumber: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("249"), lipgloss.Color("7"))),
386 Placeholder: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
387 Prompt: lipgloss.NewStyle().Foreground(lipgloss.Color("7")),
388 Text: lipgloss.NewStyle().Foreground(lightDark(lipgloss.Color("245"), lipgloss.Color("7"))),
389 }
390 s.Cursor = CursorStyle{
391 Color: lipgloss.Color("7"),
392 Shape: tea.CursorBlock,
393 Blink: true,
394 }
395 return s
396}
397
398// DefaultLightStyles returns the default styles for a light background.
399func DefaultLightStyles() Styles {
400 return DefaultStyles(false)
401}
402
403// DefaultDarkStyles returns the default styles for a dark background.
404func DefaultDarkStyles() Styles {
405 return DefaultStyles(true)
406}
407
408// Styles returns the current styles for the textarea.
409func (m Model) Styles() Styles {
410 return m.styles
411}
412
413// SetStyles updates styling for the textarea.
414func (m *Model) SetStyles(s Styles) {
415 m.styles = s
416 m.updateVirtualCursorStyle()
417}
418
419// VirtualCursor returns whether or not the virtual cursor is enabled.
420func (m Model) VirtualCursor() bool {
421 return m.useVirtualCursor
422}
423
424// SetVirtualCursor sets whether or not to use the virtual cursor.
425func (m *Model) SetVirtualCursor(v bool) {
426 m.useVirtualCursor = v
427 m.updateVirtualCursorStyle()
428}
429
430// updateVirtualCursorStyle sets styling on the virtual cursor based on the
431// textarea's style settings.
432func (m *Model) updateVirtualCursorStyle() {
433 if !m.useVirtualCursor {
434 m.virtualCursor.SetMode(cursor.CursorHide)
435 return
436 }
437
438 m.virtualCursor.Style = lipgloss.NewStyle().Foreground(m.styles.Cursor.Color)
439
440 // By default, the blink speed of the cursor is set to a default
441 // internally.
442 if m.styles.Cursor.Blink {
443 if m.styles.Cursor.BlinkSpeed > 0 {
444 m.virtualCursor.BlinkSpeed = m.styles.Cursor.BlinkSpeed
445 }
446 m.virtualCursor.SetMode(cursor.CursorBlink)
447 return
448 }
449 m.virtualCursor.SetMode(cursor.CursorStatic)
450}
451
452// SetValue sets the value of the text input.
453func (m *Model) SetValue(s string) {
454 m.Reset()
455 m.InsertString(s)
456}
457
458// InsertString inserts a string at the cursor position.
459func (m *Model) InsertString(s string) {
460 m.insertRunesFromUserInput([]rune(s))
461}
462
463// InsertRune inserts a rune at the cursor position.
464func (m *Model) InsertRune(r rune) {
465 m.insertRunesFromUserInput([]rune{r})
466}
467
468// insertRunesFromUserInput inserts runes at the current cursor position.
469func (m *Model) insertRunesFromUserInput(runes []rune) {
470 // Clean up any special characters in the input provided by the
471 // clipboard. This avoids bugs due to e.g. tab characters and
472 // whatnot.
473 runes = m.san().Sanitize(runes)
474
475 if m.CharLimit > 0 {
476 availSpace := m.CharLimit - m.Length()
477 // If the char limit's been reached, cancel.
478 if availSpace <= 0 {
479 return
480 }
481 // If there's not enough space to paste the whole thing cut the pasted
482 // runes down so they'll fit.
483 if availSpace < len(runes) {
484 runes = runes[:availSpace]
485 }
486 }
487
488 // Split the input into lines.
489 var lines [][]rune
490 lstart := 0
491 for i := range runes {
492 if runes[i] == '\n' {
493 // Queue a line to become a new row in the text area below.
494 // Beware to clamp the max capacity of the slice, to ensure no
495 // data from different rows get overwritten when later edits
496 // will modify this line.
497 lines = append(lines, runes[lstart:i:i])
498 lstart = i + 1
499 }
500 }
501 if lstart <= len(runes) {
502 // The last line did not end with a newline character.
503 // Take it now.
504 lines = append(lines, runes[lstart:])
505 }
506
507 // Obey the maximum line limit.
508 if maxLines > 0 && len(m.value)+len(lines)-1 > maxLines {
509 allowedHeight := max(0, maxLines-len(m.value)+1)
510 lines = lines[:allowedHeight]
511 }
512
513 if len(lines) == 0 {
514 // Nothing left to insert.
515 return
516 }
517
518 // Save the remainder of the original line at the current
519 // cursor position.
520 tail := make([]rune, len(m.value[m.row][m.col:]))
521 copy(tail, m.value[m.row][m.col:])
522
523 // Paste the first line at the current cursor position.
524 m.value[m.row] = append(m.value[m.row][:m.col], lines[0]...)
525 m.col += len(lines[0])
526
527 if numExtraLines := len(lines) - 1; numExtraLines > 0 {
528 // Add the new lines.
529 // We try to reuse the slice if there's already space.
530 var newGrid [][]rune
531 if cap(m.value) >= len(m.value)+numExtraLines {
532 // Can reuse the extra space.
533 newGrid = m.value[:len(m.value)+numExtraLines]
534 } else {
535 // No space left; need a new slice.
536 newGrid = make([][]rune, len(m.value)+numExtraLines)
537 copy(newGrid, m.value[:m.row+1])
538 }
539 // Add all the rows that were after the cursor in the original
540 // grid at the end of the new grid.
541 copy(newGrid[m.row+1+numExtraLines:], m.value[m.row+1:])
542 m.value = newGrid
543 // Insert all the new lines in the middle.
544 for _, l := range lines[1:] {
545 m.row++
546 m.value[m.row] = l
547 m.col = len(l)
548 }
549 }
550
551 // Finally add the tail at the end of the last line inserted.
552 m.value[m.row] = append(m.value[m.row], tail...)
553
554 m.SetCursorColumn(m.col)
555}
556
557// Value returns the value of the text input.
558func (m Model) Value() string {
559 if m.value == nil {
560 return ""
561 }
562
563 var v strings.Builder
564 for _, l := range m.value {
565 v.WriteString(string(l))
566 v.WriteByte('\n')
567 }
568
569 return strings.TrimSuffix(v.String(), "\n")
570}
571
572// Length returns the number of characters currently in the text input.
573func (m *Model) Length() int {
574 var l int
575 for _, row := range m.value {
576 l += uniseg.StringWidth(string(row))
577 }
578 // We add len(m.value) to include the newline characters.
579 return l + len(m.value) - 1
580}
581
582// LineCount returns the number of lines that are currently in the text input.
583func (m *Model) LineCount() int {
584 return len(m.value)
585}
586
587// Line returns the line position.
588func (m Model) Line() int {
589 return m.row
590}
591
592// CursorDown moves the cursor down by one line.
593// Returns whether or not the cursor blink should be reset.
594func (m *Model) CursorDown() {
595 li := m.LineInfo()
596 charOffset := max(m.lastCharOffset, li.CharOffset)
597 m.lastCharOffset = charOffset
598
599 if li.RowOffset+1 >= li.Height && m.row < len(m.value)-1 {
600 m.row++
601 m.col = 0
602 } else {
603 // Move the cursor to the start of the next line so that we can get
604 // the line information. We need to add 2 columns to account for the
605 // trailing space wrapping.
606 const trailingSpace = 2
607 m.col = min(li.StartColumn+li.Width+trailingSpace, len(m.value[m.row])-1)
608 }
609
610 nli := m.LineInfo()
611 m.col = nli.StartColumn
612
613 if nli.Width <= 0 {
614 return
615 }
616
617 offset := 0
618 for offset < charOffset {
619 if m.row >= len(m.value) || m.col >= len(m.value[m.row]) || offset >= nli.CharWidth-1 {
620 break
621 }
622 offset += rw.RuneWidth(m.value[m.row][m.col])
623 m.col++
624 }
625}
626
627// CursorUp moves the cursor up by one line.
628func (m *Model) CursorUp() {
629 li := m.LineInfo()
630 charOffset := max(m.lastCharOffset, li.CharOffset)
631 m.lastCharOffset = charOffset
632
633 if li.RowOffset <= 0 && m.row > 0 {
634 m.row--
635 m.col = len(m.value[m.row])
636 } else {
637 // Move the cursor to the end of the previous line.
638 // This can be done by moving the cursor to the start of the line and
639 // then subtracting 2 to account for the trailing space we keep on
640 // soft-wrapped lines.
641 const trailingSpace = 2
642 m.col = li.StartColumn - trailingSpace
643 }
644
645 nli := m.LineInfo()
646 m.col = nli.StartColumn
647
648 if nli.Width <= 0 {
649 return
650 }
651
652 offset := 0
653 for offset < charOffset {
654 if m.col >= len(m.value[m.row]) || offset >= nli.CharWidth-1 {
655 break
656 }
657 offset += rw.RuneWidth(m.value[m.row][m.col])
658 m.col++
659 }
660}
661
662// SetCursorColumn moves the cursor to the given position. If the position is
663// out of bounds the cursor will be moved to the start or end accordingly.
664func (m *Model) SetCursorColumn(col int) {
665 m.col = clamp(col, 0, len(m.value[m.row]))
666 // Any time that we move the cursor horizontally we need to reset the last
667 // offset so that the horizontal position when navigating is adjusted.
668 m.lastCharOffset = 0
669}
670
671// CursorStart moves the cursor to the start of the input field.
672func (m *Model) CursorStart() {
673 m.SetCursorColumn(0)
674}
675
676// CursorEnd moves the cursor to the end of the input field.
677func (m *Model) CursorEnd() {
678 m.SetCursorColumn(len(m.value[m.row]))
679}
680
681// Focused returns the focus state on the model.
682func (m Model) Focused() bool {
683 return m.focus
684}
685
686// activeStyle returns the appropriate set of styles to use depending on
687// whether the textarea is focused or blurred.
688func (m Model) activeStyle() *StyleState {
689 if m.focus {
690 return &m.styles.Focused
691 }
692 return &m.styles.Blurred
693}
694
695// Focus sets the focus state on the model. When the model is in focus it can
696// receive keyboard input and the cursor will be hidden.
697func (m *Model) Focus() tea.Cmd {
698 m.focus = true
699 return m.virtualCursor.Focus()
700}
701
702// Blur removes the focus state on the model. When the model is blurred it can
703// not receive keyboard input and the cursor will be hidden.
704func (m *Model) Blur() {
705 m.focus = false
706 m.virtualCursor.Blur()
707}
708
709// Reset sets the input to its default state with no input.
710func (m *Model) Reset() {
711 m.value = make([][]rune, minHeight, maxLines)
712 m.col = 0
713 m.row = 0
714 m.viewport.GotoTop()
715 m.SetCursorColumn(0)
716}
717
718// san initializes or retrieves the rune sanitizer.
719func (m *Model) san() runeutil.Sanitizer {
720 if m.rsan == nil {
721 // Textinput has all its input on a single line so collapse
722 // newlines/tabs to single spaces.
723 m.rsan = runeutil.NewSanitizer()
724 }
725 return m.rsan
726}
727
728// deleteBeforeCursor deletes all text before the cursor. Returns whether or
729// not the cursor blink should be reset.
730func (m *Model) deleteBeforeCursor() {
731 m.value[m.row] = m.value[m.row][m.col:]
732 m.SetCursorColumn(0)
733}
734
735// deleteAfterCursor deletes all text after the cursor. Returns whether or not
736// the cursor blink should be reset. If input is masked delete everything after
737// the cursor so as not to reveal word breaks in the masked input.
738func (m *Model) deleteAfterCursor() {
739 m.value[m.row] = m.value[m.row][:m.col]
740 m.SetCursorColumn(len(m.value[m.row]))
741}
742
743// transposeLeft exchanges the runes at the cursor and immediately
744// before. No-op if the cursor is at the beginning of the line. If
745// the cursor is not at the end of the line yet, moves the cursor to
746// the right.
747func (m *Model) transposeLeft() {
748 if m.col == 0 || len(m.value[m.row]) < 2 {
749 return
750 }
751 if m.col >= len(m.value[m.row]) {
752 m.SetCursorColumn(m.col - 1)
753 }
754 m.value[m.row][m.col-1], m.value[m.row][m.col] = m.value[m.row][m.col], m.value[m.row][m.col-1]
755 if m.col < len(m.value[m.row]) {
756 m.SetCursorColumn(m.col + 1)
757 }
758}
759
760// deleteWordLeft deletes the word left to the cursor. Returns whether or not
761// the cursor blink should be reset.
762func (m *Model) deleteWordLeft() {
763 if m.col == 0 || len(m.value[m.row]) == 0 {
764 return
765 }
766
767 // Linter note: it's critical that we acquire the initial cursor position
768 // here prior to altering it via SetCursor() below. As such, moving this
769 // call into the corresponding if clause does not apply here.
770 oldCol := m.col
771
772 m.SetCursorColumn(m.col - 1)
773 for unicode.IsSpace(m.value[m.row][m.col]) {
774 if m.col <= 0 {
775 break
776 }
777 // ignore series of whitespace before cursor
778 m.SetCursorColumn(m.col - 1)
779 }
780
781 for m.col > 0 {
782 if !unicode.IsSpace(m.value[m.row][m.col]) {
783 m.SetCursorColumn(m.col - 1)
784 } else {
785 if m.col > 0 {
786 // keep the previous space
787 m.SetCursorColumn(m.col + 1)
788 }
789 break
790 }
791 }
792
793 if oldCol > len(m.value[m.row]) {
794 m.value[m.row] = m.value[m.row][:m.col]
795 } else {
796 m.value[m.row] = append(m.value[m.row][:m.col], m.value[m.row][oldCol:]...)
797 }
798}
799
800// deleteWordRight deletes the word right to the cursor.
801func (m *Model) deleteWordRight() {
802 if m.col >= len(m.value[m.row]) || len(m.value[m.row]) == 0 {
803 return
804 }
805
806 oldCol := m.col
807
808 for m.col < len(m.value[m.row]) && unicode.IsSpace(m.value[m.row][m.col]) {
809 // ignore series of whitespace after cursor
810 m.SetCursorColumn(m.col + 1)
811 }
812
813 for m.col < len(m.value[m.row]) {
814 if !unicode.IsSpace(m.value[m.row][m.col]) {
815 m.SetCursorColumn(m.col + 1)
816 } else {
817 break
818 }
819 }
820
821 if m.col > len(m.value[m.row]) {
822 m.value[m.row] = m.value[m.row][:oldCol]
823 } else {
824 m.value[m.row] = append(m.value[m.row][:oldCol], m.value[m.row][m.col:]...)
825 }
826
827 m.SetCursorColumn(oldCol)
828}
829
830// characterRight moves the cursor one character to the right.
831func (m *Model) characterRight() {
832 if m.col < len(m.value[m.row]) {
833 m.SetCursorColumn(m.col + 1)
834 } else {
835 if m.row < len(m.value)-1 {
836 m.row++
837 m.CursorStart()
838 }
839 }
840}
841
842// characterLeft moves the cursor one character to the left.
843// If insideLine is set, the cursor is moved to the last
844// character in the previous line, instead of one past that.
845func (m *Model) characterLeft(insideLine bool) {
846 if m.col == 0 && m.row != 0 {
847 m.row--
848 m.CursorEnd()
849 if !insideLine {
850 return
851 }
852 }
853 if m.col > 0 {
854 m.SetCursorColumn(m.col - 1)
855 }
856}
857
858// wordLeft moves the cursor one word to the left. Returns whether or not the
859// cursor blink should be reset. If input is masked, move input to the start
860// so as not to reveal word breaks in the masked input.
861func (m *Model) wordLeft() {
862 for {
863 m.characterLeft(true /* insideLine */)
864 if m.col < len(m.value[m.row]) && !unicode.IsSpace(m.value[m.row][m.col]) {
865 break
866 }
867 }
868
869 for m.col > 0 {
870 if unicode.IsSpace(m.value[m.row][m.col-1]) {
871 break
872 }
873 m.SetCursorColumn(m.col - 1)
874 }
875}
876
877// wordRight moves the cursor one word to the right. Returns whether or not the
878// cursor blink should be reset. If the input is masked, move input to the end
879// so as not to reveal word breaks in the masked input.
880func (m *Model) wordRight() {
881 m.doWordRight(func(int, int) { /* nothing */ })
882}
883
884func (m *Model) doWordRight(fn func(charIdx int, pos int)) {
885 // Skip spaces forward.
886 for m.col >= len(m.value[m.row]) || unicode.IsSpace(m.value[m.row][m.col]) {
887 if m.row == len(m.value)-1 && m.col == len(m.value[m.row]) {
888 // End of text.
889 break
890 }
891 m.characterRight()
892 }
893
894 charIdx := 0
895 for m.col < len(m.value[m.row]) {
896 if unicode.IsSpace(m.value[m.row][m.col]) {
897 break
898 }
899 fn(charIdx, m.col)
900 m.SetCursorColumn(m.col + 1)
901 charIdx++
902 }
903}
904
905// uppercaseRight changes the word to the right to uppercase.
906func (m *Model) uppercaseRight() {
907 m.doWordRight(func(_ int, i int) {
908 m.value[m.row][i] = unicode.ToUpper(m.value[m.row][i])
909 })
910}
911
912// lowercaseRight changes the word to the right to lowercase.
913func (m *Model) lowercaseRight() {
914 m.doWordRight(func(_ int, i int) {
915 m.value[m.row][i] = unicode.ToLower(m.value[m.row][i])
916 })
917}
918
919// capitalizeRight changes the word to the right to title case.
920func (m *Model) capitalizeRight() {
921 m.doWordRight(func(charIdx int, i int) {
922 if charIdx == 0 {
923 m.value[m.row][i] = unicode.ToTitle(m.value[m.row][i])
924 }
925 })
926}
927
928// LineInfo returns the number of characters from the start of the
929// (soft-wrapped) line and the (soft-wrapped) line width.
930func (m Model) LineInfo() LineInfo {
931 grid := m.memoizedWrap(m.value[m.row], m.width)
932
933 // Find out which line we are currently on. This can be determined by the
934 // m.col and counting the number of runes that we need to skip.
935 var counter int
936 for i, line := range grid {
937 // We've found the line that we are on
938 if counter+len(line) == m.col && i+1 < len(grid) {
939 // We wrap around to the next line if we are at the end of the
940 // previous line so that we can be at the very beginning of the row
941 return LineInfo{
942 CharOffset: 0,
943 ColumnOffset: 0,
944 Height: len(grid),
945 RowOffset: i + 1,
946 StartColumn: m.col,
947 Width: len(grid[i+1]),
948 CharWidth: uniseg.StringWidth(string(line)),
949 }
950 }
951
952 if counter+len(line) >= m.col {
953 return LineInfo{
954 CharOffset: uniseg.StringWidth(string(line[:max(0, m.col-counter)])),
955 ColumnOffset: m.col - counter,
956 Height: len(grid),
957 RowOffset: i,
958 StartColumn: counter,
959 Width: len(line),
960 CharWidth: uniseg.StringWidth(string(line)),
961 }
962 }
963
964 counter += len(line)
965 }
966 return LineInfo{}
967}
968
969// repositionView repositions the view of the viewport based on the defined
970// scrolling behavior.
971func (m *Model) repositionView() {
972 minimum := m.viewport.YOffset()
973 maximum := minimum + m.viewport.Height() - 1
974 if row := m.cursorLineNumber(); row < minimum {
975 m.viewport.ScrollUp(minimum - row)
976 } else if row > maximum {
977 m.viewport.ScrollDown(row - maximum)
978 }
979}
980
981// Width returns the width of the textarea.
982func (m Model) Width() int {
983 return m.width
984}
985
986// moveToBegin moves the cursor to the beginning of the input.
987func (m *Model) moveToBegin() {
988 m.row = 0
989 m.SetCursorColumn(0)
990}
991
992// moveToEnd moves the cursor to the end of the input.
993func (m *Model) moveToEnd() {
994 m.row = len(m.value) - 1
995 m.SetCursorColumn(len(m.value[m.row]))
996}
997
998// SetWidth sets the width of the textarea to fit exactly within the given width.
999// This means that the textarea will account for the width of the prompt and
1000// whether or not line numbers are being shown.
1001//
1002// Ensure that SetWidth is called after setting the Prompt and ShowLineNumbers,
1003// It is important that the width of the textarea be exactly the given width
1004// and no more.
1005func (m *Model) SetWidth(w int) {
1006 // Update prompt width only if there is no prompt function as
1007 // [SetPromptFunc] updates the prompt width when it is called.
1008 if m.promptFunc == nil {
1009 // XXX: Do we even need this or can we calculate the prompt width
1010 // at render time?
1011 m.promptWidth = uniseg.StringWidth(m.Prompt)
1012 }
1013
1014 // Add base style borders and padding to reserved outer width.
1015 reservedOuter := m.activeStyle().Base.GetHorizontalFrameSize()
1016
1017 // Add prompt width to reserved inner width.
1018 reservedInner := m.promptWidth
1019
1020 // Add line number width to reserved inner width.
1021 if m.ShowLineNumbers {
1022 // XXX: this was originally documented as needing "1 cell" but was,
1023 // in practice, effectively hardcoded to 2 cells. We can, and should,
1024 // reduce this to one gap and update the tests accordingly.
1025 const gap = 2
1026
1027 // Number of digits plus 1 cell for the margin.
1028 reservedInner += numDigits(m.MaxHeight) + gap
1029 }
1030
1031 // Input width must be at least one more than the reserved inner and outer
1032 // width. This gives us a minimum input width of 1.
1033 minWidth := reservedInner + reservedOuter + 1
1034 inputWidth := max(w, minWidth)
1035
1036 // Input width must be no more than maximum width.
1037 if m.MaxWidth > 0 {
1038 inputWidth = min(inputWidth, m.MaxWidth)
1039 }
1040
1041 // Since the width of the viewport and input area is dependent on the width of
1042 // borders, prompt and line numbers, we need to calculate it by subtracting
1043 // the reserved width from them.
1044
1045 m.viewport.SetWidth(inputWidth - reservedOuter)
1046 m.width = inputWidth - reservedOuter - reservedInner
1047}
1048
1049// SetPromptFunc supersedes the Prompt field and sets a dynamic prompt instead.
1050//
1051// If the function returns a prompt that is shorter than the specified
1052// promptWidth, it will be padded to the left. If it returns a prompt that is
1053// longer, display artifacts may occur; the caller is responsible for computing
1054// an adequate promptWidth.
1055func (m *Model) SetPromptFunc(promptWidth int, fn func(lineIndex int, focused bool) string) {
1056 m.promptFunc = fn
1057 m.promptWidth = promptWidth
1058}
1059
1060// Height returns the current height of the textarea.
1061func (m Model) Height() int {
1062 return m.height
1063}
1064
1065// SetHeight sets the height of the textarea.
1066func (m *Model) SetHeight(h int) {
1067 if m.MaxHeight > 0 {
1068 m.height = clamp(h, minHeight, m.MaxHeight)
1069 m.viewport.SetHeight(clamp(h, minHeight, m.MaxHeight))
1070 } else {
1071 m.height = max(h, minHeight)
1072 m.viewport.SetHeight(max(h, minHeight))
1073 }
1074}
1075
1076// Update is the Bubble Tea update loop.
1077func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
1078 if !m.focus {
1079 m.virtualCursor.Blur()
1080 return m, nil
1081 }
1082
1083 // Used to determine if the cursor should blink.
1084 oldRow, oldCol := m.cursorLineNumber(), m.col
1085
1086 var cmds []tea.Cmd
1087
1088 if m.value[m.row] == nil {
1089 m.value[m.row] = make([]rune, 0)
1090 }
1091
1092 if m.MaxHeight > 0 && m.MaxHeight != m.cache.Capacity() {
1093 m.cache = memoization.NewMemoCache[line, [][]rune](m.MaxHeight)
1094 }
1095
1096 switch msg := msg.(type) {
1097 case tea.PasteMsg:
1098 m.insertRunesFromUserInput([]rune(msg))
1099 case tea.KeyPressMsg:
1100 switch {
1101 case key.Matches(msg, m.KeyMap.DeleteAfterCursor):
1102 m.col = clamp(m.col, 0, len(m.value[m.row]))
1103 if m.col >= len(m.value[m.row]) {
1104 m.mergeLineBelow(m.row)
1105 break
1106 }
1107 m.deleteAfterCursor()
1108 case key.Matches(msg, m.KeyMap.DeleteBeforeCursor):
1109 m.col = clamp(m.col, 0, len(m.value[m.row]))
1110 if m.col <= 0 {
1111 m.mergeLineAbove(m.row)
1112 break
1113 }
1114 m.deleteBeforeCursor()
1115 case key.Matches(msg, m.KeyMap.DeleteCharacterBackward):
1116 m.col = clamp(m.col, 0, len(m.value[m.row]))
1117 if m.col <= 0 {
1118 m.mergeLineAbove(m.row)
1119 break
1120 }
1121 if len(m.value[m.row]) > 0 {
1122 m.value[m.row] = append(m.value[m.row][:max(0, m.col-1)], m.value[m.row][m.col:]...)
1123 if m.col > 0 {
1124 m.SetCursorColumn(m.col - 1)
1125 }
1126 }
1127 case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
1128 if len(m.value[m.row]) > 0 && m.col < len(m.value[m.row]) {
1129 m.value[m.row] = slices.Delete(m.value[m.row], m.col, m.col+1)
1130 }
1131 if m.col >= len(m.value[m.row]) {
1132 m.mergeLineBelow(m.row)
1133 break
1134 }
1135 case key.Matches(msg, m.KeyMap.DeleteWordBackward):
1136 if m.col <= 0 {
1137 m.mergeLineAbove(m.row)
1138 break
1139 }
1140 m.deleteWordLeft()
1141 case key.Matches(msg, m.KeyMap.DeleteWordForward):
1142 m.col = clamp(m.col, 0, len(m.value[m.row]))
1143 if m.col >= len(m.value[m.row]) {
1144 m.mergeLineBelow(m.row)
1145 break
1146 }
1147 m.deleteWordRight()
1148 case key.Matches(msg, m.KeyMap.InsertNewline):
1149 if m.MaxHeight > 0 && len(m.value) >= m.MaxHeight {
1150 return m, nil
1151 }
1152 m.col = clamp(m.col, 0, len(m.value[m.row]))
1153 m.splitLine(m.row, m.col)
1154 case key.Matches(msg, m.KeyMap.LineEnd):
1155 m.CursorEnd()
1156 case key.Matches(msg, m.KeyMap.LineStart):
1157 m.CursorStart()
1158 case key.Matches(msg, m.KeyMap.CharacterForward):
1159 m.characterRight()
1160 case key.Matches(msg, m.KeyMap.LineNext):
1161 m.CursorDown()
1162 case key.Matches(msg, m.KeyMap.WordForward):
1163 m.wordRight()
1164 case key.Matches(msg, m.KeyMap.Paste):
1165 return m, Paste
1166 case key.Matches(msg, m.KeyMap.CharacterBackward):
1167 m.characterLeft(false /* insideLine */)
1168 case key.Matches(msg, m.KeyMap.LinePrevious):
1169 m.CursorUp()
1170 case key.Matches(msg, m.KeyMap.WordBackward):
1171 m.wordLeft()
1172 case key.Matches(msg, m.KeyMap.InputBegin):
1173 m.moveToBegin()
1174 case key.Matches(msg, m.KeyMap.InputEnd):
1175 m.moveToEnd()
1176 case key.Matches(msg, m.KeyMap.LowercaseWordForward):
1177 m.lowercaseRight()
1178 case key.Matches(msg, m.KeyMap.UppercaseWordForward):
1179 m.uppercaseRight()
1180 case key.Matches(msg, m.KeyMap.CapitalizeWordForward):
1181 m.capitalizeRight()
1182 case key.Matches(msg, m.KeyMap.TransposeCharacterBackward):
1183 m.transposeLeft()
1184
1185 default:
1186 m.insertRunesFromUserInput([]rune(msg.Text))
1187 }
1188
1189 case pasteMsg:
1190 m.insertRunesFromUserInput([]rune(msg))
1191
1192 case pasteErrMsg:
1193 m.Err = msg
1194 }
1195
1196 vp, cmd := m.viewport.Update(msg)
1197 m.viewport = &vp
1198 cmds = append(cmds, cmd)
1199
1200 if m.useVirtualCursor {
1201 m.virtualCursor, cmd = m.virtualCursor.Update(msg)
1202
1203 // If the cursor has moved, reset the blink state. This is a small UX
1204 // nuance that makes cursor movement obvious and feel snappy.
1205 newRow, newCol := m.cursorLineNumber(), m.col
1206 if (newRow != oldRow || newCol != oldCol) && m.virtualCursor.Mode() == cursor.CursorBlink {
1207 m.virtualCursor.IsBlinked = false
1208 cmd = m.virtualCursor.Blink()
1209 }
1210 cmds = append(cmds, cmd)
1211 }
1212
1213 m.repositionView()
1214
1215 return m, tea.Batch(cmds...)
1216}
1217
1218// View renders the text area in its current state.
1219func (m Model) View() string {
1220 if m.Value() == "" && m.row == 0 && m.col == 0 && m.Placeholder != "" {
1221 return m.placeholderView()
1222 }
1223 m.virtualCursor.TextStyle = m.activeStyle().computedCursorLine()
1224
1225 var (
1226 s strings.Builder
1227 style lipgloss.Style
1228 newLines int
1229 widestLineNumber int
1230 lineInfo = m.LineInfo()
1231 styles = m.activeStyle()
1232 )
1233
1234 displayLine := 0
1235 for l, line := range m.value {
1236 wrappedLines := m.memoizedWrap(line, m.width)
1237
1238 if m.row == l {
1239 style = styles.computedCursorLine()
1240 } else {
1241 style = styles.computedText()
1242 }
1243
1244 for wl, wrappedLine := range wrappedLines {
1245 prompt := m.promptView(displayLine)
1246 prompt = styles.computedPrompt().Render(prompt)
1247 s.WriteString(style.Render(prompt))
1248 displayLine++
1249
1250 var ln string
1251 if m.ShowLineNumbers {
1252 if wl == 0 { // normal line
1253 isCursorLine := m.row == l
1254 s.WriteString(m.lineNumberView(l+1, isCursorLine))
1255 } else { // soft wrapped line
1256 isCursorLine := m.row == l
1257 s.WriteString(m.lineNumberView(-1, isCursorLine))
1258 }
1259 }
1260
1261 // Note the widest line number for padding purposes later.
1262 lnw := uniseg.StringWidth(ln)
1263 if lnw > widestLineNumber {
1264 widestLineNumber = lnw
1265 }
1266
1267 strwidth := uniseg.StringWidth(string(wrappedLine))
1268 padding := m.width - strwidth
1269 // If the trailing space causes the line to be wider than the
1270 // width, we should not draw it to the screen since it will result
1271 // in an extra space at the end of the line which can look off when
1272 // the cursor line is showing.
1273 if strwidth > m.width {
1274 // The character causing the line to be wider than the width is
1275 // guaranteed to be a space since any other character would
1276 // have been wrapped.
1277 wrappedLine = []rune(strings.TrimSuffix(string(wrappedLine), " "))
1278 padding -= m.width - strwidth
1279 }
1280 if m.row == l && lineInfo.RowOffset == wl {
1281 s.WriteString(style.Render(string(wrappedLine[:lineInfo.ColumnOffset])))
1282 if m.col >= len(line) && lineInfo.CharOffset >= m.width {
1283 m.virtualCursor.SetChar(" ")
1284 s.WriteString(m.virtualCursor.View())
1285 } else {
1286 m.virtualCursor.SetChar(string(wrappedLine[lineInfo.ColumnOffset]))
1287 s.WriteString(style.Render(m.virtualCursor.View()))
1288 s.WriteString(style.Render(string(wrappedLine[lineInfo.ColumnOffset+1:])))
1289 }
1290 } else {
1291 s.WriteString(style.Render(string(wrappedLine)))
1292 }
1293 s.WriteString(style.Render(strings.Repeat(" ", max(0, padding))))
1294 s.WriteRune('\n')
1295 newLines++
1296 }
1297 }
1298
1299 // Always show at least `m.Height` lines at all times.
1300 // To do this we can simply pad out a few extra new lines in the view.
1301 for range m.height {
1302 s.WriteString(m.promptView(displayLine))
1303 displayLine++
1304
1305 // Write end of buffer content
1306 leftGutter := string(m.EndOfBufferCharacter)
1307 rightGapWidth := m.Width() - uniseg.StringWidth(leftGutter) + widestLineNumber
1308 rightGap := strings.Repeat(" ", max(0, rightGapWidth))
1309 s.WriteString(styles.computedEndOfBuffer().Render(leftGutter + rightGap))
1310 s.WriteRune('\n')
1311 }
1312
1313 m.viewport.SetContent(s.String())
1314 return styles.Base.Render(m.viewport.View())
1315}
1316
1317// promptView renders a single line of the prompt.
1318func (m Model) promptView(displayLine int) (prompt string) {
1319 prompt = m.Prompt
1320 if m.promptFunc == nil {
1321 return prompt
1322 }
1323 prompt = m.promptFunc(displayLine, m.focus)
1324 width := lipgloss.Width(prompt)
1325 if width < m.promptWidth {
1326 prompt = fmt.Sprintf("%*s%s", m.promptWidth-width, "", prompt)
1327 }
1328
1329 return m.activeStyle().computedPrompt().Render(prompt)
1330}
1331
1332// lineNumberView renders the line number.
1333//
1334// If the argument is less than 0, a space styled as a line number is returned
1335// instead. Such cases are used for soft-wrapped lines.
1336//
1337// The second argument indicates whether this line number is for a 'cursorline'
1338// line number.
1339func (m Model) lineNumberView(n int, isCursorLine bool) (str string) {
1340 if !m.ShowLineNumbers {
1341 return ""
1342 }
1343
1344 if n <= 0 {
1345 str = " "
1346 } else {
1347 str = strconv.Itoa(n)
1348 }
1349
1350 // XXX: is textStyle really necessary here?
1351 textStyle := m.activeStyle().computedText()
1352 lineNumberStyle := m.activeStyle().computedLineNumber()
1353 if isCursorLine {
1354 textStyle = m.activeStyle().computedCursorLine()
1355 lineNumberStyle = m.activeStyle().computedCursorLineNumber()
1356 }
1357
1358 // Format line number dynamically based on the maximum number of lines.
1359 digits := len(strconv.Itoa(m.MaxHeight))
1360 str = fmt.Sprintf(" %*v ", digits, str)
1361
1362 return textStyle.Render(lineNumberStyle.Render(str))
1363}
1364
1365// placeholderView returns the prompt and placeholder, if any.
1366func (m Model) placeholderView() string {
1367 var (
1368 s strings.Builder
1369 p = m.Placeholder
1370 styles = m.activeStyle()
1371 )
1372 // word wrap lines
1373 pwordwrap := ansi.Wordwrap(p, m.width, "")
1374 // hard wrap lines (handles lines that could not be word wrapped)
1375 pwrap := ansi.Hardwrap(pwordwrap, m.width, true)
1376 // split string by new lines
1377 plines := strings.Split(strings.TrimSpace(pwrap), "\n")
1378
1379 for i := range m.height {
1380 isLineNumber := len(plines) > i
1381
1382 lineStyle := styles.computedPlaceholder()
1383 if len(plines) > i {
1384 lineStyle = styles.computedCursorLine()
1385 }
1386
1387 // render prompt
1388 prompt := m.promptView(i)
1389 prompt = styles.computedPrompt().Render(prompt)
1390 s.WriteString(lineStyle.Render(prompt))
1391
1392 // when show line numbers enabled:
1393 // - render line number for only the cursor line
1394 // - indent other placeholder lines
1395 // this is consistent with vim with line numbers enabled
1396 if m.ShowLineNumbers {
1397 var ln int
1398
1399 switch {
1400 case i == 0:
1401 ln = i + 1
1402 fallthrough
1403 case len(plines) > i:
1404 s.WriteString(m.lineNumberView(ln, isLineNumber))
1405 default:
1406 }
1407 }
1408
1409 switch {
1410 // first line
1411 case i == 0:
1412 // first character of first line as cursor with character
1413 m.virtualCursor.TextStyle = styles.computedPlaceholder()
1414
1415 ch, rest, _, _ := uniseg.FirstGraphemeClusterInString(plines[0], 0)
1416 m.virtualCursor.SetChar(ch)
1417 s.WriteString(lineStyle.Render(m.virtualCursor.View()))
1418
1419 // the rest of the first line
1420 s.WriteString(lineStyle.Render(styles.computedPlaceholder().Render(rest)))
1421
1422 // extend the first line with spaces to fill the width, so that
1423 // the entire line is filled when cursorline is enabled.
1424 gap := strings.Repeat(" ", max(0, m.width-lipgloss.Width(plines[0])))
1425 s.WriteString(lineStyle.Render(gap))
1426 // remaining lines
1427 case len(plines) > i:
1428 // current line placeholder text
1429 if len(plines) > i {
1430 placeholderLine := plines[i]
1431 gap := strings.Repeat(" ", max(0, m.width-uniseg.StringWidth(plines[i])))
1432 s.WriteString(lineStyle.Render(placeholderLine + gap))
1433 }
1434 default:
1435 // end of line buffer character
1436 eob := styles.computedEndOfBuffer().Render(string(m.EndOfBufferCharacter))
1437 s.WriteString(eob)
1438 }
1439
1440 // terminate with new line
1441 s.WriteRune('\n')
1442 }
1443
1444 m.viewport.SetContent(s.String())
1445 return styles.Base.Render(m.viewport.View())
1446}
1447
1448// Blink returns the blink command for the virtual cursor.
1449func Blink() tea.Msg {
1450 return cursor.Blink()
1451}
1452
1453// Cursor returns a [tea.Cursor] for rendering a real cursor in a Bubble Tea
1454// program. This requires that [Model.VirtualCursor] is set to false.
1455//
1456// Note that you will almost certainly also need to adjust the offset cursor
1457// position per the textarea's per the textarea's position in the terminal.
1458//
1459// Example:
1460//
1461// // In your top-level View function:
1462// f := tea.NewFrame(m.textarea.View())
1463// f.Cursor = m.textarea.Cursor()
1464// f.Cursor.Position.X += offsetX
1465// f.Cursor.Position.Y += offsetY
1466func (m Model) Cursor() *tea.Cursor {
1467 if m.useVirtualCursor || !m.Focused() {
1468 return nil
1469 }
1470
1471 lineInfo := m.LineInfo()
1472 w := lipgloss.Width
1473 baseStyle := m.activeStyle().Base
1474
1475 xOffset := lineInfo.CharOffset +
1476 w(m.promptView(0)) +
1477 w(m.lineNumberView(0, false)) +
1478 baseStyle.GetMarginLeft() +
1479 baseStyle.GetPaddingLeft() +
1480 baseStyle.GetBorderLeftSize()
1481
1482 yOffset := m.cursorLineNumber() -
1483 m.viewport.YOffset() +
1484 baseStyle.GetMarginTop() +
1485 baseStyle.GetPaddingTop() +
1486 baseStyle.GetBorderTopSize()
1487
1488 c := tea.NewCursor(xOffset, yOffset)
1489 c.Blink = m.styles.Cursor.Blink
1490 c.Color = m.styles.Cursor.Color
1491 c.Shape = m.styles.Cursor.Shape
1492 return c
1493}
1494
1495func (m Model) memoizedWrap(runes []rune, width int) [][]rune {
1496 input := line{runes: runes, width: width}
1497 if v, ok := m.cache.Get(input); ok {
1498 return v
1499 }
1500 v := wrap(runes, width)
1501 m.cache.Set(input, v)
1502 return v
1503}
1504
1505// cursorLineNumber returns the line number that the cursor is on.
1506// This accounts for soft wrapped lines.
1507func (m Model) cursorLineNumber() int {
1508 line := 0
1509 for i := range m.row {
1510 // Calculate the number of lines that the current line will be split
1511 // into.
1512 line += len(m.memoizedWrap(m.value[i], m.width))
1513 }
1514 line += m.LineInfo().RowOffset
1515 return line
1516}
1517
1518// mergeLineBelow merges the current line the cursor is on with the line below.
1519func (m *Model) mergeLineBelow(row int) {
1520 if row >= len(m.value)-1 {
1521 return
1522 }
1523
1524 // To perform a merge, we will need to combine the two lines and then
1525 m.value[row] = append(m.value[row], m.value[row+1]...)
1526
1527 // Shift all lines up by one
1528 for i := row + 1; i < len(m.value)-1; i++ {
1529 m.value[i] = m.value[i+1]
1530 }
1531
1532 // And, remove the last line
1533 if len(m.value) > 0 {
1534 m.value = m.value[:len(m.value)-1]
1535 }
1536}
1537
1538// mergeLineAbove merges the current line the cursor is on with the line above.
1539func (m *Model) mergeLineAbove(row int) {
1540 if row <= 0 {
1541 return
1542 }
1543
1544 m.col = len(m.value[row-1])
1545 m.row = m.row - 1
1546
1547 // To perform a merge, we will need to combine the two lines and then
1548 m.value[row-1] = append(m.value[row-1], m.value[row]...)
1549
1550 // Shift all lines up by one
1551 for i := row; i < len(m.value)-1; i++ {
1552 m.value[i] = m.value[i+1]
1553 }
1554
1555 // And, remove the last line
1556 if len(m.value) > 0 {
1557 m.value = m.value[:len(m.value)-1]
1558 }
1559}
1560
1561func (m *Model) splitLine(row, col int) {
1562 // To perform a split, take the current line and keep the content before
1563 // the cursor, take the content after the cursor and make it the content of
1564 // the line underneath, and shift the remaining lines down by one
1565 head, tailSrc := m.value[row][:col], m.value[row][col:]
1566 tail := make([]rune, len(tailSrc))
1567 copy(tail, tailSrc)
1568
1569 m.value = append(m.value[:row+1], m.value[row:]...)
1570
1571 m.value[row] = head
1572 m.value[row+1] = tail
1573
1574 m.col = 0
1575 m.row++
1576}
1577
1578// Paste is a command for pasting from the clipboard into the text input.
1579func Paste() tea.Msg {
1580 str, err := clipboard.ReadAll()
1581 if err != nil {
1582 return pasteErrMsg{err}
1583 }
1584 return pasteMsg(str)
1585}
1586
1587func wrap(runes []rune, width int) [][]rune {
1588 var (
1589 lines = [][]rune{{}}
1590 word = []rune{}
1591 row int
1592 spaces int
1593 )
1594
1595 // Word wrap the runes
1596 for _, r := range runes {
1597 if unicode.IsSpace(r) {
1598 spaces++
1599 } else {
1600 word = append(word, r)
1601 }
1602
1603 if spaces > 0 { //nolint:nestif
1604 if uniseg.StringWidth(string(lines[row]))+uniseg.StringWidth(string(word))+spaces > width {
1605 row++
1606 lines = append(lines, []rune{})
1607 lines[row] = append(lines[row], word...)
1608 lines[row] = append(lines[row], repeatSpaces(spaces)...)
1609 spaces = 0
1610 word = nil
1611 } else {
1612 lines[row] = append(lines[row], word...)
1613 lines[row] = append(lines[row], repeatSpaces(spaces)...)
1614 spaces = 0
1615 word = nil
1616 }
1617 } else {
1618 // If the last character is a double-width rune, then we may not be able to add it to this line
1619 // as it might cause us to go past the width.
1620 lastCharLen := rw.RuneWidth(word[len(word)-1])
1621 if uniseg.StringWidth(string(word))+lastCharLen > width {
1622 // If the current line has any content, let's move to the next
1623 // line because the current word fills up the entire line.
1624 if len(lines[row]) > 0 {
1625 row++
1626 lines = append(lines, []rune{})
1627 }
1628 lines[row] = append(lines[row], word...)
1629 word = nil
1630 }
1631 }
1632 }
1633
1634 if uniseg.StringWidth(string(lines[row]))+uniseg.StringWidth(string(word))+spaces >= width {
1635 lines = append(lines, []rune{})
1636 lines[row+1] = append(lines[row+1], word...)
1637 // We add an extra space at the end of the line to account for the
1638 // trailing space at the end of the previous soft-wrapped lines so that
1639 // behaviour when navigating is consistent and so that we don't need to
1640 // continually add edges to handle the last line of the wrapped input.
1641 spaces++
1642 lines[row+1] = append(lines[row+1], repeatSpaces(spaces)...)
1643 } else {
1644 lines[row] = append(lines[row], word...)
1645 spaces++
1646 lines[row] = append(lines[row], repeatSpaces(spaces)...)
1647 }
1648
1649 return lines
1650}
1651
1652func repeatSpaces(n int) []rune {
1653 return []rune(strings.Repeat(string(' '), n))
1654}
1655
1656// numDigits returns the number of digits in an integer.
1657func numDigits(n int) int {
1658 if n == 0 {
1659 return 1
1660 }
1661 count := 0
1662 num := abs(n)
1663 for num > 0 {
1664 count++
1665 num /= 10
1666 }
1667 return count
1668}
1669
1670func clamp(v, low, high int) int {
1671 if high < low {
1672 low, high = high, low
1673 }
1674 return min(high, max(low, v))
1675}
1676
1677func abs(n int) int {
1678 if n < 0 {
1679 return -n
1680 }
1681 return n
1682}