@dxos/react-ui-editor 0.3.10-main.c5193cc → 0.3.10-main.c9b5e9b

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 (55) hide show
  1. package/dist/lib/browser/index.mjs +615 -380
  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 +6 -3
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +3 -0
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  9. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/extensions/blast.d.ts +1 -1
  11. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -1
  12. package/dist/types/src/components/TextEditor/extensions/code.d.ts +2 -0
  13. package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +1 -0
  14. package/dist/types/src/components/TextEditor/extensions/comments.d.ts +1 -1
  15. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -1
  16. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts +1 -1
  17. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -1
  18. package/dist/types/src/components/TextEditor/extensions/highlight.d.ts +24 -0
  19. package/dist/types/src/components/TextEditor/extensions/highlight.d.ts.map +1 -0
  20. package/dist/types/src/components/TextEditor/extensions/index.d.ts +2 -0
  21. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  22. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +8 -0
  23. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  24. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +3 -1
  25. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
  26. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
  27. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -1
  28. package/dist/types/src/hooks/useTextModel.d.ts +6 -0
  29. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  30. package/dist/types/src/hooks/yjs/yjs.stories.d.ts +1 -1
  31. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -1
  32. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  33. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  34. package/package.json +22 -20
  35. package/src/components/TextEditor/MarkdownEditor.stories.tsx +48 -32
  36. package/src/components/TextEditor/TextEditor.stories.tsx +12 -1
  37. package/src/components/TextEditor/TextEditor.tsx +18 -1
  38. package/src/components/TextEditor/extensions/basic.ts +4 -3
  39. package/src/components/TextEditor/extensions/blast.ts +14 -28
  40. package/src/components/TextEditor/extensions/code.ts +99 -0
  41. package/src/components/TextEditor/extensions/comments.ts +22 -40
  42. package/src/components/TextEditor/extensions/experimental.tsx +15 -36
  43. package/src/components/TextEditor/extensions/highlight.ts +128 -0
  44. package/src/components/TextEditor/extensions/index.ts +2 -0
  45. package/src/components/TextEditor/extensions/link.ts +1 -2
  46. package/src/components/TextEditor/extensions/markdown/bundle.ts +19 -19
  47. package/src/components/TextEditor/extensions/markdown/highlight.ts +37 -11
  48. package/src/components/TextEditor/extensions/table.ts +44 -48
  49. package/src/components/TextEditor/extensions/tasklist.ts +9 -9
  50. package/src/components/TextEditor/themes/default.ts +43 -2
  51. package/src/hooks/automerge/automerge.stories.tsx +1 -0
  52. package/src/hooks/useTextModel.ts +9 -0
  53. package/src/hooks/yjs/yjs.stories.tsx +1 -1
  54. package/src/styles/markdown.ts +5 -4
  55. package/src/testing/replicator.ts +1 -0
@@ -32,9 +32,15 @@ import {
32
32
  blast,
33
33
  demo,
34
34
  defaultOptions,
35
+ highlight,
36
+ code,
35
37
  } from './extensions';
36
38
  import { useTextModel } from '../../hooks';
37
39
 
40
+ // Extensions:
41
+ // TODO(burdon): Table of contents.
42
+ // TODO(burdon): Front-matter
43
+
38
44
  const str = (...lines: string[]) => lines.join('\n');
39
45
 
40
46
  const num = () => faker.number.int({ min: 0, max: 9999 }).toLocaleString();
@@ -78,6 +84,7 @@ const text = {
78
84
  '```tsx',
79
85
  'const Component = () => {',
80
86
  ' const x = 100;',
87
+ '',
81
88
  ' return () => <div>Test</div>;',
82
89
  '};',
83
90
  '',
@@ -93,24 +100,26 @@ const text = {
93
100
  '',
94
101
  ),
95
102
 
96
- headings: str(
97
- ...[1, 2, 3, 4, 5, 6].map((level) => ['#'.repeat(level) + ` Heading ${level}`, faker.lorem.sentences(), '']).flat(),
98
- ),
99
-
100
- paragraphs: str(...faker.helpers.multiple(() => [faker.lorem.paragraph(), ''], { count: 3 }).flat()),
101
-
102
103
  table: str(
103
104
  '# Table',
104
105
  '',
105
- `| ${faker.lorem.word()} | ${faker.lorem.word()} | ${faker.lorem.word()} |`,
106
- '|---|---|---|',
107
- `| ${num()} | ${num()} | ${num()} |`,
108
- `| ${num()} | ${num()} | ${num()} |`,
109
- `| ${num()} | ${num()} | ${num()} |`,
106
+ `| ${faker.lorem.word().padStart(8)} | ${faker.lorem.word().padStart(8)} | ${faker.lorem.word().padStart(8)} |`,
107
+ '|----------|----------|----------|',
108
+ `| ${num().padStart(8)} | ${num().padStart(8)} | ${num().padStart(8)} |`,
109
+ `| ${num().padStart(8)} | ${num().padStart(8)} | ${num().padStart(8)} |`,
110
+ `| ${num().padStart(8)} | ${num().padStart(8)} | ${num().padStart(8)} |`,
110
111
  '', // TODO(burdon): Possible GFM parsing bug if no newline?
111
112
  ),
112
113
 
113
114
  image: str('# Image', '', '![dxos](https://pbs.twimg.com/profile_banners/1268328127673044992/1684766689/1500x500)'),
115
+
116
+ headings: str(
117
+ ...[1, 2, 3, 4, 5, 6].map((level) => ['#'.repeat(level) + ` Heading ${level}`, faker.lorem.sentences(), '']).flat(),
118
+ ),
119
+
120
+ paragraphs: str(...faker.helpers.multiple(() => [faker.lorem.paragraph(), ''], { count: 3 }).flat()),
121
+
122
+ footer: str('', '', '', '', '')
114
123
  };
115
124
 
116
125
  const document = str(
@@ -138,6 +147,8 @@ const document = str(
138
147
  text.table,
139
148
  '---',
140
149
  text.image,
150
+ '',
151
+ text.footer,
141
152
  );
142
153
 
143
154
  const links = [
@@ -173,6 +184,7 @@ const onRender: LinkOptions['onRender'] = (el, url) => {
173
184
  );
174
185
  };
175
186
 
187
+ // TODO(burdon): Pass in model.
176
188
  const Story = ({
177
189
  text,
178
190
  automerge,
@@ -205,14 +217,15 @@ export default {
205
217
  };
206
218
 
207
219
  const extensions = [
208
- link({ onRender }),
209
- tooltip({ onHover }),
210
- tasklist(),
211
- table(),
212
- image(),
213
220
  autocomplete({
214
221
  onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
215
222
  }),
223
+ code(),
224
+ image(),
225
+ link({ onRender }),
226
+ table(),
227
+ tasklist(),
228
+ tooltip({ onHover }),
216
229
  ];
217
230
 
218
231
  export const Default = {
@@ -223,32 +236,30 @@ export const Readonly = {
223
236
  render: () => <Story text={document} extensions={extensions} readonly />,
224
237
  };
225
238
 
226
- export const Simple = {
227
- render: () => <Story text={document} />,
228
- };
229
-
230
239
  export const Tooltips = {
231
- render: () => <Story text={text.links} extensions={[tooltip({ onHover })]} />,
240
+ render: () => <Story text={str(text.links, text.footer)} extensions={[tooltip({ onHover })]} />,
232
241
  };
233
242
 
234
243
  export const Links = {
235
- render: () => <Story text={text.links} extensions={[link({ onRender })]} />,
244
+ render: () => <Story text={str(text.links, text.footer)} extensions={[link({ onRender })]} />,
245
+ };
246
+
247
+ export const Code = {
248
+ render: () => <Story text={str(text.code, text.footer)} extensions={[code()]} readonly />,
236
249
  };
237
250
 
238
251
  export const Table = {
239
- render: () => (
240
- <Story text={str('This is a table', '', text.table, '', text.table, '', '')} readonly extensions={[table()]} />
241
- ),
252
+ render: () => <Story text={str(text.table, text.footer)} extensions={[table()]} />,
242
253
  };
243
254
 
244
255
  export const Image = {
245
- render: () => <Story text={str('This is an image', '', text.image, '', '')} readonly extensions={[image()]} />,
256
+ render: () => <Story text={str(text.image, text.footer)} readonly extensions={[image()]} />,
246
257
  };
247
258
 
248
259
  export const TaskList = {
249
260
  render: () => (
250
261
  <Story
251
- text={str(text.tasks, '', text.list)}
262
+ text={str(text.tasks, '', text.list, text.footer)}
252
263
  extensions={[
253
264
  tasklist(),
254
265
  listener({
@@ -264,7 +275,7 @@ export const TaskList = {
264
275
  export const Autocomplete = {
265
276
  render: () => (
266
277
  <Story
267
- text={str('# Autocomplete', '', '', '', '', '', '')}
278
+ text={str('# Autocomplete', '', 'Press CTRL-SPACE', text.footer)}
268
279
  extensions={[
269
280
  link({ onRender }),
270
281
  autocomplete({
@@ -280,7 +291,7 @@ const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', '
280
291
  export const Mention = {
281
292
  render: () => (
282
293
  <Story
283
- text={str('# Mention', '', '', '', '', '', '')}
294
+ text={str('# Mention', '', 'Type @...', text.footer)}
284
295
  extensions={[
285
296
  mention({
286
297
  onSearch: (text) => names.filter((name) => name.toLowerCase().startsWith(text.toLowerCase())),
@@ -294,14 +305,15 @@ const mark = () => `[^${PublicKey.random().toHex()}]`;
294
305
  export const Comments = {
295
306
  render: () => (
296
307
  <Story
297
- text={str(mark(), '', text.paragraphs, mark(), '', mark(), '')}
308
+ text={str(text.paragraphs, mark(), '', mark(), text.footer)}
298
309
  extensions={[
299
310
  comments({
300
311
  onCreate: () => PublicKey.random().toHex(),
301
312
  onUpdate: (info) => {
302
- console.log('update', info);
313
+ // console.log('update', info);
303
314
  },
304
315
  }),
316
+ highlight(),
305
317
  ]}
306
318
  />
307
319
  ),
@@ -322,7 +334,7 @@ export const Diagnostics = {
322
334
  };
323
335
 
324
336
  export const Demo = {
325
- render: () => <Story text={text.paragraphs} extensions={[demo()]} />,
337
+ render: () => <Story text={str(text.paragraphs, text.footer)} extensions={[demo()]} />,
326
338
  };
327
339
 
328
340
  export const Blast = {
@@ -349,3 +361,7 @@ export const Blast = {
349
361
  />
350
362
  ),
351
363
  };
364
+
365
+ export const NoExtensions = {
366
+ render: () => <Story text={document} />,
367
+ };
@@ -57,6 +57,17 @@ export default {
57
57
  };
58
58
 
59
59
  export const Default = {
60
+ render: () => (
61
+ <Story
62
+ slots={defaultsDeep(
63
+ { editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
64
+ defaultSlots,
65
+ )}
66
+ />
67
+ ),
68
+ };
69
+
70
+ export const Text = {
60
71
  render: () => (
61
72
  <Story
62
73
  text={initialText}
@@ -71,12 +82,12 @@ export const Default = {
71
82
  export const Automerge = {
72
83
  render: () => (
73
84
  <Story
85
+ automerge
74
86
  text={initialText}
75
87
  slots={defaultsDeep(
76
88
  { editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
77
89
  defaultSlots,
78
90
  )}
79
- automerge
80
91
  />
81
92
  ),
82
93
  };
@@ -88,6 +88,12 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
88
88
  }
89
89
  }, [awareness, peer, themeMode]);
90
90
 
91
+ // TODO(burdon): Get from model.
92
+ // useHighlights(view, [
93
+ // { id: 'c-1', from: 0, to: 20 },
94
+ // { id: 'c-2', from: 100, to: 120 },
95
+ // ]);
96
+
91
97
  useEffect(() => {
92
98
  if (!root) {
93
99
  return;
@@ -102,6 +108,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
102
108
  editorMode === 'vim' && vim(),
103
109
 
104
110
  // Theme.
111
+ // TODO(burdon): Make optional.
105
112
  EditorView.baseTheme(defaultTheme),
106
113
  EditorView.theme(slots?.editor?.theme ?? {}),
107
114
  // TODO(burdon): themeMode doesn't change in storybooks.
@@ -119,7 +126,17 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
119
126
  // If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
120
127
  // Ideally this should not be hit except when changing between text objects.
121
128
  view?.destroy();
122
- setView(new EditorView({ state, parent: root }));
129
+ setView(
130
+ new EditorView({
131
+ state,
132
+ parent: root,
133
+ // NOTE: Uncomment to spy on all transactions.
134
+ // https://codemirror.net/docs/ref/#view.EditorView.dispatch
135
+ // dispatch: (transaction, view) => {
136
+ // view.update([transaction]);
137
+ // },
138
+ }),
139
+ );
123
140
  setState(state);
124
141
 
125
142
  return () => {
@@ -6,7 +6,7 @@ import { closeBrackets } from '@codemirror/autocomplete';
6
6
  import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
7
7
  import { type Extension } from '@codemirror/state';
8
8
  import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
9
- import { EditorView, placeholder } from '@codemirror/view';
9
+ import { drawSelection, EditorView, placeholder } from '@codemirror/view';
10
10
 
11
11
  import type { ThemeMode } from '@dxos/react-ui';
12
12
 
@@ -18,12 +18,13 @@ export type BasicBundleOptions = {
18
18
 
19
19
  export const basicBundle = ({ readonly, themeMode, placeholder: _placeholder }: BasicBundleOptions): Extension[] =>
20
20
  [
21
+ EditorView.lineWrapping,
22
+
21
23
  // TODO(burdon): Compare with minimalSetup.
22
24
  // https://codemirror.net/docs/ref/#codemirror.minimalSetup
23
25
  bracketMatching(),
24
26
  closeBrackets(),
25
- EditorView.lineWrapping,
26
-
27
+ drawSelection(),
27
28
  _placeholder && placeholder(_placeholder),
28
29
 
29
30
  // TODO(burdon): Is this required?
@@ -5,7 +5,7 @@
5
5
  // https://github.com/chinchang/code-blast-codemirror/blob/master/code-blast.js
6
6
  //
7
7
 
8
- import { type Extension, StateField } from '@codemirror/state';
8
+ import { type Extension } from '@codemirror/state';
9
9
  import { EditorView, keymap } from '@codemirror/view';
10
10
  import defaultsDeep from 'lodash.defaultsdeep';
11
11
 
@@ -36,7 +36,7 @@ export const defaultOptions: BlastOptions = {
36
36
  shakeIntensity: 5,
37
37
  };
38
38
 
39
- export const blast = (options: Partial<BlastOptions>): Extension => {
39
+ export const blast = (options: Partial<BlastOptions> = defaultOptions): Extension => {
40
40
  let blaster: Blaster | undefined;
41
41
  let last = 0;
42
42
 
@@ -77,39 +77,25 @@ export const blast = (options: Partial<BlastOptions>): Extension => {
77
77
  blaster.resize();
78
78
  }
79
79
 
80
- const current = update.view.state.selection.main.head;
80
+ const current = update.state.selection.main.head;
81
81
  if (current !== last) {
82
82
  last = current;
83
83
  // TODO(burdon): Null if end of line.
84
- const { element, point } = getPoint(update.view, current);
84
+ const { element, point } = getPoint(update.view, current - 1);
85
85
  if (element && point) {
86
86
  blaster.spawn({ element, point });
87
87
  }
88
88
  }
89
89
  }),
90
90
 
91
- // Document changed.
92
- StateField.define({
93
- create: () => null,
94
- update: (_value, transaction) => {
95
- if (blaster && transaction.docChanged) {
96
- // TODO(burdon): Only on delete.
97
- const view = EditorView.findFromDOM(blaster.node)!;
98
- const { element, point } = getPoint(view, transaction.selection!.main.head);
99
- if (element && point) {
100
- blaster.spawn({ element, point });
101
- }
102
- }
103
-
104
- return null;
105
- },
106
- }),
107
-
108
91
  keymap.of([
109
92
  {
110
- any: (view, event) => {
111
- if (blaster && event.key === 'Enter' && event.shiftKey) {
112
- blaster.shake({ time: 0.4 });
93
+ any: (_, event) => {
94
+ if (blaster) {
95
+ if (event.key === 'Enter' && event.shiftKey) {
96
+ blaster.shake({ time: 0.8 });
97
+ return true;
98
+ }
113
99
  }
114
100
 
115
101
  return false;
@@ -124,11 +110,11 @@ export const blast = (options: Partial<BlastOptions>): Extension => {
124
110
  //
125
111
 
126
112
  class Blaster {
113
+ private readonly _effect: Effect;
114
+
127
115
  _canvas: HTMLCanvasElement | undefined;
128
116
  _ctx: CanvasRenderingContext2D | undefined | null;
129
117
 
130
- private readonly _effect: Effect;
131
-
132
118
  _running = false;
133
119
  _lastTime: number | undefined;
134
120
  _shakeTime = 0;
@@ -156,7 +142,7 @@ class Blaster {
156
142
  this._canvas.style.position = 'absolute';
157
143
  this._canvas.style.zIndex = '0';
158
144
  this._canvas.style.pointerEvents = 'none';
159
- // this._canvas.style.border = '2px solid red';
145
+ // this._canvas.style.border = '2px dashed red';
160
146
 
161
147
  this._ctx = this._canvas.getContext('2d');
162
148
 
@@ -226,7 +212,7 @@ class Blaster {
226
212
  }
227
213
 
228
214
  shake = throttle<{ time: number }>(({ time }) => {
229
- this._shakeTime = this._shakeTimeMax;
215
+ this._shakeTime = this._shakeTimeMax || time;
230
216
  this._shakeTimeMax = time;
231
217
  }, 100);
232
218
 
@@ -0,0 +1,99 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Range } from '@codemirror/state';
6
+ import {
7
+ ViewPlugin,
8
+ type DecorationSet,
9
+ EditorView,
10
+ type ViewUpdate,
11
+ Decoration,
12
+ type BlockInfo,
13
+ } from '@codemirror/view';
14
+ import get from 'lodash.get';
15
+
16
+ import { mx } from '@dxos/react-ui-theme';
17
+
18
+ import { tokens } from '../../../styles';
19
+
20
+ const CODE_REGEX = /```[\s\S]*?```/gs;
21
+
22
+ // TODO(burdon): Reconcile with theme.
23
+ const styles = EditorView.baseTheme({
24
+ '& .cm-code-block': {
25
+ display: 'block',
26
+ paddingInline: '4px !important',
27
+ },
28
+ '&light .cm-code-block': {
29
+ background: get(tokens, 'extend.colors.neutral.50'),
30
+ },
31
+ '&dark .cm-code-block': {
32
+ background: get(tokens, 'extend.colors.neutral.850'),
33
+ },
34
+ '& .cm-code-block-first': {
35
+ paddingTop: '4px !important',
36
+ borderTopLeftRadius: '4px',
37
+ borderTopRightRadius: '4px',
38
+ },
39
+ '& .cm-code-block-last': {
40
+ paddingBottom: '4px !important',
41
+ borderBottomLeftRadius: '4px',
42
+ borderBottomRightRadius: '4px',
43
+ },
44
+ });
45
+
46
+ const getLineRange = (lines: BlockInfo[], from: number, to: number) => {
47
+ const start = lines.findIndex((line) => line.from >= from);
48
+ const end = lines.findIndex((line) => line.to >= to);
49
+ return [start, end];
50
+ };
51
+
52
+ export const code = () => {
53
+ const getDecorations = (view: EditorView) => {
54
+ const decorations: Range<Decoration>[] = [];
55
+ if (view.state.readOnly) {
56
+ const text = view.state.doc.sliceString(0);
57
+ const matches = text.matchAll(CODE_REGEX);
58
+ const blocks = view.viewportLineBlocks;
59
+ for (const match of matches) {
60
+ const range = getLineRange(blocks, match.index!, match.index! + match[0].length);
61
+ for (let i = range[0]; i <= range[1]; i++) {
62
+ const block = blocks[i];
63
+ decorations.push(
64
+ Decoration.line({
65
+ class: mx(
66
+ 'cm-code-block',
67
+ i === range[0] && 'cm-code-block-first',
68
+ i === range[1] && 'cm-code-block-last',
69
+ ),
70
+ }).range(block.from),
71
+ );
72
+ }
73
+ }
74
+ }
75
+
76
+ return Decoration.set(decorations);
77
+ };
78
+
79
+ return [
80
+ ViewPlugin.fromClass(
81
+ class {
82
+ decorations: DecorationSet;
83
+ constructor(view: EditorView) {
84
+ this.decorations = getDecorations(view);
85
+ }
86
+
87
+ update(update: ViewUpdate) {
88
+ if (update.docChanged || update.viewportChanged) {
89
+ this.decorations = getDecorations(update.view);
90
+ }
91
+ }
92
+ },
93
+ {
94
+ decorations: (value) => value.decorations,
95
+ },
96
+ ),
97
+ styles,
98
+ ];
99
+ };
@@ -18,22 +18,18 @@ import {
18
18
  import { debounce } from '@dxos/async';
19
19
  import { invariant } from '@dxos/invariant';
20
20
 
21
- // 1. TODO(burdon): Make atomic (for tasklist also).
22
- // - https://discuss.codemirror.net/t/easily-track-remove-content-with-decorations/4606
23
- // - https://discuss.codemirror.net/t/creating-atomic-replace-decorations/2961
24
- // - https://codemirror.net/docs/ref/#state.EditorState%5EtransactionFilter (transaction filter: move, delete).
25
- // 2. TODO(burdon): Create/track threads in composer (change global state). Select/follow. Scroll with page.
26
- // - Separate from chat/search.
27
- // 3. TODO(burdon): Support multiple threads in sidebar?
28
- // 4. TODO(burdon): Anchor AI thread when creating section.
29
- // 5. TODO(burdon): Button to resolve thread to close comments.
30
-
31
- // 6. TODO(burdon): Perf: Update existing rangeset.
32
- // https://discuss.codemirror.net/t/rangeset-with-metadata-and-different-decorations/3874
33
-
34
- // TODO(burdon): Import note (performance):
35
- // Easily track & remove content with decorations
36
- // https://discuss.codemirror.net/t/easily-track-remove-content-with-decorations/4606
21
+ // TODO(burdon): Reconcile with theme.
22
+ const styles = EditorView.baseTheme({
23
+ '& .cm-bookmark': {
24
+ cursor: 'pointer',
25
+ margin: '4px',
26
+ padding: '4px',
27
+ backgroundColor: 'yellow',
28
+ },
29
+ '& .cm-bookmark-selected': {
30
+ backgroundColor: 'orange',
31
+ },
32
+ });
37
33
 
38
34
  class BookmarkWidget extends WidgetType {
39
35
  constructor(private readonly _pos: number, private readonly _id: string, private readonly _handleClick: () => void) {
@@ -48,27 +44,12 @@ class BookmarkWidget extends WidgetType {
48
44
  override toDOM() {
49
45
  const span = document.createElement('span');
50
46
  span.className = 'cm-bookmark';
51
- // TODO(burdon): Call out to react?
52
- // https://emojifinder.com/comment
53
- span.textContent = '💬';
47
+ span.textContent = '§';
54
48
  span.onclick = () => this._handleClick();
55
49
  return span;
56
50
  }
57
51
  }
58
52
 
59
- // TODO(burdon): Reconcile with theme.
60
- const styles = EditorView.baseTheme({
61
- '& .cm-bookmark': {
62
- cursor: 'pointer',
63
- margin: '4px',
64
- padding: '4px',
65
- backgroundColor: 'yellow',
66
- },
67
- '& .cm-bookmark-selected': {
68
- backgroundColor: 'orange',
69
- },
70
- });
71
-
72
53
  type CommentsInfo = {
73
54
  active?: string;
74
55
  items?: { id: string; pos: number; location: Rect | null }[];
@@ -76,7 +57,7 @@ type CommentsInfo = {
76
57
 
77
58
  export type CommentsOptions = {
78
59
  key?: string;
79
- onCreate?: () => string | void;
60
+ onCreate?: (from: number, to: number) => string | void;
80
61
  onUpdate?: (info: CommentsInfo) => void;
81
62
  };
82
63
 
@@ -127,13 +108,14 @@ export const comments = (options: CommentsOptions = {}): Extension => {
127
108
  key: options?.key ?? 'shift-meta-c',
128
109
  run: (view) => {
129
110
  // Insert footnote.
130
- const id = options.onCreate?.();
111
+ const { from, to, head } = view.state.selection.main;
112
+ const id = options.onCreate?.(from, to);
131
113
  if (id) {
132
- const pos = view.state.selection.main.head;
133
114
  const tag = `[^${id}]`;
115
+ // TODO(burdon): Add selection.
134
116
  view.dispatch({
135
- changes: { from: pos, insert: tag },
136
- selection: { anchor: pos + tag.length },
117
+ changes: { from: head, insert: tag },
118
+ selection: { anchor: head + tag.length },
137
119
  });
138
120
 
139
121
  return true;
@@ -153,14 +135,14 @@ export const comments = (options: CommentsOptions = {}): Extension => {
153
135
  return;
154
136
  }
155
137
 
156
- const view = update.view;
157
- const pos = view.state.selection.main.head;
138
+ const { view, state } = update;
139
+ const pos = state.selection.main.head;
158
140
 
159
141
  const decorations: { from: number; to: number; value: Decoration }[] = [];
160
142
  const rangeSet = view.plugin(bookmarks)?.bookmarks;
161
143
  let closest = Infinity;
162
144
  // TODO(burdon): Handle multiple visible ranges in large documents.
163
- const { from, to } = /* view.visibleRanges?.[0] ?? */ { from: 0, to: view.state.doc.length };
145
+ const { from, to } = /* view.visibleRanges?.[0] ?? */ { from: 0, to: state.doc.length };
164
146
  rangeSet?.between(from, to, (from, to, value) => {
165
147
  decorations.push({ from, to, value });
166
148
  const d = Math.min(Math.abs(pos - from), Math.abs(pos - to));
@@ -13,28 +13,21 @@ import {
13
13
  type ViewUpdate,
14
14
  } from '@codemirror/view';
15
15
 
16
- // TODO(burdon): Modes: parallel parsing and decoration (e.g., associated with language).
17
- // TODO(burdon): Add-on: runmode: run lexer over content (with rendering codemirror).
18
- // https://codemirror.net/5/doc/manual.html#addon_runmode
19
- // TODO(burdon): Add-on: dialog.
20
- // TODO(burdon): Comments: https://codemirror.net/5/doc/manual.html#setBookmark
21
- // TODO(burdon): Split view: https://codemirror.net/examples/split
22
- // TODO(burdon): https://codemirror.net/5/demo/simplemode.html
23
-
24
- // https://codemirror.net/examples/autocompletion
25
- // https://codemirror.net/docs/ref/#autocomplete.autocompletion
26
- // https://codemirror.net/docs/ref/#autocomplete.Completion
27
-
28
- // TODO(burdon): Hint to customize?
29
- // https://codemirror.net/examples/autocompletion
30
-
31
- // Notes: Can't edit widget content (or cursor nav through them).
32
- // - https://discuss.codemirror.net/t/focusing-inputs-within-widgets/5178/7
33
- // Widgets intentionally always get set to contenteditable=false,
34
- // or they would become part of CodeMirror’s editable content element.
35
- // You should be able to introduce new contenteditable=true child elements inside of them.
16
+ // TODO(burdon): Reconcile with theme.
17
+ export const styles = EditorView.baseTheme({
18
+ '.cm-hyperlink-label': {
19
+ cursor: 'pointer',
20
+ textDecoration: 'underline',
21
+ },
22
+ '.cm-hyperlink-bracket': {
23
+ opacity: 0.2,
24
+ },
25
+ '.cm-hyperlink-url': {
26
+ opacity: 0.5,
27
+ fontFamily: 'monospace',
28
+ },
29
+ });
36
30
 
37
- // TODO(burdon): URL match?
38
31
  const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)(!?)/gi;
39
32
 
40
33
  /**
@@ -99,7 +92,7 @@ export function hyperLinkExtension() {
99
92
  }
100
93
  },
101
94
  {
102
- decorations: (view) => view.decorations,
95
+ decorations: (value) => value.decorations,
103
96
  eventHandlers: {
104
97
  // TODO(burdon): CLick to expand link.
105
98
  // https://discuss.codemirror.net/t/decorator-iteration-on-click-event/4226/2
@@ -118,18 +111,4 @@ export function hyperLinkExtension() {
118
111
  );
119
112
  }
120
113
 
121
- export const styles = EditorView.baseTheme({
122
- '.cm-hyperlink-label': {
123
- cursor: 'pointer',
124
- textDecoration: 'underline',
125
- },
126
- '.cm-hyperlink-bracket': {
127
- opacity: 0.2,
128
- },
129
- '.cm-hyperlink-url': {
130
- opacity: 0.5,
131
- fontFamily: 'monospace',
132
- },
133
- });
134
-
135
114
  export const hyperlinkWidget: Extension = [hyperLinkExtension(), styles];