Summary
- I used the CodeMirror NodeView provided by the Embedded Code Editor example as the basis for my own CodeMirror-based NodeView.
- While using the NodeView, I experienced some strange issues where text pasted from the clipboard would be pasted twice into CodeMirror, and cause many mysterious “position not defined” errors in the console.
- Through debugging, I tracked down the problem to the
forwardUpdate
method of the NodeView. - Carefully reading the code, the
forwardUpdate
implementation seems obviously incorrect to me. I changed the implementation to match my mental model of how ProseMirror works. - However, I’m unable to reproduce the paste bug in the Embedded Code Editor demo. To me, it seems like the demo should have the same issue. This has me questioning whether my mental model of ProseMirror is correct.
Details
I was originally using this implementation for forwardUpdate
. It is identical to the example from the docs, except for formatting and naming (and management of a preview pane).
// BEFORE -- causes duplication & errors
forwardUpdate(update: CV.ViewUpdate): void {
// ignore updates whenever codemirror editor is out of focus
if (this._updating || !this._codeMirror.hasFocus) return;
let offset = this._getPos() + 1, {main} = update.state.selection;
let selection =
TextSelection.create(
this._proseView.state.doc,
offset + main.from,
offset + main.to
);
if (update.docChanged || !this._proseView.state.selection.eq(selection)) {
let tr = this._proseView.state.tr.setSelection(selection);
update.changes.iterChanges((fromA, toA, fromB, toB, text) => {
if (text.length) {
tr.replaceWith(
offset + fromA,
offset + toA,
this._proseView.state.schema.text(text.toString())
);
} else {
tr.delete(offset + fromA, offset + toA)
}
offset += (toB - fromB) - (toA - fromA)
})
this._proseView.dispatch(tr)
}
}
While debugging, I noticed:
- Each paste event would cause two errors like
Uncaught RangeError: Position X out of range
- If I increased the total document length to be longer than X, the error would no longer occur
- The error itself originates from
TextSelection.create
– the given positions are larger than the total length of the current document.
Looking more closely at the forwardUpdate
implementation, it seems obviously incorrect to me:
-
forwardUpdate
is called by CodeMirror immediately after a ViewUpdate, and before the change is propagated to the ProseMirror document (in fact,forwardUpdate
is the function responsible for propagating those changes) - when
forwardUpdate
is called the CodeMirror selection will be at the end of the pasted text - the purpose of the
TextSelection.create
is to convert the current CodeMirror selection into a ProseMirror selection - however, the indicies passed to
TextSelection.create
here will always be invalid, because the ProseMirror document has not yet been updated with the pasted text. Before changing the selection, we should add the pasted text to the ProseMirror node, and map all the selection positions through the transaction to find their new locations.
In fact, the forwardUpdate
implementation looks so incorrect that I’m surprised it works at all in the demo. It has me questioning whether I actually understand ProseMirror!
Based on this reasoning, I changed the forwardUpdate
implementation to:
- first, apply all the CodeMirror changes to the ProseMirror document
- then, compute a new ProseMirror selection by mapping the positions through the document change transaction
The new implementation can be found in this diff. Reproduced below:
// AFTER
forwardUpdate(update: CV.ViewUpdate): void {
// ignore updates whenever codemirror editor is out of focus
if (this._updating || !this._codeMirror.hasFocus) return;
let codePos = this._getPos();
let tr = this._proseView.state.tr;
if (update.docChanged) {
// the +1 skips the code block opening token
let offset = codePos + 1;
update.changes.iterChanges((fromA, toA, fromB, toB, text) => {
if (text.length) {
tr.replaceWith(
offset + fromA,
offset + toA,
this._proseView.state.schema.text(text.toString())
);
} else {
tr.delete(offset + fromA, offset + toA)
}
offset += (toB - fromB) - (toA - fromA)
})
}
// update selection
let { main: codeMirrorSelection} = update.state.selection;
let codePosAfterTr = tr.mapping.map(codePos);
let mappedProseSelection = this._proseView.state.selection.map(tr.doc, tr.mapping);
let desiredProseSelection =
TextSelection.create(
tr.doc,
codePosAfterTr + 1 + codeMirrorSelection.from, // +1 skips code_block start token
codePosAfterTr + 1 + codeMirrorSelection.to // +1 skips code_block start token
);
if(!mappedProseSelection.eq(desiredProseSelection)) {
tr = tr.setSelection(desiredProseSelection);
}
this._proseView.dispatch(tr)
}
After this change, pasting works as expected.
Questions
So, I’m not really looking for specific debugging help, but rather answers to these questions:
- is my description of the mistake in the original
forwardUpdate
implementation accurate? Or is the original implementation correct, and I am misunderstanding something? - does my approach to fixing
forwardUpdate
seem generally correct? - if the original implementation is incorrect for the reason I say, can you think of any reason why the error is not reproducible in the demo?
- If my reasoning is correct, would you accept a PR to the Embedded Code Editor page with the fix?
Possibly Related Issues
- CodeMirror 6 focus bug question.
Version Info
Note that I am using slightly outdated versions of ProseMirror. I was relying on the now-removed Schema type parameter, and can’t upgrade until I find a workaround.
(was any behavior changed recently that might have caused the issue I’m experiencing?)
$ npm outdated
Package Current Wanted Latest
prosemirror-commands 1.1.9 1.5.1 1.5.1
prosemirror-gapcursor 1.1.5 1.3.1 1.3.1
prosemirror-history 1.1.3 1.3.0 1.3.0
prosemirror-inputrules 1.1.3 1.2.0 1.2.0
prosemirror-keymap 1.1.4 1.2.1 1.2.1
prosemirror-model 1.14.2 1.19.0 1.19.0
prosemirror-schema-basic 1.1.2 1.2.1 1.2.1
prosemirror-schema-list 1.1.4 1.2.2 1.2.2
prosemirror-state 1.3.4 1.4.2 1.4.2
prosemirror-suggest 0.7.6 0.7.6 2.0.3
prosemirror-tables 1.1.1 1.3.2 1.3.2
prosemirror-transform 1.3.2 1.7.1 1.7.1
prosemirror-view 1.18.7 1.30.2 1.30.2