-
Notifications
You must be signed in to change notification settings - Fork 3.3k
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
Compare only decorations offsets in MemoizedText. Code highlighting example improvements. #5271
Compare only decorations offsets in MemoizedText. Code highlighting example improvements. #5271
Conversation
🦋 Changeset detectedLatest commit: 469e76b The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@@ -1,13 +1,11 @@ | |||
import Prism from 'prismjs' | |||
import 'prismjs/components/prism-markdown' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just minor refactoring: using prism js markdown implementation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @dsvgit this looks good. Will leave it open a few days to give others a chance to give any suggestions or feedback.
Hey @dylans, great, thanks for taking a look at this |
@jasonphillips this one might be interesting for you 👀 |
site/examples/code-highlighting.tsx
Outdated
|
||
// set editor.intervals with intervals of a continuous stream of identical elements | ||
// set editor.intervalsStarts with set of these intervals start index (for optimization) | ||
const ExtractIntervals = () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could add this as a hook instead perhaps? Edit: oh I see why you did this, no need to make another wrapper this way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So on every editor/selection change this loops over all (top-level only?) editor children. Could you explain what an interval is in this context? Contiguous code lines?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @bryanph. Yes, it's just the contiguous elements intervals calculated on each editor/selection change And it loops only over all top-level editor children. It may have the following data:
{ "paragraph": [[0, 0], [12,14]], "code-line": [[1, 11], [15, 25]] }
So the idea here is to extract additional information based on the current editor's children state. It should be always synced with editor children. That's why these "extractors" were placed inside Slate, but before Editable. It means that all components inside Editable can rely on editor.intervals
as on editor.children
. I think it actually can be reorganised with hooks in some way. I just wanted to keep it simple.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it could be optimized by moving these calculations into
useMemo(() => {...; editor.intervals = intervals; }, [editor.children])
hook to prevent calling these calculations when only selection is changed
Great work, a well needed change to the code example. I'm not too sure about the |
@bryanph Thanks. I figured out how to keep the same performance without |
Oh, I just clicked multiple times for review request. Didn't think it works this way |
!Range.equals(range, other) || | ||
range[PLACEHOLDER_SYMBOL] !== other[PLACEHOLDER_SYMBOL] || | ||
!shallowCompare(rangeOwnProps, otherOwnProps) | ||
range.anchor.offset !== other.anchor.offset || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
compare only offsets for text inside the isTextDecorationsEqual
function
@@ -79,7 +79,7 @@ const MemoizedText = React.memo(Text, (prev, next) => { | |||
next.isLast === prev.isLast && | |||
next.renderLeaf === prev.renderLeaf && | |||
next.text === prev.text && | |||
isDecoratorRangeListEqual(next.decorations, prev.decorations) | |||
isTextDecorationsEqual(next.decorations, prev.decorations) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use the isTextDecorationsEqual
function that compares only offsets
56a3245
to
d1e14e6
Compare
@jasonphillips do you have any feedback on this one before I land it? |
This is a smart optimization! Slate already sends just the intersection of the decoration down into the text node so, as you said, the path shouldn’t matter for comparison. BasePath was confusing me a bit and I’m so wary about adding new concepts now that could affect someone’s use case, but this optimization makes perfect sense. |
But looking at the updated code-highlighting example, I have to say that I'm concerned about having that much complexity in the example. Examples are meant to illustrate how to do the basics in slate, and it seems that all this additional logic of finding continuous sequences of elements at the top level of the editor etc is going to be very confusing and counterproductive as a code-highlighting example. Each example should ideally be basic and introduce only one concept, which is the feature being illustrated (in this case, just how to use the decorate function). I'm also unsure of why this approach is chosen (continuous lists of top-level elements) instead of using something like a nested container element, eg. why not have structure One of Slate's great advantages over many other editors is to handle truly nested content, after all; and making the rendering of an element depend on its siblings rather than just parents is very tricky to get right--here, your codeline element depends upon checking whether it is the first code line in sequence (prior sibling is not codeline) in order to decide whether to show the language selection. That said, don't get me wrong--we have cases in our own application that I work on where we likewise have to think of siblings and overall structure of the editor children, and your implementation here looks nice. But I'm concerned that this introduces so much complexity as an example, and it brings in some concepts that are not exactly anti-patterns but definitely not a great place for a beginner to start learning how to use Slate the standard way. |
@jasonphillips thank you for the feedback, most points make sense. I need some time to think about it. |
… absolute ranges on passing them into TextComponent
…alculate absolute ranges on passing them into TextComponent" This reverts commit afa085c.
…elative ranges on passing them to TextComponent
… only if editor.children is changed
60e1888
to
a656d54
Compare
@jasonphillips , I updated the example, now it should be simpler
|
) | ||
|
||
const nodeToDecorations = mergeMaps( | ||
...blockEntries.map(getChildNodeToDecorations) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
precalculate editor.nodeToDecorations map to use it inside decorate function then
I didn't find a way to achieve good performance without this precalculation
} | ||
} | ||
|
||
const getChildNodeToDecorations = ([block, blockPath]: NodeEntry< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this function creates nodeToDecorations map for one code-block
element
const useDecorate = (editor: Editor) => { | ||
return useCallback(([node, path]) => { | ||
if (Element.isElement(node) && node.type === CodeLineType) { | ||
const ranges = editor.nodeToDecorations.get(node) || [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just apply precalculated decorations on CodeLine level
We can't apply it on CodeBlockElement level because of bad performance
We can't apply it on Text level because we lost right re-renders
// are always of type "plain". | ||
// This is not recursive to avoid exceeding the call-stack limit, since it's unclear | ||
// how nested Prism's tokens can become | ||
export const normalizeTokens = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this function needs to apply Prism.js tokens the right way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a good round of improvements.
GitHub Actions are showing some playwright test failures (shown via files changed).
Description
Initially, I tried to improve the Code Highlighting example on the Slate.js site. But while working on this, I didn't find a way to do it without changing the slate-react library code.
The Code Highlighting example improvements:
Slate-react changes:
slate-react
library. These changes allow returning relative ranges from thedecorate
function. ThebasePath
field is added to Point. So a range can have the following structure:isDecoratorRangeListEqual
compares ranges viaRange.equals
, these fields aren't included in the comparison. It means that even if thebasePath
is changed, the component isn't re-rendered since other properties are the same.basePath
is optional. And if it isn't set everything works as before.Issue
Example
Before:

After:

Before:

After:

Before:

After:

Before:

After:

slate-react
level:Before:

After (the same result, leaves aren't re-rendered, performance is kept):

Context
Relative points in decorations are primarily intended to prevent re-renders when elements only move their position in the editor while keeping the structure of internal decorations.
Checks
yarn test
.yarn lint
. (Fix errors withyarn fix
.)yarn start
.)yarn changeset add
.)