Skip to content

SpellCheckAdorner

317jamtay317 edited this page Jul 26, 2026 · 3 revisions

SpellCheckAdorner

The SpellCheckAdorner draws red squiggles under the misspelled words of the MarkdownRichEditor's Visual Document. It replaces WPF's built-in spell check, which cannot segment code-like identifiers and so flags the whole of this.ShouldBe().Invld() instead of only the genuinely misspelled part.

  • Class: UI.Controls.SpellCheckAdorner (derives from System.Windows.Documents.Adorner)
  • Attached by: MarkdownRichEditor on Loaded, into the editor's AdornerLayer.

Why a custom checker

WPF's SpellCheck.IsEnabled is control-level and offers no per-run opt-out and no control over word segmentation. Two things follow that it cannot do:

  1. Exclude code. Handled at projection time — code runs carry the language tag zxx (no linguistic content); this adorner skips any run so tagged.
  2. Segment identifiers. this.ShouldBe().Invld() must break into this / Should / Be / Invld so each is judged on its own and only Invld is flagged. That segmentation is the camelCase- and punctuation-aware WordTokenizer; the dictionary lookups come from SpellCheckScanner over an ISpellDictionary (the OS speller, via WindowsSpellDictionary).

How it works

  • Scan (debounced). On TextChanged the adorner clears its ranges, then — once edits settle — walks the document's prose runs (into sections, lists, tables, and spans), runs the scanner over each, and stores a TextRange per misspelling. The dictionary pass runs only here.
  • Paint (cheap). OnRender draws a squiggle under each stored range that is on-screen. Scrolling and resizing only repaint from the existing ranges — they never re-run the dictionary, so scrolling stays cheap.
  • Graceful degradation. If the OS speller is unavailable, WindowsSpellDictionary reports every word as correct, so no squiggles appear and nothing fails.

Spelling Suggestions

The adorner does not paint the squiggles interactively (IsHitTestVisible = false), but it does own the authoritative list of Misspelling ranges, so it answers one query for the editor: MisspellingAt(TextPointer) returns the Misspelling under a point, or null. On a right-click the MarkdownRichEditor uses it to decide whether to head the Editor Context Menu with Spelling Suggestions — SpellingSuggestions.For(word, dictionary) over the same ISpellDictionary, gathered into the menu by EditorContextMenu.Fill. Choosing a suggestion calls MarkdownRichEditor.CorrectMisspelling, which swaps the Misspelling's span through the same MatchReplacer a Replace runs through — so a correction inherits the Misspelling's formatting exactly as a Replacement does (correcting a word inside bold text leaves it bold, INV-022) — and Captures back into the Markdown source like any other edit. The correction is wrapped in BeginChange()/EndChange() so one Ctrl+Z undoes it whole; a range left over from a document that has since been replaced (IsInSameDocument says no) corrects nothing rather than editing the wrong document. Painting itself never changes the source.

The editor must own a ContextMenu object from construction for any of this to be reachable: WPF's built-in text-editor context menu is a class handler on ContextMenuOpening, so it runs before the editor's own handler, and finding no menu it opens its own and marks the event handled — leaving the Spelling Suggestions permanently unseen. The menu is therefore created once and only ever refilled (INV-057).

Clone this wiki locally