Skip to content

Commit 89fbd72

Browse files
committed
Fix table text copy not matching selection
When selecting text in tables, the copied content did not match the visual selection because border characters (│, ─, etc.) were not properly accounted for when mapping visual column positions to text content. The fix maps visual column positions from the rendered line (with borders) to the stripped line (without borders) by tracking which runes correspond to which visual columns, ensuring the copied text matches what the user selected. Fixes: #2167 Signed-off-by: Md Yunus <admin@yunuscollege.eu.org>
1 parent 3ff5495 commit 89fbd72

1 file changed

Lines changed: 50 additions & 14 deletions

File tree

pkg/tui/components/messages/clipboard.go

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -112,33 +112,43 @@ 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+
plainRunes := []rune(plainLine)
120+
visualCol := 0
121+
lineRuneIdx := 0
122+
123+
for _, r := range plainRunes {
124+
if !boxDrawingChars[r] {
125+
// This rune is kept in the stripped line
126+
visualToRune[visualCol] = lineRuneIdx
127+
lineRuneIdx++
128+
}
129+
visualCol += runewidth.RuneWidth(r)
130+
}
117131

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

122136
var lineText string
123137
switch i {
124138
case startLine:
125139
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]))
140+
if startRuneIdx < len(runes) && startRuneIdx < endRuneIdx {
141+
lineText = strings.TrimSpace(string(runes[startRuneIdx:endRuneIdx]))
130142
}
131143
break
132144
}
133145
// First line: from startCol to end
134-
sIdx := displayWidthToRuneIndex(line, adjustedStartCol)
135-
if sIdx < len(runes) {
136-
lineText = strings.TrimSpace(string(runes[sIdx:]))
146+
if startRuneIdx < len(runes) {
147+
lineText = strings.TrimSpace(string(runes[startRuneIdx:]))
137148
}
138149
case endLine:
139150
// Last line: from start to endCol
140-
eIdx := min(displayWidthToRuneIndex(line, adjustedEndCol), len(runes))
141-
lineText = strings.TrimSpace(string(runes[:eIdx]))
151+
lineText = strings.TrimSpace(string(runes[:endRuneIdx]))
142152
default:
143153
// Middle lines: entire line
144154
lineText = strings.TrimSpace(line)
@@ -153,6 +163,32 @@ func (m *model) extractSelectedText() string {
153163
return result.String()
154164
}
155165

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

0 commit comments

Comments
 (0)