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