@dxos/react-ui-editor 0.3.11-main.8be6c8a → 0.3.11-main.90a8b71

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 (60) hide show
  1. package/dist/lib/browser/index.mjs +146 -282
  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.map +1 -1
  5. package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -7
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +4 -4
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  9. package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
  10. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
  11. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  12. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  13. package/dist/types/src/extensions/code.d.ts.map +1 -1
  14. package/dist/types/src/extensions/comments.d.ts +2 -2
  15. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  16. package/dist/types/src/{util → extensions}/cursor.d.ts +9 -1
  17. package/dist/types/src/extensions/cursor.d.ts.map +1 -0
  18. package/dist/types/src/extensions/index.d.ts +1 -0
  19. package/dist/types/src/extensions/index.d.ts.map +1 -1
  20. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  21. package/dist/types/src/extensions/yjs/cursor.d.ts +1 -1
  22. package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -1
  23. package/dist/types/src/hooks/useTextModel.d.ts +0 -5
  24. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  25. package/dist/types/src/hooks/yjs.d.ts.map +1 -1
  26. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  27. package/dist/types/src/themes/default.d.ts.map +1 -1
  28. package/dist/types/src/util.d.ts.map +1 -0
  29. package/package.json +23 -22
  30. package/src/components/TextEditor/MarkdownEditor.stories.tsx +12 -12
  31. package/src/components/TextEditor/TextEditor.stories.tsx +10 -43
  32. package/src/components/TextEditor/TextEditor.tsx +21 -31
  33. package/src/components/TextEditor/comments.stories.tsx +315 -0
  34. package/src/extensions/automerge/automerge.ts +2 -2
  35. package/src/extensions/automerge/cursor.ts +2 -1
  36. package/src/extensions/awareness.ts +25 -32
  37. package/src/extensions/code.ts +10 -2
  38. package/src/extensions/comments.ts +20 -115
  39. package/src/extensions/cursor.ts +46 -0
  40. package/src/extensions/hr.ts +2 -2
  41. package/src/extensions/image.ts +2 -2
  42. package/src/extensions/index.ts +1 -0
  43. package/src/extensions/link.ts +4 -4
  44. package/src/extensions/markdown/highlight.ts +0 -8
  45. package/src/extensions/table.ts +2 -2
  46. package/src/extensions/tasklist.ts +2 -2
  47. package/src/extensions/yjs/cursor.ts +2 -1
  48. package/src/extensions/yjs/yjs.ts +2 -2
  49. package/src/hooks/useTextModel.ts +3 -6
  50. package/src/hooks/yjs.ts +1 -23
  51. package/src/styles/markdown.ts +0 -2
  52. package/src/themes/default.ts +17 -50
  53. package/dist/types/src/util/cursor.d.ts.map +0 -1
  54. package/dist/types/src/util/index.d.ts +0 -3
  55. package/dist/types/src/util/index.d.ts.map +0 -1
  56. package/dist/types/src/util/util.d.ts.map +0 -1
  57. package/src/util/cursor.ts +0 -29
  58. package/src/util/index.ts +0 -6
  59. /package/dist/types/src/{util/util.d.ts → util.d.ts} +0 -0
  60. /package/src/{util/util.ts → util.ts} +0 -0
@@ -16,7 +16,7 @@ import {
16
16
  import { Event } from '@dxos/async';
17
17
  import { Context } from '@dxos/context';
18
18
 
19
- import { CursorConverter } from '../util';
19
+ import { Cursor, type CursorConverter } from './cursor';
20
20
 
21
21
  export interface AwarenessProvider {
22
22
  remoteStateChange: Event<void>;
@@ -91,7 +91,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
91
91
  private _lastHead?: number = undefined;
92
92
 
93
93
  constructor(view: EditorView) {
94
- this._cursorConverter = view.state.facet(CursorConverter);
94
+ this._cursorConverter = view.state.facet(Cursor.converter);
95
95
  this._provider = view.state.facet(AwarenessProvider);
96
96
  this._provider.open();
97
97
  this._provider.remoteStateChange.on(this._ctx, () => {
@@ -156,7 +156,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
156
156
  to: end,
157
157
  value: Decoration.mark({
158
158
  attributes: { style: `background-color: ${lightColor}` },
159
- class: 'cm-ySelection',
159
+ class: 'cm-collab-selection',
160
160
  }),
161
161
  });
162
162
  } else {
@@ -166,7 +166,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
166
166
  to: startLine.from + startLine.length,
167
167
  value: Decoration.mark({
168
168
  attributes: { style: `background-color: ${color}` },
169
- class: 'cm-ySelection',
169
+ class: 'cm-collab-selection',
170
170
  }),
171
171
  });
172
172
 
@@ -176,7 +176,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
176
176
  to: end,
177
177
  value: Decoration.mark({
178
178
  attributes: { style: `background-color: ${color}` },
179
- class: 'cm-ySelection',
179
+ class: 'cm-collab-selection',
180
180
  }),
181
181
  });
182
182
 
@@ -186,7 +186,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
186
186
  from: linePos,
187
187
  to: linePos,
188
188
  value: Decoration.line({
189
- attributes: { style: `background-color: ${color}`, class: 'cm-yLineSelection' },
189
+ attributes: { style: `background-color: ${color}`, class: 'cm-collab-selectionLine' },
190
190
  }),
191
191
  });
192
192
  }
@@ -208,24 +208,22 @@ export class RemoteSelectionsDecorator implements PluginValue {
208
208
  }
209
209
 
210
210
  class RemoteCaretWidget extends WidgetType {
211
- constructor(public name: string, public color: string) {
211
+ constructor(private readonly _name: string, private readonly _color: string) {
212
212
  super();
213
- this.name = name;
214
- this.color = color;
215
213
  }
216
214
 
217
- toDOM(): HTMLElement {
215
+ override toDOM(): HTMLElement {
218
216
  const span = document.createElement('span');
219
- span.className = 'cm-ySelectionCaret';
220
- span.style.backgroundColor = this.color;
221
- span.style.borderColor = this.color;
217
+ span.className = 'cm-collab-selectionCaret';
218
+ span.style.backgroundColor = this._color;
219
+ span.style.borderColor = this._color;
222
220
 
223
221
  const dot = document.createElement('div');
224
- dot.className = 'cm-ySelectionCaretDot';
222
+ dot.className = 'cm-collab-selectionCaretDot';
225
223
 
226
224
  const info = document.createElement('div');
227
- info.className = 'cm-ySelectionInfo';
228
- info.innerText = this.name;
225
+ info.className = 'cm-collab-selectionInfo';
226
+ info.innerText = this._name;
229
227
 
230
228
  span.appendChild(document.createTextNode('\u2060'));
231
229
  span.appendChild(dot);
@@ -236,18 +234,14 @@ class RemoteCaretWidget extends WidgetType {
236
234
  return span;
237
235
  }
238
236
 
239
- override eq(widget: this) {
240
- return widget.color === this.color;
241
- }
242
-
243
- compare(widget: this) {
244
- return widget.color === this.color;
245
- }
246
-
247
237
  override updateDOM() {
248
238
  return false;
249
239
  }
250
240
 
241
+ override eq(widget: this) {
242
+ return widget._color === this._color;
243
+ }
244
+
251
245
  override get estimatedHeight() {
252
246
  return -1;
253
247
  }
@@ -257,14 +251,13 @@ class RemoteCaretWidget extends WidgetType {
257
251
  }
258
252
  }
259
253
 
260
- // TODO(burdon): Rename prefix (y for yjs?)
261
254
  const styles = EditorView.baseTheme({
262
- '.cm-ySelection': {},
263
- '.cm-yLineSelection': {
255
+ '.cm-collab-selection': {},
256
+ '.cm-collab-selectionLine': {
264
257
  padding: 0,
265
258
  margin: '0px 2px 0px 4px',
266
259
  },
267
- '.cm-ySelectionCaret': {
260
+ '.cm-collab-selectionCaret': {
268
261
  position: 'relative',
269
262
  borderLeft: '1px solid black',
270
263
  borderRight: '1px solid black',
@@ -273,7 +266,7 @@ const styles = EditorView.baseTheme({
273
266
  boxSizing: 'border-box',
274
267
  display: 'inline',
275
268
  },
276
- '.cm-ySelectionCaretDot': {
269
+ '.cm-collab-selectionCaretDot': {
277
270
  borderRadius: '50%',
278
271
  position: 'absolute',
279
272
  width: '.4em',
@@ -284,11 +277,11 @@ const styles = EditorView.baseTheme({
284
277
  transition: 'transform .3s ease-in-out',
285
278
  boxSizing: 'border-box',
286
279
  },
287
- '.cm-ySelectionCaret:hover > .cm-ySelectionCaretDot': {
280
+ '.cm-collab-selectionCaret:hover > .cm-collab-selectionCaretDot': {
288
281
  transformOrigin: 'bottom center',
289
282
  transform: 'scale(0)',
290
283
  },
291
- '.cm-ySelectionInfo': {
284
+ '.cm-collab-selectionInfo': {
292
285
  position: 'absolute',
293
286
  top: '-1.05em',
294
287
  left: '-1px',
@@ -309,7 +302,7 @@ const styles = EditorView.baseTheme({
309
302
  transitionDelay: '0s',
310
303
  whiteSpace: 'nowrap',
311
304
  },
312
- '.cm-ySelectionCaret:hover > .cm-ySelectionInfo': {
305
+ '.cm-collab-selectionCaret:hover > .cm-collab-selectionInfo': {
313
306
  opacity: 1,
314
307
  transitionDelay: '0s',
315
308
  },
@@ -55,7 +55,7 @@ const getLineRange = (lines: BlockInfo[], from: number, to: number) => {
55
55
  export const code = () => {
56
56
  const getDecorations = (view: EditorView) => {
57
57
  const decorations: Range<Decoration>[] = [];
58
- if (view.state.readOnly) {
58
+ if (!view.hasFocus || view.state.readOnly) {
59
59
  const text = view.state.doc.sliceString(0);
60
60
  const matches = text.matchAll(CODE_REGEX);
61
61
  const blocks = view.viewportLineBlocks;
@@ -78,13 +78,21 @@ export const code = () => {
78
78
  return [
79
79
  ViewPlugin.fromClass(
80
80
  class {
81
+ hasFocus = false;
81
82
  decorations: DecorationSet;
82
83
  constructor(view: EditorView) {
83
84
  this.decorations = getDecorations(view);
84
85
  }
85
86
 
86
87
  update(update: ViewUpdate) {
87
- if (update.docChanged || update.viewportChanged) {
88
+ if (
89
+ // TODO(burdon): Generalize for other extensions.
90
+ this.hasFocus !== update.view.hasFocus ||
91
+ update.startState.readOnly !== update.view.state.readOnly ||
92
+ update.docChanged ||
93
+ update.viewportChanged
94
+ ) {
95
+ this.hasFocus = update.view.hasFocus;
88
96
  this.decorations = getDecorations(update.view);
89
97
  }
90
98
  }
@@ -3,19 +3,7 @@
3
3
  //
4
4
 
5
5
  import { type Extension, StateEffect, StateField } from '@codemirror/state';
6
- import {
7
- hoverTooltip,
8
- keymap,
9
- type Command,
10
- type DecorationSet,
11
- type ViewUpdate,
12
- Decoration,
13
- EditorView,
14
- MatchDecorator,
15
- ViewPlugin,
16
- WidgetType,
17
- type Rect,
18
- } from '@codemirror/view';
6
+ import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
19
7
  import sortBy from 'lodash.sortby';
20
8
 
21
9
  import { debounce } from '@dxos/async';
@@ -23,24 +11,16 @@ import { invariant } from '@dxos/invariant';
23
11
  import { log } from '@dxos/log';
24
12
  import { nonNullable } from '@dxos/util';
25
13
 
14
+ import { Cursor } from './cursor';
26
15
  import { type CommentRange, type EditorModel, type Range } from '../hooks';
27
16
  import { getToken } from '../styles';
28
- import { callbackWrapper, CursorConverter } from '../util';
17
+ import { callbackWrapper } from '../util';
29
18
 
30
19
  // TODO(burdon): Handle delete, cut, copy, and paste (separately) text that includes comment range.
31
20
  // TODO(burdon): Consider breaking into separate plugin (since not standalone)? Like mermaid?
32
21
 
33
22
  // TODO(burdon): Reconcile with theme.
34
23
  const styles = EditorView.baseTheme({
35
- '& .cm-bookmark': {
36
- cursor: 'pointer',
37
- margin: '4px',
38
- padding: '4px',
39
- backgroundColor: 'yellow',
40
- },
41
- '& .cm-bookmark-selected': {
42
- backgroundColor: 'orange',
43
- },
44
24
  '& .cm-comment': {
45
25
  backgroundColor: getToken('extend.colors.yellow.50'),
46
26
  },
@@ -98,7 +78,7 @@ const setCommentState = StateEffect.define<CommentsState>();
98
78
  const commentsStateField = StateField.define<CommentsState>({
99
79
  create: () => ({ ranges: [] }),
100
80
  update: (value, tr) => {
101
- const cursorConverter = tr.state.facet(CursorConverter);
81
+ const cursorConverter = tr.state.facet(Cursor.converter);
102
82
 
103
83
  for (const effect of tr.effects) {
104
84
  // Update selection.
@@ -111,7 +91,7 @@ const commentsStateField = StateField.define<CommentsState>({
111
91
  const { comments } = effect.value;
112
92
  const ranges: ExtendedCommentRange[] = comments
113
93
  .map((comment) => {
114
- const range = getRangeFromCursor(cursorConverter, comment.cursor);
94
+ const range = Cursor.getRangeFromCursor(cursorConverter, comment.cursor);
115
95
  return range && { ...comment, ...range };
116
96
  })
117
97
  .filter(nonNullable);
@@ -154,42 +134,13 @@ const highlightDecorations = EditorView.decorations.compute([commentsStateField]
154
134
  return Decoration.set(decorations);
155
135
  });
156
136
 
157
- /**
158
- * Optional bookmark widget at the start/end of the selection.
159
- * May correspond to a markdown bookmark.
160
- */
161
- class BookmarkWidget extends WidgetType {
162
- constructor(
163
- private readonly _anchor: number,
164
- private readonly _id: string,
165
- private readonly _handleClick?: () => void,
166
- ) {
167
- super();
168
- invariant(this._id);
169
- }
170
-
171
- override eq(other: BookmarkWidget) {
172
- return this._anchor === other._anchor && this._id === other._id;
173
- }
174
-
175
- override toDOM() {
176
- const span = document.createElement('span');
177
- span.className = 'cm-bookmark';
178
- span.textContent = '※';
179
- if (this._handleClick) {
180
- span.onclick = () => this._handleClick!();
181
- }
182
- return span;
183
- }
184
- }
185
-
186
137
  export type CommentsOptions = {
187
138
  key?: string;
188
139
  footnote?: boolean;
189
140
  /**
190
141
  * Called to create a new thread and return the thread id.
191
142
  */
192
- onCreate?: (range: string) => string | undefined;
143
+ onCreate?: (cursor: string, location?: Rect | null) => string | undefined;
193
144
  /**
194
145
  * Called to notify which thread is currently closest to the cursor.
195
146
  */
@@ -197,7 +148,7 @@ export type CommentsOptions = {
197
148
  /**
198
149
  * Called to render tooltip.
199
150
  */
200
- onHover?: (el: Element) => void;
151
+ onHover?: (el: Element, shortcut: string) => void;
201
152
  };
202
153
 
203
154
  /**
@@ -213,13 +164,15 @@ export type CommentsOptions = {
213
164
  * 6). Optionally, implements a hoverTooltip to show hints when creating a selection range.
214
165
  */
215
166
  export const comments = (options: CommentsOptions = {}): Extension => {
167
+ const { key: shortcut = "meta-'" } = options;
168
+
216
169
  const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
217
170
 
218
171
  /**
219
172
  * Create comment thread action.
220
173
  */
221
174
  const createCommentThread: Command = (view) => {
222
- const cursorConverter = view.state.facet(CursorConverter);
175
+ const cursorConverter = view.state.facet(Cursor.converter);
223
176
 
224
177
  invariant(options.onCreate);
225
178
  const { head, from, to } = view.state.selection.main;
@@ -227,10 +180,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
227
180
  return false;
228
181
  }
229
182
 
230
- const relPos = getCursorFromRange(cursorConverter, { from, to });
231
- if (relPos) {
183
+ const cursor = Cursor.getCursorFromRange(cursorConverter, { from, to });
184
+ if (cursor) {
232
185
  // Create thread via callback.
233
- const id = callbackWrapper(options.onCreate)(relPos);
186
+ const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
234
187
  if (id) {
235
188
  // Update range.
236
189
  view.dispatch({
@@ -254,44 +207,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
254
207
  return false;
255
208
  };
256
209
 
257
- /**
258
- * Bookmark widget decoration.
259
- */
260
- const bookmarksViewPlugin = ViewPlugin.fromClass(
261
- class BookmarkViewPlugin {
262
- // Match markdown footnotes (option).
263
- // https://www.markdownguide.org/extended-syntax/#footnotes
264
- static bookmarkMatcher = new MatchDecorator({
265
- regexp: /\[\^(\w+)\]/g,
266
- decoration: (match, view, pos) => {
267
- const id = match[1];
268
- return Decoration.replace({
269
- id,
270
- widget: new BookmarkWidget(pos, id),
271
- });
272
- },
273
- });
274
-
275
- bookmarks: DecorationSet;
276
- constructor(view: EditorView) {
277
- this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.createDeco(view);
278
- }
279
-
280
- update(update: ViewUpdate) {
281
- this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.updateDeco(update, this.bookmarks);
282
- }
283
- },
284
- {
285
- decorations: (instance) => instance.bookmarks,
286
- provide: (plugin) =>
287
- EditorView.atomicRanges.of((view) => {
288
- return view.plugin(plugin)?.bookmarks || Decoration.none;
289
- }),
290
- },
291
- );
292
-
293
210
  return [
294
- bookmarksViewPlugin,
295
211
  commentsStateField,
296
212
  highlightDecorations,
297
213
  styles,
@@ -302,7 +218,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
302
218
  options.onCreate
303
219
  ? keymap.of([
304
220
  {
305
- key: options?.key ?? "meta-'",
221
+ key: shortcut,
306
222
  run: callbackWrapper(createCommentThread),
307
223
  },
308
224
  ])
@@ -324,7 +240,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
324
240
  create: () => {
325
241
  // TODO(burdon): Dispatch to react callback to render (or use SSR)?
326
242
  const el = document.createElement('div');
327
- options.onHover?.(el);
243
+ options.onHover?.(el, shortcut);
328
244
  return { dom: el, offset: { x: 0, y: 8 } };
329
245
  },
330
246
  };
@@ -341,7 +257,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
341
257
  // TODO(burdon): Is there a better (finer grained) way to do this?
342
258
  //
343
259
  EditorView.updateListener.of(({ view, state, changes }) => {
344
- const cursorConverter = view.state.facet(CursorConverter);
260
+ const cursorConverter = view.state.facet(Cursor.converter);
345
261
 
346
262
  //
347
263
  // Test if need to recompute indexed range if document changes before the end of the range.
@@ -352,7 +268,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
352
268
  changes.iterChanges((from, to, from2, to2) => {
353
269
  ranges.forEach((range) => {
354
270
  if (from2 === to2) {
355
- const newRange = getRangeFromCursor(cursorConverter, range.cursor);
271
+ const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
356
272
  if (!newRange || newRange.to - newRange.from === 0) {
357
273
  // TODO(burdon): Delete range if empty.
358
274
  log.info('deleted comment', { thread: range.id });
@@ -360,7 +276,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
360
276
  }
361
277
 
362
278
  if (from <= range.to) {
363
- const newRange = getRangeFromCursor(cursorConverter, range.cursor);
279
+ const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
364
280
  Object.assign(range, newRange);
365
281
  mod = true;
366
282
  }
@@ -392,6 +308,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
392
308
  }
393
309
  });
394
310
 
311
+ // TODO(burdon): Fire debounced callback if range selected (to show hint).
312
+
395
313
  if (selected.active !== active || selected.closest !== closest) {
396
314
  view.dispatch({ effects: setSelection.of(selected) });
397
315
 
@@ -405,16 +323,3 @@ export const comments = (options: CommentsOptions = {}): Extension => {
405
323
  }),
406
324
  ];
407
325
  };
408
-
409
- const getCursorFromRange = (cursorConverter: CursorConverter, range: Range) => {
410
- const from = cursorConverter.toCursor(range.from);
411
- const to = cursorConverter.toCursor(range.to, -1);
412
- return [from, to].join(':');
413
- };
414
-
415
- const getRangeFromCursor = (cursorConverter: CursorConverter, cursor: string) => {
416
- const parts = cursor.split(':');
417
- const from = cursorConverter.fromCursor(parts[0]);
418
- const to = cursorConverter.fromCursor(parts[1]);
419
- return from !== undefined && to !== undefined ? { from, to } : undefined;
420
- };
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Facet } from '@codemirror/state';
6
+
7
+ import type { Range } from '../hooks';
8
+
9
+ /**
10
+ * Converts indexes into the text document into stable peer-independent cursors.
11
+ *
12
+ * See:
13
+ * - https://automerge.org/automerge/api-docs/js/functions/next.getCursor.html
14
+ * - https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
15
+ *
16
+ * @param {assoc} number Negative values will associate the cursor with the previous character
17
+ * while positive - with the next one.
18
+ */
19
+ export interface CursorConverter {
20
+ toCursor(position: number, assoc?: -1 | 1 | undefined): string;
21
+ fromCursor(cursor: string): number;
22
+ }
23
+
24
+ const defaultCursorConverter: CursorConverter = {
25
+ toCursor: (position) => position.toString(),
26
+ fromCursor: (cursor) => parseInt(cursor),
27
+ };
28
+
29
+ export class Cursor {
30
+ static readonly converter = Facet.define<CursorConverter, CursorConverter>({
31
+ combine: (providers) => providers[0] ?? defaultCursorConverter,
32
+ });
33
+
34
+ static readonly getCursorFromRange = (cursorConverter: CursorConverter, range: Range) => {
35
+ const from = cursorConverter.toCursor(range.from);
36
+ const to = cursorConverter.toCursor(range.to, -1);
37
+ return [from, to].join(':');
38
+ };
39
+
40
+ static readonly getRangeFromCursor = (cursorConverter: CursorConverter, cursor: string) => {
41
+ const parts = cursor.split(':');
42
+ const from = cursorConverter.fromCursor(parts[0]);
43
+ const to = cursorConverter.fromCursor(parts[1]);
44
+ return from !== undefined && to !== undefined ? { from, to } : undefined;
45
+ };
46
+ }
@@ -28,11 +28,11 @@ class HorizontalRuleWidget extends WidgetType {
28
28
  super();
29
29
  }
30
30
 
31
- override eq(other: WidgetType) {
31
+ override eq(other: this) {
32
32
  return this._pos === (other as any as HorizontalRuleWidget)._pos;
33
33
  }
34
34
 
35
- toDOM(view: EditorView) {
35
+ override toDOM(view: EditorView) {
36
36
  // TODO(burdon): Create <hr> element? Does this clash with markdown parser?
37
37
  const el = document.createElement('div');
38
38
  el.className = 'cm-hr';
@@ -72,11 +72,11 @@ class ImageWidget extends WidgetType {
72
72
  super();
73
73
  }
74
74
 
75
- override eq(other: WidgetType) {
75
+ override eq(other: this) {
76
76
  return this._url === (other as any as ImageWidget)._url;
77
77
  }
78
78
 
79
- toDOM(view: EditorView) {
79
+ override toDOM(view: EditorView) {
80
80
  const img = document.createElement('img');
81
81
  img.setAttribute('src', this._url);
82
82
  img.setAttribute('class', 'cm-image');
@@ -9,6 +9,7 @@ export * from './basic';
9
9
  export * from './blast';
10
10
  export * from './code';
11
11
  export * from './comments';
12
+ export * from './cursor';
12
13
  export * from './hr';
13
14
  export * from './image';
14
15
  export * from './link';
@@ -93,11 +93,11 @@ class LinkButton extends WidgetType {
93
93
  super();
94
94
  }
95
95
 
96
- override eq(other: LinkButton) {
96
+ override eq(other: this) {
97
97
  return this._url === other._url;
98
98
  }
99
99
 
100
- toDOM(view: EditorView) {
100
+ override toDOM(view: EditorView) {
101
101
  const el = document.createElement('span');
102
102
  this._onAttach!(el, this._url);
103
103
  return el;
@@ -109,11 +109,11 @@ class LinkText extends WidgetType {
109
109
  super();
110
110
  }
111
111
 
112
- override eq(other: LinkText) {
112
+ override eq(other: this) {
113
113
  return this._url === other._url;
114
114
  }
115
115
 
116
- toDOM(view: EditorView) {
116
+ override toDOM(view: EditorView) {
117
117
  const link = document.createElement('a');
118
118
  link.setAttribute('class', 'cm-link');
119
119
  link.textContent = this._text;
@@ -31,7 +31,6 @@ export const markdownTags = {
31
31
  CodeText: Tag.define(),
32
32
  EmphasisMark: Tag.define(),
33
33
  HeaderMark: Tag.define(),
34
- // HorizontalRule: Tag.define(),
35
34
  InlineCode: Tag.define(),
36
35
  LinkLabel: Tag.define(),
37
36
  LinkReference: Tag.define(),
@@ -177,13 +176,6 @@ export const markdownHighlightStyle = (readonly?: boolean) => {
177
176
  class: blockquote,
178
177
  },
179
178
 
180
- // TODO(burdon): Replace with extension.
181
- // {
182
- // tag: [markdownTags.HorizontalRule],
183
- // class: mx(horizontalRule, readonly && '-indent-[100rem]'),
184
- // },
185
-
186
- // TODO(burdon): Only if being edited.
187
179
  {
188
180
  tag: [markdownTags.TableCell],
189
181
  class: 'font-mono',
@@ -97,14 +97,14 @@ class TableWidget extends WidgetType {
97
97
  super();
98
98
  }
99
99
 
100
- override eq(other: TableWidget) {
100
+ override eq(other: this) {
101
101
  return (
102
102
  this._table.header?.join() === other._table.header?.join() &&
103
103
  this._table.rows?.join() === other._table.rows?.join()
104
104
  );
105
105
  }
106
106
 
107
- toDOM(view: EditorView) {
107
+ override toDOM(view: EditorView) {
108
108
  const table = document.createElement('table');
109
109
 
110
110
  {
@@ -29,11 +29,11 @@ class CheckboxWidget extends WidgetType {
29
29
  super();
30
30
  }
31
31
 
32
- override eq(other: CheckboxWidget) {
32
+ override eq(other: this) {
33
33
  return this._checked === other._checked && this._indent === other._indent;
34
34
  }
35
35
 
36
- toDOM(view: EditorView) {
36
+ override toDOM(view: EditorView) {
37
37
  const wrap = document.createElement('span');
38
38
  wrap.className = 'cm-task-item';
39
39
  wrap.setAttribute('aria-hidden', 'true');
@@ -6,12 +6,13 @@ import * as Y from 'yjs';
6
6
 
7
7
  import { arrayToString, stringToArray } from '@dxos/util';
8
8
 
9
- import { type CursorConverter } from '../../util';
9
+ import { type CursorConverter } from '../cursor';
10
10
 
11
11
  export const cursorConverter = (text: Y.Text): CursorConverter => ({
12
12
  toCursor: (index, assoc) => {
13
13
  return arrayToString(Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text, index, assoc)));
14
14
  },
15
+
15
16
  fromCursor: (cursor) => {
16
17
  return (
17
18
  Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(stringToArray(cursor)), text.doc!)?.index ??
@@ -8,9 +8,9 @@ import type * as YP from 'y-protocols/awareness';
8
8
  import type { YText } from '@dxos/text-model';
9
9
 
10
10
  import { cursorConverter } from './cursor';
11
- import { CursorConverter } from '../../util';
11
+ import { Cursor } from '../cursor';
12
12
 
13
13
  export const yjs = (content: YText, awareness?: YP.Awareness) => [
14
- CursorConverter.of(cursorConverter(content)),
14
+ Cursor.converter.of(cursorConverter(content)),
15
15
  yCollab(content, awareness),
16
16
  ];
@@ -30,7 +30,7 @@ export type Range = {
30
30
 
31
31
  export type CommentRange = {
32
32
  id: string;
33
- cursor: string;
33
+ cursor: string; // TODO(burdon): Rename range.
34
34
  };
35
35
 
36
36
  // TODO(wittjosiah): Factor out to common package? @dxos/react-client?
@@ -41,13 +41,10 @@ export type EditorModel = {
41
41
 
42
42
  // TODO(burdon): Remove.
43
43
  content: string | YText | YXmlFragment | DocAccessor;
44
- /**
45
- * @deprecated Use CursorConverter.
46
- */
47
- getCursorFromRange?: (value: Range) => string;
48
- getRangeFromCursor?: (cursor: string) => Range | undefined;
44
+
49
45
  // TODO(burdon): Move into extension.
50
46
  awareness?: YP.Awareness;
47
+
51
48
  // TODO(burdon): Remove.
52
49
  peer?: {
53
50
  id: string;