@dxos/react-ui-editor 0.3.11-main.dd122a6 → 0.3.11-main.df1e30a

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 (160) hide show
  1. package/dist/lib/browser/index.mjs +1157 -1258
  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 +16 -13
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +20 -17
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -6
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
  11. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
  12. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  13. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  14. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  15. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  16. package/dist/types/src/extensions/automerge/defs.d.ts +7 -9
  17. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  18. package/dist/types/src/extensions/automerge/semaphore.d.ts +7 -12
  19. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  20. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
  21. package/dist/types/src/extensions/awareness.d.ts +1 -1
  22. package/dist/types/src/extensions/awareness.d.ts.map +1 -1
  23. package/dist/types/src/extensions/basic.d.ts +4 -5
  24. package/dist/types/src/extensions/basic.d.ts.map +1 -1
  25. package/dist/types/src/extensions/comments.d.ts +32 -19
  26. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  27. package/dist/types/src/{util → extensions}/cursor.d.ts +10 -2
  28. package/dist/types/src/extensions/cursor.d.ts.map +1 -0
  29. package/dist/types/src/extensions/index.d.ts +2 -7
  30. package/dist/types/src/extensions/index.d.ts.map +1 -1
  31. package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
  32. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  33. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
  34. package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
  35. package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
  36. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  37. package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
  38. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  39. package/dist/types/src/extensions/markdown/index.d.ts +7 -0
  40. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  41. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  42. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
  43. package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
  44. package/dist/types/src/extensions/mention.d.ts.map +1 -1
  45. package/dist/types/src/extensions/outliner.d.ts +4 -0
  46. package/dist/types/src/extensions/outliner.d.ts.map +1 -0
  47. package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
  48. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
  49. package/dist/types/src/hooks/defs.d.ts +25 -0
  50. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  51. package/dist/types/src/hooks/index.d.ts +2 -2
  52. package/dist/types/src/hooks/index.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useTextEditor.d.ts +17 -0
  54. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  55. package/dist/types/src/hooks/useTextModel.d.ts +2 -32
  56. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  57. package/dist/types/src/index.d.ts +2 -0
  58. package/dist/types/src/index.d.ts.map +1 -1
  59. package/dist/types/src/styles/markdown.d.ts +2 -2
  60. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  61. package/dist/types/src/themes/default.d.ts.map +1 -1
  62. package/dist/types/src/util.d.ts +7 -0
  63. package/dist/types/src/util.d.ts.map +1 -0
  64. package/package.json +34 -36
  65. package/src/components/TextEditor/MarkdownEditor.stories.tsx +82 -82
  66. package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
  67. package/src/components/TextEditor/TextEditor.tsx +122 -93
  68. package/src/components/TextEditor/comments.stories.tsx +357 -0
  69. package/src/extensions/autocomplete.ts +23 -19
  70. package/src/extensions/automerge/automerge.ts +40 -46
  71. package/src/extensions/automerge/cursor.ts +2 -1
  72. package/src/extensions/automerge/defs.ts +10 -31
  73. package/src/extensions/automerge/semaphore.ts +23 -20
  74. package/src/extensions/automerge/update-automerge.ts +2 -2
  75. package/src/extensions/awareness.ts +40 -46
  76. package/src/extensions/basic.ts +22 -17
  77. package/src/extensions/comments.ts +246 -238
  78. package/src/extensions/cursor.ts +50 -0
  79. package/src/extensions/index.ts +2 -7
  80. package/src/extensions/markdown/bundle.ts +20 -30
  81. package/src/extensions/markdown/code.ts +127 -0
  82. package/src/extensions/markdown/heading.ts +59 -0
  83. package/src/extensions/markdown/highlight.ts +9 -23
  84. package/src/extensions/markdown/hr.ts +71 -0
  85. package/src/extensions/{image.ts → markdown/image.ts} +2 -2
  86. package/src/extensions/markdown/index.ts +7 -0
  87. package/src/extensions/{link.ts → markdown/link.ts} +8 -9
  88. package/src/extensions/{table.ts → markdown/table.ts} +6 -3
  89. package/src/extensions/markdown/tasklist.ts +110 -0
  90. package/src/extensions/mention.ts +9 -2
  91. package/src/extensions/outliner.ts +12 -0
  92. package/src/hooks/awareness-provider.ts +24 -20
  93. package/src/hooks/defs.ts +42 -0
  94. package/src/hooks/index.ts +3 -2
  95. package/src/hooks/useTextEditor.ts +29 -0
  96. package/src/hooks/useTextModel.ts +48 -60
  97. package/src/index.ts +2 -0
  98. package/src/styles/markdown.ts +9 -12
  99. package/src/themes/default.ts +15 -50
  100. package/src/util.ts +41 -0
  101. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  102. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  103. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
  104. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  105. package/dist/types/src/extensions/code.d.ts.map +0 -1
  106. package/dist/types/src/extensions/hr.d.ts.map +0 -1
  107. package/dist/types/src/extensions/image.d.ts.map +0 -1
  108. package/dist/types/src/extensions/link.d.ts.map +0 -1
  109. package/dist/types/src/extensions/table.d.ts.map +0 -1
  110. package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
  111. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  112. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  113. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  114. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  115. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  116. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  117. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  118. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  119. package/dist/types/src/hooks/automerge.d.ts +0 -3
  120. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  121. package/dist/types/src/hooks/yjs.d.ts +0 -27
  122. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  123. package/dist/types/src/testing/index.d.ts +0 -3
  124. package/dist/types/src/testing/index.d.ts.map +0 -1
  125. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  126. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  127. package/dist/types/src/testing/proto/index.d.ts +0 -2
  128. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  129. package/dist/types/src/testing/replicator.d.ts +0 -45
  130. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  131. package/dist/types/src/util/cursor.d.ts.map +0 -1
  132. package/dist/types/src/util/index.d.ts +0 -3
  133. package/dist/types/src/util/index.d.ts.map +0 -1
  134. package/dist/types/src/util/util.d.ts +0 -5
  135. package/dist/types/src/util/util.d.ts.map +0 -1
  136. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  137. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  138. package/src/extensions/code.ts +0 -98
  139. package/src/extensions/hr.ts +0 -73
  140. package/src/extensions/tasklist.ts +0 -119
  141. package/src/extensions/yjs/cursor.ts +0 -21
  142. package/src/extensions/yjs/index.ts +0 -5
  143. package/src/extensions/yjs/yjs.test.ts +0 -35
  144. package/src/extensions/yjs/yjs.ts +0 -16
  145. package/src/hooks/automerge.ts +0 -52
  146. package/src/hooks/yjs.ts +0 -178
  147. package/src/testing/index.ts +0 -6
  148. package/src/testing/proto/gen/schema.ts +0 -49
  149. package/src/testing/proto/index.ts +0 -5
  150. package/src/testing/proto/schema.proto +0 -15
  151. package/src/testing/replicator.ts +0 -184
  152. package/src/util/cursor.ts +0 -29
  153. package/src/util/index.ts +0 -6
  154. package/src/util/util.ts +0 -18
  155. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  156. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  157. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  158. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  159. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  160. /package/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.ts} +0 -0
@@ -7,52 +7,56 @@ import { tags as tags2 } from "@lezer/highlight";
7
7
 
8
8
  // packages/ui/react-ui-editor/src/components/TextEditor/TextEditor.tsx
9
9
  import { EditorState as EditorState2 } from "@codemirror/state";
10
- import { EditorView as EditorView12 } from "@codemirror/view";
10
+ import { EditorView as EditorView13 } from "@codemirror/view";
11
11
  import { useFocusableGroup } from "@fluentui/react-tabster";
12
12
  import { vim } from "@replit/codemirror-vim";
13
13
  import defaultsDeep2 from "lodash.defaultsdeep";
14
- import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
15
- import { generateName } from "@dxos/display-name";
14
+ import React, { forwardRef, useCallback, useEffect as useEffect2, useImperativeHandle, useRef, useState } from "react";
15
+ import { log as log2 } from "@dxos/log";
16
16
  import { useThemeContext } from "@dxos/react-ui";
17
- import { getColorForValue, inputSurface, mx as mx3 } from "@dxos/react-ui-theme";
17
+ import { attentionSurface, mx as mx3 } from "@dxos/react-ui-theme";
18
18
 
19
19
  // packages/ui/react-ui-editor/src/extensions/autocomplete.ts
20
20
  import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
21
+ import { markdownLanguage } from "@codemirror/lang-markdown";
21
22
  import { keymap } from "@codemirror/view";
22
23
  var autocomplete = ({ onSearch }) => {
23
24
  return [
24
25
  // https://codemirror.net/docs/ref/#view.keymap
25
26
  // https://discuss.codemirror.net/t/how-can-i-replace-the-default-autocompletion-keymap-v6/3322
27
+ // TODO(burdon): Set custom keymap.
26
28
  keymap.of(completionKeymap),
27
29
  // https://codemirror.net/examples/autocompletion
28
30
  // https://codemirror.net/docs/ref/#autocomplete.autocompletion
29
31
  autocompletion({
30
- // TODO(burdon): Set custom keymap.
32
+ activateOnTyping: true,
33
+ // closeOnBlur: false,
31
34
  // defaultKeymap: false,
32
- // Don't start unless key press.
33
- activateOnTyping: false,
34
- // TODO(burdon): Option to create new page?
35
- // TODO(burdon): Optional decoration via addToOptions
36
- override: [
37
- (context) => {
38
- const match = context.matchBefore(/\w*/);
39
- if (!match || match.from === match.to && !context.explicit) {
40
- return null;
41
- }
42
- return {
43
- from: match.from,
44
- options: onSearch(match.text.toLowerCase())
45
- };
35
+ // TODO(burdon): Styles/fragments.
36
+ tooltipClass: () => "shadow rounded"
37
+ }),
38
+ // TODO(burdon): Option to create new page?
39
+ // TODO(burdon): Optional decoration via addToOptions
40
+ markdownLanguage.data.of({
41
+ autocomplete: (context) => {
42
+ const match = context.matchBefore(/\w*/);
43
+ if (!match || match.from === match.to && !context.explicit) {
44
+ return null;
46
45
  }
47
- ]
46
+ return {
47
+ from: match.from,
48
+ options: onSearch(match.text.toLowerCase())
49
+ };
50
+ }
48
51
  })
49
52
  ];
50
53
  };
51
54
 
52
55
  // packages/ui/react-ui-editor/src/extensions/automerge/automerge.ts
53
- import { Facet as Facet2, StateField } from "@codemirror/state";
54
- import { ViewPlugin } from "@codemirror/view";
56
+ import { StateField } from "@codemirror/state";
57
+ import { EditorView, ViewPlugin } from "@codemirror/view";
55
58
  import { next as A } from "@dxos/automerge/automerge";
59
+ import { invariant } from "@dxos/invariant";
56
60
 
57
61
  // packages/ui/react-ui-editor/src/extensions/automerge/cursor.ts
58
62
  import get from "lodash.get";
@@ -93,12 +97,12 @@ var cursorConverter = (handle, path) => ({
93
97
  // packages/ui/react-ui-editor/src/extensions/automerge/defs.ts
94
98
  import { Annotation, StateEffect } from "@codemirror/state";
95
99
  var effectType = StateEffect.define({});
100
+ var reconcileAnnotationType = Annotation.define();
101
+ var getPath = (state, field) => state.field(field).path;
102
+ var getLastHeads = (state, field) => state.field(field).lastHeads;
96
103
  var updateHeads = (newHeads) => effectType.of({
97
104
  newHeads
98
105
  });
99
- var getLastHeads = (state, field) => state.field(field).lastHeads;
100
- var getPath = (state, field) => state.field(field).path;
101
- var reconcileAnnotationType = Annotation.define();
102
106
  var isReconcileTx = (tr) => !!tr.annotation(reconcileAnnotationType);
103
107
 
104
108
  // packages/ui/react-ui-editor/src/extensions/automerge/semaphore.ts
@@ -236,85 +240,96 @@ var charPath = (textPath, candidatePath) => {
236
240
 
237
241
  // packages/ui/react-ui-editor/src/extensions/automerge/semaphore.ts
238
242
  var PatchSemaphore = class {
239
- constructor(_field) {
240
- this._field = _field;
243
+ // prettier-ignore
244
+ constructor(_handle, _state) {
245
+ this._handle = _handle;
246
+ this._state = _state;
241
247
  this._inReconcile = false;
242
- this._queue = [];
243
- this.reconcile = (handle, view) => {
244
- if (this._inReconcile) {
245
- return;
246
- }
247
- this._inReconcile = true;
248
- const path = getPath(view.state, this._field);
249
- const oldHeads = getLastHeads(view.state, this._field);
250
- let selection = view.state.selection;
251
- const transactions = view.state.field(this._field).unreconciledTransactions.filter((tx) => !isReconcileTx(tx));
252
- const toInvert = transactions.slice().reverse();
253
- for (const tx of toInvert) {
254
- const inverted = tx.changes.invert(tx.startState.doc);
255
- selection = selection.map(inverted);
256
- view.dispatch({
257
- changes: inverted,
258
- annotations: reconcileAnnotationType.of(true)
259
- });
260
- }
261
- let newHeads = updateAutomerge(this._field, handle, transactions, view.state);
262
- if (newHeads === null || newHeads === void 0) {
263
- newHeads = automerge2.getHeads(handle.docSync());
264
- }
265
- const diff = automerge2.equals(oldHeads, newHeads) ? [] : automerge2.diff(handle.docSync(), oldHeads, newHeads);
266
- updateCodeMirror(view, selection, path, diff);
248
+ }
249
+ // NOTE: Cannot destruct view.state.
250
+ reconcile(view) {
251
+ if (this._inReconcile) {
252
+ return;
253
+ }
254
+ this._inReconcile = true;
255
+ const path = getPath(view.state, this._state);
256
+ const oldHeads = getLastHeads(view.state, this._state);
257
+ let selection = view.state.selection;
258
+ const transactions = view.state.field(this._state).unreconciledTransactions.filter((tx) => !isReconcileTx(tx));
259
+ const toInvert = transactions.slice().reverse();
260
+ for (const tx of toInvert) {
261
+ const inverted = tx.changes.invert(tx.startState.doc);
262
+ selection = selection.map(inverted);
267
263
  view.dispatch({
268
- effects: updateHeads(newHeads),
269
- annotations: reconcileAnnotationType.of({})
264
+ changes: inverted,
265
+ annotations: reconcileAnnotationType.of(true)
270
266
  });
271
- this._inReconcile = false;
272
- };
267
+ }
268
+ let newHeads = updateAutomerge(this._state, this._handle, transactions, view.state);
269
+ if (newHeads === null || newHeads === void 0) {
270
+ newHeads = automerge2.getHeads(this._handle.docSync());
271
+ }
272
+ const diff = automerge2.equals(oldHeads, newHeads) ? [] : automerge2.diff(this._handle.docSync(), oldHeads, newHeads);
273
+ updateCodeMirror(view, selection, path, diff);
274
+ view.dispatch({
275
+ effects: updateHeads(newHeads),
276
+ annotations: reconcileAnnotationType.of({})
277
+ });
278
+ this._inReconcile = false;
273
279
  }
274
280
  };
275
281
 
276
- // packages/ui/react-ui-editor/src/util/cursor.ts
282
+ // packages/ui/react-ui-editor/src/extensions/cursor.ts
277
283
  import { Facet } from "@codemirror/state";
278
- var DEFAULT_CURSOR_CONVERTER = {
284
+ var defaultCursorConverter = {
279
285
  toCursor: (position) => position.toString(),
280
286
  fromCursor: (cursor) => parseInt(cursor)
281
287
  };
282
- var CursorConverter = Facet.define({
283
- combine: (providers) => providers[0] ?? DEFAULT_CURSOR_CONVERTER
284
- });
285
-
286
- // packages/ui/react-ui-editor/src/util/util.ts
287
- import { log } from "@dxos/log";
288
- var __dxlog_file = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/util/util.ts";
289
- var callbackWrapper = (fn) => (...args) => {
290
- try {
291
- return fn(...args);
292
- } catch (error) {
293
- log.catch(error, void 0, {
294
- F: __dxlog_file,
295
- L: 16,
296
- S: void 0,
297
- C: (f, a) => f(...a)
288
+ var Cursor = class _Cursor {
289
+ static {
290
+ this.converter = Facet.define({
291
+ combine: (providers) => providers[0] ?? defaultCursorConverter
298
292
  });
299
293
  }
294
+ static {
295
+ this.getCursorFromRange = (state, range) => {
296
+ const cursorConverter2 = state.facet(_Cursor.converter);
297
+ const from = cursorConverter2.toCursor(range.from);
298
+ const to = cursorConverter2.toCursor(range.to, -1);
299
+ return [
300
+ from,
301
+ to
302
+ ].join(":");
303
+ };
304
+ }
305
+ static {
306
+ this.getRangeFromCursor = (state, cursor) => {
307
+ const cursorConverter2 = state.facet(_Cursor.converter);
308
+ const parts = cursor.split(":");
309
+ const from = cursorConverter2.fromCursor(parts[0]);
310
+ const to = cursorConverter2.fromCursor(parts[1]);
311
+ return from !== void 0 && to !== void 0 ? {
312
+ from,
313
+ to
314
+ } : void 0;
315
+ };
316
+ }
300
317
  };
301
318
 
302
319
  // packages/ui/react-ui-editor/src/extensions/automerge/automerge.ts
303
- var semaphoreFacet = Facet2.define({
304
- combine: (values) => values.at(-1)
305
- });
320
+ var __dxlog_file = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/extensions/automerge/automerge.ts";
306
321
  var automerge3 = ({ handle, path }) => {
307
- const stateField = StateField.define({
322
+ const state = StateField.define({
308
323
  create: () => ({
324
+ path: path.slice(),
309
325
  lastHeads: A.getHeads(handle.docSync()),
310
- unreconciledTransactions: [],
311
- path: path.slice()
326
+ unreconciledTransactions: []
312
327
  }),
313
328
  update: (value, tr) => {
314
329
  const result = {
330
+ path: path.slice(),
315
331
  lastHeads: value.lastHeads,
316
- unreconciledTransactions: value.unreconciledTransactions.slice(),
317
- path: path.slice()
332
+ unreconciledTransactions: value.unreconciledTransactions.slice()
318
333
  };
319
334
  let clearUnreconciled = false;
320
335
  for (const effect of tr.effects) {
@@ -333,39 +348,44 @@ var automerge3 = ({ handle, path }) => {
333
348
  return result;
334
349
  }
335
350
  });
336
- const semaphore = new PatchSemaphore(stateField);
337
- const viewPlugin = ViewPlugin.fromClass(class AutomergeCodemirrorViewPlugin {
338
- constructor(_view) {
339
- this._view = _view;
340
- this._handleChange = () => {
341
- this._view.state.facet(semaphoreFacet).reconcile(handle, this._view);
342
- };
343
- handle.addListener("change", this._handleChange);
344
- }
345
- update(update2) {
346
- if (update2.transactions.length > 0 && update2.transactions.some((tr) => !isReconcileTx(tr))) {
347
- queueMicrotask(() => {
348
- this._view.state.facet(semaphoreFacet).reconcile(handle, this._view);
351
+ const semaphore = new PatchSemaphore(handle, state);
352
+ return [
353
+ Cursor.converter.of(cursorConverter(handle, path)),
354
+ // Reconcile external updates.
355
+ ViewPlugin.fromClass(class {
356
+ constructor(_view) {
357
+ this._view = _view;
358
+ invariant(this._view, void 0, {
359
+ F: __dxlog_file,
360
+ L: 66,
361
+ S: this,
362
+ A: [
363
+ "this._view",
364
+ ""
365
+ ]
349
366
  });
367
+ handle.addListener("change", this._handleChange.bind(this));
350
368
  }
351
- }
352
- destroy() {
353
- handle.addListener("change", this._handleChange);
354
- }
355
- });
356
- return {
357
- extension: [
358
- CursorConverter.of(cursorConverter(handle, path)),
359
- semaphoreFacet.of(semaphore),
360
- stateField,
361
- viewPlugin
362
- ]
363
- };
369
+ destroy() {
370
+ handle.removeListener("change", this._handleChange.bind(this));
371
+ }
372
+ _handleChange() {
373
+ semaphore.reconcile(this._view);
374
+ }
375
+ }),
376
+ // Reconcile local updates.
377
+ EditorView.updateListener.of(({ view, changes }) => {
378
+ if (!changes.empty) {
379
+ semaphore.reconcile(view);
380
+ }
381
+ }),
382
+ state
383
+ ];
364
384
  };
365
385
 
366
386
  // packages/ui/react-ui-editor/src/extensions/awareness.ts
367
- import { Annotation as Annotation2, Facet as Facet3, RangeSet } from "@codemirror/state";
368
- import { Decoration, EditorView, ViewPlugin as ViewPlugin2, WidgetType } from "@codemirror/view";
387
+ import { Annotation as Annotation2, Facet as Facet2, RangeSet } from "@codemirror/state";
388
+ import { Decoration, EditorView as EditorView2, ViewPlugin as ViewPlugin2, WidgetType } from "@codemirror/view";
369
389
  import { Event } from "@dxos/async";
370
390
  import { Context } from "@dxos/context";
371
391
  var dummyProvider = {
@@ -374,11 +394,11 @@ var dummyProvider = {
374
394
  },
375
395
  close: () => {
376
396
  },
397
+ getRemoteStates: () => [],
377
398
  update: () => {
378
- },
379
- getRemoteStates: () => []
399
+ }
380
400
  };
381
- var AwarenessProvider = Facet3.define({
401
+ var AwarenessProvider = Facet2.define({
382
402
  combine: (providers) => providers[0] ?? dummyProvider
383
403
  });
384
404
  var RemoteSelectionChangedAnnotation = Annotation2.define();
@@ -397,7 +417,7 @@ var RemoteSelectionsDecorator = class {
397
417
  this._ctx = new Context();
398
418
  this._lastAnchor = void 0;
399
419
  this._lastHead = void 0;
400
- this._cursorConverter = view.state.facet(CursorConverter);
420
+ this._cursorConverter = view.state.facet(Cursor.converter);
401
421
  this._provider = view.state.facet(AwarenessProvider);
402
422
  this._provider.open();
403
423
  this._provider.remoteStateChange.on(this._ctx, () => {
@@ -452,7 +472,7 @@ var RemoteSelectionsDecorator = class {
452
472
  attributes: {
453
473
  style: `background-color: ${lightColor}`
454
474
  },
455
- class: "cm-ySelection"
475
+ class: "cm-collab-selection"
456
476
  })
457
477
  });
458
478
  } else {
@@ -463,7 +483,7 @@ var RemoteSelectionsDecorator = class {
463
483
  attributes: {
464
484
  style: `background-color: ${color}`
465
485
  },
466
- class: "cm-ySelection"
486
+ class: "cm-collab-selection"
467
487
  })
468
488
  });
469
489
  decorations.push({
@@ -473,7 +493,7 @@ var RemoteSelectionsDecorator = class {
473
493
  attributes: {
474
494
  style: `background-color: ${color}`
475
495
  },
476
- class: "cm-ySelection"
496
+ class: "cm-collab-selection"
477
497
  })
478
498
  });
479
499
  for (let i = startLine.number + 1; i < endLine.number; i++) {
@@ -484,7 +504,7 @@ var RemoteSelectionsDecorator = class {
484
504
  value: Decoration.line({
485
505
  attributes: {
486
506
  style: `background-color: ${color}`,
487
- class: "cm-yLineSelection"
507
+ class: "cm-collab-selectionLine"
488
508
  }
489
509
  })
490
510
  });
@@ -504,23 +524,21 @@ var RemoteSelectionsDecorator = class {
504
524
  }
505
525
  };
506
526
  var RemoteCaretWidget = class extends WidgetType {
507
- constructor(name, color) {
527
+ constructor(_name, _color) {
508
528
  super();
509
- this.name = name;
510
- this.color = color;
511
- this.name = name;
512
- this.color = color;
529
+ this._name = _name;
530
+ this._color = _color;
513
531
  }
514
532
  toDOM() {
515
533
  const span = document.createElement("span");
516
- span.className = "cm-ySelectionCaret";
517
- span.style.backgroundColor = this.color;
518
- span.style.borderColor = this.color;
534
+ span.className = "cm-collab-selectionCaret";
535
+ span.style.backgroundColor = this._color;
536
+ span.style.borderColor = this._color;
519
537
  const dot = document.createElement("div");
520
- dot.className = "cm-ySelectionCaretDot";
538
+ dot.className = "cm-collab-selectionCaretDot";
521
539
  const info = document.createElement("div");
522
- info.className = "cm-ySelectionInfo";
523
- info.innerText = this.name;
540
+ info.className = "cm-collab-selectionInfo";
541
+ info.innerText = this._name;
524
542
  span.appendChild(document.createTextNode("\u2060"));
525
543
  span.appendChild(dot);
526
544
  span.appendChild(document.createTextNode("\u2060"));
@@ -528,15 +546,12 @@ var RemoteCaretWidget = class extends WidgetType {
528
546
  span.appendChild(document.createTextNode("\u2060"));
529
547
  return span;
530
548
  }
531
- eq(widget) {
532
- return widget.color === this.color;
533
- }
534
- compare(widget) {
535
- return widget.color === this.color;
536
- }
537
549
  updateDOM() {
538
550
  return false;
539
551
  }
552
+ eq(widget) {
553
+ return widget._color === this._color;
554
+ }
540
555
  get estimatedHeight() {
541
556
  return -1;
542
557
  }
@@ -544,58 +559,60 @@ var RemoteCaretWidget = class extends WidgetType {
544
559
  return true;
545
560
  }
546
561
  };
547
- var styles = EditorView.baseTheme({
548
- ".cm-ySelection": {},
549
- ".cm-yLineSelection": {
562
+ var styles = EditorView2.baseTheme({
563
+ ".cm-collab-selection": {},
564
+ ".cm-collab-selectionLine": {
550
565
  padding: 0,
551
566
  margin: "0px 2px 0px 4px"
552
567
  },
553
- ".cm-ySelectionCaret": {
568
+ ".cm-collab-selectionCaret": {
554
569
  position: "relative",
555
570
  borderLeft: "1px solid black",
556
571
  borderRight: "1px solid black",
557
572
  marginLeft: "-1px",
558
573
  marginRight: "-1px",
559
574
  boxSizing: "border-box",
560
- display: "inline"
575
+ display: "inline",
576
+ cursor: "pointer"
561
577
  },
562
- ".cm-ySelectionCaretDot": {
578
+ ".cm-collab-selectionCaretDot": {
563
579
  borderRadius: "50%",
564
580
  position: "absolute",
565
- width: ".4em",
566
- height: ".4em",
567
- top: "-.2em",
568
- left: "-.2em",
581
+ width: ".5em",
582
+ height: ".5em",
583
+ top: "-.25em",
584
+ left: "-.25em",
569
585
  backgroundColor: "inherit",
570
586
  transition: "transform .3s ease-in-out",
571
587
  boxSizing: "border-box"
572
588
  },
573
- ".cm-ySelectionCaret:hover > .cm-ySelectionCaretDot": {
574
- transformOrigin: "bottom center",
575
- transform: "scale(0)"
589
+ ".cm-collab-selectionCaret:hover > .cm-collab-selectionCaretDot": {
590
+ transform: "scale(0)",
591
+ transformOrigin: "center"
576
592
  },
577
- ".cm-ySelectionInfo": {
593
+ ".cm-collab-selectionInfo": {
578
594
  position: "absolute",
579
- top: "-1.05em",
580
- left: "-1px",
595
+ transform: "translate(-50%, 0)",
596
+ top: "-20px",
597
+ left: 0,
581
598
  fontSize: ".75em",
582
- fontFamily: "serif",
599
+ fontFamily: "sans-serif",
583
600
  fontStyle: "normal",
584
601
  fontWeight: "normal",
585
602
  lineHeight: "normal",
586
603
  userSelect: "none",
587
604
  color: "white",
588
- paddingLeft: "2px",
589
- paddingRight: "2px",
605
+ padding: "2px",
590
606
  zIndex: 101,
591
607
  transition: "opacity .3s ease-in-out",
592
608
  backgroundColor: "inherit",
593
- // these should be separate
609
+ borderRadius: "2px",
610
+ // These should be separate.
594
611
  opacity: 0,
595
612
  transitionDelay: "0s",
596
613
  whiteSpace: "nowrap"
597
614
  },
598
- ".cm-ySelectionCaret:hover > .cm-ySelectionInfo": {
615
+ ".cm-collab-selectionCaret:hover > .cm-collab-selectionInfo": {
599
616
  opacity: 1,
600
617
  transitionDelay: "0s"
601
618
  }
@@ -603,25 +620,27 @@ var styles = EditorView.baseTheme({
603
620
 
604
621
  // packages/ui/react-ui-editor/src/extensions/basic.ts
605
622
  import { closeBrackets } from "@codemirror/autocomplete";
623
+ import { history } from "@codemirror/commands";
606
624
  import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from "@codemirror/language";
607
625
  import { oneDarkHighlightStyle } from "@codemirror/theme-one-dark";
608
- import { drawSelection, EditorView as EditorView2, placeholder } from "@codemirror/view";
609
- var basicBundle = ({ readonly, themeMode, placeholder: _placeholder }) => [
610
- EditorView2.lineWrapping,
611
- // TODO(burdon): Compare with minimalSetup.
626
+ import { drawSelection, EditorView as EditorView3, highlightActiveLine, placeholder } from "@codemirror/view";
627
+ var basicBundle = ({ themeMode, placeholder: _placeholder, lineWrapping = true }) => [
628
+ lineWrapping ? EditorView3.lineWrapping : [],
612
629
  // https://codemirror.net/docs/ref/#codemirror.minimalSetup
613
630
  bracketMatching(),
614
631
  closeBrackets(),
615
632
  drawSelection(),
616
- _placeholder && placeholder(_placeholder),
617
- // TODO(burdon): Is this required?
633
+ highlightActiveLine(),
634
+ history(),
635
+ _placeholder ? placeholder(_placeholder) : [],
636
+ // https://github.com/codemirror/theme-one-dark
618
637
  themeMode === "dark" ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle)
619
- ].filter(Boolean);
638
+ ];
620
639
 
621
640
  // packages/ui/react-ui-editor/src/extensions/blast.ts
622
- import { EditorView as EditorView3, keymap as keymap2 } from "@codemirror/view";
641
+ import { EditorView as EditorView4, keymap as keymap2 } from "@codemirror/view";
623
642
  import defaultsDeep from "lodash.defaultsdeep";
624
- import { invariant } from "@dxos/invariant";
643
+ import { invariant as invariant2 } from "@dxos/invariant";
625
644
  var __dxlog_file2 = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/extensions/blast.ts";
626
645
  var defaultOptions = {
627
646
  effect: 2,
@@ -655,10 +674,10 @@ var blast = (options = defaultOptions) => {
655
674
  const getPoint = (view, pos) => {
656
675
  const { left: x = 0, top: y = 0 } = view.coordsForChar(pos) ?? {};
657
676
  const element = document.elementFromPoint(x, y);
658
- const { offsetLeft: left = 0, offsetTop: top = 0 } = view.scrollDOM.parentElement ?? {};
677
+ const { offsetLeft: left = 0, offsetTop: top2 = 0 } = view.scrollDOM.parentElement ?? {};
659
678
  const point = {
660
679
  x: x - left,
661
- y: y - top
680
+ y: y - top2
662
681
  };
663
682
  return {
664
683
  element,
@@ -667,7 +686,7 @@ var blast = (options = defaultOptions) => {
667
686
  };
668
687
  return [
669
688
  // Cursor moved.
670
- EditorView3.updateListener.of((update2) => {
689
+ EditorView4.updateListener.of((update2) => {
671
690
  if (blaster?.node !== update2.view.scrollDOM) {
672
691
  if (blaster) {
673
692
  blaster.destroy();
@@ -748,7 +767,7 @@ var Blaster = class {
748
767
  return this._node;
749
768
  }
750
769
  initialize() {
751
- invariant(!this._canvas && !this._ctx, void 0, {
770
+ invariant2(!this._canvas && !this._ctx, void 0, {
752
771
  F: __dxlog_file2,
753
772
  L: 138,
754
773
  S: this,
@@ -785,7 +804,7 @@ var Blaster = class {
785
804
  }
786
805
  }
787
806
  start() {
788
- invariant(this._canvas && this._ctx, void 0, {
807
+ invariant2(this._canvas && this._ctx, void 0, {
789
808
  F: __dxlog_file2,
790
809
  L: 177,
791
810
  S: this,
@@ -928,9 +947,14 @@ var random = (min, max) => {
928
947
  return min + ~~(Math.random() * (max - min + 1));
929
948
  };
930
949
 
931
- // packages/ui/react-ui-editor/src/extensions/code.ts
932
- import { ViewPlugin as ViewPlugin3, EditorView as EditorView4, Decoration as Decoration2 } from "@codemirror/view";
933
- import { mx as mx2 } from "@dxos/react-ui-theme";
950
+ // packages/ui/react-ui-editor/src/extensions/comments.ts
951
+ import { StateEffect as StateEffect2, StateField as StateField2 } from "@codemirror/state";
952
+ import { hoverTooltip, keymap as keymap3, Decoration as Decoration2, EditorView as EditorView5 } from "@codemirror/view";
953
+ import sortBy from "lodash.sortby";
954
+ import { useEffect } from "react";
955
+ import { debounce } from "@dxos/async";
956
+ import { invariant as invariant3 } from "@dxos/invariant";
957
+ import { nonNullable } from "@dxos/util";
934
958
 
935
959
  // packages/ui/react-ui-editor/src/styles/cursors.ts
936
960
  import * as random2 from "lib0/random";
@@ -973,34 +997,15 @@ var cursorColor = cursorColors[random2.uint32() % cursorColors.length];
973
997
  // packages/ui/react-ui-editor/src/styles/markdown.ts
974
998
  import { mx } from "@dxos/react-ui-theme";
975
999
  var headings = {
976
- 1: [
977
- "mbs-4 mbe-2 font-medium text-inherit no-underline text-4xl",
978
- "-ml-[10px]"
979
- ],
980
- 2: [
981
- "mbs-4 mbe-2 font-medium text-inherit no-underline text-3xl",
982
- "-ml-[8px]"
983
- ],
984
- 3: [
985
- "mbs-4 mbe-2 font-medium text-inherit no-underline text-2xl",
986
- "-ml-[7px]"
987
- ],
988
- 4: [
989
- "mbs-4 mbe-2 font-medium text-inherit no-underline text-xl",
990
- "-ml-[6px]"
991
- ],
992
- 5: [
993
- "mbs-4 mbe-2 font-medium text-inherit no-underline text-lg",
994
- "-ml-[5px]"
995
- ],
996
- 6: [
997
- "mbs-4 mbe-2 font-medium text-inherit no-underline",
998
- "-ml-[4px]"
999
- ]
1000
- };
1001
- var heading = (level, readonly) => {
1002
- const [style, offset] = headings[level];
1003
- return mx(style, readonly && offset);
1000
+ 1: "mbs-4 mbe-2 font-medium text-inherit no-underline text-4xl",
1001
+ 2: "mbs-4 mbe-2 font-medium text-inherit no-underline text-3xl",
1002
+ 3: "mbs-4 mbe-2 font-medium text-inherit no-underline text-2xl",
1003
+ 4: "mbs-4 mbe-2 font-medium text-inherit no-underline text-xl",
1004
+ 5: "mbs-4 mbe-2 font-medium text-inherit no-underline text-lg",
1005
+ 6: "mbs-4 mbe-2 font-medium text-inherit no-underline"
1006
+ };
1007
+ var heading = (level) => {
1008
+ return headings[level];
1004
1009
  };
1005
1010
  var light = "text-neutral-200 dark:text-neutral-800";
1006
1011
  var mark = mx("!font-normal !no-underline !text-inherit opacity-40", light);
@@ -1018,154 +1023,112 @@ import { tailwindConfig } from "@dxos/react-ui-theme";
1018
1023
  var tokens = tailwindConfig({}).theme;
1019
1024
  var getToken = (path, defaultValue = void 0) => get2(tokens, path, defaultValue);
1020
1025
 
1021
- // packages/ui/react-ui-editor/src/extensions/code.ts
1022
- var CODE_REGEX = /```[\s\S]*?```/gs;
1023
- var styles2 = EditorView4.baseTheme({
1024
- "& .cm-codeblock": {
1025
- fontFamily: getToken("fontFamily.mono", []).join(",")
1026
- },
1027
- '&light [aria-readonly="true"] .cm-codeblock': {
1028
- background: getToken("extend.colors.neutral.50")
1029
- },
1030
- '&dark [aria-readonly="true"] .cm-codeblock': {
1031
- background: getToken("extend.colors.neutral.850")
1032
- },
1033
- '& [aria-readonly="true"] .cm-codeblock': {
1034
- display: "block",
1035
- paddingInline: "4px !important"
1036
- },
1037
- '& [aria-readonly="true"] .cm-codeblock-first': {
1038
- paddingTop: "4px !important",
1039
- borderTopLeftRadius: "4px",
1040
- borderTopRightRadius: "4px"
1041
- },
1042
- '& [aria-readonly="true"] .cm-codeblock-last': {
1043
- paddingBottom: "4px !important",
1044
- borderBottomLeftRadius: "4px",
1045
- borderBottomRightRadius: "4px"
1026
+ // packages/ui/react-ui-editor/src/util.ts
1027
+ import { log } from "@dxos/log";
1028
+ var __dxlog_file3 = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/util.ts";
1029
+ var callbackWrapper = (fn) => (...args) => {
1030
+ try {
1031
+ return fn(...args);
1032
+ } catch (err) {
1033
+ log.catch(err, void 0, {
1034
+ F: __dxlog_file3,
1035
+ L: 18,
1036
+ S: void 0,
1037
+ C: (f, a) => f(...a)
1038
+ });
1046
1039
  }
1047
- });
1048
- var getLineRange = (lines, from, to) => {
1049
- const start = lines.findIndex((line) => line.from >= from);
1050
- const end = lines.findIndex((line) => line.to >= to);
1051
- return [
1052
- start,
1053
- end
1054
- ];
1055
1040
  };
1056
- var code2 = () => {
1057
- const getDecorations = (view) => {
1058
- const decorations = [];
1059
- if (view.state.readOnly) {
1060
- const text = view.state.doc.sliceString(0);
1061
- const matches = text.matchAll(CODE_REGEX);
1062
- const blocks = view.viewportLineBlocks;
1063
- for (const match of matches) {
1064
- const range = getLineRange(blocks, match.index, match.index + match[0].length);
1065
- for (let i = range[0]; i <= range[1]; i++) {
1066
- const block = blocks[i];
1067
- decorations.push(Decoration2.line({
1068
- class: mx2("cm-codeblock", i === range[0] && "cm-codeblock-first", i === range[1] && "cm-codeblock-last")
1069
- }).range(block.from));
1070
- }
1071
- }
1041
+ var logChanges = (trs) => {
1042
+ const changes = trs.flatMap((tr) => {
1043
+ if (tr.changes.empty) {
1044
+ return void 0;
1072
1045
  }
1073
- return Decoration2.set(decorations);
1074
- };
1075
- return [
1076
- ViewPlugin3.fromClass(class {
1077
- constructor(view) {
1078
- this.decorations = getDecorations(view);
1079
- }
1080
- update(update2) {
1081
- if (update2.docChanged || update2.viewportChanged) {
1082
- this.decorations = getDecorations(update2.view);
1083
- }
1084
- }
1085
- }, {
1086
- decorations: (value) => value.decorations
1087
- }),
1088
- styles2
1089
- ];
1046
+ const changes2 = [];
1047
+ tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => changes2.push(JSON.stringify({
1048
+ fromA,
1049
+ toA,
1050
+ fromB,
1051
+ toB,
1052
+ inserted: inserted.toString()
1053
+ })));
1054
+ return changes2;
1055
+ }).filter(Boolean);
1056
+ if (changes.length) {
1057
+ console.log("changes", changes);
1058
+ }
1090
1059
  };
1091
1060
 
1092
1061
  // packages/ui/react-ui-editor/src/extensions/comments.ts
1093
- import { StateEffect as StateEffect2, StateField as StateField2 } from "@codemirror/state";
1094
- import { hoverTooltip, keymap as keymap3, Decoration as Decoration3, EditorView as EditorView5, MatchDecorator, ViewPlugin as ViewPlugin4, WidgetType as WidgetType2 } from "@codemirror/view";
1095
- import sortBy from "lodash.sortby";
1096
- import { debounce } from "@dxos/async";
1097
- import { invariant as invariant2 } from "@dxos/invariant";
1098
- import { log as log2 } from "@dxos/log";
1099
- import { nonNullable } from "@dxos/util";
1100
- var __dxlog_file3 = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/extensions/comments.ts";
1101
- var styles3 = EditorView5.baseTheme({
1102
- "& .cm-bookmark": {
1103
- cursor: "pointer",
1104
- margin: "4px",
1105
- padding: "4px",
1106
- backgroundColor: "yellow"
1107
- },
1108
- "& .cm-bookmark-selected": {
1109
- backgroundColor: "orange"
1110
- },
1062
+ var __dxlog_file4 = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/extensions/comments.ts";
1063
+ var styles2 = EditorView5.baseTheme({
1111
1064
  "& .cm-comment": {
1112
1065
  backgroundColor: getToken("extend.colors.yellow.50")
1113
1066
  },
1114
- "& .cm-comment-active": {
1067
+ "& .cm-comment-current": {
1115
1068
  backgroundColor: getToken("extend.colors.yellow.100")
1116
1069
  }
1117
1070
  });
1118
1071
  var marks = {
1119
- highlight: Decoration3.mark({
1072
+ highlight: Decoration2.mark({
1120
1073
  class: "cm-comment"
1121
1074
  }),
1122
- highlightActive: Decoration3.mark({
1123
- class: "cm-comment-active"
1075
+ highlightActive: Decoration2.mark({
1076
+ class: "cm-comment-current"
1124
1077
  })
1125
1078
  };
1126
- var setFocus = (view, thread) => {
1127
- const range = view.state.field(commentsStateField).ranges.find((range2) => range2.id === thread);
1128
- if (!range) {
1079
+ var setFocus = (view, id, center = true) => {
1080
+ const comment = view.state.field(commentsState).comments.find((range2) => range2.comment.id === id);
1081
+ if (!comment?.comment.cursor) {
1129
1082
  return;
1130
1083
  }
1131
- view.dispatch({
1132
- effects: setSelection.of({
1133
- active: thread
1134
- }),
1135
- selection: {
1136
- anchor: range.from
1137
- },
1138
- scrollIntoView: true
1139
- });
1084
+ const range = Cursor.getRangeFromCursor(view.state, comment.comment.cursor);
1085
+ if (range) {
1086
+ view.dispatch({
1087
+ selection: {
1088
+ anchor: range.from
1089
+ },
1090
+ effects: [
1091
+ //
1092
+ EditorView5.scrollIntoView(range.from, center ? {
1093
+ y: "center"
1094
+ } : void 0),
1095
+ setSelection.of({
1096
+ current: id
1097
+ })
1098
+ ]
1099
+ });
1100
+ }
1140
1101
  };
1141
- var setCommentRange = StateEffect2.define();
1102
+ var setComments = StateEffect2.define();
1142
1103
  var setSelection = StateEffect2.define();
1143
1104
  var setCommentState = StateEffect2.define();
1144
- var commentsStateField = StateField2.define({
1105
+ var commentsState = StateField2.define({
1145
1106
  create: () => ({
1146
- ranges: []
1107
+ comments: [],
1108
+ selection: {}
1147
1109
  }),
1148
1110
  update: (value, tr) => {
1149
- const cursorConverter3 = tr.state.facet(CursorConverter);
1150
1111
  for (const effect of tr.effects) {
1151
1112
  if (effect.is(setSelection)) {
1152
1113
  return {
1153
1114
  ...value,
1154
- ...effect.value
1115
+ selection: effect.value
1155
1116
  };
1156
1117
  }
1157
- if (effect.is(setCommentRange)) {
1158
- const { comments: comments2 } = effect.value;
1159
- const ranges = comments2.map((comment) => {
1160
- const range = getRangeFromCursor(cursorConverter3, comment.cursor);
1118
+ if (effect.is(setComments)) {
1119
+ const comments2 = effect.value.map((comment) => {
1120
+ if (!comment.cursor) {
1121
+ return void 0;
1122
+ }
1123
+ const range = Cursor.getRangeFromCursor(tr.state, comment.cursor);
1161
1124
  return range && {
1162
- ...comment,
1163
- ...range
1125
+ comment,
1126
+ range
1164
1127
  };
1165
1128
  }).filter(nonNullable);
1166
1129
  return {
1167
1130
  ...value,
1168
- ranges
1131
+ comments: comments2
1169
1132
  };
1170
1133
  }
1171
1134
  if (effect.is(setCommentState)) {
@@ -1176,141 +1139,132 @@ var commentsStateField = StateField2.define({
1176
1139
  }
1177
1140
  });
1178
1141
  var highlightDecorations = EditorView5.decorations.compute([
1179
- commentsStateField
1142
+ commentsState
1180
1143
  ], (state) => {
1181
- const { active, ranges } = state.field(commentsStateField);
1182
- const decorations = sortBy(ranges ?? [], (range) => range.from)?.flatMap((selection) => {
1183
- if (selection.from === selection.to) {
1144
+ const { selection: { current }, comments: comments2 } = state.field(commentsState);
1145
+ const decorations = sortBy(comments2 ?? [], (range) => range.range.from)?.flatMap((comment) => {
1146
+ const range = comment.range;
1147
+ if (!range || range.from === range.to) {
1148
+ console.warn("Invalid range:", range);
1184
1149
  return void 0;
1185
1150
  }
1186
- const range = {
1187
- from: selection.from,
1188
- to: selection.to
1189
- };
1190
- if (selection.id === active) {
1151
+ if (comment.comment.id === current) {
1191
1152
  return marks.highlightActive.range(range.from, range.to);
1192
1153
  } else {
1193
1154
  return marks.highlight.range(range.from, range.to);
1194
1155
  }
1195
- }).filter(nonNullable) ?? [];
1196
- return Decoration3.set(decorations);
1156
+ }).filter(nonNullable);
1157
+ return Decoration2.set(decorations);
1197
1158
  });
1198
- var BookmarkWidget = class extends WidgetType2 {
1199
- constructor(_anchor, _id, _handleClick) {
1200
- super();
1201
- this._anchor = _anchor;
1202
- this._id = _id;
1203
- this._handleClick = _handleClick;
1204
- invariant2(this._id, void 0, {
1205
- F: __dxlog_file3,
1206
- L: 168,
1207
- S: this,
1208
- A: [
1209
- "this._id",
1210
- ""
1211
- ]
1212
- });
1213
- }
1214
- eq(other) {
1215
- return this._anchor === other._anchor && this._id === other._id;
1216
- }
1217
- toDOM() {
1218
- const span = document.createElement("span");
1219
- span.className = "cm-bookmark";
1220
- span.textContent = "\u203B";
1221
- if (this._handleClick) {
1222
- span.onclick = () => this._handleClick();
1159
+ var trackPastedComments = (onUpdate) => {
1160
+ let tracked = null;
1161
+ const handleTrack = (event, view) => {
1162
+ const comments2 = view.state.field(commentsState);
1163
+ const { main } = view.state.selection;
1164
+ const selectedRanges = comments2.comments.filter(({ range }) => range.from >= main.from && range.to <= main.to && range.from < range.to);
1165
+ if (!selectedRanges.length) {
1166
+ tracked = null;
1167
+ } else {
1168
+ tracked = {
1169
+ text: view.state.doc.slice(main.from, main.to),
1170
+ comments: selectedRanges.map(({ comment, range }) => ({
1171
+ id: comment.id,
1172
+ from: range.from - main.from,
1173
+ to: range.to - main.from
1174
+ }))
1175
+ };
1223
1176
  }
1224
- return span;
1225
- }
1177
+ };
1178
+ return [
1179
+ EditorView5.domEventHandlers({
1180
+ cut: handleTrack,
1181
+ copy: handleTrack
1182
+ }),
1183
+ // Handle paste.
1184
+ EditorView5.updateListener.of(({ view, state, transactions }) => {
1185
+ if (tracked) {
1186
+ const paste = transactions.find((tr) => tr.isUserEvent("input.paste"));
1187
+ if (paste) {
1188
+ let found = -1;
1189
+ paste.changes.iterChanges((fromA, toA, fromB, toB, text) => {
1190
+ if (text.eq(tracked.text)) {
1191
+ for (let i = transactions.indexOf(paste) + 1; i < transactions.length; i++) {
1192
+ fromB = transactions[i].changes.mapPos(fromB);
1193
+ }
1194
+ found = fromB;
1195
+ }
1196
+ });
1197
+ if (found > -1) {
1198
+ for (const moved of tracked.comments) {
1199
+ const range = {
1200
+ from: found + moved.from,
1201
+ to: found + moved.to
1202
+ };
1203
+ const cursor = Cursor.getCursorFromRange(state, range);
1204
+ onUpdate(moved.id, cursor);
1205
+ }
1206
+ }
1207
+ tracked = null;
1208
+ }
1209
+ }
1210
+ })
1211
+ ];
1226
1212
  };
1227
1213
  var comments = (options = {}) => {
1214
+ const { key: shortcut = "meta-'" } = options;
1228
1215
  const handleSelect = debounce((state) => options.onSelect?.(state), 200);
1229
1216
  const createCommentThread = (view) => {
1230
- const cursorConverter3 = view.state.facet(CursorConverter);
1231
- invariant2(options.onCreate, void 0, {
1232
- F: __dxlog_file3,
1233
- L: 224,
1217
+ invariant3(options.onCreate, void 0, {
1218
+ F: __dxlog_file4,
1219
+ L: 259,
1234
1220
  S: void 0,
1235
1221
  A: [
1236
1222
  "options.onCreate",
1237
1223
  ""
1238
1224
  ]
1239
1225
  });
1240
- const { head, from, to } = view.state.selection.main;
1226
+ const { from, to } = view.state.selection.main;
1241
1227
  if (from === to) {
1242
1228
  return false;
1243
1229
  }
1244
- const relPos = getCursorFromRange(cursorConverter3, {
1230
+ if (to === view.state.doc.length) {
1231
+ view.dispatch({
1232
+ changes: {
1233
+ from: to,
1234
+ insert: "\n"
1235
+ }
1236
+ });
1237
+ }
1238
+ const cursor = Cursor.getCursorFromRange(view.state, {
1245
1239
  from,
1246
1240
  to
1247
1241
  });
1248
- if (relPos) {
1249
- const id = callbackWrapper(options.onCreate)(relPos);
1242
+ if (cursor) {
1243
+ const id = options.onCreate?.(cursor, view.coordsAtPos(from));
1250
1244
  if (id) {
1251
1245
  view.dispatch({
1252
1246
  effects: setSelection.of({
1253
- active: id
1247
+ current: id
1254
1248
  }),
1255
1249
  selection: {
1256
1250
  anchor: from
1257
1251
  }
1258
1252
  });
1259
- if (options.footnote) {
1260
- const tag = `[^${id}]`;
1261
- view.dispatch({
1262
- changes: {
1263
- from: head,
1264
- insert: tag
1265
- },
1266
- selection: {
1267
- anchor: head + tag.length
1268
- }
1269
- });
1270
- }
1271
1253
  return true;
1272
1254
  }
1273
1255
  }
1274
1256
  return false;
1275
1257
  };
1276
- const bookmarksViewPlugin = ViewPlugin4.fromClass(class BookmarkViewPlugin {
1277
- static {
1278
- // Match markdown footnotes (option).
1279
- // https://www.markdownguide.org/extended-syntax/#footnotes
1280
- this.bookmarkMatcher = new MatchDecorator({
1281
- regexp: /\[\^(\w+)\]/g,
1282
- decoration: (match, view, pos) => {
1283
- const id = match[1];
1284
- return Decoration3.replace({
1285
- id,
1286
- widget: new BookmarkWidget(pos, id)
1287
- });
1288
- }
1289
- });
1290
- }
1291
- constructor(view) {
1292
- this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.createDeco(view);
1293
- }
1294
- update(update2) {
1295
- this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.updateDeco(update2, this.bookmarks);
1296
- }
1297
- }, {
1298
- decorations: (instance) => instance.bookmarks,
1299
- provide: (plugin) => EditorView5.atomicRanges.of((view) => {
1300
- return view.plugin(plugin)?.bookmarks || Decoration3.none;
1301
- })
1302
- });
1303
1258
  return [
1304
- bookmarksViewPlugin,
1305
- commentsStateField,
1259
+ commentsState,
1306
1260
  highlightDecorations,
1307
- styles3,
1261
+ styles2,
1308
1262
  //
1309
1263
  // Keymap.
1310
1264
  //
1311
1265
  options.onCreate ? keymap3.of([
1312
1266
  {
1313
- key: options?.key ?? "meta-'",
1267
+ key: shortcut,
1314
1268
  run: callbackWrapper(createCommentThread)
1315
1269
  }
1316
1270
  ]) : [],
@@ -1327,7 +1281,7 @@ var comments = (options = {}) => {
1327
1281
  above: true,
1328
1282
  create: () => {
1329
1283
  const el = document.createElement("div");
1330
- options.onHover?.(el);
1284
+ options.onHover(el, shortcut);
1331
1285
  return {
1332
1286
  dom: el,
1333
1287
  offset: {
@@ -1340,384 +1294,118 @@ var comments = (options = {}) => {
1340
1294
  }
1341
1295
  return null;
1342
1296
  }, {
1343
- hideOnChange: true,
1297
+ // TODO(burdon): Hide on change triggered immediately?
1298
+ // hideOnChange: true,
1344
1299
  hoverTime: 1e3
1345
1300
  }) : [],
1346
1301
  //
1347
- // Monitor cursor movement and text updates.
1348
- // TODO(burdon): Is there a better (finer grained) way to do this?
1302
+ // Track deleted ranges and update ranges for decorations.
1349
1303
  //
1350
1304
  EditorView5.updateListener.of(({ view, state, changes }) => {
1351
- const cursorConverter3 = view.state.facet(CursorConverter);
1352
- {
1353
- let mod = false;
1354
- const { active, ranges } = state.field(commentsStateField);
1355
- changes.iterChanges((from, to, from2, to2) => {
1356
- ranges.forEach((range) => {
1357
- if (from2 === to2) {
1358
- const newRange = getRangeFromCursor(cursorConverter3, range.cursor);
1359
- if (!newRange || newRange.to - newRange.from === 0) {
1360
- log2.info("deleted comment", {
1361
- thread: range.id
1362
- }, {
1363
- F: __dxlog_file3,
1364
- L: 358,
1365
- S: void 0,
1366
- C: (f, a) => f(...a)
1367
- });
1368
- }
1305
+ let mod = false;
1306
+ const { comments: comments2, ...value } = state.field(commentsState);
1307
+ changes.iterChanges((from, to, from2, to2) => {
1308
+ comments2.forEach(({ comment, range }) => {
1309
+ if (from2 === to2) {
1310
+ const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor);
1311
+ if (!newRange || newRange.to - newRange.from === 0) {
1312
+ options.onDelete?.(comment.id);
1369
1313
  }
1370
- if (from <= range.to) {
1371
- const newRange = getRangeFromCursor(cursorConverter3, range.cursor);
1372
- Object.assign(range, newRange);
1373
- mod = true;
1374
- }
1375
- });
1314
+ }
1315
+ if (from <= range.to) {
1316
+ const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor);
1317
+ Object.assign(range, newRange);
1318
+ mod = true;
1319
+ }
1320
+ });
1321
+ });
1322
+ if (mod) {
1323
+ view.dispatch({
1324
+ effects: setCommentState.of({
1325
+ comments: comments2,
1326
+ ...value
1327
+ })
1376
1328
  });
1377
- if (mod) {
1378
- view.dispatch({
1379
- effects: setCommentState.of({
1380
- active,
1381
- ranges
1382
- })
1383
- });
1384
- }
1385
1329
  }
1386
- {
1387
- const { head } = state.selection.main;
1388
- let min = Infinity;
1389
- const { active, closest, ranges } = state.field(commentsStateField);
1390
- const selected = {
1391
- active: void 0,
1392
- closest: void 0
1393
- };
1394
- ranges.forEach((comment) => {
1395
- const d = Math.min(Math.abs(head - comment.from), Math.abs(head - comment.to));
1396
- if (head >= comment.from && head <= comment.to) {
1397
- selected.active = comment.id;
1398
- }
1330
+ }),
1331
+ //
1332
+ // Track selection/proximity.
1333
+ //
1334
+ EditorView5.updateListener.of(({ view, state }) => {
1335
+ let min = Infinity;
1336
+ const { selection: { current, closest }, comments: comments2 } = state.field(commentsState);
1337
+ const { head } = state.selection.main;
1338
+ const selection = {};
1339
+ comments2.forEach(({ comment, range }) => {
1340
+ if (head >= range.from && head <= range.to) {
1341
+ selection.current = comment.id;
1342
+ selection.closest = void 0;
1343
+ }
1344
+ if (!selection.current) {
1345
+ const d = Math.min(Math.abs(head - range.from), Math.abs(head - range.to));
1399
1346
  if (d < min) {
1400
- selected.closest = comment.id;
1347
+ selection.closest = comment.id;
1401
1348
  min = d;
1402
1349
  }
1403
- });
1404
- if (selected.active !== active || selected.closest !== closest) {
1405
- view.dispatch({
1406
- effects: setSelection.of(selected)
1407
- });
1408
- handleSelect({
1409
- ...selected,
1410
- ranges: ranges.map((range) => ({
1411
- ...range,
1412
- location: view.coordsAtPos(range.from)
1413
- }))
1414
- });
1415
1350
  }
1351
+ });
1352
+ if (selection.current !== current || selection.closest !== closest) {
1353
+ view.dispatch({
1354
+ effects: setSelection.of(selection)
1355
+ });
1356
+ handleSelect({
1357
+ selection,
1358
+ comments: comments2.map(({ comment, range }) => ({
1359
+ comment,
1360
+ range,
1361
+ location: view.coordsAtPos(range.from)
1362
+ }))
1363
+ });
1416
1364
  }
1417
- })
1365
+ }),
1366
+ options.onUpdate ? trackPastedComments(options.onUpdate) : []
1418
1367
  ];
1419
1368
  };
1420
- var getCursorFromRange = (cursorConverter3, range) => {
1421
- const from = cursorConverter3.toCursor(range.from);
1422
- const to = cursorConverter3.toCursor(range.to, -1);
1423
- return [
1424
- from,
1425
- to
1426
- ].join(":");
1369
+ var useComments = (view, comments2 = []) => {
1370
+ useEffect(() => {
1371
+ if (view) {
1372
+ view.dispatch({
1373
+ effects: setComments.of(comments2)
1374
+ });
1375
+ }
1376
+ }, [
1377
+ view,
1378
+ comments2
1379
+ ]);
1427
1380
  };
1428
- var getRangeFromCursor = (cursorConverter3, cursor) => {
1429
- const parts = cursor.split(":");
1430
- const from = cursorConverter3.fromCursor(parts[0]);
1431
- const to = cursorConverter3.fromCursor(parts[1]);
1432
- return from !== void 0 && to !== void 0 ? {
1433
- from,
1434
- to
1435
- } : void 0;
1436
- };
1437
-
1438
- // packages/ui/react-ui-editor/src/extensions/hr.ts
1439
- import { Decoration as Decoration4, EditorView as EditorView6, MatchDecorator as MatchDecorator2, ViewPlugin as ViewPlugin5, WidgetType as WidgetType3 } from "@codemirror/view";
1440
- var styles4 = EditorView6.baseTheme({
1441
- "& .cm-hr": {
1442
- // TODO(burdon): ???
1443
- // Note that block-level decorations should not have vertical margins,
1444
- borderBottom: `1px solid ${getToken("extend.colors.neutral.200")}`
1445
- }
1446
- });
1447
- var HorizontalRuleWidget = class extends WidgetType3 {
1448
- constructor(_pos) {
1449
- super();
1450
- this._pos = _pos;
1451
- }
1452
- eq(other) {
1453
- return this._pos === other._pos;
1454
- }
1455
- toDOM(view) {
1456
- const el = document.createElement("div");
1457
- el.className = "cm-hr";
1458
- return el;
1459
- }
1460
- };
1461
- var placeholderMatcher = new MatchDecorator2({
1462
- regexp: /^---$/g,
1463
- decoration: (match, view, pos) => Decoration4.replace({
1464
- widget: new HorizontalRuleWidget(pos)
1465
- })
1466
- });
1467
- var hr = () => [
1468
- styles4,
1469
- ViewPlugin5.fromClass(class {
1470
- constructor(view) {
1471
- this.rules = placeholderMatcher.createDeco(view);
1472
- }
1473
- update(update2) {
1474
- this.rules = placeholderMatcher.updateDeco(update2, this.rules);
1475
- }
1476
- }, {
1477
- decorations: (instance) => instance.rules,
1478
- // TODO(burdon): Is this really atomic (cursor can move into ---).
1479
- provide: (plugin) => EditorView6.atomicRanges.of((view) => {
1480
- return view.plugin(plugin)?.rules || Decoration4.none;
1481
- })
1482
- })
1483
- ];
1484
-
1485
- // packages/ui/react-ui-editor/src/extensions/image.ts
1486
- import { syntaxTree } from "@codemirror/language";
1487
- import { StateField as StateField3 } from "@codemirror/state";
1488
- import { Decoration as Decoration5, EditorView as EditorView7, WidgetType as WidgetType4 } from "@codemirror/view";
1489
- var image = (options = {}) => {
1490
- return StateField3.define({
1491
- create: (state) => {
1492
- return Decoration5.set(buildDecorations(0, state.doc.length, state));
1493
- },
1494
- update: (value, tr) => {
1495
- if (!tr.docChanged && !tr.selection) {
1496
- return value;
1497
- }
1498
- const cursor = tr.state.selection.main.head;
1499
- const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);
1500
- let from = Math.min(cursor, oldCursor);
1501
- let to = Math.max(cursor, oldCursor);
1502
- tr.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
1503
- from = Math.min(from, fromB);
1504
- to = Math.max(to, toB);
1505
- });
1506
- from = tr.state.doc.lineAt(from).from;
1507
- to = tr.state.doc.lineAt(to).to;
1508
- return value.map(tr.changes).update({
1509
- filterFrom: from,
1510
- filterTo: to,
1511
- filter: () => false,
1512
- add: buildDecorations(from, to, tr.state)
1513
- });
1514
- },
1515
- provide: (field) => EditorView7.decorations.from(field)
1516
- });
1517
- };
1518
- var buildDecorations = (from, to, state) => {
1519
- const decorations = [];
1520
- const cursor = state.selection.main.head;
1521
- syntaxTree(state).iterate({
1522
- enter: (node) => {
1523
- if (node.name === "Image") {
1524
- const urlNode = node.node.getChild("URL");
1525
- if (urlNode) {
1526
- const hide = state.readOnly || cursor < node.from || cursor > node.to;
1527
- const url = state.sliceDoc(urlNode.from, urlNode.to);
1528
- decorations.push(Decoration5.replace({
1529
- block: true,
1530
- widget: new ImageWidget(url)
1531
- }).range(hide ? node.from : node.to, node.to));
1532
- }
1533
- }
1534
- },
1535
- from,
1536
- to
1537
- });
1538
- return decorations;
1539
- };
1540
- var ImageWidget = class extends WidgetType4 {
1541
- constructor(_url) {
1542
- super();
1543
- this._url = _url;
1544
- }
1545
- eq(other) {
1546
- return this._url === other._url;
1547
- }
1548
- toDOM(view) {
1549
- const img = document.createElement("img");
1550
- img.setAttribute("src", this._url);
1551
- img.setAttribute("class", "cm-image");
1552
- img.onload = () => img.classList.add("cm-loaded-image");
1553
- return img;
1554
- }
1555
- };
1556
-
1557
- // packages/ui/react-ui-editor/src/extensions/link.ts
1558
- import { syntaxTree as syntaxTree2 } from "@codemirror/language";
1559
- import { RangeSetBuilder } from "@codemirror/state";
1560
- import { Decoration as Decoration6, WidgetType as WidgetType5, hoverTooltip as hoverTooltip2, ViewPlugin as ViewPlugin6 } from "@codemirror/view";
1561
- import { tooltipContent } from "@dxos/react-ui-theme";
1562
- var link = (options = {}) => {
1563
- const extensions = [
1564
- ViewPlugin6.fromClass(class {
1565
- constructor(view) {
1566
- this.decorations = buildDecorations2(view, options);
1567
- }
1568
- update(update2) {
1569
- if (update2.docChanged || update2.viewportChanged || update2.selectionSet) {
1570
- this.decorations = buildDecorations2(update2.view, options);
1571
- }
1572
- }
1573
- }, {
1574
- decorations: (v) => v.decorations
1575
- })
1576
- ];
1577
- if (options.onHover) {
1578
- extensions.push(
1579
- // https://codemirror.net/examples/tooltip
1580
- // https://codemirror.net/docs/ref/#view.hoverTooltip
1581
- // https://github.com/codemirror/view/blob/main/src/tooltip.ts
1582
- hoverTooltip2((view, pos, side) => {
1583
- const syntax = syntaxTree2(view.state).resolveInner(pos, side);
1584
- let link2 = null;
1585
- for (let i = 0, node = syntax; !link2 && node && i < 5; node = node.parent, i++) {
1586
- link2 = node.name === "Link" ? node : null;
1587
- }
1588
- const url = link2 && link2.getChild("URL");
1589
- if (!url || !link2) {
1590
- return null;
1591
- }
1592
- const urlText = view.state.sliceDoc(url.from, url.to);
1593
- return {
1594
- pos: link2.from,
1595
- end: link2.to,
1596
- above: true,
1597
- create: () => {
1598
- const el = document.createElement("div");
1599
- el.className = tooltipContent({}, "pli-2 plb-1");
1600
- options.onHover(el, urlText);
1601
- return {
1602
- dom: el,
1603
- offset: {
1604
- x: 0,
1605
- y: 4
1606
- }
1607
- };
1608
- }
1609
- };
1610
- })
1611
- );
1612
- }
1613
- return extensions;
1614
- };
1615
- var LinkButton = class extends WidgetType5 {
1616
- constructor(_url, _onAttach) {
1617
- super();
1618
- this._url = _url;
1619
- this._onAttach = _onAttach;
1620
- }
1621
- eq(other) {
1622
- return this._url === other._url;
1623
- }
1624
- toDOM(view) {
1625
- const el = document.createElement("span");
1626
- this._onAttach(el, this._url);
1627
- return el;
1628
- }
1629
- };
1630
- var LinkText = class extends WidgetType5 {
1631
- constructor(_text, _url) {
1632
- super();
1633
- this._text = _text;
1634
- this._url = _url;
1635
- }
1636
- eq(other) {
1637
- return this._url === other._url;
1638
- }
1639
- toDOM(view) {
1640
- const link2 = document.createElement("a");
1641
- link2.setAttribute("class", "cm-link");
1642
- link2.textContent = this._text;
1643
- if (this._url) {
1644
- link2.setAttribute("rel", "noreferrer");
1645
- link2.setAttribute("target", "_blank");
1646
- link2.href = this._url;
1647
- } else {
1648
- link2.onclick = () => {
1649
- view.dispatch({
1650
- selection: {
1651
- anchor: view.posAtDOM(link2)
1652
- }
1653
- });
1654
- };
1655
- }
1656
- return link2;
1657
- }
1658
- };
1659
- var buildDecorations2 = (view, options) => {
1660
- const builder = new RangeSetBuilder();
1661
- const { state } = view;
1662
- const cursor = state.selection.main.head;
1663
- for (const { from, to } of view.visibleRanges) {
1664
- syntaxTree2(state).iterate({
1665
- enter: (node) => {
1666
- if (node.name === "Link") {
1667
- const marks2 = node.node.getChildren("LinkMark");
1668
- const text = marks2.length >= 2 ? state.sliceDoc(marks2[0].to, marks2[1].from) : "";
1669
- const urlNode = node.node.getChild("URL");
1670
- const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : "";
1671
- if (!url) {
1672
- return false;
1673
- }
1674
- if (!view.hasFocus || state.readOnly || cursor < node.from || cursor > node.to) {
1675
- builder.add(node.from, node.to, Decoration6.replace({
1676
- widget: new LinkText(text, options.link ? url : void 0)
1677
- }));
1678
- }
1679
- if (options.onRender) {
1680
- builder.add(node.to, node.to, Decoration6.replace({
1681
- widget: new LinkButton(url, options.onRender)
1682
- }));
1683
- }
1684
- return false;
1685
- }
1686
- },
1687
- from,
1688
- to
1689
- });
1690
- }
1691
- return builder.finish();
1692
- };
1693
-
1694
- // packages/ui/react-ui-editor/src/extensions/listener.ts
1695
- import { EditorView as EditorView8 } from "@codemirror/view";
1696
- var listener = ({ onFocus, onChange }) => {
1697
- const extensions = [];
1698
- onFocus && extensions.push(EditorView8.focusChangeEffect.of((_, focused) => {
1699
- onFocus(focused);
1700
- return null;
1701
- }));
1702
- onChange && extensions.push(EditorView8.updateListener.of((update2) => {
1703
- onChange(update2.state.doc.toString());
1704
- }));
1705
- return extensions;
1381
+
1382
+ // packages/ui/react-ui-editor/src/extensions/listener.ts
1383
+ import { EditorView as EditorView6 } from "@codemirror/view";
1384
+ var listener = ({ onFocus, onChange }) => {
1385
+ const extensions = [];
1386
+ onFocus && extensions.push(EditorView6.focusChangeEffect.of((_, focused) => {
1387
+ onFocus(focused);
1388
+ return null;
1389
+ }));
1390
+ onChange && extensions.push(EditorView6.updateListener.of((update2) => {
1391
+ onChange(update2.state.doc.toString());
1392
+ }));
1393
+ return extensions;
1706
1394
  };
1707
1395
 
1708
1396
  // packages/ui/react-ui-editor/src/extensions/markdown/bundle.ts
1709
1397
  import { closeBrackets as closeBrackets2, closeBracketsKeymap } from "@codemirror/autocomplete";
1710
- import { history, historyKeymap, indentWithTab, standardKeymap } from "@codemirror/commands";
1711
- import { markdownLanguage as markdownLanguage2, markdown } from "@codemirror/lang-markdown";
1398
+ import { history as history2, historyKeymap, indentWithTab, standardKeymap } from "@codemirror/commands";
1399
+ import { markdownLanguage as markdownLanguage3, markdown } from "@codemirror/lang-markdown";
1712
1400
  import { bracketMatching as bracketMatching2, defaultHighlightStyle as defaultHighlightStyle2, indentOnInput, syntaxHighlighting as syntaxHighlighting2 } from "@codemirror/language";
1713
1401
  import { languages } from "@codemirror/language-data";
1714
1402
  import { searchKeymap } from "@codemirror/search";
1715
1403
  import { EditorState } from "@codemirror/state";
1716
1404
  import { oneDarkHighlightStyle as oneDarkHighlightStyle2 } from "@codemirror/theme-one-dark";
1717
- import { crosshairCursor, drawSelection as drawSelection2, dropCursor, EditorView as EditorView9, highlightActiveLine, keymap as keymap4, placeholder as placeholder2 } from "@codemirror/view";
1405
+ import { crosshairCursor, drawSelection as drawSelection2, dropCursor, EditorView as EditorView7, highlightActiveLine as highlightActiveLine2, keymap as keymap4, placeholder as placeholder2 } from "@codemirror/view";
1718
1406
 
1719
1407
  // packages/ui/react-ui-editor/src/extensions/markdown/highlight.ts
1720
- import { markdownLanguage } from "@codemirror/lang-markdown";
1408
+ import { markdownLanguage as markdownLanguage2 } from "@codemirror/lang-markdown";
1721
1409
  import { HighlightStyle } from "@codemirror/language";
1722
1410
  import { tags, styleTags, Tag } from "@lezer/highlight";
1723
1411
  import { Table } from "@lezer/markdown";
@@ -1727,7 +1415,6 @@ var markdownTags = {
1727
1415
  CodeText: Tag.define(),
1728
1416
  EmphasisMark: Tag.define(),
1729
1417
  HeaderMark: Tag.define(),
1730
- // HorizontalRule: Tag.define(),
1731
1418
  InlineCode: Tag.define(),
1732
1419
  LinkLabel: Tag.define(),
1733
1420
  LinkReference: Tag.define(),
@@ -1812,7 +1499,7 @@ var markdownHighlightStyle = (readonly) => {
1812
1499
  markdownTags.QuoteMark,
1813
1500
  markdownTags.EmphasisMark
1814
1501
  ],
1815
- class: readonly ? "hidden" : mark
1502
+ class: mark
1816
1503
  },
1817
1504
  // E.g., code block language (after ```).
1818
1505
  {
@@ -1820,7 +1507,7 @@ var markdownHighlightStyle = (readonly) => {
1820
1507
  tags.function(tags.variableName),
1821
1508
  tags.labelName
1822
1509
  ],
1823
- class: readonly ? "hidden" : codeMark
1510
+ class: codeMark
1824
1511
  },
1825
1512
  {
1826
1513
  tag: [
@@ -1831,27 +1518,27 @@ var markdownHighlightStyle = (readonly) => {
1831
1518
  // Headings.
1832
1519
  {
1833
1520
  tag: tags.heading1,
1834
- class: heading(1, readonly)
1521
+ class: heading(1)
1835
1522
  },
1836
1523
  {
1837
1524
  tag: tags.heading2,
1838
- class: heading(2, readonly)
1525
+ class: heading(2)
1839
1526
  },
1840
1527
  {
1841
1528
  tag: tags.heading3,
1842
- class: heading(3, readonly)
1529
+ class: heading(3)
1843
1530
  },
1844
1531
  {
1845
1532
  tag: tags.heading4,
1846
- class: heading(4, readonly)
1533
+ class: heading(4)
1847
1534
  },
1848
1535
  {
1849
1536
  tag: tags.heading5,
1850
- class: heading(5, readonly)
1537
+ class: heading(5)
1851
1538
  },
1852
1539
  {
1853
1540
  tag: tags.heading6,
1854
- class: heading(6, readonly)
1541
+ class: heading(6)
1855
1542
  },
1856
1543
  // Emphasis.
1857
1544
  {
@@ -1877,7 +1564,7 @@ var markdownHighlightStyle = (readonly) => {
1877
1564
  // However, since `codeLanguages` is also defined, the `lezer` will not parse fenced code blocks,
1878
1565
  // when a language is specified. In this case, the syntax highlighting extensions will colorize
1879
1566
  // the code, but all other CSS properties will be inherited.
1880
- // IMPORTANT: Therefore, the fenced code block will use the base editor font.
1567
+ // IMPORTANT: Therefore, the fenced code block will use the base editor font unless changed by an extension.
1881
1568
  {
1882
1569
  tag: [
1883
1570
  markdownTags.CodeText,
@@ -1891,12 +1578,6 @@ var markdownHighlightStyle = (readonly) => {
1891
1578
  ],
1892
1579
  class: blockquote
1893
1580
  },
1894
- // TODO(burdon): Replace with extension.
1895
- // {
1896
- // tag: [markdownTags.HorizontalRule],
1897
- // class: mx(horizontalRule, readonly && '-indent-[100rem]'),
1898
- // },
1899
- // TODO(burdon): Only if being edited.
1900
1581
  {
1901
1582
  tag: [
1902
1583
  markdownTags.TableCell
@@ -1904,7 +1585,7 @@ var markdownHighlightStyle = (readonly) => {
1904
1585
  class: "font-mono"
1905
1586
  }
1906
1587
  ], {
1907
- scope: markdownLanguage,
1588
+ scope: markdownLanguage2,
1908
1589
  all: {
1909
1590
  fontFamily: getToken("fontFamily.body", []).join(",")
1910
1591
  }
@@ -1912,21 +1593,21 @@ var markdownHighlightStyle = (readonly) => {
1912
1593
  };
1913
1594
 
1914
1595
  // packages/ui/react-ui-editor/src/extensions/markdown/bundle.ts
1915
- var markdownBundle = ({ readonly, themeMode, placeholder: _placeholder }) => {
1596
+ var markdownBundle = ({ themeMode, placeholder: _placeholder }) => {
1916
1597
  return [
1917
1598
  EditorState.allowMultipleSelections.of(true),
1918
1599
  EditorState.tabSize.of(2),
1919
- EditorView9.lineWrapping,
1920
- customKeymap(),
1600
+ EditorView7.lineWrapping,
1601
+ // https://github.com/codemirror/basic-setup
1921
1602
  bracketMatching2(),
1922
1603
  closeBrackets2(),
1923
1604
  crosshairCursor(),
1924
1605
  dropCursor(),
1925
1606
  drawSelection2(),
1926
- highlightActiveLine(),
1927
- history(),
1607
+ highlightActiveLine2(),
1608
+ history2(),
1928
1609
  indentOnInput(),
1929
- _placeholder && placeholder2(_placeholder),
1610
+ _placeholder ? placeholder2(_placeholder) : [],
1930
1611
  // Main extension.
1931
1612
  // https://github.com/codemirror/lang-markdown
1932
1613
  // https://codemirror.net/5/mode/markdown/index.html (demo).
@@ -1936,7 +1617,7 @@ var markdownBundle = ({ readonly, themeMode, placeholder: _placeholder }) => {
1936
1617
  // NOTE: This extends the parser; it doesn't affect rendering.
1937
1618
  // https://github.github.com/gfm
1938
1619
  // https://github.com/lezer-parser/markdown?tab=readme-ov-file#github-flavored-markdown
1939
- base: markdownLanguage2,
1620
+ base: markdownLanguage3,
1940
1621
  // Languages for syntax highlighting fenced code blocks.
1941
1622
  codeLanguages: languages,
1942
1623
  // Parser extensions.
@@ -1948,58 +1629,445 @@ var markdownBundle = ({ readonly, themeMode, placeholder: _placeholder }) => {
1948
1629
  // https://github.com/codemirror/theme-one-dark
1949
1630
  themeMode === "dark" ? syntaxHighlighting2(oneDarkHighlightStyle2) : syntaxHighlighting2(defaultHighlightStyle2),
1950
1631
  // Custom styles.
1951
- syntaxHighlighting2(markdownHighlightStyle(readonly))
1952
- ].filter(Boolean);
1953
- };
1954
- var customKeymap = () => keymap4.of([
1955
- // https://codemirror.net/docs/ref/#commands.indentWithTab
1956
- indentWithTab,
1957
- // https://codemirror.net/docs/ref/#commands.standardKeymap
1958
- ...standardKeymap,
1959
- // https://codemirror.net/docs/ref/#commands.historyKeymap
1960
- ...historyKeymap,
1961
- // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
1962
- ...closeBracketsKeymap,
1963
- // https://codemirror.net/docs/ref/#search.searchKeymap
1964
- ...searchKeymap
1965
- ]);
1632
+ syntaxHighlighting2(markdownHighlightStyle()),
1633
+ keymap4.of([
1634
+ // https://codemirror.net/docs/ref/#commands.indentWithTab
1635
+ indentWithTab,
1636
+ // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
1637
+ ...closeBracketsKeymap,
1638
+ // https://codemirror.net/docs/ref/#commands.historyKeymap
1639
+ ...historyKeymap,
1640
+ // https://codemirror.net/docs/ref/#search.searchKeymap
1641
+ ...searchKeymap,
1642
+ // https://codemirror.net/docs/ref/#commands.standardKeymap
1643
+ ...standardKeymap
1644
+ ])
1645
+ ];
1646
+ };
1966
1647
 
1967
- // packages/ui/react-ui-editor/src/extensions/mention.ts
1968
- import { autocompletion as autocompletion2 } from "@codemirror/autocomplete";
1969
- var mention = ({ onSearch }) => {
1970
- return autocompletion2({
1971
- closeOnBlur: false,
1972
- override: [
1973
- (context) => {
1974
- const match = context.matchBefore(/@(\w+)?/);
1975
- if (!match || match.from === match.to && !context.explicit) {
1976
- return null;
1648
+ // packages/ui/react-ui-editor/src/extensions/markdown/code.ts
1649
+ import { syntaxTree } from "@codemirror/language";
1650
+ import { RangeSetBuilder } from "@codemirror/state";
1651
+ import { ViewPlugin as ViewPlugin3, EditorView as EditorView8, Decoration as Decoration3, WidgetType as WidgetType2 } from "@codemirror/view";
1652
+ import { mx as mx2 } from "@dxos/react-ui-theme";
1653
+ var styles3 = EditorView8.baseTheme({
1654
+ "& .cm-code": {
1655
+ paddingInline: "1rem !important",
1656
+ fontFamily: getToken("fontFamily.mono", []).join(",")
1657
+ },
1658
+ "& .cm-codeblock": {
1659
+ display: "inline-block",
1660
+ width: "100%"
1661
+ },
1662
+ "&light .cm-codeblock, &light .cm-codeblock.cm-activeLine": {
1663
+ background: getToken("extend.colors.neutral.25")
1664
+ },
1665
+ "&dark .cm-codeblock, &dark .cm-codeblock.cm-activeLine": {
1666
+ background: getToken("extend.colors.neutral.850")
1667
+ },
1668
+ "& .cm-codeblock-first": {
1669
+ borderTopLeftRadius: ".5rem",
1670
+ borderTopRightRadius: ".5rem"
1671
+ },
1672
+ "& .cm-codeblock-last": {
1673
+ borderBottomLeftRadius: ".5rem",
1674
+ borderBottomRightRadius: ".5rem"
1675
+ }
1676
+ });
1677
+ var getLineRange = (lines, from, to) => {
1678
+ const start = lines.findIndex((line) => line.from >= from);
1679
+ const end = lines.findIndex((line) => line.to >= to);
1680
+ return [
1681
+ start,
1682
+ end
1683
+ ];
1684
+ };
1685
+ var LineWidget = class extends WidgetType2 {
1686
+ constructor(_className) {
1687
+ super();
1688
+ this._className = _className;
1689
+ }
1690
+ toDOM() {
1691
+ const el = document.createElement("span");
1692
+ el.innerHTML = "&nbsp;";
1693
+ el.className = mx2("cm-code cm-codeblock", this._className);
1694
+ return el;
1695
+ }
1696
+ };
1697
+ var top = new LineWidget("cm-codeblock-first");
1698
+ var bottom = new LineWidget("cm-codeblock-last");
1699
+ var buildDecorations = (view) => {
1700
+ const builder = new RangeSetBuilder();
1701
+ const { state } = view;
1702
+ const blocks = view.viewportLineBlocks;
1703
+ const cursor = state.selection.main.head;
1704
+ for (const { from, to } of view.visibleRanges) {
1705
+ syntaxTree(state).iterate({
1706
+ enter: (node) => {
1707
+ if (node.name === "FencedCode") {
1708
+ const edit = !view.state.readOnly && cursor >= node.from && cursor <= node.to;
1709
+ const range = getLineRange(blocks, node.from, node.to);
1710
+ for (let i = range[0]; i <= range[1]; i++) {
1711
+ const block = blocks[i];
1712
+ if (!edit && (i === range[0] || i === range[1])) {
1713
+ builder.add(block.from, block.to, Decoration3.replace({
1714
+ widget: i === range[0] ? top : bottom
1715
+ }));
1716
+ } else {
1717
+ builder.add(block.from, block.from, Decoration3.line({
1718
+ class: mx2("cm-code cm-codeblock")
1719
+ }));
1720
+ }
1721
+ }
1722
+ }
1723
+ },
1724
+ from,
1725
+ to
1726
+ });
1727
+ }
1728
+ return builder.finish();
1729
+ };
1730
+ var code2 = () => {
1731
+ return [
1732
+ ViewPlugin3.fromClass(class {
1733
+ constructor(view) {
1734
+ this.decorations = buildDecorations(view);
1735
+ }
1736
+ update(update2) {
1737
+ this.decorations = buildDecorations(update2.view);
1738
+ }
1739
+ }, {
1740
+ decorations: (value) => value.decorations
1741
+ }),
1742
+ styles3
1743
+ ];
1744
+ };
1745
+
1746
+ // packages/ui/react-ui-editor/src/extensions/markdown/heading.ts
1747
+ import { syntaxTree as syntaxTree2 } from "@codemirror/language";
1748
+ import { RangeSetBuilder as RangeSetBuilder2 } from "@codemirror/state";
1749
+ import { Decoration as Decoration4, ViewPlugin as ViewPlugin4 } from "@codemirror/view";
1750
+ var buildDecorations2 = (view) => {
1751
+ const builder = new RangeSetBuilder2();
1752
+ const { state } = view;
1753
+ const cursor = state.selection.main.head;
1754
+ for (const { from, to } of view.visibleRanges) {
1755
+ syntaxTree2(state).iterate({
1756
+ enter: (node) => {
1757
+ switch (node.name) {
1758
+ case "ATXHeading1":
1759
+ case "ATXHeading2":
1760
+ case "ATXHeading3":
1761
+ case "ATXHeading4":
1762
+ case "ATXHeading5":
1763
+ case "ATXHeading6": {
1764
+ const mark2 = node.node.getChild("HeaderMark");
1765
+ if (mark2) {
1766
+ if (view.state.readOnly || cursor < node.from || cursor > node.to) {
1767
+ builder.add(mark2.from, mark2.to + 1, Decoration4.replace({}));
1768
+ }
1769
+ }
1770
+ }
1771
+ }
1772
+ },
1773
+ from,
1774
+ to
1775
+ });
1776
+ }
1777
+ return builder.finish();
1778
+ };
1779
+ var heading2 = () => {
1780
+ return [
1781
+ ViewPlugin4.fromClass(class {
1782
+ constructor(view) {
1783
+ this.decorations = buildDecorations2(view);
1784
+ }
1785
+ update(update2) {
1786
+ this.decorations = buildDecorations2(update2.view);
1787
+ }
1788
+ }, {
1789
+ decorations: (value) => value.decorations
1790
+ })
1791
+ ];
1792
+ };
1793
+
1794
+ // packages/ui/react-ui-editor/src/extensions/markdown/hr.ts
1795
+ import { syntaxTree as syntaxTree3 } from "@codemirror/language";
1796
+ import { RangeSetBuilder as RangeSetBuilder3 } from "@codemirror/state";
1797
+ import { Decoration as Decoration5, EditorView as EditorView9, ViewPlugin as ViewPlugin5, WidgetType as WidgetType3 } from "@codemirror/view";
1798
+ var styles4 = EditorView9.baseTheme({
1799
+ "& .cm-hr": {
1800
+ // Note that block-level decorations should not have vertical margins,
1801
+ borderTop: `1px solid ${getToken("extend.colors.neutral.200")}`
1802
+ }
1803
+ });
1804
+ var HorizontalRuleWidget = class extends WidgetType3 {
1805
+ toDOM() {
1806
+ const el = document.createElement("div");
1807
+ el.className = "cm-hr";
1808
+ return el;
1809
+ }
1810
+ };
1811
+ var decoration = Decoration5.replace({
1812
+ widget: new HorizontalRuleWidget()
1813
+ });
1814
+ var buildDecorations3 = (view) => {
1815
+ const builder = new RangeSetBuilder3();
1816
+ const { state } = view;
1817
+ const cursor = state.selection.main.head;
1818
+ for (const { from, to } of view.visibleRanges) {
1819
+ syntaxTree3(state).iterate({
1820
+ enter: (node) => {
1821
+ if (node.name === "HorizontalRule") {
1822
+ if (cursor <= node.from || cursor >= node.to) {
1823
+ builder.add(node.from, node.to, decoration);
1824
+ }
1825
+ }
1826
+ },
1827
+ from,
1828
+ to
1829
+ });
1830
+ }
1831
+ return builder.finish();
1832
+ };
1833
+ var hr = () => {
1834
+ return [
1835
+ ViewPlugin5.fromClass(class {
1836
+ constructor(view) {
1837
+ this.decorations = buildDecorations3(view);
1838
+ }
1839
+ update(update2) {
1840
+ this.decorations = buildDecorations3(update2.view);
1841
+ }
1842
+ }, {
1843
+ decorations: (value) => value.decorations
1844
+ }),
1845
+ styles4
1846
+ ];
1847
+ };
1848
+
1849
+ // packages/ui/react-ui-editor/src/extensions/markdown/image.ts
1850
+ import { syntaxTree as syntaxTree4 } from "@codemirror/language";
1851
+ import { StateField as StateField3 } from "@codemirror/state";
1852
+ import { Decoration as Decoration6, EditorView as EditorView10, WidgetType as WidgetType4 } from "@codemirror/view";
1853
+ var image = (options = {}) => {
1854
+ return StateField3.define({
1855
+ create: (state) => {
1856
+ return Decoration6.set(buildDecorations4(0, state.doc.length, state));
1857
+ },
1858
+ update: (value, tr) => {
1859
+ if (!tr.docChanged && !tr.selection) {
1860
+ return value;
1861
+ }
1862
+ const cursor = tr.state.selection.main.head;
1863
+ const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);
1864
+ let from = Math.min(cursor, oldCursor);
1865
+ let to = Math.max(cursor, oldCursor);
1866
+ tr.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
1867
+ from = Math.min(from, fromB);
1868
+ to = Math.max(to, toB);
1869
+ });
1870
+ from = tr.state.doc.lineAt(from).from;
1871
+ to = tr.state.doc.lineAt(to).to;
1872
+ return value.map(tr.changes).update({
1873
+ filterFrom: from,
1874
+ filterTo: to,
1875
+ filter: () => false,
1876
+ add: buildDecorations4(from, to, tr.state)
1877
+ });
1878
+ },
1879
+ provide: (field) => EditorView10.decorations.from(field)
1880
+ });
1881
+ };
1882
+ var buildDecorations4 = (from, to, state) => {
1883
+ const decorations = [];
1884
+ const cursor = state.selection.main.head;
1885
+ syntaxTree4(state).iterate({
1886
+ enter: (node) => {
1887
+ if (node.name === "Image") {
1888
+ const urlNode = node.node.getChild("URL");
1889
+ if (urlNode) {
1890
+ const hide = state.readOnly || cursor < node.from || cursor > node.to;
1891
+ const url = state.sliceDoc(urlNode.from, urlNode.to);
1892
+ decorations.push(Decoration6.replace({
1893
+ block: true,
1894
+ widget: new ImageWidget(url)
1895
+ }).range(hide ? node.from : node.to, node.to));
1896
+ }
1897
+ }
1898
+ },
1899
+ from,
1900
+ to
1901
+ });
1902
+ return decorations;
1903
+ };
1904
+ var ImageWidget = class extends WidgetType4 {
1905
+ constructor(_url) {
1906
+ super();
1907
+ this._url = _url;
1908
+ }
1909
+ eq(other) {
1910
+ return this._url === other._url;
1911
+ }
1912
+ toDOM(view) {
1913
+ const img = document.createElement("img");
1914
+ img.setAttribute("src", this._url);
1915
+ img.setAttribute("class", "cm-image");
1916
+ img.onload = () => img.classList.add("cm-loaded-image");
1917
+ return img;
1918
+ }
1919
+ };
1920
+
1921
+ // packages/ui/react-ui-editor/src/extensions/markdown/link.ts
1922
+ import { syntaxTree as syntaxTree5 } from "@codemirror/language";
1923
+ import { RangeSetBuilder as RangeSetBuilder4 } from "@codemirror/state";
1924
+ import { hoverTooltip as hoverTooltip2, Decoration as Decoration7, ViewPlugin as ViewPlugin6, WidgetType as WidgetType5 } from "@codemirror/view";
1925
+ import { tooltipContent } from "@dxos/react-ui-theme";
1926
+ var link = (options = {}) => {
1927
+ const extensions = [
1928
+ ViewPlugin6.fromClass(class {
1929
+ constructor(view) {
1930
+ this.decorations = buildDecorations5(view, options);
1931
+ }
1932
+ update(update2) {
1933
+ if (update2.docChanged || update2.viewportChanged || update2.selectionSet) {
1934
+ this.decorations = buildDecorations5(update2.view, options);
1935
+ }
1936
+ }
1937
+ }, {
1938
+ decorations: (value) => value.decorations
1939
+ })
1940
+ ];
1941
+ if (options.onHover) {
1942
+ extensions.push(
1943
+ // https://codemirror.net/examples/tooltip
1944
+ // https://codemirror.net/docs/ref/#view.hoverTooltip
1945
+ // https://github.com/codemirror/view/blob/main/src/tooltip.ts
1946
+ hoverTooltip2((view, pos, side) => {
1947
+ const syntax = syntaxTree5(view.state).resolveInner(pos, side);
1948
+ let link2 = null;
1949
+ for (let i = 0, node = syntax; !link2 && node && i < 5; node = node.parent, i++) {
1950
+ link2 = node.name === "Link" ? node : null;
1951
+ }
1952
+ const url = link2 && link2.getChild("URL");
1953
+ if (!url || !link2) {
1954
+ return null;
1955
+ }
1956
+ const urlText = view.state.sliceDoc(url.from, url.to);
1957
+ return {
1958
+ pos: link2.from,
1959
+ end: link2.to,
1960
+ above: true,
1961
+ create: () => {
1962
+ const el = document.createElement("div");
1963
+ el.className = tooltipContent({}, "pli-2 plb-1");
1964
+ options.onHover(el, urlText);
1965
+ return {
1966
+ dom: el,
1967
+ offset: {
1968
+ x: 0,
1969
+ y: 4
1970
+ }
1971
+ };
1972
+ }
1973
+ };
1974
+ })
1975
+ );
1976
+ }
1977
+ return extensions;
1978
+ };
1979
+ var LinkButton = class extends WidgetType5 {
1980
+ constructor(_url, _onAttach) {
1981
+ super();
1982
+ this._url = _url;
1983
+ this._onAttach = _onAttach;
1984
+ }
1985
+ eq(other) {
1986
+ return this._url === other._url;
1987
+ }
1988
+ toDOM(view) {
1989
+ const el = document.createElement("span");
1990
+ this._onAttach(el, this._url);
1991
+ return el;
1992
+ }
1993
+ };
1994
+ var LinkText = class extends WidgetType5 {
1995
+ constructor(_text, _url) {
1996
+ super();
1997
+ this._text = _text;
1998
+ this._url = _url;
1999
+ }
2000
+ eq(other) {
2001
+ return this._url === other._url;
2002
+ }
2003
+ toDOM(view) {
2004
+ const link2 = document.createElement("a");
2005
+ link2.setAttribute("class", "cm-link");
2006
+ link2.textContent = this._text;
2007
+ if (this._url) {
2008
+ link2.setAttribute("rel", "noreferrer");
2009
+ link2.setAttribute("target", "_blank");
2010
+ link2.href = this._url;
2011
+ } else {
2012
+ link2.onclick = () => {
2013
+ view.dispatch({
2014
+ selection: {
2015
+ anchor: view.posAtDOM(link2)
2016
+ }
2017
+ });
2018
+ };
2019
+ }
2020
+ return link2;
2021
+ }
2022
+ };
2023
+ var buildDecorations5 = (view, options) => {
2024
+ const builder = new RangeSetBuilder4();
2025
+ const { state } = view;
2026
+ const cursor = state.selection.main.head;
2027
+ for (const { from, to } of view.visibleRanges) {
2028
+ syntaxTree5(state).iterate({
2029
+ enter: (node) => {
2030
+ if (node.name === "Link") {
2031
+ const marks2 = node.node.getChildren("LinkMark");
2032
+ const text = marks2.length >= 2 ? state.sliceDoc(marks2[0].to, marks2[1].from) : "";
2033
+ const urlNode = node.node.getChild("URL");
2034
+ const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : "";
2035
+ if (!url) {
2036
+ return false;
2037
+ }
2038
+ if (!view.hasFocus || state.readOnly || cursor < node.from || cursor > node.to) {
2039
+ builder.add(node.from, node.to, Decoration7.replace({
2040
+ widget: new LinkText(text, options.link ? url : void 0)
2041
+ }));
2042
+ }
2043
+ if (options.onRender) {
2044
+ builder.add(node.to, node.to, Decoration7.replace({
2045
+ widget: new LinkButton(url, options.onRender)
2046
+ }));
2047
+ }
2048
+ return false;
1977
2049
  }
1978
- console.log(match);
1979
- return {
1980
- from: match.from,
1981
- options: onSearch(match.text.slice(1).toLowerCase()).map((value) => ({
1982
- label: `@${value}`
1983
- }))
1984
- };
1985
- }
1986
- ]
1987
- });
2050
+ },
2051
+ from,
2052
+ to
2053
+ });
2054
+ }
2055
+ return builder.finish();
1988
2056
  };
1989
2057
 
1990
- // packages/ui/react-ui-editor/src/extensions/table.ts
1991
- import { syntaxTree as syntaxTree3 } from "@codemirror/language";
1992
- import { RangeSetBuilder as RangeSetBuilder2, StateField as StateField4 } from "@codemirror/state";
1993
- import { Decoration as Decoration7, EditorView as EditorView10, WidgetType as WidgetType6 } from "@codemirror/view";
2058
+ // packages/ui/react-ui-editor/src/extensions/markdown/table.ts
2059
+ import { syntaxTree as syntaxTree6 } from "@codemirror/language";
2060
+ import { RangeSetBuilder as RangeSetBuilder5, StateField as StateField4 } from "@codemirror/state";
2061
+ import { Decoration as Decoration8, EditorView as EditorView11, WidgetType as WidgetType6 } from "@codemirror/view";
1994
2062
  var table = (options = {}) => {
1995
2063
  return StateField4.define({
1996
2064
  create: (state) => update(state, options),
1997
2065
  update: (_, tr) => update(tr.state, options),
1998
- provide: (field) => EditorView10.decorations.from(field)
2066
+ provide: (field) => EditorView11.decorations.from(field)
1999
2067
  });
2000
2068
  };
2001
2069
  var update = (state, options) => {
2002
- const builder = new RangeSetBuilder2();
2070
+ const builder = new RangeSetBuilder5();
2003
2071
  const cursor = state.selection.main.head;
2004
2072
  const tables = [];
2005
2073
  const getTable = () => tables[tables.length - 1];
@@ -2007,7 +2075,7 @@ var update = (state, options) => {
2007
2075
  const table2 = getTable();
2008
2076
  return table2.rows?.[table2.rows.length - 1];
2009
2077
  };
2010
- syntaxTree3(state).iterate({
2078
+ syntaxTree6(state).iterate({
2011
2079
  enter: (node) => {
2012
2080
  switch (node.name) {
2013
2081
  case "Table": {
@@ -2039,11 +2107,10 @@ var update = (state, options) => {
2039
2107
  });
2040
2108
  tables.forEach((table2) => {
2041
2109
  const hide = state.readOnly || cursor < table2.from || cursor > table2.to;
2042
- hide && builder.add(table2.from, table2.to, Decoration7.replace({
2043
- block: true,
2110
+ hide && builder.add(table2.from, table2.to, Decoration8.replace({
2044
2111
  widget: new TableWidget(table2)
2045
2112
  }));
2046
- builder.add(table2.from, table2.to, Decoration7.mark({
2113
+ builder.add(table2.from, table2.to, Decoration8.mark({
2047
2114
  class: "cm-table"
2048
2115
  }));
2049
2116
  });
@@ -2081,92 +2148,142 @@ var TableWidget = class extends WidgetType6 {
2081
2148
  }
2082
2149
  return table2;
2083
2150
  }
2151
+ ignoreEvent(e) {
2152
+ return !/^mouse/.test(e.type);
2153
+ }
2084
2154
  };
2085
2155
 
2086
- // packages/ui/react-ui-editor/src/extensions/tasklist.ts
2087
- import { EditorView as EditorView11, Decoration as Decoration8, WidgetType as WidgetType7, MatchDecorator as MatchDecorator3, ViewPlugin as ViewPlugin7 } from "@codemirror/view";
2088
- var styles5 = EditorView11.baseTheme({
2089
- "& .cm-task-item": {
2090
- paddingLeft: "4px",
2091
- paddingRight: "8px"
2156
+ // packages/ui/react-ui-editor/src/extensions/markdown/tasklist.ts
2157
+ import { syntaxTree as syntaxTree7 } from "@codemirror/language";
2158
+ import { RangeSetBuilder as RangeSetBuilder6 } from "@codemirror/state";
2159
+ import { EditorView as EditorView12, Decoration as Decoration9, WidgetType as WidgetType7, ViewPlugin as ViewPlugin7 } from "@codemirror/view";
2160
+ var styles5 = EditorView12.baseTheme({
2161
+ "& .cm-task": {
2162
+ color: getToken("extend.colors.blue.500")
2163
+ },
2164
+ "& .cm-task-checkbox": {
2165
+ marginLeft: "4px",
2166
+ marginRight: "4px"
2092
2167
  }
2093
2168
  });
2094
2169
  var CheckboxWidget = class extends WidgetType7 {
2095
- constructor(_checked, _indent, _onCheck) {
2170
+ constructor(_checked) {
2096
2171
  super();
2097
2172
  this._checked = _checked;
2098
- this._indent = _indent;
2099
- this._onCheck = _onCheck;
2100
2173
  }
2101
2174
  eq(other) {
2102
- return this._checked === other._checked && this._indent === other._indent;
2175
+ return this._checked === other._checked;
2103
2176
  }
2104
2177
  toDOM(view) {
2105
- const wrap = document.createElement("span");
2106
- wrap.className = "cm-task-item";
2107
- wrap.setAttribute("aria-hidden", "true");
2108
- wrap.style.setProperty("margin-left", this._indent * 24 + "px");
2109
- const input = wrap.appendChild(document.createElement("input"));
2178
+ const input = document.createElement("input");
2179
+ input.className = "cm-task-checkbox";
2110
2180
  input.type = "checkbox";
2111
2181
  input.checked = this._checked;
2112
- if (!this._onCheck) {
2182
+ if (view.state.readOnly) {
2113
2183
  input.setAttribute("disabled", "true");
2114
- }
2115
- if (this._onCheck) {
2116
- input.onchange = (event) => {
2117
- this._onCheck?.(event.target.checked);
2118
- return true;
2184
+ } else {
2185
+ input.onmousedown = (event) => {
2186
+ const pos = view.posAtDOM(input);
2187
+ const text = view.state.sliceDoc(pos, pos + 3);
2188
+ if (text === (this._checked ? "[x]" : "[ ]")) {
2189
+ view.dispatch({
2190
+ changes: {
2191
+ from: pos + 1,
2192
+ to: pos + 2,
2193
+ insert: this._checked ? " " : "x"
2194
+ }
2195
+ });
2196
+ event.preventDefault();
2197
+ }
2119
2198
  };
2120
2199
  }
2121
- return wrap;
2200
+ return input;
2122
2201
  }
2123
2202
  ignoreEvent() {
2124
2203
  return false;
2125
2204
  }
2126
2205
  };
2206
+ var checkedDecoration = Decoration9.replace({
2207
+ widget: new CheckboxWidget(true)
2208
+ });
2209
+ var uncheckedDecoration = Decoration9.replace({
2210
+ widget: new CheckboxWidget(false)
2211
+ });
2212
+ var buildDecorations6 = (view) => {
2213
+ const builder = new RangeSetBuilder6();
2214
+ const { state } = view;
2215
+ const cursor = state.selection.main.head;
2216
+ for (const { from, to } of view.visibleRanges) {
2217
+ syntaxTree7(state).iterate({
2218
+ enter: (node) => {
2219
+ if (node.name === "TaskMarker") {
2220
+ if (cursor < node.from || cursor > node.to) {
2221
+ const checked = state.doc.sliceString(node.from + 1, node.to - 1) === "x";
2222
+ builder.add(node.from - 2, node.from - 1, Decoration9.mark({
2223
+ class: "cm-task"
2224
+ }));
2225
+ builder.add(node.from, node.to, checked ? checkedDecoration : uncheckedDecoration);
2226
+ }
2227
+ }
2228
+ },
2229
+ from,
2230
+ to
2231
+ });
2232
+ }
2233
+ return builder.finish();
2234
+ };
2127
2235
  var tasklist = (options = {}) => {
2128
- const taskMatcher = new MatchDecorator3({
2129
- regexp: /^(\s*)- \[([ xX])\]\s/g,
2130
- decoration: (match, view, pos) => {
2131
- const indent = Math.floor(match[1].length / 2);
2132
- const checked = match[2] === "x" || match[2] === "X";
2133
- return Decoration8.replace({
2134
- widget: new CheckboxWidget(checked, indent, view.state.readOnly ? void 0 : (checked2) => {
2135
- const idx = pos + match[0].indexOf("[") + 1;
2136
- view.dispatch({
2137
- changes: {
2138
- from: idx,
2139
- to: idx + 1,
2140
- insert: checked2 ? "x" : " "
2141
- },
2142
- // TODO(burdon): Restore cursor position? More useful to move to end of line (can indent).
2143
- selection: {
2144
- anchor: idx + 3
2145
- }
2146
- });
2147
- })
2148
- });
2149
- }
2150
- });
2151
- const tasks = ViewPlugin7.fromClass(class {
2152
- constructor(view) {
2153
- this.tasks = taskMatcher.createDeco(view);
2154
- }
2155
- update(update2) {
2156
- this.tasks = taskMatcher.updateDeco(update2, this.tasks);
2157
- }
2158
- }, {
2159
- decorations: (value) => value.tasks,
2160
- provide: (plugin) => EditorView11.atomicRanges.of((view) => {
2161
- return view.plugin(plugin)?.tasks || Decoration8.none;
2162
- })
2163
- });
2164
2236
  return [
2165
- tasks,
2237
+ ViewPlugin7.fromClass(class {
2238
+ constructor(view) {
2239
+ this.decorations = buildDecorations6(view);
2240
+ }
2241
+ update(update2) {
2242
+ if (update2.docChanged || update2.viewportChanged || update2.selectionSet) {
2243
+ this.decorations = buildDecorations6(update2.view);
2244
+ }
2245
+ }
2246
+ }, {
2247
+ decorations: (value) => value.decorations
2248
+ }),
2166
2249
  styles5
2167
2250
  ];
2168
2251
  };
2169
2252
 
2253
+ // packages/ui/react-ui-editor/src/extensions/mention.ts
2254
+ import { autocompletion as autocompletion2 } from "@codemirror/autocomplete";
2255
+ var mention = ({ onSearch }) => {
2256
+ return autocompletion2({
2257
+ // TODO(burdon): Not working.
2258
+ activateOnTyping: true,
2259
+ // activateOnTypingDelay: 100,
2260
+ // selectOnOpen: true,
2261
+ closeOnBlur: false,
2262
+ // defaultKeymap: false,
2263
+ icons: false,
2264
+ override: [
2265
+ (context) => {
2266
+ console.log(context);
2267
+ const match = context.matchBefore(/@(\w+)?/);
2268
+ if (!match || match.from === match.to && !context.explicit) {
2269
+ return null;
2270
+ }
2271
+ return {
2272
+ from: match.from,
2273
+ options: onSearch(match.text.slice(1).toLowerCase()).map((value) => ({
2274
+ label: `@${value}`
2275
+ }))
2276
+ };
2277
+ }
2278
+ ]
2279
+ });
2280
+ };
2281
+
2282
+ // packages/ui/react-ui-editor/src/extensions/outliner.ts
2283
+ var outliner = (options = {}) => {
2284
+ return [];
2285
+ };
2286
+
2170
2287
  // packages/ui/react-ui-editor/src/extensions/typewriter.ts
2171
2288
  import { keymap as keymap5 } from "@codemirror/view";
2172
2289
  var defaultItems = [
@@ -2224,27 +2341,6 @@ var typewriter = ({ delay = 75, items = defaultItems } = {}) => {
2224
2341
  ];
2225
2342
  };
2226
2343
 
2227
- // packages/ui/react-ui-editor/src/extensions/yjs/yjs.ts
2228
- import { yCollab } from "y-codemirror.next";
2229
-
2230
- // packages/ui/react-ui-editor/src/extensions/yjs/cursor.ts
2231
- import * as Y from "yjs";
2232
- import { arrayToString, stringToArray } from "@dxos/util";
2233
- var cursorConverter2 = (text) => ({
2234
- toCursor: (index, assoc) => {
2235
- return arrayToString(Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text, index, assoc)));
2236
- },
2237
- fromCursor: (cursor) => {
2238
- return Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(stringToArray(cursor)), text.doc)?.index ?? 0;
2239
- }
2240
- });
2241
-
2242
- // packages/ui/react-ui-editor/src/extensions/yjs/yjs.ts
2243
- var yjs = (content, awareness2) => [
2244
- CursorConverter.of(cursorConverter2(content)),
2245
- yCollab(content, awareness2)
2246
- ];
2247
-
2248
2344
  // packages/ui/react-ui-editor/src/themes/default.ts
2249
2345
  import get3 from "lodash.get";
2250
2346
  var defaultTheme = {
@@ -2338,33 +2434,6 @@ var defaultTheme = {
2338
2434
  backgroundColor: get3(tokens, "extend.colors.yellow.700")
2339
2435
  },
2340
2436
  //
2341
- // collaboration
2342
- // TODO(burdon): Review classnames (YJS dependent?) Note: e2e tests depend on this class.
2343
- //
2344
- "&light .cm-ySelection, &light .cm-yLineSelection": {
2345
- mixBlendMode: "multiply"
2346
- },
2347
- "&dark .cm-ySelection, &dark .cm-yLineSelection": {
2348
- mixBlendMode: "screen"
2349
- },
2350
- ".cm-ySelectionInfo": {
2351
- padding: "2px 4px",
2352
- marginBlockStart: "-4px"
2353
- },
2354
- ".cm-ySelection, & .cm-selectionMatch": {
2355
- paddingBlockStart: ".15em",
2356
- paddingBlockEnd: ".15em"
2357
- },
2358
- ".cm-ySelectionCaret": {
2359
- display: "inline-block",
2360
- insetBlockStart: ".1em",
2361
- blockSize: "1.4em",
2362
- verticalAlign: "top"
2363
- },
2364
- ".cm-yLineSelection": {
2365
- margin: 0
2366
- },
2367
- //
2368
2437
  // link
2369
2438
  //
2370
2439
  ".cm-link": {
@@ -2401,13 +2470,6 @@ var defaultTheme = {
2401
2470
  textDecoration: "none"
2402
2471
  },
2403
2472
  //
2404
- // widgets
2405
- //
2406
- ".cm-widgetBuffer": {
2407
- display: "none",
2408
- height: 0
2409
- },
2410
- //
2411
2473
  // table
2412
2474
  //
2413
2475
  ".cm-table *": {
@@ -2438,27 +2500,17 @@ var defaultTheme = {
2438
2500
  },
2439
2501
  //
2440
2502
  // font size
2441
- // TODO(thure): This appears to be the best or only way to set selection caret heights, but it's far more verbose than it needs to be.
2503
+ // TODO(thure): This appears to be the best or only way to set selection caret heights,
2504
+ // but it's far more verbose than it needs to be.
2442
2505
  //
2443
- ...Object.keys(get3(tokens, "extend.fontSize", {})).reduce((acc, fontSize) => {
2444
- const height = get3(tokens, [
2445
- "extend",
2446
- "fontSize",
2447
- fontSize,
2448
- 1,
2449
- "lineHeight"
2450
- ]);
2451
- acc[`& .text-${fontSize} + .cm-ySelectionCaret`] = {
2452
- height
2453
- };
2454
- acc[`& .text-${fontSize} + .cm-ySelection + .cm-ySelectionCaret`] = {
2455
- height
2456
- };
2457
- acc[`& .text-${fontSize} + .cm-widgetBuffer + .cm-ySelectionCaret`] = {
2458
- height
2459
- };
2460
- return acc;
2461
- }, {}),
2506
+ // ...Object.keys(get(tokens, 'extend.fontSize', {})).reduce((acc: Record<string, any>, fontSize) => {
2507
+ // const height = get(tokens, ['extend', 'fontSize', fontSize, 1, 'lineHeight']);
2508
+ //
2509
+ // acc[`& .text-${fontSize} + .cm-ySelectionCaret`] = { height };
2510
+ // acc[`& .text-${fontSize} + .cm-ySelection + .cm-ySelectionCaret`] = { height };
2511
+ // acc[`& .text-${fontSize} + .cm-widgetBuffer + .cm-ySelectionCaret`] = { height };
2512
+ // return acc;
2513
+ // }, {}),
2462
2514
  /**
2463
2515
  * Panels
2464
2516
  * TODO(burdon): Needs styling attention (esp. dark mode).
@@ -2509,7 +2561,9 @@ var textTheme = {
2509
2561
  }
2510
2562
  };
2511
2563
  var markdownTheme = {
2512
- // NOTE: Must leave base font family as is (i.e., monospace) due to fenced code blocks.
2564
+ ".cm-scroller": {
2565
+ fontFamily: get3(tokens, "fontFamily.body", []).join(",")
2566
+ },
2513
2567
  ".cm-placeholder": {
2514
2568
  fontFamily: get3(tokens, "fontFamily.body", []).join(",")
2515
2569
  }
@@ -2524,95 +2578,89 @@ var codeTheme = {
2524
2578
  };
2525
2579
 
2526
2580
  // packages/ui/react-ui-editor/src/components/TextEditor/TextEditor.tsx
2581
+ var __dxlog_file5 = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/components/TextEditor/TextEditor.tsx";
2527
2582
  var EditorModes = [
2528
2583
  "default",
2529
2584
  "vim"
2530
2585
  ];
2531
- var BaseTextEditor = /* @__PURE__ */ forwardRef(({ model, readonly, comments: comments2, extensions = [], editorMode, slots = defaultSlots }, forwardedRef) => {
2532
- const { themeMode } = useThemeContext();
2586
+ var BaseTextEditor = /* @__PURE__ */ forwardRef(({ model, readonly, autoFocus, scrollTo, selection, editorMode, theme, slots = defaultSlots, extensions = [], debug }, forwardedRef) => {
2533
2587
  const tabsterDOMAttribute = useFocusableGroup({
2534
2588
  tabBehavior: "limited"
2535
2589
  });
2536
- const [root, setRoot] = useState(null);
2537
- const [{ state = void 0, view = void 0 } = {}, setEditor] = useState();
2538
- useImperativeHandle(forwardedRef, () => ({
2539
- root,
2540
- state,
2590
+ const { themeMode } = useThemeContext();
2591
+ const rootRef = useRef(null);
2592
+ const [view, setView] = useState(null);
2593
+ useImperativeHandle(forwardedRef, () => view, [
2541
2594
  view
2542
- }), [
2543
- view,
2544
- state,
2545
- root
2546
- ]);
2547
- const { awareness: awareness2, peer } = model;
2548
- useEffect(() => {
2549
- if (awareness2 && peer) {
2550
- awareness2.setLocalStateField("user", {
2551
- name: peer.name ?? generateName(peer.id),
2552
- color: getColorForValue({
2553
- value: peer.id,
2554
- type: "color"
2555
- }),
2556
- colorLight: getColorForValue({
2557
- value: peer.id,
2558
- themeMode,
2559
- type: "highlight"
2560
- })
2561
- });
2562
- }
2563
- }, [
2564
- awareness2,
2565
- peer,
2566
- themeMode
2567
2595
  ]);
2568
- useEffect(() => {
2569
- if (view && comments2?.length) {
2570
- view.dispatch({
2571
- effects: setCommentRange.of({
2572
- model,
2573
- comments: comments2
2574
- })
2575
- });
2596
+ useEffect2(() => {
2597
+ if (autoFocus) {
2598
+ view?.focus();
2576
2599
  }
2577
2600
  }, [
2578
2601
  view,
2579
- comments2
2602
+ autoFocus
2580
2603
  ]);
2581
- useEffect(() => {
2582
- if (!root) {
2604
+ useEffect2(() => {
2605
+ if (!model || !rootRef.current) {
2583
2606
  return;
2584
2607
  }
2585
- const state2 = EditorState2.create({
2608
+ const state = EditorState2.create({
2586
2609
  doc: model.text(),
2610
+ selection,
2587
2611
  extensions: [
2588
- readonly && EditorState2.readOnly.of(readonly),
2589
- // TODO(burdon): Factor out VIM mode? (manage via MarkdownPlugin).
2590
- editorMode === "vim" && vim(),
2612
+ // TODO(burdon): Doesn't catch errors in keymap functions.
2613
+ EditorView13.exceptionSink.of((err) => {
2614
+ log2.catch(err, void 0, {
2615
+ F: __dxlog_file5,
2616
+ L: 123,
2617
+ S: void 0,
2618
+ C: (f, a) => f(...a)
2619
+ });
2620
+ }),
2591
2621
  // Theme.
2592
- // TODO(burdon): Make theme configurable.
2593
- EditorView12.baseTheme(defaultTheme),
2594
- EditorView12.theme(slots?.editor?.theme ?? {}),
2595
- EditorView12.darkTheme.of(themeMode === "dark"),
2622
+ // TODO(burdon): Make configurable.
2623
+ EditorView13.baseTheme(defaultTheme),
2624
+ EditorView13.theme(theme ?? {}),
2625
+ EditorView13.darkTheme.of(themeMode === "dark"),
2626
+ EditorView13.editorAttributes.of({
2627
+ class: slots.editor?.className ?? ""
2628
+ }),
2629
+ EditorView13.contentAttributes.of({
2630
+ class: slots.content?.className ?? ""
2631
+ }),
2632
+ // State.
2633
+ EditorState2.readOnly.of(!!readonly),
2596
2634
  // Storage and replication.
2635
+ // NOTE: This must come before user extensions.
2597
2636
  model.extension,
2637
+ // TODO(burdon): Factor out (requires special handling for Escape/focus).
2638
+ editorMode === "vim" && vim(),
2598
2639
  // Custom.
2599
2640
  ...extensions
2600
2641
  ].filter(Boolean)
2601
2642
  });
2602
- view?.destroy();
2603
- setEditor({
2604
- state: state2,
2605
- view: new EditorView12({
2606
- state: state2,
2607
- parent: root
2608
- })
2643
+ const newView = new EditorView13({
2644
+ state,
2645
+ parent: rootRef.current,
2646
+ scrollTo,
2647
+ // NOTE: Uncomment to debug/monitor all transactions.
2648
+ // https://codemirror.net/docs/ref/#view.EditorView.dispatch
2649
+ dispatchTransactions: (trs, view2) => {
2650
+ if (debug) {
2651
+ logChanges(trs);
2652
+ }
2653
+ view2.update(trs);
2654
+ }
2609
2655
  });
2656
+ view?.destroy();
2657
+ setView(newView);
2610
2658
  return () => {
2611
- view?.destroy();
2612
- setEditor(void 0);
2659
+ newView?.destroy();
2660
+ setView(null);
2613
2661
  };
2614
2662
  }, [
2615
- root,
2663
+ rootRef,
2616
2664
  model,
2617
2665
  readonly,
2618
2666
  editorMode,
@@ -2622,11 +2670,13 @@ var BaseTextEditor = /* @__PURE__ */ forwardRef(({ model, readonly, comments: co
2622
2670
  const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
2623
2671
  switch (key) {
2624
2672
  case "Enter": {
2625
- view?.contentDOM.focus();
2673
+ view?.focus();
2626
2674
  break;
2627
2675
  }
2628
2676
  case "Escape": {
2629
- editorMode === "vim" && (altKey || shiftKey || metaKey || ctrlKey) && root?.focus();
2677
+ if (editorMode === "vim" && (altKey || shiftKey || metaKey || ctrlKey)) {
2678
+ rootRef.current?.focus();
2679
+ }
2630
2680
  break;
2631
2681
  }
2632
2682
  }
@@ -2636,83 +2686,78 @@ var BaseTextEditor = /* @__PURE__ */ forwardRef(({ model, readonly, comments: co
2636
2686
  ]);
2637
2687
  return /* @__PURE__ */ React.createElement("div", {
2638
2688
  key: model.id,
2639
- ref: setRoot,
2689
+ role: "none",
2640
2690
  tabIndex: 0,
2641
- ...slots?.root,
2691
+ onKeyUp: handleKeyUp,
2692
+ ...slots.root,
2642
2693
  ...editorMode !== "vim" && tabsterDOMAttribute,
2643
- onKeyUp: handleKeyUp
2694
+ ref: rootRef
2644
2695
  });
2645
2696
  });
2646
- var TextEditor = /* @__PURE__ */ forwardRef(({ readonly, extensions = [], slots: _slots, ...props }, forwardedRef) => {
2697
+ var TextEditor = /* @__PURE__ */ forwardRef(({ readonly, placeholder: placeholder3, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
2647
2698
  const { themeMode } = useThemeContext();
2648
- const slots = defaultsDeep2({}, _slots, defaultTextSlots);
2699
+ const updatedSlots = defaultsDeep2({}, slots, defaultTextSlots);
2649
2700
  return /* @__PURE__ */ React.createElement(BaseTextEditor, {
2650
2701
  ref: forwardedRef,
2651
2702
  readonly,
2652
2703
  extensions: [
2653
2704
  basicBundle({
2654
- readonly,
2655
2705
  themeMode,
2656
- placeholder: slots?.editor?.placeholder
2706
+ placeholder: placeholder3,
2707
+ lineWrapping
2657
2708
  }),
2658
2709
  ...extensions
2659
2710
  ],
2660
- slots,
2711
+ theme,
2712
+ slots: updatedSlots,
2661
2713
  ...props
2662
2714
  });
2663
2715
  });
2664
- var MarkdownEditor = /* @__PURE__ */ forwardRef(({ readonly, extensions = [], slots: _slots, ...props }, forwardedRef) => {
2716
+ var MarkdownEditor = /* @__PURE__ */ forwardRef(({ readonly, placeholder: placeholder3, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
2665
2717
  const { themeMode } = useThemeContext();
2666
- const slots = defaultsDeep2({}, _slots, defaultMarkdownSlots);
2718
+ const updatedSlots = defaultsDeep2({}, slots, defaultMarkdownSlots);
2667
2719
  return /* @__PURE__ */ React.createElement(BaseTextEditor, {
2668
2720
  ref: forwardedRef,
2669
2721
  readonly,
2670
2722
  extensions: [
2671
2723
  markdownBundle({
2672
- readonly,
2673
2724
  themeMode,
2674
- placeholder: slots?.editor?.placeholder
2725
+ placeholder: placeholder3
2675
2726
  }),
2676
2727
  ...extensions
2677
2728
  ],
2678
- slots,
2729
+ theme,
2730
+ slots: updatedSlots,
2679
2731
  ...props
2680
2732
  });
2681
2733
  });
2682
2734
  var defaultSlots = {
2683
2735
  root: {
2684
- className: mx3("p-2", inputSurface)
2736
+ // TODO(burdon): Add focusRing by default/as property?
2737
+ className: mx3("flex flex-col grow overflow-y-auto", attentionSurface)
2738
+ },
2739
+ editor: {
2740
+ className: "h-full p-2"
2685
2741
  }
2686
2742
  };
2687
2743
  var defaultTextSlots = {
2688
- ...defaultSlots,
2689
- editor: {
2690
- theme: textTheme
2691
- }
2744
+ ...defaultSlots
2692
2745
  };
2693
2746
  var defaultMarkdownSlots = {
2694
- ...defaultSlots,
2695
- editor: {
2696
- theme: markdownTheme
2697
- }
2747
+ ...defaultSlots
2698
2748
  };
2699
2749
 
2700
- // packages/ui/react-ui-editor/src/hooks/automerge.ts
2701
- import get4 from "lodash.get";
2702
- import { getRawDoc } from "@dxos/echo-schema";
2703
- import { isNotNullOrUndefined } from "@dxos/util";
2704
-
2705
2750
  // packages/ui/react-ui-editor/src/hooks/awareness-provider.ts
2706
2751
  import { DeferredTask, Event as Event2, sleep } from "@dxos/async";
2707
2752
  import { Context as Context2 } from "@dxos/context";
2708
- import { invariant as invariant3 } from "@dxos/invariant";
2753
+ import { invariant as invariant4 } from "@dxos/invariant";
2709
2754
  import { log as log3 } from "@dxos/log";
2710
- var __dxlog_file4 = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/hooks/awareness-provider.ts";
2755
+ var __dxlog_file6 = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/hooks/awareness-provider.ts";
2711
2756
  var DEBOUNCE_INTERVAL = 100;
2712
2757
  var SpaceAwarenessProvider = class {
2713
2758
  constructor(params) {
2714
- this.remoteStateChange = new Event2();
2715
2759
  this._remoteStates = /* @__PURE__ */ new Map();
2760
+ this.remoteStateChange = new Event2();
2716
2761
  this._space = params.space;
2717
2762
  this._channel = params.channel;
2718
2763
  this._peerId = params.peerId;
@@ -2729,7 +2774,7 @@ var SpaceAwarenessProvider = class {
2729
2774
  await sleep(DEBOUNCE_INTERVAL);
2730
2775
  }
2731
2776
  });
2732
- const unsubscribe = this._space.listen(this._channel, (message) => {
2777
+ this._ctx.onDispose(this._space.listen(this._channel, (message) => {
2733
2778
  switch (message.payload.kind) {
2734
2779
  case "query": {
2735
2780
  this._handleQueryMessage();
@@ -2740,16 +2785,15 @@ var SpaceAwarenessProvider = class {
2740
2785
  break;
2741
2786
  }
2742
2787
  }
2743
- });
2744
- this._ctx.onDispose(unsubscribe);
2788
+ }));
2745
2789
  void this._space.postMessage(this._channel, {
2746
2790
  kind: "query"
2747
2791
  }).catch((err) => {
2748
2792
  log3.debug("failed to query awareness", {
2749
2793
  err
2750
2794
  }, {
2751
- F: __dxlog_file4,
2752
- L: 85,
2795
+ F: __dxlog_file6,
2796
+ L: 89,
2753
2797
  S: this,
2754
2798
  C: (f, a) => f(...a)
2755
2799
  });
@@ -2760,10 +2804,13 @@ var SpaceAwarenessProvider = class {
2760
2804
  this._ctx = void 0;
2761
2805
  this._postTask = void 0;
2762
2806
  }
2807
+ getRemoteStates() {
2808
+ return Array.from(this._remoteStates.values());
2809
+ }
2763
2810
  update(position) {
2764
- invariant3(this._postTask, void 0, {
2765
- F: __dxlog_file4,
2766
- L: 96,
2811
+ invariant4(this._postTask, void 0, {
2812
+ F: __dxlog_file6,
2813
+ L: 104,
2767
2814
  S: this,
2768
2815
  A: [
2769
2816
  "this._postTask",
@@ -2777,13 +2824,10 @@ var SpaceAwarenessProvider = class {
2777
2824
  };
2778
2825
  this._postTask.schedule();
2779
2826
  }
2780
- getRemoteStates() {
2781
- return Array.from(this._remoteStates.values());
2782
- }
2783
2827
  _handleQueryMessage() {
2784
- invariant3(this._postTask, void 0, {
2785
- F: __dxlog_file4,
2786
- L: 111,
2828
+ invariant4(this._postTask, void 0, {
2829
+ F: __dxlog_file6,
2830
+ L: 115,
2787
2831
  S: this,
2788
2832
  A: [
2789
2833
  "this._postTask",
@@ -2793,9 +2837,9 @@ var SpaceAwarenessProvider = class {
2793
2837
  this._postTask.schedule();
2794
2838
  }
2795
2839
  _handlePostMessage(message) {
2796
- invariant3(message.kind === "post", void 0, {
2797
- F: __dxlog_file4,
2798
- L: 116,
2840
+ invariant4(message.kind === "post", void 0, {
2841
+ F: __dxlog_file6,
2842
+ L: 120,
2799
2843
  S: this,
2800
2844
  A: [
2801
2845
  "message.kind === 'post'",
@@ -2807,201 +2851,54 @@ var SpaceAwarenessProvider = class {
2807
2851
  }
2808
2852
  };
2809
2853
 
2810
- // packages/ui/react-ui-editor/src/hooks/useTextModel.ts
2854
+ // packages/ui/react-ui-editor/src/hooks/defs.ts
2811
2855
  import { StateField as StateField5 } from "@codemirror/state";
2812
- import { useEffect as useEffect2, useState as useState2 } from "react";
2813
- import { isAutomergeObject } from "@dxos/react-client/echo";
2856
+ var modelState = StateField5.define({
2857
+ create: () => void 0,
2858
+ update: (model) => model
2859
+ });
2814
2860
 
2815
- // packages/ui/react-ui-editor/src/hooks/yjs.ts
2816
- import * as decoding from "lib0/decoding";
2817
- import * as encoding from "lib0/encoding";
2818
- import { Observable } from "lib0/observable";
2819
- import * as YP from "y-protocols/awareness";
2820
- import * as Y2 from "yjs";
2821
- import { invariant as invariant4 } from "@dxos/invariant";
2822
- import { log as log4 } from "@dxos/log";
2823
- import { arrayToString as arrayToString2, stringToArray as stringToArray2 } from "@dxos/util";
2824
- var __dxlog_file5 = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/hooks/yjs.ts";
2825
- var createYjsModel = ({ identity, space, text }) => {
2826
- invariant4(text?.doc && text?.content, void 0, {
2827
- F: __dxlog_file5,
2828
- L: 23,
2829
- S: void 0,
2830
- A: [
2831
- "text?.doc && text?.content",
2832
- ""
2833
- ]
2834
- });
2835
- const provider = space ? new YAwarenessProvider({
2836
- space,
2837
- doc: text.doc,
2838
- channel: `yjs.awareness.${text.id}`
2839
- }) : void 0;
2840
- const model = {
2841
- id: text.doc.guid,
2842
- content: text.content,
2843
- text: () => text.content.toString(),
2844
- // https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
2845
- // TODO(dmaretskyi): Refactor as cursor-converter.
2846
- getCursorFromRange: (range) => {
2847
- const from = Y2.encodeRelativePosition(Y2.createRelativePositionFromTypeIndex(text.content, range.from));
2848
- const to = Y2.encodeRelativePosition(Y2.createRelativePositionFromTypeIndex(text.content, range.to, -1));
2849
- return [
2850
- arrayToString2(from),
2851
- arrayToString2(to)
2852
- ].join(":");
2853
- },
2854
- getRangeFromCursor: (cursor) => {
2855
- invariant4(text.doc, void 0, {
2856
- F: __dxlog_file5,
2857
- L: 40,
2858
- S: void 0,
2859
- A: [
2860
- "text.doc",
2861
- ""
2862
- ]
2863
- });
2864
- const parts = cursor.split(":");
2865
- const from = Y2.createAbsolutePositionFromRelativePosition(Y2.decodeRelativePosition(stringToArray2(parts[0])), text.doc);
2866
- const to = Y2.createAbsolutePositionFromRelativePosition(Y2.decodeRelativePosition(stringToArray2(parts[1])), text.doc);
2867
- return from && to ? {
2868
- from: from.index,
2869
- to: to.index
2870
- } : void 0;
2861
+ // packages/ui/react-ui-editor/src/hooks/useTextEditor.ts
2862
+ import { useState as useState2 } from "react";
2863
+ var useTextEditor = () => {
2864
+ const [view, setView] = useState2(null);
2865
+ return [
2866
+ (ref) => {
2867
+ setView(ref);
2871
2868
  },
2872
- extension: [
2873
- modelState.init(() => model),
2874
- yjs(text.content, provider?.awareness)
2875
- ],
2876
- awareness: provider?.awareness,
2877
- peer: identity ? {
2878
- id: identity.identityKey.toHex(),
2879
- name: identity.profile?.displayName
2880
- } : void 0
2881
- };
2882
- return model;
2883
- };
2884
- var messageAwareness = 1;
2885
- var messageQueryAwareness = 3;
2886
- var YAwarenessProvider = class extends Observable {
2887
- constructor({ space, doc, channel, awareness: awareness2 }) {
2888
- super();
2889
- this._space = space;
2890
- this._awareness = awareness2 ?? new YP.Awareness(doc);
2891
- this._channel = channel;
2892
- this._clientId = doc.clientID;
2893
- this._awareness.on("update", this._handleAwarenessUpdate.bind(this));
2894
- this._space.listen(this._channel, this._handleSpaceMessage.bind(this));
2895
- if (typeof window !== "undefined") {
2896
- window.addEventListener("beforeunload", this._handleBeforeUnload.bind(this));
2897
- } else if (typeof process !== "undefined") {
2898
- process.on("exit", this._handleBeforeUnload.bind(this));
2899
- }
2900
- const encoderAwarenessQuery = encoding.createEncoder();
2901
- encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness);
2902
- void this._space.postMessage(this._channel, encoding.toUint8Array(encoderAwarenessQuery));
2903
- const encoderAwarenessState = encoding.createEncoder();
2904
- encoding.writeVarUint(encoderAwarenessState, messageAwareness);
2905
- encoding.writeVarUint8Array(encoderAwarenessState, YP.encodeAwarenessUpdate(this.awareness, [
2906
- this._clientId
2907
- ]));
2908
- void this._space.postMessage(this._channel, encoding.toUint8Array(encoderAwarenessState));
2909
- }
2910
- get awareness() {
2911
- return this._awareness;
2912
- }
2913
- _handleAwarenessUpdate({ added, updated, removed }, origin) {
2914
- log4("awareness update", {
2915
- added,
2916
- updated,
2917
- removed,
2918
- origin
2919
- }, {
2920
- F: __dxlog_file5,
2921
- L: 122,
2922
- S: this,
2923
- C: (f, a) => f(...a)
2924
- });
2925
- const changedClients = added.concat(updated).concat(removed);
2926
- const encoderAwareness = encoding.createEncoder();
2927
- encoding.writeVarUint(encoderAwareness, messageAwareness);
2928
- encoding.writeVarUint8Array(encoderAwareness, YP.encodeAwarenessUpdate(this._awareness, changedClients));
2929
- void this._space.postMessage(this._channel, encoding.toUint8Array(encoderAwareness));
2930
- }
2931
- _handleSpaceMessage({ payload }) {
2932
- log4("space message", payload, {
2933
- F: __dxlog_file5,
2934
- L: 131,
2935
- S: this,
2936
- C: (f, a) => f(...a)
2937
- });
2938
- const data = new Uint8Array(Array.from(Object.values(payload)));
2939
- const encoder = this._readMessage(data);
2940
- if (encoder) {
2941
- void this._space.postMessage(this._channel, encoding.toUint8Array(encoder));
2942
- }
2943
- }
2944
- _readMessage(message) {
2945
- const decoder = decoding.createDecoder(message);
2946
- const encoder = encoding.createEncoder();
2947
- const messageType = decoding.readVarUint(decoder);
2948
- let sendReply = false;
2949
- switch (messageType) {
2950
- case messageAwareness: {
2951
- YP.applyAwarenessUpdate(this._awareness, decoding.readVarUint8Array(decoder), this);
2952
- break;
2953
- }
2954
- case messageQueryAwareness: {
2955
- encoding.writeVarUint(encoder, messageAwareness);
2956
- encoding.writeVarUint8Array(encoder, YP.encodeAwarenessUpdate(this._awareness, Array.from(this._awareness.getStates().keys())));
2957
- sendReply = true;
2958
- break;
2959
- }
2960
- default: {
2961
- console.error("Invalid message:", messageType);
2962
- return encoder;
2963
- }
2964
- }
2965
- if (!sendReply) {
2966
- return null;
2967
- }
2968
- return encoder;
2969
- }
2970
- _handleBeforeUnload() {
2971
- YP.removeAwarenessStates(this._awareness, [
2972
- this._clientId
2973
- ], "window unload");
2974
- }
2869
+ view
2870
+ ];
2975
2871
  };
2976
2872
 
2977
2873
  // packages/ui/react-ui-editor/src/hooks/useTextModel.ts
2978
- var modelState = StateField5.define({
2979
- create: () => void 0,
2980
- update: (model) => model
2981
- });
2874
+ import get4 from "lodash.get";
2875
+ import { useEffect as useEffect3, useState as useState3 } from "react";
2876
+ import { generateName } from "@dxos/display-name";
2877
+ import { getRawDoc } from "@dxos/echo-schema";
2878
+ import { invariant as invariant5 } from "@dxos/invariant";
2879
+ import { isAutomergeObject } from "@dxos/react-client/echo";
2880
+ import { isNotNullOrUndefined } from "@dxos/util";
2881
+ var __dxlog_file7 = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src/hooks/useTextModel.ts";
2982
2882
  var useTextModel = (props) => {
2983
2883
  const { identity, space, text } = props;
2984
- const [model, setModel] = useState2(() => createModel(props));
2985
- useEffect2(() => setModel(createModel(props)), [
2884
+ const [model, setModel] = useState3();
2885
+ useEffect3(() => setModel(createModel(props)), [
2986
2886
  identity,
2987
2887
  space,
2988
2888
  text
2989
2889
  ]);
2990
2890
  return model;
2991
2891
  };
2992
- var createModel = (props) => {
2993
- const { text } = props;
2994
- if (isAutomergeObject(text)) {
2995
- return createAutomergeModel(props);
2996
- } else if (text?.doc) {
2997
- return createYjsModel(props);
2998
- } else {
2999
- return void 0;
3000
- }
3001
- };
3002
-
3003
- // packages/ui/react-ui-editor/src/hooks/automerge.ts
3004
- var createAutomergeModel = ({ space, identity, text }) => {
2892
+ var createModel = ({ space, identity, text }) => {
2893
+ invariant5(isAutomergeObject(text), void 0, {
2894
+ F: __dxlog_file7,
2895
+ L: 35,
2896
+ S: void 0,
2897
+ A: [
2898
+ "isAutomergeObject(text)",
2899
+ ""
2900
+ ]
2901
+ });
3005
2902
  const obj = text;
3006
2903
  const doc = getRawDoc(obj, [
3007
2904
  obj.field
@@ -3010,7 +2907,7 @@ var createAutomergeModel = ({ space, identity, text }) => {
3010
2907
  space,
3011
2908
  channel: `automerge.awareness.${obj.id}`,
3012
2909
  info: {
3013
- displayName: identity?.profile?.displayName ?? "",
2910
+ displayName: identity ? identity.profile?.displayName ?? generateName(identity.identityKey.toHex()) : void 0,
3014
2911
  color: cursorColor.color,
3015
2912
  lightColor: cursorColor.light
3016
2913
  },
@@ -3020,7 +2917,6 @@ var createAutomergeModel = ({ space, identity, text }) => {
3020
2917
  id: obj.id,
3021
2918
  content: doc,
3022
2919
  text: () => get4(doc.handle.docSync(), doc.path),
3023
- // TODO(burdon): https://automerge.org/automerge/api-docs/js/functions/next.getCursor.html
3024
2920
  extension: [
3025
2921
  modelState.init(() => model),
3026
2922
  automerge3({
@@ -3040,6 +2936,7 @@ var createAutomergeModel = ({ space, identity, text }) => {
3040
2936
  export {
3041
2937
  AwarenessProvider,
3042
2938
  BaseTextEditor,
2939
+ Cursor,
3043
2940
  Doc,
3044
2941
  EditorModes,
3045
2942
  MarkdownEditor,
@@ -3047,7 +2944,6 @@ export {
3047
2944
  SpaceAwarenessProvider,
3048
2945
  TextEditor,
3049
2946
  TextKind,
3050
- YAwarenessProvider,
3051
2947
  YText,
3052
2948
  YXmlFragment,
3053
2949
  autocomplete,
@@ -3055,21 +2951,22 @@ export {
3055
2951
  awareness,
3056
2952
  basicBundle,
3057
2953
  blast,
2954
+ callbackWrapper,
3058
2955
  code2 as code,
3059
2956
  codeTheme,
3060
2957
  comments,
3061
- createAutomergeModel,
3062
- createYjsModel,
3063
2958
  defaultMarkdownSlots,
3064
2959
  defaultOptions,
3065
2960
  defaultSlots,
3066
2961
  defaultTextSlots,
3067
2962
  defaultTheme,
3068
2963
  getToken,
2964
+ heading2 as heading,
3069
2965
  hr,
3070
2966
  image,
3071
2967
  link,
3072
2968
  listener,
2969
+ logChanges,
3073
2970
  markdownBundle,
3074
2971
  markdownHighlightStyle,
3075
2972
  markdownTags,
@@ -3077,7 +2974,8 @@ export {
3077
2974
  markdownTheme,
3078
2975
  mention,
3079
2976
  modelState,
3080
- setCommentRange,
2977
+ outliner,
2978
+ setComments,
3081
2979
  setFocus,
3082
2980
  setSelection,
3083
2981
  table,
@@ -3085,7 +2983,8 @@ export {
3085
2983
  tasklist,
3086
2984
  textTheme,
3087
2985
  typewriter,
3088
- useTextModel,
3089
- yjs
2986
+ useComments,
2987
+ useTextEditor,
2988
+ useTextModel
3090
2989
  };
3091
2990
  //# sourceMappingURL=index.mjs.map