Skip to content

[lexical][lexical-utils][lexical-selection][lexical-table] Feature: NodeCaret abstraction for traversals and ranges#7046

Merged
etrepum merged 82 commits intofacebook:mainfrom
etrepum:lexical-caret
Feb 8, 2025
Merged

[lexical][lexical-utils][lexical-selection][lexical-table] Feature: NodeCaret abstraction for traversals and ranges#7046
etrepum merged 82 commits intofacebook:mainfrom
etrepum:lexical-caret

Conversation

@etrepum
Copy link
Copy Markdown
Collaborator

@etrepum etrepum commented Jan 13, 2025

Description

A fair amount of bugs and complexity in our code results from ambiguity and awkwardness around traversing the node tree, as well as issues around coordinates becoming stale after mutations. This is a proposal for a new set of abstractions that can be used in place of the typical PointType and RangeSelection to work through these sorts of traversal use cases with hopefully fewer bugs.

There are three major deficiencies that the design of NodeCaret hopes to address:

  • PointType is not directional, and it's also not possible to infer a direction from a collapsed RangeSelection.
  • Dealing with the borders of an ElementNode is full of quirks, especially when empty, frequently causing over-selection or under-selection or just having to frequently branch for empty and non-empty cases.
  • Instability of PointType when doing anything "before" the point (e.g. inserting a node before a point "moves" the logical location of the point to the newly inserted node)

Docs:

I can write up some flow types when/if we move forward with the proposal to reduce the maintenance burden if anything changes before acceptance.

Cost

The current estimate looks like it's currently at 31.55 KB (+8.56% 🔺) which is +~2.5kB for the production cjs build. There will be some follow-up PRs to reduce this further with additional refactoring to use this API more internally.

Other changes

I didn't refactor everything with NodeCaret but chose a few places that would be straightforward and/or make it easier to fix a few bugs. More will come later which should reduce code size.

lexical

  • Refactor RangeSelection.removeText with new algorithm based on CaretRange
  • Clean up TextNode.splitText algorithm
  • Fix over-selection in getNodes() (refactor based on NodeCaret coming later)

@lexical/selection

  • Fixed $forEachSelectedTextNode to work with types other than RangeSelection
  • Export $patchStyle for updating the style of an individual TextNode or collapsed RangeSelection
  • Export new $ensureForwardRangeSelection function to flip anchor/focus when backwards
  • $setBlocksType: Add new $afterCreateElement argument to allow user to specify how properties propagate to the new block (e.g. format and indent by default - fixes an indent related bug)
  • Export new $copyBlockFormatIndent used as new $setBlocksType optional argument default

@lexical/table

  • Export existing $createTableSelectionFrom
  • Add more unit tests for TableSelection

@lexical/utils

  • Refactor $dfs and related functions with NodeCaret implementations

Closes #7076
Closes #7081

@vercel
Copy link
Copy Markdown

vercel bot commented Jan 13, 2025

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 Feb 8, 2025 2:24am
lexical-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 8, 2025 2:24am

@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 Jan 13, 2025
@github-actions

This comment was marked as outdated.

@etrepum etrepum changed the title [WIP][lexical[lexical-utils] Feature: NodeCaret abstraction for traversals and ranges [WIP][Breaking Change][lexical[lexical-utils] Feature: NodeCaret abstraction for traversals and ranges Jan 14, 2025
@etrepum etrepum changed the title [WIP][Breaking Change][lexical[lexical-utils] Feature: NodeCaret abstraction for traversals and ranges [WIP][lexical[lexical-utils] Feature: NodeCaret abstraction for traversals and ranges Jan 14, 2025
Copy link
Copy Markdown
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.

Overall, I like the concept and the direction, thank you for putting this masterpiece together with such detailed documentation and test plan!

A couple high-level thoughts:

While most of the LexicalCaret methods are simple, this adds a major of piece of opinionated API that (will) touch on most of Lexical core methods, I'm curious how complicated it will for contributors to familiarize with it. Before that, selection and LexicalNode were mostly independent. However, it does make some of the core implementation easier to read when the verbose iteration logic is abstracted away, and your LexicalSelection fixes prove this.

Naming wise, @fantactuka proposes the use of cursor instead of caret. Previously, we introduced block "cursor" selection, I wonder why we decided to name this one caret instead.

I'll have it a second pass this week but I wonder what others think in the meantime.

@etrepum
Copy link
Copy Markdown
Collaborator Author

etrepum commented Feb 3, 2025

Overall, I like the concept and the direction, thank you for putting this masterpiece together with such detailed documentation and test plan!

A couple high-level thoughts:

While most of the LexicalCaret methods are simple, this adds a major of piece of opinionated API that (will) touch on most of Lexical core methods, I'm curious how complicated it will for contributors to familiarize with it. Before that, selection and LexicalNode were mostly independent. However, it does make some of the core implementation easier to read when the verbose iteration logic is abstracted away, and your LexicalSelection fixes prove this.

Selection and LexicalNode are still very much independent. NodeCaret has nothing to do with the selection specifically, it's an abstraction for finding a node adjacent to some origin node in some specific direction. It's just a building block that can be used to come up with things like selections (e.g. using two PointCaret you can make a CaretRange which is basically a more robust RangeSelection).

Even in situations like TableSelection, NodeSelection, etc. you still need to do document tree navigation and in those cases it will be very useful to have these abstractions at hand. In some cases RangeSelection could be used for that but it's awfully hard to work with them without setting the editor's selection which is counter-productive when you are working with another selection type.

Naming wise, @fantactuka proposes the use of cursor instead of caret. Previously, we introduced block "cursor" selection, I wonder why we decided to name this one caret instead.

The words have very similar meaning, and I think it will be hard to avoid some confusion no matter which name is chosen. I settled on caret specifically because it is used more commonly for text, where cursor is the more general term. Cursor is used in other contexts in lexical already which is why I avoided that (blockCursor, css cursor for the mouse pointer, etc.). See also:

@etrepum
Copy link
Copy Markdown
Collaborator Author

etrepum commented Feb 3, 2025

I think a more accurate way to think of it is that NodeCaret/PointCaret are a way to bring something like PointType (but more fit for purpose) to everywhere in Lexical in a way that does not depend on RangeSelection. Right now RangeSelection is too intertwined with LexicalNode and document navigation in general. For example there's probably been a hundred times where I want to programmatically create a selection that spans multiple nodes but the APIs to do that are just miserable because calling .select(...) on a node sets the EditorState's selection as a side-effect and always sets both points when you're really just trying to get an anchor or a focus. In order to get an anchor or focus from a node with NodeCaret you just create one with one function call.

zurfyx
zurfyx previously approved these changes Feb 6, 2025
@etrepum
Copy link
Copy Markdown
Collaborator Author

etrepum commented Feb 6, 2025

Will merge this after v0.24.0 so there's some nightly releases before it goes live

@etrepum
Copy link
Copy Markdown
Collaborator Author

etrepum commented Feb 8, 2025

Latest couple commits:

  • Resolve conflict with main
  • Added more docs, and copied and pasted the examples from the docs to find a small mistake or two
  • Added flow types for the new stuff
  • Refactored StepwiseIteratorConfig to use a hasNext guard instead of stop so the types didn't have to lie sometimes (this is one of the things that flow does better than typescript with implies)

Should be good to merge once re-approved

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.

Bug: selection content not deleted when double click content and paste it in firefox. Regression: Font styles not applying to text in table cells

3 participants