@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
@@ -17,8 +17,8 @@ import { fixedInsetFlexLayout, mx } from '@dxos/react-ui-theme';
17
17
  import { withTheme } from '@dxos/storybook-utils';
18
18
 
19
19
  import { MarkdownEditor, TextEditor } from './TextEditor';
20
- import { comments, type CommentsOptions, Cursor } from '../../extensions';
21
- import { type CommentRange, type Range, useTextModel } from '../../hooks';
20
+ import { comments, type CommentsOptions, setFocus, useComments } from '../../extensions';
21
+ import { type Comment, type Range, useTextModel } from '../../hooks';
22
22
 
23
23
  faker.seed(101);
24
24
 
@@ -28,31 +28,34 @@ faker.seed(101);
28
28
 
29
29
  const Editor: FC<{
30
30
  item: { text: TextObject };
31
- commentSelected?: string;
32
- commentRanges: CommentRange[];
31
+ comments: Comment[];
32
+ selected?: string;
33
33
  onCreateComment: CommentsOptions['onCreate'];
34
34
  onDeleteComment: CommentsOptions['onDelete'];
35
35
  onUpdateComment: CommentsOptions['onUpdate'];
36
36
  onSelectComment: CommentsOptions['onSelect'];
37
- }> = ({ item, commentSelected, commentRanges, onCreateComment, onDeleteComment, onUpdateComment, onSelectComment }) => {
37
+ }> = ({
38
+ item,
39
+ selected: selectedValue,
40
+ comments: commentRanges,
41
+ onCreateComment,
42
+ onDeleteComment,
43
+ onUpdateComment,
44
+ onSelectComment,
45
+ }) => {
38
46
  const model = useTextModel({ text: item.text });
39
- const editorRef = useRef<EditorView>(null);
47
+ const view = useRef<EditorView>(null);
40
48
  const [selected, setSelected] = useState<string>();
41
49
  useEffect(() => {
42
- if (!editorRef.current?.hasFocus && commentSelected !== selected) {
43
- const thread = commentRanges.find((range) => range.id === commentSelected);
44
- if (thread) {
45
- const { cursor } = thread;
46
- const range = Cursor.getRangeFromCursor(editorRef.current!.state.facet(Cursor.converter), cursor);
47
- if (range) {
48
- // TODO(burdon): Scroll selection to center of screen?
49
- editorRef.current?.dispatch({ selection: { anchor: range.from }, scrollIntoView: true });
50
- }
50
+ if (!view.current?.hasFocus && selectedValue !== selected) {
51
+ setSelected(selectedValue);
52
+ if (selectedValue) {
53
+ setFocus(view.current!, selectedValue);
51
54
  }
52
-
53
- setSelected(commentSelected);
54
55
  }
55
- }, [selected, commentRanges, commentSelected]);
56
+ }, [selected, commentRanges, selectedValue]);
57
+
58
+ useComments(view.current, commentRanges);
56
59
 
57
60
  const extensions = useMemo(() => {
58
61
  return [
@@ -72,7 +75,7 @@ const Editor: FC<{
72
75
  // TODO(burdon): Highlight currently selected comment.
73
76
  return (
74
77
  <div className='flex grow overflow-y-scroll'>
75
- <MarkdownEditor ref={editorRef} model={model} comments={commentRanges} extensions={extensions} />
78
+ <MarkdownEditor ref={view} model={model} extensions={extensions} />
76
79
  </div>
77
80
  );
78
81
  };
@@ -83,11 +86,10 @@ const Editor: FC<{
83
86
 
84
87
  type CommentThread = {
85
88
  id: string;
86
- range: CommentRange;
89
+ cursor?: string;
90
+ range?: Range;
87
91
  yPos?: number;
88
- selection?: Range;
89
92
  messages: TextObject[];
90
- deleted?: boolean;
91
93
  };
92
94
 
93
95
  const Thread: FC<{
@@ -129,16 +131,16 @@ const Thread: FC<{
129
131
  className={mx(
130
132
  'flex flex-col m-1 rounded shadow divide-y bg-white',
131
133
  selected && 'ring',
132
- thread.deleted && 'opacity-50',
134
+ !thread.cursor && 'opacity-50',
133
135
  )}
134
136
  >
135
137
  <div className='flex p-2 gap-2 items-center text-xs font-mono text-neutral-500 font-thin'>
136
138
  <span>id:{thread.id.slice(0, 4)}</span>
137
- <span>from:{thread.selection?.from}</span>
138
- <span>to:{thread.selection?.to}</span>
139
+ <span>from:{thread.range?.from}</span>
140
+ <span>to:{thread.range?.to}</span>
139
141
  <span>y:{thread.yPos}</span>
140
142
  <span className='grow' />
141
- {thread.deleted && <Trash />}
143
+ {!thread.cursor && <Trash />}
142
144
  </div>
143
145
 
144
146
  {thread.messages.map((message, i) => (
@@ -151,10 +153,10 @@ const Thread: FC<{
151
153
  <div ref={containerRef} onClick={() => onSelect()} className='flex'>
152
154
  <TextEditor
153
155
  ref={editorRef}
154
- autofocus={focus}
156
+ autoFocus={focus}
155
157
  model={model}
156
158
  placeholder={'Enter comment...'}
157
- slots={{ root: { className: 'grow p-2 rounded-b' } }}
159
+ slots={{ root: { className: 'grow rounded-b' } }}
158
160
  extensions={[
159
161
  keymap.of([
160
162
  {
@@ -218,9 +220,10 @@ type StoryProps = {
218
220
  const Story = ({ text, autoCreate }: StoryProps) => {
219
221
  const [item] = useState({ text: new TextObject(text) });
220
222
  const [threads, setThreads] = useState<CommentThread[]>([]);
221
- const commentRanges = useMemo(() => threads.map((thread) => thread.range), [threads]);
222
223
  const [selected, setSelected] = useState<string>();
223
224
 
225
+ const comments = useMemo<Comment[]>(() => threads.map(({ id, cursor }) => ({ id, cursor })), [threads]);
226
+
224
227
  // Filter by visibility.
225
228
  const visibleThreads = useMemo(() => threads.filter((thread) => thread.yPos !== undefined), [threads]);
226
229
 
@@ -231,14 +234,14 @@ const Story = ({ text, autoCreate }: StoryProps) => {
231
234
  ...threads,
232
235
  {
233
236
  id,
234
- range: { id, cursor },
237
+ cursor,
235
238
  yPos: location ? Math.floor(location.top) : undefined,
236
239
  messages: autoCreate
237
240
  ? faker.helpers.multiple(() => new TextObject(faker.lorem.sentence()), { count: { min: 1, max: 3 } })
238
241
  : [],
239
242
  },
240
243
  ]);
241
- setSelected(id);
244
+ setSelected(id); // TODO(burdon): Not required.
242
245
  return id;
243
246
  };
244
247
 
@@ -247,7 +250,7 @@ const Story = ({ text, autoCreate }: StoryProps) => {
247
250
  setThreads((threads) =>
248
251
  threads.map((thread) => {
249
252
  if (thread.id === id) {
250
- thread.deleted = true;
253
+ thread.cursor = undefined;
251
254
  }
252
255
 
253
256
  return thread;
@@ -262,8 +265,7 @@ const Story = ({ text, autoCreate }: StoryProps) => {
262
265
  setThreads((threads) =>
263
266
  threads.map((thread) => {
264
267
  if (thread.id === id) {
265
- thread.range.cursor = cursor;
266
- delete thread.deleted;
268
+ thread.cursor = cursor;
267
269
  }
268
270
 
269
271
  return thread;
@@ -271,21 +273,21 @@ const Story = ({ text, autoCreate }: StoryProps) => {
271
273
  );
272
274
  };
273
275
 
274
- const handleSelectComment: CommentsOptions['onSelect'] = ({ active, closest, ranges }) => {
275
- log.info('select', { active: active?.slice(0, 4), closest: closest?.slice(0, 4) });
276
+ const handleSelectComment: CommentsOptions['onSelect'] = ({ comments, selection: { current, closest } }) => {
277
+ log.info('select', { active: current?.slice(0, 4), closest: closest?.slice(0, 4) });
276
278
  setThreads((threads) =>
277
279
  threads.map((thread) => {
278
- const range = ranges.find((range) => range.id === thread.range.id);
279
- if (range) {
280
- thread.yPos = range.location ? Math.floor(range.location.top) : undefined;
281
- thread.selection = { from: range.from, to: range.to };
280
+ const comment = comments.find(({ comment }) => comment.id === thread.id);
281
+ if (comment) {
282
+ thread.yPos = comment.location ? Math.floor(comment.location.top) : undefined;
283
+ thread.range = { from: comment.range.from, to: comment.range.to };
282
284
  }
283
285
 
284
286
  return thread;
285
287
  }),
286
288
  );
287
289
 
288
- setSelected(active ?? closest);
290
+ setSelected(current ?? closest);
289
291
  };
290
292
 
291
293
  const handleSelectThread = (id: string) => {
@@ -303,8 +305,8 @@ const Story = ({ text, autoCreate }: StoryProps) => {
303
305
  <div className='flex flex-col h-full w-[600px]'>
304
306
  <Editor
305
307
  item={item}
306
- commentSelected={selected}
307
- commentRanges={commentRanges}
308
+ selected={selected}
309
+ comments={comments}
308
310
  onCreateComment={handleCreateComment}
309
311
  onDeleteComment={handleDeleteComment}
310
312
  onUpdateComment={handleUpdateComment}
@@ -35,10 +35,13 @@ export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
35
35
  // https://codemirror.net/examples/autocompletion
36
36
  // https://codemirror.net/docs/ref/#autocomplete.autocompletion
37
37
  autocompletion({
38
- defaultKeymap: false,
38
+ activateOnTyping: true,
39
39
 
40
- // Don't start unless key press.
41
- activateOnTyping: false,
40
+ // closeOnBlur: false,
41
+ // defaultKeymap: false,
42
+
43
+ // TODO(burdon): Styles/fragments.
44
+ tooltipClass: () => 'shadow rounded',
42
45
  }),
43
46
 
44
47
  // TODO(burdon): Option to create new page?
@@ -4,14 +4,14 @@
4
4
  // Ref: https://github.com/automerge/automerge-codemirror
5
5
  //
6
6
 
7
- import { StateField, type Extension, type Transaction } from '@codemirror/state';
8
- import { ViewPlugin, type EditorView, type PluginValue, type ViewUpdate } from '@codemirror/view';
7
+ import { StateField, type Extension } from '@codemirror/state';
8
+ import { EditorView, ViewPlugin } from '@codemirror/view';
9
9
 
10
- import { next as A } from '@dxos/automerge/automerge';
11
- import { type Prop } from '@dxos/automerge/automerge';
10
+ import { type Prop, next as A } from '@dxos/automerge/automerge';
11
+ import { invariant } from '@dxos/invariant';
12
12
 
13
13
  import { cursorConverter } from './cursor';
14
- import { effectType, type IDocHandle, isReconcileTx, semaphoreFacet, type Value } from './defs';
14
+ import { effectType, type IDocHandle, isReconcileTx, type State } from './defs';
15
15
  import { PatchSemaphore } from './semaphore';
16
16
  import { Cursor } from '../cursor';
17
17
 
@@ -21,18 +21,18 @@ export type AutomergeOptions = {
21
21
  };
22
22
 
23
23
  export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
24
- const stateField: StateField<Value> = StateField.define({
24
+ const state = StateField.define<State>({
25
25
  create: () => ({
26
+ path: path.slice(),
26
27
  lastHeads: A.getHeads(handle.docSync()!),
27
28
  unreconciledTransactions: [],
28
- path: path.slice(),
29
29
  }),
30
30
 
31
- update: (value: Value, tr: Transaction) => {
32
- const result: Value = {
31
+ update: (value, tr) => {
32
+ const result: State = {
33
+ path: path.slice(),
33
34
  lastHeads: value.lastHeads,
34
35
  unreconciledTransactions: value.unreconciledTransactions.slice(),
35
- path: path.slice(),
36
36
  };
37
37
 
38
38
  let clearUnreconciled = false;
@@ -55,38 +55,34 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
55
55
  },
56
56
  });
57
57
 
58
- const semaphore = new PatchSemaphore(handle, stateField);
58
+ const semaphore = new PatchSemaphore(handle, state);
59
59
 
60
- const viewPlugin = ViewPlugin.fromClass(
61
- class AutomergeViewPlugin implements PluginValue {
62
- constructor(private readonly _view: EditorView) {
63
- handle.addListener('change', this._handleChange);
64
- }
60
+ return [
61
+ Cursor.converter.of(cursorConverter(handle, path)),
62
+ // Reconcile external updates.
63
+ ViewPlugin.fromClass(
64
+ class {
65
+ constructor(private readonly _view: EditorView) {
66
+ invariant(this._view);
67
+ handle.addListener('change', this._handleChange.bind(this));
68
+ }
65
69
 
66
- update(update: ViewUpdate) {
67
- if (update.transactions.length > 0 && update.transactions.some((tr) => !isReconcileTx(tr))) {
68
- queueMicrotask(() => {
69
- this._view.state.facet(semaphoreFacet).reconcile(this._view);
70
- });
70
+ destroy() {
71
+ handle.removeListener('change', this._handleChange.bind(this));
71
72
  }
72
- }
73
73
 
74
- destroy() {
75
- handle.addListener('change', this._handleChange);
74
+ _handleChange() {
75
+ semaphore.reconcile(this._view);
76
+ }
77
+ },
78
+ ),
79
+ // Reconcile local updates.
80
+ EditorView.updateListener.of(({ view, changes }) => {
81
+ if (!changes.empty) {
82
+ // TODO(burdon): Loses cursor position if auto closing brackets. Call explicitly?
83
+ semaphore.reconcile(view);
76
84
  }
77
-
78
- private _handleChange = () => {
79
- this._view.state.facet(semaphoreFacet).reconcile(this._view);
80
- };
81
- },
82
- );
83
-
84
- return {
85
- extension: [
86
- Cursor.converter.of(cursorConverter(handle, path)),
87
- semaphoreFacet.of(semaphore),
88
- stateField,
89
- viewPlugin,
90
- ],
91
- };
85
+ }),
86
+ state,
87
+ ];
92
88
  };
@@ -4,15 +4,14 @@
4
4
  // Ref: https://github.com/automerge/automerge-codemirror
5
5
  //
6
6
 
7
- import { Annotation, StateEffect, type StateField, type EditorState, type Transaction, Facet } from '@codemirror/state';
7
+ import { Annotation, StateEffect, type StateField, type EditorState, type Transaction } from '@codemirror/state';
8
8
 
9
9
  import { type ChangeFn, type ChangeOptions, type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
10
10
 
11
- import { type PatchSemaphore } from './semaphore';
12
-
13
- export type Value = {
14
- lastHeads: Heads;
11
+ // TODO(burdon): Rename.
12
+ export type State = {
15
13
  path: Prop[];
14
+ lastHeads: Heads;
16
15
  unreconciledTransactions: Transaction[];
17
16
  };
18
17
 
@@ -22,39 +21,15 @@ export type UpdateHeads = {
22
21
 
23
22
  export const effectType = StateEffect.define<UpdateHeads>({});
24
23
 
25
- export const updateHeads = (newHeads: Heads): StateEffect<UpdateHeads> => effectType.of({ newHeads });
26
-
27
- export const getLastHeads = (state: EditorState, field: StateField<Value>): Heads => state.field(field).lastHeads;
28
-
29
- export const getPath = (state: EditorState, field: StateField<Value>): Prop[] => state.field(field).path;
30
-
31
24
  export const reconcileAnnotationType = Annotation.define<unknown>();
32
25
 
33
- export const isReconcileTx = (tr: Transaction): boolean => !!tr.annotation(reconcileAnnotationType);
26
+ export const getPath = (state: EditorState, field: StateField<State>): Prop[] => state.field(field).path;
34
27
 
35
- // TODO(burdon): Rename sync/create generic def (like cursor) to decouple automerge deps.
36
- export const semaphoreFacet = Facet.define<PatchSemaphore, PatchSemaphore>({
37
- combine: (values) => values.at(-1)!, // Take last.
38
- });
28
+ export const getLastHeads = (state: EditorState, field: StateField<State>): Heads => state.field(field).lastHeads;
39
29
 
40
- /*
41
- export const makeReconcile = (tr: TransactionSpec) => {
42
- if (tr.annotations != null) {
43
- if (tr.annotations instanceof Array) {
44
- tr.annotations = [...tr.annotations, reconcileAnnotationType.of({})];
45
- } else {
46
- tr.annotations = [tr.annotations, reconcileAnnotationType.of({})];
47
- }
48
- } else {
49
- tr.annotations = [reconcileAnnotationType.of({})];
50
- }
30
+ export const updateHeads = (newHeads: Heads): StateEffect<UpdateHeads> => effectType.of({ newHeads });
51
31
 
52
- // return {
53
- // ...tr,
54
- // annotations: reconcileAnnotationType.of({})
55
- // }
56
- };
57
- */
32
+ export const isReconcileTx = (tr: Transaction): boolean => !!tr.annotation(reconcileAnnotationType);
58
33
 
59
34
  export type IDocHandle<T = any> = {
60
35
  docSync(): Doc<T> | undefined;
@@ -16,33 +16,38 @@ import {
16
16
  reconcileAnnotationType,
17
17
  updateHeads,
18
18
  type IDocHandle,
19
- type Value,
19
+ type State,
20
20
  } from './defs';
21
21
  import { updateAutomerge } from './update-automerge';
22
22
  import { updateCodeMirror } from './update-codemirror';
23
23
 
24
24
  /**
25
- * Implements three-way merge (on each mutation/key-stroke).
25
+ * Implements three-way merge (on each mutation/keystroke).
26
26
  */
27
27
  export class PatchSemaphore {
28
- _inReconcile = false;
28
+ private _inReconcile = false;
29
29
 
30
- constructor(private readonly _handle: IDocHandle, private readonly _field: StateField<Value>) {}
30
+ // prettier-ignore
31
+ constructor(
32
+ private readonly _handle: IDocHandle,
33
+ private readonly _state: StateField<State>
34
+ ) {}
31
35
 
32
- reconcile = (view: EditorView) => {
36
+ // NOTE: Cannot destruct view.state.
37
+ reconcile(view: EditorView) {
33
38
  if (this._inReconcile) {
34
39
  return;
35
40
  }
36
41
  this._inReconcile = true;
37
42
 
38
- const path = getPath(view.state, this._field);
43
+ const path = getPath(view.state, this._state);
39
44
 
40
45
  // Get the heads before the unreconciled transactions are applied.
41
- const oldHeads = getLastHeads(view.state, this._field);
46
+ const oldHeads = getLastHeads(view.state, this._state);
42
47
  let selection = view.state.selection;
43
48
 
44
49
  // First undo all the unreconciled transactions.
45
- const transactions = view.state.field(this._field).unreconciledTransactions.filter((tx) => !isReconcileTx(tx));
50
+ const transactions = view.state.field(this._state).unreconciledTransactions.filter((tx) => !isReconcileTx(tx));
46
51
  const toInvert = transactions.slice().reverse();
47
52
  for (const tx of toInvert) {
48
53
  const inverted = tx.changes.invert(tx.startState.doc);
@@ -54,11 +59,10 @@ export class PatchSemaphore {
54
59
  }
55
60
 
56
61
  // Apply the unreconciled transactions to the document.
57
- let newHeads = updateAutomerge(this._field, this._handle, transactions, view.state);
58
-
59
62
  // NOTE: null and undefined each come from automerge and repo respectively.
63
+ let newHeads = updateAutomerge(this._state, this._handle, transactions, view.state);
60
64
  if (newHeads === null || newHeads === undefined) {
61
- // TODO: @alexjg this is the call that's resetting the editor state on click
65
+ // TODO(alexjg): this is the call that's resetting the editor state on click.
62
66
  newHeads = automerge.getHeads(this._handle.docSync()!);
63
67
  }
64
68
 
@@ -68,11 +72,12 @@ export class PatchSemaphore {
68
72
  : automerge.diff(this._handle.docSync()!, oldHeads, newHeads);
69
73
  updateCodeMirror(view, selection, path, diff);
70
74
 
75
+ // Update automerge state.
71
76
  view.dispatch({
72
77
  effects: updateHeads(newHeads),
73
78
  annotations: reconcileAnnotationType.of({}),
74
79
  });
75
80
 
76
81
  this._inReconcile = false;
77
- };
82
+ }
78
83
  }
@@ -8,10 +8,10 @@ import { type EditorState, type StateField, type Text, type Transaction } from '
8
8
 
9
9
  import { next as am, type Heads } from '@dxos/automerge/automerge';
10
10
 
11
- import { type IDocHandle, type Value } from './defs';
11
+ import { type IDocHandle, type State } from './defs';
12
12
 
13
13
  export const updateAutomerge = (
14
- field: StateField<Value>,
14
+ field: StateField<State>,
15
15
  handle: IDocHandle,
16
16
  transactions: Transaction[],
17
17
  state: EditorState,
@@ -24,8 +24,8 @@ export interface AwarenessProvider {
24
24
  open(): void;
25
25
  close(): void;
26
26
 
27
- update(position: AwarenessPosition | undefined): void;
28
27
  getRemoteStates(): AwarenessState[];
28
+ update(position: AwarenessPosition | undefined): void;
29
29
  }
30
30
 
31
31
  const dummyProvider: AwarenessProvider = {
@@ -34,8 +34,8 @@ const dummyProvider: AwarenessProvider = {
34
34
  open: () => {},
35
35
  close: () => {},
36
36
 
37
- update: () => {},
38
37
  getRemoteStates: () => [],
38
+ update: () => {},
39
39
  };
40
40
 
41
41
  export const AwarenessProvider = Facet.define<AwarenessProvider, AwarenessProvider>({
@@ -66,7 +66,6 @@ export type AwarenessState = {
66
66
  /**
67
67
  * Extension provides presence information about other peers.
68
68
  */
69
- // TODO(burdon): Why provide default?
70
69
  export const awareness = (provider = dummyProvider): Extension => {
71
70
  return [
72
71
  AwarenessProvider.of(provider),
@@ -251,7 +250,6 @@ class RemoteCaretWidget extends WidgetType {
251
250
  }
252
251
  }
253
252
 
254
- // TODO(burdon): Mixed case?
255
253
  const styles = EditorView.baseTheme({
256
254
  '.cm-collab-selection': {},
257
255
  '.cm-collab-selectionLine': {
@@ -266,38 +264,40 @@ const styles = EditorView.baseTheme({
266
264
  marginRight: '-1px',
267
265
  boxSizing: 'border-box',
268
266
  display: 'inline',
267
+ cursor: 'pointer',
269
268
  },
270
269
  '.cm-collab-selectionCaretDot': {
271
270
  borderRadius: '50%',
272
271
  position: 'absolute',
273
- width: '.4em',
274
- height: '.4em',
275
- top: '-.2em',
276
- left: '-.2em',
272
+ width: '.5em',
273
+ height: '.5em',
274
+ top: '-.25em',
275
+ left: '-.25em',
277
276
  backgroundColor: 'inherit',
278
277
  transition: 'transform .3s ease-in-out',
279
278
  boxSizing: 'border-box',
280
279
  },
281
280
  '.cm-collab-selectionCaret:hover > .cm-collab-selectionCaretDot': {
282
- transformOrigin: 'bottom center',
283
281
  transform: 'scale(0)',
282
+ transformOrigin: 'center',
284
283
  },
285
284
  '.cm-collab-selectionInfo': {
286
285
  position: 'absolute',
287
- top: '-1.05em',
288
- left: '-1px',
286
+ transform: 'translate(-50%, 0)',
287
+ top: '-20px',
288
+ left: 0,
289
289
  fontSize: '.75em',
290
- fontFamily: 'serif',
290
+ fontFamily: 'sans-serif',
291
291
  fontStyle: 'normal',
292
292
  fontWeight: 'normal',
293
293
  lineHeight: 'normal',
294
294
  userSelect: 'none',
295
295
  color: 'white',
296
- paddingLeft: '2px',
297
- paddingRight: '2px',
296
+ padding: '2px',
298
297
  zIndex: 101,
299
298
  transition: 'opacity .3s ease-in-out',
300
299
  backgroundColor: 'inherit',
300
+ borderRadius: '2px',
301
301
  // These should be separate.
302
302
  opacity: 0,
303
303
  transitionDelay: '0s',
@@ -3,30 +3,35 @@
3
3
  //
4
4
 
5
5
  import { closeBrackets } from '@codemirror/autocomplete';
6
+ import { history } from '@codemirror/commands';
6
7
  import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
7
8
  import { type Extension } from '@codemirror/state';
8
9
  import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
9
- import { drawSelection, EditorView, placeholder } from '@codemirror/view';
10
+ import { drawSelection, EditorView, highlightActiveLine, placeholder } from '@codemirror/view';
10
11
 
11
- import type { ThemeMode } from '@dxos/react-ui';
12
+ import { type ThemeMode } from '@dxos/react-ui';
13
+
14
+ import { type TextEditorProps } from '../components';
12
15
 
13
16
  export type BasicBundleOptions = {
14
- readonly?: boolean;
15
17
  themeMode?: ThemeMode;
16
- placeholder?: string;
17
- };
18
+ } & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
18
19
 
19
- export const basicBundle = ({ readonly, themeMode, placeholder: _placeholder }: BasicBundleOptions): Extension[] =>
20
- [
21
- EditorView.lineWrapping,
20
+ export const basicBundle = ({
21
+ themeMode,
22
+ placeholder: _placeholder,
23
+ lineWrapping = true,
24
+ }: BasicBundleOptions): Extension[] => [
25
+ lineWrapping ? EditorView.lineWrapping : [],
22
26
 
23
- // TODO(burdon): Compare with minimalSetup.
24
- // https://codemirror.net/docs/ref/#codemirror.minimalSetup
25
- bracketMatching(),
26
- closeBrackets(),
27
- drawSelection(),
28
- _placeholder && placeholder(_placeholder),
27
+ // https://codemirror.net/docs/ref/#codemirror.minimalSetup
28
+ bracketMatching(),
29
+ closeBrackets(),
30
+ drawSelection(),
31
+ highlightActiveLine(),
32
+ history(),
33
+ _placeholder ? placeholder(_placeholder) : [],
29
34
 
30
- // TODO(burdon): Is this required?
31
- themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
32
- ].filter(Boolean) as Extension[];
35
+ // https://github.com/codemirror/theme-one-dark
36
+ themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
37
+ ];