@dxos/react-ui-editor 0.3.11-main.d7d4c52 → 0.3.11-main.d8f9aa0

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