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// san initializes or retrieves the rune sanitizer.
726func (m *Model) san() runeutil.Sanitizer {
727 if m.rsan == nil {
728 // Textinput has all its input on a single line so collapse
729 // newlines/tabs to single spaces.
730 m.rsan = runeutil.NewSanitizer()
731 }
732 return m.rsan
733}
734
735// deleteBeforeCursor deletes all text before the cursor. Returns whether or
736// not the cursor blink should be reset.
737func (m *Model) deleteBeforeCursor() {
738 m.value[m.row] = m.value[m.row][m.col:]
739 m.SetCursorColumn(0)
740}
741
742// deleteAfterCursor deletes all text after the cursor. Returns whether or not
743// the cursor blink should be reset. If input is masked delete everything after
744// the cursor so as not to reveal word breaks in the masked input.
745func (m *Model) deleteAfterCursor() {
746 m.value[m.row] = m.value[m.row][:m.col]
747 m.SetCursorColumn(len(m.value[m.row]))
748}
749
750// transposeLeft exchanges the runes at the cursor and immediately
751// before. No-op if the cursor is at the beginning of the line. If
752// the cursor is not at the end of the line yet, moves the cursor to
753// the right.
754func (m *Model) transposeLeft() {
755 if m.col == 0 || len(m.value[m.row]) < 2 {
756 return
757 }
758 if m.col >= len(m.value[m.row]) {
759 m.SetCursorColumn(m.col - 1)
760 }
761 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]
762 if m.col < len(m.value[m.row]) {
763 m.SetCursorColumn(m.col + 1)
764 }
765}
766
767// deleteWordLeft deletes the word left to the cursor. Returns whether or not
768// the cursor blink should be reset.
769func (m *Model) deleteWordLeft() {
770 if m.col == 0 || len(m.value[m.row]) == 0 {
771 return
772 }
773
774 // Linter note: it's critical that we acquire the initial cursor position
775 // here prior to altering it via SetCursor() below. As such, moving this
776 // call into the corresponding if clause does not apply here.
777 oldCol := m.col
778
779 m.SetCursorColumn(m.col - 1)
780 for unicode.IsSpace(m.value[m.row][m.col]) {
781 if m.col <= 0 {
782 break
783 }
784 // ignore series of whitespace before cursor
785 m.SetCursorColumn(m.col - 1)
786 }
787
788 for m.col > 0 {
789 if !unicode.IsSpace(m.value[m.row][m.col]) {
790 m.SetCursorColumn(m.col - 1)
791 } else {
792 if m.col > 0 {
793 // keep the previous space
794 m.SetCursorColumn(m.col + 1)
795 }
796 break
797 }
798 }
799
800 if oldCol > len(m.value[m.row]) {
801 m.value[m.row] = m.value[m.row][:m.col]
802 } else {
803 m.value[m.row] = append(m.value[m.row][:m.col], m.value[m.row][oldCol:]...)
804 }
805}
806
807// deleteWordRight deletes the word right to the cursor.
808func (m *Model) deleteWordRight() {
809 if m.col >= len(m.value[m.row]) || len(m.value[m.row]) == 0 {
810 return
811 }
812
813 oldCol := m.col
814
815 for m.col < len(m.value[m.row]) && unicode.IsSpace(m.value[m.row][m.col]) {
816 // ignore series of whitespace after cursor
817 m.SetCursorColumn(m.col + 1)
818 }
819
820 for m.col < len(m.value[m.row]) {
821 if !unicode.IsSpace(m.value[m.row][m.col]) {
822 m.SetCursorColumn(m.col + 1)
823 } else {
824 break
825 }
826 }
827
828 if m.col > len(m.value[m.row]) {
829 m.value[m.row] = m.value[m.row][:oldCol]
830 } else {
831 m.value[m.row] = append(m.value[m.row][:oldCol], m.value[m.row][m.col:]...)
832 }
833
834 m.SetCursorColumn(oldCol)
835}
836
837// characterRight moves the cursor one character to the right.
838func (m *Model) characterRight() {
839 if m.col < len(m.value[m.row]) {
840 m.SetCursorColumn(m.col + 1)
841 } else {
842 if m.row < len(m.value)-1 {
843 m.row++
844 m.CursorStart()
845 }
846 }
847}
848
849// characterLeft moves the cursor one character to the left.
850// If insideLine is set, the cursor is moved to the last
851// character in the previous line, instead of one past that.
852func (m *Model) characterLeft(insideLine bool) {
853 if m.col == 0 && m.row != 0 {
854 m.row--
855 m.CursorEnd()
856 if !insideLine {
857 return
858 }
859 }
860 if m.col > 0 {
861 m.SetCursorColumn(m.col - 1)
862 }
863}
864
865// wordLeft moves the cursor one word to the left. Returns whether or not the
866// cursor blink should be reset. If input is masked, move input to the start
867// so as not to reveal word breaks in the masked input.
868func (m *Model) wordLeft() {
869 for {
870 m.characterLeft(true /* insideLine */)
871 if m.col < len(m.value[m.row]) && !unicode.IsSpace(m.value[m.row][m.col]) {
872 break
873 }
874 }
875
876 for m.col > 0 {
877 if unicode.IsSpace(m.value[m.row][m.col-1]) {
878 break
879 }
880 m.SetCursorColumn(m.col - 1)
881 }
882}
883
884// wordRight moves the cursor one word to the right. Returns whether or not the
885// cursor blink should be reset. If the input is masked, move input to the end
886// so as not to reveal word breaks in the masked input.
887func (m *Model) wordRight() {
888 m.doWordRight(func(int, int) { /* nothing */ })
889}
890
891func (m *Model) doWordRight(fn func(charIdx int, pos int)) {
892 // Skip spaces forward.
893 for m.col >= len(m.value[m.row]) || unicode.IsSpace(m.value[m.row][m.col]) {
894 if m.row == len(m.value)-1 && m.col == len(m.value[m.row]) {
895 // End of text.
896 break
897 }
898 m.characterRight()
899 }
900
901 charIdx := 0
902 for m.col < len(m.value[m.row]) {
903 if unicode.IsSpace(m.value[m.row][m.col]) {
904 break
905 }
906 fn(charIdx, m.col)
907 m.SetCursorColumn(m.col + 1)
908 charIdx++
909 }
910}
911
912// uppercaseRight changes the word to the right to uppercase.
913func (m *Model) uppercaseRight() {
914 m.doWordRight(func(_ int, i int) {
915 m.value[m.row][i] = unicode.ToUpper(m.value[m.row][i])
916 })
917}
918
919// lowercaseRight changes the word to the right to lowercase.
920func (m *Model) lowercaseRight() {
921 m.doWordRight(func(_ int, i int) {
922 m.value[m.row][i] = unicode.ToLower(m.value[m.row][i])
923 })
924}
925
926// capitalizeRight changes the word to the right to title case.
927func (m *Model) capitalizeRight() {
928 m.doWordRight(func(charIdx int, i int) {
929 if charIdx == 0 {
930 m.value[m.row][i] = unicode.ToTitle(m.value[m.row][i])
931 }
932 })
933}
934
935// LineInfo returns the number of characters from the start of the
936// (soft-wrapped) line and the (soft-wrapped) line width.
937func (m Model) LineInfo() LineInfo {
938 grid := m.memoizedWrap(m.value[m.row], m.width)
939
940 // Find out which line we are currently on. This can be determined by the
941 // m.col and counting the number of runes that we need to skip.
942 var counter int
943 for i, line := range grid {
944 // We've found the line that we are on
945 if counter+len(line) == m.col && i+1 < len(grid) {
946 // We wrap around to the next line if we are at the end of the
947 // previous line so that we can be at the very beginning of the row
948 return LineInfo{
949 CharOffset: 0,
950 ColumnOffset: 0,
951 Height: len(grid),
952 RowOffset: i + 1,
953 StartColumn: m.col,
954 Width: len(grid[i+1]),
955 CharWidth: uniseg.StringWidth(string(line)),
956 }
957 }
958
959 if counter+len(line) >= m.col {
960 return LineInfo{
961 CharOffset: uniseg.StringWidth(string(line[:max(0, m.col-counter)])),
962 ColumnOffset: m.col - counter,
963 Height: len(grid),
964 RowOffset: i,
965 StartColumn: counter,
966 Width: len(line),
967 CharWidth: uniseg.StringWidth(string(line)),
968 }
969 }
970
971 counter += len(line)
972 }
973 return LineInfo{}
974}
975
976// repositionView repositions the view of the viewport based on the defined
977// scrolling behavior.
978func (m *Model) repositionView() {
979 minimum := m.viewport.YOffset()
980 maximum := minimum + m.viewport.Height() - 1
981 if row := m.cursorLineNumber(); row < minimum {
982 m.viewport.ScrollUp(minimum - row)
983 } else if row > maximum {
984 m.viewport.ScrollDown(row - maximum)
985 }
986}
987
988// Width returns the width of the textarea.
989func (m Model) Width() int {
990 return m.width
991}
992
993// MoveToBegin moves the cursor to the beginning of the input.
994func (m *Model) MoveToBegin() {
995 m.row = 0
996 m.SetCursorColumn(0)
997}
998
999// MoveToEnd moves the cursor to the end of the input.
1000func (m *Model) MoveToEnd() {
1001 m.row = len(m.value) - 1
1002 m.SetCursorColumn(len(m.value[m.row]))
1003}
1004
1005// SetWidth sets the width of the textarea to fit exactly within the given width.
1006// This means that the textarea will account for the width of the prompt and
1007// whether or not line numbers are being shown.
1008//
1009// Ensure that SetWidth is called after setting the Prompt and ShowLineNumbers,
1010// It is important that the width of the textarea be exactly the given width
1011// and no more.
1012func (m *Model) SetWidth(w int) {
1013 // Update prompt width only if there is no prompt function as
1014 // [SetPromptFunc] updates the prompt width when it is called.
1015 if m.promptFunc == nil {
1016 // XXX: Do we even need this or can we calculate the prompt width
1017 // at render time?
1018 m.promptWidth = uniseg.StringWidth(m.Prompt)
1019 }
1020
1021 // Add base style borders and padding to reserved outer width.
1022 reservedOuter := m.activeStyle().Base.GetHorizontalFrameSize()
1023
1024 // Add prompt width to reserved inner width.
1025 reservedInner := m.promptWidth
1026
1027 // Add line number width to reserved inner width.
1028 if m.ShowLineNumbers {
1029 // XXX: this was originally documented as needing "1 cell" but was,
1030 // in practice, effectively hardcoded to 2 cells. We can, and should,
1031 // reduce this to one gap and update the tests accordingly.
1032 const gap = 2
1033
1034 // Number of digits plus 1 cell for the margin.
1035 reservedInner += numDigits(m.MaxHeight) + gap
1036 }
1037
1038 // Input width must be at least one more than the reserved inner and outer
1039 // width. This gives us a minimum input width of 1.
1040 minWidth := reservedInner + reservedOuter + 1
1041 inputWidth := max(w, minWidth)
1042
1043 // Input width must be no more than maximum width.
1044 if m.MaxWidth > 0 {
1045 inputWidth = min(inputWidth, m.MaxWidth)
1046 }
1047
1048 // Since the width of the viewport and input area is dependent on the width of
1049 // borders, prompt and line numbers, we need to calculate it by subtracting
1050 // the reserved width from them.
1051
1052 m.viewport.SetWidth(inputWidth - reservedOuter)
1053 m.width = inputWidth - reservedOuter - reservedInner
1054}
1055
1056// SetPromptFunc supersedes the Prompt field and sets a dynamic prompt instead.
1057//
1058// If the function returns a prompt that is shorter than the specified
1059// promptWidth, it will be padded to the left. If it returns a prompt that is
1060// longer, display artifacts may occur; the caller is responsible for computing
1061// an adequate promptWidth.
1062func (m *Model) SetPromptFunc(promptWidth int, fn func(PromptInfo) string) {
1063 m.promptFunc = fn
1064 m.promptWidth = promptWidth
1065}
1066
1067// Height returns the current height of the textarea.
1068func (m Model) Height() int {
1069 return m.height
1070}
1071
1072// SetHeight sets the height of the textarea.
1073func (m *Model) SetHeight(h int) {
1074 if m.MaxHeight > 0 {
1075 m.height = clamp(h, minHeight, m.MaxHeight)
1076 m.viewport.SetHeight(clamp(h, minHeight, m.MaxHeight))
1077 } else {
1078 m.height = max(h, minHeight)
1079 m.viewport.SetHeight(max(h, minHeight))
1080 }
1081}
1082
1083// Update is the Bubble Tea update loop.
1084func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
1085 if !m.focus {
1086 m.virtualCursor.Blur()
1087 return m, nil
1088 }
1089
1090 // Used to determine if the cursor should blink.
1091 oldRow, oldCol := m.cursorLineNumber(), m.col
1092
1093 var cmds []tea.Cmd
1094
1095 if m.value[m.row] == nil {
1096 m.value[m.row] = make([]rune, 0)
1097 }
1098
1099 if m.MaxHeight > 0 && m.MaxHeight != m.cache.Capacity() {
1100 m.cache = memoization.NewMemoCache[line, [][]rune](m.MaxHeight)
1101 }
1102
1103 switch msg := msg.(type) {
1104 case tea.PasteMsg:
1105 m.insertRunesFromUserInput([]rune(msg))
1106 case tea.KeyPressMsg:
1107 switch {
1108 case key.Matches(msg, m.KeyMap.DeleteAfterCursor):
1109 m.col = clamp(m.col, 0, len(m.value[m.row]))
1110 if m.col >= len(m.value[m.row]) {
1111 m.mergeLineBelow(m.row)
1112 break
1113 }
1114 m.deleteAfterCursor()
1115 case key.Matches(msg, m.KeyMap.DeleteBeforeCursor):
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 m.deleteBeforeCursor()
1122 case key.Matches(msg, m.KeyMap.DeleteCharacterBackward):
1123 m.col = clamp(m.col, 0, len(m.value[m.row]))
1124 if m.col <= 0 {
1125 m.mergeLineAbove(m.row)
1126 break
1127 }
1128 if len(m.value[m.row]) > 0 {
1129 m.value[m.row] = append(m.value[m.row][:max(0, m.col-1)], m.value[m.row][m.col:]...)
1130 if m.col > 0 {
1131 m.SetCursorColumn(m.col - 1)
1132 }
1133 }
1134 case key.Matches(msg, m.KeyMap.DeleteCharacterForward):
1135 if len(m.value[m.row]) > 0 && m.col < len(m.value[m.row]) {
1136 m.value[m.row] = slices.Delete(m.value[m.row], m.col, m.col+1)
1137 }
1138 if m.col >= len(m.value[m.row]) {
1139 m.mergeLineBelow(m.row)
1140 break
1141 }
1142 case key.Matches(msg, m.KeyMap.DeleteWordBackward):
1143 if m.col <= 0 {
1144 m.mergeLineAbove(m.row)
1145 break
1146 }
1147 m.deleteWordLeft()
1148 case key.Matches(msg, m.KeyMap.DeleteWordForward):
1149 m.col = clamp(m.col, 0, len(m.value[m.row]))
1150 if m.col >= len(m.value[m.row]) {
1151 m.mergeLineBelow(m.row)
1152 break
1153 }
1154 m.deleteWordRight()
1155 case key.Matches(msg, m.KeyMap.InsertNewline):
1156 if m.MaxHeight > 0 && len(m.value) >= m.MaxHeight {
1157 return m, nil
1158 }
1159 m.col = clamp(m.col, 0, len(m.value[m.row]))
1160 m.splitLine(m.row, m.col)
1161 case key.Matches(msg, m.KeyMap.LineEnd):
1162 m.CursorEnd()
1163 case key.Matches(msg, m.KeyMap.LineStart):
1164 m.CursorStart()
1165 case key.Matches(msg, m.KeyMap.CharacterForward):
1166 m.characterRight()
1167 case key.Matches(msg, m.KeyMap.LineNext):
1168 m.CursorDown()
1169 case key.Matches(msg, m.KeyMap.WordForward):
1170 m.wordRight()
1171 case key.Matches(msg, m.KeyMap.Paste):
1172 return m, Paste
1173 case key.Matches(msg, m.KeyMap.CharacterBackward):
1174 m.characterLeft(false /* insideLine */)
1175 case key.Matches(msg, m.KeyMap.LinePrevious):
1176 m.CursorUp()
1177 case key.Matches(msg, m.KeyMap.WordBackward):
1178 m.wordLeft()
1179 case key.Matches(msg, m.KeyMap.InputBegin):
1180 m.MoveToBegin()
1181 case key.Matches(msg, m.KeyMap.InputEnd):
1182 m.MoveToEnd()
1183 case key.Matches(msg, m.KeyMap.LowercaseWordForward):
1184 m.lowercaseRight()
1185 case key.Matches(msg, m.KeyMap.UppercaseWordForward):
1186 m.uppercaseRight()
1187 case key.Matches(msg, m.KeyMap.CapitalizeWordForward):
1188 m.capitalizeRight()
1189 case key.Matches(msg, m.KeyMap.TransposeCharacterBackward):
1190 m.transposeLeft()
1191
1192 default:
1193 m.insertRunesFromUserInput([]rune(msg.Text))
1194 }
1195
1196 case pasteMsg:
1197 m.insertRunesFromUserInput([]rune(msg))
1198
1199 case pasteErrMsg:
1200 m.Err = msg
1201 }
1202
1203 vp, cmd := m.viewport.Update(msg)
1204 m.viewport = &vp
1205 cmds = append(cmds, cmd)
1206
1207 if m.useVirtualCursor {
1208 m.virtualCursor, cmd = m.virtualCursor.Update(msg)
1209
1210 // If the cursor has moved, reset the blink state. This is a small UX
1211 // nuance that makes cursor movement obvious and feel snappy.
1212 newRow, newCol := m.cursorLineNumber(), m.col
1213 if (newRow != oldRow || newCol != oldCol) && m.virtualCursor.Mode() == cursor.CursorBlink {
1214 m.virtualCursor.IsBlinked = false
1215 cmd = m.virtualCursor.Blink()
1216 }
1217 cmds = append(cmds, cmd)
1218 }
1219
1220 m.repositionView()
1221
1222 return m, tea.Batch(cmds...)
1223}
1224
1225// View renders the text area in its current state.
1226func (m Model) View() string {
1227 if m.Value() == "" && m.row == 0 && m.col == 0 && m.Placeholder != "" {
1228 return m.placeholderView()
1229 }
1230 m.virtualCursor.TextStyle = m.activeStyle().computedCursorLine()
1231
1232 var (
1233 s strings.Builder
1234 style lipgloss.Style
1235 newLines int
1236 widestLineNumber int
1237 lineInfo = m.LineInfo()
1238 styles = m.activeStyle()
1239 )
1240
1241 displayLine := 0
1242 for l, line := range m.value {
1243 wrappedLines := m.memoizedWrap(line, m.width)
1244
1245 if m.row == l {
1246 style = styles.computedCursorLine()
1247 } else {
1248 style = styles.computedText()
1249 }
1250
1251 for wl, wrappedLine := range wrappedLines {
1252 prompt := m.promptView(displayLine)
1253 prompt = styles.computedPrompt().Render(prompt)
1254 s.WriteString(style.Render(prompt))
1255 displayLine++
1256
1257 var ln string
1258 if m.ShowLineNumbers {
1259 if wl == 0 { // normal line
1260 isCursorLine := m.row == l
1261 s.WriteString(m.lineNumberView(l+1, isCursorLine))
1262 } else { // soft wrapped line
1263 isCursorLine := m.row == l
1264 s.WriteString(m.lineNumberView(-1, isCursorLine))
1265 }
1266 }
1267
1268 // Note the widest line number for padding purposes later.
1269 lnw := uniseg.StringWidth(ln)
1270 if lnw > widestLineNumber {
1271 widestLineNumber = lnw
1272 }
1273
1274 strwidth := uniseg.StringWidth(string(wrappedLine))
1275 padding := m.width - strwidth
1276 // If the trailing space causes the line to be wider than the
1277 // width, we should not draw it to the screen since it will result
1278 // in an extra space at the end of the line which can look off when
1279 // the cursor line is showing.
1280 if strwidth > m.width {
1281 // The character causing the line to be wider than the width is
1282 // guaranteed to be a space since any other character would
1283 // have been wrapped.
1284 wrappedLine = []rune(strings.TrimSuffix(string(wrappedLine), " "))
1285 padding -= m.width - strwidth
1286 }
1287 if m.row == l && lineInfo.RowOffset == wl {
1288 s.WriteString(style.Render(string(wrappedLine[:lineInfo.ColumnOffset])))
1289 if m.col >= len(line) && lineInfo.CharOffset >= m.width {
1290 m.virtualCursor.SetChar(" ")
1291 s.WriteString(m.virtualCursor.View())
1292 } else {
1293 m.virtualCursor.SetChar(string(wrappedLine[lineInfo.ColumnOffset]))
1294 s.WriteString(style.Render(m.virtualCursor.View()))
1295 s.WriteString(style.Render(string(wrappedLine[lineInfo.ColumnOffset+1:])))
1296 }
1297 } else {
1298 s.WriteString(style.Render(string(wrappedLine)))
1299 }
1300 s.WriteString(style.Render(strings.Repeat(" ", max(0, padding))))
1301 s.WriteRune('\n')
1302 newLines++
1303 }
1304 }
1305
1306 // Always show at least `m.Height` lines at all times.
1307 // To do this we can simply pad out a few extra new lines in the view.
1308 for range m.height {
1309 s.WriteString(m.promptView(displayLine))
1310 displayLine++
1311
1312 // Write end of buffer content
1313 leftGutter := string(m.EndOfBufferCharacter)
1314 rightGapWidth := m.Width() - uniseg.StringWidth(leftGutter) + widestLineNumber
1315 rightGap := strings.Repeat(" ", max(0, rightGapWidth))
1316 s.WriteString(styles.computedEndOfBuffer().Render(leftGutter + rightGap))
1317 s.WriteRune('\n')
1318 }
1319
1320 m.viewport.SetContent(s.String())
1321 return styles.Base.Render(m.viewport.View())
1322}
1323
1324// promptView renders a single line of the prompt.
1325func (m Model) promptView(displayLine int) (prompt string) {
1326 prompt = m.Prompt
1327 if m.promptFunc == nil {
1328 return prompt
1329 }
1330 prompt = m.promptFunc(PromptInfo{
1331 LineNumber: displayLine,
1332 Focused: m.focus,
1333 })
1334 width := lipgloss.Width(prompt)
1335 if width < m.promptWidth {
1336 prompt = fmt.Sprintf("%*s%s", m.promptWidth-width, "", prompt)
1337 }
1338
1339 return m.activeStyle().computedPrompt().Render(prompt)
1340}
1341
1342// lineNumberView renders the line number.
1343//
1344// If the argument is less than 0, a space styled as a line number is returned
1345// instead. Such cases are used for soft-wrapped lines.
1346//
1347// The second argument indicates whether this line number is for a 'cursorline'
1348// line number.
1349func (m Model) lineNumberView(n int, isCursorLine bool) (str string) {
1350 if !m.ShowLineNumbers {
1351 return ""
1352 }
1353
1354 if n <= 0 {
1355 str = " "
1356 } else {
1357 str = strconv.Itoa(n)
1358 }
1359
1360 // XXX: is textStyle really necessary here?
1361 textStyle := m.activeStyle().computedText()
1362 lineNumberStyle := m.activeStyle().computedLineNumber()
1363 if isCursorLine {
1364 textStyle = m.activeStyle().computedCursorLine()
1365 lineNumberStyle = m.activeStyle().computedCursorLineNumber()
1366 }
1367
1368 // Format line number dynamically based on the maximum number of lines.
1369 digits := len(strconv.Itoa(m.MaxHeight))
1370 str = fmt.Sprintf(" %*v ", digits, str)
1371
1372 return textStyle.Render(lineNumberStyle.Render(str))
1373}
1374
1375// placeholderView returns the prompt and placeholder, if any.
1376func (m Model) placeholderView() string {
1377 var (
1378 s strings.Builder
1379 p = m.Placeholder
1380 styles = m.activeStyle()
1381 )
1382 // word wrap lines
1383 pwordwrap := ansi.Wordwrap(p, m.width, "")
1384 // hard wrap lines (handles lines that could not be word wrapped)
1385 pwrap := ansi.Hardwrap(pwordwrap, m.width, true)
1386 // split string by new lines
1387 plines := strings.Split(strings.TrimSpace(pwrap), "\n")
1388
1389 for i := range m.height {
1390 isLineNumber := len(plines) > i
1391
1392 lineStyle := styles.computedPlaceholder()
1393 if len(plines) > i {
1394 lineStyle = styles.computedCursorLine()
1395 }
1396
1397 // render prompt
1398 prompt := m.promptView(i)
1399 prompt = styles.computedPrompt().Render(prompt)
1400 s.WriteString(lineStyle.Render(prompt))
1401
1402 // when show line numbers enabled:
1403 // - render line number for only the cursor line
1404 // - indent other placeholder lines
1405 // this is consistent with vim with line numbers enabled
1406 if m.ShowLineNumbers {
1407 var ln int
1408
1409 switch {
1410 case i == 0:
1411 ln = i + 1
1412 fallthrough
1413 case len(plines) > i:
1414 s.WriteString(m.lineNumberView(ln, isLineNumber))
1415 default:
1416 }
1417 }
1418
1419 switch {
1420 // first line
1421 case i == 0:
1422 // first character of first line as cursor with character
1423 m.virtualCursor.TextStyle = styles.computedPlaceholder()
1424
1425 ch, rest, _, _ := uniseg.FirstGraphemeClusterInString(plines[0], 0)
1426 m.virtualCursor.SetChar(ch)
1427 s.WriteString(lineStyle.Render(m.virtualCursor.View()))
1428
1429 // the rest of the first line
1430 s.WriteString(lineStyle.Render(styles.computedPlaceholder().Render(rest)))
1431
1432 // extend the first line with spaces to fill the width, so that
1433 // the entire line is filled when cursorline is enabled.
1434 gap := strings.Repeat(" ", max(0, m.width-lipgloss.Width(plines[0])))
1435 s.WriteString(lineStyle.Render(gap))
1436 // remaining lines
1437 case len(plines) > i:
1438 // current line placeholder text
1439 if len(plines) > i {
1440 placeholderLine := plines[i]
1441 gap := strings.Repeat(" ", max(0, m.width-uniseg.StringWidth(plines[i])))
1442 s.WriteString(lineStyle.Render(placeholderLine + gap))
1443 }
1444 default:
1445 // end of line buffer character
1446 eob := styles.computedEndOfBuffer().Render(string(m.EndOfBufferCharacter))
1447 s.WriteString(eob)
1448 }
1449
1450 // terminate with new line
1451 s.WriteRune('\n')
1452 }
1453
1454 m.viewport.SetContent(s.String())
1455 return styles.Base.Render(m.viewport.View())
1456}
1457
1458// Blink returns the blink command for the virtual cursor.
1459func Blink() tea.Msg {
1460 return cursor.Blink()
1461}
1462
1463// Cursor returns a [tea.Cursor] for rendering a real cursor in a Bubble Tea
1464// program. This requires that [Model.VirtualCursor] is set to false.
1465//
1466// Note that you will almost certainly also need to adjust the offset cursor
1467// position per the textarea's per the textarea's position in the terminal.
1468//
1469// Example:
1470//
1471// // In your top-level View function:
1472// f := tea.NewFrame(m.textarea.View())
1473// f.Cursor = m.textarea.Cursor()
1474// f.Cursor.Position.X += offsetX
1475// f.Cursor.Position.Y += offsetY
1476func (m Model) Cursor() *tea.Cursor {
1477 if m.useVirtualCursor || !m.Focused() {
1478 return nil
1479 }
1480
1481 lineInfo := m.LineInfo()
1482 w := lipgloss.Width
1483 baseStyle := m.activeStyle().Base
1484
1485 xOffset := lineInfo.CharOffset +
1486 w(m.promptView(0)) +
1487 w(m.lineNumberView(0, false)) +
1488 baseStyle.GetMarginLeft() +
1489 baseStyle.GetPaddingLeft() +
1490 baseStyle.GetBorderLeftSize()
1491
1492 yOffset := m.cursorLineNumber() -
1493 m.viewport.YOffset() +
1494 baseStyle.GetMarginTop() +
1495 baseStyle.GetPaddingTop() +
1496 baseStyle.GetBorderTopSize()
1497
1498 c := tea.NewCursor(xOffset, yOffset)
1499 c.Blink = m.styles.Cursor.Blink
1500 c.Color = m.styles.Cursor.Color
1501 c.Shape = m.styles.Cursor.Shape
1502 return c
1503}
1504
1505func (m Model) memoizedWrap(runes []rune, width int) [][]rune {
1506 input := line{runes: runes, width: width}
1507 if v, ok := m.cache.Get(input); ok {
1508 return v
1509 }
1510 v := wrap(runes, width)
1511 m.cache.Set(input, v)
1512 return v
1513}
1514
1515// cursorLineNumber returns the line number that the cursor is on.
1516// This accounts for soft wrapped lines.
1517func (m Model) cursorLineNumber() int {
1518 line := 0
1519 for i := range m.row {
1520 // Calculate the number of lines that the current line will be split
1521 // into.
1522 line += len(m.memoizedWrap(m.value[i], m.width))
1523 }
1524 line += m.LineInfo().RowOffset
1525 return line
1526}
1527
1528// mergeLineBelow merges the current line the cursor is on with the line below.
1529func (m *Model) mergeLineBelow(row int) {
1530 if row >= len(m.value)-1 {
1531 return
1532 }
1533
1534 // To perform a merge, we will need to combine the two lines and then
1535 m.value[row] = append(m.value[row], m.value[row+1]...)
1536
1537 // Shift all lines up by one
1538 for i := row + 1; i < len(m.value)-1; i++ {
1539 m.value[i] = m.value[i+1]
1540 }
1541
1542 // And, remove the last line
1543 if len(m.value) > 0 {
1544 m.value = m.value[:len(m.value)-1]
1545 }
1546}
1547
1548// mergeLineAbove merges the current line the cursor is on with the line above.
1549func (m *Model) mergeLineAbove(row int) {
1550 if row <= 0 {
1551 return
1552 }
1553
1554 m.col = len(m.value[row-1])
1555 m.row = m.row - 1
1556
1557 // To perform a merge, we will need to combine the two lines and then
1558 m.value[row-1] = append(m.value[row-1], m.value[row]...)
1559
1560 // Shift all lines up by one
1561 for i := row; i < len(m.value)-1; i++ {
1562 m.value[i] = m.value[i+1]
1563 }
1564
1565 // And, remove the last line
1566 if len(m.value) > 0 {
1567 m.value = m.value[:len(m.value)-1]
1568 }
1569}
1570
1571func (m *Model) splitLine(row, col int) {
1572 // To perform a split, take the current line and keep the content before
1573 // the cursor, take the content after the cursor and make it the content of
1574 // the line underneath, and shift the remaining lines down by one
1575 head, tailSrc := m.value[row][:col], m.value[row][col:]
1576 tail := make([]rune, len(tailSrc))
1577 copy(tail, tailSrc)
1578
1579 m.value = append(m.value[:row+1], m.value[row:]...)
1580
1581 m.value[row] = head
1582 m.value[row+1] = tail
1583
1584 m.col = 0
1585 m.row++
1586}
1587
1588// Paste is a command for pasting from the clipboard into the text input.
1589func Paste() tea.Msg {
1590 str, err := clipboard.ReadAll()
1591 if err != nil {
1592 return pasteErrMsg{err}
1593 }
1594 return pasteMsg(str)
1595}
1596
1597func wrap(runes []rune, width int) [][]rune {
1598 var (
1599 lines = [][]rune{{}}
1600 word = []rune{}
1601 row int
1602 spaces int
1603 )
1604
1605 // Word wrap the runes
1606 for _, r := range runes {
1607 if unicode.IsSpace(r) {
1608 spaces++
1609 } else {
1610 word = append(word, r)
1611 }
1612
1613 if spaces > 0 { //nolint:nestif
1614 if uniseg.StringWidth(string(lines[row]))+uniseg.StringWidth(string(word))+spaces > width {
1615 row++
1616 lines = append(lines, []rune{})
1617 lines[row] = append(lines[row], word...)
1618 lines[row] = append(lines[row], repeatSpaces(spaces)...)
1619 spaces = 0
1620 word = nil
1621 } else {
1622 lines[row] = append(lines[row], word...)
1623 lines[row] = append(lines[row], repeatSpaces(spaces)...)
1624 spaces = 0
1625 word = nil
1626 }
1627 } else {
1628 // If the last character is a double-width rune, then we may not be able to add it to this line
1629 // as it might cause us to go past the width.
1630 lastCharLen := rw.RuneWidth(word[len(word)-1])
1631 if uniseg.StringWidth(string(word))+lastCharLen > width {
1632 // If the current line has any content, let's move to the next
1633 // line because the current word fills up the entire line.
1634 if len(lines[row]) > 0 {
1635 row++
1636 lines = append(lines, []rune{})
1637 }
1638 lines[row] = append(lines[row], word...)
1639 word = nil
1640 }
1641 }
1642 }
1643
1644 if uniseg.StringWidth(string(lines[row]))+uniseg.StringWidth(string(word))+spaces >= width {
1645 lines = append(lines, []rune{})
1646 lines[row+1] = append(lines[row+1], word...)
1647 // We add an extra space at the end of the line to account for the
1648 // trailing space at the end of the previous soft-wrapped lines so that
1649 // behaviour when navigating is consistent and so that we don't need to
1650 // continually add edges to handle the last line of the wrapped input.
1651 spaces++
1652 lines[row+1] = append(lines[row+1], repeatSpaces(spaces)...)
1653 } else {
1654 lines[row] = append(lines[row], word...)
1655 spaces++
1656 lines[row] = append(lines[row], repeatSpaces(spaces)...)
1657 }
1658
1659 return lines
1660}
1661
1662func repeatSpaces(n int) []rune {
1663 return []rune(strings.Repeat(string(' '), n))
1664}
1665
1666// numDigits returns the number of digits in an integer.
1667func numDigits(n int) int {
1668 if n == 0 {
1669 return 1
1670 }
1671 count := 0
1672 num := abs(n)
1673 for num > 0 {
1674 count++
1675 num /= 10
1676 }
1677 return count
1678}
1679
1680func clamp(v, low, high int) int {
1681 if high < low {
1682 low, high = high, low
1683 }
1684 return min(high, max(low, v))
1685}
1686
1687func abs(n int) int {
1688 if n < 0 {
1689 return -n
1690 }
1691 return n
1692}