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