Skip to content

Commit bb4ff64

Browse files
committed
Refactor ghost text calculation logic to use debouncing for improved performance and responsiveness
1 parent f5907d8 commit bb4ff64

1 file changed

Lines changed: 63 additions & 52 deletions

File tree

‎packages/ui/src/components/editor/panel/PromptField/PromptField.tsx‎

Lines changed: 63 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ export type PromptFieldProps = {
7878
export 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-zA-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-zA-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

Comments
 (0)