summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-07-10 07:49:36 -0500
committeradamdottv <[email protected]>2025-07-10 07:49:36 -0500
commitd2b1307bff9bc0ad6634b6f22058841771a0adaf (patch)
treea6bd644434b5474462f0a868a94c5003b2dde615
parentb40ba32adcbef9af1118e118e436fe3c25eaa6bb (diff)
downloadopencode-d2b1307bff9bc0ad6634b6f22058841771a0adaf.tar.gz
opencode-d2b1307bff9bc0ad6634b6f22058841771a0adaf.zip
fix(tui): textarea cursor sync issues with attachments
-rw-r--r--packages/tui/internal/components/textarea/textarea.go69
1 files changed, 65 insertions, 4 deletions
diff --git a/packages/tui/internal/components/textarea/textarea.go b/packages/tui/internal/components/textarea/textarea.go
index 7482c7adc..650d0e177 100644
--- a/packages/tui/internal/components/textarea/textarea.go
+++ b/packages/tui/internal/components/textarea/textarea.go
@@ -931,7 +931,6 @@ func (m *Model) mapVisualOffsetToSliceIndex(row int, charOffset int) int {
}
// CursorDown moves the cursor down by one line.
-// Returns whether or not the cursor blink should be reset.
func (m *Model) CursorDown() {
li := m.LineInfo()
charOffset := max(m.lastCharOffset, li.CharOffset)
@@ -940,11 +939,73 @@ func (m *Model) CursorDown() {
if li.RowOffset+1 >= li.Height && m.row < len(m.value)-1 {
// Move to the next model line
m.row++
- m.col = m.mapVisualOffsetToSliceIndex(m.row, charOffset)
+
+ // We want to land on the first wrapped line of the new model line.
+ grid := m.memoizedWrap(m.value[m.row], m.width)
+ targetLineContent := grid[0]
+
+ // Find position within the first wrapped line.
+ offset := 0
+ colInLine := 0
+ for i, item := range targetLineContent {
+ var itemWidth int
+ switch v := item.(type) {
+ case rune:
+ itemWidth = rw.RuneWidth(v)
+ case *Attachment:
+ itemWidth = uniseg.StringWidth(v.Display)
+ }
+ if offset+itemWidth > charOffset {
+ // Decide whether to stick with the previous index or move to the current
+ // one based on which is closer to the target offset.
+ if (charOffset - offset) > ((offset + itemWidth) - charOffset) {
+ colInLine = i + 1
+ } else {
+ colInLine = i
+ }
+ goto foundNextLine
+ }
+ offset += itemWidth
+ }
+ colInLine = len(targetLineContent)
+ foundNextLine:
+ m.col = colInLine // startCol is 0 for the first wrapped line
} else if li.RowOffset+1 < li.Height {
// Move to the next wrapped line within the same model line
- startOfNextWrappedLine := li.StartColumn + li.Width
- m.col = startOfNextWrappedLine + m.mapVisualOffsetToSliceIndex(m.row, charOffset)
+ grid := m.memoizedWrap(m.value[m.row], m.width)
+ targetLineContent := grid[li.RowOffset+1]
+
+ startCol := 0
+ for i := 0; i < li.RowOffset+1; i++ {
+ startCol += len(grid[i])
+ }
+
+ // Find position within the target wrapped line.
+ offset := 0
+ colInLine := 0
+ for i, item := range targetLineContent {
+ var itemWidth int
+ switch v := item.(type) {
+ case rune:
+ itemWidth = rw.RuneWidth(v)
+ case *Attachment:
+ itemWidth = uniseg.StringWidth(v.Display)
+ }
+ if offset+itemWidth > charOffset {
+ // Decide whether to stick with the previous index or move to the current
+ // one based on which is closer to the target offset.
+ if (charOffset - offset) > ((offset + itemWidth) - charOffset) {
+ colInLine = i + 1
+ } else {
+ colInLine = i
+ }
+ goto foundSameLine
+ }
+ offset += itemWidth
+ }
+ colInLine = len(targetLineContent)
+ foundSameLine:
+ m.col = startCol + colInLine
}
m.SetCursorColumn(m.col)
}