mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-08 22:21:23 +00:00
feat: add spell check feature to Roo Chat
- Add experimental spell check feature flag - Implement spell check utility with basic dictionary - Integrate spell check highlighting in ChatTextArea component - Add CSS styles for red wavy underlines on misspelled words - Add comprehensive tests for spell check functionality - Add i18n translations for the feature This feature helps users identify misspelled words in the chat interface by highlighting them with red wavy underlines, similar to standard text editors.
This commit is contained in:
parent
478869e37a
commit
8814688861
9 changed files with 712 additions and 8 deletions
|
|
@ -6,7 +6,7 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js"
|
|||
* ExperimentId
|
||||
*/
|
||||
|
||||
export const experimentIds = ["powerSteering", "multiFileApplyDiff"] as const
|
||||
export const experimentIds = ["powerSteering", "multiFileApplyDiff", "spellCheck"] as const
|
||||
|
||||
export const experimentIdsSchema = z.enum(experimentIds)
|
||||
|
||||
|
|
@ -19,6 +19,7 @@ export type ExperimentId = z.infer<typeof experimentIdsSchema>
|
|||
export const experimentsSchema = z.object({
|
||||
powerSteering: z.boolean().optional(),
|
||||
multiFileApplyDiff: z.boolean().optional(),
|
||||
spellCheck: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type Experiments = z.infer<typeof experimentsSchema>
|
||||
|
|
|
|||
|
|
@ -23,11 +23,21 @@ describe("experiments", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("SPELL_CHECK", () => {
|
||||
it("is configured correctly", () => {
|
||||
expect(EXPERIMENT_IDS.SPELL_CHECK).toBe("spellCheck")
|
||||
expect(experimentConfigsMap.SPELL_CHECK).toMatchObject({
|
||||
enabled: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("isEnabled", () => {
|
||||
it("returns false when POWER_STEERING experiment is not enabled", () => {
|
||||
const experiments: Record<ExperimentId, boolean> = {
|
||||
powerSteering: false,
|
||||
multiFileApplyDiff: false,
|
||||
spellCheck: false,
|
||||
}
|
||||
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
|
||||
})
|
||||
|
|
@ -36,6 +46,7 @@ describe("experiments", () => {
|
|||
const experiments: Record<ExperimentId, boolean> = {
|
||||
powerSteering: true,
|
||||
multiFileApplyDiff: false,
|
||||
spellCheck: false,
|
||||
}
|
||||
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true)
|
||||
})
|
||||
|
|
@ -44,6 +55,7 @@ describe("experiments", () => {
|
|||
const experiments: Record<ExperimentId, boolean> = {
|
||||
powerSteering: false,
|
||||
multiFileApplyDiff: false,
|
||||
spellCheck: false,
|
||||
}
|
||||
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { AssertEqual, Equals, Keys, Values, ExperimentId, Experiments } fro
|
|||
export const EXPERIMENT_IDS = {
|
||||
MULTI_FILE_APPLY_DIFF: "multiFileApplyDiff",
|
||||
POWER_STEERING: "powerSteering",
|
||||
SPELL_CHECK: "spellCheck",
|
||||
} as const satisfies Record<string, ExperimentId>
|
||||
|
||||
type _AssertExperimentIds = AssertEqual<Equals<ExperimentId, Values<typeof EXPERIMENT_IDS>>>
|
||||
|
|
@ -16,6 +17,7 @@ interface ExperimentConfig {
|
|||
export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
|
||||
MULTI_FILE_APPLY_DIFF: { enabled: false },
|
||||
POWER_STEERING: { enabled: false },
|
||||
SPELL_CHECK: { enabled: false },
|
||||
}
|
||||
|
||||
export const experimentDefault = Object.fromEntries(
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import { mentionRegex, mentionRegexGlobal, unescapeSpaces } from "@roo/context-m
|
|||
import { WebviewMessage } from "@roo/WebviewMessage"
|
||||
import { Mode, getAllModes } from "@roo/modes"
|
||||
import { ExtensionMessage } from "@roo/ExtensionMessage"
|
||||
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,
|
||||
|
|
@ -86,6 +88,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
togglePinnedApiConfig,
|
||||
taskHistory,
|
||||
clineMessages,
|
||||
experiments,
|
||||
} = useExtensionState()
|
||||
|
||||
// Find the ID and display text for the currently selected API configuration
|
||||
|
|
@ -180,6 +183,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
const contextMenuContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false)
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
const [spellCheckResults, setSpellCheckResults] = useState<SpellCheckResult[]>([])
|
||||
const spellCheckLayerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Use custom hook for prompt history navigation
|
||||
const { handleHistoryNavigation, resetHistoryNavigation, resetOnInputChange } = usePromptHistory({
|
||||
|
|
@ -216,6 +221,36 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
}
|
||||
}, [inputValue, sendingDisabled, setInputValue, t])
|
||||
|
||||
// Check if spell check is enabled
|
||||
const isSpellCheckEnabled = useMemo(() => {
|
||||
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(() => {
|
||||
|
|
@ -674,6 +709,52 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
}
|
||||
}, [])
|
||||
|
||||
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 += `<span class="spell-check-error">${misspelledWord.replace(
|
||||
/[<>&]/g,
|
||||
(c) => ({ "<": "<", ">": ">", "&": "&" })[c] || c,
|
||||
)}</span>`
|
||||
|
||||
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<HTMLTextAreaElement>) => {
|
||||
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(e.key)) {
|
||||
|
|
@ -1044,6 +1125,30 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
color: "transparent",
|
||||
}}
|
||||
/>
|
||||
{isSpellCheckEnabled && (
|
||||
<div
|
||||
ref={spellCheckLayerRef}
|
||||
className={cn(
|
||||
"absolute",
|
||||
"inset-0",
|
||||
"pointer-events-none",
|
||||
"whitespace-pre-wrap",
|
||||
"break-words",
|
||||
"text-transparent",
|
||||
"overflow-hidden",
|
||||
"font-vscode-font-family",
|
||||
"text-vscode-editor-font-size",
|
||||
"leading-vscode-editor-line-height",
|
||||
"py-2",
|
||||
"px-[9px]",
|
||||
"z-[5]",
|
||||
"forced-color-adjust-none",
|
||||
)}
|
||||
style={{
|
||||
color: "transparent",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<DynamicTextArea
|
||||
ref={(el) => {
|
||||
if (typeof ref === "function") {
|
||||
|
|
@ -1106,7 +1211,10 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
"scrollbar-none",
|
||||
"scrollbar-hide",
|
||||
)}
|
||||
onScroll={() => updateHighlights()}
|
||||
onScroll={() => {
|
||||
updateHighlights()
|
||||
updateSpellCheckHighlights()
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="absolute top-1 right-1 z-30">
|
||||
|
|
|
|||
|
|
@ -222,10 +222,8 @@ describe("mergeExtensionState", () => {
|
|||
apiConfiguration: { modelMaxThinkingTokens: 456, modelTemperature: 0.3 },
|
||||
experiments: {
|
||||
powerSteering: true,
|
||||
marketplace: false,
|
||||
disableCompletionCommand: false,
|
||||
concurrentFileReads: true,
|
||||
multiFileApplyDiff: true,
|
||||
spellCheck: false,
|
||||
} as Record<ExperimentId, boolean>,
|
||||
}
|
||||
|
||||
|
|
@ -238,10 +236,8 @@ describe("mergeExtensionState", () => {
|
|||
|
||||
expect(result.experiments).toEqual({
|
||||
powerSteering: true,
|
||||
marketplace: false,
|
||||
disableCompletionCommand: false,
|
||||
concurrentFileReads: true,
|
||||
multiFileApplyDiff: true,
|
||||
spellCheck: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -648,6 +648,10 @@
|
|||
"MULTI_FILE_APPLY_DIFF": {
|
||||
"name": "Enable concurrent file edits",
|
||||
"description": "When enabled, Roo can edit multiple files in a single request. When disabled, Roo must edit files one at a time. Disabling this can help when working with less capable models or when you want more control over file modifications."
|
||||
},
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"promptCaching": {
|
||||
|
|
|
|||
|
|
@ -386,6 +386,14 @@ 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
|
||||
*/
|
||||
|
|
|
|||
142
webview-ui/src/utils/__tests__/spellCheck.spec.ts
Normal file
142
webview-ui/src/utils/__tests__/spellCheck.spec.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
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")
|
||||
})
|
||||
})
|
||||
})
|
||||
431
webview-ui/src/utils/spellCheck.ts
Normal file
431
webview-ui/src/utils/spellCheck.ts
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
/**
|
||||
* 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 = /(?<![@/])\b[\w']+\b/g
|
||||
let match
|
||||
|
||||
while ((match = wordRegex.exec(text)) !== null) {
|
||||
// Double check that this word is not part of a mention or command
|
||||
const charBefore = text[match.index - 1]
|
||||
if (charBefore !== "@" && charBefore !== "/") {
|
||||
words.push({
|
||||
word: match[0],
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return words
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform spell check on the given text
|
||||
* Returns an array of potentially misspelled words with their positions
|
||||
*/
|
||||
export const checkSpelling = async (text: string): Promise<SpellCheckResult[]> => {
|
||||
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 = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number,
|
||||
): ((...args: Parameters<T>) => void) => {
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timeout) clearTimeout(timeout)
|
||||
timeout = setTimeout(() => func(...args), wait)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue