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

refactor decorations to use selections #1221

Merged
merged 12 commits into from
Oct 13, 2017
Merged

Conversation

ianstormtaylor
Copy link
Owner

This changes how decorations are applied in Slate.

Previously, you would use a decorate(text, block) function in the schema. It would be called for each text node in the block, and you would add the marks to the characters yourself. But this led to issues because there are cases (like multi-line comments in syntax highlighting) where the decoration applies across multiple text nodes, and this system wasn't really built to handle that well.

Now, you use a decorate(block) function in the schema. It is no longer called for each of the text nodes. Instead of applying the new marks to the characters yourself, you return an array of selection objects, with associated marks. For example:

{
  match: (object) => {
    return object.type == 'code_block',
  },
  decorate: (block) => {
    const text = block.getFirstText()

    return [{
      anchorKey: text.key,
      anchorOffset: 0,
      focusKey: text.key,
      focusOffset: 1,
      marks: [{ type: 'bold' }]
    }]
  }
}

This would turn add a bold mark to the first character of the matched blocks.

This would also make implementing things like multiple-cursor rendering easier, since the "decorations" now map exactly to the existing selections/ranges logic.

cc @Soreine, how does this sound? I think it should solve #893 and GitbookIO/slate-prism#5.

@lxcid
Copy link
Collaborator

lxcid commented Oct 13, 2017

This is awesome!

I do have question about does it improve dynamic decorator? When force rerender based on state not directly related to the editor (e.g. search result highlighting)

@ianstormtaylor
Copy link
Owner Author

Hey @lxcid good question!

This PR itself doesn't support that, because the decorate logic in the schema is still based on node in the document themselves.

But! It does get us closer to being able to do things like that. I was thinking of adding a state.decorations property and a change.setDecorations(decorations) method that would allow you do to that. That way you could change the decorations in the editor as the outside information changes (eg. someone performs a search) and then things will re-render.

I haven't tested the performance of doing that for large editors, since you'll be re-decorating the entire document at once, but I'm sure there are things we can do to improve it.

const decs = decorations.filter((d) => {
const { startKey, endKey } = d
if (startKey == key || endKey == key) return true
const startsBefore = document.areDescendantsSorted(startKey, key)
Copy link
Collaborator

Choose a reason for hiding this comment

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

These calls can become quite expensive. But for a classic use of decorators, the decorations should be scoped to a few blocks. And if need be, there are ways to improve the performance of areDescendantsSorted, or maybe to process decorations upfront to get a list of individual text ranges instead (where startKey === endKey).
We don't want premature optimization anyway.

Copy link
Owner Author

Choose a reason for hiding this comment

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

I agree though, this can be improved.

One improvement I think would be a method that checks (before, key, after) or something about checking whether a node "overlaps" a range. Because here we have to do two separate checks which each iterate keys, etc. when really it could be handled in one go.

@@ -81,6 +73,9 @@ class Text extends React.Component {
if (p.node == pLast && n.node != nLast) return true
}

// Re-render if the current decorations have changed.
if (!n.decorations.equals(p.decorations)) return true
Copy link
Collaborator

Choose a reason for hiding this comment

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

Having reified decorations allows to run checks like these, which is great. It will probably fix a lot of "decorations not updating" issues.

@@ -165,19 +165,12 @@ class Node {
first = normalizeKey(first)
second = normalizeKey(second)

let sorted
const keys = this.getKeysAsArray()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, here is the optimization :D

if (!rule.decorate) return
if (!rule.match(object)) return

const decorations = rule.decorate(object)
Copy link
Collaborator

Choose a reason for hiding this comment

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

So the decorate calls themselves now live under a memoized function, which is nice :)

const a = getPoint(native.anchorNode, native.anchorOffset, state, editor)
const f = getPoint(native.focusNode, native.focusOffset, state, editor)
const a = findPoint(native.anchorNode, native.anchorOffset, state)
const f = findPoint(native.focusNode, native.focusOffset, state)
Copy link
Owner Author

@ianstormtaylor ianstormtaylor Oct 13, 2017

Choose a reason for hiding this comment

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

Actually any place where we are doing findPoint for both anchor and focus can be slightly improved by checking if collapsed and if so only doing it once. But I'm not sure this is a bottleneck, so leaving for later.

*/

getKeys() {
getKeysAsArray() {
Copy link
Owner Author

Choose a reason for hiding this comment

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

I think this method can also be improved in performance if it was recursive, such that each nested child's keys would be cached, instead of having to iterate the entire tree each time the document changes.

@ianstormtaylor
Copy link
Owner Author

Actually @lxcid I've just implemented that behavior that you're asking about! I added state.decorations, which can be set based on any outside information. Along with a simple search-highlighting example to show you how it works.

@ianstormtaylor ianstormtaylor merged commit e53cee3 into master Oct 13, 2017
@lxcid
Copy link
Collaborator

lxcid commented Oct 16, 2017

Wow! Thank for considering my request @ianstormtaylor and getting it in so soon! I can't wait to use it soon! The demo looks great!

@ianstormtaylor ianstormtaylor deleted the refactor-decorators branch October 26, 2017 00:34
z2devil pushed a commit to z2devil/slate that referenced this pull request Dec 6, 2024
* refactor decorations to use selections

* update docs

* cleanup

* add Selection.createList

* fix tests

* fix for nested blocks

* fix lint

* actually merge

* revert small change

* add state.decorations, with search example
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants