Skip to content

Commit db5794c

Browse files
authored
Merge pull request #2175 from yunus25jmi1/fix/table-copy-selection-mismatch
Fix table text copy not matching selection
2 parents afd1dfc + 792e6eb commit db5794c

1 file changed

Lines changed: 48 additions & 14 deletions

File tree

pkg/tui/components/messages/clipboard.go

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -112,33 +112,41 @@ func (m *model) extractSelectedText() string {
112112
line := stripBorderChars(plainLine)
113113
runes := []rune(line)
114114

115-
// Calculate how many display columns were removed by stripping border chars
116-
borderOffset := runewidth.StringWidth(plainLine) - runewidth.StringWidth(line)
115+
// Map visual column positions from the plain line (with borders) to the
116+
// stripped line (without borders) by tracking which runes correspond to
117+
// which visual columns
118+
visualToRune := make(map[int]int)
119+
visualCol := 0
120+
lineRuneIdx := 0
121+
for _, r := range plainLine {
122+
if !boxDrawingChars[r] {
123+
// This rune is kept in the stripped line
124+
visualToRune[visualCol] = lineRuneIdx
125+
lineRuneIdx++
126+
}
127+
visualCol += runewidth.RuneWidth(r)
128+
}
117129

118-
// Adjust column positions by subtracting the border offset
119-
adjustedStartCol := max(0, startCol-borderOffset)
120-
adjustedEndCol := max(0, endCol-borderOffset)
130+
// Find the closest rune index for the start and end columns
131+
startRuneIdx := findClosestRuneIndex(visualToRune, startCol, len(runes))
132+
endRuneIdx := findClosestRuneIndex(visualToRune, endCol, len(runes))
121133

122134
var lineText string
123135
switch i {
124136
case startLine:
125137
if startLine == endLine {
126-
sIdx := displayWidthToRuneIndex(line, adjustedStartCol)
127-
eIdx := min(displayWidthToRuneIndex(line, adjustedEndCol), len(runes))
128-
if sIdx < len(runes) && sIdx < eIdx {
129-
lineText = strings.TrimSpace(string(runes[sIdx:eIdx]))
138+
if startRuneIdx < len(runes) && startRuneIdx < endRuneIdx {
139+
lineText = strings.TrimSpace(string(runes[startRuneIdx:endRuneIdx]))
130140
}
131141
break
132142
}
133143
// First line: from startCol to end
134-
sIdx := displayWidthToRuneIndex(line, adjustedStartCol)
135-
if sIdx < len(runes) {
136-
lineText = strings.TrimSpace(string(runes[sIdx:]))
144+
if startRuneIdx < len(runes) {
145+
lineText = strings.TrimSpace(string(runes[startRuneIdx:]))
137146
}
138147
case endLine:
139148
// Last line: from start to endCol
140-
eIdx := min(displayWidthToRuneIndex(line, adjustedEndCol), len(runes))
141-
lineText = strings.TrimSpace(string(runes[:eIdx]))
149+
lineText = strings.TrimSpace(string(runes[:endRuneIdx]))
142150
default:
143151
// Middle lines: entire line
144152
lineText = strings.TrimSpace(line)
@@ -153,6 +161,32 @@ func (m *model) extractSelectedText() string {
153161
return result.String()
154162
}
155163

164+
// findClosestRuneIndex finds the rune index for a given visual column,
165+
// or the closest next rune if the exact column doesn't exist
166+
func findClosestRuneIndex(visualToRune map[int]int, visualCol, maxRunes int) int {
167+
// Try exact match first
168+
if runeIdx, ok := visualToRune[visualCol]; ok {
169+
return runeIdx
170+
}
171+
172+
// Find the next available rune index after the visual column
173+
for col := visualCol + 1; col <= visualCol+10; col++ {
174+
if runeIdx, ok := visualToRune[col]; ok {
175+
return runeIdx
176+
}
177+
}
178+
179+
// Find the previous available rune index
180+
for col := visualCol - 1; col >= 0; col-- {
181+
if runeIdx, ok := visualToRune[col]; ok {
182+
return runeIdx
183+
}
184+
}
185+
186+
// Fallback: return the last rune index
187+
return maxRunes
188+
}
189+
156190
// copySelectionToClipboard copies the currently selected text to clipboard
157191
func (m *model) copySelectionToClipboard() tea.Cmd {
158192
if !m.selection.active {

0 commit comments

Comments
 (0)