Decoration Mapping Through Custom Replace Step Behavior

Hello, I’m having trouble with mapping a decorationSet through a custom step. I created a step that extends ReplaceStep, where I replace a paragraph node with another node that has per word nodes. This is done by iterating through the textContent of the paragraph and then creating new nodes that wrap each word, and then doing the paragraph node replacement. I went this route because I found that the performance of creating a ReplaceAroundStep for each word, or a ReplaceAroundStep and then doing tr.split was worse for long paragraphs. Below is a visualization of the step.

<p>Some text here </p>

The <p> paragraph gets replaced with a new paragraph <np> that contains w>'s

<np><w>Some </w><w>text </w><w>here </w></np> 

If I used the regular replace step and I had decorations in <p>, they would get dropped when they’re mapped through the regular replace step. Each decoration needs to be mapped by the number of node tokens added to each word. I provide this information in my custom step by overriding the getMap function to create the StepMap and passing it the ranges that describe the addition of the node tokens for each word. In this case, the step map is created with the following ranges: [1, 0, 1], [6, 0, 2], [11, 0, 2], [16, 0, 1]. I also created an inverse step that does the opposite, replacing <np> with <p>, which similarly has a set of ranges that remove the node tokens in the StepMap.

Problem: When I try to map a decoration set through a transaction that contains a StepMap with a range list like this one, the mapping is inconsistent and decorations are dropped/mapped wrong after going through several of these steps.

I can verify that my step ranges are correct because I’ve replaced the in my apply of the Plugin with for each Decoration.from and in decorationSet.find() to create a new DecorationSet. The decorations always get mapped correctly with this patch fix, but this is less efficient since it maps every decoration position rather than using the tree structure of the set. I’m not sure if the problem is in how decorationSets behave with a long range list (>2) or if there is something else I need to add to my new replace step. Apologies for the long post!


None of the built-in steps create mappings like this, so there might be undiscovered bug there. Can you set up a minimal script that reproduces the issue?

Here’s a Glitch link with a minimal editor that implements the custom step. The setup has a paragraph in the first line and a “wordParagraph” with word nodes in the second line. Please see the schema and the enhanceTr and compressTr for more information about how the ranges are used in the custom step. You can see the rendering taking place if you inspect the elements in Chrome DevTools.

To reproduce the issue:

  1. Type “text here” after “text” in the second line (the word paragraph)
  2. The second line should now read “Some text text here here”
  3. Click the Render button
  4. Note that the last decoration is dropped

In contrast, refresh the page to reset the editor, and then repeat only step 1 & 2 and click on the Render with kludge map button. Note that the decoration is correctly mapped. I verified that the regular mapping drops the decoration by checking the decorationSet in the plugin before and after the mapping.

Interestingly, the regular mapping works fine without any typing first, but it starts incorrectly mapping only after a certain number of insertions at certain places. The reproduction steps above show the simplest case I found. However, the “kludge mapping” works fine with any edit and render. In the full implementation I’m working with, the editor will enhance a paragraph before any edits and compress the previous wordParagraph, so this rendering happens quite frequently.

1 Like

I’ve pushed a patch that should help with this.

But a word of caution—if your actual implementation also stores precreated step map ranges in your steps, that will break very much as soon as the steps are rebased (during collaborative editing or when using addToHistory: false transactions and then undoing), because you’ll still be publishing the outdated map locations from the mapped change. (Of course, the class in the glitch didn’t override map at all, so it’d degrade to a plain replace step on mapping, which would sidestep this issue.)

Thank you for looking into it! I tried the patch, and while the behavior is better, there still seems to be some mapping issues after some edits. I’d be happy to get a set of reproducible steps, but the Glitch still has the old prosemirror-view package. I have it set up based off the examples on the website, where it’s using the require function you added that bundles the packages. Is there a way for me to update that, or do you need to update its sources? If there’s another way that you prefer to view a minimal script I could do that as well.

And thanks for the heads up! I think we’ll need to override the map function to then map the ranges on the Step correct?

Ah, thanks. The website rebuild process was broken. I’ve restarted it—the website should reflect the current version again now.

I had replied thinking that the problem was actually fixed, but unfortunately I’m still seeing some decorations dropped during certain actions (hence the deleted post). I updated the Glitch to now show the new dropped decoration behavior. The following steps demonstrate how this happens.

Reproduction Steps

  1. Click in the middle of the first word “Mollit”
  2. Click the Insert Word in WordParagraph button
  3. Click on the Render button
  4. Note that the second decoration is now missing
  5. If you click on Render again, you can see that the decoration wasn’t actually missing, but that it was mapped incorrectly and reappears at a different position

Comparison to Manual Position Mapping:

To compare against the kludge mapping which still works normally, refresh the page to reset the editor and repeat steps 1 and 2, then click on Render with kludge button. Note that the decorations are mapped correctly.

For more context regarding how the word insertion works, you can read its implementation on L96. Please let me know if you need any more information. This required quite some specific set up to be reproducible here since the demo is very barebones, so apologies for its clumsiness. In our implementation the incorrect mapping is still happening regularly.

1 Like

That’s not happening for me.

It would help a lot if you could…

  • Demonstrate the problem with a direct call to, without involving any editor views or plugins.
  • Simplify the inputs required to create the problem as much as possible, so that that boring task doesn’t fall on me.