Skip to content

feat: add BlockPathMap with incremental updates#2336

Draft
christianhg wants to merge 3 commits intomainfrom
feat/block-path-map-v2
Draft

feat: add BlockPathMap with incremental updates#2336
christianhg wants to merge 3 commits intomainfrom
feat/block-path-map-v2

Conversation

@christianhg
Copy link
Member

@christianhg christianhg commented Mar 6, 2026

This PR adds two foundational pieces for container support: a BlockPathMap for O(1) block lookup by key-path, and traversal utilities for navigating nested document structures.

BlockPathMap

A flat Map<string, number[]> that maps serialized key-paths to positional paths. For a block at value[1].rows[0].cells[2], the key-path is "table-key.row-key.cell-key" and the positional path is [1, 0, 2].

The map is built once on init and updated incrementally as Slate operations come in. Text and selection operations are zero-cost (no map update needed). Structural operations (insert, remove, split, merge, move) update only the affected entries and shift siblings.

Performance on a 5,100-entry document (100 containers x 50 children): build 42ms, lookup 0.003ms, insert 0.008ms, remove 0.006ms.

Traversal utilities

Twelve functions for navigating the document tree, exposed on EditorSnapshot:

Structural: getNode, getParent, getChildren, getNextSibling, getPrevSibling, getAncestors

Cursor-order: getNextBlock, getPrevBlock, getFirstLeaf, getLastLeaf

Convenience: isNested, getDepth, isDescendantOf, getContainingContainer

Containers are identified by a Set<string> on EditorContext - an editor concern, not a schema concern. getNextBlock/getPrevBlock treat containers as opaque cursor stops, returning the container itself rather than descending into it. Behavior authors use getFirstLeaf/getLastLeaf to explicitly enter a container.

Field discovery uses the existing schema.blockObjects field definitions with recursive of-definition resolution - a table's row type is found inside the table's field definitions, a cell inside the row's. No changes to @portabletext/schema.

What this enables

These are the building blocks for container navigation behaviors. A backspace behavior at the start of a text block can call getPrevBlock to find the preceding container, then decide whether to delete it or navigate into it. Arrow key behaviors can use getNextBlock/getPrevBlock for document-order movement and getFirstLeaf/getLastLeaf for container entry.

No public API impact beyond the additive containers: Set<string> on EditorContext. No changes to patches emitted. The test suite (129 new tests) validates both the map operations and all traversal patterns on deeply nested documents.

@changeset-bot
Copy link

changeset-bot bot commented Mar 6, 2026

🦋 Changeset detected

Latest commit: 2ea7685

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@portabletext/editor Minor
@portabletext/plugin-character-pair-decorator Major
@portabletext/plugin-emoji-picker Patch
@portabletext/plugin-input-rule Patch
@portabletext/plugin-markdown-shortcuts Major
@portabletext/plugin-one-line Major
@portabletext/plugin-paste-link Major
@portabletext/plugin-sdk-value Major
@portabletext/plugin-typeahead-picker Patch
@portabletext/plugin-typography Patch
@portabletext/toolbar Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Mar 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
portable-text-editor-documentation Ready Ready Preview, Comment Mar 9, 2026 4:29pm
portable-text-example-basic Ready Ready Preview, Comment Mar 9, 2026 4:29pm
portable-text-playground Ready Ready Preview, Comment Mar 9, 2026 4:29pm

Request Review

@github-actions
Copy link
Contributor

github-actions bot commented Mar 6, 2026

📦 Bundle Stats — @portabletext/editor

Compared against main (d312abd5)

@portabletext/editor

Metric Value vs main (d312abd)
Internal (raw) 812.2 KB +11.1 KB, +1.4%
Internal (gzip) 153.2 KB +2.9 KB, +1.9%
Bundled (raw) 1.42 MB +11.2 KB, +0.8%
Bundled (gzip) 316.2 KB +2.9 KB, +0.9%
Import time 103ms -0ms, -0.2%

@portabletext/editor/behaviors

Metric Value vs main (d312abd)
Internal (raw) 467 B -
Internal (gzip) 207 B -
Bundled (raw) 424 B -
Bundled (gzip) 171 B -
Import time 6ms +0ms, +0.0%

@portabletext/editor/plugins

Metric Value vs main (d312abd)
Internal (raw) 2.5 KB -
Internal (gzip) 910 B -
Bundled (raw) 2.3 KB -
Bundled (gzip) 839 B -
Import time 13ms +0ms, +2.8%

@portabletext/editor/selectors

Metric Value vs main (d312abd)
Internal (raw) 60.2 KB -
Internal (gzip) 9.4 KB -
Bundled (raw) 56.7 KB -
Bundled (gzip) 8.6 KB -
Import time 11ms +0ms, +0.6%

@portabletext/editor/utils

Metric Value vs main (d312abd)
Internal (raw) 24.2 KB -
Internal (gzip) 4.7 KB -
Bundled (raw) 22.2 KB -
Bundled (gzip) 4.4 KB -
Import time 10ms +0ms, +0.4%
Details
  • Import time regressions over 10% are flagged with ⚠️
  • Treemap artifacts are attached to the CI run for detailed size analysis
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

Containers are identified by a Set<string> on EditorContext, not the
schema package. Field discovery uses schema.blockObjects with recursive
of-definition resolution.

getNextBlock/getPrevBlock treat containers as opaque cursor stops.
getFirstLeaf/getLastLeaf provide explicit container entry.
BlockPathMap.rebuild() recurses into container fields when provided.
…th arrays

Add 11 tests for getFirstLeaf and getLastLeaf covering leaf blocks,
deep container traversal, intermediate containers, and mixed content.

Return a copy from blockPathMap.get() to prevent callers from seeing
stale data after in-place mutations from incremental updates.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant