@@ -10,7 +10,7 @@ import { theme } from '../semantic-colors.js';
1010import { SuggestionsDisplay } from './SuggestionsDisplay.js' ;
1111import { useInputHistory } from '../hooks/useInputHistory.js' ;
1212import { TextBuffer , logicalPosToOffset } from './shared/text-buffer.js' ;
13- import { cpSlice , cpLen } from '../utils/textUtils.js' ;
13+ import { cpSlice , cpLen , toCodePoints } from '../utils/textUtils.js' ;
1414import chalk from 'chalk' ;
1515import stringWidth from 'string-width' ;
1616import { useShellHistory } from '../hooks/useShellHistory.js' ;
@@ -403,6 +403,16 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
403403 }
404404 }
405405
406+ // Handle Tab key for ghost text acceptance
407+ if (
408+ key . name === 'tab' &&
409+ ! completion . showSuggestions &&
410+ completion . promptCompletion . text
411+ ) {
412+ completion . promptCompletion . accept ( ) ;
413+ return ;
414+ }
415+
406416 if ( ! shellModeActive ) {
407417 if ( keyMatchers [ Command . HISTORY_UP ] ( key ) ) {
408418 inputHistory . navigateUp ( ) ;
@@ -507,6 +517,17 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
507517
508518 // Fall back to the text buffer's default input handling for all other keys
509519 buffer . handleInput ( key ) ;
520+
521+ // Clear ghost text when user types regular characters (not navigation/control keys)
522+ if (
523+ completion . promptCompletion . text &&
524+ key . sequence &&
525+ key . sequence . length === 1 &&
526+ ! key . ctrl &&
527+ ! key . meta
528+ ) {
529+ completion . promptCompletion . clear ( ) ;
530+ }
510531 } ,
511532 [
512533 focus ,
@@ -540,6 +561,119 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
540561 buffer . visualCursor ;
541562 const scrollVisualRow = buffer . visualScrollRow ;
542563
564+ const getGhostTextLines = useCallback ( ( ) => {
565+ if (
566+ ! completion . promptCompletion . text ||
567+ ! buffer . text ||
568+ ! completion . promptCompletion . text . startsWith ( buffer . text )
569+ ) {
570+ return { inlineGhost : '' , additionalLines : [ ] } ;
571+ }
572+
573+ const ghostSuffix = completion . promptCompletion . text . slice (
574+ buffer . text . length ,
575+ ) ;
576+ if ( ! ghostSuffix ) {
577+ return { inlineGhost : '' , additionalLines : [ ] } ;
578+ }
579+
580+ const currentLogicalLine = buffer . lines [ buffer . cursor [ 0 ] ] || '' ;
581+ const cursorCol = buffer . cursor [ 1 ] ;
582+
583+ const textBeforeCursor = cpSlice ( currentLogicalLine , 0 , cursorCol ) ;
584+ const usedWidth = stringWidth ( textBeforeCursor ) ;
585+ const remainingWidth = Math . max ( 0 , inputWidth - usedWidth ) ;
586+
587+ const ghostTextLinesRaw = ghostSuffix . split ( '\n' ) ;
588+ const firstLineRaw = ghostTextLinesRaw . shift ( ) || '' ;
589+
590+ let inlineGhost = '' ;
591+ let remainingFirstLine = '' ;
592+
593+ if ( stringWidth ( firstLineRaw ) <= remainingWidth ) {
594+ inlineGhost = firstLineRaw ;
595+ } else {
596+ const words = firstLineRaw . split ( ' ' ) ;
597+ let currentLine = '' ;
598+ let wordIdx = 0 ;
599+ for ( const word of words ) {
600+ const prospectiveLine = currentLine ? `${ currentLine } ${ word } ` : word ;
601+ if ( stringWidth ( prospectiveLine ) > remainingWidth ) {
602+ break ;
603+ }
604+ currentLine = prospectiveLine ;
605+ wordIdx ++ ;
606+ }
607+ inlineGhost = currentLine ;
608+ if ( words . length > wordIdx ) {
609+ remainingFirstLine = words . slice ( wordIdx ) . join ( ' ' ) ;
610+ }
611+ }
612+
613+ const linesToWrap = [ ] ;
614+ if ( remainingFirstLine ) {
615+ linesToWrap . push ( remainingFirstLine ) ;
616+ }
617+ linesToWrap . push ( ...ghostTextLinesRaw ) ;
618+ const remainingGhostText = linesToWrap . join ( '\n' ) ;
619+
620+ const additionalLines : string [ ] = [ ] ;
621+ if ( remainingGhostText ) {
622+ const textLines = remainingGhostText . split ( '\n' ) ;
623+ for ( const textLine of textLines ) {
624+ const words = textLine . split ( ' ' ) ;
625+ let currentLine = '' ;
626+
627+ for ( const word of words ) {
628+ const prospectiveLine = currentLine ? `${ currentLine } ${ word } ` : word ;
629+ const prospectiveWidth = stringWidth ( prospectiveLine ) ;
630+
631+ if ( prospectiveWidth > inputWidth ) {
632+ if ( currentLine ) {
633+ additionalLines . push ( currentLine ) ;
634+ }
635+
636+ let wordToProcess = word ;
637+ while ( stringWidth ( wordToProcess ) > inputWidth ) {
638+ let part = '' ;
639+ const wordCP = toCodePoints ( wordToProcess ) ;
640+ let partWidth = 0 ;
641+ let splitIndex = 0 ;
642+ for ( let i = 0 ; i < wordCP . length ; i ++ ) {
643+ const char = wordCP [ i ] ;
644+ const charWidth = stringWidth ( char ) ;
645+ if ( partWidth + charWidth > inputWidth ) {
646+ break ;
647+ }
648+ part += char ;
649+ partWidth += charWidth ;
650+ splitIndex = i + 1 ;
651+ }
652+ additionalLines . push ( part ) ;
653+ wordToProcess = cpSlice ( wordToProcess , splitIndex ) ;
654+ }
655+ currentLine = wordToProcess ;
656+ } else {
657+ currentLine = prospectiveLine ;
658+ }
659+ }
660+ if ( currentLine ) {
661+ additionalLines . push ( currentLine ) ;
662+ }
663+ }
664+ }
665+
666+ return { inlineGhost, additionalLines } ;
667+ } , [
668+ completion . promptCompletion . text ,
669+ buffer . text ,
670+ buffer . lines ,
671+ buffer . cursor ,
672+ inputWidth ,
673+ ] ) ;
674+
675+ const { inlineGhost, additionalLines } = getGhostTextLines ( ) ;
676+
543677 return (
544678 < >
545679 < Box
@@ -573,42 +707,91 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
573707 < Text color = { theme . text . secondary } > { placeholder } </ Text >
574708 )
575709 ) : (
576- linesToRender . map ( ( lineText , visualIdxInRenderedSet ) => {
577- const cursorVisualRow = cursorVisualRowAbsolute - scrollVisualRow ;
578- let display = cpSlice ( lineText , 0 , inputWidth ) ;
579- const currentVisualWidth = stringWidth ( display ) ;
580- if ( currentVisualWidth < inputWidth ) {
581- display = display + ' ' . repeat ( inputWidth - currentVisualWidth ) ;
582- }
583-
584- if ( focus && visualIdxInRenderedSet === cursorVisualRow ) {
585- const relativeVisualColForHighlight = cursorVisualColAbsolute ;
586-
587- if ( relativeVisualColForHighlight >= 0 ) {
588- if ( relativeVisualColForHighlight < cpLen ( display ) ) {
589- const charToHighlight =
590- cpSlice (
591- display ,
592- relativeVisualColForHighlight ,
593- relativeVisualColForHighlight + 1 ,
594- ) || ' ' ;
595- const highlighted = chalk . inverse ( charToHighlight ) ;
596- display =
597- cpSlice ( display , 0 , relativeVisualColForHighlight ) +
598- highlighted +
599- cpSlice ( display , relativeVisualColForHighlight + 1 ) ;
600- } else if (
601- relativeVisualColForHighlight === cpLen ( display ) &&
602- cpLen ( display ) === inputWidth
603- ) {
604- display = display + chalk . inverse ( ' ' ) ;
710+ linesToRender
711+ . map ( ( lineText , visualIdxInRenderedSet ) => {
712+ const cursorVisualRow =
713+ cursorVisualRowAbsolute - scrollVisualRow ;
714+ let display = cpSlice ( lineText , 0 , inputWidth ) ;
715+
716+ const isOnCursorLine =
717+ focus && visualIdxInRenderedSet === cursorVisualRow ;
718+ const currentLineGhost = isOnCursorLine ? inlineGhost : '' ;
719+
720+ const ghostWidth = stringWidth ( currentLineGhost ) ;
721+
722+ if ( focus && visualIdxInRenderedSet === cursorVisualRow ) {
723+ const relativeVisualColForHighlight = cursorVisualColAbsolute ;
724+
725+ if ( relativeVisualColForHighlight >= 0 ) {
726+ if ( relativeVisualColForHighlight < cpLen ( display ) ) {
727+ const charToHighlight =
728+ cpSlice (
729+ display ,
730+ relativeVisualColForHighlight ,
731+ relativeVisualColForHighlight + 1 ,
732+ ) || ' ' ;
733+ const highlighted = chalk . inverse ( charToHighlight ) ;
734+ display =
735+ cpSlice ( display , 0 , relativeVisualColForHighlight ) +
736+ highlighted +
737+ cpSlice ( display , relativeVisualColForHighlight + 1 ) ;
738+ } else if (
739+ relativeVisualColForHighlight === cpLen ( display )
740+ ) {
741+ if ( ! currentLineGhost ) {
742+ display = display + chalk . inverse ( ' ' ) ;
743+ }
744+ }
605745 }
606746 }
607- }
608- return (
609- < Text key = { `line-${ visualIdxInRenderedSet } ` } > { display } </ Text >
610- ) ;
611- } )
747+
748+ const showCursorBeforeGhost =
749+ focus &&
750+ visualIdxInRenderedSet === cursorVisualRow &&
751+ cursorVisualColAbsolute ===
752+ // eslint-disable-next-line no-control-regex
753+ cpLen ( display . replace ( / \x1b \[ [ 0 - 9 ; ] * m / g, '' ) ) &&
754+ currentLineGhost ;
755+
756+ const actualDisplayWidth = stringWidth ( display ) ;
757+ const cursorWidth = showCursorBeforeGhost ? 1 : 0 ;
758+ const totalContentWidth =
759+ actualDisplayWidth + cursorWidth + ghostWidth ;
760+ const trailingPadding = Math . max (
761+ 0 ,
762+ inputWidth - totalContentWidth ,
763+ ) ;
764+
765+ return (
766+ < Text key = { `line-${ visualIdxInRenderedSet } ` } >
767+ { display }
768+ { showCursorBeforeGhost && chalk . inverse ( ' ' ) }
769+ { currentLineGhost && (
770+ < Text color = { theme . text . secondary } >
771+ { currentLineGhost }
772+ </ Text >
773+ ) }
774+ { trailingPadding > 0 && ' ' . repeat ( trailingPadding ) }
775+ </ Text >
776+ ) ;
777+ } )
778+ . concat (
779+ additionalLines . map ( ( ghostLine , index ) => {
780+ const padding = Math . max (
781+ 0 ,
782+ inputWidth - stringWidth ( ghostLine ) ,
783+ ) ;
784+ return (
785+ < Text
786+ key = { `ghost-line-${ index } ` }
787+ color = { theme . text . secondary }
788+ >
789+ { ghostLine }
790+ { ' ' . repeat ( padding ) }
791+ </ Text >
792+ ) ;
793+ } ) ,
794+ )
612795 ) }
613796 </ Box >
614797 </ Box >
0 commit comments