Add intent-preserving block/cell attribute edits (Phase 5)#153
Conversation
Route block type and style changes through the store interface instead of full block replacement (updateBlockInStore). YorkieDocStore uses styleByPath for CRDT-safe attribute updates, eliminating LWW conflicts when concurrent users change block type/style while others edit text in the same block. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Route cell style changes (background, borders, alignment) through the store interface using styleByPath on the cell node, instead of replacing the entire cell via updateTableCell. This preserves concurrent text edits within the cell during style changes. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Route image inline insertion through the store interface using editByPath at inline level, instead of full block replacement. Also removes the now-unused updateBlockInStore and findRootTableId private methods from Doc class — all editing operations now go through intent-preserving store methods. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Document the migration of setBlockType, applyBlockStyle, applyCellStyle, and insertImageInline from LWW to styleByPath/ editByPath. List remaining LWW operations for future work. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Categorize remaining full-replacement operations into four groups: A) cell span attributes (styleByPath), B) table-level attributes (styleByPath on block), C) cell structural changes (editByPath), D) direct block replacement. Include call sites and priority order. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Renumber Phase 4b → 5 (shipped), add Phase 6 (cell span attrs), Phase 7 (table-level attrs), Phase 8 (cell structural edits), and move undo/redo to Phase 9 as the final planned phase. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Archive completed Phase 4 (table cells) and Phase 5 (block attrs) task files. Update undo/redo task to reflect Phase 9 numbering and mark Task 4 items completed by Phase 4-5 work. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 48 minutes and 24 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughThis PR implements intent-preserving edits for block attributes and cell styling by adding new store methods ( Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Doc
participant DocStore
participant YorkieDocStore
participant Yorkie Tree
Client->>Doc: setBlockType(blockId, type)
Doc->>DocStore: setBlockType(blockId, type, opts)
DocStore->>YorkieDocStore: setBlockType(blockId, type, opts)
YorkieDocStore->>YorkieDocStore: Serialize/normalize attributes
YorkieDocStore->>Yorkie Tree: styleByPath() on block node
Yorkie Tree-->>YorkieDocStore: Update applied
YorkieDocStore->>YorkieDocStore: Update cached block
YorkieDocStore-->>DocStore: void
DocStore-->>Doc: void
Doc-->>Client: void
rect rgba(100, 150, 200, 0.5)
Note over Client,Yorkie Tree: Inline Image Insertion Flow
Client->>Doc: insertImageInline(blockId, offset, inline)
Doc->>DocStore: insertImageInline(blockId, offset, inline)
DocStore->>YorkieDocStore: insertImageInline(blockId, offset, inline)
YorkieDocStore->>Yorkie Tree: Edit/split inline nodes in tree
Yorkie Tree-->>YorkieDocStore: Tree structure modified
YorkieDocStore->>YorkieDocStore: applyInsertInline() on cached block
YorkieDocStore-->>DocStore: void
DocStore-->>Doc: void
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Verification: verify:selfResult: ✅ PASS in 120.0s
Verification: verify:integrationResult: ✅ PASS |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Unit tests (15 cases): setBlockType, applyBlockStyle, applyCellStyle, insertImageInline — including cell-internal block variants. Concurrent integration tests (3 cases): block type change + text insert, block style + text insert, cell style + text insert in cell — verifying both operations preserved without LWW conflict. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Use removeStyleByPath to strip stale headingLevel/listKind/listLevel when changing block type. Prevents convergence bug where remote clients see phantom attributes from the previous type. - Remove unnecessary serializeBlockStyle from setBlockType attrs — only type-specific attributes need to change. - Fix applyCellStyle to mutate cache after Yorkie update, matching the pattern used by all other store methods. - Add 3 tests: heading→list-item stale removal, list-item→paragraph stale removal, heading level change on existing heading. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
packages/frontend/tests/app/docs/yorkie-doc-store-concurrent.integration.ts (2)
500-555: Placement nit:setBlockType/applyBlockStyletests aren't table-cell tests.Both cases operate on a plain paragraph block (
makeBlock('HelloWorld')) but are nested insidedescribe('YorkieDocStore concurrent table cell edits', …)(line 425). Consider moving them into theYorkieDocStore concurrent inline stylingsuite (or a newconcurrent block attribute editssuite) so the describe label reflects what's actually exercised.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/tests/app/docs/yorkie-doc-store-concurrent.integration.ts` around lines 500 - 555, These two tests ("concurrent setBlockType and text insert should both be preserved" and "concurrent applyBlockStyle and text insert should both be preserved") are misplaced under the describe labeled "YorkieDocStore concurrent table cell edits"; cut these it blocks out of that describe and move them into the existing "YorkieDocStore concurrent inline styling" describe (or create a new describe "YorkieDocStore concurrent block attribute edits") so the describe label matches the behavior being tested; ensure you preserve the surrounding setup/teardown context (use of makeBlock and createTwoUserDocs and ctx.cleanup) and keep the it titles and assertions intact.
500-588: Strengthen convergence assertions with a length check.Existing cell tests in this file (e.g., line 493:
assert.equal(textA.length, 12, 'Insert should be preserved');) assert final length in addition toincludes. The three new tests only checkincludes('XX'|'YY'|'ZZ'), which would pass even if insertion were duplicated or characters from the original were lost. Adding a length assertion (e.g.,'HelloWorld'.length + 2for XX/YY,'CellText'.length + 2for ZZ) would catch regressions in the intent-preserving path where that's exactly the property we care about.♻️ Example: add length check to the setBlockType test
const textA = docA.blocks[0].inlines.map((i) => i.text).join(''); const textB = docB.blocks[0].inlines.map((i) => i.text).join(''); assert.equal(textA, textB, 'Text should converge'); + assert.equal(textA.length, 'HelloWorld'.length + 2, `Expected 12 chars, got "${textA}"`); assert.ok(textA.includes('XX'), `Inserted text "XX" should be preserved, got "${textA}"`);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/tests/app/docs/yorkie-doc-store-concurrent.integration.ts` around lines 500 - 588, Add explicit final-length assertions to the three new tests to ensure the inserted two characters weren't duplicated or lost: in the "concurrent setBlockType and text insert should both be preserved" and "concurrent applyBlockStyle and text insert should both be preserved" tests assert that textA.length and textB.length equal makeBlock('HelloWorld') length (10) + 2 => 12 (use the local variable block to compute or hardcode 12); in the "concurrent applyCellStyle and text insert in same cell should both be preserved" test assert textA.length and textB.length equal cellBlock original text length (CellText = 8) + 2 => 10 (use cellBlock to compute or hardcode 10); place these assertions alongside the existing includes(...) checks after computing textA/textB so failures show both content and length mismatches, referencing ctx.storeA/ctx.storeB and the local block/cellBlock variables to locate the assertions.docs/tasks/archive/2026/04/20260423-block-attr-intent-preserving-lessons.md (1)
1-3: Placeholder lessons file — no review needed.Consider populating this before archival (e.g., takeaways from
styleByPathvs full-block replacement, stale-attribute handling insetBlockType, cache-order fix inapplyCellStyle) so the archive entry is useful to future work.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/tasks/archive/2026/04/20260423-block-attr-intent-preserving-lessons.md` around lines 1 - 3, This is a placeholder lessons file; before archiving, populate it with concise takeaways from recent edits — summarize pros/cons of styleByPath versus full-block replacement, note stale-attribute handling in setBlockType, and document the cache-order bug and fix in applyCellStyle so future readers can find the rationale and patterns used; add short examples or links to the relevant functions (styleByPath, setBlockType, applyCellStyle) and a brief “lessons learned” bullet list.packages/frontend/tests/app/docs/yorkie-doc-store.test.ts (1)
835-1037: LGTM — thorough coverage for the new intent-preserving methods.Tests for
setBlockType(including stale-attribute clearing viaremoveStyleByPath),applyBlockStyle,applyCellStyle, andinsertImageInlineexercise the main paths including cell-internal blocks. The hoistedmakeTableWithTexthelper is cleanly reused.One optional coverage gap you may want to add later:
insertImageInlineat the end of a block's text (offset === totalText.length), which exercises a boundary inresolveBlockNodeOffset+ split-inline path.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/frontend/tests/app/docs/yorkie-doc-store.test.ts` around lines 835 - 1037, Add a boundary test that inserts an image inline at the end of a block's text (offset === totalText.length) to exercise resolveBlockNodeOffset and the split-inline path used by insertImageInline; create a block via makeBlock (or reuse existing helper), setDocument, call store.insertImageInline(block.id, totalTextLength, { text: '\uFFFC', style: { image: {...} } }), then assert the final inlines/text and that the image inline exists — this will cover the edge case the reviewer mentioned.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/tasks/active/20260403-intent-preserving-phase5-undo-redo-todo.md`:
- Line 1: The file slug and README entry are out of sync with the new heading:
rename the file `intent-preserving-phase5-undo-redo-todo.md` to match the
heading (suggested: `20260403-intent-preserving-phase9-undo-redo-todo.md`),
update any internal references (e.g., the entry currently listed in
docs/tasks/README.md as "intent preserving phase5 undo redo"), and then
regenerate the tasks index by running the project script (`pnpm tasks:index`) so
the slug and index/search entries reflect the Phase 9 name.
In `@docs/tasks/README.md`:
- Line 22: Update the stale display name and optionally the slug to match the
renamed heading in the linked file: change the table row text "intent preserving
phase5 undo redo (2026-04-03)" to "Phase 9: Yorkie-Native Undo/Redo
(2026-04-03)" and consider renaming the referenced filename
"20260403-intent-preserving-phase5-undo-redo-todo.md" to a matching slug like
"20260403-phase9-yorkie-native-undo-redo-todo.md" so the entry in
docs/tasks/README.md and the referenced file name are consistent with the
heading in
docs/tasks/active/20260403-intent-preserving-phase5-undo-redo-todo.md.
In `@packages/docs/src/model/document.ts`:
- Around line 209-213: Remove the internal snapshot call in insertImageInline:
delete the this.store.snapshot() invocation inside the insertImageInline method
so it simply delegates to this.store.insertImageInline(blockId, offset,
imageInline) and then calls this.refresh(); this makes insertImageInline
consistent with applyBlockStyle, setBlockType, and applyCellStyle (which rely on
the editor layer to call docStore.snapshot() once) and avoids creating a
duplicate undo unit.
In `@packages/frontend/src/app/docs/yorkie-doc-store.ts`:
- Around line 1641-1665: applyCellStyle currently merges new style into the
cached cell and writes serialized attributes via serializeCellStyle and
styleByPath, but serializeCellStyle omits falsy attributes so attributes set to
undefined never get cleared from the Yorkie Tree causing a cache/tree mismatch;
fix by detecting keys in the incoming style that are explicitly undefined after
computing merged (or by inspecting style), and for each such attribute call the
corresponding removeStyleByPath (same pattern used in setBlockType) instead
of/in addition to styleByPath so the tree entries are removed; update
applyCellStyle to compute attrs for styleByPath for truthy values and call
removeStyleByPath for attributes present in style with undefined to keep cache
and tree in sync.
---
Nitpick comments:
In `@docs/tasks/archive/2026/04/20260423-block-attr-intent-preserving-lessons.md`:
- Around line 1-3: This is a placeholder lessons file; before archiving,
populate it with concise takeaways from recent edits — summarize pros/cons of
styleByPath versus full-block replacement, note stale-attribute handling in
setBlockType, and document the cache-order bug and fix in applyCellStyle so
future readers can find the rationale and patterns used; add short examples or
links to the relevant functions (styleByPath, setBlockType, applyCellStyle) and
a brief “lessons learned” bullet list.
In `@packages/frontend/tests/app/docs/yorkie-doc-store-concurrent.integration.ts`:
- Around line 500-555: These two tests ("concurrent setBlockType and text insert
should both be preserved" and "concurrent applyBlockStyle and text insert should
both be preserved") are misplaced under the describe labeled "YorkieDocStore
concurrent table cell edits"; cut these it blocks out of that describe and move
them into the existing "YorkieDocStore concurrent inline styling" describe (or
create a new describe "YorkieDocStore concurrent block attribute edits") so the
describe label matches the behavior being tested; ensure you preserve the
surrounding setup/teardown context (use of makeBlock and createTwoUserDocs and
ctx.cleanup) and keep the it titles and assertions intact.
- Around line 500-588: Add explicit final-length assertions to the three new
tests to ensure the inserted two characters weren't duplicated or lost: in the
"concurrent setBlockType and text insert should both be preserved" and
"concurrent applyBlockStyle and text insert should both be preserved" tests
assert that textA.length and textB.length equal makeBlock('HelloWorld') length
(10) + 2 => 12 (use the local variable block to compute or hardcode 12); in the
"concurrent applyCellStyle and text insert in same cell should both be
preserved" test assert textA.length and textB.length equal cellBlock original
text length (CellText = 8) + 2 => 10 (use cellBlock to compute or hardcode 10);
place these assertions alongside the existing includes(...) checks after
computing textA/textB so failures show both content and length mismatches,
referencing ctx.storeA/ctx.storeB and the local block/cellBlock variables to
locate the assertions.
In `@packages/frontend/tests/app/docs/yorkie-doc-store.test.ts`:
- Around line 835-1037: Add a boundary test that inserts an image inline at the
end of a block's text (offset === totalText.length) to exercise
resolveBlockNodeOffset and the split-inline path used by insertImageInline;
create a block via makeBlock (or reuse existing helper), setDocument, call
store.insertImageInline(block.id, totalTextLength, { text: '\uFFFC', style: {
image: {...} } }), then assert the final inlines/text and that the image inline
exists — this will cover the edge case the reviewer mentioned.
🪄 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
Run ID: 398bbda4-204f-48dc-a474-a3645a25802a
📒 Files selected for processing (14)
docs/design/docs/docs-intent-preserving-edits.mddocs/tasks/README.mddocs/tasks/active/20260403-intent-preserving-phase5-undo-redo-todo.mddocs/tasks/archive/2026/04/20260403-intent-preserving-phase4-table-cells-todo.mddocs/tasks/archive/2026/04/20260423-block-attr-intent-preserving-lessons.mddocs/tasks/archive/2026/04/20260423-block-attr-intent-preserving-todo.mddocs/tasks/archive/README.mdpackages/docs/src/index.tspackages/docs/src/model/document.tspackages/docs/src/store/memory.tspackages/docs/src/store/store.tspackages/frontend/src/app/docs/yorkie-doc-store.tspackages/frontend/tests/app/docs/yorkie-doc-store-concurrent.integration.tspackages/frontend/tests/app/docs/yorkie-doc-store.test.ts
| @@ -1,11 +1,11 @@ | |||
| # Intent-Preserving Edits — Phase 5: Yorkie-Native Undo/Redo | |||
| # Intent-Preserving Edits — Phase 9: Yorkie-Native Undo/Redo | |||
There was a problem hiding this comment.
Filename/slug out of sync with renumbered phase.
Heading is now "Phase 9: Yorkie-Native Undo/Redo", but the filename slug remains intent-preserving-phase5-undo-redo-todo.md and docs/tasks/README.md line 22 still lists it as "intent preserving phase5 undo redo". Consider renaming the file (e.g., 20260403-intent-preserving-phase9-undo-redo-todo.md) and regenerating the index with pnpm tasks:index so search/navigation by slug doesn't mislead readers cross-referencing the renumbered design doc.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@docs/tasks/active/20260403-intent-preserving-phase5-undo-redo-todo.md` at
line 1, The file slug and README entry are out of sync with the new heading:
rename the file `intent-preserving-phase5-undo-redo-todo.md` to match the
heading (suggested: `20260403-intent-preserving-phase9-undo-redo-todo.md`),
update any internal references (e.g., the entry currently listed in
docs/tasks/README.md as "intent preserving phase5 undo redo"), and then
regenerate the tasks index by running the project script (`pnpm tasks:index`) so
the slug and index/search entries reflect the Phase 9 name.
| applyCellStyle( | ||
| tableBlockId: string, rowIndex: number, colIndex: number, | ||
| style: Partial<CellStyle>, | ||
| ): void { | ||
| const tablePath = this.resolveTableTreePath(tableBlockId); | ||
| const currentDoc = this.getDocument(); | ||
| const block = this.resolveTableBlock(tablePath, currentDoc); | ||
| const cell = block.tableData!.rows[rowIndex].cells[colIndex]; | ||
| const merged = { ...cell.style, ...style }; | ||
|
|
||
| // Build serialized attributes for the cell node | ||
| const attrs = serializeCellStyle({ ...cell, style: merged }); | ||
|
|
||
| this.doc.update((root) => { | ||
| const tree = root.content; | ||
| if (!tree || typeof tree.getRootTreeNode !== 'function') return; | ||
| tree.styleByPath([...tablePath, rowIndex, colIndex], attrs); | ||
| }); | ||
|
|
||
| // Update cache after Yorkie update succeeds | ||
| cell.style = merged; | ||
|
|
||
| this.cachedDoc = currentDoc; | ||
| this.dirty = false; | ||
| } |
There was a problem hiding this comment.
applyCellStyle cannot clear cell attributes.
serializeCellStyle gates each attribute with truthy checks (e.g. if (s.backgroundColor)) and styleByPath merges rather than replaces, so calling applyCellStyle(..., { backgroundColor: undefined }) will leave the prior backgroundColor attribute in the Yorkie Tree even though the merged cache has undefined. This produces a cache/tree mismatch the next time getDocument() re-parses from the tree (e.g. after a remote change).
If clearing is not a supported use case, consider tightening the JSDoc on DocStore.applyCellStyle to say "additive only"; otherwise pair the write with removeStyleByPath for attributes explicitly set to undefined in style (mirroring the pattern used in setBlockType).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/frontend/src/app/docs/yorkie-doc-store.ts` around lines 1641 - 1665,
applyCellStyle currently merges new style into the cached cell and writes
serialized attributes via serializeCellStyle and styleByPath, but
serializeCellStyle omits falsy attributes so attributes set to undefined never
get cleared from the Yorkie Tree causing a cache/tree mismatch; fix by detecting
keys in the incoming style that are explicitly undefined after computing merged
(or by inspecting style), and for each such attribute call the corresponding
removeStyleByPath (same pattern used in setBlockType) instead of/in addition to
styleByPath so the tree entries are removed; update applyCellStyle to compute
attrs for styleByPath for truthy values and call removeStyleByPath for
attributes present in style with undefined to keep cache and tree in sync.
- Handle end-of-inline boundary in insertImageInline: insert after current inline without splitting, avoiding phantom empty inline that causes tree/cache divergence. - Remove duplicate store.snapshot() in Doc.insertImageInline — editor layer already calls docStore.snapshot() before the operation. - Rename phase5 task file to phase9 to match renumbered phases. - Add JSDoc clarifying applyCellStyle is additive-only. - Add regression test for image insertion at end of block text. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Summary
setBlockType,applyBlockStyle,applyCellStyle,insertImageInlineto DocStore interfacestyleByPath/editByPathinstead of full block/cell replacement (LWW)updateBlockInStoreandfindRootTableIdChanges
setBlockType,applyBlockStyle,applyCellStyle,insertImageInline)styleByPath/editByPathTest plan
pnpm verify:fast— all 576 docs tests + frontend/backend/sheets tests pass🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Tests
Documentation