@dxos/ui-editor 0.10.0 → 0.11.0

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 (551) hide show
  1. package/dist/lib/chunk-diff.mjs +935 -0
  2. package/dist/lib/chunk-diff.mjs.map +1 -0
  3. package/dist/lib/headless.mjs +2 -0
  4. package/dist/lib/index.mjs +9937 -0
  5. package/dist/lib/index.mjs.map +1 -0
  6. package/dist/lib/types.mjs +18 -0
  7. package/dist/lib/types.mjs.map +1 -0
  8. package/dist/types/src/extensions/{bookmarks.d.ts → behavior/bookmarks.d.ts} +3 -0
  9. package/dist/types/src/extensions/behavior/bookmarks.d.ts.map +1 -0
  10. package/dist/types/src/extensions/behavior/dnd.d.ts.map +1 -0
  11. package/dist/types/src/extensions/behavior/index.d.ts +6 -0
  12. package/dist/types/src/extensions/behavior/index.d.ts.map +1 -0
  13. package/dist/types/src/extensions/{listener.d.ts → behavior/listener.d.ts} +3 -0
  14. package/dist/types/src/extensions/behavior/listener.d.ts.map +1 -0
  15. package/dist/types/src/extensions/{spacing.d.ts → behavior/spacing.d.ts} +3 -0
  16. package/dist/types/src/extensions/behavior/spacing.d.ts.map +1 -0
  17. package/dist/types/src/extensions/behavior/submit.d.ts.map +1 -0
  18. package/dist/types/src/extensions/collab/automerge/automerge.d.ts.map +1 -0
  19. package/dist/types/src/extensions/collab/automerge/automerge.test.d.ts.map +1 -0
  20. package/dist/types/src/extensions/{automerge → collab/automerge}/cursor.d.ts +1 -1
  21. package/dist/types/src/extensions/collab/automerge/cursor.d.ts.map +1 -0
  22. package/dist/types/src/extensions/{automerge → collab/automerge}/defs.d.ts +3 -1
  23. package/dist/types/src/extensions/collab/automerge/defs.d.ts.map +1 -0
  24. package/dist/types/src/extensions/collab/automerge/index.d.ts +3 -0
  25. package/dist/types/src/extensions/collab/automerge/index.d.ts.map +1 -0
  26. package/dist/types/src/extensions/collab/automerge/sync.d.ts.map +1 -0
  27. package/dist/types/src/extensions/{automerge → collab/automerge}/update-automerge.d.ts +4 -0
  28. package/dist/types/src/extensions/collab/automerge/update-automerge.d.ts.map +1 -0
  29. package/dist/types/src/extensions/{automerge → collab/automerge}/update-codemirror.d.ts +4 -0
  30. package/dist/types/src/extensions/collab/automerge/update-codemirror.d.ts.map +1 -0
  31. package/dist/types/src/extensions/collab/awareness/awareness-provider.d.ts.map +1 -0
  32. package/dist/types/src/extensions/{awareness → collab/awareness}/awareness.d.ts +4 -1
  33. package/dist/types/src/extensions/collab/awareness/awareness.d.ts.map +1 -0
  34. package/dist/types/src/extensions/collab/awareness/index.d.ts.map +1 -0
  35. package/dist/types/src/extensions/collab/index.d.ts +3 -0
  36. package/dist/types/src/extensions/collab/index.d.ts.map +1 -0
  37. package/dist/types/src/extensions/completion/autocomplete.d.ts.map +1 -0
  38. package/dist/types/src/extensions/{autocomplete → completion}/index.d.ts +1 -0
  39. package/dist/types/src/extensions/completion/index.d.ts.map +1 -0
  40. package/dist/types/src/extensions/{autocomplete → completion}/match.d.ts +2 -2
  41. package/dist/types/src/extensions/completion/match.d.ts.map +1 -0
  42. package/dist/types/src/extensions/{mention.d.ts → completion/mention.d.ts} +3 -0
  43. package/dist/types/src/extensions/completion/mention.d.ts.map +1 -0
  44. package/dist/types/src/extensions/completion/placeholder.d.ts.map +1 -0
  45. package/dist/types/src/extensions/{autocomplete → completion}/typeahead.d.ts +2 -2
  46. package/dist/types/src/extensions/completion/typeahead.d.ts.map +1 -0
  47. package/dist/types/src/extensions/{factories.d.ts → core/factories.d.ts} +23 -2
  48. package/dist/types/src/extensions/core/factories.d.ts.map +1 -0
  49. package/dist/types/src/extensions/core/factories.test.d.ts.map +1 -0
  50. package/dist/types/src/extensions/core/index.d.ts +3 -0
  51. package/dist/types/src/extensions/core/index.d.ts.map +1 -0
  52. package/dist/types/src/extensions/{modes.d.ts → core/modes.d.ts} +3 -0
  53. package/dist/types/src/extensions/core/modes.d.ts.map +1 -0
  54. package/dist/types/src/extensions/{debug.d.ts → debug/debug.d.ts} +3 -0
  55. package/dist/types/src/extensions/debug/debug.d.ts.map +1 -0
  56. package/dist/types/src/extensions/debug/index.d.ts +2 -0
  57. package/dist/types/src/extensions/debug/index.d.ts.map +1 -0
  58. package/dist/types/src/extensions/{annotations.d.ts → decoration/annotations.d.ts} +1 -1
  59. package/dist/types/src/extensions/decoration/annotations.d.ts.map +1 -0
  60. package/dist/types/src/extensions/decoration/assistant.d.ts +27 -0
  61. package/dist/types/src/extensions/decoration/assistant.d.ts.map +1 -0
  62. package/dist/types/src/extensions/decoration/fader.d.ts.map +1 -0
  63. package/dist/types/src/extensions/decoration/hashtag.d.ts +6 -0
  64. package/dist/types/src/extensions/decoration/hashtag.d.ts.map +1 -0
  65. package/dist/types/src/extensions/decoration/index.d.ts +8 -0
  66. package/dist/types/src/extensions/decoration/index.d.ts.map +1 -0
  67. package/dist/types/src/extensions/decoration/marker.d.ts.map +1 -0
  68. package/dist/types/src/extensions/{pos.d.ts → decoration/pos.d.ts} +40 -14
  69. package/dist/types/src/extensions/decoration/pos.d.ts.map +1 -0
  70. package/dist/types/src/extensions/decoration/pos.test.d.ts.map +1 -0
  71. package/dist/types/src/extensions/{replacer.d.ts → decoration/replacer.d.ts} +8 -8
  72. package/dist/types/src/extensions/decoration/replacer.d.ts.map +1 -0
  73. package/dist/types/src/extensions/decoration/replacer.test.d.ts.map +1 -0
  74. package/dist/types/src/extensions/{blast.d.ts → demo/blast.d.ts} +3 -0
  75. package/dist/types/src/extensions/demo/blast.d.ts.map +1 -0
  76. package/dist/types/src/extensions/demo/index.d.ts +3 -0
  77. package/dist/types/src/extensions/demo/index.d.ts.map +1 -0
  78. package/dist/types/src/extensions/demo/snippets.d.ts.map +1 -0
  79. package/dist/types/src/extensions/index.d.ts +10 -33
  80. package/dist/types/src/extensions/index.d.ts.map +1 -1
  81. package/dist/types/src/extensions/language/index.d.ts +5 -0
  82. package/dist/types/src/extensions/language/index.d.ts.map +1 -0
  83. package/dist/types/src/extensions/language/json/index.d.ts +2 -0
  84. package/dist/types/src/extensions/language/json/index.d.ts.map +1 -0
  85. package/dist/types/src/extensions/language/json/json.d.ts +12 -0
  86. package/dist/types/src/extensions/language/json/json.d.ts.map +1 -0
  87. package/dist/types/src/extensions/language/markdown/action.d.ts.map +1 -0
  88. package/dist/types/src/extensions/language/markdown/bundle.d.ts.map +1 -0
  89. package/dist/types/src/extensions/{markdown → language/markdown}/changes.d.ts +1 -1
  90. package/dist/types/src/extensions/language/markdown/changes.d.ts.map +1 -0
  91. package/dist/types/src/extensions/language/markdown/changes.test.d.ts.map +1 -0
  92. package/dist/types/src/extensions/{markdown → language/markdown}/debug.d.ts +3 -0
  93. package/dist/types/src/extensions/language/markdown/debug.d.ts.map +1 -0
  94. package/dist/types/src/extensions/{markdown → language/markdown}/decorate.d.ts +5 -1
  95. package/dist/types/src/extensions/language/markdown/decorate.d.ts.map +1 -0
  96. package/dist/types/src/extensions/{markdown → language/markdown}/formatting.d.ts +11 -2
  97. package/dist/types/src/extensions/language/markdown/formatting.d.ts.map +1 -0
  98. package/dist/types/src/extensions/language/markdown/formatting.test.d.ts.map +1 -0
  99. package/dist/types/src/extensions/{markdown → language/markdown}/highlight.d.ts +16 -12
  100. package/dist/types/src/extensions/language/markdown/highlight.d.ts.map +1 -0
  101. package/dist/types/src/extensions/language/markdown/image.d.ts.map +1 -0
  102. package/dist/types/src/extensions/language/markdown/image.test.d.ts.map +1 -0
  103. package/dist/types/src/extensions/language/markdown/index.d.ts.map +1 -0
  104. package/dist/types/src/extensions/language/markdown/link.d.ts +25 -0
  105. package/dist/types/src/extensions/language/markdown/link.d.ts.map +1 -0
  106. package/dist/types/src/extensions/language/markdown/link.test.d.ts +2 -0
  107. package/dist/types/src/extensions/language/markdown/link.test.d.ts.map +1 -0
  108. package/dist/types/src/extensions/language/markdown/parser.test.d.ts.map +1 -0
  109. package/dist/types/src/extensions/{markdown → language/markdown}/styles.d.ts +3 -0
  110. package/dist/types/src/extensions/language/markdown/styles.d.ts.map +1 -0
  111. package/dist/types/src/extensions/language/markdown/table.d.ts.map +1 -0
  112. package/dist/types/src/extensions/language/mermaid/index.d.ts +2 -0
  113. package/dist/types/src/extensions/language/mermaid/index.d.ts.map +1 -0
  114. package/dist/types/src/extensions/language/mermaid/mermaid.d.ts +18 -0
  115. package/dist/types/src/extensions/language/mermaid/mermaid.d.ts.map +1 -0
  116. package/dist/types/src/extensions/language/xml/extended-markdown.d.ts.map +1 -0
  117. package/dist/types/src/extensions/language/xml/extended-markdown.test.d.ts.map +1 -0
  118. package/dist/types/src/extensions/language/xml/index.d.ts.map +1 -0
  119. package/dist/types/src/extensions/language/xml/stub.d.ts +76 -0
  120. package/dist/types/src/extensions/language/xml/stub.d.ts.map +1 -0
  121. package/dist/types/src/extensions/language/xml/widgets/anchor.d.ts.map +1 -0
  122. package/dist/types/src/extensions/language/xml/widgets/index.d.ts.map +1 -0
  123. package/dist/types/src/extensions/language/xml/xml-block-decoration.d.ts +48 -0
  124. package/dist/types/src/extensions/language/xml/xml-block-decoration.d.ts.map +1 -0
  125. package/dist/types/src/extensions/language/xml/xml-formatting.d.ts.map +1 -0
  126. package/dist/types/src/extensions/{tags → language/xml}/xml-tags.d.ts +12 -1
  127. package/dist/types/src/extensions/language/xml/xml-tags.d.ts.map +1 -0
  128. package/dist/types/src/extensions/language/xml/xml-tags.test.d.ts +2 -0
  129. package/dist/types/src/extensions/language/xml/xml-tags.test.d.ts.map +1 -0
  130. package/dist/types/src/extensions/language/xml/xml-util.d.ts.map +1 -0
  131. package/dist/types/src/extensions/language/xml/xml-util.test.d.ts.map +1 -0
  132. package/dist/types/src/extensions/review/change-bar.d.ts +29 -0
  133. package/dist/types/src/extensions/review/change-bar.d.ts.map +1 -0
  134. package/dist/types/src/extensions/{comments.d.ts → review/comments.d.ts} +56 -38
  135. package/dist/types/src/extensions/review/comments.d.ts.map +1 -0
  136. package/dist/types/src/extensions/review/comments.test.d.ts +2 -0
  137. package/dist/types/src/extensions/review/comments.test.d.ts.map +1 -0
  138. package/dist/types/src/extensions/review/diff.d.ts +131 -0
  139. package/dist/types/src/extensions/review/diff.d.ts.map +1 -0
  140. package/dist/types/src/extensions/review/diff.test.d.ts +2 -0
  141. package/dist/types/src/extensions/review/diff.test.d.ts.map +1 -0
  142. package/dist/types/src/extensions/review/index.d.ts +7 -0
  143. package/dist/types/src/extensions/review/index.d.ts.map +1 -0
  144. package/dist/types/src/extensions/review/suggest.d.ts +89 -0
  145. package/dist/types/src/extensions/review/suggest.d.ts.map +1 -0
  146. package/dist/types/src/extensions/review/suggest.test.d.ts +2 -0
  147. package/dist/types/src/extensions/review/suggest.test.d.ts.map +1 -0
  148. package/dist/types/src/extensions/review/suggestions-overlay.d.ts +25 -0
  149. package/dist/types/src/extensions/review/suggestions-overlay.d.ts.map +1 -0
  150. package/dist/types/src/extensions/review/suggestions-overlay.test.d.ts +2 -0
  151. package/dist/types/src/extensions/review/suggestions-overlay.test.d.ts.map +1 -0
  152. package/dist/types/src/extensions/review/track-changes.d.ts +27 -0
  153. package/dist/types/src/extensions/review/track-changes.d.ts.map +1 -0
  154. package/dist/types/src/extensions/review/track-changes.test.d.ts +2 -0
  155. package/dist/types/src/extensions/review/track-changes.test.d.ts.map +1 -0
  156. package/dist/types/src/extensions/{busy.d.ts → state/busy.d.ts} +1 -0
  157. package/dist/types/src/extensions/state/busy.d.ts.map +1 -0
  158. package/dist/types/src/extensions/{focus.d.ts → state/focus.d.ts} +4 -0
  159. package/dist/types/src/extensions/state/focus.d.ts.map +1 -0
  160. package/dist/types/src/extensions/state/index.d.ts +5 -0
  161. package/dist/types/src/extensions/state/index.d.ts.map +1 -0
  162. package/dist/types/src/extensions/state/modal.d.ts.map +1 -0
  163. package/dist/types/src/extensions/{selection.d.ts → state/selection.d.ts} +1 -1
  164. package/dist/types/src/extensions/state/selection.d.ts.map +1 -0
  165. package/dist/types/src/extensions/state/selection.test.d.ts.map +1 -0
  166. package/dist/types/src/extensions/streaming/index.d.ts +4 -0
  167. package/dist/types/src/extensions/streaming/index.d.ts.map +1 -0
  168. package/dist/types/src/extensions/streaming/pending/index.d.ts.map +1 -0
  169. package/dist/types/src/extensions/streaming/pending/pending-text-stream.d.ts.map +1 -0
  170. package/dist/types/src/extensions/streaming/pending/pending-text-stream.test.d.ts.map +1 -0
  171. package/dist/types/src/extensions/{pending → streaming/pending}/pending-text.d.ts +1 -0
  172. package/dist/types/src/extensions/streaming/pending/pending-text.d.ts.map +1 -0
  173. package/dist/types/src/extensions/streaming/pending/pending-text.test.d.ts.map +1 -0
  174. package/dist/types/src/extensions/streaming/scrolling/auto-scroll.d.ts.map +1 -0
  175. package/dist/types/src/extensions/streaming/scrolling/crawler.d.ts.map +1 -0
  176. package/dist/types/src/extensions/streaming/scrolling/index.d.ts.map +1 -0
  177. package/dist/types/src/extensions/streaming/scrolling/scroll-past-end.d.ts +4 -0
  178. package/dist/types/src/extensions/streaming/scrolling/scroll-past-end.d.ts.map +1 -0
  179. package/dist/types/src/extensions/streaming/scrolling/scrollbar-autohide.d.ts.map +1 -0
  180. package/dist/types/src/extensions/streaming/scrolling/scroller.d.ts.map +1 -0
  181. package/dist/types/src/extensions/{typewriter.d.ts → streaming/typewriter.d.ts} +12 -12
  182. package/dist/types/src/extensions/streaming/typewriter.d.ts.map +1 -0
  183. package/dist/types/src/extensions/streaming/typewriter.test.d.ts.map +1 -0
  184. package/dist/types/src/extensions/structure/blocks/blocks.d.ts +52 -0
  185. package/dist/types/src/extensions/structure/blocks/blocks.d.ts.map +1 -0
  186. package/dist/types/src/extensions/structure/blocks/blocks.test.d.ts +2 -0
  187. package/dist/types/src/extensions/structure/blocks/blocks.test.d.ts.map +1 -0
  188. package/dist/types/src/extensions/structure/blocks/drag.d.ts +57 -0
  189. package/dist/types/src/extensions/structure/blocks/drag.d.ts.map +1 -0
  190. package/dist/types/src/extensions/structure/blocks/index.d.ts +5 -0
  191. package/dist/types/src/extensions/structure/blocks/index.d.ts.map +1 -0
  192. package/dist/types/src/extensions/structure/blocks/selection.d.ts +47 -0
  193. package/dist/types/src/extensions/structure/blocks/selection.d.ts.map +1 -0
  194. package/dist/types/src/extensions/structure/blocks/selection.test.d.ts +2 -0
  195. package/dist/types/src/extensions/structure/blocks/selection.test.d.ts.map +1 -0
  196. package/dist/types/src/extensions/structure/blocks/types.d.ts +17 -0
  197. package/dist/types/src/extensions/structure/blocks/types.d.ts.map +1 -0
  198. package/dist/types/src/extensions/structure/folding/folding.d.ts.map +1 -0
  199. package/dist/types/src/extensions/structure/folding/index.d.ts +3 -0
  200. package/dist/types/src/extensions/structure/folding/index.d.ts.map +1 -0
  201. package/dist/types/src/extensions/structure/folding/turns.d.ts +55 -0
  202. package/dist/types/src/extensions/structure/folding/turns.d.ts.map +1 -0
  203. package/dist/types/src/extensions/structure/folding/turns.test.d.ts +2 -0
  204. package/dist/types/src/extensions/structure/folding/turns.test.d.ts.map +1 -0
  205. package/dist/types/src/extensions/structure/index.d.ts +4 -0
  206. package/dist/types/src/extensions/structure/index.d.ts.map +1 -0
  207. package/dist/types/src/extensions/structure/outliner/commands.d.ts +31 -0
  208. package/dist/types/src/extensions/structure/outliner/commands.d.ts.map +1 -0
  209. package/dist/types/src/extensions/structure/outliner/dnd.d.ts +43 -0
  210. package/dist/types/src/extensions/structure/outliner/dnd.d.ts.map +1 -0
  211. package/dist/types/src/extensions/structure/outliner/dnd.test.d.ts +2 -0
  212. package/dist/types/src/extensions/structure/outliner/dnd.test.d.ts.map +1 -0
  213. package/dist/types/src/extensions/structure/outliner/editor.d.ts.map +1 -0
  214. package/dist/types/src/extensions/structure/outliner/editor.test.d.ts.map +1 -0
  215. package/dist/types/src/extensions/{outliner → structure/outliner}/index.d.ts +1 -0
  216. package/dist/types/src/extensions/structure/outliner/index.d.ts.map +1 -0
  217. package/dist/types/src/extensions/{outliner → structure/outliner}/menu.d.ts +1 -0
  218. package/dist/types/src/extensions/structure/outliner/menu.d.ts.map +1 -0
  219. package/dist/types/src/extensions/structure/outliner/outliner.d.ts.map +1 -0
  220. package/dist/types/src/extensions/structure/outliner/outliner.test.d.ts.map +1 -0
  221. package/dist/types/src/extensions/{outliner → structure/outliner}/tree.d.ts +1 -1
  222. package/dist/types/src/extensions/structure/outliner/tree.d.ts.map +1 -0
  223. package/dist/types/src/extensions/structure/outliner/tree.test.d.ts.map +1 -0
  224. package/dist/types/src/headless.d.ts +4 -0
  225. package/dist/types/src/headless.d.ts.map +1 -0
  226. package/dist/types/src/styles/theme.d.ts +1 -1
  227. package/dist/types/src/styles/theme.d.ts.map +1 -1
  228. package/dist/types/src/util/util.d.ts +2 -0
  229. package/dist/types/src/util/util.d.ts.map +1 -1
  230. package/dist/types/tsconfig.tsbuildinfo +1 -1
  231. package/package.json +48 -42
  232. package/src/defaults.ts +1 -1
  233. package/src/extensions/{bookmarks.ts → behavior/bookmarks.ts} +24 -14
  234. package/src/extensions/behavior/index.ts +9 -0
  235. package/src/extensions/{listener.ts → behavior/listener.ts} +4 -1
  236. package/src/extensions/{spacing.ts → behavior/spacing.ts} +8 -2
  237. package/src/extensions/{submit.ts → behavior/submit.ts} +2 -10
  238. package/src/extensions/{automerge → collab/automerge}/automerge.test.tsx +4 -4
  239. package/src/extensions/{automerge → collab/automerge}/automerge.ts +7 -4
  240. package/src/extensions/{automerge → collab/automerge}/cursor.ts +1 -1
  241. package/src/extensions/{automerge → collab/automerge}/defs.ts +4 -1
  242. package/src/extensions/{automerge → collab/automerge}/index.ts +1 -0
  243. package/src/extensions/{automerge → collab/automerge}/sync.ts +1 -2
  244. package/src/extensions/{automerge → collab/automerge}/update-automerge.ts +4 -0
  245. package/src/extensions/{automerge → collab/automerge}/update-codemirror.ts +5 -1
  246. package/src/extensions/{awareness → collab/awareness}/awareness-provider.ts +3 -4
  247. package/src/extensions/{awareness → collab/awareness}/awareness.ts +19 -16
  248. package/src/extensions/collab/index.ts +6 -0
  249. package/src/extensions/{autocomplete → completion}/autocomplete.ts +7 -5
  250. package/src/extensions/{autocomplete → completion}/index.ts +2 -1
  251. package/src/extensions/{autocomplete → completion}/match.ts +2 -2
  252. package/src/extensions/{mention.ts → completion/mention.ts} +3 -0
  253. package/src/extensions/{autocomplete → completion}/placeholder.ts +1 -1
  254. package/src/extensions/completion/typeahead.ts +97 -0
  255. package/src/extensions/{factories.ts → core/factories.ts} +35 -13
  256. package/src/extensions/core/index.ts +6 -0
  257. package/src/extensions/{modes.ts → core/modes.ts} +4 -1
  258. package/src/extensions/{debug.ts → debug/debug.ts} +3 -0
  259. package/src/extensions/debug/index.ts +5 -0
  260. package/src/extensions/{annotations.ts → decoration/annotations.ts} +12 -10
  261. package/src/extensions/decoration/assistant.ts +225 -0
  262. package/src/extensions/{fader.ts → decoration/fader.ts} +17 -19
  263. package/src/extensions/{hashtag.ts → decoration/hashtag.ts} +35 -28
  264. package/src/extensions/decoration/index.ts +11 -0
  265. package/src/extensions/{marker.ts → decoration/marker.ts} +18 -18
  266. package/src/extensions/{pos.test.ts → decoration/pos.test.ts} +57 -1
  267. package/src/extensions/{pos.ts → decoration/pos.ts} +165 -31
  268. package/src/extensions/{blast.ts → demo/blast.ts} +3 -0
  269. package/src/extensions/demo/index.ts +6 -0
  270. package/src/extensions/{snippets.ts → demo/snippets.ts} +5 -1
  271. package/src/extensions/index.ts +10 -33
  272. package/src/extensions/language/index.ts +8 -0
  273. package/src/extensions/language/json/index.ts +5 -0
  274. package/src/extensions/{json.ts → language/json/json.ts} +11 -6
  275. package/src/extensions/{markdown → language/markdown}/action.ts +1 -1
  276. package/src/extensions/{markdown → language/markdown}/bundle.ts +7 -1
  277. package/src/extensions/{markdown → language/markdown}/changes.test.ts +2 -2
  278. package/src/extensions/{markdown → language/markdown}/changes.ts +2 -2
  279. package/src/extensions/{markdown → language/markdown}/debug.ts +3 -0
  280. package/src/extensions/{markdown → language/markdown}/decorate.ts +96 -85
  281. package/src/extensions/{markdown → language/markdown}/formatting.test.ts +88 -1
  282. package/src/extensions/{markdown → language/markdown}/formatting.ts +193 -74
  283. package/src/extensions/{markdown → language/markdown}/highlight.ts +42 -41
  284. package/src/extensions/{markdown → language/markdown}/image.test.ts +1 -1
  285. package/src/extensions/{markdown → language/markdown}/image.ts +6 -6
  286. package/src/extensions/language/markdown/link.test.ts +58 -0
  287. package/src/extensions/language/markdown/link.ts +109 -0
  288. package/src/extensions/{markdown → language/markdown}/parser.test.ts +0 -28
  289. package/src/extensions/{markdown → language/markdown}/styles.ts +38 -7
  290. package/src/extensions/{markdown → language/markdown}/table.ts +5 -5
  291. package/src/extensions/language/mermaid/index.ts +5 -0
  292. package/src/extensions/language/mermaid/mermaid.ts +44 -0
  293. package/src/extensions/{tags → language/xml}/DESIGN.md +3 -3
  294. package/src/extensions/{tags → language/xml}/extended-markdown.test.ts +2 -1
  295. package/src/extensions/{tags → language/xml}/extended-markdown.ts +2 -4
  296. package/src/extensions/language/xml/stub.ts +233 -0
  297. package/src/extensions/language/xml/xml-block-decoration.ts +165 -0
  298. package/src/extensions/{tags → language/xml}/xml-formatting.ts +54 -38
  299. package/src/extensions/language/xml/xml-tags.test.ts +588 -0
  300. package/src/extensions/{tags → language/xml}/xml-tags.ts +165 -66
  301. package/src/extensions/{tags → language/xml}/xml-util.test.ts +2 -2
  302. package/src/extensions/review/change-bar.ts +94 -0
  303. package/src/extensions/review/comments.test.ts +72 -0
  304. package/src/extensions/{comments.ts → review/comments.ts} +395 -267
  305. package/src/extensions/review/diff.test.ts +405 -0
  306. package/src/extensions/review/diff.ts +502 -0
  307. package/src/extensions/review/index.ts +10 -0
  308. package/src/extensions/review/suggest.test.ts +170 -0
  309. package/src/extensions/review/suggest.ts +512 -0
  310. package/src/extensions/review/suggestions-overlay.test.ts +58 -0
  311. package/src/extensions/review/suggestions-overlay.ts +50 -0
  312. package/src/extensions/review/track-changes.test.ts +101 -0
  313. package/src/extensions/review/track-changes.ts +215 -0
  314. package/src/extensions/{busy.ts → state/busy.ts} +1 -0
  315. package/src/extensions/{focus.ts → state/focus.ts} +4 -0
  316. package/src/extensions/state/index.ts +8 -0
  317. package/src/extensions/{selection.ts → state/selection.ts} +3 -3
  318. package/src/extensions/streaming/index.ts +7 -0
  319. package/src/extensions/{pending → streaming/pending}/pending-text-stream.ts +10 -3
  320. package/src/extensions/{pending → streaming/pending}/pending-text.test.ts +1 -1
  321. package/src/extensions/{pending → streaming/pending}/pending-text.ts +20 -19
  322. package/src/extensions/{scrolling → streaming/scrolling}/auto-scroll.ts +9 -6
  323. package/src/extensions/{scrolling → streaming/scrolling}/scroll-past-end.ts +1 -0
  324. package/src/extensions/{scrolling → streaming/scrolling}/scrollbar-autohide.ts +13 -11
  325. package/src/extensions/{typewriter.ts → streaming/typewriter.ts} +46 -47
  326. package/src/extensions/structure/blocks/DESIGN.md +134 -0
  327. package/src/extensions/structure/blocks/blocks.test.ts +147 -0
  328. package/src/extensions/structure/blocks/blocks.ts +219 -0
  329. package/src/extensions/structure/blocks/drag.ts +996 -0
  330. package/src/extensions/structure/blocks/index.ts +8 -0
  331. package/src/extensions/structure/blocks/selection.test.ts +56 -0
  332. package/src/extensions/structure/blocks/selection.ts +279 -0
  333. package/src/extensions/structure/blocks/types.ts +22 -0
  334. package/src/extensions/{folding.ts → structure/folding/folding.ts} +12 -10
  335. package/src/extensions/structure/folding/index.ts +6 -0
  336. package/src/extensions/structure/folding/turns.test.ts +72 -0
  337. package/src/extensions/structure/folding/turns.ts +209 -0
  338. package/src/extensions/structure/index.ts +7 -0
  339. package/src/extensions/structure/outliner/DESIGN.md +183 -0
  340. package/src/extensions/structure/outliner/SPEC.mdl +305 -0
  341. package/src/extensions/{outliner → structure/outliner}/commands.ts +125 -42
  342. package/src/extensions/structure/outliner/dnd.test.ts +142 -0
  343. package/src/extensions/structure/outliner/dnd.ts +375 -0
  344. package/src/extensions/structure/outliner/editor.test.ts +85 -0
  345. package/src/extensions/{outliner → structure/outliner}/editor.ts +14 -9
  346. package/src/extensions/{outliner → structure/outliner}/index.ts +1 -0
  347. package/src/extensions/{outliner → structure/outliner}/menu.ts +48 -25
  348. package/src/extensions/structure/outliner/outliner.test.ts +193 -0
  349. package/src/extensions/structure/outliner/outliner.ts +183 -0
  350. package/src/extensions/{outliner → structure/outliner}/tree.test.ts +2 -2
  351. package/src/extensions/{outliner → structure/outliner}/tree.ts +1 -1
  352. package/src/headless.ts +13 -0
  353. package/src/styles/theme.ts +3 -1
  354. package/src/util/util.ts +3 -0
  355. package/dist/lib/browser/index.mjs +0 -9594
  356. package/dist/lib/browser/index.mjs.map +0 -7
  357. package/dist/lib/browser/meta.json +0 -1
  358. package/dist/lib/browser/types/index.mjs +0 -33
  359. package/dist/lib/browser/types/index.mjs.map +0 -7
  360. package/dist/lib/node-esm/index.mjs +0 -9596
  361. package/dist/lib/node-esm/index.mjs.map +0 -7
  362. package/dist/lib/node-esm/meta.json +0 -1
  363. package/dist/lib/node-esm/types/index.mjs +0 -35
  364. package/dist/lib/node-esm/types/index.mjs.map +0 -7
  365. package/dist/types/src/extensions/annotations.d.ts.map +0 -1
  366. package/dist/types/src/extensions/autocomplete/autocomplete.d.ts.map +0 -1
  367. package/dist/types/src/extensions/autocomplete/index.d.ts.map +0 -1
  368. package/dist/types/src/extensions/autocomplete/match.d.ts.map +0 -1
  369. package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +0 -1
  370. package/dist/types/src/extensions/autocomplete/typeahead.d.ts.map +0 -1
  371. package/dist/types/src/extensions/automerge/automerge.d.ts.map +0 -1
  372. package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +0 -1
  373. package/dist/types/src/extensions/automerge/cursor.d.ts.map +0 -1
  374. package/dist/types/src/extensions/automerge/defs.d.ts.map +0 -1
  375. package/dist/types/src/extensions/automerge/index.d.ts +0 -2
  376. package/dist/types/src/extensions/automerge/index.d.ts.map +0 -1
  377. package/dist/types/src/extensions/automerge/sync.d.ts.map +0 -1
  378. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +0 -1
  379. package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +0 -1
  380. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +0 -1
  381. package/dist/types/src/extensions/awareness/awareness.d.ts.map +0 -1
  382. package/dist/types/src/extensions/awareness/index.d.ts.map +0 -1
  383. package/dist/types/src/extensions/blast.d.ts.map +0 -1
  384. package/dist/types/src/extensions/blocks.d.ts +0 -2
  385. package/dist/types/src/extensions/blocks.d.ts.map +0 -1
  386. package/dist/types/src/extensions/bookmarks.d.ts.map +0 -1
  387. package/dist/types/src/extensions/busy.d.ts.map +0 -1
  388. package/dist/types/src/extensions/comments.d.ts.map +0 -1
  389. package/dist/types/src/extensions/debug.d.ts.map +0 -1
  390. package/dist/types/src/extensions/dnd.d.ts.map +0 -1
  391. package/dist/types/src/extensions/factories.d.ts.map +0 -1
  392. package/dist/types/src/extensions/factories.test.d.ts.map +0 -1
  393. package/dist/types/src/extensions/fader.d.ts.map +0 -1
  394. package/dist/types/src/extensions/focus.d.ts.map +0 -1
  395. package/dist/types/src/extensions/folding.d.ts.map +0 -1
  396. package/dist/types/src/extensions/hashtag.d.ts +0 -3
  397. package/dist/types/src/extensions/hashtag.d.ts.map +0 -1
  398. package/dist/types/src/extensions/json.d.ts +0 -7
  399. package/dist/types/src/extensions/json.d.ts.map +0 -1
  400. package/dist/types/src/extensions/listener.d.ts.map +0 -1
  401. package/dist/types/src/extensions/markdown/action.d.ts.map +0 -1
  402. package/dist/types/src/extensions/markdown/bundle.d.ts.map +0 -1
  403. package/dist/types/src/extensions/markdown/changes.d.ts.map +0 -1
  404. package/dist/types/src/extensions/markdown/changes.test.d.ts.map +0 -1
  405. package/dist/types/src/extensions/markdown/debug.d.ts.map +0 -1
  406. package/dist/types/src/extensions/markdown/decorate.d.ts.map +0 -1
  407. package/dist/types/src/extensions/markdown/formatting.d.ts.map +0 -1
  408. package/dist/types/src/extensions/markdown/formatting.test.d.ts.map +0 -1
  409. package/dist/types/src/extensions/markdown/highlight.d.ts.map +0 -1
  410. package/dist/types/src/extensions/markdown/image.d.ts.map +0 -1
  411. package/dist/types/src/extensions/markdown/image.test.d.ts.map +0 -1
  412. package/dist/types/src/extensions/markdown/index.d.ts.map +0 -1
  413. package/dist/types/src/extensions/markdown/link.d.ts +0 -7
  414. package/dist/types/src/extensions/markdown/link.d.ts.map +0 -1
  415. package/dist/types/src/extensions/markdown/parser.test.d.ts.map +0 -1
  416. package/dist/types/src/extensions/markdown/styles.d.ts.map +0 -1
  417. package/dist/types/src/extensions/markdown/table.d.ts.map +0 -1
  418. package/dist/types/src/extensions/marker.d.ts.map +0 -1
  419. package/dist/types/src/extensions/mention.d.ts.map +0 -1
  420. package/dist/types/src/extensions/modal.d.ts.map +0 -1
  421. package/dist/types/src/extensions/modes.d.ts.map +0 -1
  422. package/dist/types/src/extensions/outliner/commands.d.ts +0 -10
  423. package/dist/types/src/extensions/outliner/commands.d.ts.map +0 -1
  424. package/dist/types/src/extensions/outliner/editor.d.ts.map +0 -1
  425. package/dist/types/src/extensions/outliner/editor.test.d.ts.map +0 -1
  426. package/dist/types/src/extensions/outliner/index.d.ts.map +0 -1
  427. package/dist/types/src/extensions/outliner/menu.d.ts.map +0 -1
  428. package/dist/types/src/extensions/outliner/outliner.d.ts.map +0 -1
  429. package/dist/types/src/extensions/outliner/outliner.test.d.ts.map +0 -1
  430. package/dist/types/src/extensions/outliner/selection.d.ts +0 -12
  431. package/dist/types/src/extensions/outliner/selection.d.ts.map +0 -1
  432. package/dist/types/src/extensions/outliner/tree.d.ts.map +0 -1
  433. package/dist/types/src/extensions/outliner/tree.test.d.ts.map +0 -1
  434. package/dist/types/src/extensions/pending/index.d.ts.map +0 -1
  435. package/dist/types/src/extensions/pending/pending-text-stream.d.ts.map +0 -1
  436. package/dist/types/src/extensions/pending/pending-text-stream.test.d.ts.map +0 -1
  437. package/dist/types/src/extensions/pending/pending-text.d.ts.map +0 -1
  438. package/dist/types/src/extensions/pending/pending-text.test.d.ts.map +0 -1
  439. package/dist/types/src/extensions/pos.d.ts.map +0 -1
  440. package/dist/types/src/extensions/pos.test.d.ts.map +0 -1
  441. package/dist/types/src/extensions/replacer.d.ts.map +0 -1
  442. package/dist/types/src/extensions/replacer.test.d.ts.map +0 -1
  443. package/dist/types/src/extensions/scrolling/auto-scroll.d.ts.map +0 -1
  444. package/dist/types/src/extensions/scrolling/crawler.d.ts.map +0 -1
  445. package/dist/types/src/extensions/scrolling/index.d.ts.map +0 -1
  446. package/dist/types/src/extensions/scrolling/scroll-past-end.d.ts +0 -3
  447. package/dist/types/src/extensions/scrolling/scroll-past-end.d.ts.map +0 -1
  448. package/dist/types/src/extensions/scrolling/scrollbar-autohide.d.ts.map +0 -1
  449. package/dist/types/src/extensions/scrolling/scroller.d.ts.map +0 -1
  450. package/dist/types/src/extensions/selection.d.ts.map +0 -1
  451. package/dist/types/src/extensions/selection.test.d.ts.map +0 -1
  452. package/dist/types/src/extensions/snippets.d.ts.map +0 -1
  453. package/dist/types/src/extensions/spacing.d.ts.map +0 -1
  454. package/dist/types/src/extensions/state.d.ts +0 -2
  455. package/dist/types/src/extensions/state.d.ts.map +0 -1
  456. package/dist/types/src/extensions/submit.d.ts.map +0 -1
  457. package/dist/types/src/extensions/tags/extended-markdown.d.ts.map +0 -1
  458. package/dist/types/src/extensions/tags/extended-markdown.test.d.ts.map +0 -1
  459. package/dist/types/src/extensions/tags/index.d.ts.map +0 -1
  460. package/dist/types/src/extensions/tags/stub.d.ts +0 -35
  461. package/dist/types/src/extensions/tags/stub.d.ts.map +0 -1
  462. package/dist/types/src/extensions/tags/widgets/anchor.d.ts.map +0 -1
  463. package/dist/types/src/extensions/tags/widgets/index.d.ts.map +0 -1
  464. package/dist/types/src/extensions/tags/xml-block-decoration.d.ts +0 -31
  465. package/dist/types/src/extensions/tags/xml-block-decoration.d.ts.map +0 -1
  466. package/dist/types/src/extensions/tags/xml-formatting.d.ts.map +0 -1
  467. package/dist/types/src/extensions/tags/xml-tags.d.ts.map +0 -1
  468. package/dist/types/src/extensions/tags/xml-util.d.ts.map +0 -1
  469. package/dist/types/src/extensions/tags/xml-util.test.d.ts.map +0 -1
  470. package/dist/types/src/extensions/typewriter.d.ts.map +0 -1
  471. package/dist/types/src/extensions/typewriter.test.d.ts.map +0 -1
  472. package/src/extensions/autocomplete/typeahead.ts +0 -87
  473. package/src/extensions/blocks.ts +0 -131
  474. package/src/extensions/markdown/link.ts +0 -49
  475. package/src/extensions/outliner/editor.test.ts +0 -33
  476. package/src/extensions/outliner/outliner.test.ts +0 -99
  477. package/src/extensions/outliner/outliner.ts +0 -166
  478. package/src/extensions/outliner/selection.ts +0 -50
  479. package/src/extensions/state.ts +0 -7
  480. package/src/extensions/tags/stub.ts +0 -80
  481. package/src/extensions/tags/xml-block-decoration.ts +0 -123
  482. package/dist/types/src/extensions/{dnd.d.ts → behavior/dnd.d.ts} +0 -0
  483. package/dist/types/src/extensions/{submit.d.ts → behavior/submit.d.ts} +0 -0
  484. package/dist/types/src/extensions/{automerge → collab/automerge}/automerge.d.ts +0 -0
  485. package/dist/types/src/extensions/{automerge → collab/automerge}/automerge.test.d.ts +0 -0
  486. package/dist/types/src/extensions/{automerge → collab/automerge}/sync.d.ts +0 -0
  487. package/dist/types/src/extensions/{awareness → collab/awareness}/awareness-provider.d.ts +0 -0
  488. package/dist/types/src/extensions/{awareness → collab/awareness}/index.d.ts +0 -0
  489. package/dist/types/src/extensions/{autocomplete → completion}/autocomplete.d.ts +0 -0
  490. package/dist/types/src/extensions/{autocomplete → completion}/placeholder.d.ts +0 -0
  491. package/dist/types/src/extensions/{factories.test.d.ts → core/factories.test.d.ts} +0 -0
  492. package/dist/types/src/extensions/{fader.d.ts → decoration/fader.d.ts} +0 -0
  493. package/dist/types/src/extensions/{marker.d.ts → decoration/marker.d.ts} +0 -0
  494. package/dist/types/src/extensions/{pos.test.d.ts → decoration/pos.test.d.ts} +0 -0
  495. package/dist/types/src/extensions/{replacer.test.d.ts → decoration/replacer.test.d.ts} +0 -0
  496. package/dist/types/src/extensions/{snippets.d.ts → demo/snippets.d.ts} +0 -0
  497. package/dist/types/src/extensions/{markdown → language/markdown}/action.d.ts +0 -0
  498. package/dist/types/src/extensions/{markdown → language/markdown}/bundle.d.ts +0 -0
  499. package/dist/types/src/extensions/{markdown → language/markdown}/changes.test.d.ts +0 -0
  500. package/dist/types/src/extensions/{markdown → language/markdown}/formatting.test.d.ts +0 -0
  501. package/dist/types/src/extensions/{markdown → language/markdown}/image.d.ts +0 -0
  502. package/dist/types/src/extensions/{markdown → language/markdown}/image.test.d.ts +0 -0
  503. package/dist/types/src/extensions/{markdown → language/markdown}/index.d.ts +0 -0
  504. package/dist/types/src/extensions/{markdown → language/markdown}/parser.test.d.ts +0 -0
  505. package/dist/types/src/extensions/{markdown → language/markdown}/table.d.ts +0 -0
  506. package/dist/types/src/extensions/{tags → language/xml}/extended-markdown.d.ts +0 -0
  507. package/dist/types/src/extensions/{tags → language/xml}/extended-markdown.test.d.ts +0 -0
  508. package/dist/types/src/extensions/{tags → language/xml}/index.d.ts +0 -0
  509. package/dist/types/src/extensions/{tags → language/xml}/widgets/anchor.d.ts +0 -0
  510. package/dist/types/src/extensions/{tags → language/xml}/widgets/index.d.ts +0 -0
  511. package/dist/types/src/extensions/{tags → language/xml}/xml-formatting.d.ts +0 -0
  512. package/dist/types/src/extensions/{tags → language/xml}/xml-util.d.ts +0 -0
  513. package/dist/types/src/extensions/{tags → language/xml}/xml-util.test.d.ts +0 -0
  514. package/dist/types/src/extensions/{modal.d.ts → state/modal.d.ts} +0 -0
  515. package/dist/types/src/extensions/{selection.test.d.ts → state/selection.test.d.ts} +0 -0
  516. package/dist/types/src/extensions/{pending → streaming/pending}/index.d.ts +0 -0
  517. package/dist/types/src/extensions/{pending → streaming/pending}/pending-text-stream.d.ts +0 -0
  518. package/dist/types/src/extensions/{pending → streaming/pending}/pending-text-stream.test.d.ts +0 -0
  519. package/dist/types/src/extensions/{pending → streaming/pending}/pending-text.test.d.ts +0 -0
  520. package/dist/types/src/extensions/{scrolling → streaming/scrolling}/auto-scroll.d.ts +0 -0
  521. package/dist/types/src/extensions/{scrolling → streaming/scrolling}/crawler.d.ts +0 -0
  522. package/dist/types/src/extensions/{scrolling → streaming/scrolling}/index.d.ts +0 -0
  523. package/dist/types/src/extensions/{scrolling → streaming/scrolling}/scrollbar-autohide.d.ts +0 -0
  524. package/dist/types/src/extensions/{scrolling → streaming/scrolling}/scroller.d.ts +0 -0
  525. package/dist/types/src/extensions/{typewriter.test.d.ts → streaming/typewriter.test.d.ts} +0 -0
  526. package/dist/types/src/extensions/{folding.d.ts → structure/folding/folding.d.ts} +0 -0
  527. package/dist/types/src/extensions/{outliner → structure/outliner}/editor.d.ts +0 -0
  528. package/dist/types/src/extensions/{outliner → structure/outliner}/editor.test.d.ts +0 -0
  529. package/dist/types/src/extensions/{outliner → structure/outliner}/outliner.d.ts +0 -0
  530. package/dist/types/src/extensions/{outliner → structure/outliner}/outliner.test.d.ts +0 -0
  531. package/dist/types/src/extensions/{outliner → structure/outliner}/tree.test.d.ts +0 -0
  532. package/src/extensions/{dnd.ts → behavior/dnd.ts} +29 -29
  533. package/src/extensions/{awareness → collab/awareness}/index.ts +0 -0
  534. package/src/extensions/{factories.test.ts → core/factories.test.ts} +0 -0
  535. package/src/extensions/{replacer.test.ts → decoration/replacer.test.ts} +0 -0
  536. package/src/extensions/{replacer.ts → decoration/replacer.ts} +25 -25
  537. /package/src/extensions/{markdown → language/markdown}/index.ts +0 -0
  538. /package/src/extensions/{tags → language/xml}/index.ts +0 -0
  539. /package/src/extensions/{tags → language/xml}/testing/text.md +0 -0
  540. /package/src/extensions/{tags → language/xml}/testing/text.txt +0 -0
  541. /package/src/extensions/{tags → language/xml}/widgets/anchor.ts +0 -0
  542. /package/src/extensions/{tags → language/xml}/widgets/index.ts +0 -0
  543. /package/src/extensions/{tags → language/xml}/xml-util.ts +0 -0
  544. /package/src/extensions/{modal.ts → state/modal.ts} +0 -0
  545. /package/src/extensions/{selection.test.ts → state/selection.test.ts} +0 -0
  546. /package/src/extensions/{pending → streaming/pending}/index.ts +0 -0
  547. /package/src/extensions/{pending → streaming/pending}/pending-text-stream.test.ts +0 -0
  548. /package/src/extensions/{scrolling → streaming/scrolling}/crawler.ts +0 -0
  549. /package/src/extensions/{scrolling → streaming/scrolling}/index.ts +0 -0
  550. /package/src/extensions/{scrolling → streaming/scrolling}/scroller.ts +0 -0
  551. /package/src/extensions/{typewriter.test.ts → streaming/typewriter.test.ts} +0 -0
@@ -0,0 +1,502 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { Chunk, unifiedMergeView } from '@codemirror/merge';
6
+ import { type Extension, Text } from '@codemirror/state';
7
+ import { EditorView } from '@codemirror/view';
8
+ import { diffWordsWithSpace } from 'diff';
9
+
10
+ export type DiffOptions = {
11
+ /**
12
+ * The baseline document to diff against. The editor's own document is the modified side, so
13
+ * insertions and deletions are shown relative to this original.
14
+ */
15
+ original: string;
16
+ };
17
+
18
+ /**
19
+ * Orient the diff as a preview of accepting the original (the compared branch) into the editor's
20
+ * document (the current branch): the compare branch's content — which the merge view renders as
21
+ * deletions because it is absent from the editor — is what accepting ADDS, so show it as an addition;
22
+ * the current branch's deviating content is what accepting REMOVES. This overrides the merge view's
23
+ * default green-editor / red-original colouring, which reads inverted for branch review.
24
+ */
25
+ const reviewOrientationTheme = EditorView.theme({
26
+ '&.cm-merge-b .cm-changedLine, &.cm-merge-b .cm-changedText': {
27
+ backgroundColor: 'var(--color-cm-diff-remove-surface)',
28
+ },
29
+ '& .cm-deletedChunk, & .cm-deletedChunk .cm-deletedText': {
30
+ backgroundColor: 'var(--color-cm-diff-add-surface)',
31
+ textDecoration: 'none',
32
+ },
33
+ // Keep the change-gutter markers aligned with the inverted content colours: the current branch's
34
+ // deviations (removed on accept) gutter red, the compare branch's content (added on accept) green.
35
+ '&.cm-merge-b .cm-changedLineGutter': { background: 'var(--color-cm-diff-remove-gutter)' },
36
+ '& .cm-deletedLineGutter': { background: 'var(--color-cm-diff-add-gutter)' },
37
+ });
38
+
39
+ /**
40
+ * Inline (unified) diff view: renders the editor's document with insertions and deletions marked
41
+ * relative to {@link DiffOptions.original}. The editor stays editable — edits to the document update
42
+ * the diff against the (static) original live — so it can overlay a live editor to compare the
43
+ * current state against another version (e.g. another branch) without locking editing.
44
+ */
45
+ export const diffView = ({ original }: DiffOptions): Extension => [
46
+ unifiedMergeView({ original, mergeControls: false }),
47
+ reviewOrientationTheme,
48
+ ];
49
+
50
+ /**
51
+ * A single reviewable change between `before` (the original) and `after` (the proposal), anchored in
52
+ * the BEFORE text so it can be rendered as a suggestion over the original document and applied in
53
+ * place. `[from, to)` is the original range the hunk replaces (`from === to` for a pure insertion);
54
+ * `removed` is the original text in that range, `inserted` is the proposal text that replaces it.
55
+ */
56
+ export type DiffHunk = {
57
+ from: number;
58
+ to: number;
59
+ removed: string;
60
+ inserted: string;
61
+ };
62
+
63
+ /**
64
+ * Groups the word-level diff between `before` and `after` into contiguous {@link DiffHunk}s — each a
65
+ * maximal run of insertions/deletions bounded by unchanged text. Used to render Google-Docs-style
66
+ * suggestions (accept/reject per hunk) over the original document (see {@link suggestChanges}).
67
+ */
68
+ export const diffHunks = (before: string, after: string): DiffHunk[] => {
69
+ const hunks: DiffHunk[] = [];
70
+ let position = 0;
71
+ let pending: DiffHunk | undefined;
72
+ // Word-level diffing treats `WorldHello` -> `WorldHelloWorld` as one changed word, yielding a
73
+ // replace that claims the unchanged `WorldHello` — struck through and re-inserted, that renders as
74
+ // doubled text. When one side is contained in the other at an edge, trim it to the pure
75
+ // insert/delete core; genuine replaces keep word granularity (whole-word strikes read better).
76
+ const minimize = (hunk: DiffHunk): DiffHunk => {
77
+ const { removed, inserted } = hunk;
78
+ let prefix = 0;
79
+ while (prefix < removed.length && prefix < inserted.length && removed[prefix] === inserted[prefix]) {
80
+ prefix++;
81
+ }
82
+ let suffix = 0;
83
+ while (
84
+ suffix < removed.length - prefix &&
85
+ suffix < inserted.length - prefix &&
86
+ removed[removed.length - 1 - suffix] === inserted[inserted.length - 1 - suffix]
87
+ ) {
88
+ suffix++;
89
+ }
90
+ if (prefix + suffix !== removed.length && prefix + suffix !== inserted.length) {
91
+ return hunk;
92
+ }
93
+ return {
94
+ from: hunk.from + prefix,
95
+ to: hunk.to - suffix,
96
+ removed: removed.slice(prefix, removed.length - suffix),
97
+ inserted: inserted.slice(prefix, inserted.length - suffix),
98
+ };
99
+ };
100
+ const flush = () => {
101
+ if (pending) {
102
+ hunks.push(minimize(pending));
103
+ pending = undefined;
104
+ }
105
+ };
106
+ for (const change of diffWordsWithSpace(before, after)) {
107
+ if (change.added) {
108
+ pending ??= { from: position, to: position, removed: '', inserted: '' };
109
+ pending.inserted += change.value;
110
+ } else if (change.removed) {
111
+ pending ??= { from: position, to: position, removed: '', inserted: '' };
112
+ pending.removed += change.value;
113
+ pending.to = position + change.value.length;
114
+ position += change.value.length;
115
+ } else {
116
+ flush();
117
+ position += change.value.length;
118
+ }
119
+ }
120
+ flush();
121
+ return hunks;
122
+ };
123
+
124
+ /**
125
+ * Coalesce an author's adjacent {@link DiffHunk}s into one hunk per group, so a run of nearby edits
126
+ * reviews as a single unit (e.g. one anchored card in the comment-style layer). Two hunks merge when
127
+ * the unchanged `before` gap between them is at most `maxGap` characters; the merged hunk spans both
128
+ * and absorbs the gap text into `removed` (and, unchanged, into `inserted`) so it still reconstructs
129
+ * `after`. With `respectBlockBoundaries` (default) a gap containing a blank line (paragraph break) is
130
+ * never bridged, keeping edits in different blocks as separate groups.
131
+ *
132
+ * `hunks` must be ordered and non-overlapping as {@link diffHunks} returns them. Defaults
133
+ * (`maxGap: 0`) make grouping a no-op — each hunk is its own group — so callers opt into wider
134
+ * coalescing explicitly.
135
+ */
136
+ export type GroupPolicy = { maxGap?: number; respectBlockBoundaries?: boolean };
137
+
138
+ export const groupHunks = (hunks: DiffHunk[], before: string, policy: GroupPolicy = {}): DiffHunk[] => {
139
+ const { maxGap = 0, respectBlockBoundaries = true } = policy;
140
+ const groups: DiffHunk[] = [];
141
+ for (const hunk of hunks) {
142
+ const previous = groups.at(-1);
143
+ const gap = previous ? before.slice(previous.to, hunk.from) : '';
144
+ const bridgeable =
145
+ previous !== undefined && gap.length <= maxGap && !(respectBlockBoundaries && /\n[ \t]*\n/.test(gap));
146
+ if (bridgeable) {
147
+ // Absorb the unchanged gap into both sides so the merged hunk still maps `removed` → `inserted`.
148
+ previous.to = hunk.to;
149
+ previous.removed += gap + hunk.removed;
150
+ previous.inserted += gap + hunk.inserted;
151
+ } else {
152
+ groups.push({ ...hunk });
153
+ }
154
+ }
155
+ return groups;
156
+ };
157
+
158
+ /** Markdown delimiter pairs that must be suggested — and applied — atomically. */
159
+ const MARKUP_DELIMITERS = new Set(['**', '__', '*', '_', '~~', '`']);
160
+
161
+ /**
162
+ * Coalesces two pure insertions forming a matched markdown delimiter pair around intervening text
163
+ * (e.g. `**` before and after a phrase) into ONE replace hunk spanning the wrapped text. Without
164
+ * this, wrapping MULTIPLE words diffs as two zero-width inserts, and accepting either half alone
165
+ * leaves broken syntax. Only a tight wrap (same line, non-empty gap) reads as one formatting action.
166
+ */
167
+ export const pairMarkupHunks = (hunks: DiffHunk[], before: string): DiffHunk[] => {
168
+ const result: DiffHunk[] = [];
169
+ for (let index = 0; index < hunks.length; index++) {
170
+ const open = hunks[index];
171
+ const close = hunks[index + 1];
172
+ if (
173
+ close !== undefined &&
174
+ open.from === open.to &&
175
+ close.from === close.to &&
176
+ close.from > open.from &&
177
+ MARKUP_DELIMITERS.has(open.inserted) &&
178
+ close.inserted === open.inserted
179
+ ) {
180
+ const gap = before.slice(open.from, close.from);
181
+ if (gap.length > 0 && !gap.includes('\n')) {
182
+ result.push({
183
+ from: open.from,
184
+ to: close.from,
185
+ removed: gap,
186
+ inserted: open.inserted + gap + close.inserted,
187
+ });
188
+ index++;
189
+ continue;
190
+ }
191
+ }
192
+ result.push(open);
193
+ }
194
+ return result;
195
+ };
196
+
197
+ /**
198
+ * Re-anchors {@link DiffHunk}s computed against `base` into `doc` coordinates, so a foreign author's
199
+ * proposal (diffed `base` vs proposal) can be decorated over a `doc` that has itself diverged from
200
+ * `base` (the user's own branch edits). Each hunk offset is mapped through the character-level
201
+ * `base`↔`doc` diff ({@link computeCharHunks}): a position in an unchanged region shifts by the net
202
+ * length of edits before it; a position inside a doc-edited region clamps to that region's edge (the
203
+ * `from` to its start, the `to` to its end). Without this, a proposal diffed against `base` would
204
+ * render at stale offsets over the diverged `doc`.
205
+ */
206
+ export const rebaseHunks = (base: string, doc: string, hunks: DiffHunk[]): DiffHunk[] =>
207
+ rebaseHunksWith(computeCharHunks(base, doc), hunks);
208
+
209
+ /**
210
+ * {@link rebaseHunks} with a precomputed `base`↔`doc` character diff. Callers rebasing MANY hunk sets
211
+ * against the same pair — e.g. every author's proposal over one branch (see {@link suggestions}) —
212
+ * compute {@link computeCharHunks} once and reuse it, rather than re-diffing the whole document per set.
213
+ */
214
+ export const rebaseHunksWith = (charHunks: Hunk[], hunks: DiffHunk[]): DiffHunk[] => {
215
+ const mapPos = (pos: number, side: -1 | 1): number => {
216
+ let delta = 0;
217
+ for (const hunk of charHunks) {
218
+ if (pos < hunk.fromA) {
219
+ break;
220
+ }
221
+ // Any edge sitting exactly at a doc edit's start belongs to the region BEFORE the edit, so map
222
+ // it to `fromB` — never across the edit. For an upper edge this keeps a foreign hunk from
223
+ // absorbing the user's adjacent text; for a pure-insert anchor it keeps the proposal anchored
224
+ // BEFORE text the user typed at that spot (typing at a trailing suggestion lands after it).
225
+ if (pos === hunk.fromA) {
226
+ return hunk.fromB;
227
+ }
228
+ if (pos < hunk.toA) {
229
+ // Inside a doc-edited region: clamp to its edge so the offset stays a valid doc position.
230
+ return side < 0 ? hunk.fromB : hunk.toB;
231
+ }
232
+ // Past this region: `toB - toA` is the net offset shift it contributes.
233
+ delta = hunk.toB - hunk.toA;
234
+ }
235
+ return pos + delta;
236
+ };
237
+ return hunks.map((hunk) => {
238
+ // A zero-width hunk (pure insertion, from === to) must map both endpoints with the SAME side —
239
+ // otherwise, at a doc-edit boundary, the lower edge (side -1) and upper edge (side +1) can resolve
240
+ // to different offsets and invert the range (from > to). Non-zero hunks keep the asymmetric mapping
241
+ // so an exclusive upper edge never crosses into an adjacent doc edit.
242
+ if (hunk.from === hunk.to) {
243
+ const at = mapPos(hunk.from, -1);
244
+ return { ...hunk, from: at, to: at };
245
+ }
246
+ return { ...hunk, from: mapPos(hunk.from, -1), to: mapPos(hunk.to, 1) };
247
+ });
248
+ };
249
+
250
+ /** A changed hunk between two documents as character ranges in each (A = original, B = modified). */
251
+ export type Hunk = { fromA: number; toA: number; fromB: number; toB: number };
252
+
253
+ /**
254
+ * LINE-level changed hunks between `original` (A) and `modified` (B) as character ranges (via
255
+ * `@codemirror/merge` `Chunk.build`). Coarser than {@link computeWordHunks}; retained for callers that
256
+ * want whole-line change regions.
257
+ */
258
+ export const computeHunks = (original: string, modified: string): Hunk[] =>
259
+ Chunk.build(Text.of(original.split('\n')), Text.of(modified.split('\n'))).map((chunk) => ({
260
+ fromA: chunk.fromA,
261
+ toA: chunk.toA,
262
+ fromB: chunk.fromB,
263
+ toB: chunk.toB,
264
+ }));
265
+
266
+ /**
267
+ * WORD-level changed hunks between `original` (A) and `modified` (B), each carrying both coordinate
268
+ * ranges. Matches the word granularity of {@link diffHunks} (same `diffWordsWithSpace` alignment), so
269
+ * {@link cherryPickHunk} / {@link revertHunk} resolve exactly the anchored word(s) — accepting one
270
+ * word does not pull in the rest of its line.
271
+ */
272
+ export const computeWordHunks = (original: string, modified: string): Hunk[] => {
273
+ const hunks: Hunk[] = [];
274
+ let positionA = 0;
275
+ let positionB = 0;
276
+ let pending: Hunk | undefined;
277
+ const flush = () => {
278
+ if (pending) {
279
+ hunks.push(pending);
280
+ pending = undefined;
281
+ }
282
+ };
283
+ for (const change of diffWordsWithSpace(original, modified)) {
284
+ if (change.added) {
285
+ pending ??= { fromA: positionA, toA: positionA, fromB: positionB, toB: positionB };
286
+ positionB += change.value.length;
287
+ pending.toB = positionB;
288
+ } else if (change.removed) {
289
+ pending ??= { fromA: positionA, toA: positionA, fromB: positionB, toB: positionB };
290
+ positionA += change.value.length;
291
+ pending.toA = positionA;
292
+ } else {
293
+ flush();
294
+ positionA += change.value.length;
295
+ positionB += change.value.length;
296
+ }
297
+ }
298
+ flush();
299
+ return hunks;
300
+ };
301
+
302
+ /**
303
+ * CHARACTER-level changed hunks between `original` (A) and `modified` (B), each carrying both
304
+ * coordinate ranges. Finer than {@link computeWordHunks}: an in-word or whitespace/newline edit shows
305
+ * only the changed characters, not a whole-word delete+insert. Used for live "Suggesting mode" tracked
306
+ * changes (see {@link trackChanges}), where word granularity makes every keystroke read as a struck
307
+ * word rebuilt beside it.
308
+ */
309
+ export const computeCharHunks = (original: string, modified: string): Hunk[] =>
310
+ joinNearbyHunks(wordHunks(original, modified))
311
+ .map((hunk) => trimCommonAffixes(original, modified, hunk))
312
+ .map((hunk) => slideToWordBoundary(original, modified, hunk));
313
+
314
+ /**
315
+ * Unchanged text shorter than this between two changed regions does not separate them: a selection
316
+ * that cuts into the words at each end leaves a word fragment on both sides of a single space, and
317
+ * reporting those as two changes strikes text the reader never touched.
318
+ */
319
+ const MIN_COMMON_RUN = 4;
320
+
321
+ /** Merges hunks separated by less unchanged text than {@link MIN_COMMON_RUN}, absorbing the gap. */
322
+ const joinNearbyHunks = (hunks: Hunk[]): Hunk[] => {
323
+ const merged: Hunk[] = [];
324
+ for (const hunk of hunks) {
325
+ const previous = merged.at(-1);
326
+ if (previous && hunk.fromA - previous.toA < MIN_COMMON_RUN && hunk.fromB - previous.toB < MIN_COMMON_RUN) {
327
+ previous.toA = hunk.toA;
328
+ previous.toB = hunk.toB;
329
+ } else {
330
+ merged.push({ ...hunk });
331
+ }
332
+ }
333
+ return merged;
334
+ };
335
+
336
+ /** Accumulates a diff's changed regions into {@link Hunk}s, given a tokenisation of the two texts. */
337
+ const accumulate = (changes: Array<{ value: string; added?: boolean; removed?: boolean }>): Hunk[] => {
338
+ const hunks: Hunk[] = [];
339
+ let positionA = 0;
340
+ let positionB = 0;
341
+ let pending: Hunk | undefined;
342
+ const flush = () => {
343
+ if (pending) {
344
+ hunks.push(pending);
345
+ pending = undefined;
346
+ }
347
+ };
348
+ for (const change of changes) {
349
+ if (change.added) {
350
+ pending ??= { fromA: positionA, toA: positionA, fromB: positionB, toB: positionB };
351
+ positionB += change.value.length;
352
+ pending.toB = positionB;
353
+ } else if (change.removed) {
354
+ pending ??= { fromA: positionA, toA: positionA, fromB: positionB, toB: positionB };
355
+ positionA += change.value.length;
356
+ pending.toA = positionA;
357
+ } else {
358
+ flush();
359
+ positionA += change.value.length;
360
+ positionB += change.value.length;
361
+ }
362
+ }
363
+ flush();
364
+ return hunks;
365
+ };
366
+
367
+ const wordHunks = (original: string, modified: string): Hunk[] => accumulate(diffWordsWithSpace(original, modified));
368
+
369
+ /**
370
+ * Shrinks a word-level hunk to the characters that actually changed, by dropping the prefix and suffix
371
+ * its two sides share. Word granularity is what keeps one edit from fragmenting into strikes with
372
+ * untouched letters marooned between them, but on its own it reports whole words: a deletion cutting
373
+ * into the words at each end fuses the survivors, and the hunk then reads as "delete both words, insert
374
+ * this fused one". Trimming leaves exactly the range the reader removed (or typed).
375
+ */
376
+ const trimCommonAffixes = (original: string, modified: string, hunk: Hunk): Hunk => {
377
+ const removed = original.slice(hunk.fromA, hunk.toA);
378
+ const inserted = modified.slice(hunk.fromB, hunk.toB);
379
+ const limit = Math.min(removed.length, inserted.length);
380
+
381
+ let prefix = 0;
382
+ while (prefix < limit && removed[prefix] === inserted[prefix]) {
383
+ prefix++;
384
+ }
385
+ let suffix = 0;
386
+ while (suffix < limit - prefix && removed[removed.length - 1 - suffix] === inserted[inserted.length - 1 - suffix]) {
387
+ suffix++;
388
+ }
389
+
390
+ return {
391
+ fromA: hunk.fromA + prefix,
392
+ toA: hunk.toA - suffix,
393
+ fromB: hunk.fromB + prefix,
394
+ toB: hunk.toB - suffix,
395
+ };
396
+ };
397
+
398
+ /**
399
+ * Slides a one-sided hunk (a pure deletion or insertion) to the word boundary that encloses it. Which
400
+ * characters a one-sided edit removed is ambiguous whenever text repeats around it: deleting
401
+ * `suggest change` from `suggest changes` leaves an `s` that is equally the head of the first word or
402
+ * the tail of the last, and the minimal-edit answer strikes `uggest changes` — keeping a letter of the
403
+ * word the reader deleted. Rotating the span while it still describes the same edit lets it settle
404
+ * where a reader would draw it.
405
+ */
406
+ const slideToWordBoundary = (original: string, modified: string, hunk: Hunk): Hunk => {
407
+ const deletion = hunk.toB === hunk.fromB;
408
+ const insertion = hunk.toA === hunk.fromA;
409
+ if (deletion === insertion) {
410
+ return hunk;
411
+ }
412
+
413
+ const text = deletion ? original : modified;
414
+ const from = deletion ? hunk.fromA : hunk.fromB;
415
+ const to = deletion ? hunk.toA : hunk.toB;
416
+ const atBoundary = (offset: number) => offset === 0 || /\s/.test(text[offset - 1]);
417
+ if (atBoundary(from)) {
418
+ return hunk;
419
+ }
420
+
421
+ for (let shift = 1; shift <= from; shift++) {
422
+ // Rotating past a character only preserves the edit when it matches the span's last character.
423
+ if (text[from - shift] !== text[to - shift]) {
424
+ break;
425
+ }
426
+ if (atBoundary(from - shift)) {
427
+ return {
428
+ fromA: hunk.fromA - shift,
429
+ toA: hunk.toA - shift,
430
+ fromB: hunk.fromB - shift,
431
+ toB: hunk.toB - shift,
432
+ };
433
+ }
434
+ }
435
+
436
+ return hunk;
437
+ };
438
+
439
+ /**
440
+ * Does a hunk cover the anchored range? Half-open, so a range touching a neighbouring hunk's
441
+ * boundary is not on that hunk — except where either side is empty, which is the normal shape of a
442
+ * pure insertion: the inserted text occupies no span on the other side, and the caller anchors it at
443
+ * a single offset. Those collapse to a containment test, or accepting an insertion is impossible.
444
+ */
445
+ const overlaps = (from: number, to: number, range: { start: number; end: number }): boolean =>
446
+ from === to || range.start === range.end
447
+ ? from <= range.end && to >= range.start
448
+ : from < range.end && to > range.start;
449
+
450
+ /**
451
+ * Resolve a single change to apply when cherry-picking from `compare` (A) into `current` (B): find
452
+ * the hunk overlapping `range` (a character range in `current`) and return the splice that replaces
453
+ * that hunk's current text with the compare version. Returns undefined if no hunk overlaps the range
454
+ * (e.g. the change was already applied/reverted). `compare` is read live by the caller, so applying
455
+ * the result always reflects the latest diff — not a snapshot.
456
+ */
457
+ export const cherryPickHunk = (
458
+ current: string,
459
+ compare: string,
460
+ range: { start: number; end: number },
461
+ ): { from: number; del: number; insert: string } | undefined => {
462
+ // Merge EVERY hunk the range overlaps into one splice: a range spanning a delimiter pair (two
463
+ // word-hunks wrapping unchanged text) must apply atomically — half an accepted pair is broken
464
+ // syntax. A cursor-anchored range still resolves a single hunk, unchanged.
465
+ const matched = computeWordHunks(compare, current).filter((h) => overlaps(h.fromB, h.toB, range));
466
+ if (matched.length === 0) {
467
+ return undefined;
468
+ }
469
+ const first = matched[0];
470
+ const last = matched[matched.length - 1];
471
+ return {
472
+ from: first.fromB,
473
+ del: Math.min(last.toB, current.length) - first.fromB,
474
+ insert: compare.slice(first.fromA, Math.min(last.toA, compare.length)),
475
+ };
476
+ };
477
+
478
+ /**
479
+ * Resolve the splice that reverts a single change on `compare` (a branch) back to `base` (main) —
480
+ * the reject counterpart to {@link cherryPickHunk}. `baseRange` is a character range in `base`; the
481
+ * hunk overlapping it is located and the returned splice (in `compare` coordinates) replaces the
482
+ * branch's version with the base text. Returns undefined if no hunk overlaps the range.
483
+ */
484
+ export const revertHunk = (
485
+ base: string,
486
+ compare: string,
487
+ baseRange: { start: number; end: number },
488
+ ): { from: number; del: number; insert: string } | undefined => {
489
+ // A = base, B = compare (branch); locate the hunks by the base-side range and merge them so a
490
+ // pair-spanning reject reverts the whole formatting action (mirror of cherryPickHunk).
491
+ const matched = computeWordHunks(base, compare).filter((h) => overlaps(h.fromA, h.toA, baseRange));
492
+ if (matched.length === 0) {
493
+ return undefined;
494
+ }
495
+ const first = matched[0];
496
+ const last = matched[matched.length - 1];
497
+ return {
498
+ from: first.fromB,
499
+ del: Math.min(last.toB, compare.length) - first.fromB,
500
+ insert: base.slice(first.fromA, Math.min(last.toA, base.length)),
501
+ };
502
+ };
@@ -0,0 +1,10 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './change-bar';
6
+ export * from './comments';
7
+ export * from './diff';
8
+ export * from './suggest';
9
+ export * from './suggestions-overlay';
10
+ export * from './track-changes';
@@ -0,0 +1,170 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { EditorState } from '@codemirror/state';
6
+ import { EditorView } from '@codemirror/view';
7
+ import { describe, test } from 'vitest';
8
+
9
+ import { decorationSetToArray } from '../../util';
10
+ import { diffHunks } from './diff';
11
+ import { suggestChanges, suggestionKey, suggestions } from './suggest';
12
+
13
+ const ORIGINAL = 'The quick brown fox jumps over the lazy dog.';
14
+ const PROPOSAL = 'The fast brown fox leaps over the sleepy dog.';
15
+
16
+ /** Apply a single hunk (as the Accept control does) and return the resulting document. */
17
+ const accept = (doc: string, hunk: { from: number; to: number; inserted: string }): string =>
18
+ doc.slice(0, hunk.from) + hunk.inserted + doc.slice(hunk.to);
19
+
20
+ describe('suggestChanges', () => {
21
+ test('loads as an editor extension', ({ expect }) => {
22
+ const state = EditorState.create({ doc: ORIGINAL, extensions: [suggestChanges({ proposal: PROPOSAL })] });
23
+ expect(state.doc.toString()).toBe(ORIGINAL);
24
+ });
25
+
26
+ test('accepting a change splices the proposal into the document for that hunk', ({ expect }) => {
27
+ const hunks = diffHunks(ORIGINAL, PROPOSAL);
28
+ expect(hunks.length).toBeGreaterThan(1);
29
+
30
+ // Accepting the first change resolves exactly that hunk; the rest remain suggested.
31
+ const accepted = accept(ORIGINAL, hunks[0]);
32
+ const remaining = diffHunks(accepted, PROPOSAL);
33
+ expect(remaining).toHaveLength(hunks.length - 1);
34
+
35
+ // Accepting every change (in reverse to keep offsets valid) reconstructs the proposal.
36
+ const all = [...hunks].sort((a, b) => b.from - a.from).reduce(accept, ORIGINAL);
37
+ expect(all).toBe(PROPOSAL);
38
+ });
39
+
40
+ test('rejecting hides one change by its position-independent key', ({ expect }) => {
41
+ const hunks = diffHunks(ORIGINAL, PROPOSAL);
42
+ const author = '';
43
+ const dismissed = new Set([suggestionKey(hunks[0], author)]);
44
+ const visible = hunks.filter((hunk) => !dismissed.has(suggestionKey(hunk, author)));
45
+ expect(visible).toHaveLength(hunks.length - 1);
46
+ // The rejected hunk's key does not collide with the others.
47
+ expect(visible.some((hunk) => suggestionKey(hunk, author) === suggestionKey(hunks[0], author))).toBe(false);
48
+ });
49
+
50
+ test('the same change by two authors gets distinct keys', ({ expect }) => {
51
+ const [hunk] = diffHunks(ORIGINAL, PROPOSAL);
52
+ expect(suggestionKey(hunk, 'did:alice')).not.toBe(suggestionKey(hunk, 'did:bob'));
53
+ });
54
+ });
55
+
56
+ /** The struck (deleted-original) ranges the suggestion overlay currently decorates. */
57
+ const deleteRanges = (view: EditorView): Array<{ from: number; to: number }> => {
58
+ const ranges: Array<{ from: number; to: number }> = [];
59
+ for (const source of view.state.facet(EditorView.decorations)) {
60
+ const set = typeof source === 'function' ? source(view) : source;
61
+ if (!set) {
62
+ continue;
63
+ }
64
+ for (const range of decorationSetToArray(set)) {
65
+ if (range.value.spec?.class === 'cm-suggest-delete') {
66
+ ranges.push({ from: range.from, to: range.to });
67
+ }
68
+ }
69
+ }
70
+ return ranges;
71
+ };
72
+
73
+ /** The inline proposal previews the overlay currently renders. */
74
+ const insertWidgets = (view: EditorView): number => {
75
+ let count = 0;
76
+ for (const source of view.state.facet(EditorView.decorations)) {
77
+ const set = typeof source === 'function' ? source(view) : source;
78
+ if (!set) {
79
+ continue;
80
+ }
81
+ for (const range of decorationSetToArray(set)) {
82
+ if (range.value.spec?.widget) {
83
+ count++;
84
+ }
85
+ }
86
+ }
87
+ return count;
88
+ };
89
+
90
+ describe('suggestions with an explicit base', () => {
91
+ // The accepted base (main). Foreign authors' proposals are computed relative to this.
92
+ const MAIN = 'The quick brown fox jumps over the lazy dog.';
93
+ // The user's own branch (the editor document): they inserted "really " — no foreign author touched it.
94
+ const BRANCH = 'The really quick brown fox jumps over the lazy dog.';
95
+ // Bob's proposal vs MAIN: "lazy" -> "sleepy".
96
+ const BOB = 'The quick brown fox jumps over the sleepy dog.';
97
+
98
+ test('diffs a foreign source against base and rebases its hunks into doc coords', ({ expect }) => {
99
+ const view = new EditorView({
100
+ doc: BRANCH,
101
+ extensions: [suggestions({ base: MAIN, sources: [{ author: 'did:bob', colour: 'x', content: BOB }] })],
102
+ });
103
+
104
+ const ranges = deleteRanges(view);
105
+ expect(ranges).toHaveLength(1);
106
+ // The strike lands on bob's removed word, shifted past the user's own insertion.
107
+ expect(BRANCH.slice(ranges[0].from, ranges[0].to)).toBe('lazy');
108
+ // Critically it does NOT strike the user's inserted "really" (the diff-vs-doc bug).
109
+ const insertedFrom = BRANCH.indexOf('really');
110
+ const insertedTo = insertedFrom + 'really'.length;
111
+ expect(ranges.every((range) => range.to <= insertedFrom || range.from >= insertedTo)).toBe(true);
112
+
113
+ view.destroy();
114
+ });
115
+
116
+ // The ambient (editing) path has no shared base — the editor IS main — so each source carries the
117
+ // anchor its branch forked from. Without it, everything the reader types reads as that author's
118
+ // deletion, once per visible suggestion.
119
+ test('a per-source base is used when no shared base is given', ({ expect }) => {
120
+ const view = new EditorView({
121
+ doc: BRANCH,
122
+ extensions: [suggestions({ sources: [{ author: 'did:bob', colour: 'x', content: BOB, base: MAIN }] })],
123
+ });
124
+
125
+ const struck = deleteRanges(view).map((range) => BRANCH.slice(range.from, range.to));
126
+ expect(struck).toEqual(['lazy']);
127
+ expect(struck.join(' ')).not.toContain('really');
128
+
129
+ view.destroy();
130
+ });
131
+
132
+ test('without base, the foreign source diffs against the doc and strikes the user text', ({ expect }) => {
133
+ // Diffing BOB against the BRANCH doc reads the user's "really" as text bob would delete — the bug
134
+ // that `base` fixes.
135
+ const view = new EditorView({
136
+ doc: BRANCH,
137
+ extensions: [suggestions({ sources: [{ author: 'did:bob', colour: 'x', content: BOB }] })],
138
+ });
139
+
140
+ const struck = deleteRanges(view).map((range) => BRANCH.slice(range.from, range.to));
141
+ expect(struck.join(' ')).toContain('really');
142
+
143
+ view.destroy();
144
+ });
145
+
146
+ // Accepting a change splices it into the document, but the author's branch still differs from the
147
+ // revision they wrote on — so diffing against that anchor keeps proposing a change the document
148
+ // already carries, rendering the text twice (once accepted, once as a live suggestion).
149
+ test('a change already present in the document is no longer suggested', ({ expect }) => {
150
+ const anchor = 'alpha\nbravo\n';
151
+ const proposal = 'alpha\nbravo\ncharlie\n';
152
+
153
+ // Before accepting: the addition is pending.
154
+ const pending = new EditorView({
155
+ doc: anchor,
156
+ extensions: [suggestions({ sources: [{ author: 'did:bob', colour: 'x', content: proposal, base: anchor }] })],
157
+ });
158
+ expect(deleteRanges(pending).length + insertWidgets(pending)).toBeGreaterThan(0);
159
+ pending.destroy();
160
+
161
+ // After accepting, the document carries it and nothing remains to suggest.
162
+ const accepted = new EditorView({
163
+ doc: proposal,
164
+ extensions: [suggestions({ sources: [{ author: 'did:bob', colour: 'x', content: proposal, base: anchor }] })],
165
+ });
166
+ expect(deleteRanges(accepted)).toHaveLength(0);
167
+ expect(insertWidgets(accepted)).toBe(0);
168
+ accepted.destroy();
169
+ });
170
+ });