@@ -78,6 +78,9 @@ export type PromptFieldProps = {
7878export const PromptField : React . FC < PromptFieldProps > = ( props ) => {
7979 const input_ref = useRef < HTMLDivElement > ( null )
8080 const container_ref = useRef < HTMLDivElement > ( null )
81+ const ghost_text_debounce_timer_ref = useRef < ReturnType <
82+ typeof setTimeout
83+ > | null > ( null )
8184 const [ caret_position , set_caret_position ] = useState ( 0 )
8285 const [ ghost_text , set_ghost_text ] = useState ( '' )
8386 const [ show_at_sign_tooltip , set_show_at_sign_tooltip ] = useState ( false )
@@ -234,68 +237,76 @@ export const PromptField: React.FC<PromptFieldProps> = (props) => {
234237 } , [ props . currently_open_file_text ] )
235238
236239 useEffect ( ( ) => {
237- if ( ! input_ref . current || ! is_focused ) {
238- set_ghost_text ( '' )
239- return
240- }
241-
242- const selection = window . getSelection ( )
243- if ( ! selection || selection . rangeCount === 0 ) {
244- set_ghost_text ( '' )
245- return
246- }
247-
248- const range = selection . getRangeAt ( 0 )
249- if ( ! range . collapsed ) {
250- set_ghost_text ( '' )
251- return
240+ // Reset timer on any change
241+ if ( ghost_text_debounce_timer_ref . current ) {
242+ clearTimeout ( ghost_text_debounce_timer_ref . current )
243+ ghost_text_debounce_timer_ref . current = null
252244 }
253245
254- // Check if there's a character on the right side of the caret
255- const post_caret_range = range . cloneRange ( )
256- post_caret_range . selectNodeContents ( input_ref . current )
257- post_caret_range . setStart ( range . endContainer , range . endOffset )
258- const text_after_cursor = post_caret_range . toString ( )
259-
260- // Only allow ghost text if there's nothing after cursor or only whitespace
261- if ( text_after_cursor . length > 0 && text_after_cursor [ 0 ] . trim ( ) !== '' ) {
262- set_ghost_text ( '' )
263- return
264- }
246+ // Calculate potential ghost text
247+ let potential_ghost_text = ''
265248
266- const pre_caret_range = range . cloneRange ( )
267- pre_caret_range . selectNodeContents ( input_ref . current )
268- pre_caret_range . setEnd ( range . startContainer , range . startOffset )
269- const text_before_cursor = pre_caret_range . toString ( )
270-
271- const last_word_match = text_before_cursor . match ( / [ \S ] + $ / )
272- if ( ! last_word_match ) {
273- set_ghost_text ( '' )
274- return
275- }
276-
277- const last_word_with_prefix = last_word_match [ 0 ]
278- const word_start_match = last_word_with_prefix . match ( / [ a - z A - Z _ ] / )
279- if ( ! word_start_match ) {
280- set_ghost_text ( '' )
281- return
249+ if ( input_ref . current && is_focused ) {
250+ const selection = window . getSelection ( )
251+ if ( selection && selection . rangeCount > 0 ) {
252+ const range = selection . getRangeAt ( 0 )
253+ if ( range . collapsed ) {
254+ // Check if there's a character on the right side of the caret
255+ const post_caret_range = range . cloneRange ( )
256+ post_caret_range . selectNodeContents ( input_ref . current )
257+ post_caret_range . setStart ( range . endContainer , range . endOffset )
258+ const text_after_cursor = post_caret_range . toString ( )
259+
260+ // Only allow ghost text if there's nothing after cursor or only whitespace
261+ if ( text_after_cursor . trim ( ) === '' ) {
262+ const pre_caret_range = range . cloneRange ( )
263+ pre_caret_range . selectNodeContents ( input_ref . current )
264+ pre_caret_range . setEnd ( range . startContainer , range . startOffset )
265+ const text_before_cursor = pre_caret_range . toString ( )
266+ const last_word_match = text_before_cursor . match ( / [ \S ] + $ / )
267+
268+ if ( last_word_match ) {
269+ const last_word_with_prefix = last_word_match [ 0 ]
270+ const word_start_match =
271+ last_word_with_prefix . match ( / [ a - z A - Z _ ] / )
272+ if ( word_start_match ) {
273+ const last_word = last_word_with_prefix . substring (
274+ word_start_match . index ?? 0
275+ )
276+ if ( last_word . length >= 2 ) {
277+ for ( const id of identifiers ) {
278+ if ( id . startsWith ( last_word ) && id !== last_word ) {
279+ potential_ghost_text = id . substring ( last_word . length )
280+ break
281+ }
282+ }
283+ }
284+ }
285+ }
286+ }
287+ }
288+ }
282289 }
283- const last_word = last_word_with_prefix . substring ( word_start_match . index ?? 0 )
284290
285- if ( last_word . length < 2 ) {
291+ // Apply debounce logic
292+ if ( potential_ghost_text ) {
293+ if ( ghost_text ) {
294+ set_ghost_text ( potential_ghost_text )
295+ } else {
296+ ghost_text_debounce_timer_ref . current = setTimeout ( ( ) => {
297+ set_ghost_text ( potential_ghost_text )
298+ } , 500 )
299+ }
300+ } else {
286301 set_ghost_text ( '' )
287- return
288302 }
289303
290- for ( const id of identifiers ) {
291- if ( id . startsWith ( last_word ) && id !== last_word ) {
292- set_ghost_text ( id . substring ( last_word . length ) )
293- return
304+ return ( ) => {
305+ if ( ghost_text_debounce_timer_ref . current ) {
306+ clearTimeout ( ghost_text_debounce_timer_ref . current )
294307 }
295308 }
296-
297- set_ghost_text ( '' )
298- } , [ props . value , caret_position , identifiers , is_focused ] )
309+ } , [ props . value , caret_position , identifiers , is_focused , ghost_text ] )
299310
300311 useEffect ( ( ) => {
301312 if ( input_ref . current && input_ref . current . innerHTML !== highlighted_html ) {
0 commit comments