@dxos/react-ui-editor 0.3.11-main.3f845e1 → 0.3.11-main.463e5cd

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 (259) hide show
  1. package/dist/lib/browser/index.mjs +2578 -1622
  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 +14 -15
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +20 -17
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -3
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/{hooks/automerge → components/TextEditor}/automerge.stories.d.ts +6 -1
  11. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -0
  12. package/dist/types/src/components/TextEditor/hooks.stories.d.ts +23 -0
  13. package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
  14. package/dist/types/src/components/TextEditor/index.d.ts +0 -2
  15. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  16. package/dist/types/src/components/Toolbar/Toolbar.d.ts +25 -0
  17. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
  18. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +23 -0
  19. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
  20. package/dist/types/src/components/Toolbar/index.d.ts +2 -0
  21. package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
  22. package/dist/types/src/components/index.d.ts +1 -0
  23. package/dist/types/src/components/index.d.ts.map +1 -1
  24. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -0
  25. package/dist/types/src/extensions/automerge/automerge.d.ts +9 -0
  26. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -0
  27. package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
  28. package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
  29. package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
  30. package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
  31. package/dist/types/src/extensions/automerge/cursor.d.ts +5 -0
  32. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -0
  33. package/dist/types/src/extensions/automerge/defs.d.ts +24 -0
  34. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -0
  35. package/dist/types/src/extensions/automerge/index.d.ts +3 -0
  36. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -0
  37. package/dist/types/src/extensions/automerge/semaphore.d.ts +15 -0
  38. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -0
  39. package/dist/types/src/extensions/automerge/update-automerge.d.ts +5 -0
  40. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -0
  41. package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -0
  42. package/dist/types/src/extensions/awareness.d.ts +46 -0
  43. package/dist/types/src/extensions/awareness.d.ts.map +1 -0
  44. package/dist/types/src/extensions/basic.d.ts +8 -0
  45. package/dist/types/src/extensions/basic.d.ts.map +1 -0
  46. package/dist/types/src/extensions/blast.d.ts.map +1 -0
  47. package/dist/types/src/extensions/comments.d.ts +67 -0
  48. package/dist/types/src/extensions/comments.d.ts.map +1 -0
  49. package/dist/types/src/extensions/cursor.d.ts +25 -0
  50. package/dist/types/src/extensions/cursor.d.ts.map +1 -0
  51. package/dist/types/src/extensions/index.d.ts +13 -0
  52. package/dist/types/src/extensions/index.d.ts.map +1 -0
  53. package/dist/types/src/extensions/listener.d.ts +10 -0
  54. package/dist/types/src/extensions/listener.d.ts.map +1 -0
  55. package/dist/types/src/extensions/markdown/bundle.d.ts +14 -0
  56. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -0
  57. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
  58. package/dist/types/src/extensions/markdown/formatting.d.ts +13 -0
  59. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
  60. package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
  61. package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
  62. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -0
  63. package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
  64. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  65. package/dist/types/src/extensions/markdown/index.d.ts +11 -0
  66. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -0
  67. package/dist/types/src/{components/TextEditor/extensions → extensions/markdown}/link.d.ts +1 -0
  68. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  69. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
  70. package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
  71. package/dist/types/src/extensions/mention.d.ts.map +1 -0
  72. package/dist/types/src/extensions/outliner.d.ts +4 -0
  73. package/dist/types/src/extensions/outliner.d.ts.map +1 -0
  74. package/dist/types/src/extensions/typewriter.d.ts.map +1 -0
  75. package/dist/types/src/hooks/awareness-provider.d.ts +31 -0
  76. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -0
  77. package/dist/types/src/hooks/defs.d.ts +25 -0
  78. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  79. package/dist/types/src/hooks/index.d.ts +5 -1
  80. package/dist/types/src/hooks/index.d.ts.map +1 -1
  81. package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
  82. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
  83. package/dist/types/src/hooks/useEditorView.d.ts +17 -0
  84. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
  85. package/dist/types/src/hooks/useTextEditor.d.ts +39 -0
  86. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  87. package/dist/types/src/hooks/useTextModel.d.ts +14 -31
  88. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  89. package/dist/types/src/index.d.ts +5 -0
  90. package/dist/types/src/index.d.ts.map +1 -1
  91. package/dist/types/src/styles/cursors.d.ts.map +1 -0
  92. package/dist/types/src/styles/index.d.ts +1 -0
  93. package/dist/types/src/styles/index.d.ts.map +1 -1
  94. package/dist/types/src/styles/markdown.d.ts +2 -2
  95. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  96. package/dist/types/src/styles/tokens.d.ts +1 -0
  97. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  98. package/dist/types/src/{components/TextEditor/themes → themes}/default.d.ts +1 -1
  99. package/dist/types/src/themes/default.d.ts.map +1 -0
  100. package/dist/types/src/themes/index.d.ts.map +1 -0
  101. package/dist/types/src/util.d.ts +7 -0
  102. package/dist/types/src/util.d.ts.map +1 -0
  103. package/package.json +48 -36
  104. package/src/components/TextEditor/MarkdownEditor.stories.tsx +93 -102
  105. package/src/components/TextEditor/TextEditor.stories.tsx +13 -34
  106. package/src/components/TextEditor/TextEditor.tsx +137 -97
  107. package/src/{hooks/automerge → components/TextEditor}/automerge.stories.tsx +65 -6
  108. package/src/components/TextEditor/hooks.stories.tsx +106 -0
  109. package/src/components/TextEditor/index.ts +0 -3
  110. package/src/components/Toolbar/Toolbar.stories.tsx +90 -0
  111. package/src/components/Toolbar/Toolbar.tsx +166 -0
  112. package/src/components/Toolbar/index.ts +5 -0
  113. package/src/components/index.ts +1 -0
  114. package/src/{components/TextEditor/extensions → extensions}/autocomplete.ts +23 -19
  115. package/src/extensions/automerge/automerge.spec.tsx +76 -0
  116. package/src/extensions/automerge/automerge.test.ts +13 -0
  117. package/src/extensions/automerge/automerge.ts +103 -0
  118. package/src/extensions/automerge/cursor.ts +52 -0
  119. package/src/extensions/automerge/defs.ts +41 -0
  120. package/src/extensions/automerge/index.ts +6 -0
  121. package/src/extensions/automerge/semaphore.ts +81 -0
  122. package/src/extensions/automerge/update-automerge.ts +49 -0
  123. package/src/{hooks → extensions}/automerge/update-codemirror.ts +17 -9
  124. package/src/extensions/awareness.ts +313 -0
  125. package/src/{components/TextEditor/extensions → extensions}/basic.ts +17 -10
  126. package/src/{components/TextEditor/extensions → extensions}/blast.ts +4 -1
  127. package/src/extensions/comments.ts +476 -0
  128. package/src/extensions/cursor.ts +50 -0
  129. package/src/{components/TextEditor/extensions → extensions}/index.ts +4 -8
  130. package/src/extensions/listener.ts +35 -0
  131. package/src/{components/TextEditor/extensions → extensions}/markdown/bundle.ts +23 -28
  132. package/src/extensions/markdown/code.ts +150 -0
  133. package/src/extensions/markdown/formatting.ts +188 -0
  134. package/src/extensions/markdown/heading.ts +59 -0
  135. package/src/{components/TextEditor/extensions → extensions}/markdown/highlight.ts +12 -27
  136. package/src/extensions/markdown/hr.ts +71 -0
  137. package/src/extensions/markdown/image.ts +87 -0
  138. package/src/extensions/markdown/index.ts +14 -0
  139. package/src/{components/TextEditor/extensions → extensions/markdown}/link.ts +121 -58
  140. package/src/{components/TextEditor/extensions → extensions/markdown}/table.ts +10 -4
  141. package/src/extensions/markdown/tasklist.ts +110 -0
  142. package/src/{components/TextEditor/extensions → extensions}/mention.ts +9 -2
  143. package/src/extensions/outliner.ts +12 -0
  144. package/src/{components/TextEditor/extensions → extensions}/typewriter.ts +1 -1
  145. package/src/hooks/awareness-provider.ts +124 -0
  146. package/src/hooks/defs.ts +42 -0
  147. package/src/hooks/index.ts +6 -1
  148. package/src/hooks/useActionHandler.ts +62 -0
  149. package/src/hooks/useEditorView.ts +29 -0
  150. package/src/hooks/useTextEditor.ts +153 -0
  151. package/src/hooks/useTextModel.ts +48 -114
  152. package/src/index.ts +5 -0
  153. package/src/styles/index.ts +1 -0
  154. package/src/styles/markdown.ts +9 -12
  155. package/src/styles/tokens.ts +3 -0
  156. package/src/{components/TextEditor/themes → themes}/default.ts +58 -82
  157. package/src/util.ts +41 -0
  158. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  159. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  160. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +0 -1
  161. package/dist/types/src/components/TextEditor/extensions/basic.d.ts +0 -9
  162. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +0 -1
  163. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +0 -1
  164. package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +0 -1
  165. package/dist/types/src/components/TextEditor/extensions/comments.d.ts +0 -48
  166. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +0 -1
  167. package/dist/types/src/components/TextEditor/extensions/hr.d.ts.map +0 -1
  168. package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +0 -1
  169. package/dist/types/src/components/TextEditor/extensions/index.d.ts +0 -17
  170. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +0 -1
  171. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +0 -1
  172. package/dist/types/src/components/TextEditor/extensions/listener.d.ts +0 -9
  173. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +0 -1
  174. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +0 -17
  175. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +0 -1
  176. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +0 -1
  177. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts +0 -3
  178. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +0 -1
  179. package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +0 -1
  180. package/dist/types/src/components/TextEditor/extensions/mermaid.d.ts +0 -3
  181. package/dist/types/src/components/TextEditor/extensions/mermaid.d.ts.map +0 -1
  182. package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +0 -1
  183. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +0 -1
  184. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +0 -12
  185. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +0 -1
  186. package/dist/types/src/components/TextEditor/extensions/typewriter.d.ts.map +0 -1
  187. package/dist/types/src/components/TextEditor/extensions/util.d.ts +0 -5
  188. package/dist/types/src/components/TextEditor/extensions/util.d.ts.map +0 -1
  189. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +0 -1
  190. package/dist/types/src/components/TextEditor/themes/index.d.ts.map +0 -1
  191. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +0 -1
  192. package/dist/types/src/hooks/automerge/handle.d.ts +0 -9
  193. package/dist/types/src/hooks/automerge/handle.d.ts.map +0 -1
  194. package/dist/types/src/hooks/automerge/index.d.ts +0 -3
  195. package/dist/types/src/hooks/automerge/index.d.ts.map +0 -1
  196. package/dist/types/src/hooks/automerge/plugin.d.ts +0 -26
  197. package/dist/types/src/hooks/automerge/plugin.d.ts.map +0 -1
  198. package/dist/types/src/hooks/automerge/semaphore.d.ts +0 -19
  199. package/dist/types/src/hooks/automerge/semaphore.d.ts.map +0 -1
  200. package/dist/types/src/hooks/automerge/update-automerge.d.ts +0 -6
  201. package/dist/types/src/hooks/automerge/update-automerge.d.ts.map +0 -1
  202. package/dist/types/src/hooks/automerge/update-codemirror.d.ts.map +0 -1
  203. package/dist/types/src/hooks/yjs/cursors.d.ts.map +0 -1
  204. package/dist/types/src/hooks/yjs/index.d.ts +0 -3
  205. package/dist/types/src/hooks/yjs/index.d.ts.map +0 -1
  206. package/dist/types/src/hooks/yjs/space-provider.d.ts +0 -27
  207. package/dist/types/src/hooks/yjs/space-provider.d.ts.map +0 -1
  208. package/dist/types/src/hooks/yjs/yjs.stories.d.ts +0 -12
  209. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +0 -1
  210. package/dist/types/src/hooks/yjs/yjs.test.d.ts +0 -2
  211. package/dist/types/src/hooks/yjs/yjs.test.d.ts.map +0 -1
  212. package/dist/types/src/testing/index.d.ts +0 -3
  213. package/dist/types/src/testing/index.d.ts.map +0 -1
  214. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  215. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  216. package/dist/types/src/testing/proto/index.d.ts +0 -2
  217. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  218. package/dist/types/src/testing/replicator.d.ts +0 -45
  219. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  220. package/src/components/TextEditor/codemirror.stories.ts +0 -114
  221. package/src/components/TextEditor/extensions/code.ts +0 -99
  222. package/src/components/TextEditor/extensions/comments.ts +0 -388
  223. package/src/components/TextEditor/extensions/hr.ts +0 -74
  224. package/src/components/TextEditor/extensions/image.ts +0 -69
  225. package/src/components/TextEditor/extensions/listener.ts +0 -23
  226. package/src/components/TextEditor/extensions/markdown/index.ts +0 -6
  227. package/src/components/TextEditor/extensions/mermaid.ts +0 -11
  228. package/src/components/TextEditor/extensions/tasklist.ts +0 -121
  229. package/src/components/TextEditor/extensions/tooltip.ts +0 -61
  230. package/src/components/TextEditor/extensions/util.ts +0 -18
  231. package/src/hooks/automerge/handle.ts +0 -14
  232. package/src/hooks/automerge/index.ts +0 -6
  233. package/src/hooks/automerge/plugin.ts +0 -134
  234. package/src/hooks/automerge/semaphore.ts +0 -78
  235. package/src/hooks/automerge/update-automerge.ts +0 -44
  236. package/src/hooks/yjs/index.ts +0 -6
  237. package/src/hooks/yjs/space-provider.ts +0 -126
  238. package/src/hooks/yjs/yjs.stories.tsx +0 -56
  239. package/src/hooks/yjs/yjs.test.ts +0 -59
  240. package/src/testing/index.ts +0 -6
  241. package/src/testing/proto/gen/schema.ts +0 -49
  242. package/src/testing/proto/index.ts +0 -5
  243. package/src/testing/proto/schema.proto +0 -15
  244. package/src/testing/replicator.ts +0 -189
  245. /package/dist/types/src/{components/TextEditor/extensions → extensions}/autocomplete.d.ts +0 -0
  246. /package/dist/types/src/{hooks → extensions}/automerge/update-codemirror.d.ts +0 -0
  247. /package/dist/types/src/{components/TextEditor/extensions → extensions}/blast.d.ts +0 -0
  248. /package/dist/types/src/{components/TextEditor/extensions → extensions/markdown}/code.d.ts +0 -0
  249. /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/highlight.d.ts +0 -0
  250. /package/dist/types/src/{components/TextEditor/extensions → extensions/markdown}/hr.d.ts +0 -0
  251. /package/dist/types/src/{components/TextEditor/extensions → extensions/markdown}/image.d.ts +0 -0
  252. /package/dist/types/src/{components/TextEditor/extensions → extensions/markdown}/table.d.ts +0 -0
  253. /package/dist/types/src/{components/TextEditor/extensions → extensions/markdown}/tasklist.d.ts +0 -0
  254. /package/dist/types/src/{components/TextEditor/extensions → extensions}/mention.d.ts +0 -0
  255. /package/dist/types/src/{components/TextEditor/extensions → extensions}/typewriter.d.ts +0 -0
  256. /package/dist/types/src/{hooks/yjs → styles}/cursors.d.ts +0 -0
  257. /package/dist/types/src/{components/TextEditor/themes → themes}/index.d.ts +0 -0
  258. /package/src/{hooks/yjs → styles}/cursors.ts +0 -0
  259. /package/src/{components/TextEditor/themes → themes}/index.ts +0 -0
@@ -0,0 +1,476 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { invertedEffects } from '@codemirror/commands';
6
+ import { type Extension, Facet, StateEffect, StateField, type Text, type ChangeDesc } from '@codemirror/state';
7
+ import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
8
+ import sortBy from 'lodash.sortby';
9
+ import { useEffect } from 'react';
10
+
11
+ import { debounce } from '@dxos/async';
12
+ import { nonNullable } from '@dxos/util';
13
+
14
+ import { Cursor } from './cursor';
15
+ import { type Comment, type Range } from '../hooks';
16
+ import { getToken } from '../styles';
17
+ import { callbackWrapper } from '../util';
18
+
19
+ // TODO(burdon): Reconcile with theme.
20
+ const styles = EditorView.baseTheme({
21
+ '& .cm-comment': {
22
+ backgroundColor: getToken('extend.colors.yellow.50'),
23
+ },
24
+ '& .cm-comment-current': {
25
+ backgroundColor: getToken('extend.colors.yellow.100'),
26
+ },
27
+ });
28
+
29
+ const commentMark = Decoration.mark({ class: 'cm-comment' });
30
+ const commentCurrentMark = Decoration.mark({ class: 'cm-comment-current' });
31
+
32
+ type CommentState = {
33
+ comment: Comment;
34
+ range: Range;
35
+ location?: Rect | null;
36
+ };
37
+
38
+ type SelectionState = {
39
+ current?: string;
40
+ closest?: string;
41
+ };
42
+
43
+ export type CommentsState = {
44
+ comments: CommentState[];
45
+ selection: SelectionState;
46
+ };
47
+
48
+ export const focusComment = (view: EditorView, id: string, center = true) => {
49
+ const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === id);
50
+ if (!comment?.comment.cursor) {
51
+ return;
52
+ }
53
+
54
+ const range = Cursor.getRangeFromCursor(view.state, comment.comment.cursor);
55
+ if (range) {
56
+ view.dispatch({
57
+ selection: { anchor: range.from },
58
+ effects: [
59
+ //
60
+ EditorView.scrollIntoView(range.from, center ? { y: 'center' } : undefined),
61
+ setSelection.of({ current: id }),
62
+ ],
63
+ });
64
+ }
65
+ };
66
+
67
+ export const setComments = StateEffect.define<Comment[]>();
68
+
69
+ export const setSelection = StateEffect.define<SelectionState>();
70
+
71
+ const setCommentState = StateEffect.define<CommentsState>();
72
+
73
+ /**
74
+ * State field (reducer) that tracks comment ranges.
75
+ * The ranges are tracked as Automerge cursors from which the absolute indexed ranges can be computed.
76
+ */
77
+ const commentsState = StateField.define<CommentsState>({
78
+ create: () => ({ comments: [], selection: {} }),
79
+ update: (value, tr) => {
80
+ for (const effect of tr.effects) {
81
+ // Update selection.
82
+ if (effect.is(setSelection)) {
83
+ return { ...value, selection: effect.value };
84
+ }
85
+
86
+ // Update range from store.
87
+ if (effect.is(setComments)) {
88
+ const comments: CommentState[] = effect.value
89
+ .map((comment) => {
90
+ // Skip cut/deleted comments.
91
+ if (!comment.cursor) {
92
+ return undefined;
93
+ }
94
+
95
+ const range = Cursor.getRangeFromCursor(tr.state, comment.cursor);
96
+ return range && { comment, range };
97
+ })
98
+ .filter(nonNullable);
99
+
100
+ return { ...value, comments };
101
+ }
102
+
103
+ // Update entire state.
104
+ if (effect.is(setCommentState)) {
105
+ return effect.value;
106
+ }
107
+ }
108
+
109
+ return value;
110
+ },
111
+ });
112
+
113
+ /**
114
+ * Decorate ranges.
115
+ */
116
+ const commentsDecorations = EditorView.decorations.compute([commentsState], (state) => {
117
+ const {
118
+ selection: { current },
119
+ comments,
120
+ } = state.field(commentsState);
121
+
122
+ const decorations = sortBy(comments ?? [], (range) => range.range.from)
123
+ ?.flatMap((comment) => {
124
+ const range = comment.range;
125
+ if (!range || range.from === range.to) {
126
+ console.warn('Invalid range:', range);
127
+ return undefined;
128
+ }
129
+
130
+ if (comment.comment.id === current) {
131
+ return commentCurrentMark.range(range.from, range.to);
132
+ } else {
133
+ return commentMark.range(range.from, range.to);
134
+ }
135
+ })
136
+ .filter(nonNullable);
137
+
138
+ return Decoration.set(decorations);
139
+ });
140
+
141
+ export type CommentsOptions = {
142
+ /**
143
+ * Key shortcut to create a new thread.
144
+ */
145
+ key?: string;
146
+ /**
147
+ * Called to create a new thread and return the thread id.
148
+ */
149
+ onCreate?: (cursor: string, location?: Rect | null) => string | undefined;
150
+ /**
151
+ * Selection cut/deleted.
152
+ */
153
+ onDelete?: (thread: string) => void;
154
+ /**
155
+ * Called when a comment is moved.
156
+ */
157
+ onUpdate?: (thread: string, cursor: string) => void;
158
+ /**
159
+ * Called to notify which thread is currently closest to the cursor.
160
+ */
161
+ onSelect?: (state: CommentsState) => void;
162
+ /**
163
+ * Called to render tooltip.
164
+ */
165
+ onHover?: (el: Element, shortcut: string) => void;
166
+ };
167
+
168
+ type TrackedComment = { id: string; from: number; to: number };
169
+
170
+ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>) => {
171
+ // Tracks indexed selections within text.
172
+ let tracked: { text: Text; comments: TrackedComment[] } | null = null;
173
+
174
+ // Track cut or copy (enables cut-and-paste and copy-delete-paste to restore comment selection).
175
+ const handleTrack = (event: Event, view: EditorView) => {
176
+ const comments = view.state.field(commentsState);
177
+ const { main } = view.state.selection;
178
+ const selectedRanges = comments.comments.filter(
179
+ ({ range }) => range.from >= main.from && range.to <= main.to && range.from < range.to,
180
+ );
181
+
182
+ if (!selectedRanges.length) {
183
+ tracked = null;
184
+ } else {
185
+ tracked = {
186
+ text: view.state.doc.slice(main.from, main.to),
187
+ comments: selectedRanges.map(({ comment, range }) => ({
188
+ id: comment.id,
189
+ from: range.from - main.from,
190
+ to: range.to - main.from,
191
+ })),
192
+ };
193
+ }
194
+ };
195
+
196
+ return [
197
+ EditorView.domEventHandlers({
198
+ cut: handleTrack,
199
+ copy: handleTrack,
200
+ }),
201
+
202
+ // Track deleted comments.
203
+ invertedEffects.of((tr) => {
204
+ const { comments } = tr.startState.field(commentsState);
205
+ const effects: StateEffect<any>[] = [];
206
+ tr.changes.iterChangedRanges((fromA, toA) => {
207
+ for (const {
208
+ comment: { id },
209
+ range: { from, to },
210
+ } of comments) {
211
+ if (from < to && from >= fromA && to <= toA) {
212
+ effects.push(restoreCommentEffect.of({ id, from, to }));
213
+ }
214
+ }
215
+ });
216
+
217
+ return effects;
218
+ }),
219
+
220
+ // Handle paste or the undo of comment deletion.
221
+ EditorView.updateListener.of((update) => {
222
+ const restore: TrackedComment[] = [];
223
+
224
+ for (let i = 0; i < update.transactions.length; i++) {
225
+ const tr = update.transactions[i];
226
+ for (let j = 0; j < restore.length; j++) {
227
+ restore[j] = mapTrackedComment(restore[j], tr.changes);
228
+ }
229
+ for (const effect of tr.effects) {
230
+ if (effect.is(restoreCommentEffect)) {
231
+ restore.push(effect.value);
232
+ }
233
+ }
234
+ }
235
+
236
+ if (tracked) {
237
+ const paste = update.transactions.find((tr) => tr.isUserEvent('input.paste'));
238
+ if (paste) {
239
+ let found = -1;
240
+ paste.changes.iterChanges((fromA, toA, fromB, toB, text) => {
241
+ if (text.eq(tracked!.text)) {
242
+ for (let i = update.transactions.indexOf(paste!) + 1; i < update.transactions.length; i++) {
243
+ fromB = update.transactions[i].changes.mapPos(fromB);
244
+ }
245
+
246
+ found = fromB;
247
+ }
248
+ });
249
+
250
+ if (found > -1) {
251
+ for (const moved of tracked.comments) {
252
+ restore.push({ id: moved.id, from: found + moved.from, to: found + moved.to });
253
+ }
254
+ }
255
+
256
+ tracked = null;
257
+ }
258
+ }
259
+
260
+ for (const comment of restore) {
261
+ const { comments } = update.startState.field(commentsState);
262
+ const exists = comments.some((c) => c.comment.id === comment.id && c.range.from < c.range.to);
263
+ if (!exists) {
264
+ const cursor = Cursor.getCursorFromRange(update.state, comment);
265
+ onUpdate(comment.id, cursor);
266
+ }
267
+ }
268
+ }),
269
+ ];
270
+ };
271
+
272
+ const mapTrackedComment = (comment: TrackedComment, changes: ChangeDesc) => ({
273
+ id: comment.id,
274
+ from: changes.mapPos(comment.from, 1),
275
+ to: changes.mapPos(comment.to, 1),
276
+ });
277
+
278
+ // These are attached to undone/redone transactions in the editor for the purpose of restoring comments
279
+ // that were deleted by the original changes.
280
+ const restoreCommentEffect = StateEffect.define<TrackedComment>({ map: mapTrackedComment });
281
+
282
+ const optionsFacet = Facet.define<CommentsOptions, CommentsOptions>({
283
+ combine: (providers) => providers[0],
284
+ });
285
+
286
+ /**
287
+ * Create comment thread action.
288
+ */
289
+ export const createComment: Command = (view) => {
290
+ const options = view.state.facet(optionsFacet);
291
+ const { from, to } = view.state.selection.main;
292
+ if (from === to) {
293
+ return false;
294
+ }
295
+
296
+ // Don't allow selection at end of document.
297
+ if (to === view.state.doc.length) {
298
+ view.dispatch({
299
+ changes: {
300
+ from: to,
301
+ insert: '\n',
302
+ },
303
+ });
304
+ }
305
+
306
+ const cursor = Cursor.getCursorFromRange(view.state, { from, to });
307
+ if (cursor) {
308
+ // Create thread via callback.
309
+ const id = options.onCreate?.(cursor, view.coordsAtPos(from));
310
+ if (id) {
311
+ // Update range.
312
+ view.dispatch({
313
+ effects: setSelection.of({ current: id }),
314
+ selection: { anchor: to },
315
+ });
316
+
317
+ return true;
318
+ }
319
+ }
320
+
321
+ return false;
322
+ };
323
+
324
+ /**
325
+ * Comment threads.
326
+ * 1). Updates the EditorModel to store relative selections for a set of comments threads.
327
+ * Since the selections are relative, they do not need to be updated when the document is edited.
328
+ * 2). Implements a StateField to track absolute selections corresponding to the comments (i.e., when the document is edited).
329
+ * 3). Creates decoration marks to apply classes to each selection.
330
+ * 4). Tracks the current cursor position to:
331
+ * a). Update the decoration to show if the cursor is within a current selection.
332
+ * b). Calls a handler to indicate which is the closest selection (e.g., to update the thread sidebar).
333
+ * 5). Optionally, implements a hoverTooltip to show hints when creating a selection range.
334
+ */
335
+ export const comments = (options: CommentsOptions = {}): Extension => {
336
+ const { key: shortcut = "meta-'" } = options;
337
+
338
+ const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
339
+
340
+ return [
341
+ optionsFacet.of(options),
342
+ commentsState,
343
+ commentsDecorations,
344
+ styles,
345
+
346
+ //
347
+ // Keymap.
348
+ //
349
+ options.onCreate
350
+ ? keymap.of([
351
+ {
352
+ key: shortcut,
353
+ run: callbackWrapper(createComment),
354
+ },
355
+ ])
356
+ : [],
357
+
358
+ //
359
+ // Hover tooltip (for key shortcut hints, etc.)
360
+ // TODO(burdon): Factor out to generic hints extension for current selection/line.
361
+ //
362
+ options.onHover
363
+ ? hoverTooltip(
364
+ (view, pos) => {
365
+ const selection = view.state.selection.main;
366
+ if (selection && pos >= selection.from && pos <= selection.to) {
367
+ return {
368
+ pos: selection.from,
369
+ end: selection.to,
370
+ above: true,
371
+ create: () => {
372
+ const el = document.createElement('div');
373
+ options.onHover!(el, shortcut);
374
+ return { dom: el, offset: { x: 0, y: 8 } };
375
+ },
376
+ };
377
+ }
378
+
379
+ return null;
380
+ },
381
+ {
382
+ // TODO(burdon): Hide on change triggered immediately?
383
+ // hideOnChange: true,
384
+ hoverTime: 1_000,
385
+ },
386
+ )
387
+ : [],
388
+
389
+ //
390
+ // Track deleted ranges and update ranges for decorations.
391
+ //
392
+ EditorView.updateListener.of(({ view, state, changes }) => {
393
+ let mod = false;
394
+ const { comments, ...value } = state.field(commentsState);
395
+ changes.iterChanges((from, to, from2, to2) => {
396
+ comments.forEach(({ comment, range }) => {
397
+ // Test if range deleted.
398
+ if (from2 === to2) {
399
+ const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor!);
400
+ if (!newRange || newRange.to - newRange.from === 0) {
401
+ options.onDelete?.(comment.id);
402
+ }
403
+ }
404
+
405
+ // Update range.
406
+ if (from <= range.to) {
407
+ const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor!);
408
+ Object.assign(range, newRange);
409
+ mod = true;
410
+ }
411
+ });
412
+ });
413
+
414
+ if (mod) {
415
+ view.dispatch({ effects: setCommentState.of({ comments, ...value }) });
416
+ }
417
+ }),
418
+
419
+ //
420
+ // Track selection/proximity.
421
+ //
422
+ EditorView.updateListener.of(({ view, state }) => {
423
+ let min = Infinity;
424
+ const {
425
+ selection: { current, closest },
426
+ comments,
427
+ } = state.field(commentsState);
428
+
429
+ const { head } = state.selection.main;
430
+ const selection: SelectionState = {};
431
+ comments.forEach(({ comment, range }) => {
432
+ if (head >= range.from && head <= range.to) {
433
+ selection.current = comment.id;
434
+ selection.closest = undefined;
435
+ }
436
+
437
+ if (!selection.current) {
438
+ const d = Math.min(Math.abs(head - range.from), Math.abs(head - range.to));
439
+ if (d < min) {
440
+ selection.closest = comment.id;
441
+ min = d;
442
+ }
443
+ }
444
+ });
445
+
446
+ if (selection.current !== current || selection.closest !== closest) {
447
+ view.dispatch({ effects: setSelection.of(selection) });
448
+
449
+ // Update callback.
450
+ handleSelect({
451
+ selection,
452
+ comments: comments.map(({ comment, range }) => ({
453
+ comment,
454
+ range,
455
+ location: view.coordsAtPos(range.from),
456
+ })),
457
+ });
458
+ }
459
+ }),
460
+
461
+ options.onUpdate ? trackPastedComments(options.onUpdate) : [],
462
+ ];
463
+ };
464
+
465
+ /**
466
+ * Update comments state field.
467
+ */
468
+ export const useComments = (view: EditorView | null, comments: Comment[] = []) => {
469
+ useEffect(() => {
470
+ if (view) {
471
+ view.dispatch({
472
+ effects: setComments.of(comments),
473
+ });
474
+ }
475
+ }, [view, comments]);
476
+ };
@@ -0,0 +1,50 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type EditorState, Facet } from '@codemirror/state';
6
+
7
+ import type { Range } from '../hooks';
8
+
9
+ /**
10
+ * Converts indexes into the text document into stable peer-independent cursors.
11
+ *
12
+ * See:
13
+ * - https://automerge.org/automerge/api-docs/js/functions/next.getCursor.html
14
+ * - https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
15
+ *
16
+ * @param {assoc} number Negative values will associate the cursor with the previous character
17
+ * while positive - with the next one.
18
+ */
19
+ export interface CursorConverter {
20
+ toCursor(position: number, assoc?: -1 | 1 | undefined): string;
21
+ fromCursor(cursor: string): number;
22
+ }
23
+
24
+ const defaultCursorConverter: CursorConverter = {
25
+ toCursor: (position) => position.toString(),
26
+ fromCursor: (cursor) => parseInt(cursor),
27
+ };
28
+
29
+ export class Cursor {
30
+ static readonly converter = Facet.define<CursorConverter, CursorConverter>({
31
+ combine: (providers) => providers[0] ?? defaultCursorConverter,
32
+ });
33
+
34
+ static readonly getCursorFromRange = (state: EditorState, range: Range) => {
35
+ const cursorConverter = state.facet(Cursor.converter);
36
+
37
+ const from = cursorConverter.toCursor(range.from);
38
+ const to = cursorConverter.toCursor(range.to, -1);
39
+ return [from, to].join(':');
40
+ };
41
+
42
+ static readonly getRangeFromCursor = (state: EditorState, cursor: string) => {
43
+ const cursorConverter = state.facet(Cursor.converter);
44
+
45
+ const parts = cursor.split(':');
46
+ const from = cursorConverter.fromCursor(parts[0]);
47
+ const to = cursorConverter.fromCursor(parts[1]);
48
+ return from !== undefined && to !== undefined ? { from, to } : undefined;
49
+ };
50
+ }
@@ -3,18 +3,14 @@
3
3
  //
4
4
 
5
5
  export * from './autocomplete';
6
+ export * from './automerge';
7
+ export * from './awareness';
6
8
  export * from './basic';
7
9
  export * from './blast';
8
- export * from './code';
9
10
  export * from './comments';
10
- export * from './hr';
11
- export * from './image';
12
- export * from './link';
11
+ export * from './cursor';
13
12
  export * from './listener';
14
13
  export * from './markdown';
15
14
  export * from './mention';
16
- export * from './mermaid';
17
- export * from './table';
18
- export * from './tasklist';
19
- export * from './tooltip';
15
+ export * from './outliner';
20
16
  export * from './typewriter';
@@ -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
+ };
@@ -21,36 +21,33 @@ import {
21
21
  } from '@codemirror/view';
22
22
 
23
23
  import type { ThemeMode } from '@dxos/react-ui';
24
+ import { isNotFalsy } from '@dxos/util';
24
25
 
25
26
  import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
27
+ import { type TextEditorProps } from '../../components';
26
28
 
27
29
  export type MarkdownBundleOptions = {
28
- readonly?: boolean;
29
30
  themeMode?: ThemeMode;
30
- placeholder?: string;
31
- };
31
+ } & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
32
32
 
33
33
  /**
34
34
  * Markdown bundle.
35
35
  * Refs:
36
- * https://github.com/codemirror/basic-setup
37
- * https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts#L50
38
36
  * https://codemirror.net/docs/community
39
37
  * https://codemirror.net/docs/ref/#codemirror.basicSetup
40
38
  */
41
39
  // TODO(burdon): Add Composer here: https://codemirror.net/docs/community
42
- export const markdownBundle = ({
43
- readonly,
40
+ export const createMarkdownExtensions = ({
44
41
  themeMode,
45
42
  placeholder: _placeholder,
46
- }: MarkdownBundleOptions): Extension[] => {
43
+ lineWrapping = true,
44
+ }: MarkdownBundleOptions = {}): Extension[] => {
47
45
  return [
46
+ lineWrapping && EditorView.lineWrapping,
48
47
  EditorState.allowMultipleSelections.of(true),
49
48
  EditorState.tabSize.of(2),
50
- EditorView.lineWrapping,
51
-
52
- customKeymap(),
53
49
 
50
+ // https://github.com/codemirror/basic-setup
54
51
  bracketMatching(),
55
52
  closeBrackets(),
56
53
  crosshairCursor(),
@@ -86,21 +83,19 @@ export const markdownBundle = ({
86
83
  themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
87
84
 
88
85
  // Custom styles.
89
- syntaxHighlighting(markdownHighlightStyle(readonly)),
90
- ].filter(Boolean) as Extension[];
91
- };
86
+ syntaxHighlighting(markdownHighlightStyle()),
92
87
 
93
- // https://codemirror.net/docs/ref/#view.keymap
94
- const customKeymap = () =>
95
- keymap.of([
96
- // https://codemirror.net/docs/ref/#commands.indentWithTab
97
- indentWithTab,
98
- // https://codemirror.net/docs/ref/#commands.standardKeymap
99
- ...standardKeymap,
100
- // https://codemirror.net/docs/ref/#commands.historyKeymap
101
- ...historyKeymap,
102
- // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
103
- ...closeBracketsKeymap,
104
- // https://codemirror.net/docs/ref/#search.searchKeymap
105
- ...searchKeymap,
106
- ]);
88
+ keymap.of([
89
+ // https://codemirror.net/docs/ref/#commands.indentWithTab
90
+ indentWithTab,
91
+ // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
92
+ ...closeBracketsKeymap,
93
+ // https://codemirror.net/docs/ref/#commands.historyKeymap
94
+ ...historyKeymap,
95
+ // https://codemirror.net/docs/ref/#search.searchKeymap
96
+ ...searchKeymap,
97
+ // https://codemirror.net/docs/ref/#commands.standardKeymap
98
+ ...standardKeymap,
99
+ ]),
100
+ ].filter(isNotFalsy);
101
+ };