textarea.go

   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}