I have a demo of this integration mostly working here on Glitch.
But I have a problem in that selections in the CodeMirror nodeView aren’t stable.
tl;dr: The problem is that immediately after forwardSelection runs, CodeMirror modifies the DOM which causes prosemirror-view to observe the DOM change and reset the selection to the first character before the nodeView.
(Read on for details and demo).
Here’s a couple of screenshots to make it easier to visualize what we’re going for:
What actually happens:
This nearly works! forwardSelection runs properly and “notifies” ProseMirror of the CodeMirror selection. We can see this if we use the excellent prosemirror-dev-tools:
It seems that if you manage to click directly on the CodeMirror border before highlighting, the delegated selection count works and the tooltip is shown, but the location is off (look at the right side):
CodeMirror maintains a hidden div for the textarea, which is what prosemirror-view picks up with getSelection().
It turns out, if we hack in devtools and 1. make that div visible and 2. click on that textarea then we get the tooltip count and proper locations:
Notice pink “glitch” in the gif below, which is CodeMirror’s invisible textarea made visible. This is an unreliable hack and not a real solution.
… but this is totally a hack just to track down what’s going on and not a real answer to the problem.
The good news is that it seems that sharing selections between CodeMirror and ProseMirror are not fundamentally incompatible – there just seems to be a bug somewhere along the line wrt keeping them in sync.
Yes. I’ve tried ignoreMutation but the problem is that the flush() function in prosemirror-view tries to update the selection even if all mutations are filtered out.
Here’s the relevant function from flush() in prosemirror-view, but I’ve added my own comments to show my issue:
flush(mutations) {
if (!this.view.docView) return
// 1. We get the mutation records, so far so good
if (!mutations) mutations = this.observer.takeRecords()
if (this.queue.length) {
mutations = this.queue.concat(mutations)
this.queue.length = 0
}
// 2. sel will be set to the `textarea` for CodeMirror in this case
let sel = this.view.root.getSelection()
// 3. newSel will be true (we're changing the selection)
let newSel = !this.currentSelection.eq(sel) && hasSelection(this.view)
let from = -1, to = -1, typeOver = false
if (this.view.editable) {
for (let i = 0; i < mutations.length; i++) {
// 4. registerMutation _will_ filter out the mutation records
// because our nodeView is set to ignoreMutation, but
// it doesn't matter because below...
let result = this.registerMutation(mutations[i])
if (result) {
from = from < 0 ? result.from : Math.min(result.from, from)
to = to < 0 ? result.to : Math.max(result.to, to)
if (result.typeOver) typeOver = true
}
}
}
if (from > -1 || newSel) {
if (from > -1) this.view.docView.markDirty(from, to)
// 5. handleDOMChange is picked up by `readDOMChange` which
// changes our selection (the key problem)
this.handleDOMChange(from, to, typeOver)
if (this.view.docView.dirty) this.view.updateState(this.view.state)
// 6. we might hope that this call would "fix" the selection,
// but the `currentSelection` is already changed -- and the
// selections are now equal
else if (!this.currentSelection.eq(sel)) selectionToDOM(this.view)
}
}
Roughly speaking, it’s almost like selection is a special “mutation” that isn’t ignored because the observers will try to update the selection automatically.
The basic problem seems to be that readDOMChange tries to read the selection from the DOM, which is CodeMirror’s textarea.
Of course, when ProseMirror tries to find the position in selectionFromDOM the best it can do is find the head of the custom nodeView.
It almost seems that what we need is a hook within the custom nodeView to return the appropriate selection. E.g. modify selectionFromDOM to something like:
// ...
while (nearestDesc && !nearestDesc.node) nearestDesc = nearestDesc.parent
// add this
if( nearestDesc && nearestDesc.spec.selectionFromDOM) {
selection = nearestDesc.spec.selectionFromDOM()
} else
// current code below
if (nearestDesc && nearestDesc.node.isAtom && NodeSelection.isSelectable(nearestDesc.node) && nearestDesc.parent) {
let pos = nearestDesc.posBefore
selection = new NodeSelection(head == pos ? $head : doc.resolve(pos))
}
// ...
Then within the custom nodeView we can just specify selectionFromDOM to return the correct value.
What do you think about this idea? (Or am I doing something wrong earlier in the process?)
Thanks for diagnosing the issue. I agree that that’s a misbehavior.
I think the right thing to do would be to ignore DOM selection changes inside content that isn’t managed by ProseMirror itself, just like we ignore DOM changes.
What do you think about calling .ignoreMutation with a custom type of mutation record that has {type: "selection", target: nearestElementAroundSelection()} properties? That way, ignoreMutation methods that just check whether record.target is in some unmanaged sub-node will automatically do the right thing, and those that want to handle selections specially can do so by checking the type property.
It sounds like what you’re suggesting would work, but I’ll defer to you as to the solution as I don’t have a complete picture of the rest of the library.
Interestingly, I found that I could make it work even with the current code by making two small (but hacky) changes:
The “hack” here is reaching into domObserver and telling it to setCurSelection – this effectively “syncs” up the view of the DOM selection between CodeMirror and Prosemirror (down the line).
But for whatever reason, this didn’t completely fix the issue. I also had to:
Make the CodeMirror textarea containing div have a non-zero size:
(To be honest, I’m not sure why this matters or even works)
But together we do get the right interaction! A properly positioned hovering tooltip within CodeMirror!
I’m not proposing the above code as an actual solution, but rather I’m just sharing it to provide insight into the problematic interaction.
I think your suggestion of being able to explicitly ignore selection mutations within the library is a good one, as far as I can tell, though it is interesting that with the above changes we get synced selections nearly “automatically”.
I have what is probably a similar issue, where the CodeMirror cursor blinking (which modifies the DOM to show and hide the cursor div) causes the ProseMirror selection to move in and out of the node view containing the editor.
Might there be a branch implementing the RFC that could be tested to see if it solves this issue?