Skip to content

[lexical] Refactor: Cache RangeSelection.isBackward() result on the instance#8474

Merged
etrepum merged 2 commits intofacebook:mainfrom
mayrang:fix/issue-5825-cache-isBackward
May 7, 2026
Merged

[lexical] Refactor: Cache RangeSelection.isBackward() result on the instance#8474
etrepum merged 2 commits intofacebook:mainfrom
mayrang:fix/issue-5825-cache-isBackward

Conversation

@mayrang
Copy link
Copy Markdown
Contributor

@mayrang mayrang commented May 7, 2026

Description

RangeSelection.isBackward() calls Point.isBefore, which for the different-key case builds two normalized carets and walks the tree via $comparePointCaretNext. The same answer is reached repeatedly during a single update cycle (multiple call sites in the core and extensions read isBackward against the same selection) and the underlying carets stay valid for the lifetime of that anchor/focus pair.

This PR caches the result on the RangeSelection instance, mirroring the existing _cachedNodes pattern. The cache fills inside an update (gated by !isCurrentlyReadOnlyMode() to avoid writing to frozen state during reads) and invalidates when anchor or focus mutates via Point.set.

What changed

  • RangeSelection gains a _cachedIsBackward: boolean | null field, initialized to null in the constructor and read/written in isBackward().
  • Point.set's existing invalidation block (which already nulls _cachedNodes) also nulls _cachedIsBackward when the parent is a RangeSelection.
  • Four call sites bypass Point.set and write directly to point.offset to avoid setting selection.dirty = true mid-IME (RangeSelection.insertText × 3 and $setTextContentWithSelection × 1). Each now nulls _cachedNodes and _cachedIsBackward inline so both caches stay in sync with the offset change. This also closes a pre-existing latent gap where _cachedNodes could go stale on those paths — currently masked because the IME paths typically operate on a collapsed selection, so the cached node list happens to remain correct.

Backwards compatibility

isBackward() returns the same value it did before for any input. The only observable change is that a second call against the same RangeSelection instance no longer re-runs Point.isBefore. No interface or class signature changes; BaseSelection is unchanged.

Closes #5825

Test plan

  • pnpm vitest run --project unit — 2451 pass + 1 skipped, no regressions. New regression test in LexicalSelection.test.ts builds a forward selection, exercises the cache hit, mutates anchor/focus through setTextNodeRange (which routes through Point.set), and verifies invalidation + recomputation (anchor=5, focus=0 → backward). The test fails as expected when the cache logic is reverted.
  • pnpm tsc clean.
  • pnpm lint-flow — no new warnings (_cachedIsBackward is an internal RangeSelection field, not in BaseSelection or Lexical.js.flow).

…nstance

RangeSelection.isBackward() calls Point.isBefore which, for the different-key case, builds two normalized carets and walks the tree via $comparePointCaretNext. The same answer is reached repeatedly during a single update cycle and the underlying carets stay valid for the lifetime of that anchor/focus pair. Cache the result on the RangeSelection instance, mirroring the existing _cachedNodes pattern: cache fills inside an update (gated on !isCurrentlyReadOnlyMode()) and invalidates when anchor or focus mutates via Point.set. The four direct-offset mutation sites that bypass Point.set (RangeSelection.insertText × 3 and $setTextContentWithSelection × 1) now null both _cachedNodes and _cachedIsBackward inline, which also closes a pre-existing latent gap in _cachedNodes invalidation on those paths.

Closes facebook#5825
@vercel
Copy link
Copy Markdown

vercel Bot commented May 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment May 7, 2026 5:55pm
lexical-playground Ready Ready Preview, Comment May 7, 2026 5:55pm

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 7, 2026
Copy link
Copy Markdown
Collaborator

@etrepum etrepum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label May 7, 2026
…osition workaround

Per etrepum's audit, $handleInput's Firefox-only composition rewind
(LexicalEvents.ts:1144) directly mutates anchor.offset, bypassing
Point.set and leaving _cachedNodes / _cachedIsBackward stale. Null
both caches at the mutation site to preserve the PR's invariant.
@mayrang
Copy link
Copy Markdown
Contributor Author

mayrang commented May 7, 2026

Yeah, my audit was too narrow — only RangeSelection.ts and LexicalUtils.ts. 8e8e7a5 nulls both caches at LexicalEvents.ts:1144.

@etrepum etrepum added this pull request to the merge queue May 7, 2026
Merged via the queue into facebook:main with commit a73c4e1 May 7, 2026
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cache isBackward/isBefore

2 participants