Skip to content

Commit

Permalink
feat(textarea): add Cursor method (#712)
Browse files Browse the repository at this point in the history
* feat(textarea): add CursorPosition method

This returns the current cursor position accounting any soft-wrapped
lines and multi-rune characters.

* chore(textarea): redefine how real cursor properties are accessed (#714)

---------

Co-authored-by: Christian Rocha <christian@rocha.is>
  • Loading branch information
aymanbagabas and meowgorithm authored Jan 23, 2025
1 parent 0c83e6f commit 518ff7d
Show file tree
Hide file tree
Showing 3 changed files with 40 additions and 19 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.18
require (
github.com/MakeNowJust/heredoc v1.0.0
github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250114201644-43a5b4dd0af0
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123211610-443afa6fa0c1
github.com/charmbracelet/harmonica v0.2.0
github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20250114171829-b67eb015d607
github.com/charmbracelet/x/ansi v0.7.0
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250114201644-43a5b4dd0af0 h1:BWjXQRSwBjoCpLeNu8zT93n+NHhZZhkQQLveXMmnkYc=
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250114201644-43a5b4dd0af0/go.mod h1:hT2875Ank3ylgW13kqu6cjDc9XIk9sE5JsOFYdl09b8=
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123204203-55f6f9f70bf6 h1:L2+Kl71AsucUpl32AqmbjVv/4Ha7dwlSFwqrU4sAeTE=
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123204203-55f6f9f70bf6/go.mod h1:hT2875Ank3ylgW13kqu6cjDc9XIk9sE5JsOFYdl09b8=
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123210727-80fa20da7d7b h1:QqN3KApDbHJl+B1lVSir6GyRbxH7EA6U1SCDoxz8xYU=
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123210727-80fa20da7d7b/go.mod h1:hT2875Ank3ylgW13kqu6cjDc9XIk9sE5JsOFYdl09b8=
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123210853-99e3bbf892cd h1:1WsMNlPUaDXgJprIvWg+ZsXmc4GiL4KsBEFNZ3ymKeA=
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123210853-99e3bbf892cd/go.mod h1:hT2875Ank3ylgW13kqu6cjDc9XIk9sE5JsOFYdl09b8=
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123211610-443afa6fa0c1 h1:tktnM4YimEWSYd58iZlPDB3Xz25/r94VYZZsHK5zWL0=
github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250123211610-443afa6fa0c1/go.mod h1:hT2875Ank3ylgW13kqu6cjDc9XIk9sE5JsOFYdl09b8=
github.com/charmbracelet/colorprofile v0.1.9 h1:5JnfvX+I9D6rRNu8xK3pgIqknaBVTXHU9pGu1jkZxLw=
github.com/charmbracelet/colorprofile v0.1.9/go.mod h1:+jpmObxZl1Dab3H3IMVIPSZTsKcFpjJUv97G0dLqM60=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
Expand Down
49 changes: 31 additions & 18 deletions textarea/textarea.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,8 @@ type Model struct {
// when switching focus states.
activeStyle *StyleState

// Cursor is the text area cursor.
Cursor cursor.Model
// VirtualCursor is the text area cursor.
VirtualCursor cursor.Model

// CharLimit is the maximum number of characters this input element will
// accept. If 0 or less, there's no limit.
Expand Down Expand Up @@ -305,7 +305,7 @@ func New() Model {
cache: memoization.NewMemoCache[line, [][]rune](maxLines),
EndOfBufferCharacter: ' ',
ShowLineNumbers: true,
Cursor: cur,
VirtualCursor: cur,
KeyMap: DefaultKeyMap(),

value: make([][]rune, minHeight, maxLines),
Expand Down Expand Up @@ -600,15 +600,15 @@ func (m Model) Focused() bool {
func (m *Model) Focus() tea.Cmd {
m.focus = true
m.activeStyle = &m.Styles.Focused
return m.Cursor.Focus()
return m.VirtualCursor.Focus()
}

// Blur removes the focus state on the model. When the model is blurred it can
// not receive keyboard input and the cursor will be hidden.
func (m *Model) Blur() {
m.focus = false
m.activeStyle = &m.Styles.Blurred
m.Cursor.Blur()
m.VirtualCursor.Blur()
}

// Reset sets the input to its default state with no input.
Expand Down Expand Up @@ -976,7 +976,7 @@ func (m *Model) SetHeight(h int) {
// Update is the Bubble Tea update loop.
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
if !m.focus {
m.Cursor.Blur()
m.VirtualCursor.Blur()
return m, nil
}

Expand Down Expand Up @@ -1098,10 +1098,10 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
cmds = append(cmds, cmd)

newRow, newCol := m.cursorLineNumber(), m.col
m.Cursor, cmd = m.Cursor.Update(msg)
if (newRow != oldRow || newCol != oldCol) && m.Cursor.Mode() == cursor.CursorBlink {
m.Cursor.Blink = false
cmd = m.Cursor.BlinkCmd()
m.VirtualCursor, cmd = m.VirtualCursor.Update(msg)
if (newRow != oldRow || newCol != oldCol) && m.VirtualCursor.Mode() == cursor.CursorBlink {
m.VirtualCursor.Blink = false
cmd = m.VirtualCursor.BlinkCmd()
}
cmds = append(cmds, cmd)

Expand All @@ -1115,7 +1115,7 @@ func (m Model) View() string {
if m.Value() == "" && m.row == 0 && m.col == 0 && m.Placeholder != "" {
return m.placeholderView()
}
m.Cursor.TextStyle = m.activeStyle.computedCursorLine()
m.VirtualCursor.TextStyle = m.activeStyle.computedCursorLine()

var (
s strings.Builder
Expand Down Expand Up @@ -1184,11 +1184,11 @@ func (m Model) View() string {
if m.row == l && lineInfo.RowOffset == wl {
s.WriteString(style.Render(string(wrappedLine[:lineInfo.ColumnOffset])))
if m.col >= len(line) && lineInfo.CharOffset >= m.width {
m.Cursor.SetChar(" ")
s.WriteString(m.Cursor.View())
m.VirtualCursor.SetChar(" ")
s.WriteString(m.VirtualCursor.View())
} else {
m.Cursor.SetChar(string(wrappedLine[lineInfo.ColumnOffset]))
s.WriteString(style.Render(m.Cursor.View()))
m.VirtualCursor.SetChar(string(wrappedLine[lineInfo.ColumnOffset]))
s.WriteString(style.Render(m.VirtualCursor.View()))
s.WriteString(style.Render(string(wrappedLine[lineInfo.ColumnOffset+1:])))
}
} else {
Expand Down Expand Up @@ -1291,9 +1291,9 @@ func (m Model) placeholderView() string {
// first line
case i == 0:
// first character of first line as cursor with character
m.Cursor.TextStyle = m.activeStyle.computedPlaceholder()
m.Cursor.SetChar(string(plines[0][0]))
s.WriteString(lineStyle.Render(m.Cursor.View()))
m.VirtualCursor.TextStyle = m.activeStyle.computedPlaceholder()
m.VirtualCursor.SetChar(string(plines[0][0]))
s.WriteString(lineStyle.Render(m.VirtualCursor.View()))

// the rest of the first line
s.WriteString(lineStyle.Render(style.Render(plines[0][1:] + strings.Repeat(" ", max(0, m.width-uniseg.StringWidth(plines[0]))))))
Expand Down Expand Up @@ -1322,6 +1322,19 @@ func Blink() tea.Msg {
return cursor.Blink()
}

// Cursor returns the current cursor position accounting any
// soft-wrapped lines.
func (m Model) Cursor() *tea.Cursor {
lineInfo := m.LineInfo()
x, y := lineInfo.CharOffset, m.cursorLineNumber()-m.viewport.YOffset

// TODO: sort out where these properties live.

Check failure on line 1331 in textarea/textarea.go

View workflow job for this annotation

GitHub Actions / lint / lint-soft (windows-latest)

\textarea\textarea.go:1331: Line contains TODO/BUG/FIXME: "TODO: sort out where these properties li..." (godox)
c := tea.NewCursor(x, y)
c.Blink = true
c.Color = m.VirtualCursor.Style.GetForeground()
return c
}

func (m Model) memoizedWrap(runes []rune, width int) [][]rune {
input := line{runes: runes, width: width}
if v, ok := m.cache.Get(input); ok {
Expand Down

0 comments on commit 518ff7d

Please sign in to comment.