@dxos/react-ui-editor 0.3.11-main.00a28cb → 0.3.11-main.08776d7

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 (45) hide show
  1. package/dist/lib/browser/index.mjs +258 -252
  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/TextEditor.d.ts +4 -2
  5. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -1
  7. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  8. package/dist/types/src/extensions/automerge/defs.d.ts +5 -7
  9. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  10. package/dist/types/src/extensions/automerge/semaphore.d.ts +5 -4
  11. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  12. package/dist/types/src/extensions/comments.d.ts +12 -12
  13. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  14. package/dist/types/src/extensions/cursor.d.ts +3 -3
  15. package/dist/types/src/extensions/cursor.d.ts.map +1 -1
  16. package/dist/types/src/extensions/index.d.ts +1 -0
  17. package/dist/types/src/extensions/index.d.ts.map +1 -1
  18. package/dist/types/src/extensions/sync.d.ts +7 -0
  19. package/dist/types/src/extensions/sync.d.ts.map +1 -0
  20. package/dist/types/src/hooks/automerge.d.ts +2 -1
  21. package/dist/types/src/hooks/automerge.d.ts.map +1 -1
  22. package/dist/types/src/hooks/defs.d.ts +28 -0
  23. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  24. package/dist/types/src/hooks/index.d.ts +1 -0
  25. package/dist/types/src/hooks/index.d.ts.map +1 -1
  26. package/dist/types/src/hooks/useTextModel.d.ts +2 -27
  27. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  28. package/dist/types/src/hooks/yjs.d.ts +2 -1
  29. package/dist/types/src/hooks/yjs.d.ts.map +1 -1
  30. package/package.json +23 -23
  31. package/src/components/TextEditor/MarkdownEditor.stories.tsx +5 -5
  32. package/src/components/TextEditor/TextEditor.tsx +46 -38
  33. package/src/components/TextEditor/comments.stories.tsx +35 -28
  34. package/src/extensions/automerge/automerge.ts +10 -39
  35. package/src/extensions/automerge/defs.ts +8 -13
  36. package/src/extensions/automerge/semaphore.ts +14 -7
  37. package/src/extensions/comments.ts +137 -126
  38. package/src/extensions/cursor.ts +7 -3
  39. package/src/extensions/index.ts +1 -0
  40. package/src/extensions/sync.ts +15 -0
  41. package/src/hooks/automerge.ts +2 -1
  42. package/src/hooks/defs.ts +47 -0
  43. package/src/hooks/index.ts +1 -0
  44. package/src/hooks/useTextModel.ts +2 -45
  45. package/src/hooks/yjs.ts +2 -1
@@ -52,7 +52,6 @@ var autocomplete = ({ onSearch }) => {
52
52
 
53
53
  // packages/ui/react-ui-editor/src/extensions/automerge/automerge.ts
54
54
  import { StateField } from "@codemirror/state";
55
- import { ViewPlugin } from "@codemirror/view";
56
55
  import { next as A } from "@dxos/automerge/automerge";
57
56
 
58
57
  // packages/ui/react-ui-editor/src/extensions/automerge/cursor.ts
@@ -92,18 +91,15 @@ var cursorConverter = (handle, path) => ({
92
91
  });
93
92
 
94
93
  // packages/ui/react-ui-editor/src/extensions/automerge/defs.ts
95
- import { Annotation, StateEffect, Facet } from "@codemirror/state";
94
+ import { Annotation, StateEffect } from "@codemirror/state";
96
95
  var effectType = StateEffect.define({});
96
+ var reconcileAnnotationType = Annotation.define();
97
+ var getPath = (state, field) => state.field(field).path;
98
+ var getLastHeads = (state, field) => state.field(field).lastHeads;
97
99
  var updateHeads = (newHeads) => effectType.of({
98
100
  newHeads
99
101
  });
100
- var getLastHeads = (state, field) => state.field(field).lastHeads;
101
- var getPath = (state, field) => state.field(field).path;
102
- var reconcileAnnotationType = Annotation.define();
103
102
  var isReconcileTx = (tr) => !!tr.annotation(reconcileAnnotationType);
104
- var semaphoreFacet = Facet.define({
105
- combine: (values) => values.at(-1)
106
- });
107
103
 
108
104
  // packages/ui/react-ui-editor/src/extensions/automerge/semaphore.ts
109
105
  import { next as automerge2 } from "@dxos/automerge/automerge";
@@ -240,57 +236,60 @@ var charPath = (textPath, candidatePath) => {
240
236
 
241
237
  // packages/ui/react-ui-editor/src/extensions/automerge/semaphore.ts
242
238
  var PatchSemaphore = class {
239
+ // prettier-ignore
243
240
  constructor(_handle, _field) {
244
241
  this._handle = _handle;
245
242
  this._field = _field;
246
243
  this._inReconcile = false;
247
- this.reconcile = (view) => {
248
- if (this._inReconcile) {
249
- return;
250
- }
251
- this._inReconcile = true;
252
- const path = getPath(view.state, this._field);
253
- const oldHeads = getLastHeads(view.state, this._field);
254
- let selection = view.state.selection;
255
- const transactions = view.state.field(this._field).unreconciledTransactions.filter((tx) => !isReconcileTx(tx));
256
- const toInvert = transactions.slice().reverse();
257
- for (const tx of toInvert) {
258
- const inverted = tx.changes.invert(tx.startState.doc);
259
- selection = selection.map(inverted);
260
- view.dispatch({
261
- changes: inverted,
262
- annotations: reconcileAnnotationType.of(true)
263
- });
264
- }
265
- let newHeads = updateAutomerge(this._field, this._handle, transactions, view.state);
266
- if (newHeads === null || newHeads === void 0) {
267
- newHeads = automerge2.getHeads(this._handle.docSync());
268
- }
269
- const diff = automerge2.equals(oldHeads, newHeads) ? [] : automerge2.diff(this._handle.docSync(), oldHeads, newHeads);
270
- updateCodeMirror(view, selection, path, diff);
244
+ }
245
+ // NOTE: Cannot destruct view.state.
246
+ reconcile(view) {
247
+ if (this._inReconcile) {
248
+ return;
249
+ }
250
+ this._inReconcile = true;
251
+ const path = getPath(view.state, this._field);
252
+ const oldHeads = getLastHeads(view.state, this._field);
253
+ let selection = view.state.selection;
254
+ const transactions = view.state.field(this._field).unreconciledTransactions.filter((tx) => !isReconcileTx(tx));
255
+ const toInvert = transactions.slice().reverse();
256
+ for (const tx of toInvert) {
257
+ const inverted = tx.changes.invert(tx.startState.doc);
258
+ selection = selection.map(inverted);
271
259
  view.dispatch({
272
- effects: updateHeads(newHeads),
273
- annotations: reconcileAnnotationType.of({})
260
+ changes: inverted,
261
+ annotations: reconcileAnnotationType.of(true)
274
262
  });
275
- this._inReconcile = false;
276
- };
263
+ }
264
+ let newHeads = updateAutomerge(this._field, this._handle, transactions, view.state);
265
+ if (newHeads === null || newHeads === void 0) {
266
+ newHeads = automerge2.getHeads(this._handle.docSync());
267
+ }
268
+ const diff = automerge2.equals(oldHeads, newHeads) ? [] : automerge2.diff(this._handle.docSync(), oldHeads, newHeads);
269
+ updateCodeMirror(view, selection, path, diff);
270
+ view.dispatch({
271
+ effects: updateHeads(newHeads),
272
+ annotations: reconcileAnnotationType.of({})
273
+ });
274
+ this._inReconcile = false;
277
275
  }
278
276
  };
279
277
 
280
278
  // packages/ui/react-ui-editor/src/extensions/cursor.ts
281
- import { Facet as Facet2 } from "@codemirror/state";
279
+ import { Facet } from "@codemirror/state";
282
280
  var defaultCursorConverter = {
283
281
  toCursor: (position) => position.toString(),
284
282
  fromCursor: (cursor) => parseInt(cursor)
285
283
  };
286
- var Cursor = class {
284
+ var Cursor = class _Cursor {
287
285
  static {
288
- this.converter = Facet2.define({
286
+ this.converter = Facet.define({
289
287
  combine: (providers) => providers[0] ?? defaultCursorConverter
290
288
  });
291
289
  }
292
290
  static {
293
- this.getCursorFromRange = (cursorConverter3, range) => {
291
+ this.getCursorFromRange = (state, range) => {
292
+ const cursorConverter3 = state.facet(_Cursor.converter);
294
293
  const from = cursorConverter3.toCursor(range.from);
295
294
  const to = cursorConverter3.toCursor(range.to, -1);
296
295
  return [
@@ -300,7 +299,8 @@ var Cursor = class {
300
299
  };
301
300
  }
302
301
  static {
303
- this.getRangeFromCursor = (cursorConverter3, cursor) => {
302
+ this.getRangeFromCursor = (state, cursor) => {
303
+ const cursorConverter3 = state.facet(_Cursor.converter);
304
304
  const parts = cursor.split(":");
305
305
  const from = cursorConverter3.fromCursor(parts[0]);
306
306
  const to = cursorConverter3.fromCursor(parts[1]);
@@ -312,19 +312,25 @@ var Cursor = class {
312
312
  }
313
313
  };
314
314
 
315
+ // packages/ui/react-ui-editor/src/extensions/sync.ts
316
+ import { Facet as Facet2 } from "@codemirror/state";
317
+ var syncFacet = Facet2.define({
318
+ combine: (values) => values.at(-1)
319
+ });
320
+
315
321
  // packages/ui/react-ui-editor/src/extensions/automerge/automerge.ts
316
322
  var automerge3 = ({ handle, path }) => {
317
323
  const stateField = StateField.define({
318
324
  create: () => ({
325
+ path: path.slice(),
319
326
  lastHeads: A.getHeads(handle.docSync()),
320
- unreconciledTransactions: [],
321
- path: path.slice()
327
+ unreconciledTransactions: []
322
328
  }),
323
329
  update: (value, tr) => {
324
330
  const result = {
331
+ path: path.slice(),
325
332
  lastHeads: value.lastHeads,
326
- unreconciledTransactions: value.unreconciledTransactions.slice(),
327
- path: path.slice()
333
+ unreconciledTransactions: value.unreconciledTransactions.slice()
328
334
  };
329
335
  let clearUnreconciled = false;
330
336
  for (const effect of tr.effects) {
@@ -344,38 +350,18 @@ var automerge3 = ({ handle, path }) => {
344
350
  }
345
351
  });
346
352
  const semaphore = new PatchSemaphore(handle, stateField);
347
- const viewPlugin = ViewPlugin.fromClass(class AutomergeViewPlugin {
348
- constructor(_view) {
349
- this._view = _view;
350
- this._handleChange = () => {
351
- this._view.state.facet(semaphoreFacet).reconcile(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(this._view);
359
- });
360
- }
361
- }
362
- destroy() {
363
- handle.addListener("change", this._handleChange);
364
- }
365
- });
366
353
  return {
367
354
  extension: [
368
355
  Cursor.converter.of(cursorConverter(handle, path)),
369
- semaphoreFacet.of(semaphore),
370
- stateField,
371
- viewPlugin
356
+ syncFacet.of(semaphore),
357
+ stateField
372
358
  ]
373
359
  };
374
360
  };
375
361
 
376
362
  // packages/ui/react-ui-editor/src/extensions/awareness.ts
377
363
  import { Annotation as Annotation2, Facet as Facet3, RangeSet } from "@codemirror/state";
378
- import { Decoration, EditorView, ViewPlugin as ViewPlugin2, WidgetType } from "@codemirror/view";
364
+ import { Decoration, EditorView, ViewPlugin, WidgetType } from "@codemirror/view";
379
365
  import { Event } from "@dxos/async";
380
366
  import { Context } from "@dxos/context";
381
367
  var dummyProvider = {
@@ -395,7 +381,7 @@ var RemoteSelectionChangedAnnotation = Annotation2.define();
395
381
  var awareness = (provider = dummyProvider) => {
396
382
  return [
397
383
  AwarenessProvider.of(provider),
398
- ViewPlugin2.fromClass(RemoteSelectionsDecorator, {
384
+ ViewPlugin.fromClass(RemoteSelectionsDecorator, {
399
385
  decorations: (value) => value.decorations
400
386
  }),
401
387
  styles
@@ -934,7 +920,7 @@ var random = (min, max) => {
934
920
  };
935
921
 
936
922
  // packages/ui/react-ui-editor/src/extensions/code.ts
937
- import { ViewPlugin as ViewPlugin3, EditorView as EditorView4, Decoration as Decoration2 } from "@codemirror/view";
923
+ import { ViewPlugin as ViewPlugin2, EditorView as EditorView4, Decoration as Decoration2 } from "@codemirror/view";
938
924
  import { mx as mx2 } from "@dxos/react-ui-theme";
939
925
 
940
926
  // packages/ui/react-ui-editor/src/styles/cursors.ts
@@ -1076,7 +1062,7 @@ var code2 = () => {
1076
1062
  return Decoration2.set(decorations);
1077
1063
  };
1078
1064
  return [
1079
- ViewPlugin3.fromClass(class {
1065
+ ViewPlugin2.fromClass(class {
1080
1066
  constructor(view) {
1081
1067
  this.hasFocus = false;
1082
1068
  this.decorations = buildDecorations4(view);
@@ -1140,8 +1126,8 @@ var marks = {
1140
1126
  })
1141
1127
  };
1142
1128
  var setFocus = (view, thread) => {
1143
- const range = view.state.field(commentsStateField).ranges.find((range2) => range2.id === thread);
1144
- if (!range) {
1129
+ const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === thread);
1130
+ if (!comment) {
1145
1131
  return;
1146
1132
  }
1147
1133
  view.dispatch({
@@ -1149,39 +1135,41 @@ var setFocus = (view, thread) => {
1149
1135
  active: thread
1150
1136
  }),
1151
1137
  selection: {
1152
- anchor: range.from
1138
+ anchor: comment.range.from
1153
1139
  },
1154
1140
  scrollIntoView: true
1155
1141
  });
1156
1142
  };
1157
- var setCommentRange = StateEffect2.define();
1143
+ var setComments = StateEffect2.define();
1158
1144
  var setSelection = StateEffect2.define();
1159
1145
  var setCommentState = StateEffect2.define();
1160
- var commentsStateField = StateField2.define({
1146
+ var commentsState = StateField2.define({
1161
1147
  create: () => ({
1162
- ranges: []
1148
+ comments: [],
1149
+ selection: {}
1163
1150
  }),
1164
1151
  update: (value, tr) => {
1165
- const cursorConverter3 = tr.state.facet(Cursor.converter);
1166
1152
  for (const effect of tr.effects) {
1167
1153
  if (effect.is(setSelection)) {
1168
1154
  return {
1169
1155
  ...value,
1170
- ...effect.value
1156
+ selection: effect.value
1171
1157
  };
1172
1158
  }
1173
- if (effect.is(setCommentRange)) {
1174
- const { comments: comments2 } = effect.value;
1175
- const ranges = comments2.map((comment) => {
1176
- const range = Cursor.getRangeFromCursor(cursorConverter3, comment.cursor);
1159
+ if (effect.is(setComments)) {
1160
+ const comments2 = effect.value.map((comment) => {
1161
+ if (!comment.cursor) {
1162
+ return void 0;
1163
+ }
1164
+ const range = Cursor.getRangeFromCursor(tr.state, comment.cursor);
1177
1165
  return range && {
1178
- ...comment,
1179
- ...range
1166
+ comment,
1167
+ range
1180
1168
  };
1181
1169
  }).filter(nonNullable);
1182
1170
  return {
1183
1171
  ...value,
1184
- ranges
1172
+ comments: comments2
1185
1173
  };
1186
1174
  }
1187
1175
  if (effect.is(setCommentState)) {
@@ -1192,38 +1180,36 @@ var commentsStateField = StateField2.define({
1192
1180
  }
1193
1181
  });
1194
1182
  var highlightDecorations = EditorView5.decorations.compute([
1195
- commentsStateField
1183
+ commentsState
1196
1184
  ], (state) => {
1197
- const { active, ranges } = state.field(commentsStateField);
1198
- const decorations = sortBy(ranges ?? [], (range) => range.from)?.flatMap((selection) => {
1199
- if (selection.from === selection.to) {
1185
+ const { selection: { active }, comments: comments2 } = state.field(commentsState);
1186
+ const decorations = sortBy(comments2 ?? [], (range) => range.range.from)?.flatMap((comment) => {
1187
+ const range = comment.range;
1188
+ if (!range || range.from === range.to) {
1189
+ console.warn("Invalid range:", range);
1200
1190
  return void 0;
1201
1191
  }
1202
- const range = {
1203
- from: selection.from,
1204
- to: selection.to
1205
- };
1206
- if (selection.id === active) {
1192
+ if (comment.comment.id === active) {
1207
1193
  return marks.highlightActive.range(range.from, range.to);
1208
1194
  } else {
1209
1195
  return marks.highlight.range(range.from, range.to);
1210
1196
  }
1211
- }).filter(nonNullable) ?? [];
1197
+ }).filter(nonNullable);
1212
1198
  return Decoration3.set(decorations);
1213
1199
  });
1214
1200
  var trackPastedComments = (onUpdate) => {
1215
1201
  let tracked = null;
1216
1202
  const handleTrack = (event, view) => {
1217
- const comments2 = view.state.field(commentsStateField);
1203
+ const comments2 = view.state.field(commentsState);
1218
1204
  const { main } = view.state.selection;
1219
- const selectedRanges = comments2.ranges.filter((range) => range.from >= main.from && range.to <= main.to && range.from < range.to);
1205
+ const selectedRanges = comments2.comments.filter(({ range }) => range.from >= main.from && range.to <= main.to && range.from < range.to);
1220
1206
  if (!selectedRanges.length) {
1221
1207
  tracked = null;
1222
1208
  } else {
1223
1209
  tracked = {
1224
1210
  text: view.state.doc.slice(main.from, main.to),
1225
- comments: selectedRanges.map((range) => ({
1226
- id: range.id,
1211
+ comments: selectedRanges.map(({ comment, range }) => ({
1212
+ id: comment.id,
1227
1213
  from: range.from - main.from,
1228
1214
  to: range.to - main.from
1229
1215
  }))
@@ -1250,18 +1236,14 @@ var trackPastedComments = (onUpdate) => {
1250
1236
  }
1251
1237
  });
1252
1238
  if (found > -1) {
1253
- const comments2 = tracked.comments;
1254
- view.state.facet(semaphoreFacet).reconcile(view);
1255
- const active = state.field(commentsStateField).ranges;
1256
- for (const moved of comments2) {
1257
- if (active.some((range) => range.id === moved.id && range.from === range.to)) {
1258
- const range = {
1259
- from: found + moved.from,
1260
- to: found + moved.to
1261
- };
1262
- const cursor = Cursor.getCursorFromRange(state.facet(Cursor.converter), range);
1263
- onUpdate(moved.id, cursor);
1264
- }
1239
+ state.facet(syncFacet)?.reconcile(view);
1240
+ for (const moved of tracked.comments) {
1241
+ const range = {
1242
+ from: found + moved.from,
1243
+ to: found + moved.to
1244
+ };
1245
+ const cursor = Cursor.getCursorFromRange(state, range);
1246
+ onUpdate(moved.id, cursor);
1265
1247
  }
1266
1248
  }
1267
1249
  tracked = null;
@@ -1274,10 +1256,9 @@ var comments = (options = {}) => {
1274
1256
  const { key: shortcut = "meta-'" } = options;
1275
1257
  const handleSelect = debounce((state) => options.onSelect?.(state), 200);
1276
1258
  const createCommentThread = (view) => {
1277
- const cursorConverter3 = view.state.facet(Cursor.converter);
1278
1259
  invariant2(options.onCreate, void 0, {
1279
1260
  F: __dxlog_file3,
1280
- L: 264,
1261
+ L: 262,
1281
1262
  S: void 0,
1282
1263
  A: [
1283
1264
  "options.onCreate",
@@ -1288,7 +1269,15 @@ var comments = (options = {}) => {
1288
1269
  if (from === to) {
1289
1270
  return false;
1290
1271
  }
1291
- const cursor = Cursor.getCursorFromRange(cursorConverter3, {
1272
+ if (to === view.state.doc.length) {
1273
+ view.dispatch({
1274
+ changes: {
1275
+ from: to,
1276
+ insert: "\n"
1277
+ }
1278
+ });
1279
+ }
1280
+ const cursor = Cursor.getCursorFromRange(view.state, {
1292
1281
  from,
1293
1282
  to
1294
1283
  });
@@ -1309,7 +1298,7 @@ var comments = (options = {}) => {
1309
1298
  return false;
1310
1299
  };
1311
1300
  return [
1312
- commentsStateField,
1301
+ commentsState,
1313
1302
  highlightDecorations,
1314
1303
  styles3,
1315
1304
  //
@@ -1318,6 +1307,7 @@ var comments = (options = {}) => {
1318
1307
  options.onCreate ? keymap3.of([
1319
1308
  {
1320
1309
  key: shortcut,
1310
+ // TODO(burdon): Check if there's a better way?
1321
1311
  run: callbackWrapper(createCommentThread)
1322
1312
  }
1323
1313
  ]) : [],
@@ -1352,68 +1342,68 @@ var comments = (options = {}) => {
1352
1342
  hoverTime: 1e3
1353
1343
  }) : [],
1354
1344
  //
1355
- // Monitor cursor movement and text updates.
1356
- // TODO(burdon): Is there a better (finer grained) way to do this?
1345
+ // Track deleted ranges and update ranges for decorations.
1357
1346
  //
1358
1347
  EditorView5.updateListener.of(({ view, state, changes }) => {
1359
- const cursorConverter3 = view.state.facet(Cursor.converter);
1360
- {
1361
- let mod = false;
1362
- const { active, ranges } = state.field(commentsStateField);
1363
- changes.iterChanges((from, to, from2, to2) => {
1364
- ranges.forEach((range) => {
1365
- if (from2 === to2) {
1366
- const newRange = Cursor.getRangeFromCursor(cursorConverter3, range.cursor);
1367
- if (!newRange || newRange.to - newRange.from === 0) {
1368
- options.onDelete?.(range.id);
1369
- }
1370
- }
1371
- if (from <= range.to) {
1372
- const newRange = Cursor.getRangeFromCursor(cursorConverter3, range.cursor);
1373
- Object.assign(range, newRange);
1374
- mod = true;
1348
+ let mod = false;
1349
+ const { comments: comments2, ...value } = state.field(commentsState);
1350
+ changes.iterChanges((from, to, from2, to2) => {
1351
+ comments2.forEach(({ comment, range }) => {
1352
+ if (from2 === to2) {
1353
+ const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor);
1354
+ if (!newRange || newRange.to - newRange.from === 0) {
1355
+ options.onDelete?.(comment.id);
1375
1356
  }
1376
- });
1357
+ }
1358
+ if (from <= range.to) {
1359
+ const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor);
1360
+ Object.assign(range, newRange);
1361
+ mod = true;
1362
+ }
1363
+ });
1364
+ });
1365
+ if (mod) {
1366
+ view.dispatch({
1367
+ effects: setCommentState.of({
1368
+ comments: comments2,
1369
+ ...value
1370
+ })
1377
1371
  });
1378
- if (mod) {
1379
- view.dispatch({
1380
- effects: setCommentState.of({
1381
- active,
1382
- ranges
1383
- })
1384
- });
1385
- }
1386
1372
  }
1387
- {
1388
- const { head } = state.selection.main;
1389
- let min = Infinity;
1390
- const { active, closest, ranges } = state.field(commentsStateField);
1391
- const selected = {
1392
- active: void 0,
1393
- closest: void 0
1394
- };
1395
- ranges.forEach((comment) => {
1396
- const d = Math.min(Math.abs(head - comment.from), Math.abs(head - comment.to));
1397
- if (head >= comment.from && head <= comment.to) {
1398
- selected.active = comment.id;
1399
- }
1373
+ }),
1374
+ //
1375
+ // Track selection/proximity.
1376
+ //
1377
+ EditorView5.updateListener.of(({ view, state, changes }) => {
1378
+ let min = Infinity;
1379
+ const { selection: { active, closest }, comments: comments2 } = state.field(commentsState);
1380
+ const { head } = state.selection.main;
1381
+ const selection = {};
1382
+ comments2.forEach(({ comment, range }) => {
1383
+ if (head >= range.from && head <= range.to) {
1384
+ selection.active = comment.id;
1385
+ selection.closest = void 0;
1386
+ }
1387
+ if (!selection.active) {
1388
+ const d = Math.min(Math.abs(head - range.from), Math.abs(head - range.to));
1400
1389
  if (d < min) {
1401
- selected.closest = comment.id;
1390
+ selection.closest = comment.id;
1402
1391
  min = d;
1403
1392
  }
1404
- });
1405
- if (selected.active !== active || selected.closest !== closest) {
1406
- view.dispatch({
1407
- effects: setSelection.of(selected)
1408
- });
1409
- handleSelect({
1410
- ...selected,
1411
- ranges: ranges.map((range) => ({
1412
- ...range,
1413
- location: view.coordsAtPos(range.from)
1414
- }))
1415
- });
1416
1393
  }
1394
+ });
1395
+ if (selection.active !== active || selection.closest !== closest) {
1396
+ view.dispatch({
1397
+ effects: setSelection.of(selection)
1398
+ });
1399
+ handleSelect({
1400
+ selection,
1401
+ comments: comments2.map(({ comment, range }) => ({
1402
+ comment,
1403
+ range,
1404
+ location: view.coordsAtPos(range.from)
1405
+ }))
1406
+ });
1417
1407
  }
1418
1408
  }),
1419
1409
  options.onUpdate ? trackPastedComments(options.onUpdate) : []
@@ -1421,7 +1411,7 @@ var comments = (options = {}) => {
1421
1411
  };
1422
1412
 
1423
1413
  // packages/ui/react-ui-editor/src/extensions/hr.ts
1424
- import { Decoration as Decoration4, EditorView as EditorView6, MatchDecorator, ViewPlugin as ViewPlugin4, WidgetType as WidgetType2 } from "@codemirror/view";
1414
+ import { Decoration as Decoration4, EditorView as EditorView6, MatchDecorator, ViewPlugin as ViewPlugin3, WidgetType as WidgetType2 } from "@codemirror/view";
1425
1415
  var styles4 = EditorView6.baseTheme({
1426
1416
  "& .cm-hr": {
1427
1417
  // TODO(burdon): ???
@@ -1452,7 +1442,7 @@ var placeholderMatcher = new MatchDecorator({
1452
1442
  });
1453
1443
  var hr = () => [
1454
1444
  styles4,
1455
- ViewPlugin4.fromClass(class {
1445
+ ViewPlugin3.fromClass(class {
1456
1446
  constructor(view) {
1457
1447
  this.rules = placeholderMatcher.createDeco(view);
1458
1448
  }
@@ -1539,11 +1529,11 @@ var ImageWidget = class extends WidgetType3 {
1539
1529
  // packages/ui/react-ui-editor/src/extensions/link.ts
1540
1530
  import { syntaxTree as syntaxTree2 } from "@codemirror/language";
1541
1531
  import { RangeSetBuilder } from "@codemirror/state";
1542
- import { hoverTooltip as hoverTooltip2, Decoration as Decoration6, ViewPlugin as ViewPlugin5, WidgetType as WidgetType4 } from "@codemirror/view";
1532
+ import { hoverTooltip as hoverTooltip2, Decoration as Decoration6, ViewPlugin as ViewPlugin4, WidgetType as WidgetType4 } from "@codemirror/view";
1543
1533
  import { tooltipContent } from "@dxos/react-ui-theme";
1544
1534
  var link = (options = {}) => {
1545
1535
  const extensions = [
1546
- ViewPlugin5.fromClass(class {
1536
+ ViewPlugin4.fromClass(class {
1547
1537
  constructor(view) {
1548
1538
  this.decorations = buildDecorations2(view, options);
1549
1539
  }
@@ -2068,7 +2058,7 @@ var TableWidget = class extends WidgetType5 {
2068
2058
  // packages/ui/react-ui-editor/src/extensions/tasklist.ts
2069
2059
  import { syntaxTree as syntaxTree4 } from "@codemirror/language";
2070
2060
  import { RangeSetBuilder as RangeSetBuilder3 } from "@codemirror/state";
2071
- import { EditorView as EditorView11, Decoration as Decoration8, WidgetType as WidgetType6, ViewPlugin as ViewPlugin6 } from "@codemirror/view";
2061
+ import { EditorView as EditorView11, Decoration as Decoration8, WidgetType as WidgetType6, ViewPlugin as ViewPlugin5 } from "@codemirror/view";
2072
2062
  var CheckboxWidget = class extends WidgetType6 {
2073
2063
  constructor(_checked) {
2074
2064
  super();
@@ -2112,7 +2102,7 @@ var uncheckedDeco = Decoration8.replace({
2112
2102
  widget: new CheckboxWidget(false)
2113
2103
  });
2114
2104
  var tasklist = (options = {}) => {
2115
- return ViewPlugin6.fromClass(class {
2105
+ return ViewPlugin5.fromClass(class {
2116
2106
  constructor(view) {
2117
2107
  this.decorations = buildDecorations3(view);
2118
2108
  }
@@ -2467,25 +2457,20 @@ var EditorModes = [
2467
2457
  "default",
2468
2458
  "vim"
2469
2459
  ];
2470
- var BaseTextEditor = /* @__PURE__ */ forwardRef(({ model, autofocus, readonly, selection, comments: comments2, extensions = [], editorMode, theme, slots = defaultSlots }, forwardedRef) => {
2471
- const tabsterDOMAttribute = useFocusableGroup({
2472
- tabBehavior: "limited"
2473
- });
2474
- const { themeMode } = useThemeContext();
2475
- const rootRef = useRef(null);
2476
- const [view, setView] = useState(null);
2477
- useImperativeHandle(forwardedRef, () => view, [
2478
- view
2479
- ]);
2460
+ var useComments = (view, comments2) => {
2480
2461
  useEffect(() => {
2481
- if (autofocus) {
2482
- view?.focus();
2462
+ if (view && comments2 !== void 0) {
2463
+ view.dispatch({
2464
+ effects: setComments.of(comments2)
2465
+ });
2483
2466
  }
2484
2467
  }, [
2485
- autofocus,
2486
- view
2468
+ view,
2469
+ comments2
2487
2470
  ]);
2488
- const { awareness: awareness2, peer } = model;
2471
+ };
2472
+ var useAwareness = ({ awareness: awareness2, peer }) => {
2473
+ const { themeMode } = useThemeContext();
2489
2474
  useEffect(() => {
2490
2475
  if (awareness2 && peer) {
2491
2476
  awareness2.setLocalStateField("user", {
@@ -2506,19 +2491,27 @@ var BaseTextEditor = /* @__PURE__ */ forwardRef(({ model, autofocus, readonly, s
2506
2491
  peer,
2507
2492
  themeMode
2508
2493
  ]);
2494
+ };
2495
+ var BaseTextEditor = /* @__PURE__ */ forwardRef(({ model, autofocus, readonly, selection, comments: comments2, extensions = [], editorMode, theme, slots = defaultSlots }, forwardedRef) => {
2496
+ const tabsterDOMAttribute = useFocusableGroup({
2497
+ tabBehavior: "limited"
2498
+ });
2499
+ const { themeMode } = useThemeContext();
2500
+ const rootRef = useRef(null);
2501
+ const [view, setView] = useState(null);
2502
+ useImperativeHandle(forwardedRef, () => view, [
2503
+ view
2504
+ ]);
2509
2505
  useEffect(() => {
2510
- if (view && comments2 !== void 0) {
2511
- view.dispatch({
2512
- effects: setCommentRange.of({
2513
- model,
2514
- comments: comments2
2515
- })
2516
- });
2506
+ if (autofocus) {
2507
+ view?.focus();
2517
2508
  }
2518
2509
  }, [
2519
2510
  view,
2520
- comments2
2511
+ autofocus
2521
2512
  ]);
2513
+ useAwareness(model);
2514
+ useComments(view, comments2);
2522
2515
  useEffect(() => {
2523
2516
  if (!model || !rootRef.current) {
2524
2517
  return;
@@ -2529,14 +2522,14 @@ var BaseTextEditor = /* @__PURE__ */ forwardRef(({ model, autofocus, readonly, s
2529
2522
  selection,
2530
2523
  extensions: [
2531
2524
  readonly && EditorState2.readOnly.of(readonly),
2532
- // TODO(burdon): Factor out VIM mode? (manage via MarkdownPlugin).
2533
- editorMode === "vim" && vim(),
2534
2525
  // Theme.
2535
2526
  // TODO(burdon): Make theme configurable.
2536
2527
  EditorView12.baseTheme(defaultTheme),
2537
2528
  EditorView12.theme(theme ?? {}),
2538
2529
  EditorView12.darkTheme.of(themeMode === "dark"),
2539
- // Storage and replication.
2530
+ // TODO(burdon): Factor out VIM mode? (manage via MarkdownPlugin).
2531
+ editorMode === "vim" && vim(),
2532
+ // Storage and replication (NOTE: must come before other extensions).
2540
2533
  model.extension,
2541
2534
  // Custom.
2542
2535
  ...extensions
@@ -2545,7 +2538,15 @@ var BaseTextEditor = /* @__PURE__ */ forwardRef(({ model, autofocus, readonly, s
2545
2538
  view?.destroy();
2546
2539
  const newView = new EditorView12({
2547
2540
  parent: rootRef.current,
2548
- state
2541
+ state,
2542
+ // NOTE: Uncomment to spy on all transactions.
2543
+ // https://codemirror.net/docs/ref/#view.EditorView.dispatch
2544
+ dispatch: (transaction, view2) => {
2545
+ view2.update([
2546
+ transaction
2547
+ ]);
2548
+ view2.state.facet(syncFacet)?.reconcile(view2);
2549
+ }
2549
2550
  });
2550
2551
  setView(newView);
2551
2552
  return () => {
@@ -2563,7 +2564,9 @@ var BaseTextEditor = /* @__PURE__ */ forwardRef(({ model, autofocus, readonly, s
2563
2564
  const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
2564
2565
  switch (key) {
2565
2566
  case "Escape": {
2566
- editorMode === "vim" && (altKey || shiftKey || metaKey || ctrlKey) && rootRef.current?.focus();
2567
+ if (editorMode === "vim" && (altKey || shiftKey || metaKey || ctrlKey)) {
2568
+ rootRef.current?.focus();
2569
+ }
2567
2570
  break;
2568
2571
  }
2569
2572
  }
@@ -2741,10 +2744,50 @@ var SpaceAwarenessProvider = class {
2741
2744
  }
2742
2745
  };
2743
2746
 
2744
- // packages/ui/react-ui-editor/src/hooks/useTextModel.ts
2747
+ // packages/ui/react-ui-editor/src/hooks/defs.ts
2745
2748
  import { StateField as StateField5 } from "@codemirror/state";
2746
- import { useEffect as useEffect2, useState as useState2 } from "react";
2747
- import { isAutomergeObject } from "@dxos/react-client/echo";
2749
+ var modelState = StateField5.define({
2750
+ create: () => void 0,
2751
+ update: (model) => model
2752
+ });
2753
+
2754
+ // packages/ui/react-ui-editor/src/hooks/automerge.ts
2755
+ var createAutomergeModel = ({ space, identity, text }) => {
2756
+ const obj = text;
2757
+ const doc = getRawDoc(obj, [
2758
+ obj.field
2759
+ ]);
2760
+ const awarenessProvider = space && new SpaceAwarenessProvider({
2761
+ space,
2762
+ channel: `automerge.awareness.${obj.id}`,
2763
+ info: {
2764
+ displayName: identity?.profile?.displayName ?? "",
2765
+ color: cursorColor.color,
2766
+ lightColor: cursorColor.light
2767
+ },
2768
+ peerId: identity?.identityKey.toHex() ?? "Anonymous"
2769
+ });
2770
+ const model = {
2771
+ id: obj.id,
2772
+ content: doc,
2773
+ text: () => get4(doc.handle.docSync(), doc.path),
2774
+ // TODO(burdon): https://automerge.org/automerge/api-docs/js/functions/next.getCursor.html
2775
+ extension: [
2776
+ modelState.init(() => model),
2777
+ automerge3({
2778
+ handle: doc.handle,
2779
+ path: doc.path
2780
+ }),
2781
+ awarenessProvider && AwarenessProvider.of(awarenessProvider),
2782
+ awareness()
2783
+ ].filter(isNotNullOrUndefined),
2784
+ peer: identity ? {
2785
+ id: identity.identityKey.toHex(),
2786
+ name: identity.profile?.displayName
2787
+ } : void 0
2788
+ };
2789
+ return model;
2790
+ };
2748
2791
 
2749
2792
  // packages/ui/react-ui-editor/src/hooks/yjs.ts
2750
2793
  import * as decoding from "lib0/decoding";
@@ -2757,7 +2800,7 @@ var __dxlog_file5 = "/home/runner/work/dxos/dxos/packages/ui/react-ui-editor/src
2757
2800
  var createYjsModel = ({ identity, space, text }) => {
2758
2801
  invariant4(text?.doc && text?.content, void 0, {
2759
2802
  F: __dxlog_file5,
2760
- L: 21,
2803
+ L: 22,
2761
2804
  S: void 0,
2762
2805
  A: [
2763
2806
  "text?.doc && text?.content",
@@ -2822,7 +2865,7 @@ var YAwarenessProvider = class extends Observable {
2822
2865
  origin
2823
2866
  }, {
2824
2867
  F: __dxlog_file5,
2825
- L: 100,
2868
+ L: 101,
2826
2869
  S: this,
2827
2870
  C: (f, a) => f(...a)
2828
2871
  });
@@ -2835,7 +2878,7 @@ var YAwarenessProvider = class extends Observable {
2835
2878
  _handleSpaceMessage({ payload }) {
2836
2879
  log3("space message", payload, {
2837
2880
  F: __dxlog_file5,
2838
- L: 109,
2881
+ L: 110,
2839
2882
  S: this,
2840
2883
  C: (f, a) => f(...a)
2841
2884
  });
@@ -2879,10 +2922,8 @@ var YAwarenessProvider = class extends Observable {
2879
2922
  };
2880
2923
 
2881
2924
  // packages/ui/react-ui-editor/src/hooks/useTextModel.ts
2882
- var modelState = StateField5.define({
2883
- create: () => void 0,
2884
- update: (model) => model
2885
- });
2925
+ import { useEffect as useEffect2, useState as useState2 } from "react";
2926
+ import { isAutomergeObject } from "@dxos/react-client/echo";
2886
2927
  var useTextModel = (props) => {
2887
2928
  const { identity, space, text } = props;
2888
2929
  const [model, setModel] = useState2();
@@ -2903,44 +2944,6 @@ var createModel = (props) => {
2903
2944
  return void 0;
2904
2945
  }
2905
2946
  };
2906
-
2907
- // packages/ui/react-ui-editor/src/hooks/automerge.ts
2908
- var createAutomergeModel = ({ space, identity, text }) => {
2909
- const obj = text;
2910
- const doc = getRawDoc(obj, [
2911
- obj.field
2912
- ]);
2913
- const awarenessProvider = space && new SpaceAwarenessProvider({
2914
- space,
2915
- channel: `automerge.awareness.${obj.id}`,
2916
- info: {
2917
- displayName: identity?.profile?.displayName ?? "",
2918
- color: cursorColor.color,
2919
- lightColor: cursorColor.light
2920
- },
2921
- peerId: identity?.identityKey.toHex() ?? "Anonymous"
2922
- });
2923
- const model = {
2924
- id: obj.id,
2925
- content: doc,
2926
- text: () => get4(doc.handle.docSync(), doc.path),
2927
- // TODO(burdon): https://automerge.org/automerge/api-docs/js/functions/next.getCursor.html
2928
- extension: [
2929
- modelState.init(() => model),
2930
- automerge3({
2931
- handle: doc.handle,
2932
- path: doc.path
2933
- }),
2934
- awarenessProvider && AwarenessProvider.of(awarenessProvider),
2935
- awareness()
2936
- ].filter(isNotNullOrUndefined),
2937
- peer: identity ? {
2938
- id: identity.identityKey.toHex(),
2939
- name: identity.profile?.displayName
2940
- } : void 0
2941
- };
2942
- return model;
2943
- };
2944
2947
  export {
2945
2948
  AwarenessProvider,
2946
2949
  BaseTextEditor,
@@ -2984,14 +2987,17 @@ export {
2984
2987
  mention,
2985
2988
  modelState,
2986
2989
  outliner,
2987
- setCommentRange,
2990
+ setComments,
2988
2991
  setFocus,
2989
2992
  setSelection,
2993
+ syncFacet,
2990
2994
  table,
2991
2995
  tags2 as tags,
2992
2996
  tasklist,
2993
2997
  textTheme,
2994
2998
  typewriter,
2999
+ useAwareness,
3000
+ useComments,
2995
3001
  useTextModel,
2996
3002
  yjs
2997
3003
  };