Auto-check after every word; disable browser spellcheck

- spellCheck={false} on EditorContent — LanguageTool's wavy marks
  take over entirely, no double-squiggle confusion
- onUpdate schedules runLint() 1.5 s after the last keystroke via
  lintDebounce ref; typing resets the timer so it only fires when
  the writer pauses (naturally after finishing a word or sentence)
- runLintRef kept in sync with useEffect so the stale onUpdate
  closure always calls the latest runLint
- Timer cleaned up on unmount

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chris 2026-05-24 21:48:47 -04:00
parent bdafd3f2c3
commit 91071270a4

View File

@ -110,7 +110,9 @@ const Editor = forwardRef(function Editor(
const [lintStatus, setLintStatus] = useState('idle') // idle | checking | done | stale const [lintStatus, setLintStatus] = useState('idle') // idle | checking | done | stale
const [lintCount, setLintCount] = useState(0) const [lintCount, setLintCount] = useState(0)
const [lintPopover, setLintPopover] = useState(null) // { top, left, message, replacements, from, to } const [lintPopover, setLintPopover] = useState(null) // { top, left, message, replacements, from, to }
const lintStatusRef = useRef('idle') const lintStatusRef = useRef('idle')
const lintDebounce = useRef(null)
const runLintRef = useRef(null) // always holds latest runLint, safe to call from onUpdate closure
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [
@ -126,11 +128,9 @@ const Editor = forwardRef(function Editor(
content: '', content: '',
onUpdate({ editor }) { onUpdate({ editor }) {
onChange(stripLintMarks(editor.getJSON())) onChange(stripLintMarks(editor.getJSON()))
// Mark results as stale when the user edits so they know to re-check // Debounced auto-check: fire 1.5 s after the last keystroke
if (lintStatusRef.current === 'done') { clearTimeout(lintDebounce.current)
lintStatusRef.current = 'stale' lintDebounce.current = setTimeout(() => runLintRef.current?.(), 1500)
setLintStatus('stale')
}
}, },
}) })
@ -153,6 +153,11 @@ const Editor = forwardRef(function Editor(
}, [editor, content]) }, [editor, content])
// Lint check // Lint check
// Keep ref in sync so the onUpdate closure (which never re-captures) always
// calls the latest version of runLint.
useEffect(() => { runLintRef.current = runLint }, [runLint])
useEffect(() => () => clearTimeout(lintDebounce.current), [])
const runLint = useCallback(async () => { const runLint = useCallback(async () => {
if (!editor || lintStatusRef.current === 'checking') return if (!editor || lintStatusRef.current === 'checking') return
lintStatusRef.current = 'checking' lintStatusRef.current = 'checking'
@ -265,7 +270,7 @@ const Editor = forwardRef(function Editor(
lintCount={lintCount} lintCount={lintCount}
/> />
<div className="editor-wrap" onClick={handleEditorClick}> <div className="editor-wrap" onClick={handleEditorClick}>
<EditorContent editor={editor} className="editor-body" /> <EditorContent editor={editor} className="editor-body" spellCheck={false} />
<div className="word-count">{wordCount.toLocaleString()} {wordCount === 1 ? 'word' : 'words'}</div> <div className="word-count">{wordCount.toLocaleString()} {wordCount === 1 ? 'word' : 'words'}</div>
</div> </div>