@@ -853,7 +853,7 @@ export interface TextBufferState {
853853 lines : string [ ] ;
854854 cursorRow : number ;
855855 cursorCol : number ;
856- preferredCol : number | null ; // This is visual preferred col
856+ preferredCol : number | null ; // This is the logical character offset in the visual line
857857 undoStack : UndoHistoryEntry [ ] ;
858858 redoStack : UndoHistoryEntry [ ] ;
859859 clipboard : string | null ;
@@ -2022,20 +2022,40 @@ export function useTextBuffer({
20222022 Math . min ( visRow , visualLines . length - 1 ) ,
20232023 ) ;
20242024 const visualLine = visualLines [ clampedVisRow ] || '' ;
2025- // Clamp visCol to the length of the visual line
2026- const clampedVisCol = Math . max ( 0 , Math . min ( visCol , cpLen ( visualLine ) ) ) ;
20272025
20282026 if ( visualToLogicalMap [ clampedVisRow ] ) {
20292027 const [ logRow , logStartCol ] = visualToLogicalMap [ clampedVisRow ] ;
2028+
2029+ const codePoints = toCodePoints ( visualLine ) ;
2030+ let currentVisX = 0 ;
2031+ let charOffset = 0 ;
2032+
2033+ for ( const char of codePoints ) {
2034+ const charWidth = getCachedStringWidth ( char ) ;
2035+ // If the click is within this character
2036+ if ( visCol < currentVisX + charWidth ) {
2037+ // Check if we clicked the second half of a wide character
2038+ if ( charWidth > 1 && visCol >= currentVisX + charWidth / 2 ) {
2039+ charOffset ++ ;
2040+ }
2041+ break ;
2042+ }
2043+ currentVisX += charWidth ;
2044+ charOffset ++ ;
2045+ }
2046+
2047+ // Clamp charOffset to length
2048+ charOffset = Math . min ( charOffset , codePoints . length ) ;
2049+
20302050 const newCursorRow = logRow ;
2031- const newCursorCol = logStartCol + clampedVisCol ;
2051+ const newCursorCol = logStartCol + charOffset ;
20322052
20332053 dispatch ( {
20342054 type : 'set_cursor' ,
20352055 payload : {
20362056 cursorRow : newCursorRow ,
20372057 cursorCol : newCursorCol ,
2038- preferredCol : clampedVisCol ,
2058+ preferredCol : charOffset ,
20392059 } ,
20402060 } ) ;
20412061 }
0 commit comments