@@ -14,9 +14,17 @@ import { useOpentuiPaste } from '../hooks/use-opentui-paste'
1414import { useTheme } from '../hooks/use-theme'
1515import { clamp } from '../utils/math'
1616import { computeInputLayoutMetrics } from '../utils/text-layout'
17+ import { calculateNewCursorPosition } from '../utils/word-wrap-utils'
1718
1819import type { InputValue } from '../state/chat-store'
19- import type { KeyEvent , PasteEvent , ScrollBoxRenderable } from '@opentui/core'
20+ import type {
21+ KeyEvent ,
22+ LineInfo ,
23+ PasteEvent ,
24+ ScrollBoxRenderable ,
25+ TextBufferView ,
26+ TextRenderable ,
27+ } from '@opentui/core'
2028
2129// Helper functions for text manipulation
2230function findLineStart ( text : string , cursor : number ) : number {
@@ -210,6 +218,33 @@ export const MultilineInput = forwardRef<
210218 setMeasuredCols ( cols )
211219 } , [ width ] )
212220
221+ const textRef = useRef < TextRenderable | null > ( null )
222+
223+ // Helper function to get current lineInfo from the text ref
224+ const getLineInfo = useCallback ( ( ) => {
225+ if ( ! textRef . current ) {
226+ return {
227+ lineStarts : [ ] ,
228+ lineWidths : [ ] ,
229+ maxLineWidth : 0 ,
230+ } satisfies LineInfo
231+ }
232+
233+ return ( ( textRef . current as any ) . textBufferView as TextBufferView ) . lineInfo
234+ } , [ ] )
235+
236+ const isPlaceholder = value . length === 0 && placeholder . length > 0
237+ const displayValue = isPlaceholder ? placeholder : value
238+ const showCursor = focused
239+ const beforeCursor = showCursor ? displayValue . slice ( 0 , cursorPosition ) : ''
240+ const afterCursor = showCursor ? displayValue . slice ( cursorPosition ) : ''
241+ const activeChar = afterCursor . charAt ( 0 ) || ' '
242+ const shouldHighlight =
243+ showCursor &&
244+ ! isPlaceholder &&
245+ cursorPosition < displayValue . length &&
246+ displayValue [ cursorPosition ] !== '\n'
247+
213248 // Handle all keyboard input with advanced shortcuts
214249 useKeyboard (
215250 useCallback (
@@ -607,65 +642,32 @@ export const MultilineInput = forwardRef<
607642 // Up arrow (no modifiers)
608643 if ( key . name === 'up' && ! key . ctrl && ! key . meta && ! key . option ) {
609644 preventKeyDefault ( key )
610- const cols = getEffectiveCols ( )
611- const prevNewline = value . lastIndexOf ( '\n' , cursorPosition - 1 )
612- if ( cursorPosition - cols >= prevNewline ) {
613- onChange ( {
614- text : value ,
615- cursorPosition : cursorPosition - cols ,
616- lastEditDueToNav : false ,
617- } )
618- return
619- }
620-
621- const priorNewline = value . lastIndexOf ( '\n' , prevNewline - 1 )
622- const lastParagraphLength = prevNewline - priorNewline
623- const lastRow = Math . floor ( lastParagraphLength / cols )
624645 onChange ( {
625646 text : value ,
626- cursorPosition : Math . min (
627- priorNewline + lastRow * cols + cursorPosition - prevNewline ,
628- prevNewline ,
629- ) ,
647+ cursorPosition : calculateNewCursorPosition ( {
648+ cursorPosition,
649+ lineInfo : getLineInfo ( ) ,
650+ cursorIsChar : ! shouldHighlight ,
651+ direction : 'up' ,
652+ } ) ,
630653 lastEditDueToNav : false ,
631654 } )
655+ return
632656 }
633657
634658 // Down arrow (no modifiers)
635659 if ( key . name === 'down' && ! key . ctrl && ! key . meta && ! key . option ) {
636- preventKeyDefault ( key )
637- const cols = getEffectiveCols ( )
638- let nextNewlineInclusive = value . indexOf ( '\n' , cursorPosition )
639- if ( nextNewlineInclusive === - 1 ) {
640- nextNewlineInclusive = value . length
641- }
642- if ( cursorPosition + cols <= nextNewlineInclusive ) {
643- onChange ( {
644- text : value ,
645- cursorPosition : cursorPosition + cols ,
646- lastEditDueToNav : false ,
647- } )
648- return
649- }
650-
651- let afterNewline = value . indexOf ( '\n' , nextNewlineInclusive + 1 )
652- if ( afterNewline === - 1 ) {
653- afterNewline = value . length
654- }
655- /*
656- * The second argument of lastIndexOf is converted to 0 if it's
657- * negative, so we need to special case cursorPosition === 0
658- */
659- let prevNewlineExclusive =
660- cursorPosition === 0
661- ? - 1
662- : value . lastIndexOf ( '\n' , cursorPosition - 1 )
663- const col = ( cursorPosition - prevNewlineExclusive ) % cols
664660 onChange ( {
665661 text : value ,
666- cursorPosition : Math . min ( nextNewlineInclusive + col , afterNewline ) ,
662+ cursorPosition : calculateNewCursorPosition ( {
663+ cursorPosition,
664+ lineInfo : getLineInfo ( ) ,
665+ cursorIsChar : ! shouldHighlight ,
666+ direction : 'down' ,
667+ } ) ,
667668 lastEditDueToNav : false ,
668669 } )
670+ return
669671 }
670672
671673 // Regular character input
@@ -689,22 +691,20 @@ export const MultilineInput = forwardRef<
689691 return
690692 }
691693 } ,
692- [ focused , value , cursorPosition , onChange , onSubmit , onKeyIntercept ] ,
694+ [
695+ focused ,
696+ value ,
697+ cursorPosition ,
698+ shouldHighlight ,
699+ getLineInfo ,
700+ onChange ,
701+ onSubmit ,
702+ onKeyIntercept ,
703+ ] ,
693704 ) ,
694705 )
695706
696707 // Calculate display with cursor
697- const isPlaceholder = value . length === 0 && placeholder . length > 0
698- const displayValue = isPlaceholder ? placeholder : value
699- const showCursor = focused
700- const beforeCursor = showCursor ? displayValue . slice ( 0 , cursorPosition ) : ''
701- const afterCursor = showCursor ? displayValue . slice ( cursorPosition ) : ''
702- const activeChar = afterCursor . charAt ( 0 ) || ' '
703- const shouldHighlight =
704- showCursor &&
705- ! isPlaceholder &&
706- cursorPosition < displayValue . length &&
707- displayValue [ cursorPosition ] !== '\n'
708708
709709 const layoutContent = showCursor
710710 ? shouldHighlight
@@ -777,7 +777,7 @@ export const MultilineInput = forwardRef<
777777 } ,
778778 } }
779779 >
780- < text style = { { ...textStyle , wrapMode : 'char ' } } >
780+ < text ref = { textRef } style = { { ...textStyle , wrapMode : 'word ' } } >
781781 { showCursor ? (
782782 < >
783783 { beforeCursor }
0 commit comments