@dxos/react-ui-editor 0.3.11-main.7ff9773 → 0.3.11-main.80db41b

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 (166) hide show
  1. package/dist/lib/browser/index.mjs +1402 -934
  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 +1 -2
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +3 -2
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -1
  9. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/{yjs.stories.d.ts → hooks.stories.d.ts} +13 -2
  11. package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
  12. package/dist/types/src/components/Toolbar/Toolbar.d.ts +25 -0
  13. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
  14. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +23 -0
  15. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
  16. package/dist/types/src/components/Toolbar/index.d.ts +2 -0
  17. package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
  18. package/dist/types/src/components/index.d.ts +1 -0
  19. package/dist/types/src/components/index.d.ts.map +1 -1
  20. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  21. package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
  22. package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
  23. package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
  24. package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
  25. package/dist/types/src/extensions/automerge/defs.d.ts +5 -5
  26. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  27. package/dist/types/src/extensions/automerge/semaphore.d.ts +2 -1
  28. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  29. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  30. package/dist/types/src/extensions/awareness.d.ts +2 -4
  31. package/dist/types/src/extensions/awareness.d.ts.map +1 -1
  32. package/dist/types/src/extensions/basic.d.ts +2 -2
  33. package/dist/types/src/extensions/basic.d.ts.map +1 -1
  34. package/dist/types/src/extensions/comments.d.ts +9 -5
  35. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  36. package/dist/types/src/extensions/index.d.ts +0 -7
  37. package/dist/types/src/extensions/index.d.ts.map +1 -1
  38. package/dist/types/src/extensions/markdown/bundle.d.ts +2 -2
  39. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  40. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
  41. package/dist/types/src/extensions/markdown/formatting.d.ts +13 -0
  42. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
  43. package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
  44. package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
  45. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  46. package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
  47. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  48. package/dist/types/src/extensions/markdown/index.d.ts +8 -0
  49. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  50. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  51. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
  52. package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
  53. package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
  54. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
  55. package/dist/types/src/hooks/defs.d.ts +1 -4
  56. package/dist/types/src/hooks/defs.d.ts.map +1 -1
  57. package/dist/types/src/hooks/index.d.ts +3 -2
  58. package/dist/types/src/hooks/index.d.ts.map +1 -1
  59. package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
  60. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
  61. package/dist/types/src/hooks/useEditorView.d.ts +17 -0
  62. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
  63. package/dist/types/src/hooks/useTextEditor.d.ts +39 -0
  64. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  65. package/dist/types/src/hooks/useTextModel.d.ts +12 -0
  66. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  67. package/dist/types/src/index.d.ts +1 -1
  68. package/dist/types/src/index.d.ts.map +1 -1
  69. package/dist/types/src/styles/markdown.d.ts +2 -2
  70. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  71. package/dist/types/src/themes/default.d.ts.map +1 -1
  72. package/package.json +39 -29
  73. package/src/components/TextEditor/MarkdownEditor.stories.tsx +15 -19
  74. package/src/components/TextEditor/TextEditor.tsx +46 -27
  75. package/src/components/TextEditor/automerge.stories.tsx +1 -2
  76. package/src/components/TextEditor/hooks.stories.tsx +106 -0
  77. package/src/components/Toolbar/Toolbar.stories.tsx +90 -0
  78. package/src/components/Toolbar/Toolbar.tsx +166 -0
  79. package/src/components/Toolbar/index.ts +5 -0
  80. package/src/components/index.ts +1 -0
  81. package/src/extensions/automerge/automerge.spec.tsx +76 -0
  82. package/src/extensions/automerge/automerge.test.ts +13 -0
  83. package/src/extensions/automerge/automerge.ts +44 -10
  84. package/src/extensions/automerge/cursor.ts +3 -3
  85. package/src/extensions/automerge/defs.ts +9 -9
  86. package/src/extensions/automerge/semaphore.ts +17 -19
  87. package/src/extensions/automerge/update-automerge.ts +12 -6
  88. package/src/extensions/automerge/update-codemirror.ts +5 -5
  89. package/src/extensions/awareness.ts +21 -35
  90. package/src/extensions/basic.ts +17 -15
  91. package/src/extensions/blast.ts +4 -1
  92. package/src/extensions/comments.ts +137 -79
  93. package/src/extensions/index.ts +0 -7
  94. package/src/extensions/markdown/bundle.ts +9 -8
  95. package/src/extensions/markdown/code.ts +150 -0
  96. package/src/extensions/markdown/formatting.ts +188 -0
  97. package/src/extensions/markdown/heading.ts +59 -0
  98. package/src/extensions/markdown/highlight.ts +9 -15
  99. package/src/extensions/{hr.ts → markdown/hr.ts} +3 -4
  100. package/src/extensions/markdown/index.ts +8 -0
  101. package/src/extensions/{link.ts → markdown/link.ts} +13 -4
  102. package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +2 -3
  103. package/src/hooks/awareness-provider.ts +24 -20
  104. package/src/hooks/defs.ts +1 -6
  105. package/src/hooks/index.ts +3 -2
  106. package/src/hooks/useActionHandler.ts +62 -0
  107. package/src/hooks/useEditorView.ts +29 -0
  108. package/src/hooks/useTextEditor.ts +153 -0
  109. package/src/hooks/useTextModel.ts +65 -12
  110. package/src/index.ts +1 -1
  111. package/src/styles/markdown.ts +9 -10
  112. package/src/themes/default.ts +11 -8
  113. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  114. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  115. package/dist/types/src/components/TextEditor/comments.stories.d.ts +0 -26
  116. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +0 -1
  117. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  118. package/dist/types/src/extensions/code.d.ts.map +0 -1
  119. package/dist/types/src/extensions/hr.d.ts.map +0 -1
  120. package/dist/types/src/extensions/image.d.ts.map +0 -1
  121. package/dist/types/src/extensions/link.d.ts.map +0 -1
  122. package/dist/types/src/extensions/table.d.ts.map +0 -1
  123. package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
  124. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  125. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  126. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  127. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  128. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  129. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  130. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  131. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  132. package/dist/types/src/hooks/automerge.d.ts +0 -4
  133. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  134. package/dist/types/src/hooks/yjs.d.ts +0 -28
  135. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  136. package/dist/types/src/testing/index.d.ts +0 -3
  137. package/dist/types/src/testing/index.d.ts.map +0 -1
  138. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  139. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  140. package/dist/types/src/testing/proto/index.d.ts +0 -2
  141. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  142. package/dist/types/src/testing/replicator.d.ts +0 -45
  143. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  144. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  145. package/src/components/TextEditor/comments.stories.tsx +0 -357
  146. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  147. package/src/extensions/code.ts +0 -104
  148. package/src/extensions/yjs/cursor.ts +0 -22
  149. package/src/extensions/yjs/index.ts +0 -5
  150. package/src/extensions/yjs/yjs.test.ts +0 -35
  151. package/src/extensions/yjs/yjs.ts +0 -16
  152. package/src/hooks/automerge.ts +0 -53
  153. package/src/hooks/yjs.ts +0 -157
  154. package/src/testing/index.ts +0 -6
  155. package/src/testing/proto/gen/schema.ts +0 -49
  156. package/src/testing/proto/index.ts +0 -5
  157. package/src/testing/proto/schema.proto +0 -15
  158. package/src/testing/replicator.ts +0 -184
  159. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  160. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  161. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  162. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  163. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  164. /package/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.ts} +0 -0
  165. /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
  166. /package/src/extensions/{table.ts → markdown/table.ts} +0 -0
@@ -2,13 +2,13 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type Extension, StateEffect, StateField, type Text } from '@codemirror/state';
5
+ import { invertedEffects } from '@codemirror/commands';
6
+ import { type Extension, Facet, StateEffect, StateField, type Text, type ChangeDesc } from '@codemirror/state';
6
7
  import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
7
8
  import sortBy from 'lodash.sortby';
8
9
  import { useEffect } from 'react';
9
10
 
10
11
  import { debounce } from '@dxos/async';
11
- import { invariant } from '@dxos/invariant';
12
12
  import { nonNullable } from '@dxos/util';
13
13
 
14
14
  import { Cursor } from './cursor';
@@ -18,18 +18,26 @@ import { callbackWrapper } from '../util';
18
18
 
19
19
  // TODO(burdon): Reconcile with theme.
20
20
  const styles = EditorView.baseTheme({
21
- '& .cm-comment': {
21
+ '&light .cm-comment, &light .cm-comment-current': { mixBlendMode: 'darken' },
22
+ '&dark .cm-comment, &dark .cm-comment-current': { mixBlendMode: 'plus-lighter' },
23
+ '&light .cm-comment': {
22
24
  backgroundColor: getToken('extend.colors.yellow.50'),
23
25
  },
24
- '& .cm-comment-active': {
26
+ '&light .cm-comment-current': {
25
27
  backgroundColor: getToken('extend.colors.yellow.100'),
26
28
  },
29
+ '&dark .cm-comment': {
30
+ color: getToken('extend.colors.yellow.50'),
31
+ backgroundColor: getToken('extend.colors.yellow.900'),
32
+ },
33
+ '&dark .cm-comment-current': {
34
+ color: getToken('extend.colors.yellow.100'),
35
+ backgroundColor: getToken('extend.colors.yellow.950'),
36
+ },
27
37
  });
28
38
 
29
- const marks = {
30
- highlight: Decoration.mark({ class: 'cm-comment' }),
31
- highlightActive: Decoration.mark({ class: 'cm-comment-active' }),
32
- };
39
+ const commentMark = Decoration.mark({ class: 'cm-comment' });
40
+ const commentCurrentMark = Decoration.mark({ class: 'cm-comment-current' });
33
41
 
34
42
  type CommentState = {
35
43
  comment: Comment;
@@ -38,7 +46,7 @@ type CommentState = {
38
46
  };
39
47
 
40
48
  type SelectionState = {
41
- active?: string;
49
+ current?: string;
42
50
  closest?: string;
43
51
  };
44
52
 
@@ -47,7 +55,7 @@ export type CommentsState = {
47
55
  selection: SelectionState;
48
56
  };
49
57
 
50
- export const setFocus = (view: EditorView, id: string, center = true) => {
58
+ export const focusComment = (view: EditorView, id: string, center = true) => {
51
59
  const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === id);
52
60
  if (!comment?.comment.cursor) {
53
61
  return;
@@ -60,7 +68,7 @@ export const setFocus = (view: EditorView, id: string, center = true) => {
60
68
  effects: [
61
69
  //
62
70
  EditorView.scrollIntoView(range.from, center ? { y: 'center' } : undefined),
63
- setSelection.of({ active: id }),
71
+ setSelection.of({ current: id }),
64
72
  ],
65
73
  });
66
74
  }
@@ -115,9 +123,9 @@ const commentsState = StateField.define<CommentsState>({
115
123
  /**
116
124
  * Decorate ranges.
117
125
  */
118
- const highlightDecorations = EditorView.decorations.compute([commentsState], (state) => {
126
+ const commentsDecorations = EditorView.decorations.compute([commentsState], (state) => {
119
127
  const {
120
- selection: { active },
128
+ selection: { current },
121
129
  comments,
122
130
  } = state.field(commentsState);
123
131
 
@@ -129,10 +137,10 @@ const highlightDecorations = EditorView.decorations.compute([commentsState], (st
129
137
  return undefined;
130
138
  }
131
139
 
132
- if (comment.comment.id === active) {
133
- return marks.highlightActive.range(range.from, range.to);
140
+ if (comment.comment.id === current) {
141
+ return commentCurrentMark.range(range.from, range.to);
134
142
  } else {
135
- return marks.highlight.range(range.from, range.to);
143
+ return commentMark.range(range.from, range.to);
136
144
  }
137
145
  })
138
146
  .filter(nonNullable);
@@ -167,11 +175,11 @@ export type CommentsOptions = {
167
175
  onHover?: (el: Element, shortcut: string) => void;
168
176
  };
169
177
 
170
- // TODO(burdon): Handle cut/restore via undo (need to integrate with history?)
178
+ type TrackedComment = { id: string; from: number; to: number };
179
+
171
180
  const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>) => {
172
181
  // Tracks indexed selections within text.
173
- // TODO(burdon): Move to main state field?
174
- let tracked: { text: Text; comments: { id: string; from: number; to: number }[] } | null = null;
182
+ let tracked: { text: Text; comments: TrackedComment[] } | null = null;
175
183
 
176
184
  // Track cut or copy (enables cut-and-paste and copy-delete-paste to restore comment selection).
177
185
  const handleTrack = (event: Event, view: EditorView) => {
@@ -201,16 +209,48 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
201
209
  copy: handleTrack,
202
210
  }),
203
211
 
204
- // Handle paste.
205
- EditorView.updateListener.of(({ view, state, transactions }) => {
212
+ // Track deleted comments.
213
+ invertedEffects.of((tr) => {
214
+ const { comments } = tr.startState.field(commentsState);
215
+ const effects: StateEffect<any>[] = [];
216
+ tr.changes.iterChangedRanges((fromA, toA) => {
217
+ for (const {
218
+ comment: { id },
219
+ range: { from, to },
220
+ } of comments) {
221
+ if (from < to && from >= fromA && to <= toA) {
222
+ effects.push(restoreCommentEffect.of({ id, from, to }));
223
+ }
224
+ }
225
+ });
226
+
227
+ return effects;
228
+ }),
229
+
230
+ // Handle paste or the undo of comment deletion.
231
+ EditorView.updateListener.of((update) => {
232
+ const restore: TrackedComment[] = [];
233
+
234
+ for (let i = 0; i < update.transactions.length; i++) {
235
+ const tr = update.transactions[i];
236
+ for (let j = 0; j < restore.length; j++) {
237
+ restore[j] = mapTrackedComment(restore[j], tr.changes);
238
+ }
239
+ for (const effect of tr.effects) {
240
+ if (effect.is(restoreCommentEffect)) {
241
+ restore.push(effect.value);
242
+ }
243
+ }
244
+ }
245
+
206
246
  if (tracked) {
207
- const paste = transactions.find((tr) => tr.isUserEvent('input.paste'));
247
+ const paste = update.transactions.find((tr) => tr.isUserEvent('input.paste'));
208
248
  if (paste) {
209
249
  let found = -1;
210
250
  paste.changes.iterChanges((fromA, toA, fromB, toB, text) => {
211
251
  if (text.eq(tracked!.text)) {
212
- for (let i = transactions.indexOf(paste!) + 1; i < transactions.length; i++) {
213
- fromB = transactions[i].changes.mapPos(fromB);
252
+ for (let i = update.transactions.indexOf(paste!) + 1; i < update.transactions.length; i++) {
253
+ fromB = update.transactions[i].changes.mapPos(fromB);
214
254
  }
215
255
 
216
256
  found = fromB;
@@ -219,23 +259,78 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
219
259
 
220
260
  if (found > -1) {
221
261
  for (const moved of tracked.comments) {
222
- const range = {
223
- from: found + moved.from,
224
- to: found + moved.to,
225
- };
226
-
227
- const cursor = Cursor.getCursorFromRange(state, range);
228
- onUpdate(moved.id, cursor);
262
+ restore.push({ id: moved.id, from: found + moved.from, to: found + moved.to });
229
263
  }
230
264
  }
231
265
 
232
266
  tracked = null;
233
267
  }
234
268
  }
269
+
270
+ for (const comment of restore) {
271
+ const { comments } = update.startState.field(commentsState);
272
+ const exists = comments.some((c) => c.comment.id === comment.id && c.range.from < c.range.to);
273
+ if (!exists) {
274
+ const cursor = Cursor.getCursorFromRange(update.state, comment);
275
+ onUpdate(comment.id, cursor);
276
+ }
277
+ }
235
278
  }),
236
279
  ];
237
280
  };
238
281
 
282
+ const mapTrackedComment = (comment: TrackedComment, changes: ChangeDesc) => ({
283
+ id: comment.id,
284
+ from: changes.mapPos(comment.from, 1),
285
+ to: changes.mapPos(comment.to, 1),
286
+ });
287
+
288
+ // These are attached to undone/redone transactions in the editor for the purpose of restoring comments
289
+ // that were deleted by the original changes.
290
+ const restoreCommentEffect = StateEffect.define<TrackedComment>({ map: mapTrackedComment });
291
+
292
+ const optionsFacet = Facet.define<CommentsOptions, CommentsOptions>({
293
+ combine: (providers) => providers[0],
294
+ });
295
+
296
+ /**
297
+ * Create comment thread action.
298
+ */
299
+ export const createComment: Command = (view) => {
300
+ const options = view.state.facet(optionsFacet);
301
+ const { from, to } = view.state.selection.main;
302
+ if (from === to) {
303
+ return false;
304
+ }
305
+
306
+ // Don't allow selection at end of document.
307
+ if (to === view.state.doc.length) {
308
+ view.dispatch({
309
+ changes: {
310
+ from: to,
311
+ insert: '\n',
312
+ },
313
+ });
314
+ }
315
+
316
+ const cursor = Cursor.getCursorFromRange(view.state, { from, to });
317
+ if (cursor) {
318
+ // Create thread via callback.
319
+ const id = options.onCreate?.(cursor, view.coordsAtPos(from));
320
+ if (id) {
321
+ // Update range.
322
+ view.dispatch({
323
+ effects: setSelection.of({ current: id }),
324
+ selection: { anchor: to },
325
+ });
326
+
327
+ return true;
328
+ }
329
+ }
330
+
331
+ return false;
332
+ };
333
+
239
334
  /**
240
335
  * Comment threads.
241
336
  * 1). Updates the EditorModel to store relative selections for a set of comments threads.
@@ -252,47 +347,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
252
347
 
253
348
  const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
254
349
 
255
- /**
256
- * Create comment thread action.
257
- */
258
- const createCommentThread: Command = (view) => {
259
- invariant(options.onCreate);
260
- const { from, to } = view.state.selection.main;
261
- if (from === to) {
262
- return false;
263
- }
264
-
265
- // Don't allow selection at end of document.
266
- if (to === view.state.doc.length) {
267
- view.dispatch({
268
- changes: {
269
- from: to,
270
- insert: '\n',
271
- },
272
- });
273
- }
274
-
275
- const cursor = Cursor.getCursorFromRange(view.state, { from, to });
276
- if (cursor) {
277
- // Create thread via callback.
278
- const id = options.onCreate?.(cursor, view.coordsAtPos(from));
279
- if (id) {
280
- // Update range.
281
- view.dispatch({
282
- effects: setSelection.of({ active: id }),
283
- selection: { anchor: from },
284
- });
285
-
286
- return true;
287
- }
288
- }
289
-
290
- return false;
291
- };
292
-
293
350
  return [
351
+ optionsFacet.of(options),
294
352
  commentsState,
295
- highlightDecorations,
353
+ commentsDecorations,
296
354
  styles,
297
355
 
298
356
  //
@@ -302,7 +360,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
302
360
  ? keymap.of([
303
361
  {
304
362
  key: shortcut,
305
- run: callbackWrapper(createCommentThread),
363
+ run: callbackWrapper(createComment),
306
364
  },
307
365
  ])
308
366
  : [],
@@ -371,10 +429,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
371
429
  //
372
430
  // Track selection/proximity.
373
431
  //
374
- EditorView.updateListener.of(({ view, state, changes }) => {
432
+ EditorView.updateListener.of(({ view, state }) => {
375
433
  let min = Infinity;
376
434
  const {
377
- selection: { active, closest },
435
+ selection: { current, closest },
378
436
  comments,
379
437
  } = state.field(commentsState);
380
438
 
@@ -382,11 +440,11 @@ export const comments = (options: CommentsOptions = {}): Extension => {
382
440
  const selection: SelectionState = {};
383
441
  comments.forEach(({ comment, range }) => {
384
442
  if (head >= range.from && head <= range.to) {
385
- selection.active = comment.id;
443
+ selection.current = comment.id;
386
444
  selection.closest = undefined;
387
445
  }
388
446
 
389
- if (!selection.active) {
447
+ if (!selection.current) {
390
448
  const d = Math.min(Math.abs(head - range.from), Math.abs(head - range.to));
391
449
  if (d < min) {
392
450
  selection.closest = comment.id;
@@ -395,7 +453,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
395
453
  }
396
454
  });
397
455
 
398
- if (selection.active !== active || selection.closest !== closest) {
456
+ if (selection.current !== current || selection.closest !== closest) {
399
457
  view.dispatch({ effects: setSelection.of(selection) });
400
458
 
401
459
  // Update callback.
@@ -415,11 +473,11 @@ export const comments = (options: CommentsOptions = {}): Extension => {
415
473
  };
416
474
 
417
475
  /**
418
- * Update state field.
476
+ * Update comments state field.
419
477
  */
420
- export const useComments = (view?: EditorView | null, comments?: Comment[]) => {
478
+ export const useComments = (view: EditorView | null, comments: Comment[] = []) => {
421
479
  useEffect(() => {
422
- if (view && comments) {
480
+ if (view) {
423
481
  view.dispatch({
424
482
  effects: setComments.of(comments),
425
483
  });
@@ -7,17 +7,10 @@ export * from './automerge';
7
7
  export * from './awareness';
8
8
  export * from './basic';
9
9
  export * from './blast';
10
- export * from './code';
11
10
  export * from './comments';
12
11
  export * from './cursor';
13
- export * from './hr';
14
- export * from './image';
15
- export * from './link';
16
12
  export * from './listener';
17
13
  export * from './markdown';
18
14
  export * from './mention';
19
15
  export * from './outliner';
20
- export * from './table';
21
- export * from './tasklist';
22
16
  export * from './typewriter';
23
- export * from './yjs';
@@ -21,13 +21,14 @@ 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';
26
27
  import { type TextEditorProps } from '../../components';
27
28
 
28
29
  export type MarkdownBundleOptions = {
29
30
  themeMode?: ThemeMode;
30
- } & Pick<TextEditorProps, 'readonly' | 'placeholder'>;
31
+ } & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
31
32
 
32
33
  /**
33
34
  * Markdown bundle.
@@ -36,15 +37,15 @@ export type MarkdownBundleOptions = {
36
37
  * https://codemirror.net/docs/ref/#codemirror.basicSetup
37
38
  */
38
39
  // TODO(burdon): Add Composer here: https://codemirror.net/docs/community
39
- export const markdownBundle = ({
40
- readonly,
40
+ export const createMarkdownExtensions = ({
41
41
  themeMode,
42
42
  placeholder: _placeholder,
43
- }: MarkdownBundleOptions): Extension[] => {
43
+ lineWrapping = true,
44
+ }: MarkdownBundleOptions = {}): Extension[] => {
44
45
  return [
46
+ lineWrapping && EditorView.lineWrapping,
45
47
  EditorState.allowMultipleSelections.of(true),
46
48
  EditorState.tabSize.of(2),
47
- EditorView.lineWrapping,
48
49
 
49
50
  // https://github.com/codemirror/basic-setup
50
51
  bracketMatching(),
@@ -55,7 +56,7 @@ export const markdownBundle = ({
55
56
  highlightActiveLine(),
56
57
  history(),
57
58
  indentOnInput(),
58
- _placeholder ? placeholder(_placeholder) : [],
59
+ _placeholder && placeholder(_placeholder),
59
60
 
60
61
  // Main extension.
61
62
  // https://github.com/codemirror/lang-markdown
@@ -82,7 +83,7 @@ export const markdownBundle = ({
82
83
  themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
83
84
 
84
85
  // Custom styles.
85
- syntaxHighlighting(markdownHighlightStyle(readonly)),
86
+ syntaxHighlighting(markdownHighlightStyle()),
86
87
 
87
88
  keymap.of([
88
89
  // https://codemirror.net/docs/ref/#commands.indentWithTab
@@ -96,5 +97,5 @@ export const markdownBundle = ({
96
97
  // https://codemirror.net/docs/ref/#commands.standardKeymap
97
98
  ...standardKeymap,
98
99
  ]),
99
- ];
100
+ ].filter(isNotFalsy);
100
101
  };
@@ -0,0 +1,150 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { RangeSetBuilder } from '@codemirror/state';
7
+ import {
8
+ ViewPlugin,
9
+ type DecorationSet,
10
+ EditorView,
11
+ type ViewUpdate,
12
+ Decoration,
13
+ type BlockInfo,
14
+ WidgetType,
15
+ } from '@codemirror/view';
16
+
17
+ import { mx } from '@dxos/react-ui-theme';
18
+
19
+ import { getToken } from '../../styles';
20
+
21
+ // TODO(burdon): Reconcile with theme.
22
+ const styles = EditorView.baseTheme({
23
+ '& .cm-code': {
24
+ fontFamily: getToken('fontFamily.mono', []).join(','),
25
+ },
26
+ '& .cm-codeblock': {
27
+ width: '100%',
28
+ paddingInline: '1rem !important',
29
+ position: 'relative',
30
+ '&::after': {
31
+ content: '""',
32
+ position: 'absolute',
33
+ inset: 0,
34
+ },
35
+ },
36
+ '&light .cm-codeblock': {
37
+ '&::after': {
38
+ background: getToken('extend.semanticColors.input.light'),
39
+ mixBlendMode: 'darken',
40
+ },
41
+ },
42
+ '&dark .cm-codeblock': {
43
+ '&::after': {
44
+ background: getToken('extend.semanticColors.input.dark'),
45
+ mixBlendMode: 'lighten',
46
+ },
47
+ },
48
+ '& .cm-codeblock-first': {
49
+ display: 'inline-block',
50
+ '&::after': {
51
+ borderTopLeftRadius: '.5rem',
52
+ borderTopRightRadius: '.5rem',
53
+ },
54
+ },
55
+ '& .cm-codeblock-last': {
56
+ display: 'inline-block',
57
+ '&::after': {
58
+ borderBottomLeftRadius: '.5rem',
59
+ borderBottomRightRadius: '.5rem',
60
+ },
61
+ },
62
+ });
63
+
64
+ // TODO(burdon): Start from EditorView.lineBlockAt(pos).
65
+ const getLineRange = (lines: BlockInfo[], from: number, to: number) => {
66
+ const start = lines.findIndex((line) => line.from >= from);
67
+ const end = lines.findIndex((line) => line.to >= to);
68
+ return [start, end];
69
+ };
70
+
71
+ // TODO(burdon): Add copy to clipboard widget.
72
+ class LineWidget extends WidgetType {
73
+ constructor(private readonly _className: string) {
74
+ super();
75
+ }
76
+
77
+ override toDOM() {
78
+ const el = document.createElement('span');
79
+ el.innerHTML = '&nbsp;';
80
+ el.className = mx('cm-code cm-codeblock', this._className);
81
+ return el;
82
+ }
83
+ }
84
+
85
+ const top = new LineWidget('cm-codeblock-first');
86
+ const bottom = new LineWidget('cm-codeblock-last');
87
+
88
+ // TODO(burdon): Selection isn't visible unless multiline (e.g., can't highlight word).
89
+
90
+ const buildDecorations = (view: EditorView): DecorationSet => {
91
+ const builder = new RangeSetBuilder<Decoration>();
92
+ const { state } = view;
93
+ const blocks = view.viewportLineBlocks;
94
+ const cursor = state.selection.main.head;
95
+
96
+ for (const { from, to } of view.visibleRanges) {
97
+ syntaxTree(state).iterate({
98
+ enter: (node) => {
99
+ if (node.name === 'FencedCode') {
100
+ // FencedCode > CodeMark > [CodeInfo] > CodeText > CodeMark
101
+ const edit = !view.state.readOnly && cursor >= node.from && cursor <= node.to;
102
+ const range = getLineRange(blocks, node.from, node.to);
103
+ for (let i = range[0]; i <= range[1]; i++) {
104
+ const block = blocks[i];
105
+ if (!edit && (i === range[0] || i === range[1])) {
106
+ builder.add(block.from, block.to, Decoration.replace({ widget: i === range[0] ? top : bottom }));
107
+ } else {
108
+ builder.add(
109
+ block.from,
110
+ block.from,
111
+ Decoration.line({
112
+ class: mx(
113
+ 'cm-code cm-codeblock',
114
+ i === range[0] && 'cm-codeblock-first',
115
+ i === range[1] && 'cm-codeblock-last',
116
+ ),
117
+ }),
118
+ );
119
+ }
120
+ }
121
+ }
122
+ },
123
+ from,
124
+ to,
125
+ });
126
+ }
127
+
128
+ return builder.finish();
129
+ };
130
+
131
+ export const code = () => {
132
+ return [
133
+ ViewPlugin.fromClass(
134
+ class {
135
+ decorations: DecorationSet;
136
+ constructor(view: EditorView) {
137
+ this.decorations = buildDecorations(view);
138
+ }
139
+
140
+ update(update: ViewUpdate) {
141
+ this.decorations = buildDecorations(update.view);
142
+ }
143
+ },
144
+ {
145
+ decorations: (value) => value.decorations,
146
+ },
147
+ ),
148
+ styles,
149
+ ];
150
+ };