@@ -22,6 +22,7 @@ import {
2222import { cn } from "@src/lib/utils"
2323import { convertToMentionPath } from "@src/utils/path-mentions"
2424import { StandardTooltip } from "@src/components/ui"
25+ import { useSpellCheck , SpellCheckResult } from "@src/hooks/useSpellCheck"
2526
2627import Thumbnails from "../common/Thumbnails"
2728import { ModeSelector } from "./ModeSelector"
@@ -32,6 +33,7 @@ import ContextMenu from "./ContextMenu"
3233import { IndexingStatusBadge } from "./IndexingStatusBadge"
3334import { usePromptHistory } from "./hooks/usePromptHistory"
3435import { CloudAccountSwitcher } from "../cloud/CloudAccountSwitcher"
36+ import { SpellCheckSuggestions } from "./SpellCheckSuggestions"
3537
3638interface ChatTextAreaProps {
3739 inputValue : string
@@ -215,6 +217,18 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
215217 const [ isEnhancingPrompt , setIsEnhancingPrompt ] = useState ( false )
216218 const [ isFocused , setIsFocused ] = useState ( false )
217219
220+ // Spell check state
221+ const {
222+ misspelledWords,
223+ checkSpelling,
224+ isSupported : isSpellCheckSupported ,
225+ } = useSpellCheck ( {
226+ enabled : true ,
227+ debounceMs : 500 ,
228+ } )
229+ const [ selectedMisspelling , setSelectedMisspelling ] = useState < SpellCheckResult | null > ( null )
230+ const [ spellCheckMenuPosition , setSpellCheckMenuPosition ] = useState < { x : number ; y : number } | null > ( null )
231+
218232 // Use custom hook for prompt history navigation
219233 const { handleHistoryNavigation, resetHistoryNavigation, resetOnInputChange } = usePromptHistory ( {
220234 clineMessages,
@@ -558,6 +572,11 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
558572 // Reset history navigation when user types
559573 resetOnInputChange ( )
560574
575+ // Check spelling when text changes
576+ if ( isSpellCheckSupported ) {
577+ checkSpelling ( newValue )
578+ }
579+
561580 const newCursorPosition = e . target . selectionStart
562581 setCursorPosition ( newCursorPosition )
563582
@@ -615,7 +634,15 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
615634 setFileSearchResults ( [ ] ) // Clear file search results.
616635 }
617636 } ,
618- [ setInputValue , setSearchRequestId , setFileSearchResults , setSearchLoading , resetOnInputChange ] ,
637+ [
638+ setInputValue ,
639+ setSearchRequestId ,
640+ setFileSearchResults ,
641+ setSearchLoading ,
642+ resetOnInputChange ,
643+ isSpellCheckSupported ,
644+ checkSpelling ,
645+ ] ,
619646 )
620647
621648 useEffect ( ( ) => {
@@ -750,15 +777,35 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
750777 return match // Return unhighlighted if command is not valid
751778 } )
752779
780+ // Add spell check highlights
781+ if ( isSpellCheckSupported && misspelledWords . length > 0 ) {
782+ // Sort misspellings by position (reverse order to maintain positions)
783+ const sortedMisspellings = [ ...misspelledWords ] . sort ( ( a , b ) => b . start - a . start )
784+
785+ sortedMisspellings . forEach ( ( misspelling ) => {
786+ const before = processedText . slice ( 0 , misspelling . start )
787+ const word = processedText . slice ( misspelling . start , misspelling . end )
788+ const after = processedText . slice ( misspelling . end )
789+
790+ // Only add spell check highlight if the word isn't already highlighted
791+ if ( ! word . includes ( "<mark" ) ) {
792+ processedText =
793+ before +
794+ `<span class="spell-check-underline" data-word="${ misspelling . word } " data-start="${ misspelling . start } " data-end="${ misspelling . end } ">${ word } </span>` +
795+ after
796+ }
797+ } )
798+ }
799+
753800 highlightLayerRef . current . innerHTML = processedText
754801
755802 highlightLayerRef . current . scrollTop = textAreaRef . current . scrollTop
756803 highlightLayerRef . current . scrollLeft = textAreaRef . current . scrollLeft
757- } , [ commands ] )
804+ } , [ commands , isSpellCheckSupported , misspelledWords ] )
758805
759806 useLayoutEffect ( ( ) => {
760807 updateHighlights ( )
761- } , [ inputValue , updateHighlights ] )
808+ } , [ inputValue , updateHighlights , misspelledWords ] )
762809
763810 const updateCursorPosition = useCallback ( ( ) => {
764811 if ( textAreaRef . current ) {
@@ -903,6 +950,63 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
903950 [ setMode ] ,
904951 )
905952
953+ // Handle spell check word click
954+ const handleSpellCheckClick = useCallback (
955+ ( event : React . MouseEvent ) => {
956+ const target = event . target as HTMLElement
957+ if ( target . classList . contains ( "spell-check-underline" ) ) {
958+ const word = target . getAttribute ( "data-word" )
959+ const start = parseInt ( target . getAttribute ( "data-start" ) || "0" , 10 )
960+ const end = parseInt ( target . getAttribute ( "data-end" ) || "0" , 10 )
961+
962+ const misspelling = misspelledWords . find (
963+ ( m ) => m . word === word && m . start === start && m . end === end ,
964+ )
965+
966+ if ( misspelling ) {
967+ const rect = target . getBoundingClientRect ( )
968+ setSelectedMisspelling ( misspelling )
969+ setSpellCheckMenuPosition ( {
970+ x : rect . left ,
971+ y : rect . bottom + 5 ,
972+ } )
973+ }
974+ }
975+ } ,
976+ [ misspelledWords ] ,
977+ )
978+
979+ // Handle spell check suggestion selection
980+ const handleSpellCheckSuggestionSelect = useCallback (
981+ ( suggestion : string ) => {
982+ if ( selectedMisspelling && textAreaRef . current ) {
983+ const currentValue = inputValue
984+ const before = currentValue . slice ( 0 , selectedMisspelling . start )
985+ const after = currentValue . slice ( selectedMisspelling . end )
986+ const newValue = before + suggestion + after
987+
988+ setInputValue ( newValue )
989+ setSelectedMisspelling ( null )
990+ setSpellCheckMenuPosition ( null )
991+
992+ // Update cursor position
993+ const newCursorPos = selectedMisspelling . start + suggestion . length
994+ setTimeout ( ( ) => {
995+ if ( textAreaRef . current ) {
996+ textAreaRef . current . focus ( )
997+ textAreaRef . current . setSelectionRange ( newCursorPos , newCursorPos )
998+ }
999+ } , 0 )
1000+
1001+ // Re-check spelling with the corrected text
1002+ if ( isSpellCheckSupported ) {
1003+ checkSpelling ( newValue )
1004+ }
1005+ }
1006+ } ,
1007+ [ selectedMisspelling , inputValue , setInputValue , isSpellCheckSupported , checkSpelling ] ,
1008+ )
1009+
9061010 // Helper function to handle API config change
9071011 const handleApiConfigChange = useCallback ( ( value : string ) => {
9081012 vscode . postMessage ( { type : "loadApiConfigurationById" , text : value } )
@@ -1011,6 +1115,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
10111115 style = { {
10121116 color : "transparent" ,
10131117 } }
1118+ onClick = { handleSpellCheckClick }
10141119 />
10151120 < DynamicTextArea
10161121 ref = { ( el ) => {
@@ -1268,6 +1373,17 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
12681373 ) }
12691374 </ div >
12701375 </ div >
1376+
1377+ { /* Spell check suggestions menu */ }
1378+ < SpellCheckSuggestions
1379+ misspelling = { selectedMisspelling }
1380+ position = { spellCheckMenuPosition }
1381+ onSelect = { handleSpellCheckSuggestionSelect }
1382+ onDismiss = { ( ) => {
1383+ setSelectedMisspelling ( null )
1384+ setSpellCheckMenuPosition ( null )
1385+ } }
1386+ />
12711387 </ div >
12721388 )
12731389 } ,
0 commit comments