Skip to content

fix(editor): resolve stale text ranges before layout, highlighting and accessibility read them - #2353

Merged
datlechin merged 1 commit into
mainfrom
fix/stale-range-guards
Aug 21, 2026
Merged

fix(editor): resolve stale text ranges before layout, highlighting and accessibility read them#2353
datlechin merged 1 commit into
mainfrom
fix/stale-range-guards

Conversation

@datlechin

Copy link
Copy Markdown
Member

Fixes #2340. Uses the NSRange.resolved(inDocumentOfLength:) added in #2351, which is already merged.

The problem

Four places read the text storage with a range or index that was computed against an older version of the document. NSTextStorage and NSString raise for those rather than clamping, and on macOS 26 and later an exception raised inside a draw or layout pass terminates the process.

  • TextLine.prepareForDisplay (TextLine.swift:57) slices the storage with position.range from the line storage. That storage is updated from the edited range, so it can be longer than the string it indexes while an edit is still in flight, which is the same invariant that produced the rectForOffset crash fixed in fix(editor): stop the rectForOffset crash and keep the autocomplete popup below the caret #1835. Every other consumer of it got a guard in that commit; this one, which runs from the layout pass at TextLayoutManager+Layout.swift:241, did not. Measured: NSRangeException, "NSConcreteTextStorage attributedSubstringFromRange:: Out of bounds".
  • TextView+Accessibility.swift:141 guarded index < documentRange.length with no lower bound, so a negative index from an accessibility client reached rangeOfComposedCharacterSequence(at:), which raises. accessibilityLine(for:) next to it had the same gap.
  • Highlighter.styleContainerDidUpdate (Highlighter.swift:279) applies style runs tracked against the storage's length as of the last edit, so a run can name text a newer edit removed. It runs inside an editing transaction on the storage.
  • MinimapLineFragmentView.addDrawingRunsUntil (MinimapLineFragmentView.swift:82-95) reads attribute(_:at:longestEffectiveRange:in:) and character(at:) at positions from its own fragment, with no bounds check. The minimap runs a second layout manager, which is exactly the one that goes stale. This one is currently unreachable in TablePro, because EditorPeripherals.swift:62 hardcodes showMinimap: false; it is fixed so that turning the minimap on does not turn it back into a crash.

The fix

Each site resolves its range against the storage it is about to read, using the resolver added in #2351. Nothing clamps silently into a different range: where a partial answer would be wrong, the site skips the work and leaves it for the next pass.

  • prepareForDisplay returns without typesetting when its range does not fit, leaving needsLayout set so the line is typeset again once the line storage and the string agree.
  • The two accessibility entry points check the lower bound and answer .notFound and -1, which is what they already answer for an index past the end.
  • The highlighter skips a run that no longer fits and keeps advancing, so the rest of the batch still applies.
  • The minimap clamps its scan to the storage length.

Verification

  • swift test --package-path LocalPackages/CodeEditTextView: 169 tests in 17 suites pass, including 4 new ones. Against the unfixed code the new TextLineDisplayRangeTests kills the test host with NSRangeException: NSConcreteTextStorage attributedSubstringFromRange:: Out of bounds.
  • xcodebuild -scheme TablePro build: passes, which is what compiles the CodeEditSourceEditor half of this change.

The two CodeEditSourceEditor fixes carry no tests: that package's test target does not run in CI (macos-tests.yml runs CodeEditTextView only, and the comment there records that swift test cannot build CodeEditSourceEditor on the runner), so a test placed there would be dead weight. Both are covered by the build and by the resolver's own unit tests in the package that does run.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@datlechin
datlechin merged commit eadf76e into main Aug 21, 2026
7 checks passed
@datlechin
datlechin deleted the fix/stale-range-guards branch August 21, 2026 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Editor: remaining unguarded text-storage ranges on the layout and highlight paths

1 participant