diff options
| author | adamdottv <[email protected]> | 2025-07-10 07:49:36 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-07-10 07:49:36 -0500 |
| commit | d2b1307bff9bc0ad6634b6f22058841771a0adaf (patch) | |
| tree | a6bd644434b5474462f0a868a94c5003b2dde615 | |
| parent | b40ba32adcbef9af1118e118e436fe3c25eaa6bb (diff) | |
| download | opencode-d2b1307bff9bc0ad6634b6f22058841771a0adaf.tar.gz opencode-d2b1307bff9bc0ad6634b6f22058841771a0adaf.zip | |
fix(tui): textarea cursor sync issues with attachments
| -rw-r--r-- | packages/tui/internal/components/textarea/textarea.go | 69 |
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) } |
