[lexical][lexical-list][lexical-selection][lexical-link] Refactor: Centralize replace-area selection mapping + bulk splice#8505
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| const {anchor, focus} = selection; | ||
| if (anchor.key === toReplaceKey && anchor.type === 'element') { | ||
| anchor.set( | ||
| replaceWithNode.getKey(), | ||
| prevSizeBeforeChildrenTransfer + anchor.offset, | ||
| 'element', | ||
| ); | ||
| } | ||
| if (focus.key === toReplaceKey && focus.type === 'element') { | ||
| focus.set( | ||
| replaceWithNode.getKey(), | ||
| prevSizeBeforeChildrenTransfer + focus.offset, | ||
| 'element', | ||
| ); | ||
| } |
There was a problem hiding this comment.
Maybe we should refine the type of RangeSelection.getStartEndPoints() to be strictly [PointType, PointType] to turn this sort of code into a loop, e.g.
| const {anchor, focus} = selection; | |
| if (anchor.key === toReplaceKey && anchor.type === 'element') { | |
| anchor.set( | |
| replaceWithNode.getKey(), | |
| prevSizeBeforeChildrenTransfer + anchor.offset, | |
| 'element', | |
| ); | |
| } | |
| if (focus.key === toReplaceKey && focus.type === 'element') { | |
| focus.set( | |
| replaceWithNode.getKey(), | |
| prevSizeBeforeChildrenTransfer + focus.offset, | |
| 'element', | |
| ); | |
| } | |
| for (const point of selection.getStartEndPoints()) { | |
| if (point.key === toReplaceKey && point.type === 'element') { | |
| point.set( | |
| replaceWithNode.getKey(), | |
| prevSizeBeforeChildrenTransfer + point.offset, | |
| 'element', | |
| ); | |
| } | |
| } |
There was a problem hiding this comment.
Narrowed RangeSelection.getStartEndPoints() to [PointType, PointType] (matches the shape TableSelection.getStartEndPoints() already returns) and dropped the per-point branches for the suggested loop. Also cleaned two callsites the narrower type made dead: the ! in markSelection.ts $getOrderedSelectionPoints and the invariant !== null in formatList.ts $insertList. Left the BaseSelection-typed callsites as-is since NodeSelection.getStartEndPoints() still returns null.
…ntralize replace-area selection mapping + bulk splice Follow-up to facebook#8501. The base LexicalNode.replace now maps element-anchored selections from the replaced node to {key: replacement, offset: prevSize + originalOffset, type: 'element'}. Two pieces were held back for this PR: the same mapping in ListItemNode.replace (an override that bypasses super.replace), and removal of the \$setBlocksType workaround that compensated for the lossy pre-facebook#8501 behavior. ListItemNode.replace now mirrors the base prevSize + offset mapping in the includeChildren branch. The mapping runs before the trailing this.remove() so the subsequent \$removeNode → moveSelectionPointToSibling sees anchor/focus already redirected and is a no-op for those points. Text-anchored selections inside transferred children follow their text node's key across the children move, so no extra mapping is needed there. \$setBlocksType drops the cloned-newSelection + per-iteration override + selection.is(\$getSelection()) commit. Both replace paths now handle the mapping, making the caller-side workaround redundant. getChildren().forEach(parent.append) patterns collapse to parent.splice(parent.getChildrenSize(), 0, getChildren()) at four sites: base LexicalNode.replace, ListItemNode.replace, \$setBlocksType's wrap branch, and \$toggleLink's unlink branch. Since append is splice-of-1, this is one bookkeeping pass instead of N. \$toggleLink also switches to parentLink.getParentOrThrow() to keep the existence assertion loud.
f81a755 to
50d67cb
Compare
Description
Follow-up to #8501. PR #8501 taught
LexicalNode.replaceto map an element-anchored selection on the replaced node to{key: replacement, offset: prevSize + originalOffset, type: 'element'}and left two pieces for this PR: the same mapping inListItemNode.replace(an override that bypassessuper.replace), and removal of the$setBlocksTypeworkaround that compensated for the lossy pre-#8501 behavior.What changed
ListItemNode.replacemirrors the baseprevSize + offsetmapping in itsincludeChildrenbranch. The mapping runs before the trailingthis.remove()so the subsequent$removeNode → moveSelectionPointToSiblingsees anchor/focus already redirected to the replacement and is a no-op for those points. Text-anchored selections inside transferred children follow their text node's key across the children move, so no extra mapping is needed for that case. This catches the override up to the contract basereplacestarted honoring in [Breaking Change][lexical] Bug Fix: Adjust selection when removeFromParent callers move a node out of its parent #8501.$setBlocksTypedrops the cloned-newSelection+ per-iteration override +selection.is($getSelection())commit. With base and list-itemreplacepaths both handling element-anchored mapping, the caller-side workaround is redundant. Unused$createRangeSelectionand$getSelectionimports removed.getChildren().forEach(parent.append)patterns collapse to a singleparent.splice(parent.getChildrenSize(), 0, getChildren())call at the four sites in this area: baseLexicalNode.replace,ListItemNode.replace,$setBlocksType's wrap branch, and$toggleLink's unlink branch. Sinceappendissplice-of-1, this is one bookkeeping pass instead of N.$toggleLinkunlink: the originalparentLink.getChildren().forEach(c => parentLink.insertBefore(c))is a "promote children to siblings before self" shape. Splice equivalent:parentLink.getParentOrThrow().splice(parentLink.getIndexWithinParent(), 0, parentLink.getChildren()).parentLinkwas just located via$findMatchingParenton a descendant so its parent is guaranteed —getParentOrThrow()keeps that as a loud invariant rather than a silent skip.Note on
$toggleLinkselection adjustmentThe
forEach(insertBefore)→ singlesplicesubstitution has a subtle semantic difference: eachinsertBeforecalls$updateElementSelectionOnCreateDeleteNode(one selection shift per insert), while a singlesplicewithdeleteCount=0does not. In practice this path runs only whenselection.isCollapsed() && url === nulland the collapsed selection sits inside the link's text content, so no element-anchored selection in the grandparent at the link's index is reachable. Behavior unchanged for the documented entry conditions.Closes the follow-up scope discussed at #8501 (comment)
Test plan
pnpm vitest run --project unit— full unit suite (114 files, 2565 tests + 1 skipped) green.$setBlocksTypedescribe block inLexicalSelection.test.tsxcovers the workaround-removal regression surface; the list-item regression that blocked the cleanup attempt during [Breaking Change][lexical] Bug Fix: Adjust selection when removeFromParent callers move a node out of its parent #8501 (the "Expected ListItemNode to have block ElementNode ancestor" invariant) does not reproduce on this branch.