Skip to content

Commit 6e4f195

Browse files
committed
refactor: Improve cursor positioning with useLayoutEffect
- Replace setTimeout(..., 0) with useLayoutEffect for more reliable cursor positioning - Implement state-based cursor positioning pattern suggested by @mochiya98 - Add CursorPositionState interface for better type safety - Maintain all existing functionality while improving timing reliability This addresses the technical suggestion in PR #4450 comment about using useLayoutEffect instead of setTimeout for DOM manipulation timing.
1 parent c38c43b commit 6e4f195

File tree

1 file changed

+39
-27
lines changed

1 file changed

+39
-27
lines changed

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ interface ChatTextAreaProps {
4545
modeShortcutText: string
4646
}
4747

48+
interface CursorPositionState {
49+
value: string
50+
afterRender?: "SET_CURSOR_FIRST_LINE" | "SET_CURSOR_LAST_LINE" | "SET_CURSOR_START"
51+
}
52+
4853
const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
4954
(
5055
{
@@ -158,6 +163,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
158163
const [historyIndex, setHistoryIndex] = useState(-1)
159164
const [tempInput, setTempInput] = useState("")
160165
const [promptHistory, setPromptHistory] = useState<string[]>([])
166+
const [inputValueWithCursor, setInputValueWithCursor] = useState<CursorPositionState>({ value: inputValue })
161167

162168
// Initialize prompt history from task history
163169
useEffect(() => {
@@ -411,17 +417,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
411417
setHistoryIndex(newIndex)
412418
const historicalPrompt = promptHistory[newIndex]
413419
setInputValue(historicalPrompt)
414-
415-
// Set cursor to end of first line
416-
setTimeout(() => {
417-
if (textAreaRef.current) {
418-
const firstLineEnd =
419-
historicalPrompt.indexOf("\n") === -1
420-
? historicalPrompt.length
421-
: historicalPrompt.indexOf("\n")
422-
textAreaRef.current.setSelectionRange(firstLineEnd, firstLineEnd)
423-
}
424-
}, 0)
420+
setInputValueWithCursor({
421+
value: historicalPrompt,
422+
afterRender: "SET_CURSOR_FIRST_LINE",
423+
})
425424
}
426425
return
427426
}
@@ -435,26 +434,18 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
435434
setHistoryIndex(newIndex)
436435
const historicalPrompt = promptHistory[newIndex]
437436
setInputValue(historicalPrompt)
438-
439-
// Set cursor to start of last line
440-
setTimeout(() => {
441-
if (textAreaRef.current) {
442-
const lines = historicalPrompt.split("\n")
443-
const lastLineStart = historicalPrompt.length - lines[lines.length - 1].length
444-
textAreaRef.current.setSelectionRange(lastLineStart, lastLineStart)
445-
}
446-
}, 0)
437+
setInputValueWithCursor({
438+
value: historicalPrompt,
439+
afterRender: "SET_CURSOR_LAST_LINE",
440+
})
447441
} else if (historyIndex === 0) {
448442
// Return to current input
449443
setHistoryIndex(-1)
450444
setInputValue(tempInput)
451-
452-
// Set cursor to start
453-
setTimeout(() => {
454-
if (textAreaRef.current) {
455-
textAreaRef.current.setSelectionRange(0, 0)
456-
}
457-
}, 0)
445+
setInputValueWithCursor({
446+
value: tempInput,
447+
afterRender: "SET_CURSOR_START",
448+
})
458449
}
459450
return
460451
}
@@ -544,6 +535,27 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
544535
}
545536
}, [inputValue, intendedCursorPosition])
546537

538+
// Handle cursor positioning after history navigation
539+
useLayoutEffect(() => {
540+
if (!inputValueWithCursor.afterRender || !textAreaRef.current) return
541+
542+
if (inputValueWithCursor.afterRender === "SET_CURSOR_FIRST_LINE") {
543+
const firstLineEnd =
544+
inputValueWithCursor.value.indexOf("\n") === -1
545+
? inputValueWithCursor.value.length
546+
: inputValueWithCursor.value.indexOf("\n")
547+
textAreaRef.current.setSelectionRange(firstLineEnd, firstLineEnd)
548+
} else if (inputValueWithCursor.afterRender === "SET_CURSOR_LAST_LINE") {
549+
const lines = inputValueWithCursor.value.split("\n")
550+
const lastLineStart = inputValueWithCursor.value.length - lines[lines.length - 1].length
551+
textAreaRef.current.setSelectionRange(lastLineStart, lastLineStart)
552+
} else if (inputValueWithCursor.afterRender === "SET_CURSOR_START") {
553+
textAreaRef.current.setSelectionRange(0, 0)
554+
}
555+
556+
setInputValueWithCursor({ value: inputValueWithCursor.value })
557+
}, [inputValueWithCursor])
558+
547559
// Ref to store the search timeout.
548560
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null)
549561

0 commit comments

Comments
 (0)