Resolved Pos node is not the same as doc.nodeAt()

I’m facing a strange case where the resolved pos node is not the same as doc.nodeAt(), I m assuming they should be the same, here’s the details: My document:

Node: topic, Pos: 0
Node: title, Pos: 1
Node: text, Pos: 2
Node: body, Pos: 14
Node: section, Pos: 15
Node: p, Pos: 16
Node: text, Pos: 17
Node: p, Pos: 35
Node: p, Pos: 37
Node: text, Pos: 38

The cursor is placed in Pos 35 and here’s the proof

state.selection.from is 35
state.selection.to is 35
state.selection.anchor is 35

So I m expecting the resolvedPos.node() to return the p node at Pos 35, but this is what I m getting:

state.selection.$anchor.node().type.name comes out to 'section'

and If try to use doc.nodeAt it will return the expected node

state.doc.nodeAt(35).type.name comes out to 'p'

if I replaced the hard coded 35 with state.selection.anchor the results will be the same.

Is this a bug? or this is by design, due to this issue I m having some hard time telling if the cursor is at the end of line or not and also getting it’s parent.

I asked one the LLM and this was the answer provided:

Why This Happens

In ProseMirror, empty nodes (like an empty p node) don’t occupy a distinct position in the document tree. Instead, the cursor is considered to be directly inside the parent node.

When you call $pos.node(depth), ProseMirror skips empty nodes and returns the nearest non-empty ancestor.

Is this the case?

That is nonsense. Don’t trust LLMs to answer subtle questions, I guess.

You say your cursor is at position 35. Since regular cursors can only be at inline positions (assuming you aren’t talking about some custom selection type), I’m going to assume that position 35 is inside a paragraph. nodeAt returns the node that starts right at the given position. So I suspect your initial node position listing is off somehow. Can you show an actual representation of the document? (Such as state.doc.toString().)

(assuming you aren’t talking about some custom selection type)

Yes, nothing of the sort, here’s a representation of the document in that state

'doc(topic(title("Test File 2"), body(section(p("A test paragraph."), p, p("another paragraph")))))'

With that document, your list of positions is indeed correct, and position 35 is before the empty paragraph. Which isn’t a selection that ProseMirror will normally create. Could it be that your own code created that selection?

In any case, the node() accessor on a resolved position will return the node around that position, not the node after it. So section is absolutely the expected return value for the expression you provided.

Yes, the selection was created by my own code, after I create the new node.

// get the new cursor position
  const side = (!$from.parentOffset && $to.index() < parent.childCount ? $from : $to).pos + 1;
  const transaction = state.tr.insert(side, createNode(newNodeType, {}));
  // select the new node
  transaction.setSelection(TextSelection.create(transaction.doc, side));
// Apply the transaction
  return dispatch(transaction);

Okay, that explains that, then. If you’re inserting a textblock node at position P, the cursor position inside it would be P + 1.

1 Like

So this issue originated from our schema marking some nodes are inline, the text selection was correct.