Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions .github/CODEOWNERS

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'}}
steps:
- id: metadata
uses: dependabot/fetch-metadata@v2
uses: dependabot/fetch-metadata@v3
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- run: |
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Bubbles

<img src="https://github.com/user-attachments/assets/09d46497-9b4e-4de9-bb8e-685ef5c80c96" width="350" />
<img src="https://github.com/user-attachments/assets/b89fa46e-d451-4b33-a009-c68d4765520f" width="350" />

[![Latest Release](https://img.shields.io/github/release/charmbracelet/bubbles.svg)](https://github.com/charmbracelet/bubbles/releases)
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/charmbracelet/bubbles)
[![Build Status](https://github.com/charmbracelet/bubbles/workflows/build/badge.svg)](https://github.com/charmbracelet/bubbles/actions)
[![Go ReportCard](https://goreportcard.com/badge/charmbracelet/bubbles)](https://goreportcard.com/report/charmbracelet/bubbles)

Some components for [Bubble Tea](https://github.com/charmbracelet/bubbletea)
Primitives for [Bubble Tea](https://github.com/charmbracelet/bubbletea)
applications. These components are used in production in [Crush][crush], and [many other applications][otherstuff].

> [!TIP]
Expand Down Expand Up @@ -194,7 +194,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
To check out community-maintained Bubbles see [Charm & Friends][charmandfriends].
Made a cool Bubble that you want to share? [PRs][prs] are welcome!

[charmandfriends]:
[charmandfriends]: https://github.com/charm-and-friends/additional-bubbles
[prs]: https://github.com/charm-and-friends/additional-bubbles?tab=readme-ov-file#what-is-a-complete-project

## Contributing
Expand Down
8 changes: 4 additions & 4 deletions cursor/cursor.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ type BlinkMsg struct {
tag int
}

// blinkCanceled is sent when a blink operation is canceled.
type blinkCanceled struct{}
// BlinkCanceled is sent when a blink operation is canceled.
type BlinkCanceled struct{}

// blinkCtx manages cursor blinking.
type blinkCtx struct {
Expand Down Expand Up @@ -151,7 +151,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
}
return m, cmd

case blinkCanceled: // no-op
case BlinkCanceled: // no-op
return m, nil
}
return m, nil
Expand Down Expand Up @@ -201,7 +201,7 @@ func (m *Model) Blink() tea.Cmd {
if ctx.Err() == context.DeadlineExceeded {
return blinkMsg
}
return blinkCanceled{}
return BlinkCanceled{}
}
}

Expand Down
2 changes: 1 addition & 1 deletion cursor/cursor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
// if ctx.Err() == context.DeadlineExceeded {
// return BlinkMsg{id: m.id, tag: m.blinkTag}
// }
// return blinkCanceled{}
// return BlinkCanceled{}
// }
//
// A race on “m.blinkTag” will occur if:
Expand Down
27 changes: 13 additions & 14 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,35 +1,34 @@
module charm.land/bubbles/v2

go 1.24.2
go 1.25.0

require (
charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251106192006-06c0cda318b3
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7
charm.land/bubbletea/v2 v2.0.6
charm.land/lipgloss/v2 v2.0.3
github.com/MakeNowJust/heredoc v1.0.0
github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/harmonica v0.2.0
github.com/charmbracelet/x/ansi v0.11.6
github.com/charmbracelet/x/ansi v0.11.7
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f
github.com/dustin/go-humanize v1.0.1
github.com/mattn/go-runewidth v0.0.20
github.com/mattn/go-runewidth v0.0.23
github.com/rivo/uniseg v0.4.7
github.com/sahilm/fuzzy v0.1.1
)

require (
github.com/aymanbagabas/go-udiff v0.3.1 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 // indirect
github.com/aymanbagabas/go-udiff v0.4.1 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
)
50 changes: 24 additions & 26 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251106192006-06c0cda318b3 h1:kHeOXDvccLh2f0gH3qxyMhN07VEnmc/3gPAZ50wn8ko=
charm.land/bubbletea/v2 v2.0.0-rc.1.0.20251106192006-06c0cda318b3/go.mod h1:e3yWIY4Tl/LJnOMOv9H4YgvDwrknVDm5az5ep5QRLfk=
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7 h1:059k1h5vvZ4ASinki9nmBguxu9Rq0UDDSa6q8LOUphk=
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
charm.land/bubbletea/v2 v2.0.6 h1:UHN/91OyuhaOFGSrBXQ/hMZD8IO1Uc4BvHlgHXL2WJo=
charm.land/bubbletea/v2 v2.0.6/go.mod h1:MH/D8ZLlN3op37vQvijKuU29g3rqTp+aQapURFonF9g=
charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU=
charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 h1:r/3jQZ1LjWW6ybp8HHfhrKrwHIWiJhUuY7wwYIWZulQ=
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692/go.mod h1:Y8B4DzWeTb0ama8l3+KyopZtkE8fZjwRQ3aEAPEXHE0=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468 h1:Q9fO0y1Zo5KB/5Vu8JZoLGm1N3RzF9bNj3Ao3xoR+Ac=
github.com/charmbracelet/ultraviolet v0.0.0-20260416155717-489999b90468/go.mod h1:bAAz7dh/FTYfC+oiHavL4mX1tOIBZ0ZwYjSi3qE6ivM=
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
Expand All @@ -24,20 +24,18 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
Expand All @@ -48,7 +46,7 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
108 changes: 107 additions & 1 deletion textarea/textarea.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,23 @@ type Model struct {
// there's no limit.
MaxWidth int

// DynamicHeight, when true, causes the textarea to automatically grow
// and shrink its height to fit the content. The height is clamped between
// MinHeight and MaxHeight.
DynamicHeight bool

// MinHeight is the minimum height of the text area in rows when
// DynamicHeight is enabled. If 0 or less, defaults to 1.
MinHeight int

// MaxContentHeight is the maximum content height in visual rows
// (accounting for soft wraps). When set (> 0), input is blocked once
// the total visual lines reach this limit, while MaxHeight controls
// only the visible viewport height. When 0, the content guard falls
// back to the legacy MaxHeight behavior (blocking at MaxHeight
// logical lines) for backward compatibility.
MaxContentHeight int

// Styling. Styles are defined in [Styles]. Use [SetStyles] and [GetStyles]
// to work with this value publicly.
styles Styles
Expand Down Expand Up @@ -464,16 +481,19 @@ func (m *Model) updateVirtualCursorStyle() {
func (m *Model) SetValue(s string) {
m.Reset()
m.InsertString(s)
m.recalculateHeight()
}

// InsertString inserts a string at the cursor position.
func (m *Model) InsertString(s string) {
m.insertRunesFromUserInput([]rune(s))
m.recalculateHeight()
}

// InsertRune inserts a rune at the cursor position.
func (m *Model) InsertRune(r rune) {
m.insertRunesFromUserInput([]rune{r})
m.recalculateHeight()
}

// insertRunesFromUserInput inserts runes at the current cursor position.
Expand Down Expand Up @@ -521,6 +541,18 @@ func (m *Model) insertRunesFromUserInput(runes []rune) {
lines = lines[:allowedHeight]
}

// Obey MaxContentHeight in visual rows when set.
if m.MaxContentHeight > 0 {
budget := m.MaxContentHeight - m.totalVisualLines()
// Trim lines from the end until we fit within the budget.
for len(lines) > 1 && m.visualLinesForInsert(lines) > budget {
lines = lines[:len(lines)-1]
}
if m.visualLinesForInsert(lines) > budget {
return
}
}

if len(lines) == 0 {
// Nothing left to insert.
return
Expand Down Expand Up @@ -740,6 +772,7 @@ func (m *Model) Reset() {
m.row = 0
m.viewport.GotoTop()
m.SetCursorColumn(0)
m.recalculateHeight()
}

// Word returns the word at the cursor position.
Expand Down Expand Up @@ -1134,6 +1167,7 @@ func (m *Model) SetWidth(w int) {

m.viewport.SetWidth(inputWidth - reservedOuter)
m.width = inputWidth - reservedOuter - reservedInner
m.recalculateHeight()
}

// SetPromptFunc supersedes the Prompt field and sets a dynamic prompt instead.
Expand Down Expand Up @@ -1238,7 +1272,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
}
m.deleteWordRight()
case key.Matches(msg, m.KeyMap.InsertNewline):
if m.MaxHeight > 0 && len(m.value) >= m.MaxHeight {
if m.atContentLimit() {
return m, nil
}
m.col = clamp(m.col, 0, len(m.value[m.row]))
Expand Down Expand Up @@ -1289,6 +1323,8 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
m.Err = msg
}

m.recalculateHeight()

// Make sure we set the content of the viewport before updating it.
view := m.view()
m.viewport.SetContent(view)
Expand Down Expand Up @@ -1627,6 +1663,76 @@ func (m Model) cursorLineNumber() int {
return line
}

// totalVisualLines returns the total number of display lines across all
// logical lines, accounting for soft wraps.
func (m *Model) totalVisualLines() int {
n := 0
for _, line := range m.value {
n += len(m.memoizedWrap(line, m.width))
}
return n
}

// recalculateHeight recomputes and applies the textarea height based on
// content when DynamicHeight is enabled. It is a no-op otherwise.
func (m *Model) recalculateHeight() {
if !m.DynamicHeight {
return
}
minH := max(m.MinHeight, minHeight)
total := m.totalVisualLines()
h := max(total, minH)
if m.MaxHeight > 0 {
h = min(h, m.MaxHeight)
}
if maxOffset := total - h; m.viewport.YOffset() > maxOffset {
m.viewport.SetYOffset(max(0, maxOffset))
}
m.SetHeight(h)
}

// atContentLimit reports whether the textarea has reached its content limit.
// When MaxContentHeight is set (> 0), it checks total visual lines.
// Otherwise it falls back to the legacy MaxHeight logical-line check for
// backward compatibility.
func (m *Model) atContentLimit() bool {
if m.MaxContentHeight > 0 {
return m.totalVisualLines() >= m.MaxContentHeight
}
return m.MaxHeight > 0 && len(m.value) >= m.MaxHeight
}

// visualLinesForInsert estimates how many additional visual lines would result
// from inserting the given lines at the current cursor position. The first
// element merges into the current line; subsequent elements become new lines.
func (m *Model) visualLinesForInsert(lines [][]rune) int {
if len(lines) == 0 {
return 0
}

// The current row's visual line count before insertion.
currentRowVisual := len(m.memoizedWrap(m.value[m.row], m.width))

// Simulate merging the first paste line into the current row.
merged := make([]rune, m.col+len(lines[0]))
copy(merged, m.value[m.row][:m.col])
copy(merged[m.col:], lines[0])
if len(lines) == 1 {
merged = append(merged, m.value[m.row][m.col:]...)
}
delta := len(m.memoizedWrap(merged, m.width)) - currentRowVisual

// Each additional line is a new logical line.
for i, content := range lines {
if i == len(lines)-1 {
content = append(content, m.value[m.row][m.col:]...)
}
delta += len(m.memoizedWrap(content, m.width))
}

return delta
}

// mergeLineBelow merges the current line the cursor is on with the line below.
func (m *Model) mergeLineBelow(row int) {
if row >= len(m.value)-1 {
Expand Down
Loading