@@ -5,6 +5,7 @@ import { Icon } from '../../common/Icon'
55import { get_highlighted_text } from './utils/get-highlighted-text'
66import { use_handlers } from './hooks/use-handlers'
77import { use_dropdown } from './hooks/use-dropdown'
8+ import { use_ghost_text } from './hooks/use-ghost-text'
89import { use_drag_drop } from './hooks/use-drag-drop'
910import { DropdownMenu } from '../../common/DropdownMenu'
1011import { use_is_narrow_viewport , use_is_mac } from '@shared/hooks'
@@ -77,12 +78,7 @@ export type PromptFieldProps = {
7778
7879export const PromptField : React . FC < PromptFieldProps > = ( props ) => {
7980 const input_ref = useRef < HTMLDivElement > ( null )
80- const container_ref = useRef < HTMLDivElement > ( null )
81- const ghost_text_debounce_timer_ref = useRef < ReturnType <
82- typeof setTimeout
83- > | null > ( null )
8481 const [ caret_position , set_caret_position ] = useState ( 0 )
85- const [ ghost_text , set_ghost_text ] = useState ( '' )
8682 const [ show_at_sign_tooltip , set_show_at_sign_tooltip ] = useState ( false )
8783 const [ show_submit_tooltip , set_show_submit_tooltip ] = useState ( false )
8884 const [ is_text_selecting , set_is_text_selecting ] = useState ( false )
@@ -95,6 +91,13 @@ export const PromptField: React.FC<PromptFieldProps> = (props) => {
9591 handle_copy_click,
9692 handle_select_click
9793 } = use_dropdown ( props )
94+ const { ghost_text, handle_accept_ghost_text } = use_ghost_text ( {
95+ value : props . value ,
96+ input_ref,
97+ is_focused,
98+ currently_open_file_text : props . currently_open_file_text ,
99+ caret_position
100+ } )
98101 const {
99102 handle_clear,
100103 handle_input_change,
@@ -114,31 +117,6 @@ export const PromptField: React.FC<PromptFieldProps> = (props) => {
114117
115118 const mouse_down_pos_ref = useRef < { x : number ; y : number } | null > ( null )
116119
117- function handle_accept_ghost_text ( ) {
118- if ( ! ghost_text || ! input_ref . current ) return
119-
120- const ghost_node = input_ref . current . querySelector (
121- 'span[data-type="ghost-text"]'
122- )
123- if ( ghost_node ) {
124- const text_node = document . createTextNode ( ghost_node . textContent || '' )
125- ghost_node . parentNode ?. replaceChild ( text_node , ghost_node )
126-
127- const selection = window . getSelection ( )
128- if ( selection ) {
129- const range = document . createRange ( )
130- range . setStartAfter ( text_node )
131- range . collapse ( true )
132- selection . removeAllRanges ( )
133- selection . addRange ( range )
134- }
135-
136- input_ref . current . dispatchEvent (
137- new Event ( 'input' , { bubbles : true , cancelable : true } )
138- )
139- }
140- }
141-
142120 const handle_mouse_down = useCallback (
143121 ( e : React . MouseEvent < HTMLDivElement > ) => {
144122 mouse_down_pos_ref . current = { x : e . clientX , y : e . clientY }
@@ -228,86 +206,6 @@ export const PromptField: React.FC<PromptFieldProps> = (props) => {
228206 props . context_file_paths
229207 ] )
230208
231- const identifiers = useMemo ( ( ) => {
232- if ( ! props . currently_open_file_text ) return new Set < string > ( )
233- const matches =
234- props . currently_open_file_text . match ( / [ a - z A - Z _ ] [ a - z A - Z 0 - 9 _ ] * / g)
235- if ( ! matches ) return new Set < string > ( )
236- return new Set ( matches . filter ( ( id ) => id . length >= 3 ) )
237- } , [ props . currently_open_file_text ] )
238-
239- useEffect ( ( ) => {
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
244- }
245-
246- // Calculate potential ghost text
247- let potential_ghost_text = ''
248-
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- }
289- }
290-
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 {
301- set_ghost_text ( '' )
302- }
303-
304- return ( ) => {
305- if ( ghost_text_debounce_timer_ref . current ) {
306- clearTimeout ( ghost_text_debounce_timer_ref . current )
307- }
308- }
309- } , [ props . value , caret_position , identifiers , is_focused , ghost_text ] )
310-
311209 useEffect ( ( ) => {
312210 if ( input_ref . current && input_ref . current . innerHTML !== highlighted_html ) {
313211 const selection_start = get_caret_position_from_div ( input_ref . current )
@@ -383,37 +281,6 @@ export const PromptField: React.FC<PromptFieldProps> = (props) => {
383281 document . removeEventListener ( 'selectionchange' , on_selection_change )
384282 } , [ props . on_caret_position_change , props . value , props . context_file_paths ] )
385283
386- useEffect ( ( ) => {
387- const input = input_ref . current
388- if ( ! input ) return
389-
390- const existing_ghost = input . querySelector ( 'span[data-type="ghost-text"]' )
391- if ( existing_ghost ) {
392- const parent = existing_ghost . parentNode
393- if ( parent ) {
394- parent . removeChild ( existing_ghost )
395- parent . normalize ( )
396- }
397- }
398-
399- if ( ghost_text ) {
400- const selection = window . getSelection ( )
401- if ( selection && selection . rangeCount > 0 ) {
402- const range = selection . getRangeAt ( 0 )
403- if ( range . collapsed ) {
404- const span = document . createElement ( 'span' )
405- span . dataset . type = 'ghost-text'
406- span . textContent = ghost_text
407- span . style . color = 'var(--vscode-editorGhostText-foreground, #888)'
408- span . style . pointerEvents = 'none'
409-
410- range . insertNode ( span )
411- selection . collapse ( range . startContainer , range . startOffset )
412- }
413- }
414- }
415- } , [ ghost_text ] )
416-
417284 const placeholder = useMemo ( ( ) => {
418285 const active_history = props . chat_history
419286
@@ -475,7 +342,6 @@ export const PromptField: React.FC<PromptFieldProps> = (props) => {
475342 }
476343 }
477344 } }
478- ref = { container_ref }
479345 onClick = { ( ) => input_ref . current ?. focus ( ) }
480346 >
481347 { props . value && (
0 commit comments