@@ -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
157193func (m * model ) copySelectionToClipboard () tea.Cmd {
158194 if ! m .selection .active {
0 commit comments