@dxos/react-ui-editor 0.3.11-main.a996413 → 0.3.11-main.b225571

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 (145) hide show
  1. package/dist/lib/browser/index.mjs +1393 -1052
  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 +9 -5
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +12 -17
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -3
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/{hooks/automerge → components/TextEditor}/automerge.stories.d.ts +6 -1
  11. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -0
  12. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +1 -1
  13. package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
  14. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
  15. package/dist/types/src/{hooks/yjs → components/TextEditor}/yjs.stories.d.ts +1 -1
  16. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -0
  17. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  18. package/dist/types/src/extensions/automerge/automerge.d.ts +9 -0
  19. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -0
  20. package/dist/types/src/extensions/automerge/cursor.d.ts +5 -0
  21. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -0
  22. package/dist/types/src/extensions/automerge/defs.d.ts +26 -0
  23. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -0
  24. package/dist/types/src/extensions/automerge/index.d.ts +4 -0
  25. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -0
  26. package/dist/types/src/{hooks → extensions}/automerge/semaphore.d.ts +4 -4
  27. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -0
  28. package/dist/types/src/extensions/automerge/update-automerge.d.ts +5 -0
  29. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -0
  30. package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -0
  31. package/dist/types/src/extensions/awareness.d.ts +46 -0
  32. package/dist/types/src/extensions/awareness.d.ts.map +1 -0
  33. package/dist/types/src/extensions/code.d.ts.map +1 -1
  34. package/dist/types/src/extensions/comments.d.ts +2 -2
  35. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  36. package/dist/types/src/extensions/cursor.d.ts +25 -0
  37. package/dist/types/src/extensions/cursor.d.ts.map +1 -0
  38. package/dist/types/src/extensions/hr.d.ts.map +1 -1
  39. package/dist/types/src/extensions/image.d.ts.map +1 -1
  40. package/dist/types/src/extensions/index.d.ts +4 -1
  41. package/dist/types/src/extensions/index.d.ts.map +1 -1
  42. package/dist/types/src/extensions/link.d.ts.map +1 -1
  43. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  44. package/dist/types/src/extensions/tasklist.d.ts.map +1 -1
  45. package/dist/types/src/extensions/yjs/cursor.d.ts +4 -0
  46. package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -0
  47. package/dist/types/src/extensions/yjs/index.d.ts +2 -0
  48. package/dist/types/src/extensions/yjs/index.d.ts.map +1 -0
  49. package/dist/types/src/extensions/yjs/yjs.d.ts +4 -0
  50. package/dist/types/src/extensions/yjs/yjs.d.ts.map +1 -0
  51. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +1 -0
  52. package/dist/types/src/hooks/automerge.d.ts +3 -0
  53. package/dist/types/src/hooks/automerge.d.ts.map +1 -0
  54. package/dist/types/src/hooks/awareness-provider.d.ts +28 -0
  55. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -0
  56. package/dist/types/src/hooks/index.d.ts +2 -0
  57. package/dist/types/src/hooks/index.d.ts.map +1 -1
  58. package/dist/types/src/hooks/useTextModel.d.ts +4 -8
  59. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  60. package/dist/types/src/hooks/{yjs/space-provider.d.ts → yjs.d.ts} +8 -8
  61. package/dist/types/src/hooks/yjs.d.ts.map +1 -0
  62. package/dist/types/src/index.d.ts +3 -0
  63. package/dist/types/src/index.d.ts.map +1 -1
  64. package/dist/types/src/styles/cursors.d.ts.map +1 -0
  65. package/dist/types/src/styles/index.d.ts +1 -0
  66. package/dist/types/src/styles/index.d.ts.map +1 -1
  67. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  68. package/dist/types/src/styles/tokens.d.ts +1 -0
  69. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  70. package/dist/types/src/testing/replicator.d.ts +2 -2
  71. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  72. package/dist/types/src/themes/default.d.ts.map +1 -1
  73. package/dist/types/src/util.d.ts.map +1 -0
  74. package/package.json +23 -21
  75. package/src/components/TextEditor/MarkdownEditor.stories.tsx +25 -23
  76. package/src/components/TextEditor/TextEditor.stories.tsx +13 -34
  77. package/src/components/TextEditor/TextEditor.tsx +69 -65
  78. package/src/{hooks/automerge → components/TextEditor}/automerge.stories.tsx +66 -6
  79. package/src/components/TextEditor/codemirror.stories.ts +3 -2
  80. package/src/components/TextEditor/comments.stories.tsx +315 -0
  81. package/src/{hooks/yjs → components/TextEditor}/yjs.stories.tsx +1 -1
  82. package/src/extensions/autocomplete.ts +18 -17
  83. package/src/{hooks/automerge/plugin.ts → extensions/automerge/automerge.ts} +20 -58
  84. package/src/extensions/automerge/cursor.ts +52 -0
  85. package/src/extensions/automerge/defs.ts +62 -0
  86. package/src/extensions/automerge/index.ts +7 -0
  87. package/src/{hooks → extensions}/automerge/semaphore.ts +14 -12
  88. package/src/{hooks → extensions}/automerge/update-automerge.ts +5 -6
  89. package/src/{hooks → extensions}/automerge/update-codemirror.ts +13 -5
  90. package/src/extensions/awareness.ts +309 -0
  91. package/src/extensions/code.ts +14 -7
  92. package/src/extensions/comments.ts +29 -109
  93. package/src/extensions/cursor.ts +46 -0
  94. package/src/extensions/hr.ts +4 -5
  95. package/src/extensions/image.ts +40 -22
  96. package/src/extensions/index.ts +4 -1
  97. package/src/extensions/link.ts +86 -88
  98. package/src/extensions/markdown/highlight.ts +3 -12
  99. package/src/extensions/table.ts +10 -4
  100. package/src/extensions/tasklist.ts +3 -5
  101. package/src/extensions/yjs/cursor.ts +22 -0
  102. package/src/extensions/yjs/index.ts +5 -0
  103. package/src/{hooks → extensions}/yjs/yjs.test.ts +1 -25
  104. package/src/extensions/yjs/yjs.ts +16 -0
  105. package/src/hooks/automerge.ts +52 -0
  106. package/src/hooks/awareness-provider.ts +120 -0
  107. package/src/hooks/index.ts +2 -0
  108. package/src/hooks/useTextModel.ts +20 -97
  109. package/src/hooks/{yjs/space-provider.ts → yjs.ts} +49 -19
  110. package/src/index.ts +3 -0
  111. package/src/styles/index.ts +1 -0
  112. package/src/styles/markdown.ts +0 -2
  113. package/src/styles/tokens.ts +3 -0
  114. package/src/testing/replicator.ts +10 -15
  115. package/src/themes/default.ts +52 -71
  116. package/dist/types/src/extensions/mermaid.d.ts +0 -3
  117. package/dist/types/src/extensions/mermaid.d.ts.map +0 -1
  118. package/dist/types/src/extensions/util.d.ts.map +0 -1
  119. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +0 -1
  120. package/dist/types/src/hooks/automerge/handle.d.ts +0 -9
  121. package/dist/types/src/hooks/automerge/handle.d.ts.map +0 -1
  122. package/dist/types/src/hooks/automerge/index.d.ts +0 -3
  123. package/dist/types/src/hooks/automerge/index.d.ts.map +0 -1
  124. package/dist/types/src/hooks/automerge/plugin.d.ts +0 -26
  125. package/dist/types/src/hooks/automerge/plugin.d.ts.map +0 -1
  126. package/dist/types/src/hooks/automerge/semaphore.d.ts.map +0 -1
  127. package/dist/types/src/hooks/automerge/update-automerge.d.ts +0 -6
  128. package/dist/types/src/hooks/automerge/update-automerge.d.ts.map +0 -1
  129. package/dist/types/src/hooks/automerge/update-codemirror.d.ts.map +0 -1
  130. package/dist/types/src/hooks/yjs/cursors.d.ts.map +0 -1
  131. package/dist/types/src/hooks/yjs/index.d.ts +0 -3
  132. package/dist/types/src/hooks/yjs/index.d.ts.map +0 -1
  133. package/dist/types/src/hooks/yjs/space-provider.d.ts.map +0 -1
  134. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +0 -1
  135. package/dist/types/src/hooks/yjs/yjs.test.d.ts.map +0 -1
  136. package/src/extensions/mermaid.ts +0 -11
  137. package/src/hooks/automerge/handle.ts +0 -14
  138. package/src/hooks/automerge/index.ts +0 -6
  139. package/src/hooks/yjs/index.ts +0 -6
  140. /package/dist/types/src/{hooks → extensions}/automerge/update-codemirror.d.ts +0 -0
  141. /package/dist/types/src/{hooks → extensions}/yjs/yjs.test.d.ts +0 -0
  142. /package/dist/types/src/{hooks/yjs → styles}/cursors.d.ts +0 -0
  143. /package/dist/types/src/{extensions/util.d.ts → util.d.ts} +0 -0
  144. /package/src/{hooks/yjs → styles}/cursors.ts +0 -0
  145. /package/src/{extensions/util.ts → util.ts} +0 -0
@@ -3,20 +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';
19
- import get from 'lodash.get';
6
+ import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
20
7
  import sortBy from 'lodash.sortby';
21
8
 
22
9
  import { debounce } from '@dxos/async';
@@ -24,29 +11,21 @@ import { invariant } from '@dxos/invariant';
24
11
  import { log } from '@dxos/log';
25
12
  import { nonNullable } from '@dxos/util';
26
13
 
27
- import { callbackWrapper } from './util';
28
- import { type CommentRange, type EditorModel, modelState, type Range } from '../hooks';
29
- import { tokens } from '../styles';
14
+ import { Cursor } from './cursor';
15
+ import { type CommentRange, type EditorModel, type Range } from '../hooks';
16
+ import { getToken } from '../styles';
17
+ import { callbackWrapper } from '../util';
30
18
 
31
19
  // TODO(burdon): Handle delete, cut, copy, and paste (separately) text that includes comment range.
32
20
  // TODO(burdon): Consider breaking into separate plugin (since not standalone)? Like mermaid?
33
21
 
34
22
  // TODO(burdon): Reconcile with theme.
35
23
  const styles = EditorView.baseTheme({
36
- '& .cm-bookmark': {
37
- cursor: 'pointer',
38
- margin: '4px',
39
- padding: '4px',
40
- backgroundColor: 'yellow',
41
- },
42
- '& .cm-bookmark-selected': {
43
- backgroundColor: 'orange',
44
- },
45
24
  '& .cm-comment': {
46
- backgroundColor: get(tokens, 'extend.colors.yellow.50'),
25
+ backgroundColor: getToken('extend.colors.yellow.50'),
47
26
  },
48
27
  '& .cm-comment-active': {
49
- backgroundColor: get(tokens, 'extend.colors.yellow.100'),
28
+ backgroundColor: getToken('extend.colors.yellow.100'),
50
29
  },
51
30
  });
52
31
 
@@ -99,6 +78,8 @@ const setCommentState = StateEffect.define<CommentsState>();
99
78
  const commentsStateField = StateField.define<CommentsState>({
100
79
  create: () => ({ ranges: [] }),
101
80
  update: (value, tr) => {
81
+ const cursorConverter = tr.state.facet(Cursor.converter);
82
+
102
83
  for (const effect of tr.effects) {
103
84
  // Update selection.
104
85
  if (effect.is(setSelection)) {
@@ -107,10 +88,10 @@ const commentsStateField = StateField.define<CommentsState>({
107
88
 
108
89
  // Update range from store.
109
90
  if (effect.is(setCommentRange)) {
110
- const { model, comments } = effect.value;
91
+ const { comments } = effect.value;
111
92
  const ranges: ExtendedCommentRange[] = comments
112
93
  .map((comment) => {
113
- const range = model.getRangeFromCursor!(comment.cursor);
94
+ const range = Cursor.getRangeFromCursor(cursorConverter, comment.cursor);
114
95
  return range && { ...comment, ...range };
115
96
  })
116
97
  .filter(nonNullable);
@@ -153,42 +134,13 @@ const highlightDecorations = EditorView.decorations.compute([commentsStateField]
153
134
  return Decoration.set(decorations);
154
135
  });
155
136
 
156
- /**
157
- * Optional bookmark widget at the start/end of the selection.
158
- * May correspond to a markdown bookmark.
159
- */
160
- class BookmarkWidget extends WidgetType {
161
- constructor(
162
- private readonly _anchor: number,
163
- private readonly _id: string,
164
- private readonly _handleClick?: () => void,
165
- ) {
166
- super();
167
- invariant(this._id);
168
- }
169
-
170
- override eq(other: BookmarkWidget) {
171
- return this._anchor === other._anchor && this._id === other._id;
172
- }
173
-
174
- override toDOM() {
175
- const span = document.createElement('span');
176
- span.className = 'cm-bookmark';
177
- span.textContent = '※';
178
- if (this._handleClick) {
179
- span.onclick = () => this._handleClick!();
180
- }
181
- return span;
182
- }
183
- }
184
-
185
137
  export type CommentsOptions = {
186
138
  key?: string;
187
139
  footnote?: boolean;
188
140
  /**
189
141
  * Called to create a new thread and return the thread id.
190
142
  */
191
- onCreate?: (range: string) => string | undefined;
143
+ onCreate?: (cursor: string, location?: Rect | null) => string | undefined;
192
144
  /**
193
145
  * Called to notify which thread is currently closest to the cursor.
194
146
  */
@@ -196,7 +148,7 @@ export type CommentsOptions = {
196
148
  /**
197
149
  * Called to render tooltip.
198
150
  */
199
- onHover?: (el: Element) => void;
151
+ onHover?: (el: Element, shortcut: string) => void;
200
152
  };
201
153
 
202
154
  /**
@@ -212,23 +164,26 @@ export type CommentsOptions = {
212
164
  * 6). Optionally, implements a hoverTooltip to show hints when creating a selection range.
213
165
  */
214
166
  export const comments = (options: CommentsOptions = {}): Extension => {
167
+ const { key: shortcut = "meta-'" } = options;
168
+
215
169
  const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
216
170
 
217
171
  /**
218
172
  * Create comment thread action.
219
173
  */
220
174
  const createCommentThread: Command = (view) => {
175
+ const cursorConverter = view.state.facet(Cursor.converter);
176
+
221
177
  invariant(options.onCreate);
222
178
  const { head, from, to } = view.state.selection.main;
223
179
  if (from === to) {
224
180
  return false;
225
181
  }
226
182
 
227
- const model = view.state.field(modelState);
228
- const relPos = model?.getCursorFromRange?.({ from, to });
229
- if (relPos) {
183
+ const cursor = Cursor.getCursorFromRange(cursorConverter, { from, to });
184
+ if (cursor) {
230
185
  // Create thread via callback.
231
- const id = callbackWrapper(options.onCreate)(relPos);
186
+ const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
232
187
  if (id) {
233
188
  // Update range.
234
189
  view.dispatch({
@@ -252,44 +207,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
252
207
  return false;
253
208
  };
254
209
 
255
- /**
256
- * Bookmark widget decoration.
257
- */
258
- const bookmarksViewPlugin = ViewPlugin.fromClass(
259
- class BookmarkViewPlugin {
260
- // Match markdown footnotes (option).
261
- // https://www.markdownguide.org/extended-syntax/#footnotes
262
- static bookmarkMatcher = new MatchDecorator({
263
- regexp: /\[\^(\w+)\]/g,
264
- decoration: (match, view, pos) => {
265
- const id = match[1];
266
- return Decoration.replace({
267
- id,
268
- widget: new BookmarkWidget(pos, id),
269
- });
270
- },
271
- });
272
-
273
- bookmarks: DecorationSet;
274
- constructor(view: EditorView) {
275
- this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.createDeco(view);
276
- }
277
-
278
- update(update: ViewUpdate) {
279
- this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.updateDeco(update, this.bookmarks);
280
- }
281
- },
282
- {
283
- decorations: (instance) => instance.bookmarks,
284
- provide: (plugin) =>
285
- EditorView.atomicRanges.of((view) => {
286
- return view.plugin(plugin)?.bookmarks || Decoration.none;
287
- }),
288
- },
289
- );
290
-
291
210
  return [
292
- bookmarksViewPlugin,
293
211
  commentsStateField,
294
212
  highlightDecorations,
295
213
  styles,
@@ -300,7 +218,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
300
218
  options.onCreate
301
219
  ? keymap.of([
302
220
  {
303
- key: options?.key ?? "meta-'",
221
+ key: shortcut,
304
222
  run: callbackWrapper(createCommentThread),
305
223
  },
306
224
  ])
@@ -322,7 +240,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
322
240
  create: () => {
323
241
  // TODO(burdon): Dispatch to react callback to render (or use SSR)?
324
242
  const el = document.createElement('div');
325
- options.onHover?.(el);
243
+ options.onHover?.(el, shortcut);
326
244
  return { dom: el, offset: { x: 0, y: 8 } };
327
245
  },
328
246
  };
@@ -338,19 +256,19 @@ export const comments = (options: CommentsOptions = {}): Extension => {
338
256
  // Monitor cursor movement and text updates.
339
257
  // TODO(burdon): Is there a better (finer grained) way to do this?
340
258
  //
341
- EditorView.updateListener.of(({ view, state, startState, changes }) => {
259
+ EditorView.updateListener.of(({ view, state, changes }) => {
260
+ const cursorConverter = view.state.facet(Cursor.converter);
261
+
342
262
  //
343
263
  // Test if need to recompute indexed range if document changes before the end of the range.
344
264
  //
345
265
  {
346
266
  let mod = false;
347
- const model = state.field(modelState);
348
267
  const { active, ranges } = state.field(commentsStateField);
349
268
  changes.iterChanges((from, to, from2, to2) => {
350
- invariant(model);
351
269
  ranges.forEach((range) => {
352
270
  if (from2 === to2) {
353
- const newRange = model.getRangeFromCursor!(range.cursor);
271
+ const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
354
272
  if (!newRange || newRange.to - newRange.from === 0) {
355
273
  // TODO(burdon): Delete range if empty.
356
274
  log.info('deleted comment', { thread: range.id });
@@ -358,7 +276,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
358
276
  }
359
277
 
360
278
  if (from <= range.to) {
361
- const newRange = model.getRangeFromCursor!(range.cursor);
279
+ const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
362
280
  Object.assign(range, newRange);
363
281
  mod = true;
364
282
  }
@@ -390,6 +308,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
390
308
  }
391
309
  });
392
310
 
311
+ // TODO(burdon): Fire debounced callback if range selected (to show hint).
312
+
393
313
  if (selected.active !== active || selected.closest !== closest) {
394
314
  view.dispatch({ effects: setSelection.of(selected) });
395
315
 
@@ -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
+ }
@@ -11,16 +11,15 @@ import {
11
11
  type ViewUpdate,
12
12
  WidgetType,
13
13
  } from '@codemirror/view';
14
- import get from 'lodash.get';
15
14
 
16
- import { tokens } from '../styles';
15
+ import { getToken } from '../styles';
17
16
 
18
17
  // TODO(burdon): Reconcile with theme.
19
18
  const styles = EditorView.baseTheme({
20
19
  '& .cm-hr': {
21
20
  // TODO(burdon): ???
22
21
  // Note that block-level decorations should not have vertical margins,
23
- borderBottom: `1px solid ${get(tokens, 'extend.colors.neutral.200')}`,
22
+ borderBottom: `1px solid ${getToken('extend.colors.neutral.200')}`,
24
23
  },
25
24
  });
26
25
 
@@ -29,11 +28,11 @@ class HorizontalRuleWidget extends WidgetType {
29
28
  super();
30
29
  }
31
30
 
32
- override eq(other: WidgetType) {
31
+ override eq(other: this) {
33
32
  return this._pos === (other as any as HorizontalRuleWidget)._pos;
34
33
  }
35
34
 
36
- toDOM(view: EditorView) {
35
+ override toDOM(view: EditorView) {
37
36
  // TODO(burdon): Create <hr> element? Does this clash with markdown parser?
38
37
  const el = document.createElement('div');
39
38
  el.className = 'cm-hr';
@@ -3,28 +3,45 @@
3
3
  //
4
4
 
5
5
  import { syntaxTree } from '@codemirror/language';
6
- import {
7
- type EditorState,
8
- type Extension,
9
- type RangeSet,
10
- RangeSetBuilder,
11
- StateField,
12
- type Transaction,
13
- } from '@codemirror/state';
14
- import { Decoration, EditorView, WidgetType } from '@codemirror/view';
6
+ import { type EditorState, type Extension, StateField, type Transaction, type Range } from '@codemirror/state';
7
+ import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
15
8
 
16
9
  export type ImageOptions = {};
17
10
 
18
11
  export const image = (options: ImageOptions = {}): Extension => {
19
- return StateField.define<RangeSet<any>>({
20
- create: (state) => update(state, options),
21
- update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
12
+ return StateField.define<DecorationSet>({
13
+ create: (state) => {
14
+ return Decoration.set(buildDecorations(0, state.doc.length, state));
15
+ },
16
+ update: (value: DecorationSet, tr: Transaction) => {
17
+ if (!tr.docChanged && !tr.selection) {
18
+ return value;
19
+ }
20
+ // Find range of changes and cursor changes.
21
+ const cursor = tr.state.selection.main.head;
22
+ const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);
23
+ let from = Math.min(cursor, oldCursor);
24
+ let to = Math.max(cursor, oldCursor);
25
+ tr.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
26
+ from = Math.min(from, fromB);
27
+ to = Math.max(to, toB);
28
+ });
29
+ // Expand to cover lines
30
+ from = tr.state.doc.lineAt(from).from;
31
+ to = tr.state.doc.lineAt(to).to;
32
+ return value.map(tr.changes).update({
33
+ filterFrom: from,
34
+ filterTo: to,
35
+ filter: () => false,
36
+ add: buildDecorations(from, to, tr.state),
37
+ });
38
+ },
22
39
  provide: (field) => EditorView.decorations.from(field),
23
40
  });
24
41
  };
25
42
 
26
- const update = (state: EditorState, options: ImageOptions) => {
27
- const builder = new RangeSetBuilder();
43
+ const buildDecorations = (from: number, to: number, state: EditorState) => {
44
+ const decorations: Range<Decoration>[] = [];
28
45
  const cursor = state.selection.main.head;
29
46
 
30
47
  syntaxTree(state).iterate({
@@ -34,20 +51,20 @@ const update = (state: EditorState, options: ImageOptions) => {
34
51
  if (urlNode) {
35
52
  const hide = state.readOnly || cursor < node.from || cursor > node.to;
36
53
  const url = state.sliceDoc(urlNode.from, urlNode.to);
37
- builder.add(
38
- hide ? node.from : node.to,
39
- node.to,
54
+ decorations.push(
40
55
  Decoration.replace({
41
56
  block: true, // Prevent cursor from entering.
42
57
  widget: new ImageWidget(url),
43
- }),
58
+ }).range(hide ? node.from : node.to, node.to),
44
59
  );
45
60
  }
46
61
  }
47
62
  },
63
+ from,
64
+ to,
48
65
  });
49
66
 
50
- return builder.finish();
67
+ return decorations;
51
68
  };
52
69
 
53
70
  class ImageWidget extends WidgetType {
@@ -55,15 +72,16 @@ class ImageWidget extends WidgetType {
55
72
  super();
56
73
  }
57
74
 
58
- // TODO(burdon): Does this need to be unique for each position?
59
- override eq(other: WidgetType) {
75
+ override eq(other: this) {
60
76
  return this._url === (other as any as ImageWidget)._url;
61
77
  }
62
78
 
63
- toDOM(view: EditorView) {
79
+ override toDOM(view: EditorView) {
64
80
  const img = document.createElement('img');
65
81
  img.setAttribute('src', this._url);
66
82
  img.setAttribute('class', 'cm-image');
83
+ // Images are hidden until successfully loaded to avoid flickering effects.
84
+ img.onload = () => img.classList.add('cm-loaded-image');
67
85
  return img;
68
86
  }
69
87
  }
@@ -3,17 +3,20 @@
3
3
  //
4
4
 
5
5
  export * from './autocomplete';
6
+ export * from './automerge';
7
+ export * from './awareness';
6
8
  export * from './basic';
7
9
  export * from './blast';
8
10
  export * from './code';
9
11
  export * from './comments';
12
+ export * from './cursor';
10
13
  export * from './hr';
11
14
  export * from './image';
12
15
  export * from './link';
13
16
  export * from './listener';
14
17
  export * from './markdown';
15
18
  export * from './mention';
16
- export * from './mermaid';
17
19
  export * from './table';
18
20
  export * from './tasklist';
19
21
  export * from './typewriter';
22
+ export * from './yjs';