diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 6775a6c633..c675065808 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -11,7 +11,6 @@ import { EXPERIMENT_IDS, experiments as experimentsLib } from "@roo/experiments" import { vscode } from "@/utils/vscode" import { useExtensionState } from "@/context/ExtensionStateContext" import { useAppTranslation } from "@/i18n/TranslationContext" -import { checkSpelling, debounce, SpellCheckResult } from "@/utils/spellCheck" import { ContextMenuOptionType, getContextMenuOptions, @@ -183,8 +182,6 @@ const ChatTextArea = forwardRef( const contextMenuContainerRef = useRef(null) const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false) const [isFocused, setIsFocused] = useState(false) - const [spellCheckResults, setSpellCheckResults] = useState([]) - const spellCheckLayerRef = useRef(null) // Use custom hook for prompt history navigation const { handleHistoryNavigation, resetHistoryNavigation, resetOnInputChange } = usePromptHistory({ @@ -226,31 +223,6 @@ const ChatTextArea = forwardRef( return experiments && experimentsLib.isEnabled(experiments, EXPERIMENT_IDS.SPELL_CHECK) }, [experiments]) - // Debounced spell check function - const performSpellCheck = useMemo( - () => - debounce(async (text: string) => { - if (!isSpellCheckEnabled || !text.trim()) { - setSpellCheckResults([]) - return - } - - try { - const results = await checkSpelling(text) - setSpellCheckResults(results) - } catch (error) { - console.error("Spell check error:", error) - setSpellCheckResults([]) - } - }, 300), - [isSpellCheckEnabled], - ) - - // Perform spell check when input changes - useEffect(() => { - performSpellCheck(inputValue) - }, [inputValue, performSpellCheck]) - const allModes = useMemo(() => getAllModes(customModes), [customModes]) const queryItems = useMemo(() => { @@ -709,52 +681,6 @@ const ChatTextArea = forwardRef( } }, []) - const updateSpellCheckHighlights = useCallback(() => { - if (!spellCheckLayerRef.current || !isSpellCheckEnabled) return - - const text = inputValue - let html = "" - let lastIndex = 0 - - // Sort spell check results by start index - const sortedResults = [...spellCheckResults].sort((a, b) => a.startIndex - b.startIndex) - - sortedResults.forEach((result) => { - // Add text before the misspelled word - const beforeText = text.slice(lastIndex, result.startIndex) - html += beforeText - .replace(/\n$/, "\n\n") - .replace(/[<>&]/g, (c) => ({ "<": "<", ">": ">", "&": "&" })[c] || c) - - // Add the misspelled word with highlighting - const misspelledWord = text.slice(result.startIndex, result.endIndex) - html += `${misspelledWord.replace( - /[<>&]/g, - (c) => ({ "<": "<", ">": ">", "&": "&" })[c] || c, - )}` - - lastIndex = result.endIndex - }) - - // Add remaining text - const remainingText = text.slice(lastIndex) - html += remainingText - .replace(/\n$/, "\n\n") - .replace(/[<>&]/g, (c) => ({ "<": "<", ">": ">", "&": "&" })[c] || c) - - spellCheckLayerRef.current.innerHTML = html - - // Sync scroll position - if (textAreaRef.current) { - spellCheckLayerRef.current.scrollTop = textAreaRef.current.scrollTop - spellCheckLayerRef.current.scrollLeft = textAreaRef.current.scrollLeft - } - }, [inputValue, spellCheckResults, isSpellCheckEnabled]) - - useLayoutEffect(() => { - updateSpellCheckHighlights() - }, [inputValue, spellCheckResults, updateSpellCheckHighlights]) - const handleKeyUp = useCallback( (e: React.KeyboardEvent) => { if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(e.key)) { @@ -1125,30 +1051,6 @@ const ChatTextArea = forwardRef( color: "transparent", }} /> - {isSpellCheckEnabled && ( -
- )} { if (typeof ref === "function") { @@ -1181,6 +1083,7 @@ const ChatTextArea = forwardRef( minRows={3} maxRows={15} autoFocus={true} + spellCheck={isSpellCheckEnabled} className={cn( "w-full", "text-vscode-input-foreground", @@ -1213,7 +1116,6 @@ const ChatTextArea = forwardRef( )} onScroll={() => { updateHighlights() - updateSpellCheckHighlights() }} /> diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 640d46cd65..b6f812feff 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -651,7 +651,7 @@ }, "SPELL_CHECK": { "name": "Enable spell check in chat", - "description": "When enabled, misspelled words in the chat input will be underlined in red. This helps catch typos before sending messages to Roo." + "description": "When enabled, uses the browser's native spell check to underline misspelled words in the chat input. This helps catch typos before sending messages to Roo." } }, "promptCaching": { diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index 18bde66a96..fbb362ca8f 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -386,14 +386,6 @@ vscode-dropdown::part(listbox) { box-shadow: 0 0 0 0.5px color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent); } -/* Spell check styles */ -.spell-check-error { - text-decoration: underline; - text-decoration-color: #ff0000; - text-decoration-style: wavy; - text-underline-offset: 2px; -} - /** * vscrui Overrides / Hacks */ diff --git a/webview-ui/src/utils/__tests__/spellCheck.spec.ts b/webview-ui/src/utils/__tests__/spellCheck.spec.ts deleted file mode 100644 index d6af606990..0000000000 --- a/webview-ui/src/utils/__tests__/spellCheck.spec.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" -import { checkSpelling, debounce, isSpellCheckSupported } from "../spellCheck" - -describe("spellCheck", () => { - describe("isSpellCheckSupported", () => { - it("should return true when spellcheck property exists", () => { - // Mock document.createElement - const mockElement = { spellcheck: true } - vi.spyOn(document, "createElement").mockReturnValue(mockElement as any) - - expect(isSpellCheckSupported()).toBe(true) - }) - - it("should return false when spellcheck property does not exist", () => { - // Mock document.createElement - const mockElement = {} - vi.spyOn(document, "createElement").mockReturnValue(mockElement as any) - - expect(isSpellCheckSupported()).toBe(false) - }) - }) - - describe("checkSpelling", () => { - it("should return empty array for empty text", async () => { - const results = await checkSpelling("") - expect(results).toEqual([]) - }) - - it("should return empty array for text with only common words", async () => { - const results = await checkSpelling("the quick brown fox jumps over the lazy dog") - expect(results).toEqual([]) - }) - - it("should detect misspelled words", async () => { - const results = await checkSpelling("This is a tset of speling") - expect(results).toHaveLength(2) - expect(results[0]).toEqual({ - word: "tset", - startIndex: 10, - endIndex: 14, - }) - expect(results[1]).toEqual({ - word: "speling", - startIndex: 18, - endIndex: 25, - }) - }) - - it("should ignore words with numbers", async () => { - const results = await checkSpelling("test123 abc456") - expect(results).toEqual([]) - }) - - it("should ignore words that are all uppercase", async () => { - const results = await checkSpelling("API URL HTTP") - expect(results).toEqual([]) - }) - - it("should ignore words starting with @ or /", async () => { - const results = await checkSpelling("@mention /command") - expect(results).toEqual([]) - }) - - it("should ignore very short words", async () => { - const results = await checkSpelling("a I to") - expect(results).toEqual([]) - }) - - it("should handle contractions correctly", async () => { - const results = await checkSpelling("don't can't won't") - expect(results).toEqual([]) - }) - - it("should detect misspelled words in a longer text", async () => { - const text = "The quick brown fox jumps over the lazy dog. This sentense has a mispelling." - const results = await checkSpelling(text) - expect(results).toHaveLength(2) - expect(results[0].word).toBe("sentense") - expect(results[1].word).toBe("mispelling") - }) - - it("should handle technical terms correctly", async () => { - const results = await checkSpelling("function variable const class method async await promise") - expect(results).toEqual([]) - }) - - it("should handle Roo-specific terms correctly", async () => { - const results = await checkSpelling("roo chat message task workspace api token context prompt") - expect(results).toEqual([]) - }) - }) - - describe("debounce", () => { - beforeEach(() => { - vi.useFakeTimers() - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - it("should debounce function calls", () => { - const mockFn = vi.fn() - const debouncedFn = debounce(mockFn, 100) - - // Call multiple times quickly - debouncedFn("arg1") - debouncedFn("arg2") - debouncedFn("arg3") - - // Function should not have been called yet - expect(mockFn).not.toHaveBeenCalled() - - // Fast forward time - vi.advanceTimersByTime(100) - - // Function should have been called once with the last arguments - expect(mockFn).toHaveBeenCalledTimes(1) - expect(mockFn).toHaveBeenCalledWith("arg3") - }) - - it("should reset timer on each call", () => { - const mockFn = vi.fn() - const debouncedFn = debounce(mockFn, 100) - - debouncedFn("arg1") - vi.advanceTimersByTime(50) - - debouncedFn("arg2") - vi.advanceTimersByTime(50) - - // Function should not have been called yet - expect(mockFn).not.toHaveBeenCalled() - - vi.advanceTimersByTime(50) - - // Function should have been called once with the last arguments - expect(mockFn).toHaveBeenCalledTimes(1) - expect(mockFn).toHaveBeenCalledWith("arg2") - }) - }) -}) diff --git a/webview-ui/src/utils/spellCheck.ts b/webview-ui/src/utils/spellCheck.ts deleted file mode 100644 index 421344b2e2..0000000000 --- a/webview-ui/src/utils/spellCheck.ts +++ /dev/null @@ -1,431 +0,0 @@ -/** - * Simple spell check utility for detecting misspelled words - * Uses the browser's built-in spell check API when available - */ - -export interface SpellCheckResult { - word: string - startIndex: number - endIndex: number - suggestions?: string[] -} - -/** - * Check if the browser supports the native spell check API - */ -export const isSpellCheckSupported = (): boolean => { - // Check if we're in a browser environment and if the spell check API is available - return typeof window !== "undefined" && "spellcheck" in document.createElement("textarea") -} - -/** - * Common English words that should not be marked as misspelled - * This is a basic dictionary for fallback when native spell check is not available - */ -const commonWords = new Set([ - // Common words - "the", - "be", - "to", - "of", - "and", - "a", - "in", - "that", - "have", - "i", - "it", - "for", - "not", - "on", - "with", - "he", - "as", - "you", - "do", - "at", - "this", - "but", - "his", - "by", - "from", - "they", - "we", - "say", - "her", - "she", - "or", - "an", - "will", - "my", - "one", - "all", - "would", - "there", - "their", - "what", - "so", - "up", - "out", - "if", - "about", - "who", - "get", - "which", - "go", - "me", - "when", - "make", - "can", - "like", - "time", - "no", - "just", - "him", - "know", - "take", - "people", - "into", - "year", - "your", - "good", - "some", - "could", - "them", - "see", - "other", - "than", - "then", - "now", - "look", - "only", - "come", - "its", - "over", - "think", - "also", - "back", - "after", - "use", - "two", - "how", - "our", - "work", - "first", - "well", - "way", - "even", - "new", - "want", - "because", - "any", - "these", - "give", - "day", - "most", - "us", - "is", - "was", - "are", - "been", - "has", - "had", - "were", - "said", - "did", - "getting", - "made", - "find", - "where", - "much", - "too", - "very", - "still", - "being", - "going", - "why", - "before", - "never", - "here", - "more", - "always", - "those", - "tell", - "really", - "something", - "nothing", - "everything", - "anything", - // Additional common words - "quick", - "brown", - "fox", - "jumps", - "lazy", - "dog", - "sentence", - "misspelling", - // Contractions - "don't", - "can't", - "won't", - "isn't", - "aren't", - "wasn't", - "weren't", - "hasn't", - "haven't", - "hadn't", - "doesn't", - "didn't", - "wouldn't", - "shouldn't", - "couldn't", - "mightn't", - "mustn't", - "would've", - "should've", - "could've", - "might've", - "must've", - "i'll", - "you'll", - "he'll", - "she'll", - "we'll", - "they'll", - "i'd", - "you'd", - "he'd", - "she'd", - "we'd", - "they'd", - "i've", - "you've", - "we've", - "they've", - "i'm", - "you're", - "he's", - "she's", - "it's", - "we're", - "they're", - "let's", - "that's", - "who's", - "what's", - "where's", - "when's", - "why's", - "how's", - "here's", - "there's", - // Tech-related words - "code", - "function", - "variable", - "const", - "let", - "var", - "class", - "method", - "property", - "object", - "array", - "string", - "number", - "boolean", - "null", - "undefined", - "true", - "false", - "if", - "else", - "for", - "while", - "do", - "switch", - "case", - "break", - "continue", - "return", - "try", - "catch", - "finally", - "throw", - "new", - "this", - "super", - "extends", - "implements", - "interface", - "enum", - "type", - "namespace", - "module", - "import", - "export", - "default", - "from", - "as", - "async", - "await", - "promise", - "then", - "catch", - "finally", - "callback", - "error", - "debug", - "console", - "log", - "warn", - "info", - "trace", - "assert", - "clear", - "count", - "group", - "time", - "profile", - // Roo-specific words - "roo", - "chat", - "message", - "task", - "file", - "directory", - "workspace", - "project", - "api", - "model", - "token", - "context", - "prompt", - "response", - "request", - "approve", - "reject", - "execute", - "command", - "terminal", - "browser", - "search", - "replace", - "edit", - "create", - "delete", - "read", - "write", - "list", - "view", - "open", - "close", - "save", - "load", - "refresh", - "update", - "install", - "uninstall", - "build", - "test", - "run", - "start", - "stop", - "restart", - "deploy", - "commit", - "push", - "pull", - "merge", - "branch", - "checkout", - "clone", - "fork", -]) - -/** - * Check if a word is likely misspelled using a basic dictionary - * This is a fallback for when native spell check is not available - */ -const isLikelyMisspelled = (word: string): boolean => { - // Ignore very short words - if (word.length <= 2) return false - - // Ignore words with numbers - if (/\d/.test(word)) return false - - // Ignore words that are all uppercase (likely acronyms) - if (word === word.toUpperCase()) return false - - // Don't check words that start with @ or / (mentions and commands) - // Extract the actual word without the prefix for checking - const wordToCheck = word.startsWith("@") || word.startsWith("/") ? word.substring(1) : word - - // Check against common words dictionary - return !commonWords.has(wordToCheck.toLowerCase()) -} - -/** - * Extract words from text with their positions - */ -const extractWords = (text: string): Array<{ word: string; start: number; end: number }> => { - const words: Array<{ word: string; start: number; end: number }> = [] - // Match words (including contractions like "don't", "it's") but exclude @ and / mentions - // This regex will not match words that are part of @mentions or /commands - const wordRegex = /(? => { - const results: SpellCheckResult[] = [] - const words = extractWords(text) - - // For now, use the basic dictionary check - // In the future, this could be enhanced with a proper spell check API - for (const { word, start, end } of words) { - if (isLikelyMisspelled(word)) { - results.push({ - word, - startIndex: start, - endIndex: end, - }) - } - } - - return results -} - -/** - * Debounce function to limit spell check frequency - */ -export const debounce = any>( - func: T, - wait: number, -): ((...args: Parameters) => void) => { - let timeout: NodeJS.Timeout | null = null - - return (...args: Parameters) => { - if (timeout) clearTimeout(timeout) - timeout = setTimeout(() => func(...args), wait) - } -}