@dxos/react-ui-editor 0.3.11-main.c244d7e → 0.3.11-main.c865ffb

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 (168) hide show
  1. package/dist/lib/browser/index.mjs +1385 -929
  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 +7 -11
  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/extensions/mention.d.ts.map +1 -1
  54. package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
  55. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
  56. package/dist/types/src/hooks/defs.d.ts +1 -4
  57. package/dist/types/src/hooks/defs.d.ts.map +1 -1
  58. package/dist/types/src/hooks/index.d.ts +3 -2
  59. package/dist/types/src/hooks/index.d.ts.map +1 -1
  60. package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
  61. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
  62. package/dist/types/src/hooks/useEditorView.d.ts +17 -0
  63. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
  64. package/dist/types/src/hooks/useTextEditor.d.ts +39 -0
  65. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  66. package/dist/types/src/hooks/useTextModel.d.ts +12 -0
  67. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  68. package/dist/types/src/index.d.ts +1 -1
  69. package/dist/types/src/index.d.ts.map +1 -1
  70. package/dist/types/src/styles/markdown.d.ts +2 -2
  71. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  72. package/dist/types/src/themes/default.d.ts.map +1 -1
  73. package/package.json +46 -36
  74. package/src/components/TextEditor/MarkdownEditor.stories.tsx +41 -46
  75. package/src/components/TextEditor/TextEditor.tsx +46 -27
  76. package/src/components/TextEditor/automerge.stories.tsx +1 -2
  77. package/src/components/TextEditor/hooks.stories.tsx +106 -0
  78. package/src/components/Toolbar/Toolbar.stories.tsx +90 -0
  79. package/src/components/Toolbar/Toolbar.tsx +166 -0
  80. package/src/components/Toolbar/index.ts +5 -0
  81. package/src/components/index.ts +1 -0
  82. package/src/extensions/automerge/automerge.spec.tsx +76 -0
  83. package/src/extensions/automerge/automerge.test.ts +13 -0
  84. package/src/extensions/automerge/automerge.ts +44 -11
  85. package/src/extensions/automerge/cursor.ts +3 -3
  86. package/src/extensions/automerge/defs.ts +9 -9
  87. package/src/extensions/automerge/semaphore.ts +18 -24
  88. package/src/extensions/automerge/update-automerge.ts +12 -6
  89. package/src/extensions/automerge/update-codemirror.ts +5 -5
  90. package/src/extensions/awareness.ts +21 -35
  91. package/src/extensions/basic.ts +17 -15
  92. package/src/extensions/blast.ts +4 -1
  93. package/src/extensions/comments.ts +126 -78
  94. package/src/extensions/index.ts +0 -7
  95. package/src/extensions/markdown/bundle.ts +9 -8
  96. package/src/extensions/markdown/code.ts +150 -0
  97. package/src/extensions/markdown/formatting.ts +188 -0
  98. package/src/extensions/markdown/heading.ts +59 -0
  99. package/src/extensions/markdown/highlight.ts +9 -15
  100. package/src/extensions/{hr.ts → markdown/hr.ts} +3 -4
  101. package/src/extensions/markdown/index.ts +8 -0
  102. package/src/extensions/{link.ts → markdown/link.ts} +13 -4
  103. package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +2 -3
  104. package/src/extensions/mention.ts +6 -4
  105. package/src/hooks/awareness-provider.ts +24 -20
  106. package/src/hooks/defs.ts +1 -6
  107. package/src/hooks/index.ts +3 -2
  108. package/src/hooks/useActionHandler.ts +62 -0
  109. package/src/hooks/useEditorView.ts +29 -0
  110. package/src/hooks/useTextEditor.ts +153 -0
  111. package/src/hooks/useTextModel.ts +65 -12
  112. package/src/index.ts +1 -1
  113. package/src/styles/markdown.ts +9 -10
  114. package/src/themes/default.ts +9 -6
  115. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  116. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  117. package/dist/types/src/components/TextEditor/comments.stories.d.ts +0 -26
  118. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +0 -1
  119. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  120. package/dist/types/src/extensions/code.d.ts.map +0 -1
  121. package/dist/types/src/extensions/hr.d.ts.map +0 -1
  122. package/dist/types/src/extensions/image.d.ts.map +0 -1
  123. package/dist/types/src/extensions/link.d.ts.map +0 -1
  124. package/dist/types/src/extensions/table.d.ts.map +0 -1
  125. package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
  126. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  127. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  128. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  129. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  130. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  131. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  132. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  133. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  134. package/dist/types/src/hooks/automerge.d.ts +0 -4
  135. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  136. package/dist/types/src/hooks/yjs.d.ts +0 -28
  137. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  138. package/dist/types/src/testing/index.d.ts +0 -3
  139. package/dist/types/src/testing/index.d.ts.map +0 -1
  140. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  141. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  142. package/dist/types/src/testing/proto/index.d.ts +0 -2
  143. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  144. package/dist/types/src/testing/replicator.d.ts +0 -45
  145. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  146. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  147. package/src/components/TextEditor/comments.stories.tsx +0 -357
  148. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  149. package/src/extensions/code.ts +0 -104
  150. package/src/extensions/yjs/cursor.ts +0 -22
  151. package/src/extensions/yjs/index.ts +0 -5
  152. package/src/extensions/yjs/yjs.test.ts +0 -35
  153. package/src/extensions/yjs/yjs.ts +0 -16
  154. package/src/hooks/automerge.ts +0 -53
  155. package/src/hooks/yjs.ts +0 -157
  156. package/src/testing/index.ts +0 -6
  157. package/src/testing/proto/gen/schema.ts +0 -49
  158. package/src/testing/proto/index.ts +0 -5
  159. package/src/testing/proto/schema.proto +0 -15
  160. package/src/testing/replicator.ts +0 -184
  161. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  162. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  163. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  164. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  165. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  166. /package/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.ts} +0 -0
  167. /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
  168. /package/src/extensions/{table.ts → markdown/table.ts} +0 -0
@@ -12,16 +12,11 @@ import {
12
12
  type ViewUpdate,
13
13
  WidgetType,
14
14
  } from '@codemirror/view';
15
- import { useEffect } from 'react';
16
15
 
17
16
  import { Event } from '@dxos/async';
18
17
  import { Context } from '@dxos/context';
19
- import { generateName } from '@dxos/display-name';
20
- import { useThemeContext } from '@dxos/react-ui';
21
- import { getColorForValue } from '@dxos/react-ui-theme';
22
18
 
23
19
  import { Cursor, type CursorConverter } from './cursor';
24
- import { type EditorModel } from '../hooks';
25
20
 
26
21
  export interface AwarenessProvider {
27
22
  remoteStateChange: Event<void>;
@@ -29,8 +24,8 @@ export interface AwarenessProvider {
29
24
  open(): void;
30
25
  close(): void;
31
26
 
32
- update(position: AwarenessPosition | undefined): void;
33
27
  getRemoteStates(): AwarenessState[];
28
+ update(position: AwarenessPosition | undefined): void;
34
29
  }
35
30
 
36
31
  const dummyProvider: AwarenessProvider = {
@@ -39,11 +34,11 @@ const dummyProvider: AwarenessProvider = {
39
34
  open: () => {},
40
35
  close: () => {},
41
36
 
42
- update: () => {},
43
37
  getRemoteStates: () => [],
38
+ update: () => {},
44
39
  };
45
40
 
46
- export const AwarenessProvider = Facet.define<AwarenessProvider, AwarenessProvider>({
41
+ export const awarenessProvider = Facet.define<AwarenessProvider, AwarenessProvider>({
47
42
  combine: (providers) => providers[0] ?? dummyProvider,
48
43
  });
49
44
 
@@ -73,7 +68,7 @@ export type AwarenessState = {
73
68
  */
74
69
  export const awareness = (provider = dummyProvider): Extension => {
75
70
  return [
76
- AwarenessProvider.of(provider),
71
+ awarenessProvider.of(provider),
77
72
  ViewPlugin.fromClass(RemoteSelectionsDecorator, {
78
73
  decorations: (value) => value.decorations,
79
74
  }),
@@ -81,19 +76,6 @@ export const awareness = (provider = dummyProvider): Extension => {
81
76
  ];
82
77
  };
83
78
 
84
- export const useAwareness = ({ awareness, peer }: EditorModel) => {
85
- const { themeMode } = useThemeContext();
86
- useEffect(() => {
87
- if (awareness && peer) {
88
- awareness.setLocalStateField('user', {
89
- name: peer.name ?? generateName(peer.id),
90
- color: getColorForValue({ value: peer.id, type: 'color' }),
91
- colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
92
- });
93
- }
94
- }, [awareness, peer, themeMode]);
95
- };
96
-
97
79
  /**
98
80
  * Generates selection decorations from remote peers.
99
81
  */
@@ -109,7 +91,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
109
91
 
110
92
  constructor(view: EditorView) {
111
93
  this._cursorConverter = view.state.facet(Cursor.converter);
112
- this._provider = view.state.facet(AwarenessProvider);
94
+ this._provider = view.state.facet(awarenessProvider);
113
95
  this._provider.open();
114
96
  this._provider.remoteStateChange.on(this._ctx, () => {
115
97
  view.dispatch({ annotations: [RemoteSelectionChangedAnnotation.of([])] });
@@ -225,7 +207,10 @@ export class RemoteSelectionsDecorator implements PluginValue {
225
207
  }
226
208
 
227
209
  class RemoteCaretWidget extends WidgetType {
228
- constructor(private readonly _name: string, private readonly _color: string) {
210
+ constructor(
211
+ private readonly _name: string,
212
+ private readonly _color: string,
213
+ ) {
229
214
  super();
230
215
  }
231
216
 
@@ -268,7 +253,6 @@ class RemoteCaretWidget extends WidgetType {
268
253
  }
269
254
  }
270
255
 
271
- // TODO(burdon): Mixed case?
272
256
  const styles = EditorView.baseTheme({
273
257
  '.cm-collab-selection': {},
274
258
  '.cm-collab-selectionLine': {
@@ -283,38 +267,40 @@ const styles = EditorView.baseTheme({
283
267
  marginRight: '-1px',
284
268
  boxSizing: 'border-box',
285
269
  display: 'inline',
270
+ cursor: 'pointer',
286
271
  },
287
272
  '.cm-collab-selectionCaretDot': {
288
273
  borderRadius: '50%',
289
274
  position: 'absolute',
290
- width: '.4em',
291
- height: '.4em',
292
- top: '-.2em',
293
- left: '-.2em',
275
+ width: '.5em',
276
+ height: '.5em',
277
+ top: '-.25em',
278
+ left: '-.25em',
294
279
  backgroundColor: 'inherit',
295
280
  transition: 'transform .3s ease-in-out',
296
281
  boxSizing: 'border-box',
297
282
  },
298
283
  '.cm-collab-selectionCaret:hover > .cm-collab-selectionCaretDot': {
299
- transformOrigin: 'bottom center',
300
284
  transform: 'scale(0)',
285
+ transformOrigin: 'center',
301
286
  },
302
287
  '.cm-collab-selectionInfo': {
303
288
  position: 'absolute',
304
- top: '-1.05em',
305
- left: '-1px',
289
+ transform: 'translate(-50%, 0)',
290
+ top: '-20px',
291
+ left: 0,
306
292
  fontSize: '.75em',
307
- fontFamily: 'serif',
293
+ fontFamily: 'sans-serif',
308
294
  fontStyle: 'normal',
309
295
  fontWeight: 'normal',
310
296
  lineHeight: 'normal',
311
297
  userSelect: 'none',
312
298
  color: 'white',
313
- paddingLeft: '2px',
314
- paddingRight: '2px',
299
+ padding: '2px',
315
300
  zIndex: 101,
316
301
  transition: 'opacity .3s ease-in-out',
317
302
  backgroundColor: 'inherit',
303
+ borderRadius: '2px',
318
304
  // These should be separate.
319
305
  opacity: 0,
320
306
  transitionDelay: '0s',
@@ -10,28 +10,30 @@ import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
10
10
  import { drawSelection, EditorView, highlightActiveLine, placeholder } from '@codemirror/view';
11
11
 
12
12
  import { type ThemeMode } from '@dxos/react-ui';
13
+ import { isNotFalsy } from '@dxos/util';
13
14
 
14
15
  import { type TextEditorProps } from '../components';
15
16
 
16
17
  export type BasicBundleOptions = {
17
18
  themeMode?: ThemeMode;
18
- } & Pick<TextEditorProps, 'placeholder' | 'multiline'>;
19
+ } & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
19
20
 
20
- export const basicBundle = ({
21
+ export const createBasicBundle = ({
21
22
  themeMode,
22
23
  placeholder: _placeholder,
23
- multiline = true,
24
- }: BasicBundleOptions): Extension[] => [
25
- multiline ? EditorView.lineWrapping : [],
24
+ lineWrapping = true,
25
+ }: BasicBundleOptions = {}): Extension[] =>
26
+ [
27
+ lineWrapping && EditorView.lineWrapping,
26
28
 
27
- // https://codemirror.net/docs/ref/#codemirror.minimalSetup
28
- bracketMatching(),
29
- closeBrackets(),
30
- drawSelection(),
31
- highlightActiveLine(),
32
- history(),
33
- _placeholder ? placeholder(_placeholder) : [],
29
+ // https://codemirror.net/docs/ref/#codemirror.minimalSetup
30
+ bracketMatching(),
31
+ closeBrackets(),
32
+ drawSelection(),
33
+ highlightActiveLine(),
34
+ history(),
35
+ _placeholder && placeholder(_placeholder),
34
36
 
35
- // https://github.com/codemirror/theme-one-dark
36
- themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
37
- ];
37
+ // https://github.com/codemirror/theme-one-dark
38
+ themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
39
+ ].filter(isNotFalsy);
@@ -125,7 +125,10 @@ class Blaster {
125
125
 
126
126
  _lastPoint = { x: 0, y: 0 };
127
127
 
128
- constructor(private readonly _node: HTMLElement, private readonly _options: BlastOptions) {
128
+ constructor(
129
+ private readonly _node: HTMLElement,
130
+ private readonly _options: BlastOptions,
131
+ ) {
129
132
  this._effect = this._options.effect === 1 ? new Effect1(_options) : new Effect2(_options);
130
133
  }
131
134
 
@@ -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';
@@ -21,15 +21,13 @@ const styles = EditorView.baseTheme({
21
21
  '& .cm-comment': {
22
22
  backgroundColor: getToken('extend.colors.yellow.50'),
23
23
  },
24
- '& .cm-comment-active': {
24
+ '& .cm-comment-current': {
25
25
  backgroundColor: getToken('extend.colors.yellow.100'),
26
26
  },
27
27
  });
28
28
 
29
- const marks = {
30
- highlight: Decoration.mark({ class: 'cm-comment' }),
31
- highlightActive: Decoration.mark({ class: 'cm-comment-active' }),
32
- };
29
+ const commentMark = Decoration.mark({ class: 'cm-comment' });
30
+ const commentCurrentMark = Decoration.mark({ class: 'cm-comment-current' });
33
31
 
34
32
  type CommentState = {
35
33
  comment: Comment;
@@ -38,7 +36,7 @@ type CommentState = {
38
36
  };
39
37
 
40
38
  type SelectionState = {
41
- active?: string;
39
+ current?: string;
42
40
  closest?: string;
43
41
  };
44
42
 
@@ -47,7 +45,7 @@ export type CommentsState = {
47
45
  selection: SelectionState;
48
46
  };
49
47
 
50
- export const setFocus = (view: EditorView, id: string, center = true) => {
48
+ export const focusComment = (view: EditorView, id: string, center = true) => {
51
49
  const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === id);
52
50
  if (!comment?.comment.cursor) {
53
51
  return;
@@ -60,7 +58,7 @@ export const setFocus = (view: EditorView, id: string, center = true) => {
60
58
  effects: [
61
59
  //
62
60
  EditorView.scrollIntoView(range.from, center ? { y: 'center' } : undefined),
63
- setSelection.of({ active: id }),
61
+ setSelection.of({ current: id }),
64
62
  ],
65
63
  });
66
64
  }
@@ -115,9 +113,9 @@ const commentsState = StateField.define<CommentsState>({
115
113
  /**
116
114
  * Decorate ranges.
117
115
  */
118
- const highlightDecorations = EditorView.decorations.compute([commentsState], (state) => {
116
+ const commentsDecorations = EditorView.decorations.compute([commentsState], (state) => {
119
117
  const {
120
- selection: { active },
118
+ selection: { current },
121
119
  comments,
122
120
  } = state.field(commentsState);
123
121
 
@@ -129,10 +127,10 @@ const highlightDecorations = EditorView.decorations.compute([commentsState], (st
129
127
  return undefined;
130
128
  }
131
129
 
132
- if (comment.comment.id === active) {
133
- return marks.highlightActive.range(range.from, range.to);
130
+ if (comment.comment.id === current) {
131
+ return commentCurrentMark.range(range.from, range.to);
134
132
  } else {
135
- return marks.highlight.range(range.from, range.to);
133
+ return commentMark.range(range.from, range.to);
136
134
  }
137
135
  })
138
136
  .filter(nonNullable);
@@ -167,11 +165,11 @@ export type CommentsOptions = {
167
165
  onHover?: (el: Element, shortcut: string) => void;
168
166
  };
169
167
 
170
- // TODO(burdon): Handle cut/restore via undo (need to integrate with history?)
168
+ type TrackedComment = { id: string; from: number; to: number };
169
+
171
170
  const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>) => {
172
171
  // 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;
172
+ let tracked: { text: Text; comments: TrackedComment[] } | null = null;
175
173
 
176
174
  // Track cut or copy (enables cut-and-paste and copy-delete-paste to restore comment selection).
177
175
  const handleTrack = (event: Event, view: EditorView) => {
@@ -201,16 +199,48 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
201
199
  copy: handleTrack,
202
200
  }),
203
201
 
204
- // Handle paste.
205
- EditorView.updateListener.of(({ view, state, transactions }) => {
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
+
206
236
  if (tracked) {
207
- const paste = transactions.find((tr) => tr.isUserEvent('input.paste'));
237
+ const paste = update.transactions.find((tr) => tr.isUserEvent('input.paste'));
208
238
  if (paste) {
209
239
  let found = -1;
210
240
  paste.changes.iterChanges((fromA, toA, fromB, toB, text) => {
211
241
  if (text.eq(tracked!.text)) {
212
- for (let i = transactions.indexOf(paste!) + 1; i < transactions.length; i++) {
213
- fromB = transactions[i].changes.mapPos(fromB);
242
+ for (let i = update.transactions.indexOf(paste!) + 1; i < update.transactions.length; i++) {
243
+ fromB = update.transactions[i].changes.mapPos(fromB);
214
244
  }
215
245
 
216
246
  found = fromB;
@@ -219,23 +249,78 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
219
249
 
220
250
  if (found > -1) {
221
251
  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);
252
+ restore.push({ id: moved.id, from: found + moved.from, to: found + moved.to });
229
253
  }
230
254
  }
231
255
 
232
256
  tracked = null;
233
257
  }
234
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
+ }
235
268
  }),
236
269
  ];
237
270
  };
238
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
+
239
324
  /**
240
325
  * Comment threads.
241
326
  * 1). Updates the EditorModel to store relative selections for a set of comments threads.
@@ -252,47 +337,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
252
337
 
253
338
  const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
254
339
 
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
340
  return [
341
+ optionsFacet.of(options),
294
342
  commentsState,
295
- highlightDecorations,
343
+ commentsDecorations,
296
344
  styles,
297
345
 
298
346
  //
@@ -302,7 +350,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
302
350
  ? keymap.of([
303
351
  {
304
352
  key: shortcut,
305
- run: callbackWrapper(createCommentThread),
353
+ run: callbackWrapper(createComment),
306
354
  },
307
355
  ])
308
356
  : [],
@@ -371,10 +419,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
371
419
  //
372
420
  // Track selection/proximity.
373
421
  //
374
- EditorView.updateListener.of(({ view, state, changes }) => {
422
+ EditorView.updateListener.of(({ view, state }) => {
375
423
  let min = Infinity;
376
424
  const {
377
- selection: { active, closest },
425
+ selection: { current, closest },
378
426
  comments,
379
427
  } = state.field(commentsState);
380
428
 
@@ -382,11 +430,11 @@ export const comments = (options: CommentsOptions = {}): Extension => {
382
430
  const selection: SelectionState = {};
383
431
  comments.forEach(({ comment, range }) => {
384
432
  if (head >= range.from && head <= range.to) {
385
- selection.active = comment.id;
433
+ selection.current = comment.id;
386
434
  selection.closest = undefined;
387
435
  }
388
436
 
389
- if (!selection.active) {
437
+ if (!selection.current) {
390
438
  const d = Math.min(Math.abs(head - range.from), Math.abs(head - range.to));
391
439
  if (d < min) {
392
440
  selection.closest = comment.id;
@@ -395,7 +443,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
395
443
  }
396
444
  });
397
445
 
398
- if (selection.active !== active || selection.closest !== closest) {
446
+ if (selection.current !== current || selection.closest !== closest) {
399
447
  view.dispatch({ effects: setSelection.of(selection) });
400
448
 
401
449
  // Update callback.
@@ -415,11 +463,11 @@ export const comments = (options: CommentsOptions = {}): Extension => {
415
463
  };
416
464
 
417
465
  /**
418
- * Update state field.
466
+ * Update comments state field.
419
467
  */
420
- export const useComments = (view?: EditorView | null, comments?: Comment[]) => {
468
+ export const useComments = (view: EditorView | null, comments: Comment[] = []) => {
421
469
  useEffect(() => {
422
- if (view && comments) {
470
+ if (view) {
423
471
  view.dispatch({
424
472
  effects: setComments.of(comments),
425
473
  });
@@ -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
  };