@dxos/react-ui-editor 0.4.10-main.fa5a270 → 0.4.10-main.fe71b4c

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 (34) hide show
  1. package/dist/lib/browser/index.mjs +706 -693
  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.map +1 -1
  5. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  7. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  8. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  9. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  10. package/dist/types/src/extensions/factories.d.ts +1 -1
  11. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  12. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  13. package/dist/types/src/extensions/markdown/formatting.d.ts +1 -1
  14. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  15. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  16. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
  17. package/dist/types/src/hooks/index.d.ts +0 -1
  18. package/dist/types/src/hooks/index.d.ts.map +1 -1
  19. package/package.json +30 -29
  20. package/src/components/TextEditor/TextEditor.stories.tsx +20 -18
  21. package/src/components/TextEditor/TextEditor.tsx +6 -3
  22. package/src/components/Toolbar/Toolbar.stories.tsx +9 -9
  23. package/src/extensions/automerge/automerge.stories.tsx +7 -6
  24. package/src/extensions/automerge/cursor.ts +4 -37
  25. package/src/extensions/factories.ts +2 -2
  26. package/src/extensions/markdown/decorate.ts +90 -62
  27. package/src/extensions/markdown/formatting.ts +42 -35
  28. package/src/extensions/markdown/highlight.ts +3 -1
  29. package/src/extensions/markdown/table.ts +7 -6
  30. package/src/hooks/index.ts +0 -1
  31. package/src/hooks/useTextEditor.ts +1 -1
  32. package/dist/types/src/hooks/useDocAccessor.d.ts +0 -9
  33. package/dist/types/src/hooks/useDocAccessor.d.ts.map +0 -1
  34. package/src/hooks/useDocAccessor.ts +0 -27
@@ -10,7 +10,8 @@ import defaultsDeep from 'lodash.defaultsdeep';
10
10
  import React, { type FC, type KeyboardEvent, StrictMode, useMemo, useRef, useState } from 'react';
11
11
  import { createRoot } from 'react-dom/client';
12
12
 
13
- import { TextObject } from '@dxos/echo-schema';
13
+ import { TextV0Type } from '@braneframe/types';
14
+ import { createDocAccessor, create, createEchoObject } from '@dxos/echo-schema';
14
15
  import { keySymbols, parseShortcut } from '@dxos/keyboard';
15
16
  import { PublicKey } from '@dxos/keys';
16
17
  import { log } from '@dxos/log';
@@ -49,7 +50,6 @@ import {
49
50
  type CommentsOptions,
50
51
  type SelectionState,
51
52
  } from '../../extensions';
52
- import { useDocAccessor } from '../../hooks';
53
53
  import translations from '../../translations';
54
54
 
55
55
  faker.seed(101);
@@ -65,11 +65,11 @@ const text = {
65
65
  //
66
66
  '## Tasks',
67
67
  '',
68
- '- [x] decorator',
69
- '- [ ] checkbox',
70
- ' - [ ] state',
71
- ' - [ ] indent',
72
- ' - [x] style',
68
+ `- [x] ${faker.lorem.sentences()}`,
69
+ `- [ ] ${faker.lorem.sentences()}`,
70
+ ` - [ ] ${faker.lorem.sentences()}`,
71
+ ` - [ ] ${faker.lorem.sentences()}`,
72
+ ` - [x] ${faker.lorem.sentences()}`,
73
73
  '',
74
74
  ),
75
75
 
@@ -77,9 +77,9 @@ const text = {
77
77
  //
78
78
  '## List',
79
79
  '',
80
- '- new york',
81
- '- london',
82
- '- tokyo',
80
+ `- ${faker.lorem.sentences()}`,
81
+ `- ${faker.lorem.sentences()}`,
82
+ `- ${faker.lorem.sentences()}`,
83
83
  '',
84
84
  ),
85
85
 
@@ -87,9 +87,9 @@ const text = {
87
87
  //
88
88
  '## Numbered',
89
89
  '',
90
- '1. one',
91
- '2. two',
92
- '3. three',
90
+ `1. ${faker.lorem.sentences()}`,
91
+ `2. ${faker.lorem.sentences()}`,
92
+ `3. ${faker.lorem.sentences()}`,
93
93
  '',
94
94
  ),
95
95
 
@@ -241,7 +241,7 @@ type StoryProps = {
241
241
  comments?: Comment[];
242
242
  readonly?: boolean;
243
243
  placeholder?: string;
244
- } & Pick<TextEditorProps, 'extensions'>;
244
+ } & Pick<TextEditorProps, 'selection' | 'extensions'>;
245
245
 
246
246
  const Story = ({
247
247
  id = 'editor-' + PublicKey.random().toHex().slice(0, 8),
@@ -252,7 +252,7 @@ const Story = ({
252
252
  placeholder = 'New document.',
253
253
  ...props
254
254
  }: StoryProps) => {
255
- const { accessor } = useDocAccessor(new TextObject(text));
255
+ const [object] = useState(createEchoObject(create(TextV0Type, { content: text ?? '' })));
256
256
 
257
257
  const viewRef = useRef<EditorView>(null);
258
258
  useComments(viewRef.current, id, comments);
@@ -268,10 +268,10 @@ const Story = ({
268
268
  editor: { className: 'min-bs-dvh px-8 bg-white dark:bg-black' },
269
269
  },
270
270
  }),
271
- createDataExtensions({ id, text: accessor }),
271
+ createDataExtensions({ id, text: createDocAccessor(object, ['content']) }),
272
272
  _extensions,
273
273
  ],
274
- [_extensions],
274
+ [_extensions, object],
275
275
  );
276
276
 
277
277
  return (
@@ -294,11 +294,13 @@ export default {
294
294
  parameters: { translations, layout: 'fullscreen' },
295
295
  };
296
296
 
297
+ // TODO(burdon): Test invalid inputs (e.g., selection).
298
+
297
299
  const defaults = [
298
300
  autocomplete({
299
301
  onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
300
302
  }),
301
- decorateMarkdown({ renderLinkButton }),
303
+ decorateMarkdown({ renderLinkButton, selectionChangeDelay: 100 }),
302
304
  formattingKeymap(),
303
305
  image(),
304
306
  table(),
@@ -16,6 +16,7 @@ import React, {
16
16
  } from 'react';
17
17
 
18
18
  import { log } from '@dxos/log';
19
+ import { useDefaultValue } from '@dxos/react-ui';
19
20
  import { isNotFalsy } from '@dxos/util';
20
21
 
21
22
  import { documentId, editorMode, focusEvent } from '../../extensions';
@@ -51,12 +52,13 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
51
52
  (
52
53
  {
53
54
  id,
55
+ // TODO(wittjosiah): Rename initialText?
54
56
  doc,
55
57
  selection,
56
58
  extensions,
57
59
  className,
58
60
  autoFocus,
59
- scrollTo = EditorView.scrollIntoView(0, { yMargin: 0 }),
61
+ scrollTo: propsScrollTo,
60
62
  moveToEndOfLine,
61
63
  debug,
62
64
  dataTestId,
@@ -65,6 +67,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
65
67
  ) => {
66
68
  // NOTE: Increments by 2 in strict mode.
67
69
  const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
70
+ const scrollTo = useDefaultValue(propsScrollTo, EditorView.scrollIntoView(0, { yMargin: 0 }));
68
71
 
69
72
  // TODO(burdon): Make tabster optional.
70
73
  const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
@@ -90,10 +93,10 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
90
93
  //
91
94
  // EditorState
92
95
  // https://codemirror.net/docs/ref/#state.EditorStateConfig
96
+ // NOTE: Don't set selection here in case it is invalid (and crashes the state); dispatch below.
93
97
  //
94
98
  const state = EditorState.create({
95
99
  doc,
96
- selection,
97
100
  extensions: [
98
101
  id && documentId.of(id),
99
102
  // TODO(burdon): NOTE: Doesn't catch errors in keymap functions.
@@ -150,7 +153,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
150
153
  log('destroy', { id, instanceId });
151
154
  view?.destroy();
152
155
  };
153
- }, [doc, selection, extensions]);
156
+ }, [id, selection, scrollTo, editorMode, extensions]);
154
157
 
155
158
  // Focus editor on Enter (e.g., when tabbing to this component).
156
159
  const handleKeyUp = useCallback<KeyboardEventHandler<HTMLDivElement>>(
@@ -6,7 +6,8 @@ import '@dxosTheme';
6
6
 
7
7
  import React, { type FC, useState } from 'react';
8
8
 
9
- import { TextObject } from '@dxos/echo-schema';
9
+ import { TextV0Type } from '@braneframe/types';
10
+ import { createDocAccessor, create } from '@dxos/echo-schema';
10
11
  import { PublicKey } from '@dxos/keys';
11
12
  import { faker } from '@dxos/random';
12
13
  import { Tooltip, useThemeContext } from '@dxos/react-ui';
@@ -28,26 +29,25 @@ import {
28
29
  useComments,
29
30
  useFormattingState,
30
31
  } from '../../extensions';
31
- import { useActionHandler, useDocAccessor, useTextEditor } from '../../hooks';
32
+ import { useActionHandler, useTextEditor } from '../../hooks';
32
33
  import translations from '../../translations';
33
34
 
34
35
  faker.seed(101);
35
36
 
36
37
  const Story: FC<{ content: string }> = ({ content }) => {
37
38
  const { themeMode } = useThemeContext();
38
- const [item] = useState({ text: new TextObject(content) });
39
- const { id, doc, accessor } = useDocAccessor(item.text);
39
+ const [text] = useState(create(TextV0Type, { content }));
40
40
  const [formattingState, formattingObserver] = useFormattingState();
41
41
  const { parentRef, view } = useTextEditor(() => {
42
42
  return {
43
- id,
44
- doc,
43
+ id: text.id,
44
+ doc: text.content,
45
45
  extensions: [
46
46
  formattingObserver,
47
47
  createBasicExtensions(),
48
48
  createMarkdownExtensions({ themeMode }),
49
49
  createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2' } } }),
50
- createDataExtensions({ id, text: accessor }),
50
+ createDataExtensions({ id: text.id, text: createDocAccessor(text, ['content']) }),
51
51
  comments({
52
52
  onCreate: ({ cursor }) => {
53
53
  const id = PublicKey.random().toHex();
@@ -61,12 +61,12 @@ const Story: FC<{ content: string }> = ({ content }) => {
61
61
  table(),
62
62
  ],
63
63
  };
64
- }, [id, accessor, formattingObserver, themeMode]);
64
+ }, [text, formattingObserver, themeMode]);
65
65
 
66
66
  const handleAction = useActionHandler(view);
67
67
 
68
68
  const [_comments, setComments] = useState<Comment[]>([]);
69
- useComments(view, id, _comments);
69
+ useComments(view, text.id, _comments);
70
70
 
71
71
  return (
72
72
  <Tooltip.Provider>
@@ -8,10 +8,11 @@ import { BroadcastChannelNetworkAdapter } from '@automerge/automerge-repo-networ
8
8
  import '@preact/signals-react';
9
9
  import React, { useEffect, useMemo, useState } from 'react';
10
10
 
11
+ import { TextV0Type } from '@braneframe/types';
11
12
  import { Repo } from '@dxos/automerge/automerge-repo';
12
- import { createDocAccessor, Filter, DocAccessor } from '@dxos/echo-schema';
13
+ import { Filter, DocAccessor, create, createDocAccessor, type Expando } from '@dxos/echo-schema';
13
14
  import { type PublicKey } from '@dxos/keys';
14
- import { Expando, TextObject, useSpace } from '@dxos/react-client/echo';
15
+ import { useSpace } from '@dxos/react-client/echo';
15
16
  import { ClientRepeater } from '@dxos/react-client/testing';
16
17
  import { useThemeContext } from '@dxos/react-ui';
17
18
  import { withTheme } from '@dxos/storybook-utils';
@@ -97,9 +98,9 @@ const EchoStory = ({ spaceKey }: { spaceKey: PublicKey }) => {
97
98
  // const identity = useIdentity();
98
99
  const space = useSpace(spaceKey);
99
100
  const source = useMemo<DocAccessor | undefined>(() => {
100
- const { objects = [] } = space?.db.query(Filter.from({ type: 'test' })) ?? {};
101
+ const { objects = [] } = space?.db.query<Expando>(Filter.from({ type: 'test' })) ?? {};
101
102
  if (objects.length) {
102
- return createDocAccessor(objects[0].content);
103
+ return createDocAccessor(objects[0].content, ['content']);
103
104
  }
104
105
  }, [space]);
105
106
 
@@ -124,9 +125,9 @@ export const WithEcho = {
124
125
  createSpace
125
126
  onCreateSpace={async (space) => {
126
127
  space.db.add(
127
- new Expando({
128
+ create({
128
129
  type: 'test',
129
- content: new TextObject(initialContent),
130
+ content: create(TextV0Type, { content: initialContent }),
130
131
  }),
131
132
  );
132
133
  }}
@@ -2,30 +2,16 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import get from 'lodash.get';
6
-
7
- import { next as A } from '@dxos/automerge/automerge';
8
- import { type DocAccessor } from '@dxos/echo-schema';
5
+ import { toCursor, type DocAccessor, fromCursor } from '@dxos/echo-schema';
9
6
  import { log } from '@dxos/log';
10
7
 
11
8
  import { type CursorConverter } from '../cursor';
12
9
 
13
- export const cursorConverter = ({ handle, path }: DocAccessor): CursorConverter => ({
10
+ export const cursorConverter = (accessor: DocAccessor): CursorConverter => ({
14
11
  // TODO(burdon): Handle assoc to associate with a previous character.
15
12
  toCursor: (pos) => {
16
- const doc = handle.docSync();
17
- if (!doc) {
18
- return '';
19
- }
20
-
21
- const value = get(doc, path);
22
- if (typeof value === 'string' && value.length <= pos) {
23
- return 'end';
24
- }
25
-
26
13
  try {
27
- // NOTE: Slice is needed because getCursor mutates the array.
28
- return A.getCursor(doc, path.slice(), pos);
14
+ return toCursor(accessor, pos);
29
15
  } catch (err) {
30
16
  log.catch(err);
31
17
  return ''; // In case of invalid request (e.g., wrong document).
@@ -33,27 +19,8 @@ export const cursorConverter = ({ handle, path }: DocAccessor): CursorConverter
33
19
  },
34
20
 
35
21
  fromCursor: (cursor) => {
36
- if (cursor === '') {
37
- return 0;
38
- }
39
-
40
- const doc = handle.docSync();
41
- if (!doc) {
42
- return 0;
43
- }
44
-
45
- if (cursor === 'end') {
46
- const value = get(doc, path);
47
- if (typeof value === 'string') {
48
- return value.length;
49
- } else {
50
- return 0;
51
- }
52
- }
53
-
54
22
  try {
55
- // NOTE: Slice is needed because getCursor mutates the array.
56
- return A.getCursorPosition(doc, path.slice(), cursor);
23
+ return fromCursor(accessor, cursor);
57
24
  } catch (err) {
58
25
  log.catch(err);
59
26
  return 0; // In case of invalid request (e.g., wrong document).
@@ -162,14 +162,14 @@ export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: Theme
162
162
 
163
163
  export type DataExtensionsProps = {
164
164
  id: string;
165
- text: DocAccessor;
165
+ text?: DocAccessor;
166
166
  space?: Space;
167
167
  identity?: Identity | null;
168
168
  };
169
169
 
170
170
  // TODO(burdon): Move out of react-ui-editor (remove echo deps).
171
171
  export const createDataExtensions = ({ id, text, space, identity }: DataExtensionsProps): Extension[] => {
172
- const extensions: Extension[] = [automerge(text)];
172
+ const extensions: Extension[] = text ? [automerge(text)] : [];
173
173
 
174
174
  if (space && identity) {
175
175
  const peerId = identity?.identityKey.toHex();
@@ -81,6 +81,7 @@ const horizontalRule = Decoration.replace({ widget: new HorizontalRuleWidget() }
81
81
  const checkedTask = Decoration.replace({ widget: new CheckboxWidget(true) });
82
82
  const uncheckedTask = Decoration.replace({ widget: new CheckboxWidget(false) });
83
83
 
84
+ // Check if cursor is inside text.
84
85
  const editingRange = (state: EditorState, range: { from: number; to: number }, focus: boolean) => {
85
86
  const {
86
87
  readOnly,
@@ -103,76 +104,103 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
103
104
  from,
104
105
  to,
105
106
  enter: (node) => {
106
- if (node.name === 'FencedCode') {
107
- const editing = editingRange(state, node, focus);
107
+ switch (node.name) {
108
108
  // FencedCode > CodeMark > [CodeInfo] > CodeText > CodeMark
109
- for (const block of view.viewportLineBlocks) {
110
- if (block.to < node.from) {
111
- continue;
112
- }
113
- if (block.from > node.to) {
114
- break;
115
- }
116
- const first = block.from <= node.from;
117
- const last = block.to >= node.to && /^(\s>)*```$/.test(state.doc.sliceString(block.from, block.to));
118
- deco.add(block.from, block.from, first ? fencedCodeLineFirst : last ? fencedCodeLineLast : fencedCodeLine);
119
- if (!editing && (first || last)) {
120
- atomicDeco.add(block.from, block.to, hide);
109
+ case 'FencedCode': {
110
+ const editing = editingRange(state, node, focus);
111
+ for (const block of view.viewportLineBlocks) {
112
+ if (block.to < node.from) {
113
+ continue;
114
+ }
115
+ if (block.from > node.to) {
116
+ break;
117
+ }
118
+ const first = block.from <= node.from;
119
+ const last = block.to >= node.to && /^(\s>)*```$/.test(state.doc.sliceString(block.from, block.to));
120
+ deco.add(
121
+ block.from,
122
+ block.from,
123
+ first ? fencedCodeLineFirst : last ? fencedCodeLineLast : fencedCodeLine,
124
+ );
125
+ if (!editing && (first || last)) {
126
+ atomicDeco.add(block.from, block.to, hide);
127
+ }
121
128
  }
129
+ return false;
122
130
  }
123
- return false;
124
- } else if (node.name === 'Link') {
125
- const marks = node.node.getChildren('LinkMark');
126
- const urlNode = node.node.getChild('URL');
127
- const editing = editingRange(state, node, focus);
128
- if (urlNode && marks.length >= 2) {
129
- const url = state.sliceDoc(urlNode.from, urlNode.to);
130
- if (!editing) {
131
- atomicDeco.add(node.from, marks[0].to, hide);
132
- }
133
- deco.add(
134
- marks[0].to,
135
- marks[1].from,
136
- Decoration.mark({
137
- tagName: 'a',
138
- attributes: {
139
- class: 'cm-link',
140
- href: url,
141
- rel: 'noreferrer',
142
- target: '_blank',
143
- },
144
- }),
145
- );
146
- if (!editing) {
147
- atomicDeco.add(
131
+
132
+ case 'Link': {
133
+ const marks = node.node.getChildren('LinkMark');
134
+ const urlNode = node.node.getChild('URL');
135
+ const editing = editingRange(state, node, focus);
136
+ if (urlNode && marks.length >= 2) {
137
+ const url = state.sliceDoc(urlNode.from, urlNode.to);
138
+ if (!editing) {
139
+ atomicDeco.add(node.from, marks[0].to, hide);
140
+ }
141
+ deco.add(
142
+ marks[0].to,
148
143
  marks[1].from,
149
- node.to,
150
- options.renderLinkButton
151
- ? Decoration.replace({ widget: new LinkButton(url, options.renderLinkButton) })
152
- : hide,
144
+ Decoration.mark({
145
+ tagName: 'a',
146
+ attributes: {
147
+ class: 'cm-link',
148
+ href: url,
149
+ rel: 'noreferrer',
150
+ target: '_blank',
151
+ },
152
+ }),
153
153
  );
154
+ if (!editing) {
155
+ atomicDeco.add(
156
+ marks[1].from,
157
+ node.to,
158
+ options.renderLinkButton
159
+ ? Decoration.replace({ widget: new LinkButton(url, options.renderLinkButton) })
160
+ : hide,
161
+ );
162
+ }
163
+ }
164
+ break;
165
+ }
166
+
167
+ case 'HeaderMark': {
168
+ const parent = node.node.parent!;
169
+ if (/^ATX/.test(parent.name) && !editingRange(state, state.doc.lineAt(node.from), focus)) {
170
+ const next = state.doc.sliceString(node.to, node.to + 1);
171
+ atomicDeco.add(node.from, node.to + (next === ' ' ? 1 : 0), hide);
154
172
  }
173
+ break;
155
174
  }
156
- } else if (node.name === 'HeaderMark') {
157
- const parent = node.node.parent!;
158
- if (/^ATX/.test(parent.name) && !editingRange(state, state.doc.lineAt(node.from), focus)) {
159
- const next = state.doc.sliceString(node.to, node.to + 1);
160
- atomicDeco.add(node.from, node.to + (next === ' ' ? 1 : 0), hide);
175
+
176
+ case 'HorizontalRule': {
177
+ if (!editingRange(state, node, focus)) {
178
+ deco.add(node.from, node.to, horizontalRule);
179
+ }
180
+ break;
161
181
  }
162
- } else if (node.name === 'HorizontalRule') {
163
- if (!editingRange(state, node, focus)) {
164
- deco.add(node.from, node.to, horizontalRule);
182
+
183
+ case 'TaskMarker': {
184
+ if (!editingRange(state, node, focus)) {
185
+ const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
186
+ atomicDeco.add(node.from - 2, node.from - 1, Decoration.mark({ class: 'cm-task' }));
187
+ atomicDeco.add(node.from, node.to, checked ? checkedTask : uncheckedTask);
188
+ }
189
+ break;
165
190
  }
166
- } else if (node.name === 'TaskMarker') {
167
- // Check if cursor is inside text.
168
- if (!editingRange(state, node, focus)) {
169
- const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
170
- atomicDeco.add(node.from - 2, node.from - 1, Decoration.mark({ class: 'cm-task' }));
171
- atomicDeco.add(node.from, node.to, checked ? checkedTask : uncheckedTask);
191
+
192
+ case 'ListItem': {
193
+ const start = state.doc.lineAt(node.from);
194
+ deco.add(start.from, start.from, Decoration.line({ class: 'cm-list-item' }));
195
+ break;
172
196
  }
173
- } else if (MarksByParent.has(node.name)) {
174
- if (!editingRange(state, node.node.parent!, focus)) {
175
- atomicDeco.add(node.from, node.to, hide);
197
+
198
+ default: {
199
+ if (MarksByParent.has(node.name)) {
200
+ if (!editingRange(state, node.node.parent!, focus)) {
201
+ atomicDeco.add(node.from, node.to, hide);
202
+ }
203
+ }
176
204
  }
177
205
  }
178
206
  },
@@ -224,8 +252,6 @@ export const decorateMarkdown = (options: DecorateOptions = {}) => {
224
252
  }
225
253
  }
226
254
 
227
- // TODO(burdon): BUG: If the cursor is at the end of a link at the end of a line,
228
- // the cursor will float in space or be in the wrong position after the decoration is applied.
229
255
  scheduleUpdate(view: EditorView) {
230
256
  this.clearUpdate();
231
257
  this.pendingUpdate = setTimeout(() => {
@@ -304,4 +330,6 @@ const formattingStyles = EditorView.baseTheme({
304
330
  marginLeft: '4px',
305
331
  marginRight: '4px',
306
332
  },
333
+
334
+ // '& .cm-list-item > span:nth-child(2)': {},
307
335
  });