Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/api/crepe.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const defaultFeatures: Record<CrepeFeature, boolean> = {
[Crepe.Feature.Table]: true,
[Crepe.Feature.Latex]: true,
[Crepe.Feature.TopBar]: false,
[Crepe.Feature.Diff]: false,
}
```

Expand Down
173 changes: 173 additions & 0 deletions docs/api/plugin-diff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# @milkdown/plugin-diff

Diff review plugin for [milkdown](https://milkdown.dev/). Compares two documents and lets users accept or reject individual changes.

## Usage

```typescript
import { Editor } from '@milkdown/kit/core'
import { diff } from '@milkdown/kit/plugin/diff'
import { diffComponent } from '@milkdown/kit/component/diff'
import { commonmark } from '@milkdown/kit/preset/commonmark'

const editor = await Editor.make()
.use(commonmark)
.use(diff)
.use(diffComponent)
.create()
```

### With Crepe

```typescript
import { Crepe, CrepeFeature } from '@milkdown/crepe'

const crepe = new Crepe({
root: '#editor',
features: {
[CrepeFeature.Diff]: true,
},
})
await crepe.create()
```

## Starting a Diff Review

Pass the modified markdown to `startDiffReviewCmd`. The editor will show the differences and lock editing until the review is complete.

```typescript
import { callCommand } from '@milkdown/kit/utils'
import { startDiffReviewCmd } from '@milkdown/kit/plugin/diff'

editor.action(
callCommand(startDiffReviewCmd.key, '# Updated content\n\nNew paragraph.')
)
```

## Accepting and Rejecting Changes

Users can click the Accept/Reject buttons on each change in the UI. You can also control this programmatically:

```typescript
import { callCommand } from '@milkdown/kit/utils'
import {
acceptAllDiffsCmd,
rejectAllDiffsCmd,
clearDiffReviewCmd,
acceptDiffChunkCmd,
rejectDiffChunkCmd,
} from '@milkdown/kit/plugin/diff'

// Accept all remaining changes
editor.action(callCommand(acceptAllDiffsCmd.key))

// Reject all remaining changes
editor.action(callCommand(rejectAllDiffsCmd.key))

// Cancel the review without applying anything
editor.action(callCommand(clearDiffReviewCmd.key))

// Accept/reject a specific change by index
editor.action(callCommand(acceptDiffChunkCmd.key, 0))
editor.action(callCommand(rejectDiffChunkCmd.key, 0))
```

The diff automatically deactivates and unlocks the editor when all changes have been resolved.

## Plugin Configuration

```typescript
import { diffConfig } from '@milkdown/kit/plugin/diff'

Editor.make()
.config((ctx) => {
ctx.update(diffConfig.key, (prev) => ({
...prev,
lockOnReview: false, // Allow editing during diff review (default: true)
}))
})
.use(diff)
.use(diffComponent)
.create()
```

## Component Configuration

The diff component handles the visual rendering of changes. It can be configured through `diffComponentConfig`:

```typescript
import { diffComponentConfig } from '@milkdown/kit/component/diff'

Editor.make()
.config((ctx) => {
ctx.update(diffComponentConfig.key, (prev) => ({
...prev,
classPrefix: 'my-diff', // CSS class prefix (default: 'milkdown-diff')
acceptLabel: 'Apply', // Accept button text (default: 'Accept')
rejectLabel: 'Discard', // Reject button text (default: 'Reject')
customBlockTypes: [
// Node types using custom node views
'table',
'image-block',
'code_block',
],
}))
})
.use(diff)
.use(diffComponent)
.create()
```

### Custom Block Types

ProseMirror's inline decorations cannot penetrate custom node views. The `customBlockTypes` option tells the diff component which node types need block-level replacement handling instead of inline decorations.

When using Crepe, this is pre-configured with `['table', 'image-block', 'code_block']`.

## Styling

The diff component uses CSS classes that you need to style. When using Crepe, styles are included in the theme CSS automatically.

For standalone usage, the main CSS classes are:

| Class | Description |
| ------------------------------- | ------------------------------------------ |
| `.milkdown-diff-removed` | Inline deletion (strikethrough) |
| `.milkdown-diff-removed-block` | Block-level deletion (node overlay) |
| `.milkdown-diff-added` | Inline insertion |
| `.milkdown-diff-added-block` | Block-level insertion widget |
| `.milkdown-diff-controls` | Inline Accept/Reject button container |
| `.milkdown-diff-controls-block` | Block-level Accept/Reject button container |
| `.milkdown-diff-accept` | Accept button |
| `.milkdown-diff-reject` | Reject button |

## Plugin

@diff
@diffPlugin
@diffPluginKey
@diffConfig

## Commands

@startDiffReviewCmd
@acceptDiffChunkCmd
@rejectDiffChunkCmd
@acceptDiffRangeCmd
@rejectDiffRangeCmd
@acceptAllDiffsCmd
@rejectAllDiffsCmd
@clearDiffReviewCmd

## Utilities

@computeDocDiff
@getPendingChanges
@isChangeRejected

## Types

@DiffState
@DiffConfig
@DiffRange
@DiffAction
7 changes: 7 additions & 0 deletions e2e/shim.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,11 @@ declare global {
var __macros__: {
insert: typeof insert
}

var __applyDiff__: (markdown: string) => boolean
var __acceptAll__: () => boolean
var __rejectAll__: () => boolean
var __clearDiff__: () => boolean
var __acceptChunk__: (index: number) => boolean
var __rejectChunk__: (index: number) => boolean
}
11 changes: 11 additions & 0 deletions e2e/src/crepe-diff/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Crepe Diff</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/crepe-diff/main.ts"></script>
</body>
</html>
4 changes: 4 additions & 0 deletions e2e/src/crepe-diff/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const crepeDiff = {
title: 'Crepe Diff',
link: '/crepe-diff/',
}
34 changes: 34 additions & 0 deletions e2e/src/crepe-diff/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Crepe, CrepeFeature } from '@milkdown/crepe'
import '@milkdown/crepe/theme/common/style.css'
import '@milkdown/crepe/theme/frame.css'
import { callCommand } from '@milkdown/utils'

import { setup } from '../utils'

// Command names match the string IDs registered by $command() in plugin-diff.
// Using strings here because @milkdown/kit is not a direct dependency of e2e.
setup(async () => {
const crepe = new Crepe({
root: '#app',
features: {
[CrepeFeature.Diff]: true,
},
})
globalThis.__crepe__ = crepe
await crepe.create()

globalThis.__applyDiff__ = (markdown: string) =>
crepe.editor.action(callCommand('StartDiffReview', markdown))
globalThis.__acceptAll__ = () =>
crepe.editor.action(callCommand('AcceptAllDiffs'))
globalThis.__rejectAll__ = () =>
crepe.editor.action(callCommand('RejectAllDiffs'))
globalThis.__clearDiff__ = () =>
crepe.editor.action(callCommand('ClearDiffReview'))
globalThis.__acceptChunk__ = (index: number) =>
crepe.editor.action(callCommand('AcceptDiffChunk', index))
globalThis.__rejectChunk__ = (index: number) =>
crepe.editor.action(callCommand('RejectDiffChunk', index))

return crepe.editor
}).catch(console.error)
2 changes: 2 additions & 0 deletions e2e/src/data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { crepe } from './crepe'
import { crepeDiff } from './crepe-diff'
import { crepeTopBar } from './crepe-top-bar'
import { imageBlock } from './image-block'
import { multiEditor } from './multi-editor'
Expand All @@ -14,6 +15,7 @@ export const cases: { title: string; link: string }[] = [
listener,
automd,
crepe,
crepeDiff,
crepeTopBar,
imageBlock,
]
Loading
Loading