Commit bb10a96
committed
wip: TextKit 2 fragment redraw issue on insert (diagnostics included)
This commit folds two iterations together:
1. Remove `textView.isContinuousSpellCheckingEnabled = false` toggle
that caused a recursive textDidChange cascade.
2. Add `[SC]` diagnostic prints + try four fragment-redraw strategies
to fix the on-insert underline disappearance — none worked.
--- Part 1: recursive cascade fix (kept) ---
Setting `textView.isContinuousSpellCheckingEnabled = false` in
`scheduleSpellCheck` caused a recursive cascade on macOS 15.x:
1. AppKit synchronously clears `.spellingState` attributes
2. Attribute change triggers `textDidChange` delegate
3. `textDidChange` calls `clearSpellMisspellings` (cache empty) +
`scheduleSpellCheck` (cancels the current debounced pass)
4. Original work item is cancelled before it fires.
Deletion masked the bug because by the time the user deletes, the
flag is already false, so re-setting is a no-op — no recursive
trigger. Insert reliably reproduced it on the first keystroke after
mount.
The system's own continuous spell-check pass is now left enabled.
On macOS 15.x it paints on the default fragment (which our custom
`MarkdownTextLayoutFragment` replaces), so its `.spellingState`
underlines are invisible. Our cache-based drawing in
`drawSpellMisspellings` is the sole visible painter.
This change is independent of the redraw issue described below;
even after removing it, the on-insert disappearance still occurs.
--- Part 2: redraw issue (still open) ---
Symptom on macOS 15.7.7:
- Open a note with misspellings -> underlines drawn correctly on
initial layout.
- Type any character -> all underlines disappear and never come back
until something forces a full layout re-run (panel show/hide,
select-all, big scroll, etc.).
- Delete a character -> underlines blink and reappear after the
400 ms debounce (works correctly).
- The 400 ms-debounced spell-check pipeline itself is verified
correct via `[SC]` diagnostic prints (left in for maintainer
review):
textDidChange -> update was=N now=0 -> schedule -> run -> done -> update was=0 now=N
The cache `spellMisspelledRanges` repopulates with the right count
after every keystroke. NSSpellChecker returns the same ranges as
before the edit.
Root-cause hypothesis:
TextKit 2 maintains a per-fragment rendering cache. After an
insert, only the edited paragraph's fragments redraw on the next
display pass; the other fragments containing misspellings reuse
their pre-update imagery (no underlines) even though
`coordinator.spellMisspelledRanges` is correct.
Attempted fixes (all rebuilt and tested — none worked for insert):
1. `textView.needsDisplay = true` (original) -> only edited
fragment redraws.
2. `setNeedsDisplay(fragment.layoutFragmentFrame)` per
overlapping fragment -> rect goes stale during typing bursts
as fragments shift.
3. `tlm.invalidateRenderingAttributes(for: textRange)` per
misspelled range -> silently no-ops; no `draw(at:in:)`
follows.
4. `tlm.invalidateLayout(for: textRange)` per misspelled range
-> only the range overlapping the edited fragment redraws
(e.g. {349,7} drawing 54 times in our log); other fragments
still reuse cached imagery.
In all four attempts, the diagnostic confirms
`coordinator.spellMisspelledRanges` is correctly repopulated; the
failure is purely in the redraw pipeline.
Diagnostics added (kept for maintainer review):
- NativeTextViewCoordinator+TextDelegate.swift:
`[SC] textDidChange:` entry
- NativeTextViewCoordinator+SpellCheck.swift:
`[SC] schedule:` (queued / toggle-off-clearing)
`[SC] run: start | empty | done rawHits=N kept=N tag=T`
`[SC] update: was=N now=N`
- MarkdownTextLayoutFragment.swift:
`[SC] draw frag={loc,len} cacheCount=N`
Repro recipe:
1. Build EdgeMark (or any TextKit 2 host) against this branch on
macOS 15.x.
2. Run with Console filtered by `[SC]`.
3. Open a note with several misspellings spread across paragraphs.
4. Insert one character; observe `[SC] update: was=0 now=N`
followed by draws covering only the edited fragment (and
possibly the prev paragraph from restyle scope), not the
fragments holding the other misspellings. Visually all
underlines vanish.
5. Select-all (or hide/show side panel). Logs show all relevant
fragments redraw; underlines reappear.
Open question for engine authors:
Is there a TextKit 2 invalidation API that reliably forces
fragments overlapping an arbitrary document range to re-execute
`draw(at:in:)` without re-laying out (which would clobber the
styler's `.spellingState: 0` stamps the styler just put down)? Or
is the recommended pattern to drive fragment redraw via
`setRenderingAttributes` (and have the spell-check underline live
as a rendering attribute rather than custom drawing)?
Local SPM dependency note:
This branch is being iterated against EdgeMark via a local SPM
path (`XCLocalSwiftPackageReference`); no Package.resolved pin
needed.1 parent 083ec31 commit bb10a96
3 files changed
Lines changed: 63 additions & 17 deletions
File tree
- Sources/MarkdownEngine
- Renderer
- TextView/Coordinator
Lines changed: 1 addition & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
628 | 628 | | |
629 | 629 | | |
630 | 630 | | |
| 631 | + | |
631 | 632 | | |
632 | 633 | | |
633 | 634 | | |
| |||
Lines changed: 59 additions & 17 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | | - | |
15 | | - | |
| 14 | + | |
| 15 | + | |
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
20 | | - | |
21 | | - | |
22 | | - | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
23 | 24 | | |
24 | 25 | | |
25 | 26 | | |
| |||
58 | 59 | | |
59 | 60 | | |
60 | 61 | | |
| 62 | + | |
61 | 63 | | |
62 | 64 | | |
63 | 65 | | |
64 | | - | |
65 | | - | |
66 | | - | |
67 | | - | |
68 | | - | |
69 | | - | |
70 | | - | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
71 | 77 | | |
72 | 78 | | |
73 | 79 | | |
| |||
101 | 107 | | |
102 | 108 | | |
103 | 109 | | |
| 110 | + | |
104 | 111 | | |
105 | 112 | | |
106 | 113 | | |
107 | 114 | | |
108 | 115 | | |
109 | 116 | | |
| 117 | + | |
110 | 118 | | |
| 119 | + | |
111 | 120 | | |
112 | 121 | | |
113 | 122 | | |
| |||
118 | 127 | | |
119 | 128 | | |
120 | 129 | | |
| 130 | + | |
121 | 131 | | |
122 | 132 | | |
123 | 133 | | |
| |||
129 | 139 | | |
130 | 140 | | |
131 | 141 | | |
| 142 | + | |
132 | 143 | | |
133 | 144 | | |
134 | 145 | | |
135 | 146 | | |
136 | 147 | | |
| 148 | + | |
137 | 149 | | |
138 | 150 | | |
139 | 151 | | |
| |||
145 | 157 | | |
146 | 158 | | |
147 | 159 | | |
148 | | - | |
149 | | - | |
150 | | - | |
151 | | - | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
152 | 181 | | |
| 182 | + | |
| 183 | + | |
153 | 184 | | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
154 | 196 | | |
155 | 197 | | |
156 | 198 | | |
| |||
Lines changed: 3 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
62 | 65 | | |
63 | 66 | | |
64 | 67 | | |
| |||
0 commit comments