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