Skip to content

Extend paste-into-cell design note to cover #333 - #597

Merged
hackerwins merged 5 commits into
wafflebase:mainfrom
semimikoh:fix/nested-table-paste-input-routing
Jul 31, 2026
Merged

hackerwins merged 5 commits into
wafflebase:mainfrom
semimikoh:fix/nested-table-paste-input-routing

Conversation

@semimikoh

@semimikoh semimikoh commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Design note for issue #333: pasting a table that contains nested tables
misroutes input on the pasted copy — the deepest nested level rejects the
caret outright, and every level above it visually accepts the caret but
routes typed characters into the corresponding cell of the original
table instead.

This turns out to share its root cause and its fix with #528 (already
merged, PR #531): the multi-block paste path's fresh-id regeneration only
covered a cloned block's own top-level id, not ids nested inside its table
cells, so a pasted nested table's inner cell blocks kept the source's ids.
Rather than opening a new design doc, this folds #333 into the existing
docs-paste-table-into-cell.md note per docs/design/README.md's guidance
to fold single-PR notes into the relevant doc when they land on the same
code path.

This is the design-note-first step (no code yet) — opening for
direction before pushing the implementation. Implementation is already
drafted locally and will follow on this same branch.

Why

Doc.getBlock / findBlockInCells resolved the shared ids back to the
source table for every level above the deepest (visually-correct caret,
wrong input target); the deepest level's cell wasn't in _blockParentMap
yet (only rebuilt at the next layout pass) and so couldn't resolve at all,
which is why it rejected the caret outright.

The note proposes: use cloneBlockWithFreshIds (already used for the
single-table paste branch) for the multi-block branch's middle-block clones
too — its refreshBlockIds already recurses through
tableData.rows[].cells[].blocks[], so this alone fixes the id-sharing —
plus a walkCellsForBlock recursive fallback in findBlockInCells for
blocks not yet in _blockParentMap (e.g. the paste's own middle blocks).

Test plan

No code changes yet — design note only. The note's Testing Strategy covers
the implementation that will follow: a model-level regression test
(Doc.getBlock resolves a block inside a freshly pasted nested table that
is not yet in _blockParentMap), plus manual re-verification of #333's
2-level and 3-level repro steps in the packages/docs standalone demo.

Notes for Reviewers

AI tools assisted with this PR: the root-cause investigation (tracing the
id-sharing bug through cloneBlockWithFreshIds and findBlockInCells) and
the note draft were produced with Claude Code (Claude Sonnet 5). I reviewed
the root-cause analysis and every line of the note.

Fixes #333.

Summary by CodeRabbit

  • Bug Fixes
    • Improved paste-into-table-cell behavior for nested tables and multi-block clipboard content, preventing misrouting and crashes.
    • Enhanced deep block lookup after paste to avoid identifier conflicts and ensure the correct content is targeted.
  • Documentation
    • Updated the paste-into-cell design documentation and added a nested-table paste routing task note.
  • Demo
    • Added a temporary “Table” toolbar button that inserts a 2×2 table at the current caret position.

The same paste path that wafflebase#528 fixes (multi-block clipboard payload
merged into a table cell) turns out to also cause wafflebase#333: pasting a
table containing nested tables leaves the pasted copy's inner cell
blocks sharing ids with the source, so clicking/typing into a
nested table on the pasted copy silently edits the source instead
(and the deepest nested level rejects the caret outright).

Both bugs live on the same insertBlocks code path and are fixed by
the same change (recursive fresh-id regeneration + a
findBlockInCells fallback for blocks not yet in the layout-time
parent map), so this folds wafflebase#333 into the existing note rather than
adding a new file, per docs/design/README.md's guidance to fold
single-PR notes into the relevant doc. Design-note-first;
implementation already drafted on this branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@hackerwins, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 58 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9c24c8b8-3146-4c05-b0ba-4fb7d54217ca

📥 Commits

Reviewing files that changed from the base of the PR and between 4e6b7a4 and c77effd.

📒 Files selected for processing (3)
  • docs/tasks/active/20260730-docs-nested-table-paste-input-routing-todo.md
  • packages/docs/src/view/text-editor.ts
  • packages/docs/test/model/paste-table-into-cell.test.ts
📝 Walkthrough

Walkthrough

The PR expands paste-into-cell handling to multi-block insertion and nested-table input routing. It adds cell-aware block chaining, recursive ID and block lookup, recursive nested-table hit testing, demo table controls, updated design/task documentation, and regression tests.

Changes

Paste-into-cell behavior

Layer / File(s) Summary
Scope and paste scenarios
docs/design/docs/docs-paste-table-into-cell.md, docs/tasks/active/20260730-docs-nested-table-paste-input-routing-todo.md
Documents the multi-block cell paste crash, nested-table routing issue, implementation scope, checklist, and verification steps.
Cell-aware paste insertion
packages/docs/src/view/text-editor.ts, docs/design/docs/docs-paste-table-into-cell.md
Uses cloneBlockWithFreshIds and chained insertBlockAfter calls for multi-block pastes, including insertion inside cells.
Nested lookup and hit testing
packages/docs/src/model/document.ts, packages/docs/src/view/text-editor.ts
Adds recursive cell block lookup and recursive nested-table click resolution with merged-cell handling and character offsets.
Validation and demo wiring
packages/docs/test/model/paste-table-into-cell.test.ts, packages/docs/demo.ts, packages/docs/index.html, docs/design/docs/docs-paste-table-into-cell.md
Adds regression coverage for cell ordering and nested IDs, and adds a temporary 2×2 table toolbar action to the demo.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant TextEditor
  participant Doc
  participant NestedTableResolver
  User->>TextEditor: paste table content or click nested cell
  TextEditor->>TextEditor: clone blocks with fresh IDs
  TextEditor->>Doc: insertBlockAfter chained blocks
  Doc-->>TextEditor: preserve containing cell placement
  TextEditor->>NestedTableResolver: resolve nested-table offset
  NestedTableResolver->>Doc: find owning block
  Doc-->>TextEditor: return blockId and offset
  TextEditor-->>User: place caret in pasted table
Loading

Possibly related PRs

Suggested reviewers: hwisoo-kim

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects the main change: extending the paste-into-cell design note for issue #333.
Linked Issues check ✅ Passed The PR addresses #333 by making pasted nested tables independent, with recursive ID regeneration, lookup fallback, and nested click resolution.
Out of Scope Changes check ✅ Passed The added demo control and task note support the nested-table paste fix and stay aligned with the issue scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/design/docs/docs-paste-table-into-cell.md`:
- Around line 139-149: Update the documentation’s verification statements around
the model tests and manual checks to use planned or conditional wording rather
than claiming completion. Revise the sections covering the listed test coverage
and repro/regression checks, or link to concrete landed test and
manual-verification evidence if available; do not imply documentation-only
changes have already exercised or re-ran those checks.
- Around line 18-22: The documentation incorrectly attributes issues `#528` and
`#333` to the same change. Reword the affected note to distinguish the body-only
insertion fix from the separate recursive cloning and _blockParentMap lookup
fallback required for nested pasted tables, while describing both as coordinated
fixes on the shared paste path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7b8f867d-4589-47d7-aafc-4bb9a0f76ff1

📥 Commits

Reviewing files that changed from the base of the PR and between 2c63997 and 3188d33.

📒 Files selected for processing (1)
  • docs/design/docs/docs-paste-table-into-cell.md

Comment thread docs/design/docs/docs-paste-table-into-cell.md Outdated
Comment thread docs/design/docs/docs-paste-table-into-cell.md
@codecov

codecov Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 17.34694% with 81 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/docs/src/view/text-editor.ts 0.00% 76 Missing ⚠️
packages/docs/src/model/document.ts 77.27% 2 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

semimikoh and others added 3 commits July 30, 2026 14:46
Two wording fixes from PR wafflebase#597's review:

- Summary said wafflebase#528 and wafflebase#333 are "fixed by the same change" — reword
  to describe them as separate fixes (insertBlockAfter chaining for
  wafflebase#528, recursive fresh-id cloning + findBlockInCells fallback for
  wafflebase#333) that happen to land on the same insertBlocks code path.
- Testing Strategy / Risks described wafflebase#333 test coverage and repro
  re-verification as already landed, but this PR is documentation
  only — the wafflebase#333-specific regression test isn't in this diff yet.
  Reword to distinguish the four existing tests (already on main from
  wafflebase#528/PR wafflebase#531) from the wafflebase#333 regression test (planned for the
  implementation PR), and note the manual repro re-verification was
  against a local draft, not this PR's contents.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pasting a table that contains nested tables left the pasted copy's
inner cell blocks sharing ids with the source: the multi-block paste
path's middle-block clone only refreshed a block's own top-level id
(`{ ...block, id: generateBlockId() }`), not ids nested inside its
table cells. Doc.getBlock / findBlockInCells then resolved those
shared ids back to the source for every level above the deepest
(caret visually correct, input landed in the source instead); the
deepest level's cell wasn't in _blockParentMap yet (only rebuilt at
the next layout pass) and couldn't resolve at all, rejecting the
caret outright.

Swap the middle-block clone for cloneBlockWithFreshIds (already used
by the single-table paste branch) — its refreshBlockIds already
recurses through tableData.rows[].cells[].blocks[], so this alone
fixes the id-sharing. findBlockInCells gains a walkCellsForBlock
recursive fallback for blocks not yet in _blockParentMap (e.g. the
paste's own middle blocks), so getBlock resolves them instead of
throwing "Block not found".

Adds a model-level regression test asserting getBlock resolves a
block inside a freshly pasted nested table before any layout pass has
run, and a Table button in the standalone docs demo for manually
exercising nested-table paste.

Design note: docs/design/docs/docs-paste-table-into-cell.md (wafflebase#333
folded into the existing wafflebase#528 paste-into-cell note, see PR wafflebase#597).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
resolveOffsetInCellAtXY resolved a click into a cell's inner table
(2 nesting levels deep), but a table nested inside that inner table's
own cell only got a "further nesting" comment and a bail-out that
returned a position on the table block itself — which has no text —
instead of continuing to drill in. That is what made the deepest
level of a 3+-level nested table reject the caret outright: clicking
did nothing because the resolved position wasn't inside any editable
content.

Extract the inner-table resolution into resolveOffsetInNestedTable, a
recursive method: it resolves row/column/cell/line for the table it's
given, and calls itself when that cell's target line is itself a
nested table, instead of stopping after one level. Only the outermost
table's rows are page/split-aware (only they have entries in the
paginated layout — layoutCellBlocks in table-layout.ts lays out a
nested table as a single synthetic line inside its parent cell), so
the page-aware Y lookup happens once, in the caller, and the
recursive method carries that resolved Y origin forward with plain
relative arithmetic for every level below it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
docs/tasks/active/20260730-docs-nested-table-paste-input-routing-todo.md (1)

44-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Work checklist looks stale relative to this PR. The regression test at line 54 appears implemented (packages/docs/test/model/paste-table-into-cell.test.ts now contains the #333 test). Consider ticking it before merge; per prior learnings the doc should be archived via pnpm tasks:archive rather than banner-marked as complete.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/tasks/active/20260730-docs-nested-table-paste-input-routing-todo.md`
around lines 44 - 62, Update the Work checklist to mark the `#333` automated
regression test complete, reflecting the existing test in
paste-table-into-cell.test.ts. After the remaining work is finished, archive
this task document using pnpm tasks:archive rather than adding a completion
banner.

Source: Learnings

packages/docs/src/view/text-editor.ts (1)

4677-4683: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Docstring contradicts the recursion call. It says localX is "reused unchanged across levels", but the recursive call at Line 4781 passes cellLocalX (cell-content-relative). The code looks right; the comment should say the X coordinate is re-based onto the cell's content box at each level.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/docs/src/view/text-editor.ts` around lines 4677 - 4683, The
docstring for the recursive table layout method incorrectly says localX is
reused unchanged. Update the description around tableAbsY and localX to state
that each recursive level re-bases the X coordinate to the containing cell’s
content box, matching the recursive call’s cellLocalX argument; leave the
implementation unchanged.
packages/docs/src/model/document.ts (1)

177-204: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Fallback walk repeats per recursion level and on every miss.

Two effects worth considering:

  • The self-call at Line 191 re-enters findBlockInCells, which — when its own fast path misses — runs the full body/header/footer walk for the parent table id before returning. So a deep nested lookup can trigger O(depth) full document walks.
  • findBlock is called on hot paths (e.g. the cursor-validity guard in handleKeyDown); for a genuinely missing id the walk now traverses every table in all three regions on each call.

Extracting a resolveViaParentMap(blockId) helper (fast path only, recursive) and running walkCellsForBlock exactly once at the top level would keep the fix while bounding the cost.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/docs/src/model/document.ts` around lines 177 - 204, Refactor the
cell-block lookup around findBlockInCells so the parent-map fast path is
isolated in a recursive resolveViaParentMap(blockId) helper that does not
perform the full fallback walk. Ensure nested lookups use this helper, while
findBlockInCells invokes walkCellsForBlock across document, header, and footer
regions exactly once after the fast path fails, including for missing IDs.
packages/docs/test/model/paste-table-into-cell.test.ts (1)

152-167: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider also asserting ID disjointness from the source. The #333 root cause was shared IDs; collectIds (Line 8) already exists for exactly this. One extra assertion pins the regression directly:

💚 Suggested addition
     expect(() => doc.getBlock(deepestBlockId)).not.toThrow();
     expect(doc.getBlock(deepestBlockId).id).toBe(deepestBlockId);
+    // Fresh ids at every nesting level — no sharing with the source table.
+    const sourceIds = new Set(collectIds(sourceOuter));
+    for (const id of collectIds(pasted)) expect(sourceIds.has(id)).toBe(false);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/docs/test/model/paste-table-into-cell.test.ts` around lines 152 -
167, Add an assertion using the existing collectIds helper to verify that the
cloned pasted table's IDs are disjoint from sourceOuter's IDs before or after
insertion. Keep the existing deepest-block lookup assertions unchanged, and
ensure the check covers all IDs in both the source and cloned structures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/tasks/active/20260730-docs-nested-table-paste-input-routing-todo.md`:
- Line 11: Update the “#333's symptom, by nesting depth:” text in the documented
issue section so the leading issue reference is wrapped or otherwise formatted
as inline content, preventing markdownlint from interpreting it as an ATX
heading while preserving the displayed wording.

In `@packages/docs/src/view/text-editor.ts`:
- Around line 4720-4736: Update the merged-cell resolution block around
dataCell0 so scanning stops immediately after the first valid owning cell is
found; use a labeled outer-loop break or an equivalent immediate return,
ensuring later rows cannot overwrite the resolved row and col.

---

Nitpick comments:
In `@docs/tasks/active/20260730-docs-nested-table-paste-input-routing-todo.md`:
- Around line 44-62: Update the Work checklist to mark the `#333` automated
regression test complete, reflecting the existing test in
paste-table-into-cell.test.ts. After the remaining work is finished, archive
this task document using pnpm tasks:archive rather than adding a completion
banner.

In `@packages/docs/src/model/document.ts`:
- Around line 177-204: Refactor the cell-block lookup around findBlockInCells so
the parent-map fast path is isolated in a recursive resolveViaParentMap(blockId)
helper that does not perform the full fallback walk. Ensure nested lookups use
this helper, while findBlockInCells invokes walkCellsForBlock across document,
header, and footer regions exactly once after the fast path fails, including for
missing IDs.

In `@packages/docs/src/view/text-editor.ts`:
- Around line 4677-4683: The docstring for the recursive table layout method
incorrectly says localX is reused unchanged. Update the description around
tableAbsY and localX to state that each recursive level re-bases the X
coordinate to the containing cell’s content box, matching the recursive call’s
cellLocalX argument; leave the implementation unchanged.

In `@packages/docs/test/model/paste-table-into-cell.test.ts`:
- Around line 152-167: Add an assertion using the existing collectIds helper to
verify that the cloned pasted table's IDs are disjoint from sourceOuter's IDs
before or after insertion. Keep the existing deepest-block lookup assertions
unchanged, and ensure the check covers all IDs in both the source and cloned
structures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 783eceb3-1326-4d26-9fde-38eb41e49d3c

📥 Commits

Reviewing files that changed from the base of the PR and between 3188d33 and 4e6b7a4.

📒 Files selected for processing (7)
  • docs/design/docs/docs-paste-table-into-cell.md
  • docs/tasks/active/20260730-docs-nested-table-paste-input-routing-todo.md
  • packages/docs/demo.ts
  • packages/docs/index.html
  • packages/docs/src/model/document.ts
  • packages/docs/src/view/text-editor.ts
  • packages/docs/test/model/paste-table-into-cell.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/design/docs/docs-paste-table-into-cell.md

Comment thread docs/tasks/active/20260730-docs-nested-table-paste-input-routing-todo.md Outdated
Comment thread packages/docs/src/view/text-editor.ts
@semimikoh

Copy link
Copy Markdown
Contributor Author

Apologies — I jumped ahead of our own process here. Per the repo's design-note-first workflow (note PR → maintainer agreement → implementation), I should have waited for direction/agreement on the design note before pushing the implementation. Instead I pushed the implementation commits (c4e82cdbe, 4e6b7a4f5) onto this same branch before that step.

The implementation is up for reference/early review if useful, but I'm treating the design note as not yet agreed, and I'm happy to hold off on anything further (or roll the implementation back to a separate branch) until there's agreement on the approach. Sorry for the noise.

The merged-cell resolution in resolveOffsetInNestedTable used a bare
break that only exited the inner column loop, so the outer row loop kept
scanning against the already-mutated row/col and could re-match a lower
row, overwriting the correct merge top-left. Use a labeled `break outer`,
matching the sibling resolveHFCellOffset. Also correct the docstring: the
X coordinate is re-based onto each cell's content box (the recursion
passes cellLocalX), not "reused unchanged across levels". Add an id-
disjointness assertion pinning wafflebase#333's shared-id root cause directly, and
wrap the leading issue ref in the task note so markdownlint stops reading
it as an ATX heading.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hackerwins
hackerwins requested a review from a team July 31, 2026 00:01

@hackerwins hackerwins left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution.

@hackerwins
hackerwins merged commit 4a211ad into wafflebase:main Jul 31, 2026
4 checks passed
@semimikoh
semimikoh deleted the fix/nested-table-paste-input-routing branch July 31, 2026 02:08
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.

Pasted nested tables misroute text input to the original table

2 participants