Hi! The project I’m currently working on has a pretty messy plugin for tracking changes and approving/rejecting insertions/deletions. The problem we’re now having is that, presumably after a ProseMirror update, when pressing backspace before a space character, ProseMirror is creating a ReplaceStep that replaces a bunch of words before it with the same ones but without the deleted character (instead of just replacing the deleted character with an empty Slice.)
Ideally, our code shouldn’t break in these circumstances, but is there a way to make ProseMirror do the more reasonable thing (just replacing the deleted character with an empty Slice)?
Do you have a way reproduce this? In principle, ProseMirror does a diff of the old and updated content and creates a minimal change, so I’m not sure how/when this would happen. Some IME systems (especially Android virtual keyboards) sometimes randomly delete a bunch of text and reinsert it, but those would only be visible if they end up dispatched as separate ProseMirror view updates, which the library tries to avoid, and which, from your mentioning of this being a single replace step, doesn’t seem to match what you describe.
I tried to make a minimal repro but I can’t get it to reproduce :')
However, I’ve boiled it down to something in the 1.23.6 update in prosemirror-view. It seems there was changes on how the DOM is detected to be dirty, and this codebase might have some of that, so I’m investigating.
Okay, I finally found what causes it – and no, it seems it’s not specific to that prosemirror-view version. Basically, when there’s newline (\n) characters inside text nodes inside paragraphs that are imported raw (not from a DOM), ProseMirror seems to try to remove them when editing it. This seems something ProseMirror could have better safety around. For example, it seems that this isn’t detected when calling Node.check(). At the very least, if ProseMirror had thrown a console.warn about it, it would’ve saved me a lot of time.
My next steps then are making sure that instead of having \n, they get split into separate paragraphs. (This happened because the ProseMirror JSON was generated by the backend)
Ugh, it seems that wasn’t what caused the backspace issue specifically, and I’m struggling to create a minimal reproduction. Back to prosemirror-view 1.23.5, I guess.
I believe it was something related to having \n in the ProseMirror JSON, which shouldn’t exist, and should have <br> (or equivalent in your schema) instead, as mentioned above. But I’m not sure, though.
For example, there is a content Some text<span> some mark</span>. Where span is any custom Mark. Removing the “t” to the left of the space that comes first inside the span will call step containing the text inside the Mark, but without the Mark and insert it again. It turns out that this Mark simply disappears.
Comparing changes with
let changes = ChangeSet.create(oldState.doc);
changes = changes.addSteps(tr.doc, tr.mapping.maps, undefined);
gives only deleted “t” and does not have information about reinserted marked text.
Anyway it is weird, and it is a nightmare to create track changes for this editor. Just another example: you have two spaces in a row, the second one is marked as deleted (e.g. wrapped with del tag), you try to delete the first space. As a result: step reinserts the second space and the text after it. Trying to replace this slice with my method mentioned above gives another side effect: changes says that I have deleted the second space, not the first one. Here comes some kind of optimization I have faced many times in prosemirror, like replacing “abc123” with “df4c15” gives changes like: delete “ab”, insert “df4”, leave c1, delete “23”, insert “5”. So I don’t have idea how to deal with this.
You mean you paste the second string over the first and you get multiple replace steps? I don’t think that’s even possible, in plain ProseMirror.
What’s still missing from this thread is a concrete example of how to reproduce this with vanilla ProseMirror. Until I have that, I am not really interested in all these complaints.
Alright, I tried to create a reproducible example on ProseMirror, but I couldn’t get it to work. It seems the issue lies somewhere in Tiptap. Here are two identical examples:
To observe the problem, place the cursor somewhere around “World” and start deleting characters. In the Tiptap version, when deleting a character to the left of a space, a collision occurs. The cause seems to be that ReplaceStep returns incorrect from and to positions, and the slice contains content that shouldn’t be there. The plugin code in track-changes.mjs with appendTransaction is absolutely identical. I’ll probably post this question in the Tiptap repository as well. If you have a chance to take a look, I’d greatly appreciate it.