@dxos/react-ui-editor 0.3.11-main.668f0ff → 0.3.11-main.690ca41

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 (204) hide show
  1. package/dist/lib/browser/index.mjs +1643 -1148
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +3 -6
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +3 -0
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  9. package/dist/types/src/{hooks/automerge → components/TextEditor}/automerge.stories.d.ts +6 -1
  10. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -0
  11. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/TextEditor/index.d.ts +0 -2
  13. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  14. package/dist/types/src/{hooks/yjs → components/TextEditor}/yjs.stories.d.ts +1 -1
  15. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -0
  16. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -0
  17. package/dist/types/src/extensions/automerge/automerge.d.ts +9 -0
  18. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -0
  19. package/dist/types/src/extensions/automerge/cursor.d.ts +5 -0
  20. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -0
  21. package/dist/types/src/extensions/automerge/defs.d.ts +26 -0
  22. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -0
  23. package/dist/types/src/extensions/automerge/index.d.ts +3 -0
  24. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -0
  25. package/dist/types/src/{hooks → extensions}/automerge/semaphore.d.ts +4 -4
  26. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -0
  27. package/dist/types/src/extensions/automerge/update-automerge.d.ts +5 -0
  28. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -0
  29. package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -0
  30. package/dist/types/src/extensions/awareness.d.ts +46 -0
  31. package/dist/types/src/extensions/awareness.d.ts.map +1 -0
  32. package/dist/types/src/extensions/basic.d.ts.map +1 -0
  33. package/dist/types/src/extensions/blast.d.ts.map +1 -0
  34. package/dist/types/src/extensions/code.d.ts.map +1 -0
  35. package/dist/types/src/{components/TextEditor/extensions → extensions}/comments.d.ts +4 -2
  36. package/dist/types/src/extensions/comments.d.ts.map +1 -0
  37. package/dist/types/src/extensions/hr.d.ts.map +1 -0
  38. package/dist/types/src/extensions/image.d.ts.map +1 -0
  39. package/dist/types/src/{components/TextEditor/extensions → extensions}/index.d.ts +3 -2
  40. package/dist/types/src/extensions/index.d.ts.map +1 -0
  41. package/dist/types/src/{components/TextEditor/extensions → extensions}/link.d.ts +1 -0
  42. package/dist/types/src/extensions/link.d.ts.map +1 -0
  43. package/dist/types/src/extensions/listener.d.ts.map +1 -0
  44. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -0
  45. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -0
  46. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -0
  47. package/dist/types/src/extensions/mention.d.ts.map +1 -0
  48. package/dist/types/src/extensions/table.d.ts.map +1 -0
  49. package/dist/types/src/extensions/tasklist.d.ts.map +1 -0
  50. package/dist/types/src/extensions/typewriter.d.ts.map +1 -0
  51. package/dist/types/src/extensions/yjs/cursor.d.ts +4 -0
  52. package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -0
  53. package/dist/types/src/extensions/yjs/index.d.ts +2 -0
  54. package/dist/types/src/extensions/yjs/index.d.ts.map +1 -0
  55. package/dist/types/src/extensions/yjs/yjs.d.ts +4 -0
  56. package/dist/types/src/extensions/yjs/yjs.d.ts.map +1 -0
  57. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +1 -0
  58. package/dist/types/src/hooks/automerge.d.ts +3 -0
  59. package/dist/types/src/hooks/automerge.d.ts.map +1 -0
  60. package/dist/types/src/hooks/awareness-provider.d.ts +28 -0
  61. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -0
  62. package/dist/types/src/hooks/index.d.ts +2 -0
  63. package/dist/types/src/hooks/index.d.ts.map +1 -1
  64. package/dist/types/src/hooks/useTextModel.d.ts +9 -8
  65. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  66. package/dist/types/src/hooks/{yjs/space-provider.d.ts → yjs.d.ts} +8 -8
  67. package/dist/types/src/hooks/yjs.d.ts.map +1 -0
  68. package/dist/types/src/index.d.ts +3 -0
  69. package/dist/types/src/index.d.ts.map +1 -1
  70. package/dist/types/src/styles/cursors.d.ts.map +1 -0
  71. package/dist/types/src/styles/index.d.ts +1 -0
  72. package/dist/types/src/styles/index.d.ts.map +1 -1
  73. package/dist/types/src/styles/tokens.d.ts +1 -0
  74. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  75. package/dist/types/src/testing/replicator.d.ts +2 -2
  76. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  77. package/dist/types/src/{components/TextEditor/themes → themes}/default.d.ts +1 -1
  78. package/dist/types/src/themes/default.d.ts.map +1 -0
  79. package/dist/types/src/themes/index.d.ts.map +1 -0
  80. package/dist/types/src/util/cursor.d.ts +17 -0
  81. package/dist/types/src/util/cursor.d.ts.map +1 -0
  82. package/dist/types/src/util/index.d.ts +3 -0
  83. package/dist/types/src/util/index.d.ts.map +1 -0
  84. package/dist/types/src/util/util.d.ts.map +1 -0
  85. package/package.json +22 -21
  86. package/src/components/TextEditor/MarkdownEditor.stories.tsx +17 -20
  87. package/src/components/TextEditor/TextEditor.stories.tsx +14 -1
  88. package/src/components/TextEditor/TextEditor.tsx +3 -3
  89. package/src/{hooks/automerge → components/TextEditor}/automerge.stories.tsx +66 -6
  90. package/src/components/TextEditor/codemirror.stories.ts +3 -2
  91. package/src/components/TextEditor/index.ts +0 -3
  92. package/src/{hooks/yjs → components/TextEditor}/yjs.stories.tsx +1 -1
  93. package/src/extensions/automerge/automerge.ts +94 -0
  94. package/src/extensions/automerge/cursor.ts +51 -0
  95. package/src/extensions/automerge/defs.ts +62 -0
  96. package/src/extensions/automerge/index.ts +6 -0
  97. package/src/{hooks → extensions}/automerge/semaphore.ts +14 -12
  98. package/src/{hooks → extensions}/automerge/update-automerge.ts +5 -6
  99. package/src/{hooks → extensions}/automerge/update-codemirror.ts +13 -5
  100. package/src/extensions/awareness.ts +316 -0
  101. package/src/{components/TextEditor/extensions → extensions}/code.ts +4 -5
  102. package/src/{components/TextEditor/extensions → extensions}/comments.ts +50 -19
  103. package/src/{components/TextEditor/extensions → extensions}/hr.ts +2 -3
  104. package/src/extensions/image.ts +87 -0
  105. package/src/{components/TextEditor/extensions → extensions}/index.ts +3 -2
  106. package/src/{components/TextEditor/extensions → extensions}/link.ts +112 -58
  107. package/src/extensions/listener.ts +35 -0
  108. package/src/{components/TextEditor/extensions → extensions}/markdown/highlight.ts +4 -5
  109. package/src/{components/TextEditor/extensions → extensions}/table.ts +5 -2
  110. package/src/{components/TextEditor/extensions → extensions}/tasklist.ts +1 -3
  111. package/src/{components/TextEditor/extensions → extensions}/typewriter.ts +1 -1
  112. package/src/extensions/yjs/cursor.ts +21 -0
  113. package/src/extensions/yjs/index.ts +5 -0
  114. package/src/{hooks → extensions}/yjs/yjs.test.ts +1 -25
  115. package/src/extensions/yjs/yjs.ts +16 -0
  116. package/src/hooks/automerge.ts +52 -0
  117. package/src/hooks/awareness-provider.ts +120 -0
  118. package/src/hooks/index.ts +2 -0
  119. package/src/hooks/useTextModel.ts +20 -93
  120. package/src/hooks/{yjs/space-provider.ts → yjs.ts} +71 -19
  121. package/src/index.ts +3 -0
  122. package/src/styles/index.ts +1 -0
  123. package/src/styles/tokens.ts +3 -0
  124. package/src/testing/replicator.ts +10 -15
  125. package/src/{components/TextEditor/themes → themes}/default.ts +40 -34
  126. package/src/util/cursor.ts +29 -0
  127. package/src/util/index.ts +6 -0
  128. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +0 -1
  129. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +0 -1
  130. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +0 -1
  131. package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +0 -1
  132. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +0 -1
  133. package/dist/types/src/components/TextEditor/extensions/hr.d.ts.map +0 -1
  134. package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +0 -1
  135. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +0 -1
  136. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +0 -1
  137. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +0 -1
  138. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +0 -1
  139. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +0 -1
  140. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +0 -1
  141. package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +0 -1
  142. package/dist/types/src/components/TextEditor/extensions/mermaid.d.ts +0 -3
  143. package/dist/types/src/components/TextEditor/extensions/mermaid.d.ts.map +0 -1
  144. package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +0 -1
  145. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +0 -1
  146. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +0 -12
  147. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +0 -1
  148. package/dist/types/src/components/TextEditor/extensions/typewriter.d.ts.map +0 -1
  149. package/dist/types/src/components/TextEditor/extensions/util.d.ts.map +0 -1
  150. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +0 -1
  151. package/dist/types/src/components/TextEditor/themes/index.d.ts.map +0 -1
  152. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +0 -1
  153. package/dist/types/src/hooks/automerge/handle.d.ts +0 -9
  154. package/dist/types/src/hooks/automerge/handle.d.ts.map +0 -1
  155. package/dist/types/src/hooks/automerge/index.d.ts +0 -3
  156. package/dist/types/src/hooks/automerge/index.d.ts.map +0 -1
  157. package/dist/types/src/hooks/automerge/plugin.d.ts +0 -26
  158. package/dist/types/src/hooks/automerge/plugin.d.ts.map +0 -1
  159. package/dist/types/src/hooks/automerge/semaphore.d.ts.map +0 -1
  160. package/dist/types/src/hooks/automerge/update-automerge.d.ts +0 -6
  161. package/dist/types/src/hooks/automerge/update-automerge.d.ts.map +0 -1
  162. package/dist/types/src/hooks/automerge/update-codemirror.d.ts.map +0 -1
  163. package/dist/types/src/hooks/yjs/cursors.d.ts.map +0 -1
  164. package/dist/types/src/hooks/yjs/index.d.ts +0 -3
  165. package/dist/types/src/hooks/yjs/index.d.ts.map +0 -1
  166. package/dist/types/src/hooks/yjs/space-provider.d.ts.map +0 -1
  167. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +0 -1
  168. package/dist/types/src/hooks/yjs/yjs.test.d.ts.map +0 -1
  169. package/src/components/TextEditor/extensions/image.ts +0 -69
  170. package/src/components/TextEditor/extensions/listener.ts +0 -39
  171. package/src/components/TextEditor/extensions/mermaid.ts +0 -11
  172. package/src/components/TextEditor/extensions/tooltip.ts +0 -61
  173. package/src/hooks/automerge/handle.ts +0 -14
  174. package/src/hooks/automerge/index.ts +0 -6
  175. package/src/hooks/automerge/plugin.ts +0 -134
  176. package/src/hooks/yjs/index.ts +0 -6
  177. /package/dist/types/src/{components/TextEditor/extensions → extensions}/autocomplete.d.ts +0 -0
  178. /package/dist/types/src/{hooks → extensions}/automerge/update-codemirror.d.ts +0 -0
  179. /package/dist/types/src/{components/TextEditor/extensions → extensions}/basic.d.ts +0 -0
  180. /package/dist/types/src/{components/TextEditor/extensions → extensions}/blast.d.ts +0 -0
  181. /package/dist/types/src/{components/TextEditor/extensions → extensions}/code.d.ts +0 -0
  182. /package/dist/types/src/{components/TextEditor/extensions → extensions}/hr.d.ts +0 -0
  183. /package/dist/types/src/{components/TextEditor/extensions → extensions}/image.d.ts +0 -0
  184. /package/dist/types/src/{components/TextEditor/extensions → extensions}/listener.d.ts +0 -0
  185. /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/bundle.d.ts +0 -0
  186. /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/highlight.d.ts +0 -0
  187. /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/index.d.ts +0 -0
  188. /package/dist/types/src/{components/TextEditor/extensions → extensions}/mention.d.ts +0 -0
  189. /package/dist/types/src/{components/TextEditor/extensions → extensions}/table.d.ts +0 -0
  190. /package/dist/types/src/{components/TextEditor/extensions → extensions}/tasklist.d.ts +0 -0
  191. /package/dist/types/src/{components/TextEditor/extensions → extensions}/typewriter.d.ts +0 -0
  192. /package/dist/types/src/{hooks → extensions}/yjs/yjs.test.d.ts +0 -0
  193. /package/dist/types/src/{hooks/yjs → styles}/cursors.d.ts +0 -0
  194. /package/dist/types/src/{components/TextEditor/themes → themes}/index.d.ts +0 -0
  195. /package/dist/types/src/{components/TextEditor/extensions → util}/util.d.ts +0 -0
  196. /package/src/{components/TextEditor/extensions → extensions}/autocomplete.ts +0 -0
  197. /package/src/{components/TextEditor/extensions → extensions}/basic.ts +0 -0
  198. /package/src/{components/TextEditor/extensions → extensions}/blast.ts +0 -0
  199. /package/src/{components/TextEditor/extensions → extensions}/markdown/bundle.ts +0 -0
  200. /package/src/{components/TextEditor/extensions → extensions}/markdown/index.ts +0 -0
  201. /package/src/{components/TextEditor/extensions → extensions}/mention.ts +0 -0
  202. /package/src/{hooks/yjs → styles}/cursors.ts +0 -0
  203. /package/src/{components/TextEditor/themes → themes}/index.ts +0 -0
  204. /package/src/{components/TextEditor/extensions → util}/util.ts +0 -0
@@ -4,15 +4,19 @@
4
4
  //
5
5
 
6
6
  import { syntaxTree } from '@codemirror/language';
7
+ import { type Extension, RangeSetBuilder } from '@codemirror/state';
7
8
  import {
8
- type EditorState,
9
- type Extension,
10
- type RangeSet,
11
- RangeSetBuilder,
12
- StateField,
13
- type Transaction,
14
- } from '@codemirror/state';
15
- import { Decoration, EditorView, WidgetType } from '@codemirror/view';
9
+ Decoration,
10
+ type EditorView,
11
+ WidgetType,
12
+ hoverTooltip,
13
+ ViewPlugin,
14
+ type ViewUpdate,
15
+ type DecorationSet,
16
+ } from '@codemirror/view';
17
+ import { type SyntaxNode } from '@lezer/common';
18
+
19
+ import { tooltipContent } from '@dxos/react-ui-theme';
16
20
 
17
21
  // Adapted from:
18
22
  // https://codemirror.net/try/?c=aW1wb3J0IHsgYmFzaWNTZXR1cCwgRWRpdG9yVmlldyB9IGZyb20gImNvZGVtaXJyb3IiCmltcG9ydCB7IG1hcmtkb3duIH0gZnJvbSAiQGNvZGVtaXJyb3IvbGFuZy1tYXJrZG93biIKaW1wb3J0IHsgc3ludGF4VHJlZSB9IGZyb20gIkBjb2RlbWlycm9yL2xhbmd1YWdlIjsKaW1wb3J0IHsgRWRpdG9yU3RhdGUsIFJhbmdlU2V0QnVpbGRlciwgU3RhdGVGaWVsZCB9IGZyb20gIkBjb2RlbWlycm9yL3N0YXRlIjsKaW1wb3J0IHsgRGVjb3JhdGlvbiwgV2lkZ2V0VHlwZSB9IGZyb20gIkBjb2RlbWlycm9yL3ZpZXciOwoKY2xhc3MgTGluayBleHRlbmRzIFdpZGdldFR5cGUgewogIGNvbnN0cnVjdG9yKHRleHQsIHVybCkgewogICAgc3VwZXIoKQogICAgdGhpcy50ZXh0ID0gdGV4dAogICAgdGhpcy51cmwgPSB1cmwKICB9CiAgZXEob3RoZXIpIHsgcmV0dXJuIHRoaXMudGV4dCA9PSBvdGhlci50ZXh0ICYmIHRoaXMudXJsID09IG90aGVyLnVybCB9CiAgdG9ET00oKSB7CiAgICBsZXQgbGluayA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImEiKQogICAgbGluay50ZXh0Q29udGVudCA9IHRoaXMudGV4dAogICAgbGluay5ocmVmID0gdGhpcy51cmwKICAgIHJldHVybiBsaW5rOwogIH0KfQoKbGV0IGRlY29yYXRpb25zRmllbGQgPSBTdGF0ZUZpZWxkLmRlZmluZSh7CiAgY3JlYXRlKCkgewogICAgcmV0dXJuIERlY29yYXRpb24ubm9uZTsKICB9LAogIHVwZGF0ZShfLCB0cikgewogICAgY29uc3QgYnVpbGRlciA9IG5ldyBSYW5nZVNldEJ1aWxkZXIoKTsKICAgIGxldCBjdXJzb3IgPSB0ci5zdGF0ZS5zZWxlY3Rpb24ubWFpbi5oZWFkOwogICAgc3ludGF4VHJlZSh0ci5zdGF0ZSkuaXRlcmF0ZSh7CiAgICAgIGVudGVyOiAobm9kZSkgPT4gewogICAgICAgIGlmICgoY3Vyc29yIDwgbm9kZS5mcm9tIHx8IGN1cnNvciA+IG5vZGUudG8pICYmIG5vZGUubmFtZSA9PSAiTGluayIpIHsKICAgICAgICAgIGxldCBtYXJrcyA9IG5vZGUubm9kZS5nZXRDaGlsZHJlbigiTGlua01hcmsiKTsKICAgICAgICAgIGxldCB0ZXh0ID0gbWFya3MubGVuZ3RoID49IDIgPyB0ci5zdGF0ZS5zbGljZURvYyhtYXJrc1swXS50bywgbWFya3NbMV0uZnJvbSkgOiAiIgoKICAgICAgICAgIGxldCB1cmxOb2RlID0gbm9kZS5ub2RlLmdldENoaWxkKCJVUkwiKTsKICAgICAgICAgIGxldCB1cmwgPSB1cmxOb2RlID8gdHIuc3RhdGUuc2xpY2VEb2ModXJsTm9kZS5mcm9tLCB1cmxOb2RlLnRvKSA6ICIiCiAgICAgICAgICBidWlsZGVyLmFkZChub2RlLmZyb20sIG5vZGUudG8sIERlY29yYXRpb24ucmVwbGFjZSh7CiAgICAgICAgICAgIHdpZGdldDogbmV3IExpbmsodGV4dCwgdXJsKSwKICAgICAgICAgIH0pLAogICAgICAgICAgICAgICAgICAgICApOwogICAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdHJ1ZTsKICAgICAgfSwKICAgIH0pOwogICAgcmV0dXJuIGJ1aWxkZXIuZmluaXNoKCk7CiAgfSwKICBwcm92aWRlOiAoZikgPT4gRWRpdG9yVmlldy5kZWNvcmF0aW9ucy5mcm9tKGYpLAp9KTsKCmxldCB2aWV3ID0gbmV3IEVkaXRvclZpZXcoewogIGRvYzogIiMgSGVsbG8gV29ybGRcblxuW1Rlc3RdKGh0dHBzOi8vZXhhbXBsZS5jb20pXG5cblxuIiwKICBleHRlbnNpb25zOiBbZGVjb3JhdGlvbnNGaWVsZCwgbWFya2Rvd24oKV0sCiAgcGFyZW50OiBkb2N1bWVudC5ib2R5LAp9KTsKCnZpZXcuZm9jdXMoKTs=
@@ -20,6 +24,7 @@ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
20
24
 
21
25
  export type LinkOptions = {
22
26
  link?: boolean;
27
+ onHover?: (el: Element, url: string) => void;
23
28
  onRender?: (el: Element, url: string) => void;
24
29
  };
25
30
 
@@ -28,24 +33,68 @@ export type LinkOptions = {
28
33
  * https://codemirror.net/docs/ref/#state.StateField
29
34
  */
30
35
  export const link = (options: LinkOptions = {}): Extension => {
31
- return StateField.define<RangeSet<any>>({
32
- create: (state) => update(state, options),
33
- update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
34
- provide: (field) => EditorView.decorations.from(field),
35
- });
36
+ const extensions: Extension[] = [
37
+ ViewPlugin.fromClass(
38
+ class {
39
+ decorations: DecorationSet;
40
+
41
+ constructor(view: EditorView) {
42
+ this.decorations = buildDecorations(view, options);
43
+ }
44
+
45
+ update(update: ViewUpdate) {
46
+ if (update.docChanged || update.viewportChanged || update.selectionSet) {
47
+ this.decorations = buildDecorations(update.view, options);
48
+ }
49
+ }
50
+ },
51
+ {
52
+ decorations: (v) => v.decorations,
53
+ },
54
+ ),
55
+ ];
56
+
57
+ if (options.onHover) {
58
+ extensions.push(
59
+ // https://codemirror.net/examples/tooltip
60
+ // https://codemirror.net/docs/ref/#view.hoverTooltip
61
+ // https://github.com/codemirror/view/blob/main/src/tooltip.ts
62
+ hoverTooltip((view, pos, side) => {
63
+ const syntax = syntaxTree(view.state).resolveInner(pos, side);
64
+ let link = null;
65
+ for (let i = 0, node: SyntaxNode | null = syntax; !link && node && i < 5; node = node.parent, i++) {
66
+ link = node.name === 'Link' ? node : null;
67
+ }
68
+ const url = link && link.getChild('URL');
69
+ if (!url || !link) {
70
+ return null;
71
+ }
72
+ const urlText = view.state.sliceDoc(url.from, url.to);
73
+ return {
74
+ pos: link.from,
75
+ end: link.to,
76
+ above: true,
77
+ create: () => {
78
+ const el = document.createElement('div');
79
+ el.className = tooltipContent({}, 'pli-2 plb-1');
80
+ options.onHover!(el, urlText);
81
+ return { dom: el, offset: { x: 0, y: 4 } };
82
+ },
83
+ };
84
+ }),
85
+ );
86
+ }
87
+
88
+ return extensions;
36
89
  };
37
90
 
38
91
  class LinkButton extends WidgetType {
39
- constructor(
40
- private readonly _pos: number,
41
- private readonly _url: string,
42
- private readonly _onAttach: LinkOptions['onRender'],
43
- ) {
92
+ constructor(private readonly _url: string, private readonly _onAttach: LinkOptions['onRender']) {
44
93
  super();
45
94
  }
46
95
 
47
96
  override eq(other: LinkButton) {
48
- return this._pos === other._pos && this._url === other._url;
97
+ return this._url === other._url;
49
98
  }
50
99
 
51
100
  toDOM(view: EditorView) {
@@ -56,12 +105,12 @@ class LinkButton extends WidgetType {
56
105
  }
57
106
 
58
107
  class LinkText extends WidgetType {
59
- constructor(private readonly _pos: number, private readonly _text: string, private readonly _url?: string) {
108
+ constructor(private readonly _text: string, private readonly _url?: string) {
60
109
  super();
61
110
  }
62
111
 
63
112
  override eq(other: LinkText) {
64
- return this._pos === other._pos && this._url === other._url;
113
+ return this._url === other._url;
65
114
  }
66
115
 
67
116
  toDOM(view: EditorView) {
@@ -76,7 +125,7 @@ class LinkText extends WidgetType {
76
125
  link.onclick = () => {
77
126
  view.dispatch({
78
127
  selection: {
79
- anchor: this._pos,
128
+ anchor: view.posAtDOM(link),
80
129
  },
81
130
  });
82
131
  };
@@ -91,46 +140,51 @@ class LinkText extends WidgetType {
91
140
  * possibly overlapping ranges in such a way that they can efficiently be mapped though document changes.
92
141
  * https://codemirror.net/docs/ref/#state
93
142
  */
94
- const update = (state: EditorState, options: LinkOptions) => {
95
- const builder = new RangeSetBuilder();
143
+ const buildDecorations = (view: EditorView, options: LinkOptions): DecorationSet => {
144
+ const builder = new RangeSetBuilder<Decoration>();
145
+ const { state } = view;
96
146
  const cursor = state.selection.main.head;
97
147
 
98
- syntaxTree(state).iterate({
99
- enter: (node) => {
100
- // Check if cursor is inside text.
101
- if (node.name === 'Link') {
102
- const marks = node.node.getChildren('LinkMark');
103
- const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
104
- const urlNode = node.node.getChild('URL');
105
- const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
106
- if (!url) {
107
- return false;
108
- }
109
-
110
- if (state.readOnly || cursor < node.from || cursor > node.to) {
111
- builder.add(
112
- node.from,
113
- node.to,
114
- Decoration.replace({
115
- widget: new LinkText(node.from, text, options.link ? url : undefined),
116
- }),
117
- );
118
- }
148
+ for (const { from, to } of view.visibleRanges) {
149
+ syntaxTree(state).iterate({
150
+ enter: (node) => {
151
+ // Check if cursor is inside text.
152
+ if (node.name === 'Link') {
153
+ const marks = node.node.getChildren('LinkMark');
154
+ const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
155
+ const urlNode = node.node.getChild('URL');
156
+ const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
157
+ if (!url) {
158
+ return false;
159
+ }
160
+
161
+ if (!view.hasFocus || state.readOnly || cursor < node.from || cursor > node.to) {
162
+ builder.add(
163
+ node.from,
164
+ node.to,
165
+ Decoration.replace({
166
+ widget: new LinkText(text, options.link ? url : undefined),
167
+ }),
168
+ );
169
+ }
170
+
171
+ if (options.onRender) {
172
+ builder.add(
173
+ node.to,
174
+ node.to,
175
+ Decoration.replace({
176
+ widget: new LinkButton(url, options.onRender),
177
+ }),
178
+ );
179
+ }
119
180
 
120
- if (options.onRender) {
121
- builder.add(
122
- node.to,
123
- node.to,
124
- Decoration.replace({
125
- widget: new LinkButton(node.from, url, options.onRender),
126
- }),
127
- );
181
+ return false;
128
182
  }
129
-
130
- return false;
131
- }
132
- },
133
- });
183
+ },
184
+ from,
185
+ to,
186
+ });
187
+ }
134
188
 
135
189
  return builder.finish();
136
190
  };
@@ -0,0 +1,35 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+ import { EditorView } from '@codemirror/view';
7
+
8
+ export type ListenerOptions = {
9
+ onFocus?: (focused: boolean) => void;
10
+ onChange?: (text: string) => void;
11
+ };
12
+
13
+ /**
14
+ * Event listener.
15
+ */
16
+ export const listener = ({ onFocus, onChange }: ListenerOptions): Extension => {
17
+ const extensions: Extension[] = [];
18
+
19
+ onFocus &&
20
+ extensions.push(
21
+ EditorView.focusChangeEffect.of((_, focused) => {
22
+ onFocus(focused);
23
+ return null;
24
+ }),
25
+ );
26
+
27
+ onChange &&
28
+ extensions.push(
29
+ EditorView.updateListener.of((update) => {
30
+ onChange(update.state.doc.toString());
31
+ }),
32
+ );
33
+
34
+ return extensions;
35
+ };
@@ -6,20 +6,19 @@ import { markdownLanguage } from '@codemirror/lang-markdown';
6
6
  import { HighlightStyle } from '@codemirror/language';
7
7
  import { tags, styleTags, Tag } from '@lezer/highlight';
8
8
  import { type MarkdownConfig, Table } from '@lezer/markdown';
9
- import get from 'lodash.get';
10
9
 
11
10
  import {
12
11
  blockquote,
13
12
  bold,
14
13
  code,
15
14
  codeMark,
15
+ getToken,
16
16
  heading,
17
17
  inlineUrl,
18
18
  italic,
19
19
  mark,
20
20
  strikethrough,
21
- tokens,
22
- } from '../../../../styles';
21
+ } from '../../styles';
23
22
 
24
23
  /**
25
24
  * Custom tags defined and processed by the GFM lezer extension.
@@ -193,12 +192,12 @@ export const markdownHighlightStyle = (readonly?: boolean) => {
193
192
  // Main content, paragraphs, etc.
194
193
  // {
195
194
  // tag: [tags.content],
196
- // fontFamily: get(tokens, 'fontFamily.body', []).join(','),
195
+ // fontFamily: getToken('fontFamily.body', []).join(','),
197
196
  // },
198
197
  ],
199
198
  {
200
199
  scope: markdownLanguage,
201
- all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') },
200
+ all: { fontFamily: getToken('fontFamily.body', []).join(',') },
202
201
  },
203
202
  );
204
203
  };
@@ -98,8 +98,11 @@ class TableWidget extends WidgetType {
98
98
  super();
99
99
  }
100
100
 
101
- override eq(other: WidgetType) {
102
- return this._table.from === (other as any as TableWidget)?._table?.from;
101
+ override eq(other: TableWidget) {
102
+ return (
103
+ this._table.header?.join() === other._table.header?.join() &&
104
+ this._table.rows?.join() === other._table.rows?.join()
105
+ );
103
106
  }
104
107
 
105
108
  toDOM(view: EditorView) {
@@ -22,7 +22,6 @@ const styles = EditorView.baseTheme({
22
22
 
23
23
  class CheckboxWidget extends WidgetType {
24
24
  constructor(
25
- private readonly _pos: number,
26
25
  private _checked: boolean,
27
26
  private readonly _indent: number,
28
27
  private readonly _onCheck?: (check: boolean) => void,
@@ -31,7 +30,7 @@ class CheckboxWidget extends WidgetType {
31
30
  }
32
31
 
33
32
  override eq(other: CheckboxWidget) {
34
- return this._pos === other._pos && this._checked === other._checked && this._indent === other._indent;
33
+ return this._checked === other._checked && this._indent === other._indent;
35
34
  }
36
35
 
37
36
  toDOM(view: EditorView) {
@@ -73,7 +72,6 @@ export const tasklist = (options: TasklistOptions = {}) => {
73
72
  const checked = match[2] === 'x' || match[2] === 'X';
74
73
  return Decoration.replace({
75
74
  widget: new CheckboxWidget(
76
- pos,
77
75
  checked,
78
76
  indent,
79
77
  view.state.readOnly
@@ -19,7 +19,7 @@ const defaultItems = ['hello world!', 'this is a test.', 'this is [DXOS](https:/
19
19
  */
20
20
  export const typewriter = ({ delay = 75, items = defaultItems }: DemoOptions = {}): Extension => {
21
21
  let t: any;
22
- let idx = 0;
22
+ let idx = 0; // TODO(burdon): Make global.
23
23
 
24
24
  return [
25
25
  keymap.of([
@@ -0,0 +1,21 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import * as Y from 'yjs';
6
+
7
+ import { arrayToString, stringToArray } from '@dxos/util';
8
+
9
+ import { type CursorConverter } from '../../util';
10
+
11
+ export const cursorConverter = (text: Y.Text): CursorConverter => ({
12
+ toCursor: (index, assoc) => {
13
+ return arrayToString(Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text, index, assoc)));
14
+ },
15
+ fromCursor: (cursor) => {
16
+ return (
17
+ Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(stringToArray(cursor)), text.doc!)?.index ??
18
+ 0
19
+ );
20
+ },
21
+ });
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './yjs';
@@ -2,13 +2,10 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { effect } from '@preact/signals-react';
6
5
  import { expect } from 'chai';
7
6
  import * as Y from 'yjs';
8
7
 
9
- import { Trigger } from '@dxos/async';
10
- import { Expando, setGlobalAutomergePreference, TextObject } from '@dxos/echo-schema';
11
- import { registerSignalFactory } from '@dxos/echo-signals';
8
+ import { TextObject } from '@dxos/echo-schema';
12
9
  import { describe, test } from '@dxos/test';
13
10
  import { type YText } from '@dxos/text-model';
14
11
 
@@ -35,25 +32,4 @@ describe('YJS', () => {
35
32
  expect(obj.text.substring(cursor!.index)).to.equal('world');
36
33
  }
37
34
  });
38
-
39
- // TODO(burdon): Test deleting end character.
40
-
41
- test.skip('effect', async () => {
42
- setGlobalAutomergePreference(true);
43
- registerSignalFactory();
44
- const obj = new Expando({ comments: [] });
45
-
46
- const trigger = new Trigger();
47
- const unsubscribe = effect(() => {
48
- // TODO(burdon): BUG: doesn't work with YJS.
49
- console.log(obj.comments);
50
- if (obj.comments.length > 0) {
51
- trigger.wake();
52
- }
53
- });
54
-
55
- obj.comments.push({ message: 'hello' });
56
- await trigger.wait();
57
- unsubscribe();
58
- });
59
35
  });
@@ -0,0 +1,16 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { yCollab } from 'y-codemirror.next';
6
+ import type * as YP from 'y-protocols/awareness';
7
+
8
+ import type { YText } from '@dxos/text-model';
9
+
10
+ import { cursorConverter } from './cursor';
11
+ import { CursorConverter } from '../../util';
12
+
13
+ export const yjs = (content: YText, awareness?: YP.Awareness) => [
14
+ CursorConverter.of(cursorConverter(content)),
15
+ yCollab(content, awareness),
16
+ ];
@@ -0,0 +1,52 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import get from 'lodash.get';
6
+
7
+ import { type AutomergeTextCompat, getRawDoc } from '@dxos/echo-schema';
8
+ import { isNotNullOrUndefined } from '@dxos/util';
9
+
10
+ import { SpaceAwarenessProvider } from './awareness-provider';
11
+ import { type EditorModel, modelState, type UseTextModelProps } from './useTextModel';
12
+ import { automerge, awareness, AwarenessProvider } from '../extensions';
13
+ import { cursorColor } from '../styles';
14
+
15
+ export const createAutomergeModel = ({ space, identity, text }: UseTextModelProps): EditorModel => {
16
+ const obj = text as any as AutomergeTextCompat;
17
+ const doc = getRawDoc(obj, [obj.field]);
18
+
19
+ const awarenessProvider =
20
+ space &&
21
+ new SpaceAwarenessProvider({
22
+ space,
23
+ channel: `automerge.awareness.${obj.id}`,
24
+ info: {
25
+ displayName: identity?.profile?.displayName ?? '',
26
+ color: cursorColor.color,
27
+ lightColor: cursorColor.light,
28
+ },
29
+ peerId: identity?.identityKey.toHex() ?? 'Anonymous',
30
+ });
31
+
32
+ const model: EditorModel = {
33
+ id: obj.id,
34
+ content: doc,
35
+ text: () => get(doc.handle.docSync(), doc.path),
36
+ // TODO(burdon): https://automerge.org/automerge/api-docs/js/functions/next.getCursor.html
37
+ extension: [
38
+ modelState.init(() => model),
39
+ automerge({ handle: doc.handle, path: doc.path }),
40
+ awarenessProvider && AwarenessProvider.of(awarenessProvider),
41
+ awareness(),
42
+ ].filter(isNotNullOrUndefined),
43
+ peer: identity
44
+ ? {
45
+ id: identity.identityKey.toHex(),
46
+ name: identity.profile?.displayName,
47
+ }
48
+ : undefined,
49
+ };
50
+
51
+ return model;
52
+ };
@@ -0,0 +1,120 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { DeferredTask, Event, sleep } from '@dxos/async';
6
+ import { Context } from '@dxos/context';
7
+ import { invariant } from '@dxos/invariant';
8
+ import { log } from '@dxos/log';
9
+ import { type Space } from '@dxos/react-client/echo';
10
+ import { type GossipMessage } from '@dxos/react-client/mesh';
11
+
12
+ import { type AwarenessInfo, type AwarenessPosition, type AwarenessProvider, type AwarenessState } from '../extensions';
13
+
14
+ type ProtocolMessage =
15
+ | {
16
+ kind: 'query';
17
+ }
18
+ | {
19
+ kind: 'post';
20
+ state: AwarenessState;
21
+ };
22
+
23
+ const DEBOUNCE_INTERVAL = 100; // ms
24
+
25
+ export type AwarenessProviderParams = {
26
+ space: Space;
27
+ channel: string;
28
+ peerId: string;
29
+ info: AwarenessInfo;
30
+ };
31
+
32
+ export class SpaceAwarenessProvider implements AwarenessProvider {
33
+ public readonly remoteStateChange = new Event<void>();
34
+
35
+ private readonly _remoteStates = new Map<string, AwarenessState>();
36
+
37
+ private readonly _space: Space;
38
+ private readonly _channel: string;
39
+ private readonly _peerId: string;
40
+ private readonly _info: AwarenessInfo;
41
+
42
+ private _localState?: AwarenessState;
43
+ private _postTask?: DeferredTask;
44
+ private _ctx?: Context;
45
+
46
+ constructor(params: AwarenessProviderParams) {
47
+ this._space = params.space;
48
+ this._channel = params.channel;
49
+ this._peerId = params.peerId;
50
+ this._info = params.info;
51
+ }
52
+
53
+ open() {
54
+ this._ctx = new Context();
55
+ this._postTask = new DeferredTask(this._ctx, async () => {
56
+ if (this._localState) {
57
+ await this._space.postMessage(this._channel, {
58
+ kind: 'post',
59
+ state: this._localState,
60
+ } satisfies ProtocolMessage);
61
+
62
+ await sleep(DEBOUNCE_INTERVAL);
63
+ }
64
+ });
65
+
66
+ const unsubscribe = this._space.listen(this._channel, (message: GossipMessage) => {
67
+ switch (message.payload.kind) {
68
+ case 'query': {
69
+ this._handleQueryMessage();
70
+ break;
71
+ }
72
+ case 'post': {
73
+ this._handlePostMessage(message.payload);
74
+ break;
75
+ }
76
+ }
77
+ });
78
+ this._ctx.onDispose(unsubscribe);
79
+
80
+ void this._space
81
+ .postMessage(this._channel, {
82
+ kind: 'query',
83
+ } satisfies ProtocolMessage)
84
+ .catch((err) => {
85
+ log.debug('failed to query awareness', { err });
86
+ });
87
+ }
88
+
89
+ close() {
90
+ void this._ctx?.dispose();
91
+ this._ctx = undefined;
92
+ this._postTask = undefined;
93
+ }
94
+
95
+ update(position: AwarenessPosition | undefined): void {
96
+ invariant(this._postTask);
97
+ this._localState = {
98
+ peerId: this._peerId,
99
+ position,
100
+ info: this._info,
101
+ };
102
+
103
+ this._postTask.schedule();
104
+ }
105
+
106
+ getRemoteStates(): AwarenessState[] {
107
+ return Array.from(this._remoteStates.values());
108
+ }
109
+
110
+ private _handleQueryMessage() {
111
+ invariant(this._postTask);
112
+ this._postTask.schedule();
113
+ }
114
+
115
+ private _handlePostMessage(message: ProtocolMessage) {
116
+ invariant(message.kind === 'post');
117
+ this._remoteStates.set(message.state.peerId, message.state);
118
+ this.remoteStateChange.emit();
119
+ }
120
+ }
@@ -2,5 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './automerge';
6
+ export * from './awareness-provider';
5
7
  export * from './useTextModel';
6
8
  export * from './yjs';