Back to Plate

Slate v2 full-block delete fast path must stop at selection boundaries

docs/solutions/logic-errors/2026-05-09-slate-v2-full-block-delete-fast-path-must-stop-at-selection-boundaries.md

53.0.62.6 KB
Original Source

Slate v2 full-block delete fast path must stop at selection boundaries

Problem

The model-owned full-block delete fast path assumed that a selection from the start of one block to the start of another block meant "remove the first block." That is only true when the focus is at the immediate next sibling.

Symptoms

  • huge-document-cut expected block 2502 to shift into index 2500.
  • The browser actually showed old block 2501, proving only one block was removed.
  • A first local fix that disabled the one-block fast path for multi-block ranges left an empty heading, proving generic text deletion was also the wrong owner for full-block sibling selections.

What Didn't Work

  • Treating the assertion as a stale faker/text oracle. The expected and actual strings mapped exactly to source blocks 2502 and 2501.
  • Only narrowing the one-block fast path to adjacent siblings. That avoided the wrong one-block removal, but made the multi-block case fall through to text deletion and leave an empty start block.

Solution

Return all fully selected sibling block paths, then remove them in reverse path order:

ts
if (
  !Path.isSibling(blockPath, endBlockPath) ||
  !Path.isBefore(blockPath, endBlockPath)
) {
  return null
}

const paths: Path[] = []
let path = blockPath

while (!Path.equals(path, endBlockPath)) {
  paths.push(path)
  path = Path.next(path)
}

editor.update((tx) => {
  for (const blockPath of [...blockPaths].reverse()) {
    tx.nodes.remove({ at: blockPath })
  }
})

Why This Works

A start-to-start selection is exclusive of the focus block. For a selection from block N start to block N + 2 start, the fully selected sibling blocks are N and N + 1, not just N, and not text inside N.

Reverse removal preserves path correctness while deleting multiple siblings.

Prevention

  • For delete fast paths, classify selection geometry first: same block, adjacent sibling boundary, multi-sibling boundary, or partial text range.
  • Browser stress rows should assert the shifted block after cut, not just that a delete command trace happened.
  • When optimizing structural delete, include a multi-block start-to-start row; adjacent-only coverage misses this bug.