Skip to content

Table drag: view.tablePos is never remapped through tr.mapping, throws on a concurrent edit mid-drag #2921

Description

@mustafa-yilmaz

Summary

While building a table-drag visual-feedback example (#2920), a reviewer asked about behavior when two users edit a table at the same time. Investigating turned up a crash bug in TableHandlesExtension itself, unrelated to that PR's own code.

Root cause

In packages/core/src/extensions/TableHandles/TableHandles.ts, TableHandlesView.tablePos is set once per mousemove:

this.tablePos = pmNodeInfo.posBeforeNode + 1;

It's then used directly, without ever being remapped through tr.mapping, in several places - notably the drop-cursor decorations():

const tableResolvedPos = state.doc.resolve(view.tablePos + 1);

mousemove doesn't fire on the dragged-over element while a native HTML5 drag is in progress, so tablePos (and the state.block content snapshot used later by dropHandler) can't refresh during a drag. If any transaction changes the document elsewhere while a drag is active - a concurrent local edit, or another collaborator's change over Yjs - tablePos goes stale.

Reproduction

  1. Start dragging a table row (mousedown on the row handle, then move over another row without releasing).
  2. While the drag is still in progress, dispatch any transaction that changes the document before the table (e.g. editor._tiptapEditor.view.dispatch(editor._tiptapEditor.view.state.tr.insertText("X", 1, 1)) to insert into a heading above the table).
  3. The next dragover recomputes the drop-cursor decoration from the now-stale tablePos, which resolves into the wrong node and throws:
RangeError: Index 1 out of range for <"...heading text...">
  at posAtIndex (prosemirror-model)
  at TableHandles.ts:695 (decorations)

This throws out of viewDecorationsupdateStateInnerdispatchTransaction, i.e. it breaks the transaction dispatch for the other user's edit, not just the drag.

Suggested fix

Same pattern already used correctly elsewhere for stored positions across transactions: remap tablePos (and any other stored position/snapshot used mid-drag) through tr.mapping in an appendTransaction/plugin apply, rather than only refreshing it on mousemove. Happy to open a PR for this if useful, but wanted to report it as its own issue since it's unrelated to the table-drag-visualization example in #2920 where it was found.

Environment

Found against main while working from a local build of packages/core/packages/react/packages/mantine.


Update — after #2920 (225c1ccf4)

Recording where this stands, since #2920 changes the symptom but not the cause.

The crash path is now guarded. #2920 moves the drag decoration build into getTableDragDecorations and calls it inside a single try/catch in the plugin's decorations prop, covering every resolve/posAtIndex in both the highlight and drop-cursor branches. It also verifies the resolved node is actually a table before indexing into it. With that in place the reproduction above no longer throws — the decorations are skipped for that state, so the other user's transaction dispatches normally instead of dispatchTransaction blowing up.

What this issue still covers, unchanged:

  • view.tablePos is captured on mousemove and never remapped through tr.mapping.
  • view.state.block is a snapshot taken at the same moment, and dropHandler still works from it.

So a concurrent edit mid-drag can still land a wrong drop. After #2920 this stops being a crash and becomes a silent correctness bug — arguably harder to notice, which is an argument for fixing it properly rather than leaving it at the guard.

The suggested fix is unchanged: remap the stored position through tr.mapping and refresh the snapshot, rather than only refreshing on mousemove.

Note the stack trace above cites TableHandles.ts:695; that line number predates #2920 and has since moved.

Per @nperez0111's comment below, this will come as its own PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug:P3Medium: Noticeable but non-blocking issues.

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions