Scope list restyles to affected items - #149
Merged
luca-chen198 merged 4 commits intoAug 12, 2026
Merged
Conversation
Contiguous list blocks previously rebuilt and reparsed every item for a paragraph-scoped edit, making normal typing scale with the whole list. Normalize edit scopes once, retain only intersecting physical items, and preserve ordered numbering by widening structural marker edits and reseeding disjoint scopes.
Post-styling text-storage ranges can expand a one-character edit to an entire paragraph, preventing embedders from mirroring edits incrementally. Publish a completed single-edit descriptor from the coordinator's authoritative native lifecycle so consumers can preserve the exact UTF-16 transition.
YishenTu
marked this pull request as ready for review
August 11, 2026 11:14
Narrowing a scoped list node to the items its scope reached leaves two holes the numbering walk cannot see, and both surface on a plain caret move: the restyle covers the caret paragraph plus the one it left, and applyStyledRanges resets exactly the region the styler is asked to re-emit, so a number it declines to paint is visibly gone. LEADING hole: the node can start at a LATER item, but previousItemEnd started nil, so nothing re-seeded. With the preceding content block having just cleared needsSeed, the item fell back to its source literal and its overlay disappeared -- `# H` above `1./1./1.`, caret from the heading into item 3, which then renders `1.` and stays wrong until that item is restyled again. TRAILING hole: contiguousEnd kept the full block range, hiding the items the scope dropped from the tail. The gap to the next list block then read as loose-list spacing, so a count that stopped early carried over and painted a WRONG number over a correct literal -- `1. one/1. two`, blank, `1. three`, caret off item 3's digits into item 1, and item 3 renders `2.`. Also stop publishing onTextMutation for attribute-only edits. AppKit proposes those with a nil replacement string (data detection linkifying a phone number, Format > Font); coercing it to "" told a listener mirroring edits that the range had been deleted, while no text moved. Tests: both caret-move repros, the attribute-only case, and a scoped-vs-full equivalence sweep over 21 list shapes -- every line as its own scope and every line pair as the two-region scope a click produces. All four fail without these changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The number an ordered item shows is positional; the digit in the file is not. In a run written `1./1./1.` -- the spelling that lets the renderer do the counting -- every item's source reads `1.`, so revealing it was not "show me what I am editing" but "rename the item you are pointing at": insert an item in the middle, click inside a marker below it, and the `3.` you just watched appear turned back into `1.`. Select all and every number below the insertion dropped at once. Both reveals go, and with them the reason the selection one existed. This marker was the only one the engine hid by COLOUR (`foregroundColor: .clear`), and `NSTextView.selectedTextAttributes` carries a `selectedTextColor` -- AppKit repaints every selected glyph opaque, so the source digits came back under the highlight and collided with the number painted over them. Revealing was the workaround. The marker is now hidden by SIZE like every other marker here (`hiddenMarkerFontSize`), which a selection cannot override, and the run is kerned back out to the display marker's width so the slot, the hanging indent and the highlight still measure the same thing. The painter takes the view's base font instead of the run's, or it would draw the number at 0.1pt too. Measured by rendering offscreen and counting glyph pixels (page background and highlight classified out), three items, one overlay each: colour-hidden unselected 2169 selected 2488 (+319: the digits) size-hidden unselected 2169 selected 2150 With nothing about the marker depending on the caret or the selection any more, its crossing signal in textViewDidChangeSelection has no work to do, and neither does the ordered-list branch of the selection-span probe: both are removed, along with the two membership helpers that only fed them. Editing the digits still works -- they are the characters under the overlay, and any edit to them re-runs the numbering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A scoped edit inside a list still did list-wide work.
MarkdownAST.parse(scopes:)narrowed the affected block, but once a scope touched a continuous list the list parser enumerated, inline-parsed, and restyled every item in that run. On a long list, an ordinary one-character edit therefore scaled with the whole list instead of the affected lines.Ordered-list display numbering adds a correctness constraint: marker or indentation changes can affect later items and must widen the restyle run, while ordinary content edits should remain local.
Consumers mirroring native edits had a related fallback cost.
NSTextStorage.didProcessEditingcan report a range broadened by MarkdownEngine attribute work rather than the exact text mutation, making a safe incremental mirror fall back to replacing and restyling the full destination editor.Solution
MarkdownTextMutationthroughNativeTextViewWrapper.onTextMutation, reporting the exact accepted native text edit independently of later styling rangesPerformance
Measured on a 128 KiB continuous task list with edits inside a middle list item. The mirrored path applies each mutation to a second editor through the public integration surface.
Validation
swift test— 337 tests passedswift build