Add automatic typo fixing and correction explanations#654
Conversation
| guard let corrected = bestCorrection(current.result.word) else { | ||
| return .suppress | ||
| } | ||
| if automaticallyFixTypos, current.trailingSpaceCount == 1 { | ||
| return .applyCorrection(word: current.result.word, correctedWord: corrected) | ||
| } | ||
| if offerTypoCorrections { | ||
| return .offerCorrection(word: current.result.word, correctedWord: corrected) | ||
| } | ||
| return .suppress |
There was a problem hiding this comment.
In suppression-only mode (
suppressCompletionsOnTypo: true, both offerTypoCorrections and automaticallyFixTypos false), bestCorrection is now invoked on every prediction cycle that hits a typo, even though its result is never used. The old code guarded the call behind offerTypoCorrections. The bestCorrection closure chains symSpellCorrector then falls back to spellChecker.bestCorrection, so an NSSpellChecker round-trip can fire even when neither fix feature is on.
| guard let corrected = bestCorrection(current.result.word) else { | |
| return .suppress | |
| } | |
| if automaticallyFixTypos, current.trailingSpaceCount == 1 { | |
| return .applyCorrection(word: current.result.word, correctedWord: corrected) | |
| } | |
| if offerTypoCorrections { | |
| return .offerCorrection(word: current.result.word, correctedWord: corrected) | |
| } | |
| return .suppress | |
| guard automaticallyFixTypos || offerTypoCorrections else { | |
| return .suppress | |
| } | |
| guard let corrected = bestCorrection(current.result.word) else { | |
| return .suppress | |
| } | |
| if automaticallyFixTypos, current.trailingSpaceCount == 1 { | |
| return .applyCorrection(word: current.result.word, correctedWord: corrected) | |
| } | |
| if offerTypoCorrections { | |
| return .offerCorrection(word: current.result.word, correctedWord: corrected) | |
| } | |
| return .suppress |
| logStage( | ||
| "typo-auto-correction-failed", | ||
| workID: workID, | ||
| generation: liveContext.generation, | ||
| message: message, | ||
| normalizedOutput: correctedWord | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| cancelPredictionWork() | ||
| clearSuggestion(clearDiagnostics: false) | ||
| hideOverlay(reason: "Overlay hidden because Cotabby automatically fixed a typo.") | ||
| latestAcceptanceAction = "Automatically corrected \"\(typoWord)\" to \"\(correctedWord)\"." | ||
| state = .idle | ||
| logStage( | ||
| "typo-auto-corrected", | ||
| workID: workID, | ||
| generation: liveContext.generation, | ||
| message: "Automatically replaced the completed misspelled word after Space.", | ||
| normalizedOutput: correctedWord | ||
| ) |
There was a problem hiding this comment.
normalizedOutput diverges from the acceptance path on both branches
Both the failure log (line 234) and success log (line 249) pass correctedWord — the raw string from bestCorrection, before normalization and before trailing-space preservation. The manual-acceptance path in acceptCorrection consistently passes replacement.replacementText for normalizedOutput on its failure and success branches. Using replacement.replacementText here as well would make both paths comparable when correlating auto-corrected and manually-accepted events in diagnostics.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
Adds always-visible explanatory text to the typo correction settings and introduces an opt-in automatic typo-fixing mode. Automatic fixes run only after the user commits a misspelled word with Space, then revalidate the exact live trailing word and preserve its spacing before replacement.
The correction preference is persisted through the settings store/model/snapshot pipeline, indexed for Settings search, and shares one fail-closed replacement planner with manual correction acceptance.
Validation
xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' build-for-testing -derivedDataPath build/DerivedData** TEST BUILD SUCCEEDED **TypoGateTests,CurrentWordExtractorTests, andSuggestionSettingsStoreTests.CotabbyTests.xctestwere signed with different Team IDs. This is the documented local test-host signing limitation.git diff --checkpassed.Linked issues
None.
Risk / rollout notes
cotabbyAutomaticallyFixTyposUserDefaults key. It defaults tofalse, so existing behavior is unchanged until the user opts in.Greptile Summary
This PR introduces an opt-in "Automatically Fix Typos" mode that replaces a completed misspelled word immediately after Space, without requiring the user's accept key. The correction preference is persisted through the full settings pipeline (store → model → snapshot) and the replacement logic is unified in a new
TypoCorrectionReplacementPlannershared by both auto-fix and manual-acceptance paths.TypoCorrectionReplacementPlannercentralises the word-match validation, UTF-16 delete-count computation, and trailing-space preservation that were previously duplicated in the acceptance path; the new auto-fix path uses the same planner withrequiresTrailingSpace: trueto fail-closed if the user has already continued typing.TypoGategains a.applyCorrectioncase that fires only when there is exactly one trailing Space, preventing destructive edits on unfinished words; the gate already guarded all fix features behindsuppressCompletionsOnTypo.Confidence Score: 4/5
Safe to merge; the new auto-fix path is fail-closed and defaults to off, leaving existing behaviour unchanged until the user opts in.
The core replacement planner is well-tested, both fix paths share the same validation logic, and the feature defaults to off. Two small rough edges:
bestCorrection(a SymSpell + NSSpellChecker chain) is now called on every typo-detected cycle even when both offer and auto-fix flags are off, adding unnecessary work in suppression-only mode; andnormalizedOutputin the auto-correction log diverges from the accepted-correction log, which could make diagnostic comparisons awkward.Cotabby/Support/TypoGate.swift — the
bestCorrectionguard placement; Cotabby/App/Coordinators/SuggestionCoordinator+Prediction.swift — thenormalizedOutputargument in both log calls insideapplyAutomaticCorrection.Important Files Changed
Sequence Diagram
sequenceDiagram participant User participant Coordinator as SuggestionCoordinator participant Gate as TypoGate participant Planner as TypoCorrectionReplacementPlanner participant Inserter as SuggestionInserter User->>Coordinator: keystroke (debounced) Coordinator->>Gate: resolve(precedingText, suppress, offer, autoFix) alt no typo / suppression off Gate-->>Coordinator: .proceed Coordinator->>Coordinator: normal generation else typo, no correction available Gate-->>Coordinator: .suppress Coordinator->>Coordinator: clearSuggestion / hideOverlay else typo, offer on (no trailing space or autoFix off) Gate-->>Coordinator: .offerCorrection(word, correctedWord) Coordinator->>Coordinator: presentCorrection (green session) User->>Coordinator: presses accept key Coordinator->>Planner: plan(precedingText, expectedTypo, correctedWord, requiresTrailingSpace: false) Planner-->>Coordinator: TypoCorrectionReplacement Coordinator->>Inserter: replace(deletingUTF16Count, replacementText) Inserter-->>User: corrected text else "typo, autoFix on, trailing space = 1" Gate-->>Coordinator: .applyCorrection(word, correctedWord) Coordinator->>Planner: plan(precedingText, expectedTypo, correctedWord, requiresTrailingSpace: true) Planner-->>Coordinator: TypoCorrectionReplacement (or nil, abort) Coordinator->>Inserter: replace(deletingUTF16Count, replacementText) Inserter-->>User: corrected text (no key press needed) Coordinator->>Coordinator: schedulePredictionAfterHostPublishDelay endReviews (1): Last reviewed commit: "feat: add automatic typo fixing" | Re-trigger Greptile