textarea.go 47 KB

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