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