Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements in insertNodes #5201

Merged
merged 17 commits into from
Dec 1, 2023
Merged

Conversation

GermanJablo
Copy link
Contributor

@GermanJablo GermanJablo commented Nov 3, 2023

  • In insertNodes I divided the process into two cases: All elements of the array are inline, or at least one element is not inline. That makes insertNodes about 10 lines longer, but much easier to reason with and refactor.
  • I renamed $basicInsertStrategy to $wrapInlineNodes and moved it from clipboard to insertNodes. Now insertNodes and paste should not have different behaviors, which also made refactoring more difficult. Thanks to this, an incorrect test was detected and corrected: "'white-space: pre (no touchy) (2)".
  • Tests about "disjoint sibling text node" were removed. Before the previous PR where I changed insertNodes, these tests verified that an error was thrown in that case. This does not happen now. However, the case with "disjoint sibling text node" has already been tested in the G Docs pasting suite.
  • Instead of iterating over each node and inserting it, I defined an insertRangeAfter method in LexicalNode that can be used to let users define how the nodes should be pasted. This PR makes Modularize the paste process with a new INSERT_NODES_COMMAND #4900 obsolete as it offers a simpler solution.

Copy link

vercel bot commented Nov 3, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
lexical ✅ Ready (Inspect) Visit Preview 💬 Add feedback Nov 24, 2023 5:08pm
lexical-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Nov 24, 2023 5:08pm

@facebook-github-bot facebook-github-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 3, 2023
@GermanJablo GermanJablo changed the title Fix insertNodes (part2) Fix insertNodes (part 2) Nov 3, 2023
@GermanJablo GermanJablo changed the title Fix insertNodes (part 2) Small improvements in insertNodes Nov 5, 2023
@GermanJablo GermanJablo marked this pull request as ready for review November 5, 2023 18:06
@GermanJablo GermanJablo marked this pull request as draft November 6, 2023 06:16
Comment on lines +724 to +725
output = output.replace(/\s__playwright_target__="[^"]+"/, '');

Copy link
Contributor Author

Choose a reason for hiding this comment

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

locally sometimes some tests fail me due to this attribute that playwright uses. I don't know if it happens to any of you too.
It seems to be a bug:
microsoft/playwright#22004

@GermanJablo GermanJablo marked this pull request as ready for review November 8, 2023 12:22
@GermanJablo GermanJablo changed the title Small improvements in insertNodes Improvements in insertNodes Nov 8, 2023
@acywatson
Copy link
Contributor

I'm good here - gonna let @zurfyx take a pass since he's more familiar with the new $insertNodes logic at the moment.

Copy link
Member

@zurfyx zurfyx left a comment

Choose a reason for hiding this comment

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

Thank you GermanJablo! Sorry for the late review

return;
}

selection.insertNodes([heading, $createTextNode('bar')]);
Copy link
Member

Choose a reason for hiding this comment

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

Do we have tests for the correct behavior of this? I think it's important to ensure that block + block will remain on future iterations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this case (block + text) and also the one you mention (block + block) are covered in the copy paste suite

*
*
* */
insertRangeAfter(firstToInsert: LexicalNode, lastToInsert?: LexicalNode) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you share some more thoughts on why we need this additional method on the API? I feel like insertAfter already covers most use cases and otherwise a spread (array) params like we have for append will do. The signature is also a bit confusing in my opinion, I understand the performance aspect of it but it's a hard trade-off.

For the sake of insertNodes I would personally have this function as a module level.

insertRangeAfter(node: LexicalNode, ...other params)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, two reasons.

  1. First, it provides a way to modify the behavior of multiple nodes inserted after another. For example, in my case I want that when several blocks are pasted, the indent level of the initial block is added to all of them. This is not currently possible. Using PASTE_COMMAND would mean having to repeat hundreds of lines of code. See Modularize the paste process with a new INSERT_NODES_COMMAND #4900

  2. Secondly, these methods offer a much more performant API than splice and append, optimized for the double linked list structure that uses lexical. See Deprecate splice, append, getChildIndex and getIndexWithinParent #5194

I don't recommend using a spread array param, since you almost always insert nodes that are already siblings anyway, and that would require an additional iteration loop.

I also wouldn't "hide" it inside the insertNodes module for internal use. This use case is not only useful for copy-paste. For example, in my application you can drag and drop a series of blocks to another position.

Copy link
Member

Choose a reason for hiding this comment

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

@GermanJablo - The two points you made are totally valid, but I also think that having an ergonomic API and backward compatibility to certain extent is very important.

In my opinion, linked lists are a fairly advanced CS concept and even with a good understanding of them, having firstToInsert and lastToInsert would still lead me to some trial and error to fully understand how the function works.

As for performance, I would take into account real-life use cases. insertNodes may be worth optimizing, a plugin which handles clipboard paste once every 5 min can probably do with a function that takes slightly longer to initialize. At the end of the day the cost is a linear O(n).

I propose the following to unblock this PR. We hide this function from the public API and I bring this and #5194 up for team discussion to gather more opinions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change is backward compatible.

...other params would be O(n), but lastToInsert is O(1). Yes, in insertNodes you have to iterate all the nodes anyway. But this may not always be the case. A real life example of this is dragging consecutive blocks. firstToInsert and lastToInsert are obtained immediately from the anchor and focus of the selection.

I understand that ...other params may be a little easier to understand, but do you think that even with the JSDOC comments lastToInsert is difficult to understand?

Copy link
Member

Choose a reason for hiding this comment

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

Isn't the function implementation O(2n)? The while loop + for loop. I believe the additional for loop isn't necessary but in any case we're now talking about O(3n) which is the same order of magnitude.

IMO the function is not immediately obvious, it's a sort of slice based function and the preconditions are that the first and the last have to be children of the same parent. To me having insertAfter(node1, node2, node3) is more intuitive and iff optimal performance a must have I'd probably consider a utility that does this for you

$insertChildrenAfter(refNode: LexicalNode, parent: ElementNode);

Copy link
Contributor Author

@GermanJablo GermanJablo Nov 30, 2023

Choose a reason for hiding this comment

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

Actually, I haven't finished the function. I'm simply iterating and applying insertAfter on each node to save time. But the idea is to correct the implementation later.

My idea of the algorithm is:

  • loop to check from firstToInsert to lastToInsert with 3 goals: (1) check that lastToInsert is a later sibling of firstToInsert, (2) calculate the distance between those two nodes (count++) , needed to update the size property of the newParent and oldParent and (3) update the parent property of the nodes in the range.

If we use ...params, you will be doing the same job + additionally:

  • You need an additional iteration to store the nodes to be inserted in an array (plus memory space)
  • modifying the size of newParent and oldParent N times.
  • Modifying nextSibling and prevSibling 3N times (for each node moved, unnecessarily).

Yes, we are talking about the same order of magnitude. The 2 cases are O(N)... the difference is that in the first case fewer iterations are done, and also fewer things in the iterations. I wouldn't be surprised if the second implementation is 3-5X slower when there are many nodes.

Please correct me if I'm wrong

} else if (notInline(node)) {
currentBlock = currentBlock.insertAfter(node) as ElementNode;
function restoreSelection() {
if (nodeToSelect.select) {
Copy link
Member

Choose a reason for hiding this comment

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

I would be more inclined to keep against $isTextNode and $isElementNode as it would give us type-safetiness, it's currently typed as any

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or better yet: #5205
? 😅

Comment on lines +1579 to +1580
const isLI = (node: LexicalNode) =>
'__value' in node && '__checked' in node;
Copy link
Member

Choose a reason for hiding this comment

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

Is this part of the command abstraction part? Like code blocks?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the current version of insertNodes it was pretending to be lexical/list agnostic, but it wasn't.
That's why one of the current conditions of isMergeable is (!firstBlock.isEmpty() || !$isRootOrShadowRoot(firstBlock.getParentOrThrow()));.

Surely anyone who reviews that will not understand that condition (in fact it's still hard for me lol), so I came clean with a more declarative condition.

@zurfyx zurfyx merged commit 6578046 into facebook:main Dec 1, 2023
44 of 45 checks passed
@GermanJablo GermanJablo deleted the fix-insertNodes branch December 1, 2023 19:47
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.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants