@jvs-milkdown/crepe 1.2.13 → 1.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/lib/cjs/builder.js +41 -2
  2. package/lib/cjs/builder.js.map +1 -1
  3. package/lib/cjs/feature/block-edit/index.js +9 -2
  4. package/lib/cjs/feature/block-edit/index.js.map +1 -1
  5. package/lib/cjs/feature/code-mirror/index.js +9 -2
  6. package/lib/cjs/feature/code-mirror/index.js.map +1 -1
  7. package/lib/cjs/feature/cursor/index.js +9 -2
  8. package/lib/cjs/feature/cursor/index.js.map +1 -1
  9. package/lib/cjs/feature/image-block/index.js +9 -2
  10. package/lib/cjs/feature/image-block/index.js.map +1 -1
  11. package/lib/cjs/feature/inline-diff/index.js +1298 -0
  12. package/lib/cjs/feature/inline-diff/index.js.map +1 -0
  13. package/lib/cjs/feature/latex/index.js +9 -2
  14. package/lib/cjs/feature/latex/index.js.map +1 -1
  15. package/lib/cjs/feature/link-tooltip/index.js +9 -2
  16. package/lib/cjs/feature/link-tooltip/index.js.map +1 -1
  17. package/lib/cjs/feature/list-item/index.js +9 -2
  18. package/lib/cjs/feature/list-item/index.js.map +1 -1
  19. package/lib/cjs/feature/placeholder/index.js +9 -2
  20. package/lib/cjs/feature/placeholder/index.js.map +1 -1
  21. package/lib/cjs/feature/table/index.js +9 -2
  22. package/lib/cjs/feature/table/index.js.map +1 -1
  23. package/lib/cjs/feature/toolbar/index.js +14 -6
  24. package/lib/cjs/feature/toolbar/index.js.map +1 -1
  25. package/lib/cjs/index.js +1280 -260
  26. package/lib/cjs/index.js.map +1 -1
  27. package/lib/esm/builder.js +41 -2
  28. package/lib/esm/builder.js.map +1 -1
  29. package/lib/esm/feature/block-edit/index.js +9 -2
  30. package/lib/esm/feature/block-edit/index.js.map +1 -1
  31. package/lib/esm/feature/code-mirror/index.js +9 -2
  32. package/lib/esm/feature/code-mirror/index.js.map +1 -1
  33. package/lib/esm/feature/cursor/index.js +9 -2
  34. package/lib/esm/feature/cursor/index.js.map +1 -1
  35. package/lib/esm/feature/image-block/index.js +9 -2
  36. package/lib/esm/feature/image-block/index.js.map +1 -1
  37. package/lib/esm/feature/inline-diff/index.js +1274 -0
  38. package/lib/esm/feature/inline-diff/index.js.map +1 -0
  39. package/lib/esm/feature/latex/index.js +9 -2
  40. package/lib/esm/feature/latex/index.js.map +1 -1
  41. package/lib/esm/feature/link-tooltip/index.js +9 -2
  42. package/lib/esm/feature/link-tooltip/index.js.map +1 -1
  43. package/lib/esm/feature/list-item/index.js +9 -2
  44. package/lib/esm/feature/list-item/index.js.map +1 -1
  45. package/lib/esm/feature/placeholder/index.js +9 -2
  46. package/lib/esm/feature/placeholder/index.js.map +1 -1
  47. package/lib/esm/feature/table/index.js +9 -2
  48. package/lib/esm/feature/table/index.js.map +1 -1
  49. package/lib/esm/feature/toolbar/index.js +14 -6
  50. package/lib/esm/feature/toolbar/index.js.map +1 -1
  51. package/lib/esm/index.js +1262 -261
  52. package/lib/esm/index.js.map +1 -1
  53. package/lib/theme/common/diff-block.css +41 -0
  54. package/lib/theme/common/inline-diff.css +142 -0
  55. package/lib/theme/common/list-item.css +113 -0
  56. package/lib/theme/common/style.css +2 -0
  57. package/lib/tsconfig.tsbuildinfo +1 -1
  58. package/lib/types/core/builder.d.ts +2 -0
  59. package/lib/types/core/builder.d.ts.map +1 -1
  60. package/lib/types/core/locale.d.ts +4 -0
  61. package/lib/types/core/locale.d.ts.map +1 -1
  62. package/lib/types/feature/diff-block/index.d.ts +10 -0
  63. package/lib/types/feature/diff-block/index.d.ts.map +1 -0
  64. package/lib/types/feature/fixed-toolbar/index.d.ts.map +1 -1
  65. package/lib/types/feature/fixed-toolbar/outline-panel.d.ts.map +1 -1
  66. package/lib/types/feature/index.d.ts +7 -1
  67. package/lib/types/feature/index.d.ts.map +1 -1
  68. package/lib/types/feature/inline-diff/change-panel.d.ts +4 -0
  69. package/lib/types/feature/inline-diff/change-panel.d.ts.map +1 -0
  70. package/lib/types/feature/inline-diff/config.d.ts +12 -0
  71. package/lib/types/feature/inline-diff/config.d.ts.map +1 -0
  72. package/lib/types/feature/inline-diff/diff-engine.d.ts +20 -0
  73. package/lib/types/feature/inline-diff/diff-engine.d.ts.map +1 -0
  74. package/lib/types/feature/inline-diff/diff-view.d.ts +2 -0
  75. package/lib/types/feature/inline-diff/diff-view.d.ts.map +1 -0
  76. package/lib/types/feature/inline-diff/doc-builder.d.ts +21 -0
  77. package/lib/types/feature/inline-diff/doc-builder.d.ts.map +1 -0
  78. package/lib/types/feature/inline-diff/index.d.ts +9 -0
  79. package/lib/types/feature/inline-diff/index.d.ts.map +1 -0
  80. package/lib/types/feature/loader.d.ts.map +1 -1
  81. package/lib/types/feature/toolbar/component.d.ts.map +1 -1
  82. package/package.json +15 -4
  83. package/src/core/builder.ts +19 -0
  84. package/src/core/locale.ts +7 -0
  85. package/src/feature/diff-block/index.ts +48 -0
  86. package/src/feature/fixed-toolbar/index.ts +62 -23
  87. package/src/feature/fixed-toolbar/outline-panel.tsx +5 -3
  88. package/src/feature/index.ts +12 -0
  89. package/src/feature/inline-diff/change-panel.ts +280 -0
  90. package/src/feature/inline-diff/config.ts +28 -0
  91. package/src/feature/inline-diff/diff-engine.ts +181 -0
  92. package/src/feature/inline-diff/diff-view.ts +2 -0
  93. package/src/feature/inline-diff/doc-builder.ts +139 -0
  94. package/src/feature/inline-diff/index.ts +514 -0
  95. package/src/feature/loader.ts +8 -0
  96. package/src/feature/toolbar/component.tsx +3 -2
  97. package/src/theme/common/diff-block.css +43 -0
  98. package/src/theme/common/inline-diff.css +148 -0
  99. package/src/theme/common/list-item.css +122 -0
  100. package/src/theme/common/style.css +2 -0
@@ -0,0 +1,514 @@
1
+ import type { Ctx } from '@jvs-milkdown/kit/ctx'
2
+ import type { Node as PMNode } from '@jvs-milkdown/kit/prose/model'
3
+ import type { PluginView } from '@jvs-milkdown/kit/prose/state'
4
+ import type { EditorView } from '@jvs-milkdown/kit/prose/view'
5
+
6
+ import { parserCtx } from '@jvs-milkdown/kit/core'
7
+ import { Plugin, PluginKey } from '@jvs-milkdown/kit/prose/state'
8
+ import { Decoration, DecorationSet } from '@jvs-milkdown/kit/prose/view'
9
+ import { $prose } from '@jvs-milkdown/kit/utils'
10
+
11
+ import type { DefineFeature } from '../shared'
12
+
13
+ import { i18n } from '../../core/locale'
14
+ import { crepeFeatureConfig } from '../../core/slice'
15
+ import { CrepeFeature } from '../index'
16
+ import { mountChangePanel } from './change-panel'
17
+ import {
18
+ inlineDiffConfig,
19
+ inlineDiffApiCtx,
20
+ type InlineDiffConfig,
21
+ type InlineDiffApi,
22
+ } from './config'
23
+ import { computeDiff, type DiffChunk } from './diff-engine'
24
+ import { buildNewDoc, type ChangeInfo } from './doc-builder'
25
+
26
+ export type InlineDiffFeatureConfig = Partial<InlineDiffConfig>
27
+
28
+ export { inlineDiffApiCtx, type InlineDiffApi }
29
+
30
+ export const inlineDiffKey = new PluginKey('CREPE_INLINE_DIFF')
31
+
32
+ const PANEL_WIDTH = 280
33
+
34
+ let sharedDecorationSet = DecorationSet.empty
35
+
36
+ class InlineDiffView implements PluginView {
37
+ #ctx: Ctx
38
+ #view: EditorView
39
+ #isActive = false
40
+ #panelApp: ReturnType<typeof mountChangePanel> | null = null
41
+ #panelContainer: HTMLElement | null = null
42
+ #resizeObserver: ResizeObserver | null = null
43
+ #scrollContainers: Element[] = []
44
+ #updateGeometry: (() => void) | null = null
45
+ #originalEditable: ((state: any) => boolean) | null = null
46
+ #chunks: DiffChunk[] = []
47
+ #schema: any = null
48
+
49
+ constructor(ctx: Ctx, view: EditorView) {
50
+ this.#ctx = ctx
51
+ this.#view = view
52
+
53
+ const api: InlineDiffApi = {
54
+ showDiff: (oldMd: string, newMd?: string) => this.showDiff(oldMd, newMd),
55
+ hideDiff: () => this.hideDiff(),
56
+ isShowing: () => this.#isActive,
57
+ }
58
+ ctx.set(inlineDiffApiCtx.key, api)
59
+ }
60
+
61
+ showDiff = (oldMarkdown: string, newMarkdown?: string): void => {
62
+ if (this.#isActive) return
63
+
64
+ const parser = this.#ctx.get(parserCtx)
65
+ this.#schema = this.#view.state.schema
66
+
67
+ let oldDoc: PMNode
68
+ let newDoc: PMNode
69
+ try {
70
+ oldDoc = parser(oldMarkdown)
71
+ if (typeof newMarkdown === 'string' && newMarkdown.trim() !== '') {
72
+ newDoc = parser(newMarkdown)
73
+ } else {
74
+ newDoc = this.#view.state.doc
75
+ }
76
+ } catch {
77
+ return
78
+ }
79
+
80
+ const oldBlocks: PMNode[] = []
81
+ const newBlocks: PMNode[] = []
82
+ oldDoc.forEach((node) => oldBlocks.push(node))
83
+ newDoc.forEach((node) => newBlocks.push(node))
84
+
85
+ this.#chunks = computeDiff(oldBlocks, newBlocks)
86
+
87
+ const { changes } = buildNewDoc(this.#chunks, this.#schema)
88
+ if (changes.length === 0) {
89
+ return
90
+ }
91
+
92
+ if (!this.#originalEditable) {
93
+ this.#originalEditable =
94
+ (this.#view.someProp('editable') as any) || (() => true)
95
+ }
96
+ this.#view.setProps({
97
+ editable: () => false,
98
+ })
99
+
100
+ this.#isActive = true
101
+ this.#render(true)
102
+ }
103
+
104
+ #render = (initPanel = false) => {
105
+ const { doc: newDocWithPos, changes } = buildNewDoc(
106
+ this.#chunks,
107
+ this.#schema
108
+ )
109
+ const acceptText = i18n(this.#ctx, 'inlineDiff.accept')
110
+ const rejectText = i18n(this.#ctx, 'inlineDiff.reject')
111
+
112
+ const ds = buildDecorations(
113
+ newDocWithPos,
114
+ changes,
115
+ (id) => this.acceptChunk(id),
116
+ (id) => this.rejectChunk(id),
117
+ acceptText,
118
+ rejectText
119
+ )
120
+ sharedDecorationSet = ds
121
+
122
+ const tr = this.#view.state.tr.replaceWith(
123
+ 0,
124
+ this.#view.state.doc.content.size,
125
+ newDocWithPos.content
126
+ )
127
+ this.#view.dispatch(tr)
128
+
129
+ if (initPanel) {
130
+ this.#createPanel(changes)
131
+ } else if (this.#panelApp) {
132
+ this.#createPanel(changes)
133
+ }
134
+ }
135
+
136
+ acceptChunk = (id: string) => {
137
+ const chunk = this.#chunks.find((c) => c.id === id)
138
+ if (chunk) {
139
+ chunk.status = 'accepted'
140
+ const hasPending = this.#chunks.some(
141
+ (c) =>
142
+ c.status === 'pending' && c.diffs.some((d) => d.type !== 'unchanged')
143
+ )
144
+ if (!hasPending) {
145
+ this.hideDiff()
146
+ } else {
147
+ this.#render()
148
+ }
149
+ }
150
+ }
151
+
152
+ rejectChunk = (id: string) => {
153
+ const chunk = this.#chunks.find((c) => c.id === id)
154
+ if (chunk) {
155
+ chunk.status = 'rejected'
156
+ const hasPending = this.#chunks.some(
157
+ (c) =>
158
+ c.status === 'pending' && c.diffs.some((d) => d.type !== 'unchanged')
159
+ )
160
+ if (!hasPending) {
161
+ this.hideDiff()
162
+ } else {
163
+ this.#render()
164
+ }
165
+ }
166
+ }
167
+
168
+ hideDiff = (): void => {
169
+ if (!this.#isActive) return
170
+ this.#removePanel()
171
+
172
+ for (const chunk of this.#chunks) {
173
+ if (chunk.status === 'pending') {
174
+ chunk.status = 'accepted'
175
+ }
176
+ }
177
+
178
+ const { doc: finalDoc } = buildNewDoc(this.#chunks, this.#schema)
179
+ const tr = this.#view.state.tr.replaceWith(
180
+ 0,
181
+ this.#view.state.doc.content.size,
182
+ finalDoc.content
183
+ )
184
+
185
+ sharedDecorationSet = DecorationSet.empty
186
+ this.#isActive = false
187
+ this.#chunks = []
188
+
189
+ this.#view.dispatch(tr)
190
+
191
+ if (this.#originalEditable) {
192
+ this.#view.setProps({
193
+ editable: this.#originalEditable,
194
+ })
195
+ this.#originalEditable = null
196
+ } else {
197
+ this.#view.setProps({
198
+ editable: () => true,
199
+ })
200
+ }
201
+ }
202
+
203
+ update = (view: EditorView) => {
204
+ this.#view = view
205
+ }
206
+
207
+ destroy = () => {
208
+ this.#removePanel()
209
+ }
210
+
211
+ #scrollTo(pos: number) {
212
+ try {
213
+ const view = this.#view
214
+ const coords = view.coordsAtPos(pos)
215
+
216
+ let scrollTarget: HTMLElement | null = view.dom as HTMLElement
217
+ while (scrollTarget) {
218
+ const { overflowY } = getComputedStyle(scrollTarget)
219
+ if (
220
+ (overflowY === 'auto' || overflowY === 'scroll') &&
221
+ scrollTarget.scrollHeight > scrollTarget.clientHeight
222
+ ) {
223
+ break
224
+ }
225
+ scrollTarget = scrollTarget.parentElement
226
+ }
227
+
228
+ if (scrollTarget) {
229
+ const targetRect = scrollTarget.getBoundingClientRect()
230
+ const scrollOffset =
231
+ coords.top - targetRect.top + scrollTarget.scrollTop - 60
232
+ scrollTarget.scrollTo({ top: scrollOffset, behavior: 'smooth' })
233
+ } else {
234
+ window.scrollTo({
235
+ top: coords.top + window.scrollY - 100,
236
+ behavior: 'smooth',
237
+ })
238
+ }
239
+ } catch {
240
+ // ignore
241
+ }
242
+ }
243
+
244
+ #createPanel(changes: ChangeInfo[]) {
245
+ this.#removePanel()
246
+
247
+ const editorContainer = this.#view.dom.parentElement
248
+ if (!editorContainer) return
249
+ const root = editorContainer.parentElement
250
+ if (!root) return
251
+
252
+ // Create fixed-positioned container — same pattern as outline panel
253
+ const container = document.createElement('div')
254
+ container.style.position = 'fixed'
255
+ container.style.zIndex = '100'
256
+ container.style.width = `${PANEL_WIDTH}px`
257
+ container.style.overflow = 'hidden'
258
+ container.style.borderLeft =
259
+ '1px solid var(--crepe-color-outline-variant, color-mix(in srgb, var(--crepe-color-outline, #ddd), transparent 80%))'
260
+ container.style.backgroundColor = 'var(--crepe-color-surface, #fff)'
261
+ root.appendChild(container)
262
+ this.#panelContainer = container
263
+
264
+ // Push editor content left to make room for panel
265
+ editorContainer.style.paddingRight = `${PANEL_WIDTH}px`
266
+
267
+ this.#panelApp = mountChangePanel(
268
+ container,
269
+ changes,
270
+ (from) => this.#scrollTo(from),
271
+ () => this.hideDiff()
272
+ )
273
+
274
+ // Geometry: compute top/right/height to align with editor area
275
+ this.#updateGeometry = () => {
276
+ const rootRect = root.getBoundingClientRect()
277
+ let top = rootRect.top
278
+
279
+ const toolbar = root.querySelector(
280
+ '.milkdown-fixed-toolbar'
281
+ ) as HTMLElement | null
282
+ if (toolbar && toolbar.offsetHeight > 0) {
283
+ const toolbarRect = toolbar.getBoundingClientRect()
284
+ top = Math.max(top, toolbarRect.bottom)
285
+ }
286
+
287
+ const coverEl = root.querySelector(
288
+ '.milkdown-document-cover'
289
+ ) as HTMLElement | null
290
+ if (coverEl && coverEl.offsetHeight > 0) {
291
+ top = Math.max(top, coverEl.getBoundingClientRect().bottom)
292
+ }
293
+
294
+ let height = window.innerHeight - top
295
+ if (top + height > rootRect.bottom) {
296
+ height = Math.max(0, rootRect.bottom - top)
297
+ }
298
+ container.style.top = `${top}px`
299
+ container.style.height = `${height}px`
300
+ container.style.left = 'auto'
301
+
302
+ let baseOffset = window.innerWidth - rootRect.right
303
+ let rightOffset = baseOffset
304
+ let hasVScroll = false
305
+ if (document.documentElement.scrollHeight > window.innerHeight) {
306
+ hasVScroll = true
307
+ }
308
+ let parent: HTMLElement | null = root
309
+ while (parent && parent !== document.body) {
310
+ const { overflowY } = getComputedStyle(parent)
311
+ if (overflowY === 'auto' || overflowY === 'scroll') {
312
+ if (parent.scrollHeight > parent.clientHeight) {
313
+ hasVScroll = true
314
+ break
315
+ }
316
+ }
317
+ parent = parent.parentElement
318
+ }
319
+ if (hasVScroll) {
320
+ rightOffset = Math.max(rightOffset, 32)
321
+ } else {
322
+ rightOffset = Math.max(rightOffset, 0)
323
+ }
324
+ container.style.right = `${rightOffset}px`
325
+
326
+ // Ensure paddingRight is set to accommodate the differences panel
327
+ editorContainer.style.paddingRight = `${PANEL_WIDTH}px`
328
+ }
329
+
330
+ this.#updateGeometry()
331
+
332
+ setTimeout(() => {
333
+ this.#updateGeometry?.()
334
+ }, 100)
335
+
336
+ // Re-evaluate on resize / scroll
337
+ this.#resizeObserver = new ResizeObserver(this.#updateGeometry)
338
+ this.#resizeObserver.observe(root)
339
+ window.addEventListener('resize', this.#updateGeometry)
340
+ window.addEventListener('scroll', this.#updateGeometry, {
341
+ capture: true,
342
+ passive: true,
343
+ })
344
+
345
+ let scrollerNode: Node | null = root
346
+ while (scrollerNode && scrollerNode !== document) {
347
+ if (scrollerNode instanceof Element) {
348
+ scrollerNode.addEventListener('scroll', this.#updateGeometry, {
349
+ passive: true,
350
+ })
351
+ this.#scrollContainers.push(scrollerNode)
352
+ }
353
+ scrollerNode =
354
+ scrollerNode.parentNode || (scrollerNode as ShadowRoot).host
355
+ }
356
+ }
357
+
358
+ #removePanel() {
359
+ if (this.#panelApp) {
360
+ this.#panelApp.unmount()
361
+ this.#panelApp = null
362
+ }
363
+ if (this.#panelContainer) {
364
+ this.#panelContainer.remove()
365
+ this.#panelContainer = null
366
+ }
367
+
368
+ if (this.#resizeObserver) {
369
+ this.#resizeObserver.disconnect()
370
+ this.#resizeObserver = null
371
+ }
372
+
373
+ if (this.#updateGeometry) {
374
+ window.removeEventListener('resize', this.#updateGeometry)
375
+ window.removeEventListener('scroll', this.#updateGeometry, {
376
+ capture: true,
377
+ })
378
+
379
+ for (const container of this.#scrollContainers) {
380
+ container.removeEventListener('scroll', this.#updateGeometry)
381
+ }
382
+ this.#scrollContainers = []
383
+ this.#updateGeometry = null
384
+ }
385
+
386
+ const editorContainer = this.#view.dom.parentElement
387
+ if (editorContainer) {
388
+ editorContainer.style.paddingRight = ''
389
+ }
390
+ }
391
+ }
392
+
393
+ function buildDecorations(
394
+ doc: PMNode,
395
+ changes: ChangeInfo[],
396
+ onAccept: (id: string) => void,
397
+ onReject: (id: string) => void,
398
+ acceptText: string,
399
+ rejectText: string
400
+ ): DecorationSet {
401
+ const decorations: Decoration[] = []
402
+ const addedWidgets = new Set<string>()
403
+
404
+ for (const change of changes) {
405
+ if (!addedWidgets.has(change.chunkId)) {
406
+ addedWidgets.add(change.chunkId)
407
+
408
+ decorations.push(
409
+ Decoration.widget(
410
+ change.blockRange.from,
411
+ () => {
412
+ const wrapper = document.createElement('div')
413
+ wrapper.style.position = 'relative'
414
+ wrapper.style.width = '100%'
415
+ wrapper.style.height = '0'
416
+ wrapper.style.overflow = 'visible'
417
+
418
+ const container = document.createElement('div')
419
+ container.className = 'crepe-diff-actions'
420
+ container.style.position = 'absolute'
421
+ container.style.right = '4px'
422
+ container.style.top = '4px'
423
+ container.style.zIndex = '10'
424
+ container.style.display = 'flex'
425
+ container.style.gap = '4px'
426
+
427
+ const btnAccept = document.createElement('button')
428
+ btnAccept.textContent = acceptText
429
+ btnAccept.style.backgroundColor = '#2ea043'
430
+ btnAccept.style.color = 'white'
431
+ btnAccept.style.border = 'none'
432
+ btnAccept.style.borderRadius = '4px'
433
+ btnAccept.style.padding = '2px 8px'
434
+ btnAccept.style.cursor = 'pointer'
435
+ btnAccept.style.fontSize = '12px'
436
+ btnAccept.addEventListener('mousedown', (e) => {
437
+ e.preventDefault()
438
+ onAccept(change.chunkId)
439
+ })
440
+
441
+ const btnReject = document.createElement('button')
442
+ btnReject.textContent = rejectText
443
+ btnReject.style.backgroundColor = '#f85149'
444
+ btnReject.style.color = 'white'
445
+ btnReject.style.border = 'none'
446
+ btnReject.style.borderRadius = '4px'
447
+ btnReject.style.padding = '2px 8px'
448
+ btnReject.style.cursor = 'pointer'
449
+ btnReject.style.fontSize = '12px'
450
+ btnReject.addEventListener('mousedown', (e) => {
451
+ e.preventDefault()
452
+ onReject(change.chunkId)
453
+ })
454
+
455
+ container.appendChild(btnAccept)
456
+ container.appendChild(btnReject)
457
+ wrapper.appendChild(container)
458
+ return wrapper
459
+ },
460
+ { side: -1 }
461
+ )
462
+ )
463
+ }
464
+
465
+ const blockCls =
466
+ change.type === 'added' ? 'crepe-diff-added' : 'crepe-diff-deleted'
467
+ decorations.push(
468
+ Decoration.node(change.blockRange.from, change.blockRange.to, {
469
+ class: blockCls,
470
+ })
471
+ )
472
+
473
+ const inlineCls =
474
+ change.type === 'added'
475
+ ? 'crepe-diff-inline-added'
476
+ : 'crepe-diff-inline-deleted'
477
+ for (const r of change.inlineRanges) {
478
+ if (r.to > r.from) {
479
+ decorations.push(Decoration.inline(r.from, r.to, { class: inlineCls }))
480
+ }
481
+ }
482
+ }
483
+
484
+ return DecorationSet.create(doc, decorations)
485
+ }
486
+
487
+ export const inlineDiffPlugin = $prose((ctx) => {
488
+ return new Plugin({
489
+ key: inlineDiffKey,
490
+ view: (view) => new InlineDiffView(ctx, view),
491
+ props: {
492
+ decorations: () => sharedDecorationSet,
493
+ },
494
+ })
495
+ })
496
+
497
+ export const inlineDiff: DefineFeature<InlineDiffFeatureConfig> = (
498
+ editor,
499
+ config
500
+ ) => {
501
+ editor
502
+ .config(crepeFeatureConfig(CrepeFeature.InlineDiff))
503
+ .config((ctx) => {
504
+ if (config) {
505
+ ctx.update(inlineDiffConfig.key, (prev) => ({
506
+ ...prev,
507
+ ...config,
508
+ }))
509
+ }
510
+ })
511
+ .use(inlineDiffApiCtx)
512
+ .use(inlineDiffConfig)
513
+ .use(inlineDiffPlugin)
514
+ }
@@ -4,9 +4,11 @@ import { attachment } from './attachment'
4
4
  import { blockEdit } from './block-edit'
5
5
  import { codeMirror } from './code-mirror'
6
6
  import { cursor } from './cursor'
7
+ import { diffBlockFeature } from './diff-block'
7
8
  import { fixedToolbar } from './fixed-toolbar'
8
9
  import { imageBlock } from './image-block'
9
10
  import { CrepeFeature } from './index'
11
+ import { inlineDiff } from './inline-diff'
10
12
  import { latex } from './latex'
11
13
  import { linkTooltip } from './link-tooltip'
12
14
  import { listItem } from './list-item'
@@ -56,5 +58,11 @@ export function loadFeature(
56
58
  case CrepeFeature.Attachment: {
57
59
  return attachment(editor, config)
58
60
  }
61
+ case CrepeFeature.InlineDiff: {
62
+ return inlineDiff(editor, config)
63
+ }
64
+ case CrepeFeature.DiffBlock: {
65
+ return diffBlockFeature(editor, config)
66
+ }
59
67
  }
60
68
  }
@@ -93,11 +93,11 @@ import {
93
93
  splitCellIcon,
94
94
  formatPainterIcon,
95
95
  } from '../../icons'
96
- import { keepAlive } from '../../utils/keep-alive'
97
96
  import {
98
97
  incrementPopupCount,
99
98
  decrementPopupCount,
100
99
  } from '../../utils/fixed-toolbar-popup-state'
100
+ import { keepAlive } from '../../utils/keep-alive'
101
101
  import { menuAPI } from '../block-edit/menu'
102
102
  import { getGroups as getBlockGroups } from '../block-edit/menu/config'
103
103
  import {
@@ -571,7 +571,8 @@ export const Toolbar = defineComponent<ToolbarProps>({
571
571
  containerWidth = container.clientWidth
572
572
  }
573
573
  } else {
574
- containerWidth = container.clientWidth
574
+ const view = ctx.get(editorViewCtx)
575
+ containerWidth = view.dom.clientWidth - 32
575
576
  }
576
577
 
577
578
  if (containerWidth === lastContainerWidth) {
@@ -0,0 +1,43 @@
1
+ .milkdown {
2
+ .milkdown-diff-block {
3
+ display: block;
4
+ position: relative;
5
+ padding: 16px 0;
6
+ margin: 20px 0;
7
+ border-radius: 8px;
8
+ background: var(--crepe-color-surface-low);
9
+ overflow: hidden;
10
+
11
+ .cm-editor {
12
+ outline: none !important;
13
+ background: transparent;
14
+ }
15
+
16
+ .cm-merge-a .cm-merge-chunk {
17
+ background-color: rgba(244, 63, 94, 0.15) !important;
18
+ }
19
+
20
+ .cm-merge-b .cm-merge-chunk {
21
+ background-color: rgba(16, 185, 129, 0.15) !important;
22
+ }
23
+
24
+ .cm-merge-chunk-a {
25
+ background-color: rgba(244, 63, 94, 0.3) !important;
26
+ }
27
+
28
+ .cm-merge-chunk-b {
29
+ background-color: rgba(16, 185, 129, 0.3) !important;
30
+ }
31
+
32
+ .cm-merge-spacer {
33
+ background-color: transparent !important;
34
+ background-image: repeating-linear-gradient(
35
+ 45deg,
36
+ rgba(150, 150, 150, 0.05),
37
+ rgba(150, 150, 150, 0.05) 10px,
38
+ transparent 10px,
39
+ transparent 20px
40
+ );
41
+ }
42
+ }
43
+ }