summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-07-10 07:38:49 -0500
committeradamdottv <[email protected]>2025-07-10 07:38:57 -0500
commitb40ba32adcbef9af1118e118e436fe3c25eaa6bb (patch)
tree6d502227b1e7c485e3c03d73089be1d11d3027cc
parentce0cebb7d748e00a754119e8944681eabc6f1f47 (diff)
downloadopencode-b40ba32adcbef9af1118e118e436fe3c25eaa6bb.tar.gz
opencode-b40ba32adcbef9af1118e118e436fe3c25eaa6bb.zip
fix(tui): textarea issues
-rw-r--r--packages/tui/internal/components/textarea/textarea.go122
1 files changed, 95 insertions, 27 deletions
diff --git a/packages/tui/internal/components/textarea/textarea.go b/packages/tui/internal/components/textarea/textarea.go
index 41a4a3d91..7482c7adc 100644
--- a/packages/tui/internal/components/textarea/textarea.go
+++ b/packages/tui/internal/components/textarea/textarea.go
@@ -956,22 +956,77 @@ func (m *Model) CursorUp() {
m.lastCharOffset = charOffset
if li.RowOffset <= 0 && m.row > 0 {
- // Move to the previous model line
+ // Move to the previous model line. We want to land on the last wrapped
+ // line of the previous model line.
m.row--
- m.col = m.mapVisualOffsetToSliceIndex(m.row, charOffset)
+ grid := m.memoizedWrap(m.value[m.row], m.width)
+ targetLineContent := grid[len(grid)-1]
+
+ // Find start of last wrapped line.
+ startCol := len(m.value[m.row]) - len(targetLineContent)
+
+ // Find position within the last 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 foundPrevLine
+ }
+ offset += itemWidth
+ }
+ colInLine = len(targetLineContent)
+ foundPrevLine:
+ m.col = startCol + colInLine
} else if li.RowOffset > 0 {
- // Move to the previous wrapped line within the same model line
- // To do this, we need to find the start of the previous wrapped line.
- prevLineInfo := m.LineInfo()
- // prevLineStart := 0
- if prevLineInfo.RowOffset > 0 {
- // This is complex, so we'll approximate by moving to the start of the current wrapped line
- // and then letting characterLeft handle it. A more precise calculation would
- // require re-wrapping to find the previous line's start.
- // For now, a simpler approach:
- m.col = li.StartColumn - 1
+ // Move to the previous wrapped line within the same model line.
+ 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])
}
- m.col = m.mapVisualOffsetToSliceIndex(m.row, charOffset)
+
+ // 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)
}
@@ -1591,7 +1646,7 @@ func (m Model) View() string {
// guaranteed to be a space since any other character would
// have been wrapped.
wrappedLineStr = strings.TrimSuffix(wrappedLineStr, " ")
- padding -= m.width - strwidth
+ padding = m.width - uniseg.StringWidth(wrappedLineStr)
}
if m.row == l && lineInfo.RowOffset == wl {
@@ -1903,6 +1958,16 @@ func (m *Model) splitLine(row, col int) {
m.row++
}
+func itemWidth(item any) int {
+ switch v := item.(type) {
+ case rune:
+ return rw.RuneWidth(v)
+ case *Attachment:
+ return uniseg.StringWidth(v.Display)
+ }
+ return 0
+}
+
func wrapInterfaces(content []any, width int) [][]any {
if width <= 0 {
return [][]any{content}
@@ -1945,19 +2010,18 @@ func wrapInterfaces(content []any, width int) [][]any {
}
inSpaces = true
spaceW += itemW
- } else {
+ } else { // It's not a space, it's a character for a word.
if inSpaces {
- // End of spaces
- if lineW > 0 && lineW+spaceW > width {
- lines = append(lines, []any{})
- lineW = 0
- } else {
- lineW += spaceW
- }
- // Add spaces to current line
+ // We just finished a block of spaces. Handle them now.
+ lineW += spaceW
for i := 0; i < spaceW; i++ {
lines[len(lines)-1] = append(lines[len(lines)-1], rune(' '))
}
+ if lineW > width {
+ // The spaces made the line overflow. Start a new line for the upcoming word.
+ lines = append(lines, []any{})
+ lineW = 0
+ }
spaceW = 0
}
inSpaces = false
@@ -1966,20 +2030,24 @@ func wrapInterfaces(content []any, width int) [][]any {
}
}
- // Handle any remaining word/spaces
+ // Handle any remaining word/spaces at the end of the content.
if wordW > 0 {
if lineW > 0 && lineW+wordW > width {
lines = append(lines, word)
+ lineW = wordW
} else {
lines[len(lines)-1] = append(lines[len(lines)-1], word...)
+ lineW += wordW
}
}
if spaceW > 0 {
- if lineW > 0 && lineW+spaceW > width {
- lines = append(lines, []any{})
- }
+ // There are trailing spaces. Add them.
for i := 0; i < spaceW; i++ {
lines[len(lines)-1] = append(lines[len(lines)-1], rune(' '))
+ lineW += 1
+ }
+ if lineW > width {
+ lines = append(lines, []any{})
}
}