@dxos/react-ui-editor 0.3.11-main.cd64fe0 → 0.3.11-main.cf49a01

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 (140) hide show
  1. package/dist/lib/browser/index.mjs +1016 -1024
  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 +12 -16
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +13 -5
  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/comments.stories.d.ts.map +1 -1
  10. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  11. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  12. package/dist/types/src/extensions/automerge/defs.d.ts +6 -8
  13. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  14. package/dist/types/src/extensions/automerge/semaphore.d.ts +6 -6
  15. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  16. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
  17. package/dist/types/src/extensions/awareness.d.ts +1 -1
  18. package/dist/types/src/extensions/awareness.d.ts.map +1 -1
  19. package/dist/types/src/extensions/basic.d.ts +4 -5
  20. package/dist/types/src/extensions/basic.d.ts.map +1 -1
  21. package/dist/types/src/extensions/comments.d.ts +18 -14
  22. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  23. package/dist/types/src/extensions/cursor.d.ts +3 -3
  24. package/dist/types/src/extensions/cursor.d.ts.map +1 -1
  25. package/dist/types/src/extensions/index.d.ts +0 -7
  26. package/dist/types/src/extensions/index.d.ts.map +1 -1
  27. package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
  28. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  29. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
  30. package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
  31. package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
  32. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  33. package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
  34. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  35. package/dist/types/src/extensions/markdown/index.d.ts +7 -0
  36. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  37. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  38. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
  39. package/dist/types/src/extensions/markdown/tasklist.d.ts +3 -0
  40. package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
  41. package/dist/types/src/extensions/mention.d.ts.map +1 -1
  42. package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
  43. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
  44. package/dist/types/src/hooks/defs.d.ts +25 -0
  45. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  46. package/dist/types/src/hooks/index.d.ts +2 -2
  47. package/dist/types/src/hooks/index.d.ts.map +1 -1
  48. package/dist/types/src/hooks/useTextEditor.d.ts +17 -0
  49. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  50. package/dist/types/src/hooks/useTextModel.d.ts +2 -27
  51. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  52. package/dist/types/src/styles/markdown.d.ts +2 -2
  53. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  54. package/dist/types/src/themes/default.d.ts.map +1 -1
  55. package/dist/types/src/util.d.ts +2 -0
  56. package/dist/types/src/util.d.ts.map +1 -1
  57. package/package.json +32 -35
  58. package/src/components/TextEditor/MarkdownEditor.stories.tsx +60 -65
  59. package/src/components/TextEditor/TextEditor.tsx +91 -66
  60. package/src/components/TextEditor/comments.stories.tsx +45 -43
  61. package/src/extensions/autocomplete.ts +6 -3
  62. package/src/extensions/automerge/automerge.ts +35 -39
  63. package/src/extensions/automerge/defs.ts +8 -33
  64. package/src/extensions/automerge/semaphore.ts +17 -12
  65. package/src/extensions/automerge/update-automerge.ts +2 -2
  66. package/src/extensions/awareness.ts +14 -14
  67. package/src/extensions/basic.ts +22 -17
  68. package/src/extensions/comments.ts +161 -141
  69. package/src/extensions/cursor.ts +7 -3
  70. package/src/extensions/index.ts +0 -7
  71. package/src/extensions/markdown/bundle.ts +20 -30
  72. package/src/extensions/markdown/code.ts +127 -0
  73. package/src/extensions/markdown/heading.ts +59 -0
  74. package/src/extensions/markdown/highlight.ts +9 -15
  75. package/src/extensions/markdown/hr.ts +71 -0
  76. package/src/extensions/markdown/index.ts +7 -0
  77. package/src/extensions/{link.ts → markdown/link.ts} +1 -2
  78. package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +48 -34
  79. package/src/extensions/mention.ts +9 -2
  80. package/src/hooks/awareness-provider.ts +24 -20
  81. package/src/hooks/defs.ts +42 -0
  82. package/src/hooks/index.ts +2 -2
  83. package/src/hooks/useTextEditor.ts +29 -0
  84. package/src/hooks/useTextModel.ts +47 -56
  85. package/src/styles/markdown.ts +9 -10
  86. package/src/themes/default.ts +0 -2
  87. package/src/util.ts +26 -3
  88. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  89. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  90. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
  91. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  92. package/dist/types/src/extensions/code.d.ts.map +0 -1
  93. package/dist/types/src/extensions/hr.d.ts.map +0 -1
  94. package/dist/types/src/extensions/image.d.ts.map +0 -1
  95. package/dist/types/src/extensions/link.d.ts.map +0 -1
  96. package/dist/types/src/extensions/table.d.ts.map +0 -1
  97. package/dist/types/src/extensions/tasklist.d.ts +0 -7
  98. package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
  99. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  100. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  101. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  102. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  103. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  104. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  105. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  106. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  107. package/dist/types/src/hooks/automerge.d.ts +0 -3
  108. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  109. package/dist/types/src/hooks/yjs.d.ts +0 -27
  110. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  111. package/dist/types/src/testing/index.d.ts +0 -3
  112. package/dist/types/src/testing/index.d.ts.map +0 -1
  113. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  114. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  115. package/dist/types/src/testing/proto/index.d.ts +0 -2
  116. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  117. package/dist/types/src/testing/replicator.d.ts +0 -45
  118. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  119. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  120. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  121. package/src/extensions/code.ts +0 -104
  122. package/src/extensions/hr.ts +0 -72
  123. package/src/extensions/yjs/cursor.ts +0 -22
  124. package/src/extensions/yjs/index.ts +0 -5
  125. package/src/extensions/yjs/yjs.test.ts +0 -35
  126. package/src/extensions/yjs/yjs.ts +0 -16
  127. package/src/hooks/automerge.ts +0 -52
  128. package/src/hooks/yjs.ts +0 -156
  129. package/src/testing/index.ts +0 -6
  130. package/src/testing/proto/gen/schema.ts +0 -49
  131. package/src/testing/proto/index.ts +0 -5
  132. package/src/testing/proto/schema.proto +0 -15
  133. package/src/testing/replicator.ts +0 -184
  134. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  135. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  136. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  137. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  138. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  139. /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
  140. /package/src/extensions/{table.ts → markdown/table.ts} +0 -0
@@ -5,14 +5,14 @@
5
5
  import { type Extension, StateEffect, StateField, type Text } from '@codemirror/state';
6
6
  import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
7
7
  import sortBy from 'lodash.sortby';
8
+ import { useEffect } from 'react';
8
9
 
9
10
  import { debounce } from '@dxos/async';
10
11
  import { invariant } from '@dxos/invariant';
11
12
  import { nonNullable } from '@dxos/util';
12
13
 
13
- import { semaphoreFacet } from './automerge/defs';
14
14
  import { Cursor } from './cursor';
15
- import { type CommentRange, type EditorModel, type Range } from '../hooks';
15
+ import { type Comment, type Range } from '../hooks';
16
16
  import { getToken } from '../styles';
17
17
  import { callbackWrapper } from '../util';
18
18
 
@@ -21,49 +21,54 @@ 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
29
  const marks = {
30
30
  highlight: Decoration.mark({ class: 'cm-comment' }),
31
- highlightActive: Decoration.mark({ class: 'cm-comment-active' }),
31
+ highlightActive: Decoration.mark({ class: 'cm-comment-current' }),
32
32
  };
33
33
 
34
- // TODO(burdon): Should this be part of state?
35
- type CommentSelected = {
36
- active?: string;
37
- closest?: string;
34
+ type CommentState = {
35
+ comment: Comment;
36
+ range: Range;
37
+ location?: Rect | null;
38
38
  };
39
39
 
40
- // TODO(burdon): Rename.
41
- type ExtendedCommentRange = Range &
42
- CommentRange & {
43
- // TODO(burdon): Not part of state; just required for callback.
44
- location?: Rect | null;
45
- };
40
+ type SelectionState = {
41
+ current?: string;
42
+ closest?: string;
43
+ };
46
44
 
47
- export type CommentsState = CommentSelected & {
48
- ranges: ExtendedCommentRange[];
45
+ export type CommentsState = {
46
+ comments: CommentState[];
47
+ selection: SelectionState;
49
48
  };
50
49
 
51
- export const setFocus = (view: EditorView, thread: string) => {
52
- const range = view.state.field(commentsStateField).ranges.find((range) => range.id === thread);
53
- if (!range) {
50
+ export const setFocus = (view: EditorView, id: string, center = true) => {
51
+ const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === id);
52
+ if (!comment?.comment.cursor) {
54
53
  return;
55
54
  }
56
55
 
57
- view.dispatch({
58
- effects: setSelection.of({ active: thread }),
59
- selection: { anchor: range.from },
60
- scrollIntoView: true, // TODO(burdon): Scroll to y-position (or center of screen?)
61
- });
56
+ const range = Cursor.getRangeFromCursor(view.state, comment.comment.cursor);
57
+ if (range) {
58
+ view.dispatch({
59
+ selection: { anchor: range.from },
60
+ effects: [
61
+ //
62
+ EditorView.scrollIntoView(range.from, center ? { y: 'center' } : undefined),
63
+ setSelection.of({ current: id }),
64
+ ],
65
+ });
66
+ }
62
67
  };
63
68
 
64
- export const setCommentRange = StateEffect.define<{ model: EditorModel; comments: CommentRange[] }>();
69
+ export const setComments = StateEffect.define<Comment[]>();
65
70
 
66
- export const setSelection = StateEffect.define<CommentSelected>();
71
+ export const setSelection = StateEffect.define<SelectionState>();
67
72
 
68
73
  const setCommentState = StateEffect.define<CommentsState>();
69
74
 
@@ -71,29 +76,30 @@ const setCommentState = StateEffect.define<CommentsState>();
71
76
  * State field (reducer) that tracks comment ranges.
72
77
  * The ranges are tracked as Automerge cursors from which the absolute indexed ranges can be computed.
73
78
  */
74
- const commentsStateField = StateField.define<CommentsState>({
75
- create: () => ({ ranges: [] }),
79
+ const commentsState = StateField.define<CommentsState>({
80
+ create: () => ({ comments: [], selection: {} }),
76
81
  update: (value, tr) => {
77
- const cursorConverter = tr.state.facet(Cursor.converter);
78
-
79
82
  for (const effect of tr.effects) {
80
83
  // Update selection.
81
84
  if (effect.is(setSelection)) {
82
- return { ...value, ...effect.value };
85
+ return { ...value, selection: effect.value };
83
86
  }
84
87
 
85
88
  // Update range from store.
86
- if (effect.is(setCommentRange)) {
87
- const { comments } = effect.value;
88
- const ranges: ExtendedCommentRange[] = comments
89
+ if (effect.is(setComments)) {
90
+ const comments: CommentState[] = effect.value
89
91
  .map((comment) => {
90
- const range = Cursor.getRangeFromCursor(cursorConverter, comment.cursor);
91
- // console.log('update', JSON.stringify({ range, cursor: comment.cursor }, undefined, 2));
92
- return range && { ...comment, ...range };
92
+ // Skip cut/deleted comments.
93
+ if (!comment.cursor) {
94
+ return undefined;
95
+ }
96
+
97
+ const range = Cursor.getRangeFromCursor(tr.state, comment.cursor);
98
+ return range && { comment, range };
93
99
  })
94
100
  .filter(nonNullable);
95
101
 
96
- return { ...value, ranges };
102
+ return { ...value, comments };
97
103
  }
98
104
 
99
105
  // Update entire state.
@@ -109,24 +115,27 @@ const commentsStateField = StateField.define<CommentsState>({
109
115
  /**
110
116
  * Decorate ranges.
111
117
  */
112
- const highlightDecorations = EditorView.decorations.compute([commentsStateField], (state) => {
113
- const { active, ranges } = state.field(commentsStateField);
114
- const decorations =
115
- sortBy(ranges ?? [], (range) => range.from)
116
- ?.flatMap((selection) => {
117
- // TODO(burdon): Invalid range (e.g., deleted).
118
- if (selection.from === selection.to) {
119
- return undefined;
120
- }
118
+ const highlightDecorations = EditorView.decorations.compute([commentsState], (state) => {
119
+ const {
120
+ selection: { current },
121
+ comments,
122
+ } = state.field(commentsState);
123
+
124
+ const decorations = sortBy(comments ?? [], (range) => range.range.from)
125
+ ?.flatMap((comment) => {
126
+ const range = comment.range;
127
+ if (!range || range.from === range.to) {
128
+ console.warn('Invalid range:', range);
129
+ return undefined;
130
+ }
121
131
 
122
- const range = { from: selection.from, to: selection.to };
123
- if (selection.id === active) {
124
- return marks.highlightActive.range(range.from, range.to);
125
- } else {
126
- return marks.highlight.range(range.from, range.to);
127
- }
128
- })
129
- .filter(nonNullable) ?? [];
132
+ if (comment.comment.id === current) {
133
+ return marks.highlightActive.range(range.from, range.to);
134
+ } else {
135
+ return marks.highlight.range(range.from, range.to);
136
+ }
137
+ })
138
+ .filter(nonNullable);
130
139
 
131
140
  return Decoration.set(decorations);
132
141
  });
@@ -160,16 +169,16 @@ export type CommentsOptions = {
160
169
 
161
170
  // TODO(burdon): Handle cut/restore via undo (need to integrate with history?)
162
171
  const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>) => {
163
- // Cut/deleted comments.
164
172
  // Tracks indexed selections within text.
173
+ // TODO(burdon): Move to main state field?
165
174
  let tracked: { text: Text; comments: { id: string; from: number; to: number }[] } | null = null;
166
175
 
167
176
  // Track cut or copy (enables cut-and-paste and copy-delete-paste to restore comment selection).
168
177
  const handleTrack = (event: Event, view: EditorView) => {
169
- const comments = view.state.field(commentsStateField);
178
+ const comments = view.state.field(commentsState);
170
179
  const { main } = view.state.selection;
171
- const selectedRanges = comments.ranges.filter(
172
- (range) => range.from >= main.from && range.to <= main.to && range.from < range.to,
180
+ const selectedRanges = comments.comments.filter(
181
+ ({ range }) => range.from >= main.from && range.to <= main.to && range.from < range.to,
173
182
  );
174
183
 
175
184
  if (!selectedRanges.length) {
@@ -177,15 +186,13 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
177
186
  } else {
178
187
  tracked = {
179
188
  text: view.state.doc.slice(main.from, main.to),
180
- comments: selectedRanges.map((range) => ({
181
- id: range.id,
189
+ comments: selectedRanges.map(({ comment, range }) => ({
190
+ id: comment.id,
182
191
  from: range.from - main.from,
183
192
  to: range.to - main.from,
184
193
  })),
185
194
  };
186
195
  }
187
-
188
- // console.log('track', JSON.stringify({ tracked }, undefined, 2));
189
196
  };
190
197
 
191
198
  return [
@@ -211,24 +218,14 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
211
218
  });
212
219
 
213
220
  if (found > -1) {
214
- const comments = tracked.comments;
215
-
216
- // Sync before recomputing cursor/range.
217
- // TODO(burdon): Generalize sync facet to decouple from automerge?
218
- view.state.facet(semaphoreFacet).reconcile(view);
219
-
220
- const active = state.field(commentsStateField).ranges;
221
- for (const moved of comments) {
222
- if (active.some((range) => range.id === moved.id && range.from === range.to)) {
223
- const range = {
224
- from: found + moved.from,
225
- to: found + moved.to,
226
- };
227
-
228
- const cursor = Cursor.getCursorFromRange(state.facet(Cursor.converter), range);
229
- // console.log('paste', JSON.stringify({ moved, range, cursor }, undefined, 2));
230
- onUpdate(moved.id, cursor);
231
- }
221
+ 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);
232
229
  }
233
230
  }
234
231
 
@@ -259,22 +256,30 @@ export const comments = (options: CommentsOptions = {}): Extension => {
259
256
  * Create comment thread action.
260
257
  */
261
258
  const createCommentThread: Command = (view) => {
262
- const cursorConverter = view.state.facet(Cursor.converter);
263
-
264
259
  invariant(options.onCreate);
265
260
  const { from, to } = view.state.selection.main;
266
261
  if (from === to) {
267
262
  return false;
268
263
  }
269
264
 
270
- const cursor = Cursor.getCursorFromRange(cursorConverter, { from, to });
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 });
271
276
  if (cursor) {
272
277
  // Create thread via callback.
273
- const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
278
+ const id = options.onCreate?.(cursor, view.coordsAtPos(from));
274
279
  if (id) {
275
280
  // Update range.
276
281
  view.dispatch({
277
- effects: setSelection.of({ active: id }),
282
+ effects: setSelection.of({ current: id }),
278
283
  selection: { anchor: from },
279
284
  });
280
285
 
@@ -286,7 +291,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
286
291
  };
287
292
 
288
293
  return [
289
- commentsStateField,
294
+ commentsState,
290
295
  highlightDecorations,
291
296
  styles,
292
297
 
@@ -334,75 +339,90 @@ export const comments = (options: CommentsOptions = {}): Extension => {
334
339
  : [],
335
340
 
336
341
  //
337
- // Monitor cursor movement and text updates.
338
- // TODO(burdon): Is there a better (finer grained) way to do this?
342
+ // Track deleted ranges and update ranges for decorations.
339
343
  //
340
344
  EditorView.updateListener.of(({ view, state, changes }) => {
341
- const cursorConverter = view.state.facet(Cursor.converter);
342
-
343
- //
344
- // Test if need to recompute indexed range if document changes before the end of the range.
345
- //
346
- {
347
- let mod = false;
348
- const { active, ranges } = state.field(commentsStateField);
349
- changes.iterChanges((from, to, from2, to2) => {
350
- // TODO(burdon): Skip deleted (tracked) ranges.
351
- ranges.forEach((range) => {
352
- if (from2 === to2) {
353
- const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
354
- if (!newRange || newRange.to - newRange.from === 0) {
355
- options.onDelete?.(range.id);
356
- }
345
+ let mod = false;
346
+ const { comments, ...value } = state.field(commentsState);
347
+ changes.iterChanges((from, to, from2, to2) => {
348
+ comments.forEach(({ comment, range }) => {
349
+ // Test if range deleted.
350
+ if (from2 === to2) {
351
+ const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor!);
352
+ if (!newRange || newRange.to - newRange.from === 0) {
353
+ options.onDelete?.(comment.id);
357
354
  }
355
+ }
358
356
 
359
- if (from <= range.to) {
360
- const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
361
- Object.assign(range, newRange);
362
- mod = true;
363
- }
364
- });
357
+ // Update range.
358
+ if (from <= range.to) {
359
+ const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor!);
360
+ Object.assign(range, newRange);
361
+ mod = true;
362
+ }
365
363
  });
364
+ });
366
365
 
367
- if (mod) {
368
- view.dispatch({ effects: setCommentState.of({ active, ranges }) });
369
- }
366
+ if (mod) {
367
+ view.dispatch({ effects: setCommentState.of({ comments, ...value }) });
370
368
  }
369
+ }),
371
370
 
372
- //
373
- // Track the current selection.
374
- //
375
- {
376
- const { head } = state.selection.main;
377
-
378
- let min = Infinity;
379
- const { active, closest, ranges } = state.field(commentsStateField);
380
- const selected: CommentSelected = { active: undefined, closest: undefined };
381
-
382
- // TODO(burdon): Skip deleted (tracked) ranges.
383
- ranges.forEach((comment) => {
384
- const d = Math.min(Math.abs(head - comment.from), Math.abs(head - comment.to));
385
- if (head >= comment.from && head <= comment.to) {
386
- selected.active = comment.id;
387
- }
371
+ //
372
+ // Track selection/proximity.
373
+ //
374
+ EditorView.updateListener.of(({ view, state }) => {
375
+ let min = Infinity;
376
+ const {
377
+ selection: { current, closest },
378
+ comments,
379
+ } = state.field(commentsState);
380
+
381
+ const { head } = state.selection.main;
382
+ const selection: SelectionState = {};
383
+ comments.forEach(({ comment, range }) => {
384
+ if (head >= range.from && head <= range.to) {
385
+ selection.current = comment.id;
386
+ selection.closest = undefined;
387
+ }
388
+
389
+ if (!selection.current) {
390
+ const d = Math.min(Math.abs(head - range.from), Math.abs(head - range.to));
388
391
  if (d < min) {
389
- selected.closest = comment.id;
392
+ selection.closest = comment.id;
390
393
  min = d;
391
394
  }
392
- });
393
-
394
- if (selected.active !== active || selected.closest !== closest) {
395
- view.dispatch({ effects: setSelection.of(selected) });
396
-
397
- // Update callback.
398
- handleSelect({
399
- ...selected,
400
- ranges: ranges.map((range) => ({ ...range, location: view.coordsAtPos(range.from) })),
401
- });
402
395
  }
396
+ });
397
+
398
+ if (selection.current !== current || selection.closest !== closest) {
399
+ view.dispatch({ effects: setSelection.of(selection) });
400
+
401
+ // Update callback.
402
+ handleSelect({
403
+ selection,
404
+ comments: comments.map(({ comment, range }) => ({
405
+ comment,
406
+ range,
407
+ location: view.coordsAtPos(range.from),
408
+ })),
409
+ });
403
410
  }
404
411
  }),
405
412
 
406
413
  options.onUpdate ? trackPastedComments(options.onUpdate) : [],
407
414
  ];
408
415
  };
416
+
417
+ /**
418
+ * Update comments state field.
419
+ */
420
+ export const useComments = (view: EditorView | null, comments: Comment[] = []) => {
421
+ useEffect(() => {
422
+ if (view) {
423
+ view.dispatch({
424
+ effects: setComments.of(comments),
425
+ });
426
+ }
427
+ }, [view, comments]);
428
+ };
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Facet } from '@codemirror/state';
5
+ import { type EditorState, Facet } from '@codemirror/state';
6
6
 
7
7
  import type { Range } from '../hooks';
8
8
 
@@ -31,13 +31,17 @@ export class Cursor {
31
31
  combine: (providers) => providers[0] ?? defaultCursorConverter,
32
32
  });
33
33
 
34
- static readonly getCursorFromRange = (cursorConverter: CursorConverter, range: Range) => {
34
+ static readonly getCursorFromRange = (state: EditorState, range: Range) => {
35
+ const cursorConverter = state.facet(Cursor.converter);
36
+
35
37
  const from = cursorConverter.toCursor(range.from);
36
38
  const to = cursorConverter.toCursor(range.to, -1);
37
39
  return [from, to].join(':');
38
40
  };
39
41
 
40
- static readonly getRangeFromCursor = (cursorConverter: CursorConverter, cursor: string) => {
42
+ static readonly getRangeFromCursor = (state: EditorState, cursor: string) => {
43
+ const cursorConverter = state.facet(Cursor.converter);
44
+
41
45
  const parts = cursor.split(':');
42
46
  const from = cursorConverter.fromCursor(parts[0]);
43
47
  const to = cursorConverter.fromCursor(parts[1]);
@@ -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';
@@ -23,34 +23,26 @@ import {
23
23
  import type { ThemeMode } from '@dxos/react-ui';
24
24
 
25
25
  import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
26
+ import { type TextEditorProps } from '../../components';
26
27
 
27
28
  export type MarkdownBundleOptions = {
28
- readonly?: boolean;
29
29
  themeMode?: ThemeMode;
30
- placeholder?: string;
31
- };
30
+ } & Pick<TextEditorProps, 'placeholder'>;
32
31
 
33
32
  /**
34
33
  * Markdown bundle.
35
34
  * Refs:
36
- * https://github.com/codemirror/basic-setup
37
- * https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts#L50
38
35
  * https://codemirror.net/docs/community
39
36
  * https://codemirror.net/docs/ref/#codemirror.basicSetup
40
37
  */
41
38
  // TODO(burdon): Add Composer here: https://codemirror.net/docs/community
42
- export const markdownBundle = ({
43
- readonly,
44
- themeMode,
45
- placeholder: _placeholder,
46
- }: MarkdownBundleOptions): Extension[] => {
39
+ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: MarkdownBundleOptions): Extension[] => {
47
40
  return [
48
41
  EditorState.allowMultipleSelections.of(true),
49
42
  EditorState.tabSize.of(2),
50
43
  EditorView.lineWrapping,
51
44
 
52
- customKeymap(),
53
-
45
+ // https://github.com/codemirror/basic-setup
54
46
  bracketMatching(),
55
47
  closeBrackets(),
56
48
  crosshairCursor(),
@@ -59,7 +51,7 @@ export const markdownBundle = ({
59
51
  highlightActiveLine(),
60
52
  history(),
61
53
  indentOnInput(),
62
- _placeholder && placeholder(_placeholder),
54
+ _placeholder ? placeholder(_placeholder) : [],
63
55
 
64
56
  // Main extension.
65
57
  // https://github.com/codemirror/lang-markdown
@@ -86,21 +78,19 @@ export const markdownBundle = ({
86
78
  themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
87
79
 
88
80
  // Custom styles.
89
- syntaxHighlighting(markdownHighlightStyle(readonly)),
90
- ].filter(Boolean) as Extension[];
91
- };
81
+ syntaxHighlighting(markdownHighlightStyle()),
92
82
 
93
- // https://codemirror.net/docs/ref/#view.keymap
94
- const customKeymap = () =>
95
- keymap.of([
96
- // https://codemirror.net/docs/ref/#commands.indentWithTab
97
- indentWithTab,
98
- // https://codemirror.net/docs/ref/#commands.standardKeymap
99
- ...standardKeymap,
100
- // https://codemirror.net/docs/ref/#commands.historyKeymap
101
- ...historyKeymap,
102
- // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
103
- ...closeBracketsKeymap,
104
- // https://codemirror.net/docs/ref/#search.searchKeymap
105
- ...searchKeymap,
106
- ]);
83
+ keymap.of([
84
+ // https://codemirror.net/docs/ref/#commands.indentWithTab
85
+ indentWithTab,
86
+ // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
87
+ ...closeBracketsKeymap,
88
+ // https://codemirror.net/docs/ref/#commands.historyKeymap
89
+ ...historyKeymap,
90
+ // https://codemirror.net/docs/ref/#search.searchKeymap
91
+ ...searchKeymap,
92
+ // https://codemirror.net/docs/ref/#commands.standardKeymap
93
+ ...standardKeymap,
94
+ ]),
95
+ ];
96
+ };