Confused about the "structure" option on ReplaceStep / ReplaceAroundStep

Hey!

Was reading through the documentation and came across:

https://prosemirror.net/docs/ref/#transform.ReplaceStep

new ReplaceStep(from: number, to: number, slice: Slice, structure: ?⁠bool)

The given slice should fit the ‘gap’ between from and to—the depths must line up, and the surrounding nodes must be able to be joined with the open sides of the slice. When structure is true, the step will fail if the content between from and to is not just a sequence of closing and then opening tokens (this is to guard against rebased replace steps overwriting something they weren’t supposed to).

I’m unclear what you mean by “sequence of closing and then opening tokens” - could you clear this up? :slight_smile:

Bonus thanks if you can give a specific example of when this might occur!

I believe it’s a safe-guard to protect data from being accidentally deleted by bugs in the implementation. When true, it asserts that no text or leaf nodes will be removed when the step is applied. The term token refers to the counting system used to address positions in a document. Recall that you calculate a position in a document by counting the number of tokens that precede the location.

An example of a ReplaceStep that would have structure=true would be merging two adjacent paragraphs, where you turn <p>hello</p><p>world</p> into <p>helloworld</p>, by deleting </p><p>.

I’ve actually had errors in production where the safe-guard triggered and threw an error when it detects that content was going to be removed when it shouldn’t be.

This flag is used when, for example, joining two paragraphs. That deletes the closing token at the end of the first paragraph, and the opening token of the next paragraph. So, say that the paragraph boundary lies at position 10, this would be a step that replaces 9 to 11 with nothing.

In principle, when remapping such a step, which the undo history and collaborative editing will do in various circumstances, it might end up being remapped over a step that inserts a third paragraph in between those two paragraphs. If that paragraph is 20 tokens long, the remapped step will be “replace 9 to 31 with nothing”, and applying it would delete the newly inserted paragraph, even though the original intention was a join, not a deletion.

Thus, this flag is an extra check to avoid remapped versions of steps like these from overwriting content that they should not. It is definitely a bit ad-hoc and non-general, but it covers a bunch of potential issues and I haven’t found a better way to express it (without creating additional step classes).