Skip to content

[WIP]feat(core): markdown-diff & patch apply #12844

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: canary
Choose a base branch
from

Conversation

yoyoyohamapi
Copy link
Contributor

@yoyoyohamapi yoyoyohamapi commented Jun 18, 2025

Summary by CodeRabbit

  • New Features
    • Introduced block-level diffing for markdown content, allowing detection of insertions, deletions, and replacements between document versions.
    • Added the ability to apply patch operations to documents, enabling updates such as block insertion, deletion, and content replacement.
  • Tests
    • Added comprehensive test suites to verify the correctness of markdown diffing and patch application functionalities.
  • Chores
    • Updated dependencies and improved test utility imports for better maintainability and convenience.
    • Consolidated test utilities into a single importable module.

Copy link

coderabbitai bot commented Jun 18, 2025

Walkthrough

This update introduces new utilities for diffing and patching markdown documents at the block level, including functions to compute and apply patches. It adds comprehensive tests for these features, refactors test utility imports, and consolidates test utilities under a single entry point. Dependency and export configurations are also updated accordingly.

Changes

File(s) Change Summary
blocksuite/affine/shared/package.json, packages/frontend/core/package.json Added new workspace dependencies and updated exports for test utilities.
blocksuite/affine/shared/src/test-utils/index.ts New module re-exporting all test utilities from a single entry point.
blocksuite/affine/shared/src/test-utils/affine-template.ts Refactored extension management to use dynamic DI-based setup for block schema extensions.
blocksuite/affine/shared/src/test-utils/affine-test-utils.ts, .../create-test-host.ts Minor refactorings and comment updates in test utility files.
blocksuite/affine/shared/src/tests/commands/... Updated test files to import helpers from the new test-utils entry point.
packages/frontend/core/src/blocksuite/ai/utils/apply-model/markdown-diff.ts Added new module for parsing markdown into blocks, computing diffs, and generating patch operations.
packages/frontend/core/src/blocksuite/ai/utils/apply-model/apply-patch-to-doc.ts Added new function to apply patch operations to a document store.
packages/frontend/core/src/tests/ai/utils/apply-model/markdown-diff.spec.ts Added tests for markdown diffing logic, covering insert, delete, replace, and mixed operations.
packages/frontend/core/src/tests/ai/utils/apply-model/apply-patch-to-doc.spec.ts Added tests for applying patch operations to a document using the new patch application logic.

Sequence Diagram(s)

sequenceDiagram
    participant Test as Test Suite
    participant Diff as diffMarkdown
    participant Patch as applyPatchToDoc
    participant Store as Document Store

    Test->>Diff: Provide old and new markdown
    Diff->>Diff: Parse markdown into blocks
    Diff->>Diff: Compare block lists, generate PatchOps
    Diff-->>Test: Return PatchOps

    Test->>Patch: Provide Store and PatchOps
    Patch->>Store: Locate note blocks
    Patch->>Store: For each PatchOp, modify blocks (insert, delete, replace)
    Patch-->>Test: Store updated
Loading

Suggested labels

test, app:core

Poem

A hop and a leap, new diffs appear,
Blocks shuffle and patch without fear.
Test bunnies gather, their whiskers a-twitch,
As markdown transforms with a magical switch.
With imports aligned and utilities neat,
This code garden grows—oh, what a treat! 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added docs Improvements or additions to documentation test Related to test cases app:core labels Jun 18, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

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 (6)
blocksuite/affine/shared/src/__tests__/commands/model-crud/replace-selected-text-with-blocks.unit.spec.ts (1)

4-5: Simplify side-effect import

The only purpose of this import is to register custom matchers.
You can rely on the barrel to pull in affine-test-utils internally and avoid addressing the file directly:

-import '../../../test-utils/affine-test-utils';
+import '../../../test-utils';

This makes the import resilient to future file moves inside test-utils.

packages/frontend/core/src/blocksuite/ai/utils/apply-model/apply-patch-to-doc.ts (1)

15-20: Scope limited to the first note – is that intentional?

applyPatchToDoc silently ignores every note block after the first.
If multiple notes are allowed in a page, patches targeting the others will never be applied. Clarify via docs or throw when patch refers to out-of-scope blocks.

packages/frontend/core/src/__tests__/ai/utils/apply-model/apply-patch-to-doc.spec.ts (1)

78-83: new_block includes type but it is never asserted

The test constructs new_block.type = 'affine:paragraph' yet later only checks for a paragraph element in the expected template.
If future ops require non-paragraph types this test will not guard against regressions.
Consider parameterising the test to cover multiple block types.

packages/frontend/core/src/blocksuite/ai/utils/apply-model/markdown-diff.ts (1)

64-86: Algorithm ignores block re-ordering

diffBlockLists emits nothing when a block keeps the same ID/content but moves position.
Applying the patch will therefore preserve the old order.
If ordering matters you’ll need a move/insert+delete strategy.

blocksuite/affine/shared/src/test-utils/affine-template.ts (1)

81-86: Inconsistent store initialisation API

affine() now calls doc.getStore({ extensions, provider: container.provider() }),
but block() (line 176) still uses the old signature doc.getStore({ extensions }).
Keep both paths consistent to avoid subtle test-only discrepancies.

packages/frontend/core/src/__tests__/ai/utils/apply-model/markdown-diff.spec.ts (1)

208-221: Patch order assertion may hide real-world ordering bugs

The test expects insert_at before the corresponding delete.
Depending on how the patch is applied, deleting first could be safer (avoids index drift).
Consider adding a second test asserting the alternative order or relaxing order using expect.arrayContaining.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 138c35a and e8d774a.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (15)
  • blocksuite/affine/shared/package.json (2 hunks)
  • blocksuite/affine/shared/src/__tests__/commands/block-crud/get-first-block.unit.spec.ts (1 hunks)
  • blocksuite/affine/shared/src/__tests__/commands/block-crud/get-last-block.unit.spec.ts (1 hunks)
  • blocksuite/affine/shared/src/__tests__/commands/model-crud/replace-selected-text-with-blocks.unit.spec.ts (1 hunks)
  • blocksuite/affine/shared/src/__tests__/commands/selection/is-nothing-selected.unit.spec.ts (1 hunks)
  • blocksuite/affine/shared/src/__tests__/test-utils/affine-template.unit.spec.ts (1 hunks)
  • blocksuite/affine/shared/src/test-utils/affine-template.ts (2 hunks)
  • blocksuite/affine/shared/src/test-utils/affine-test-utils.ts (1 hunks)
  • blocksuite/affine/shared/src/test-utils/create-test-host.ts (1 hunks)
  • blocksuite/affine/shared/src/test-utils/index.ts (1 hunks)
  • packages/frontend/core/package.json (1 hunks)
  • packages/frontend/core/src/__tests__/ai/utils/apply-model/apply-patch-to-doc.spec.ts (1 hunks)
  • packages/frontend/core/src/__tests__/ai/utils/apply-model/markdown-diff.spec.ts (1 hunks)
  • packages/frontend/core/src/blocksuite/ai/utils/apply-model/apply-patch-to-doc.ts (1 hunks)
  • packages/frontend/core/src/blocksuite/ai/utils/apply-model/markdown-diff.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (14)
  • GitHub Check: test-build-mobile-app / build-android-web
  • GitHub Check: test-build-mobile-app / build-ios-web
  • GitHub Check: loom thread test
  • GitHub Check: E2E Test (7)
  • GitHub Check: Run native tests
  • GitHub Check: fuzzing
  • GitHub Check: Build AFFiNE native (x86_64-pc-windows-msvc)
  • GitHub Check: Build AFFiNE native (aarch64-pc-windows-msvc)
  • GitHub Check: Build AFFiNE native (x86_64-apple-darwin)
  • GitHub Check: Analyze (javascript, blocksuite)
  • GitHub Check: Build Server native
  • GitHub Check: Analyze (typescript, blocksuite)
  • GitHub Check: Analyze (typescript, affine)
  • GitHub Check: Analyze (javascript, affine)
🔇 Additional comments (14)
blocksuite/affine/shared/package.json (2)

13-14: Add new workspace dependencies
Introducing @blocksuite/affine and @blocksuite/affine-ext-loader as workspace dependencies. Ensure these packages are at compatible versions and verify the monorepo build/bundling picks up the updates without conflict.


68-69: Expose new export paths for adapters and test-utils
Adding "./adapters" and "./test-utils" exports consolidates utilities for downstream imports. Confirm that these paths resolve correctly in TS builds and that the target files (src/adapters/index.ts, src/test-utils/index.ts) exist and re-export as intended.

blocksuite/affine/shared/src/test-utils/affine-test-utils.ts (1)

66-67: Refactor loop to use for...of with destructuring
Switching to for (const [i, child] of actual.children.entries()) is more concise and avoids index-based access errors. Implementation looks correct and improves readability.

packages/frontend/core/package.json (1)

22-22: Add @blocksuite/affine-shared as a workspace dependency
Linking the frontend core to the newly extended shared package is necessary for the markdown-diff and patch features. Ensure that all imports from @blocksuite/affine-shared resolve and that CI builds/tests pass.

blocksuite/affine/shared/src/test-utils/create-test-host.ts (1)

243-243: Clarify @ts-expect-error as a dev-only exception
Updating the comment to indicate a "dev-only" exception improves context for future maintainers. This change is purely cosmetic and safe.

blocksuite/affine/shared/src/__tests__/commands/block-crud/get-last-block.unit.spec.ts (1)

7-7: Update import path to consolidated test-utils module
Changing from helpers/affine-template to the new test-utils entry point aligns all tests to use the unified utilities. Verify no residual legacy imports remain.

blocksuite/affine/shared/src/__tests__/commands/block-crud/get-first-block.unit.spec.ts (1)

7-8: Centralised test-utils import looks correct

The new relative path resolves cleanly to src/test-utils and removes duplicate helper paths – ✅ from my side.

blocksuite/affine/shared/src/__tests__/commands/selection/is-nothing-selected.unit.spec.ts (1)

9-10: Good move to the barrel file

Importing from the barrel ../../../test-utils keeps the tests in sync with the new public export. No issues spotted.

blocksuite/affine/shared/src/__tests__/test-utils/affine-template.unit.spec.ts (1)

4-4: Barrel import ✅

The switch to ../../test-utils maintains correct depth and benefits maintainability.

blocksuite/affine/shared/src/__tests__/commands/model-crud/replace-selected-text-with-blocks.unit.spec.ts (1)

10-11: Barrel file gives you affine/block already – nice

No further action needed – import is correct.

blocksuite/affine/shared/src/test-utils/index.ts (1)

1-3: Consider export order where side-effects matter

affine-test-utils registers Vitest matchers via side-effects.
If in the future either affine-template or create-test-host also mutate globals, order may become significant.
Document (or enforce) the expected load order to avoid hard-to-trace issues.

No changes required right now.

packages/frontend/core/src/blocksuite/ai/utils/apply-model/apply-patch-to-doc.ts (1)

22-25: blockIdMap becomes stale after mutations

blockIdMap is built once and never updated.
If a later patch op references a block that was inserted or re-created by an earlier op in the same batch, the lookup will fail.
Either rebuild the map (cheap, couple of dozen nodes) inside the loop or switch to doc.getBlock(id) on demand.

packages/frontend/core/src/__tests__/ai/utils/apply-model/apply-patch-to-doc.spec.ts (1)

87-97: Test relies on deterministic IDs that the store may not guarantee

expected hard-codes paragraph-3, but insertFromMarkdown generates a fresh ID internally.
If the underlying ID generator changes these assertions will break while the feature still works.
Prefer structural assertions (e.g. compare text & order, ignore IDs) or fetch the inserted block ID from the store before asserting.

blocksuite/affine/shared/src/test-utils/affine-template.ts (1)

10-12: StoreExtensionManager.get('store') might be async

If get returns a promise (common for dynamic loaders) the current code will break at runtime.
Double-check the API and await if required.

Comment on lines +40 to +51
await insertFromMarkdown(undefined, op.new_content, doc, parentId, index);
} else if (op.op === 'insert_at') {
// Insert new block
const parentId = note.id;
const index = op.index;
await insertFromMarkdown(
undefined,
op.new_block.content,
doc,
parentId,
index
);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

insert_at ignores the requested block type

The insert_at branch passes only op.new_block.content to insertFromMarkdown, silently discarding op.new_block.type.
If the diff asks for something other than a paragraph (e.g. a title), the inserted block will default to a paragraph and the resulting document will diverge from the desired state.

-      await insertFromMarkdown(
-        undefined,
-        op.new_block.content,
-        doc,
-        parentId,
-        index
-      );
+      const markdown =
+        op.new_block.type === 'paragraph'
+          ? op.new_block.content
+          : `# ${op.new_block.content}`; // minimal example – build markdown per type
+      await insertFromMarkdown(undefined, markdown, doc, parentId, index);

Consider centralising the type ➜ markdown mapping instead of the quick-hack above.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await insertFromMarkdown(undefined, op.new_content, doc, parentId, index);
} else if (op.op === 'insert_at') {
// Insert new block
const parentId = note.id;
const index = op.index;
await insertFromMarkdown(
undefined,
op.new_block.content,
doc,
parentId,
index
);
await insertFromMarkdown(undefined, op.new_content, doc, parentId, index);
} else if (op.op === 'insert_at') {
// Insert new block
const parentId = note.id;
const index = op.index;
const markdown =
op.new_block.type === 'paragraph'
? op.new_block.content
: `# ${op.new_block.content}`; // minimal example – build markdown per type
await insertFromMarkdown(undefined, markdown, doc, parentId, index);
🤖 Prompt for AI Agents
In
packages/frontend/core/src/blocksuite/ai/utils/apply-model/apply-patch-to-doc.ts
around lines 40 to 51, the insert_at case calls insertFromMarkdown with only the
content, ignoring the block type from op.new_block.type, causing all inserted
blocks to default to paragraphs. To fix this, modify the call to
insertFromMarkdown to include the block type by converting op.new_block.type to
the appropriate markdown format and passing it along with the content, ensuring
the inserted block matches the requested type. Consider centralizing the
type-to-markdown conversion logic to maintain consistency and avoid duplication.

Comment on lines +21 to +24
for (const line of lines) {
const match = line.match(/^<!--\s*block_id=(.*?)\s+type=(.*?)\s*-->/);
if (match) {
// If there is a block being collected, push it into blocks first
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Regex misses comments with leading whitespace

/^<!-- … -->/ fails when the comment is indented (common inside markdown lists).
Trim the line or allow optional leading space:

-    const match = line.match(/^<!--\s*block_id=(.*?)\s+type=(.*?)\s*-->/);
+    const match = line.match(/^\s*<!--\s*block_id=(.*?)\s+type=(.*?)\s*-->/);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const line of lines) {
const match = line.match(/^<!--\s*block_id=(.*?)\s+type=(.*?)\s*-->/);
if (match) {
// If there is a block being collected, push it into blocks first
for (const line of lines) {
const match = line.match(/^\s*<!--\s*block_id=(.*?)\s+type=(.*?)\s*-->/);
if (match) {
// If there is a block being collected, push it into blocks first
🤖 Prompt for AI Agents
In packages/frontend/core/src/blocksuite/ai/utils/apply-model/markdown-diff.ts
around lines 21 to 24, the regex used to match HTML comments does not account
for leading whitespace, causing it to miss indented comments. Modify the regex
to allow optional leading whitespace before the comment start, or trim the line
before applying the regex, so that indented comments inside markdown lists are
correctly matched.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
app:core docs Improvements or additions to documentation test Related to test cases
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

1 participant