Skip to content

[lexical] Bug fix: $dfsCaretIterator should be able to stop at its last descendant#7978

Merged
etrepum merged 5 commits intofacebook:mainfrom
nigelgutzmann:nigel-fix-dfs-on-last-descendant
Nov 11, 2025
Merged

[lexical] Bug fix: $dfsCaretIterator should be able to stop at its last descendant#7978
etrepum merged 5 commits intofacebook:mainfrom
nigelgutzmann:nigel-fix-dfs-on-last-descendant

Conversation

@nigelgutzmann
Copy link
Copy Markdown
Contributor

@nigelgutzmann nigelgutzmann commented Nov 8, 2025

Description

When you have an element node and you want to iterate through all it's children, you might do something like:

$dfsIterator(elementNode, elementNode.getLastDescendant());

You would expect it to end at the last descendant inside of the elementNode. However, currently it will iterate through all the descendants, and then just continue right on through to the end of the entire editor's tree.

Describe the changes in this pull request
I checked the condition where endCaret is null even though the user did pass in an endNode. In this case, we should get the adjacent parent as the end caret.
Closes #

Test plan

Unit test included.

Before

It would return all nodes to the end of the editor's tree

After

It will properly stop at the node that is passed in as the endNode

@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 Nov 8, 2025
@vercel
Copy link
Copy Markdown

vercel bot commented Nov 8, 2025

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

Project Deployment Preview Comments Updated (UTC)
lexical Ready Ready Preview Comment Nov 10, 2025 4:02am
lexical-playground Ready Ready Preview Comment Nov 10, 2025 4:02am

Comment on lines +272 to +283
let endCaret = $getEndCaret(start, direction);
if (endNode) {
endCaret = $getAdjacentChildCaret(
$getChildCaretOrSelf($getSiblingCaret(endNode, direction)),
);
if (endCaret === null) {
const next = $getAdjacentSiblingOrParentSiblingCaret(
$getSiblingCaret(endNode, direction),
);
endCaret = next && next[0];
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This looks like a copy pasted version of $getEndCaret(endNode, direction)

Suggested change
let endCaret = $getEndCaret(start, direction);
if (endNode) {
endCaret = $getAdjacentChildCaret(
$getChildCaretOrSelf($getSiblingCaret(endNode, direction)),
);
if (endCaret === null) {
const next = $getAdjacentSiblingOrParentSiblingCaret(
$getSiblingCaret(endNode, direction),
);
endCaret = next && next[0];
}
}
const endCaret = endNode
? $getAdjacentChildCaret(
$getChildCaretOrSelf($getSiblingCaret(endNode, direction)),
) || $getEndCaret(endNode, direction)
: $getEndCaret(start, direction);

@etrepum
Copy link
Copy Markdown
Collaborator

etrepum commented Nov 8, 2025

While there is clearly some sort of bug here I think if we're going to try and fix anything with these functions it should be a more comprehensive fix to the tests and API docs to fully specify what the behavior actually should be. The caret APIs were added because functions like this one (and many of the functions that work with selections) are so ambiguous about how the edges and ElementNodes should be handled. These functions aren't even used at all in lexical itself (outside of a few tests) except for a single $dfs() call in useCharacterLimit to iterate the entire document with no specified start or end nodes.

From the API docs it's not really even clear whether endNode should be inclusive or not nor does it specify what happens if it's an ElementNode with children.

@nigelgutzmann
Copy link
Copy Markdown
Contributor Author

@etrepum , I made the changes you requested - simplified the code and added to the jsdocs for the relevant functions. To maintain backwards compatibility, both startNode and endNode should be inclusive.

However, just to be clear, the problem being solved here is not whether the startNode and endNode are inclusive or not, but that in this situation (where endNode has no next sibling), the caller supplies an endNode, the iterator hits the end node, and then just keeps going and going all the way to the end of the ENTIRE editor tree. The API is pretty clear that if the caller supplies an endNode, the iterator should stop at the endNode - no ambiguity there.

@nigelgutzmann
Copy link
Copy Markdown
Contributor Author

I don't really understand your point about ElementNodes being ambiguous. To me, it seems like the rules of Depth-First-Search are pretty clear when it comes to a node with children. Can you explain your point a bit more?

@etrepum
Copy link
Copy Markdown
Collaborator

etrepum commented Nov 8, 2025

My point is about an ElementNode being used as the endNode. ElementNode are basically visited twice, once on enter and once on exit. You could stop at or after either depending on the expected semantics of the function, because without carets to distinguish between them it’s very ambiguous. Depending on the definition there may or may not be a way to specify “include this ElementNode but not any of its children”.

@etrepum
Copy link
Copy Markdown
Collaborator

etrepum commented Nov 8, 2025

To be clear there’s definitely a problem with the implementation of endNode before this PR, but it’s also not really clear what this function was really supposed to do with endNode. There were not any docs, tests, or usage to specify what the edge case handling should do.

* @param startNode - The node to start the search, if omitted, it will start at the root node.
* @param endNode - The node to end the search, if omitted, it will find all descendants of the startingNode.
* @param startNode - The node to start the search (inclusive), if omitted, it will start at the root node.
* @param endNode - The node to end the search (inclusive), if omitted, it will find all descendants of the startingNode.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This does not specify (in docs or tests) whether endNode is also inclusive of all descendants of endNode

@nigelgutzmann
Copy link
Copy Markdown
Contributor Author

Got it! Thanks for the explanation. I can make those changes! 👍

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.

This looks great, now the function clearly specifies how it's supposed to behave so it's finally clear what is a bug and what isn't

@etrepum etrepum added this pull request to the merge queue Nov 11, 2025
@etrepum etrepum changed the title [lexical] Bug fix: $dfsCaretIterator should be able to stop at it's last descendant [lexical] Bug fix: $dfsCaretIterator should be able to stop at its last descendant Nov 11, 2025
Merged via the queue into facebook:main with commit a305fe3 Nov 11, 2025
39 checks passed
@etrepum etrepum mentioned this pull request Dec 10, 2025
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.

2 participants