@dxos/react-ui-editor 0.3.11-main.1caa3af → 0.3.11-main.24b414e

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 (210) hide show
  1. package/dist/lib/browser/index.mjs +1750 -1384
  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 +9 -8
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +14 -19
  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/codemirror.stories.d.ts.map +1 -1
  13. package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
  14. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
  15. package/dist/types/src/components/TextEditor/index.d.ts +0 -2
  16. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  17. package/dist/types/src/{hooks/yjs → components/TextEditor}/yjs.stories.d.ts +1 -1
  18. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -0
  19. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -0
  20. package/dist/types/src/extensions/automerge/automerge.d.ts +9 -0
  21. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -0
  22. package/dist/types/src/extensions/automerge/cursor.d.ts +5 -0
  23. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -0
  24. package/dist/types/src/extensions/automerge/defs.d.ts +26 -0
  25. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -0
  26. package/dist/types/src/extensions/automerge/index.d.ts +4 -0
  27. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -0
  28. package/dist/types/src/{hooks → extensions}/automerge/semaphore.d.ts +4 -4
  29. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -0
  30. package/dist/types/src/extensions/automerge/update-automerge.d.ts +5 -0
  31. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -0
  32. package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -0
  33. package/dist/types/src/extensions/awareness.d.ts +46 -0
  34. package/dist/types/src/extensions/awareness.d.ts.map +1 -0
  35. package/dist/types/src/extensions/basic.d.ts.map +1 -0
  36. package/dist/types/src/extensions/blast.d.ts.map +1 -0
  37. package/dist/types/src/extensions/code.d.ts.map +1 -0
  38. package/dist/types/src/{components/TextEditor/extensions → extensions}/comments.d.ts +6 -4
  39. package/dist/types/src/extensions/comments.d.ts.map +1 -0
  40. package/dist/types/src/extensions/cursor.d.ts +25 -0
  41. package/dist/types/src/extensions/cursor.d.ts.map +1 -0
  42. package/dist/types/src/extensions/hr.d.ts.map +1 -0
  43. package/dist/types/src/extensions/image.d.ts.map +1 -0
  44. package/dist/types/src/{components/TextEditor/extensions → extensions}/index.d.ts +4 -2
  45. package/dist/types/src/extensions/index.d.ts.map +1 -0
  46. package/dist/types/src/{components/TextEditor/extensions → extensions}/link.d.ts +1 -0
  47. package/dist/types/src/extensions/link.d.ts.map +1 -0
  48. package/dist/types/src/extensions/listener.d.ts +10 -0
  49. package/dist/types/src/extensions/listener.d.ts.map +1 -0
  50. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -0
  51. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -0
  52. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -0
  53. package/dist/types/src/extensions/mention.d.ts.map +1 -0
  54. package/dist/types/src/extensions/table.d.ts.map +1 -0
  55. package/dist/types/src/extensions/tasklist.d.ts +7 -0
  56. package/dist/types/src/extensions/tasklist.d.ts.map +1 -0
  57. package/dist/types/src/extensions/typewriter.d.ts.map +1 -0
  58. package/dist/types/src/extensions/yjs/cursor.d.ts +4 -0
  59. package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -0
  60. package/dist/types/src/extensions/yjs/index.d.ts +2 -0
  61. package/dist/types/src/extensions/yjs/index.d.ts.map +1 -0
  62. package/dist/types/src/extensions/yjs/yjs.d.ts +4 -0
  63. package/dist/types/src/extensions/yjs/yjs.d.ts.map +1 -0
  64. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +1 -0
  65. package/dist/types/src/hooks/automerge.d.ts +3 -0
  66. package/dist/types/src/hooks/automerge.d.ts.map +1 -0
  67. package/dist/types/src/hooks/awareness-provider.d.ts +28 -0
  68. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -0
  69. package/dist/types/src/hooks/index.d.ts +2 -0
  70. package/dist/types/src/hooks/index.d.ts.map +1 -1
  71. package/dist/types/src/hooks/useTextModel.d.ts +4 -8
  72. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  73. package/dist/types/src/hooks/{yjs/space-provider.d.ts → yjs.d.ts} +8 -8
  74. package/dist/types/src/hooks/yjs.d.ts.map +1 -0
  75. package/dist/types/src/index.d.ts +5 -0
  76. package/dist/types/src/index.d.ts.map +1 -1
  77. package/dist/types/src/styles/cursors.d.ts.map +1 -0
  78. package/dist/types/src/styles/index.d.ts +1 -0
  79. package/dist/types/src/styles/index.d.ts.map +1 -1
  80. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  81. package/dist/types/src/styles/tokens.d.ts +1 -0
  82. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  83. package/dist/types/src/testing/replicator.d.ts +2 -2
  84. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  85. package/dist/types/src/{components/TextEditor/themes → themes}/default.d.ts +1 -1
  86. package/dist/types/src/themes/default.d.ts.map +1 -0
  87. package/dist/types/src/themes/index.d.ts.map +1 -0
  88. package/dist/types/src/util.d.ts.map +1 -0
  89. package/package.json +24 -22
  90. package/src/components/TextEditor/MarkdownEditor.stories.tsx +36 -36
  91. package/src/components/TextEditor/TextEditor.stories.tsx +13 -34
  92. package/src/components/TextEditor/TextEditor.tsx +74 -70
  93. package/src/{hooks/automerge → components/TextEditor}/automerge.stories.tsx +66 -6
  94. package/src/components/TextEditor/codemirror.stories.ts +3 -2
  95. package/src/components/TextEditor/comments.stories.tsx +315 -0
  96. package/src/components/TextEditor/index.ts +0 -3
  97. package/src/{hooks/yjs → components/TextEditor}/yjs.stories.tsx +1 -1
  98. package/src/{components/TextEditor/extensions → extensions}/autocomplete.ts +18 -17
  99. package/src/extensions/automerge/automerge.ts +94 -0
  100. package/src/extensions/automerge/cursor.ts +52 -0
  101. package/src/extensions/automerge/defs.ts +62 -0
  102. package/src/extensions/automerge/index.ts +7 -0
  103. package/src/{hooks → extensions}/automerge/semaphore.ts +14 -12
  104. package/src/{hooks → extensions}/automerge/update-automerge.ts +5 -6
  105. package/src/{hooks → extensions}/automerge/update-codemirror.ts +13 -5
  106. package/src/extensions/awareness.ts +309 -0
  107. package/src/{components/TextEditor/extensions → extensions}/code.ts +14 -7
  108. package/src/{components/TextEditor/extensions → extensions}/comments.ts +50 -113
  109. package/src/extensions/cursor.ts +46 -0
  110. package/src/{components/TextEditor/extensions → extensions}/hr.ts +4 -5
  111. package/src/extensions/image.ts +87 -0
  112. package/src/{components/TextEditor/extensions → extensions}/index.ts +4 -2
  113. package/src/{components/TextEditor/extensions → extensions}/link.ts +116 -62
  114. package/src/extensions/listener.ts +35 -0
  115. package/src/{components/TextEditor/extensions → extensions}/markdown/highlight.ts +4 -13
  116. package/src/{components/TextEditor/extensions → extensions}/table.ts +10 -4
  117. package/src/extensions/tasklist.ts +95 -0
  118. package/src/{components/TextEditor/extensions → extensions}/typewriter.ts +1 -1
  119. package/src/extensions/yjs/cursor.ts +22 -0
  120. package/src/extensions/yjs/index.ts +5 -0
  121. package/src/{hooks → extensions}/yjs/yjs.test.ts +2 -26
  122. package/src/extensions/yjs/yjs.ts +16 -0
  123. package/src/hooks/automerge.ts +52 -0
  124. package/src/hooks/awareness-provider.ts +120 -0
  125. package/src/hooks/index.ts +2 -0
  126. package/src/hooks/useTextModel.ts +20 -96
  127. package/src/hooks/{yjs/space-provider.ts → yjs.ts} +49 -19
  128. package/src/index.ts +5 -0
  129. package/src/styles/index.ts +1 -0
  130. package/src/styles/markdown.ts +0 -2
  131. package/src/styles/tokens.ts +3 -0
  132. package/src/testing/replicator.ts +10 -15
  133. package/src/{components/TextEditor/themes → themes}/default.ts +52 -79
  134. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +0 -1
  135. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +0 -1
  136. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +0 -1
  137. package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +0 -1
  138. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +0 -1
  139. package/dist/types/src/components/TextEditor/extensions/hr.d.ts.map +0 -1
  140. package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +0 -1
  141. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +0 -1
  142. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +0 -1
  143. package/dist/types/src/components/TextEditor/extensions/listener.d.ts +0 -9
  144. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +0 -1
  145. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +0 -1
  146. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +0 -1
  147. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +0 -1
  148. package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +0 -1
  149. package/dist/types/src/components/TextEditor/extensions/mermaid.d.ts +0 -3
  150. package/dist/types/src/components/TextEditor/extensions/mermaid.d.ts.map +0 -1
  151. package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +0 -1
  152. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +0 -3
  153. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +0 -1
  154. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +0 -12
  155. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +0 -1
  156. package/dist/types/src/components/TextEditor/extensions/typewriter.d.ts.map +0 -1
  157. package/dist/types/src/components/TextEditor/extensions/util.d.ts.map +0 -1
  158. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +0 -1
  159. package/dist/types/src/components/TextEditor/themes/index.d.ts.map +0 -1
  160. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +0 -1
  161. package/dist/types/src/hooks/automerge/handle.d.ts +0 -9
  162. package/dist/types/src/hooks/automerge/handle.d.ts.map +0 -1
  163. package/dist/types/src/hooks/automerge/index.d.ts +0 -3
  164. package/dist/types/src/hooks/automerge/index.d.ts.map +0 -1
  165. package/dist/types/src/hooks/automerge/plugin.d.ts +0 -26
  166. package/dist/types/src/hooks/automerge/plugin.d.ts.map +0 -1
  167. package/dist/types/src/hooks/automerge/semaphore.d.ts.map +0 -1
  168. package/dist/types/src/hooks/automerge/update-automerge.d.ts +0 -6
  169. package/dist/types/src/hooks/automerge/update-automerge.d.ts.map +0 -1
  170. package/dist/types/src/hooks/automerge/update-codemirror.d.ts.map +0 -1
  171. package/dist/types/src/hooks/yjs/cursors.d.ts.map +0 -1
  172. package/dist/types/src/hooks/yjs/index.d.ts +0 -3
  173. package/dist/types/src/hooks/yjs/index.d.ts.map +0 -1
  174. package/dist/types/src/hooks/yjs/space-provider.d.ts.map +0 -1
  175. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +0 -1
  176. package/dist/types/src/hooks/yjs/yjs.test.d.ts.map +0 -1
  177. package/src/components/TextEditor/extensions/image.ts +0 -69
  178. package/src/components/TextEditor/extensions/listener.ts +0 -23
  179. package/src/components/TextEditor/extensions/mermaid.ts +0 -11
  180. package/src/components/TextEditor/extensions/tasklist.ts +0 -121
  181. package/src/components/TextEditor/extensions/tooltip.ts +0 -61
  182. package/src/hooks/automerge/handle.ts +0 -14
  183. package/src/hooks/automerge/index.ts +0 -6
  184. package/src/hooks/automerge/plugin.ts +0 -134
  185. package/src/hooks/yjs/index.ts +0 -6
  186. /package/dist/types/src/{components/TextEditor/extensions → extensions}/autocomplete.d.ts +0 -0
  187. /package/dist/types/src/{hooks → extensions}/automerge/update-codemirror.d.ts +0 -0
  188. /package/dist/types/src/{components/TextEditor/extensions → extensions}/basic.d.ts +0 -0
  189. /package/dist/types/src/{components/TextEditor/extensions → extensions}/blast.d.ts +0 -0
  190. /package/dist/types/src/{components/TextEditor/extensions → extensions}/code.d.ts +0 -0
  191. /package/dist/types/src/{components/TextEditor/extensions → extensions}/hr.d.ts +0 -0
  192. /package/dist/types/src/{components/TextEditor/extensions → extensions}/image.d.ts +0 -0
  193. /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/bundle.d.ts +0 -0
  194. /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/highlight.d.ts +0 -0
  195. /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/index.d.ts +0 -0
  196. /package/dist/types/src/{components/TextEditor/extensions → extensions}/mention.d.ts +0 -0
  197. /package/dist/types/src/{components/TextEditor/extensions → extensions}/table.d.ts +0 -0
  198. /package/dist/types/src/{components/TextEditor/extensions → extensions}/typewriter.d.ts +0 -0
  199. /package/dist/types/src/{hooks → extensions}/yjs/yjs.test.d.ts +0 -0
  200. /package/dist/types/src/{hooks/yjs → styles}/cursors.d.ts +0 -0
  201. /package/dist/types/src/{components/TextEditor/themes → themes}/index.d.ts +0 -0
  202. /package/dist/types/src/{components/TextEditor/extensions/util.d.ts → util.d.ts} +0 -0
  203. /package/src/{components/TextEditor/extensions → extensions}/basic.ts +0 -0
  204. /package/src/{components/TextEditor/extensions → extensions}/blast.ts +0 -0
  205. /package/src/{components/TextEditor/extensions → extensions}/markdown/bundle.ts +0 -0
  206. /package/src/{components/TextEditor/extensions → extensions}/markdown/index.ts +0 -0
  207. /package/src/{components/TextEditor/extensions → extensions}/mention.ts +0 -0
  208. /package/src/{hooks/yjs → styles}/cursors.ts +0 -0
  209. /package/src/{components/TextEditor/themes → themes}/index.ts +0 -0
  210. /package/src/{components/TextEditor/extensions/util.ts → util.ts} +0 -0
@@ -3,19 +3,7 @@
3
3
  //
4
4
 
5
5
  import { type Extension, StateEffect, StateField } from '@codemirror/state';
6
- import {
7
- hoverTooltip,
8
- keymap,
9
- type Command,
10
- type DecorationSet,
11
- type ViewUpdate,
12
- Decoration,
13
- EditorView,
14
- MatchDecorator,
15
- ViewPlugin,
16
- WidgetType,
17
- type Rect,
18
- } from '@codemirror/view';
6
+ import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
19
7
  import sortBy from 'lodash.sortby';
20
8
 
21
9
  import { debounce } from '@dxos/async';
@@ -23,28 +11,21 @@ import { invariant } from '@dxos/invariant';
23
11
  import { log } from '@dxos/log';
24
12
  import { nonNullable } from '@dxos/util';
25
13
 
26
- import { callbackWrapper } from './util';
27
- import { type CommentRange, type EditorModel, modelState, type Range } from '../../../hooks';
14
+ import { Cursor } from './cursor';
15
+ import { type CommentRange, type EditorModel, type Range } from '../hooks';
16
+ import { getToken } from '../styles';
17
+ import { callbackWrapper } from '../util';
28
18
 
29
19
  // TODO(burdon): Handle delete, cut, copy, and paste (separately) text that includes comment range.
30
20
  // TODO(burdon): Consider breaking into separate plugin (since not standalone)? Like mermaid?
31
21
 
32
22
  // TODO(burdon): Reconcile with theme.
33
23
  const styles = EditorView.baseTheme({
34
- '& .cm-bookmark': {
35
- cursor: 'pointer',
36
- margin: '4px',
37
- padding: '4px',
38
- backgroundColor: 'yellow',
39
- },
40
- '& .cm-bookmark-selected': {
41
- backgroundColor: 'orange',
42
- },
43
24
  '& .cm-comment': {
44
- backgroundColor: 'yellow',
25
+ backgroundColor: getToken('extend.colors.yellow.50'),
45
26
  },
46
27
  '& .cm-comment-active': {
47
- backgroundColor: 'orange',
28
+ backgroundColor: getToken('extend.colors.yellow.100'),
48
29
  },
49
30
  });
50
31
 
@@ -70,9 +51,22 @@ export type CommentsState = CommentSelected & {
70
51
  ranges: ExtendedCommentRange[];
71
52
  };
72
53
 
54
+ export const setFocus = (view: EditorView, thread: string) => {
55
+ const range = view.state.field(commentsStateField).ranges.find((range) => range.id === thread);
56
+ if (!range) {
57
+ return;
58
+ }
59
+
60
+ view.dispatch({
61
+ effects: setSelection.of({ active: thread }),
62
+ selection: { anchor: range.from },
63
+ scrollIntoView: true,
64
+ });
65
+ };
66
+
73
67
  export const setCommentRange = StateEffect.define<{ model: EditorModel; comments: CommentRange[] }>();
74
68
 
75
- const setSelection = StateEffect.define<CommentSelected>();
69
+ export const setSelection = StateEffect.define<CommentSelected>();
76
70
 
77
71
  const setCommentState = StateEffect.define<CommentsState>();
78
72
 
@@ -84,6 +78,8 @@ const setCommentState = StateEffect.define<CommentsState>();
84
78
  const commentsStateField = StateField.define<CommentsState>({
85
79
  create: () => ({ ranges: [] }),
86
80
  update: (value, tr) => {
81
+ const cursorConverter = tr.state.facet(Cursor.converter);
82
+
87
83
  for (const effect of tr.effects) {
88
84
  // Update selection.
89
85
  if (effect.is(setSelection)) {
@@ -92,11 +88,13 @@ const commentsStateField = StateField.define<CommentsState>({
92
88
 
93
89
  // Update range from store.
94
90
  if (effect.is(setCommentRange)) {
95
- const { model, comments } = effect.value;
96
- const ranges: ExtendedCommentRange[] = comments.map((comment) => {
97
- const range = model.getRangeFromCursor!(comment.cursor)!;
98
- return { ...comment, ...range };
99
- });
91
+ const { comments } = effect.value;
92
+ const ranges: ExtendedCommentRange[] = comments
93
+ .map((comment) => {
94
+ const range = Cursor.getRangeFromCursor(cursorConverter, comment.cursor);
95
+ return range && { ...comment, ...range };
96
+ })
97
+ .filter(nonNullable);
100
98
 
101
99
  return { ...value, ranges };
102
100
  }
@@ -136,42 +134,13 @@ const highlightDecorations = EditorView.decorations.compute([commentsStateField]
136
134
  return Decoration.set(decorations);
137
135
  });
138
136
 
139
- /**
140
- * Optional bookmark widget at the start/end of the selection.
141
- * May correspond to a markdown bookmark.
142
- */
143
- class BookmarkWidget extends WidgetType {
144
- constructor(
145
- private readonly _anchor: number,
146
- private readonly _id: string,
147
- private readonly _handleClick?: () => void,
148
- ) {
149
- super();
150
- invariant(this._id);
151
- }
152
-
153
- override eq(other: BookmarkWidget) {
154
- return this._anchor === other._anchor && this._id === other._id;
155
- }
156
-
157
- override toDOM() {
158
- const span = document.createElement('span');
159
- span.className = 'cm-bookmark';
160
- span.textContent = '※';
161
- if (this._handleClick) {
162
- span.onclick = () => this._handleClick!();
163
- }
164
- return span;
165
- }
166
- }
167
-
168
137
  export type CommentsOptions = {
169
138
  key?: string;
170
139
  footnote?: boolean;
171
140
  /**
172
141
  * Called to create a new thread and return the thread id.
173
142
  */
174
- onCreate?: (range: string) => string | undefined;
143
+ onCreate?: (cursor: string, location?: Rect | null) => string | undefined;
175
144
  /**
176
145
  * Called to notify which thread is currently closest to the cursor.
177
146
  */
@@ -179,7 +148,7 @@ export type CommentsOptions = {
179
148
  /**
180
149
  * Called to render tooltip.
181
150
  */
182
- onHover?: (el: Element) => void;
151
+ onHover?: (el: Element, shortcut: string) => void;
183
152
  };
184
153
 
185
154
  /**
@@ -195,23 +164,26 @@ export type CommentsOptions = {
195
164
  * 6). Optionally, implements a hoverTooltip to show hints when creating a selection range.
196
165
  */
197
166
  export const comments = (options: CommentsOptions = {}): Extension => {
167
+ const { key: shortcut = "meta-'" } = options;
168
+
198
169
  const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
199
170
 
200
171
  /**
201
172
  * Create comment thread action.
202
173
  */
203
174
  const createCommentThread: Command = (view) => {
175
+ const cursorConverter = view.state.facet(Cursor.converter);
176
+
204
177
  invariant(options.onCreate);
205
178
  const { head, from, to } = view.state.selection.main;
206
179
  if (from === to) {
207
180
  return false;
208
181
  }
209
182
 
210
- const model = view.state.field(modelState);
211
- const relPos = model?.getCursorFromRange?.({ from, to });
212
- if (relPos) {
183
+ const cursor = Cursor.getCursorFromRange(cursorConverter, { from, to });
184
+ if (cursor) {
213
185
  // Create thread via callback.
214
- const id = callbackWrapper(options.onCreate)(relPos);
186
+ const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
215
187
  if (id) {
216
188
  // Update range.
217
189
  view.dispatch({
@@ -235,44 +207,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
235
207
  return false;
236
208
  };
237
209
 
238
- /**
239
- * Bookmark widget decoration.
240
- */
241
- const bookmarksViewPlugin = ViewPlugin.fromClass(
242
- class BookmarkViewPlugin {
243
- // Match markdown footnotes (option).
244
- // https://www.markdownguide.org/extended-syntax/#footnotes
245
- static bookmarkMatcher = new MatchDecorator({
246
- regexp: /\[\^(\w+)\]/g,
247
- decoration: (match, view, pos) => {
248
- const id = match[1];
249
- return Decoration.replace({
250
- id,
251
- widget: new BookmarkWidget(pos, id),
252
- });
253
- },
254
- });
255
-
256
- bookmarks: DecorationSet;
257
- constructor(view: EditorView) {
258
- this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.createDeco(view);
259
- }
260
-
261
- update(update: ViewUpdate) {
262
- this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.updateDeco(update, this.bookmarks);
263
- }
264
- },
265
- {
266
- decorations: (instance) => instance.bookmarks,
267
- provide: (plugin) =>
268
- EditorView.atomicRanges.of((view) => {
269
- return view.plugin(plugin)?.bookmarks || Decoration.none;
270
- }),
271
- },
272
- );
273
-
274
210
  return [
275
- bookmarksViewPlugin,
276
211
  commentsStateField,
277
212
  highlightDecorations,
278
213
  styles,
@@ -283,8 +218,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
283
218
  options.onCreate
284
219
  ? keymap.of([
285
220
  {
286
- key: options?.key ?? "meta-'",
287
- run: createCommentThread,
221
+ key: shortcut,
222
+ run: callbackWrapper(createCommentThread),
288
223
  },
289
224
  ])
290
225
  : [],
@@ -305,7 +240,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
305
240
  create: () => {
306
241
  // TODO(burdon): Dispatch to react callback to render (or use SSR)?
307
242
  const el = document.createElement('div');
308
- options.onHover?.(el);
243
+ options.onHover?.(el, shortcut);
309
244
  return { dom: el, offset: { x: 0, y: 8 } };
310
245
  },
311
246
  };
@@ -321,27 +256,27 @@ export const comments = (options: CommentsOptions = {}): Extension => {
321
256
  // Monitor cursor movement and text updates.
322
257
  // TODO(burdon): Is there a better (finer grained) way to do this?
323
258
  //
324
- EditorView.updateListener.of(({ view, state, startState, changes }) => {
259
+ EditorView.updateListener.of(({ view, state, changes }) => {
260
+ const cursorConverter = view.state.facet(Cursor.converter);
261
+
325
262
  //
326
263
  // Test if need to recompute indexed range if document changes before the end of the range.
327
264
  //
328
265
  {
329
266
  let mod = false;
330
- const model = state.field(modelState);
331
267
  const { active, ranges } = state.field(commentsStateField);
332
268
  changes.iterChanges((from, to, from2, to2) => {
333
- invariant(model);
334
269
  ranges.forEach((range) => {
335
270
  if (from2 === to2) {
336
- const newRange = model.getRangeFromCursor!(range.cursor);
337
- if (newRange.to - newRange.from === 0) {
271
+ const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
272
+ if (!newRange || newRange.to - newRange.from === 0) {
338
273
  // TODO(burdon): Delete range if empty.
339
274
  log.info('deleted comment', { thread: range.id });
340
275
  }
341
276
  }
342
277
 
343
278
  if (from <= range.to) {
344
- const newRange = model.getRangeFromCursor!(range.cursor);
279
+ const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
345
280
  Object.assign(range, newRange);
346
281
  mod = true;
347
282
  }
@@ -373,6 +308,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
373
308
  }
374
309
  });
375
310
 
311
+ // TODO(burdon): Fire debounced callback if range selected (to show hint).
312
+
376
313
  if (selected.active !== active || selected.closest !== closest) {
377
314
  view.dispatch({ effects: setSelection.of(selected) });
378
315
 
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { 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 = (cursorConverter: CursorConverter, range: Range) => {
35
+ const from = cursorConverter.toCursor(range.from);
36
+ const to = cursorConverter.toCursor(range.to, -1);
37
+ return [from, to].join(':');
38
+ };
39
+
40
+ static readonly getRangeFromCursor = (cursorConverter: CursorConverter, cursor: string) => {
41
+ const parts = cursor.split(':');
42
+ const from = cursorConverter.fromCursor(parts[0]);
43
+ const to = cursorConverter.fromCursor(parts[1]);
44
+ return from !== undefined && to !== undefined ? { from, to } : undefined;
45
+ };
46
+ }
@@ -11,16 +11,15 @@ import {
11
11
  type ViewUpdate,
12
12
  WidgetType,
13
13
  } from '@codemirror/view';
14
- import get from 'lodash.get';
15
14
 
16
- import { tokens } from '../../../styles';
15
+ import { getToken } from '../styles';
17
16
 
18
17
  // TODO(burdon): Reconcile with theme.
19
18
  const styles = EditorView.baseTheme({
20
19
  '& .cm-hr': {
21
20
  // TODO(burdon): ???
22
21
  // Note that block-level decorations should not have vertical margins,
23
- borderBottom: `1px solid ${get(tokens, 'extend.colors.neutral.200')}`,
22
+ borderBottom: `1px solid ${getToken('extend.colors.neutral.200')}`,
24
23
  },
25
24
  });
26
25
 
@@ -29,11 +28,11 @@ class HorizontalRuleWidget extends WidgetType {
29
28
  super();
30
29
  }
31
30
 
32
- override eq(other: WidgetType) {
31
+ override eq(other: this) {
33
32
  return this._pos === (other as any as HorizontalRuleWidget)._pos;
34
33
  }
35
34
 
36
- toDOM(view: EditorView) {
35
+ override toDOM(view: EditorView) {
37
36
  // TODO(burdon): Create <hr> element? Does this clash with markdown parser?
38
37
  const el = document.createElement('div');
39
38
  el.className = 'cm-hr';
@@ -0,0 +1,87 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { type EditorState, type Extension, StateField, type Transaction, type Range } from '@codemirror/state';
7
+ import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
8
+
9
+ export type ImageOptions = {};
10
+
11
+ export const image = (options: ImageOptions = {}): Extension => {
12
+ return StateField.define<DecorationSet>({
13
+ create: (state) => {
14
+ return Decoration.set(buildDecorations(0, state.doc.length, state));
15
+ },
16
+ update: (value: DecorationSet, tr: Transaction) => {
17
+ if (!tr.docChanged && !tr.selection) {
18
+ return value;
19
+ }
20
+ // Find range of changes and cursor changes.
21
+ const cursor = tr.state.selection.main.head;
22
+ const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);
23
+ let from = Math.min(cursor, oldCursor);
24
+ let to = Math.max(cursor, oldCursor);
25
+ tr.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
26
+ from = Math.min(from, fromB);
27
+ to = Math.max(to, toB);
28
+ });
29
+ // Expand to cover lines
30
+ from = tr.state.doc.lineAt(from).from;
31
+ to = tr.state.doc.lineAt(to).to;
32
+ return value.map(tr.changes).update({
33
+ filterFrom: from,
34
+ filterTo: to,
35
+ filter: () => false,
36
+ add: buildDecorations(from, to, tr.state),
37
+ });
38
+ },
39
+ provide: (field) => EditorView.decorations.from(field),
40
+ });
41
+ };
42
+
43
+ const buildDecorations = (from: number, to: number, state: EditorState) => {
44
+ const decorations: Range<Decoration>[] = [];
45
+ const cursor = state.selection.main.head;
46
+
47
+ syntaxTree(state).iterate({
48
+ enter: (node) => {
49
+ if (node.name === 'Image') {
50
+ const urlNode = node.node.getChild('URL');
51
+ if (urlNode) {
52
+ const hide = state.readOnly || cursor < node.from || cursor > node.to;
53
+ const url = state.sliceDoc(urlNode.from, urlNode.to);
54
+ decorations.push(
55
+ Decoration.replace({
56
+ block: true, // Prevent cursor from entering.
57
+ widget: new ImageWidget(url),
58
+ }).range(hide ? node.from : node.to, node.to),
59
+ );
60
+ }
61
+ }
62
+ },
63
+ from,
64
+ to,
65
+ });
66
+
67
+ return decorations;
68
+ };
69
+
70
+ class ImageWidget extends WidgetType {
71
+ constructor(readonly _url: string) {
72
+ super();
73
+ }
74
+
75
+ override eq(other: this) {
76
+ return this._url === (other as any as ImageWidget)._url;
77
+ }
78
+
79
+ override toDOM(view: EditorView) {
80
+ const img = document.createElement('img');
81
+ img.setAttribute('src', this._url);
82
+ img.setAttribute('class', 'cm-image');
83
+ // Images are hidden until successfully loaded to avoid flickering effects.
84
+ img.onload = () => img.classList.add('cm-loaded-image');
85
+ return img;
86
+ }
87
+ }
@@ -3,18 +3,20 @@
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
10
  export * from './code';
9
11
  export * from './comments';
12
+ export * from './cursor';
10
13
  export * from './hr';
11
14
  export * from './image';
12
15
  export * from './link';
13
16
  export * from './listener';
14
17
  export * from './markdown';
15
18
  export * from './mention';
16
- export * from './mermaid';
17
19
  export * from './table';
18
20
  export * from './tasklist';
19
- export * from './tooltip';
20
21
  export * from './typewriter';
22
+ export * from './yjs';