Skip to content

fix(open-knowledge): Backspace/Delete at a list-item boundary orphani…#613

Open
blokboy wants to merge 3 commits into
inkeep:mainfrom
blokboy:fix/609-list-backspace-delete-orphan
Open

fix(open-knowledge): Backspace/Delete at a list-item boundary orphani…#613
blokboy wants to merge 3 commits into
inkeep:mainfrom
blokboy:fix/609-list-backspace-delete-orphan

Conversation

@blokboy

@blokboy blokboy commented Jul 13, 2026

Copy link
Copy Markdown

What & why

Addressing Issue #609

How this was verified

bun run check followed by a manual evaluation of the behavior locally.

Checklist

  • Ran bun run check (lint, typecheck, tests) locally
  • Added a changeset (bun run changeset) if this changes behavior
  • Updated docs if this changes a user-facing surface
  • I have read CONTRIBUTING.md and agree to license my contribution under the project's terms (CLA)

After you open this PR

  • A maintainer will review your PR. If you don't hear back within a few business days, a comment to nudge is welcome.
  • When your change is accepted, this PR closes automatically — that's how it merges, and your authorship is preserved.

See CONTRIBUTING.md for details.

@CLAassistant

CLAassistant commented Jul 13, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

blokboy added 2 commits July 13, 2026 15:45
…ng rows

Merging list items via Backspace (start of item) or Delete (end of item)
routed through StarterKit's ListKeymap sub-extension, which assumes the
fragmented BulletList/OrderedList/TaskList/TaskItem schema this repo
replaced with a unified listItem node. Two of its branches misfire against
that schema: Backspace into a previous item that has a nested sublist
lifts the merged item clean out of the list into a bare, unmarked
paragraph, and Delete across a nested/top-level depth change re-nests the
merged item at the wrong depth and drops its checked attr — both read by
users as "orphan" list rows (inkeep#609).

ListItemNode now binds Backspace/Delete itself (priority 101, ahead of
ListKeymap's default 100) using prosemirror-commands' joinTextblockBackward/
joinTextblockForward, which descend through container nodes to the actual
textblocks and merge only those, falling back to joinBackward/joinForward
when there's no adjacent item to join into.
Two new list-keymap.e2e.ts cases exercise the exact orphan scenarios via
real Playwright keystrokes: Backspace merging into an item whose preceding
sibling has a nested sublist, and Delete merging across a nested/top-level
depth boundary. Testing locally also appears issue is resolved.
@blokboy
blokboy force-pushed the fix/609-list-backspace-delete-orphan branch from 7c4a03d to e4c01c3 Compare July 13, 2026 20:47
inkeep-oss-sync Bot pushed a commit that referenced this pull request Jul 14, 2026
…(#2630)

* test(open-knowledge): pin nested list-item boundary merge defects RED

Reproduces issue #609's remaining orphan source on a
mounted TipTap editor via real keydown dispatch: Backspace at the start of
an item after a sibling with a nested sublist lifts it to a bare paragraph
(orphan), and Delete at the end of a nested item before a shallower task
item re-nests it at the wrong depth and drops its checked attr. Covers
Mod-Backspace and Mod-Delete, ordered lists, and multi-paragraph items,
plus controls pinning flat merges, stock first-item lift, undoInputRule,
trailing-paragraph rejoin, empty-nested-item removal, ranged selections,
and suggestion-layer Enter/Tab precedence.

Test scenarios adapted from #613.

* fix(open-knowledge): merge nested list-item boundaries on Backspace/Delete

Upstream ListKeymap (StarterKit's @tiptap/extension-list sub-extension)
misfires in two branches at nested item boundaries, by upstream design
rather than schema wiring: Backspace at the start of an item whose
previous sibling has a nested sublist takes the liftListItem fallthrough
and mints a bare orphan paragraph, and Delete at the end of a nested item
before a shallower item takes the nextListIsHigher branch, re-nesting the
next item at the wrong depth and dropping its checked attr.

The new ListBoundaryMerge extension preempts exactly those two
configurations across all four bindings ListKeymap holds (Backspace,
Delete, and their Mod variants) using joinTextblockBackward and
joinTextblockForward, which merge only the adjacent textblocks and
preserve the surviving item's marker, depth, and checked attr. Every
other configuration falls through, so ListKeymap's good branches (flat
joins, first-item lift, undoInputRule, trailing-paragraph rejoin, empty
nested-item removal) are untouched. Preemption relies on registration
order after StarterKit at the default priority, so suggestion-layer
Enter/Tab precedence (precedent #48) is unaffected.

Schema snapshot regenerated: additive extensionOrder entry only.

Fixes #609. Supersedes and credits
#613 by blokboy (defect localization, the
joinTextblock repair mechanism, and the test scenarios are theirs).

* test(open-knowledge): pin nested boundary merges in e2e and add changeset

Extends list-keymap.e2e.ts with the two nested-boundary scenarios,
asserting persisted Y.Text bytes through the full CRDT bridge, including
the checkbox-preservation assertion strong enough to catch the pre-fix
indented plain-bullet corruption. Changeset marks a patch release.

Test scenarios adapted from #613.

* chore(open-knowledge): drop decision markers from test docblock

md-audit comment-discipline flags spec decision markers in source
comments; the docblock now names the two failure shapes in prose.

* chore(open-knowledge): refresh ng-anchors catalog test file count

The catalog freshness gate deep-equals a fresh enumeration, which counts
test files; the new list boundary merge unit suite changed the count.

* test(open-knowledge): cover 3-level nesting and explain recursive scan

Review follow-up: adds Backspace/Delete merge tests at three nesting
levels so the depth-traversal loops are exercised beyond one iteration,
and documents why itemContainsSublist scans descendants rather than
direct children (it must mirror upstream ListKeymap's descendants-based
listItemHasSubList so the guard fires exactly when the upstream branch
misfires, including listItem > blockquote > list shapes).

* test(open-knowledge): anchor merge-target checkbox and ranged control

Review follow-up: the Backspace merge tests now pin the merge target's
checkbox, marker, and nested indent with anchored regexes matching the
Delete-side rigor, and the ranged-selection control also asserts the
trailing item survives without orphaning.

* test(open-knowledge): pin depth in the 3-level Delete merge test

Review follow-up: the 3-level Delete test now anchors the merged item at
level-3 indentation and guards against the one-level-too-shallow form,
matching the 2-level depth rigor.

* test(open-knowledge): pin 3-level Backspace depth and edge merge targets

Review follow-up. The 3-level Backspace test now anchors the merged item
at level-3 indentation. Two new pins cover the owned configuration's
unusual targets: a code block as the deepest preceding textblock (the
merge lands inside it, no fallthrough to the upstream lift) and a fresh
autoformatted empty item at the nested boundary (dissolves into the list
rather than minting an empty orphan paragraph; the undoInputRule-first
call in the handler keeps upstream ordering whenever the undoable state
is live, which the app's trailing appended transaction typically
consumes).

---------

GitOrigin-RevId: 465e670a9205ab14c8d24cae3a4f69029c73bbce
inkeep-oss-sync Bot pushed a commit that referenced this pull request Jul 14, 2026
…(#2630)

* test(open-knowledge): pin nested list-item boundary merge defects RED

Reproduces issue #609's remaining orphan source on a
mounted TipTap editor via real keydown dispatch: Backspace at the start of
an item after a sibling with a nested sublist lifts it to a bare paragraph
(orphan), and Delete at the end of a nested item before a shallower task
item re-nests it at the wrong depth and drops its checked attr. Covers
Mod-Backspace and Mod-Delete, ordered lists, and multi-paragraph items,
plus controls pinning flat merges, stock first-item lift, undoInputRule,
trailing-paragraph rejoin, empty-nested-item removal, ranged selections,
and suggestion-layer Enter/Tab precedence.

Test scenarios adapted from #613.

* fix(open-knowledge): merge nested list-item boundaries on Backspace/Delete

Upstream ListKeymap (StarterKit's @tiptap/extension-list sub-extension)
misfires in two branches at nested item boundaries, by upstream design
rather than schema wiring: Backspace at the start of an item whose
previous sibling has a nested sublist takes the liftListItem fallthrough
and mints a bare orphan paragraph, and Delete at the end of a nested item
before a shallower item takes the nextListIsHigher branch, re-nesting the
next item at the wrong depth and dropping its checked attr.

The new ListBoundaryMerge extension preempts exactly those two
configurations across all four bindings ListKeymap holds (Backspace,
Delete, and their Mod variants) using joinTextblockBackward and
joinTextblockForward, which merge only the adjacent textblocks and
preserve the surviving item's marker, depth, and checked attr. Every
other configuration falls through, so ListKeymap's good branches (flat
joins, first-item lift, undoInputRule, trailing-paragraph rejoin, empty
nested-item removal) are untouched. Preemption relies on registration
order after StarterKit at the default priority, so suggestion-layer
Enter/Tab precedence (precedent #48) is unaffected.

Schema snapshot regenerated: additive extensionOrder entry only.

Fixes #609. Supersedes and credits
#613 by blokboy (defect localization, the
joinTextblock repair mechanism, and the test scenarios are theirs).

* test(open-knowledge): pin nested boundary merges in e2e and add changeset

Extends list-keymap.e2e.ts with the two nested-boundary scenarios,
asserting persisted Y.Text bytes through the full CRDT bridge, including
the checkbox-preservation assertion strong enough to catch the pre-fix
indented plain-bullet corruption. Changeset marks a patch release.

Test scenarios adapted from #613.

* chore(open-knowledge): drop decision markers from test docblock

md-audit comment-discipline flags spec decision markers in source
comments; the docblock now names the two failure shapes in prose.

* chore(open-knowledge): refresh ng-anchors catalog test file count

The catalog freshness gate deep-equals a fresh enumeration, which counts
test files; the new list boundary merge unit suite changed the count.

* test(open-knowledge): cover 3-level nesting and explain recursive scan

Review follow-up: adds Backspace/Delete merge tests at three nesting
levels so the depth-traversal loops are exercised beyond one iteration,
and documents why itemContainsSublist scans descendants rather than
direct children (it must mirror upstream ListKeymap's descendants-based
listItemHasSubList so the guard fires exactly when the upstream branch
misfires, including listItem > blockquote > list shapes).

* test(open-knowledge): anchor merge-target checkbox and ranged control

Review follow-up: the Backspace merge tests now pin the merge target's
checkbox, marker, and nested indent with anchored regexes matching the
Delete-side rigor, and the ranged-selection control also asserts the
trailing item survives without orphaning.

* test(open-knowledge): pin depth in the 3-level Delete merge test

Review follow-up: the 3-level Delete test now anchors the merged item at
level-3 indentation and guards against the one-level-too-shallow form,
matching the 2-level depth rigor.

* test(open-knowledge): pin 3-level Backspace depth and edge merge targets

Review follow-up. The 3-level Backspace test now anchors the merged item
at level-3 indentation. Two new pins cover the owned configuration's
unusual targets: a code block as the deepest preceding textblock (the
merge lands inside it, no fallthrough to the upstream lift) and a fresh
autoformatted empty item at the nested boundary (dissolves into the list
rather than minting an empty orphan paragraph; the undoInputRule-first
call in the handler keeps upstream ordering whenever the undoable state
is live, which the app's trailing appended transaction typically
consumes).

---------

GitOrigin-RevId: 465e670a9205ab14c8d24cae3a4f69029c73bbce
inkeep-oss-sync Bot pushed a commit that referenced this pull request Jul 14, 2026
…(#2630)

* test(open-knowledge): pin nested list-item boundary merge defects RED

Reproduces issue #609's remaining orphan source on a
mounted TipTap editor via real keydown dispatch: Backspace at the start of
an item after a sibling with a nested sublist lifts it to a bare paragraph
(orphan), and Delete at the end of a nested item before a shallower task
item re-nests it at the wrong depth and drops its checked attr. Covers
Mod-Backspace and Mod-Delete, ordered lists, and multi-paragraph items,
plus controls pinning flat merges, stock first-item lift, undoInputRule,
trailing-paragraph rejoin, empty-nested-item removal, ranged selections,
and suggestion-layer Enter/Tab precedence.

Test scenarios adapted from #613.

* fix(open-knowledge): merge nested list-item boundaries on Backspace/Delete

Upstream ListKeymap (StarterKit's @tiptap/extension-list sub-extension)
misfires in two branches at nested item boundaries, by upstream design
rather than schema wiring: Backspace at the start of an item whose
previous sibling has a nested sublist takes the liftListItem fallthrough
and mints a bare orphan paragraph, and Delete at the end of a nested item
before a shallower item takes the nextListIsHigher branch, re-nesting the
next item at the wrong depth and dropping its checked attr.

The new ListBoundaryMerge extension preempts exactly those two
configurations across all four bindings ListKeymap holds (Backspace,
Delete, and their Mod variants) using joinTextblockBackward and
joinTextblockForward, which merge only the adjacent textblocks and
preserve the surviving item's marker, depth, and checked attr. Every
other configuration falls through, so ListKeymap's good branches (flat
joins, first-item lift, undoInputRule, trailing-paragraph rejoin, empty
nested-item removal) are untouched. Preemption relies on registration
order after StarterKit at the default priority, so suggestion-layer
Enter/Tab precedence (precedent #48) is unaffected.

Schema snapshot regenerated: additive extensionOrder entry only.

Fixes #609. Supersedes and credits
#613 by blokboy (defect localization, the
joinTextblock repair mechanism, and the test scenarios are theirs).

* test(open-knowledge): pin nested boundary merges in e2e and add changeset

Extends list-keymap.e2e.ts with the two nested-boundary scenarios,
asserting persisted Y.Text bytes through the full CRDT bridge, including
the checkbox-preservation assertion strong enough to catch the pre-fix
indented plain-bullet corruption. Changeset marks a patch release.

Test scenarios adapted from #613.

* chore(open-knowledge): drop decision markers from test docblock

md-audit comment-discipline flags spec decision markers in source
comments; the docblock now names the two failure shapes in prose.

* chore(open-knowledge): refresh ng-anchors catalog test file count

The catalog freshness gate deep-equals a fresh enumeration, which counts
test files; the new list boundary merge unit suite changed the count.

* test(open-knowledge): cover 3-level nesting and explain recursive scan

Review follow-up: adds Backspace/Delete merge tests at three nesting
levels so the depth-traversal loops are exercised beyond one iteration,
and documents why itemContainsSublist scans descendants rather than
direct children (it must mirror upstream ListKeymap's descendants-based
listItemHasSubList so the guard fires exactly when the upstream branch
misfires, including listItem > blockquote > list shapes).

* test(open-knowledge): anchor merge-target checkbox and ranged control

Review follow-up: the Backspace merge tests now pin the merge target's
checkbox, marker, and nested indent with anchored regexes matching the
Delete-side rigor, and the ranged-selection control also asserts the
trailing item survives without orphaning.

* test(open-knowledge): pin depth in the 3-level Delete merge test

Review follow-up: the 3-level Delete test now anchors the merged item at
level-3 indentation and guards against the one-level-too-shallow form,
matching the 2-level depth rigor.

* test(open-knowledge): pin 3-level Backspace depth and edge merge targets

Review follow-up. The 3-level Backspace test now anchors the merged item
at level-3 indentation. Two new pins cover the owned configuration's
unusual targets: a code block as the deepest preceding textblock (the
merge lands inside it, no fallthrough to the upstream lift) and a fresh
autoformatted empty item at the nested boundary (dissolves into the list
rather than minting an empty orphan paragraph; the undoInputRule-first
call in the handler keeps upstream ordering whenever the undoable state
is live, which the app's trailing appended transaction typically
consumes).

---------

GitOrigin-RevId: 465e670a9205ab14c8d24cae3a4f69029c73bbce
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.

2 participants