textarea: expose the cursor as a byte offset into Value()
Is your feature request related to a problem? Please describe.
Line() and Column() locate the cursor in the value's grid of rows and runes. That is the wrong coordinate space for a caller that wants to treat Value() as a flat string — slicing around the cursor, scanning backwards for a token boundary, splicing a completion in.
Converting grid coordinates to a byte index means re-deriving the join Value() already performed, and every caller writes the same loop:
off := 0
for i, row := range /* rows the caller cannot reach */ {
if i == m.Line() { ... }
off += len(string(row)) + 1
}m.value is unexported, so callers cannot even write that loop against the real rows — they have to re-split Value() on "\n" and hope the split matches.
Describe the solution you'd like
The pair that closes the round trip:
func (m Model) ByteOffset() int
func (m *Model) SetCursorByteOffset(off int)ByteOffset always lands on a UTF-8 boundary. SetCursorByteOffset clamps a negative offset to the start and a past-the-end offset to the end, and snaps an offset landing inside a multi-byte rune forward to the next real position — so round-tripping an offset produced by ByteOffset is exact. It also repositions the viewport, like every other cursor mover, so restoring a saved offset after an edit cannot leave the cursor scrolled out of sight.
Describe alternatives you've considered
- Callers computing it from
Value(). The status quo. It duplicates the join, and the mid-rune case is quiet and surprising to get wrong: slicing a row at the requested byte yields a partial UTF-8 sequence that decodes to U+FFFD and counts as a whole rune, so asking for byte 2 of"世界"lands the cursor at byte 6 — past the rune the caller was pointing into, not before it. - Exposing the rows instead (
func (m Model) Rows() [][]rune). Larger surface, still leaves every caller writing the offset loop. - Rune offsets rather than byte offsets. Byte offsets are what
Value()slicing,regexp, andstrings.Indexall speak.
Additional context
Implementation and tests in #1031. No new dependencies; two new methods, no change to existing behaviour.
Posted on behalf of @joestump by claude-opus-5 using Claude Code.
Source: charmbracelet/bubbles