1- import { Box , Text , useInput , useStdout } from 'ink' ;
2- import { useEffect , useMemo , useRef , useState } from 'react' ;
1+ import { Box , Text , useInput , usePaste , useStdout } from 'ink' ;
2+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
33
44interface Props {
55 value : string ;
66 isDisabled ?: boolean ;
77 placeholder ?: string ;
88 cursorPosition ?: number ;
9+ allowMultilinePaste ?: boolean ;
910 wrapIndent ?: number ;
1011 onChange : ( value : string ) => void ;
1112 onSubmit : ( value : string ) => void ;
@@ -25,48 +26,64 @@ function buildLineSegments(
2526 width : number ,
2627) : LineSegment [ ] {
2728 const safeWidth = Math . max ( 1 , width ) ;
28- const cursorChar = displayValue [ cursorPosition ] || ' ' ;
29- const renderValue =
30- displayValue . slice ( 0 , cursorPosition ) +
31- cursorChar +
32- displayValue . slice ( cursorPosition + 1 ) ;
33- const totalLength = Math . max ( 1 , renderValue . length ) ;
3429 const lines : LineSegment [ ] = [ ] ;
30+ const logicalLines = displayValue . split ( '\n' ) ;
31+ let lineStart = 0 ;
3532
36- for ( let start = 0 ; start < totalLength ; start += safeWidth ) {
37- const end = start + safeWidth ;
38- const text = renderValue . slice ( start , end ) ;
39- const hasCursor = cursorPosition >= start && cursorPosition < end ;
33+ for ( const [ lineIndex , logicalLine ] of logicalLines . entries ( ) ) {
34+ const lineEnd = lineStart + logicalLine . length ;
35+ const hasCursorOnLine =
36+ cursorPosition >= lineStart && cursorPosition <= lineEnd ;
37+ const cursorOffset = cursorPosition - lineStart ;
38+ const renderValue =
39+ hasCursorOnLine && cursorOffset === logicalLine . length
40+ ? `${ logicalLine } `
41+ : logicalLine ;
42+ const totalLength = Math . max ( 1 , renderValue . length ) ;
4043
41- if ( ! hasCursor ) {
44+ for ( let start = 0 ; start < totalLength ; start += safeWidth ) {
45+ const end = start + safeWidth ;
46+ const text = renderValue . slice ( start , end ) ;
47+ const hasCursor =
48+ hasCursorOnLine && cursorOffset >= start && cursorOffset < end ;
49+
50+ if ( ! hasCursor ) {
51+ lines . push ( {
52+ text,
53+ hasCursor,
54+ beforeCursor : '' ,
55+ cursorChar : ' ' ,
56+ afterCursor : '' ,
57+ } ) ;
58+ continue ;
59+ }
60+
61+ const offset = cursorOffset - start ;
4262 lines . push ( {
4363 text,
4464 hasCursor,
45- beforeCursor : '' ,
46- cursorChar : ' ' ,
47- afterCursor : '' ,
65+ beforeCursor : text . slice ( 0 , offset ) ,
66+ cursorChar : text [ offset ] ,
67+ afterCursor : text . slice ( offset + 1 ) ,
4868 } ) ;
49- continue ;
5069 }
5170
52- const offset = cursorPosition - start ;
53- lines . push ( {
54- text,
55- hasCursor,
56- beforeCursor : text . slice ( 0 , offset ) ,
57- cursorChar : text [ offset ] || ' ' ,
58- afterCursor : text . slice ( offset + 1 ) ,
59- } ) ;
71+ lineStart = lineEnd + ( lineIndex < logicalLines . length - 1 ? 1 : 0 ) ;
6072 }
6173
6274 return lines ;
6375}
6476
77+ function normalizePastedText ( input : string ) {
78+ return input . replaceAll ( '\r\n' , '\n' ) . replaceAll ( '\r' , '\n' ) ;
79+ }
80+
6581export function TextInput ( {
6682 value,
6783 isDisabled = false ,
6884 placeholder,
6985 cursorPosition : externalCursorPosition ,
86+ allowMultilinePaste = false ,
7087 wrapIndent = 0 ,
7188 onChange,
7289 onSubmit,
@@ -109,13 +126,37 @@ export function TextInput({
109126 }
110127 } , [ value , cursorPosition , externalCursorPosition ] ) ;
111128
129+ const insertText = useCallback (
130+ ( text : string ) => {
131+ const newValue =
132+ value . slice ( 0 , cursorPosition ) + text + value . slice ( cursorPosition ) ;
133+ onChange ( newValue ) ;
134+ setCursorPosition ( cursorPosition + text . length ) ;
135+ } ,
136+ [ cursorPosition , onChange , value ] ,
137+ ) ;
138+
139+ usePaste (
140+ ( text ) => {
141+ insertText ( normalizePastedText ( text ) ) ;
142+ } ,
143+ { isActive : allowMultilinePaste && ! isDisabled } ,
144+ ) ;
145+
112146 useInput (
113147 ( input , key ) => {
114148 // v8 ignore next
115149 if ( isDisabled ) {
116150 return ;
117151 }
118152
153+ const hasPastedNewlines = allowMultilinePaste && / [ \r \n ] / . test ( input ) ;
154+
155+ if ( hasPastedNewlines ) {
156+ insertText ( normalizePastedText ( input ) ) ;
157+ return ;
158+ }
159+
119160 if ( key . return ) {
120161 onSubmit ( value ) ;
121162 setCursorPosition ( 0 ) ;
@@ -181,10 +222,7 @@ export function TextInput({
181222
182223 // v8 ignore start
183224 if ( input ) {
184- const newValue =
185- value . slice ( 0 , cursorPosition ) + input + value . slice ( cursorPosition ) ;
186- onChange ( newValue ) ;
187- setCursorPosition ( cursorPosition + input . length ) ;
225+ insertText ( input ) ;
188226 }
189227 // v8 ignore stop
190228 } ,
0 commit comments