Prosemirror-gapcursor at start and end of doc

From the description of the module, it seems that gapcursors are allowed at the beginning and end of the topmost document:

This is a plugin that adds a type of selection for focusing places that don’t allow regular selection (such as positions that have a leaf block node, table, or the end of the document both before and after them).

However in the prosemirror basic setup, when the only content is a codeblock, the cursor is trapped within that node: pressing the arrow keys results in selections at beginning or end of only the codeblock, not of the parent document as well.

What is the correct behavior of prosemirror-gapcursor in this scenario? Are gapcursors at the beginning and end of the document allowed?

Walking through the source code, the reason no gapcursor is created for the beginning and end of the document is because of line 53, where null is returned because there is no child before or after the codeblock, and because the the depth is 0 at the top most node.

      for (let d = $pos.depth;; d--) {
        let parent = $pos.node(d)
        if (dir > 0 ? $pos.indexAfter(d) < parent.childCount : $pos.index(d) > 0) {
          next = parent.child(dir > 0 ? $pos.indexAfter(d) : $pos.index(d) - 1)
          break
        } else if (d == 0) { // line 53
          return null
        }
        pos += dir
        let $cur = $pos.doc.resolve(pos)
        if (GapCursor.valid($cur)) return $cur
      }

A seemingly simple fix that would allow gapcursors at the beginning and end would be to modify prosemirror-gapcursor/src/gapcursor.js to return $pos instead of null, but I haven’t tested this extensively. Edit: this works with codeblocks, but not with blockquotes perhaps because the former has as its content “text*” and the latter has “blocks+”.

        } else if (d == 0) { // line 53
          return $pos
        }
1 Like

What is the correct behavior of prosemirror-gapcursor in this scenario? Are gapcursors at the beginning and end of the document allowed?

Yes, but only if there’s a block leaf node at the start or end of the document. The scenario with the code block is similar to a scenario where you just have a single paragraph—that also won’t allow a gap cursor.

(The way to escape the single-code-block situation would be shift-enter to create a paragraph after it, and then, if necessary, possibly change the type of the code block to some other block node.)

The scenario with the code block is similar to a scenario where you just have a single paragraph—that also won’t allow a gap cursor.

I feel like the key difference here is: with paragraphs (and headings), you’re not trapped within the node because at the start or end of paragraphs, pressing enter with the baseKeymap creates a paragraph nearby; while with codeblocks, pressing enter results in a newline within the codeblock, so that you’re still inside the same node.

The "Mod-Enter": exitCode helps with exiting codeblocks as you’ve mentioned, but only works in creating paragraph below and not above.


Yes, but only if there’s a block leaf node at the start or end of the document.

The programming logic for this is within closedBefore and closedAfter right? With

if ((x.childCount == 0 && !x.inlineContent) || x.isAtom || x.type.spec.isolating) return true
if (x.inlineContent) return false

Is it possible to modify these 2 (+ 2) lines to include x.type.spec.allowGapCursor to be considered

if ((x.childCount == 0 && !x.inlineContent) || x.isAtom || x.type.spec.isolating || x.type.spec.allowGapCursor) return true
if (x.inlineContent) return false

so that within the code_block spec, to allow gap cursors we would just write

  get schema(): NodeSpec {
    return {
      content: "text*",
      marks: "",
      group: "block",
      code: true,
      defining: true,
      allowGapCursor: true,
      parseDOM: [{tag: "pre", preserveWhitespace: "full"}],
      toDOM(node: PMNode) { return ["pre", ["code", 0]] },
    }
  }

or does that not make sense with the rest of ProseMirror?

The reason to check x.type.spec.allowGapCursor is because within the GapCursor.valid method, it short-circuits on the third line with closedBefore or closedAfter being true. So even if we set the parent spec (in this case, doc) to allowGapCursor=true, no gapcursor would be created.

  GapCursor.valid = function valid ($pos) {
    var parent = $pos.parent;
    if (parent.isTextblock || !closedBefore($pos) || !closedAfter($pos)) { return false }
    var override = parent.type.spec.allowGapCursor;
    if (override != null) { return override }
    var deflt = parent.contentMatchAt($pos.index()).defaultType;
    return deflt && deflt.isTextblock
  };

That approach doesn’t seem to align very well with what gap cursors currently are—they are defined as occurring on the block level, so a code block (which has inline content) can’t contain them. But the allowGapCursor configuration is about whether a given node can contain gap cursors. I’m not really comfortable further blurring this, because it is already pretty subtle and error-prone (there have been bugs with accidental gap cursors appearing in the wrong places before).