@dxos/react-ui-editor 0.3.9 → 0.3.10-main.051aa7d

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 (128) hide show
  1. package/dist/lib/browser/index.mjs +952 -736
  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 +38 -0
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -0
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +5 -2
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +9 -4
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts +10 -0
  11. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -0
  12. package/dist/types/src/components/TextEditor/extensions/{basic/bundle.d.ts → basic.d.ts} +1 -1
  13. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -0
  14. package/dist/types/src/components/TextEditor/extensions/comments.d.ts +18 -0
  15. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -0
  16. package/dist/types/src/components/TextEditor/extensions/demo.d.ts +10 -0
  17. package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -0
  18. package/dist/types/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.d.ts → experimental.d.ts} +2 -2
  19. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -0
  20. package/dist/types/src/components/TextEditor/extensions/index.d.ts +7 -2
  21. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  22. package/dist/types/src/components/TextEditor/extensions/link.d.ts +11 -0
  23. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -0
  24. package/dist/types/src/components/TextEditor/extensions/listener.d.ts +9 -0
  25. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -0
  26. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  27. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +31 -0
  28. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -0
  29. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts +1 -1
  30. package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +1 -1
  31. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +2 -0
  32. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -0
  33. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +12 -0
  34. package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +1 -0
  35. package/dist/types/src/components/TextEditor/index.d.ts +2 -0
  36. package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
  37. package/dist/types/src/components/TextEditor/themes/default.d.ts +20 -0
  38. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -0
  39. package/dist/types/src/components/TextEditor/themes/index.d.ts +2 -0
  40. package/dist/types/src/components/TextEditor/themes/index.d.ts.map +1 -0
  41. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +1 -0
  42. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -1
  43. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -0
  44. package/dist/types/src/hooks/automerge/amToCodemirror.d.ts.map +1 -0
  45. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -0
  46. package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts.map +1 -0
  47. package/dist/types/src/hooks/automerge/handle.d.ts.map +1 -0
  48. package/dist/types/src/hooks/automerge/index.d.ts.map +1 -0
  49. package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -0
  50. package/dist/types/src/hooks/index.d.ts +0 -1
  51. package/dist/types/src/hooks/index.d.ts.map +1 -1
  52. package/dist/types/src/hooks/useTextModel.d.ts +4 -4
  53. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  54. package/dist/types/src/styles/markdown.d.ts +12 -10
  55. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  56. package/dist/types/src/testing/replicator.d.ts.map +1 -1
  57. package/package.json +22 -32
  58. package/src/components/TextEditor/MarkdownEditor.stories.tsx +275 -0
  59. package/src/components/TextEditor/TextEditor.stories.tsx +46 -43
  60. package/src/components/TextEditor/TextEditor.tsx +79 -44
  61. package/src/components/TextEditor/extensions/autocomplete.ts +74 -0
  62. package/src/components/TextEditor/extensions/{basic/bundle.ts → basic.ts} +4 -2
  63. package/src/components/TextEditor/extensions/comments.ts +190 -0
  64. package/src/components/TextEditor/extensions/demo.ts +66 -0
  65. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.tsx → experimental.tsx} +18 -2
  66. package/src/components/TextEditor/extensions/index.ts +7 -2
  67. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkDecoration.ts → link.ts} +76 -31
  68. package/src/components/TextEditor/extensions/{change.ts → listener.ts} +4 -3
  69. package/src/components/TextEditor/extensions/markdown/bundle.ts +21 -25
  70. package/src/components/TextEditor/extensions/markdown/highlight.ts +167 -0
  71. package/src/components/TextEditor/extensions/markdown/index.ts +1 -1
  72. package/src/components/TextEditor/extensions/tasklist.ts +107 -0
  73. package/src/components/TextEditor/extensions/{hyperlink/hyperlinkTooltip.tsx → tooltip.tsx} +9 -9
  74. package/src/components/TextEditor/index.ts +3 -0
  75. package/src/components/TextEditor/themes/default.ts +185 -0
  76. package/src/components/TextEditor/themes/index.ts +5 -0
  77. package/src/components/TextEditor/yjs.stories.tsx +1 -0
  78. package/src/{automerge → hooks/automerge}/automerge.stories.tsx +2 -1
  79. package/src/hooks/index.ts +0 -1
  80. package/src/hooks/useTextModel.ts +52 -39
  81. package/src/styles/markdown.ts +26 -24
  82. package/src/testing/replicator.ts +6 -6
  83. package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +0 -1
  84. package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +0 -1
  85. package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +0 -1
  86. package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
  87. package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +0 -1
  88. package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +0 -1
  89. package/dist/types/src/automerge/automerge.stories.d.ts.map +0 -1
  90. package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts.map +0 -1
  91. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts +0 -3
  92. package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts.map +0 -1
  93. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts +0 -10
  94. package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts.map +0 -1
  95. package/dist/types/src/components/TextEditor/extensions/change.d.ts +0 -7
  96. package/dist/types/src/components/TextEditor/extensions/change.d.ts.map +0 -1
  97. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts +0 -11
  98. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts.map +0 -1
  99. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts +0 -8
  100. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +0 -1
  101. package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts.map +0 -1
  102. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts +0 -4
  103. package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts.map +0 -1
  104. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts +0 -17
  105. package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts.map +0 -1
  106. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts +0 -21
  107. package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts.map +0 -1
  108. package/dist/types/src/hooks/useCollaboration.d.ts +0 -9
  109. package/dist/types/src/hooks/useCollaboration.d.ts.map +0 -1
  110. package/src/components/TextEditor/extensions/basic/index.ts +0 -6
  111. package/src/components/TextEditor/extensions/basic/theme.ts +0 -49
  112. package/src/components/TextEditor/extensions/hyperlink/index.ts +0 -7
  113. package/src/components/TextEditor/extensions/markdown/tags.ts +0 -38
  114. package/src/components/TextEditor/extensions/markdown/theme.ts +0 -265
  115. package/src/hooks/useCollaboration.ts +0 -45
  116. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.d.ts +0 -0
  117. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/amToCodemirror.d.ts +0 -0
  118. /package/dist/types/src/{automerge → hooks/automerge}/automerge.stories.d.ts +0 -0
  119. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/codeMirrorToAm.d.ts +0 -0
  120. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/handle.d.ts +0 -0
  121. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/index.d.ts +0 -0
  122. /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/plugin.d.ts +0 -0
  123. /package/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.ts +0 -0
  124. /package/src/{automerge/automerge-plugin → hooks/automerge}/amToCodemirror.ts +0 -0
  125. /package/src/{automerge/automerge-plugin → hooks/automerge}/codeMirrorToAm.ts +0 -0
  126. /package/src/{automerge/automerge-plugin → hooks/automerge}/handle.ts +0 -0
  127. /package/src/{automerge/automerge-plugin → hooks/automerge}/index.ts +0 -0
  128. /package/src/{automerge/automerge-plugin → hooks/automerge}/plugin.ts +0 -0
@@ -7,23 +7,24 @@ import { EditorView } from '@codemirror/view';
7
7
  import { useFocusableGroup } from '@fluentui/react-tabster';
8
8
  import { vim } from '@replit/codemirror-vim';
9
9
  import defaultsDeep from 'lodash.defaultsdeep';
10
- import get from 'lodash.get';
11
10
  import React, {
11
+ type ComponentProps,
12
12
  type KeyboardEvent,
13
13
  forwardRef,
14
14
  useCallback,
15
15
  useEffect,
16
16
  useImperativeHandle,
17
17
  useState,
18
- type ComponentProps,
19
18
  } from 'react';
20
19
 
21
- import { isDocAccessor } from '@dxos/echo-schema';
20
+ import { generateName } from '@dxos/display-name';
22
21
  import { useThemeContext } from '@dxos/react-ui';
22
+ import { getColorForValue, inputSurface, mx } from '@dxos/react-ui-theme';
23
23
 
24
- import { basicBundle, basicTheme, markdownBundle, markdownTheme } from './extensions';
25
- import { type EditorModel, useCollaboration } from '../../hooks';
26
- import type { ThemeStyles } from '../../styles';
24
+ import { basicBundle, demo, markdownBundle } from './extensions';
25
+ import { defaultTheme, markdownTheme, textTheme } from './themes';
26
+ import { type EditorModel } from '../../hooks';
27
+ import { type ThemeStyles } from '../../styles';
27
28
 
28
29
  export const EditorModes = ['default', 'vim'] as const;
29
30
  export type EditorMode = (typeof EditorModes)[number];
@@ -37,7 +38,7 @@ export type CursorInfo = {
37
38
  };
38
39
 
39
40
  export type TextEditorRef = {
40
- editor: HTMLDivElement | null;
41
+ root: HTMLDivElement | null;
41
42
  state?: EditorState;
42
43
  view?: EditorView;
43
44
  };
@@ -65,66 +66,65 @@ export type TextEditorProps = {
65
66
  * NOTE: Rather than adding properties, try to create extensions that can be reused.
66
67
  */
67
68
  export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
68
- ({ model, extensions = [], slots, editorMode }, forwardedRef) => {
69
+ ({ model, extensions = [], slots = defaultSlots, editorMode }, forwardedRef) => {
69
70
  const { themeMode } = useThemeContext();
70
71
  const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
71
72
 
72
- const [parent, setParent] = useState<HTMLDivElement | null>(null);
73
+ const [root, setRoot] = useState<HTMLDivElement | null>(null);
73
74
  const [state, setState] = useState<EditorState>();
74
75
  const [view, setView] = useState<EditorView>();
75
- useImperativeHandle(
76
- forwardedRef,
77
- () => ({
78
- editor: parent,
79
- state,
80
- view,
81
- }),
82
- [view, state, parent],
83
- );
76
+ useImperativeHandle(forwardedRef, () => ({ root, state, view }), [view, state, root]);
84
77
 
85
- // TODO(burdon): Pass in extension?
86
- const collaboration = useCollaboration(model, themeMode);
78
+ // TODO(burdon): Factor out?
79
+ const { awareness, peer } = model;
80
+ useEffect(() => {
81
+ if (awareness && peer) {
82
+ awareness.setLocalStateField('user', {
83
+ name: peer.name ?? generateName(peer.id),
84
+ color: getColorForValue({ value: peer.id, type: 'color' }),
85
+ colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
86
+ });
87
+ }
88
+ }, [awareness, peer, themeMode]);
87
89
 
88
90
  useEffect(() => {
89
- if (!parent) {
91
+ if (!root) {
90
92
  return;
91
93
  }
92
94
 
93
- const initialText = isDocAccessor(model.content)
94
- ? get(model.content.handle.docSync(), model.content.path)
95
- : model.content?.toString();
96
-
97
95
  const state = EditorState.create({
98
- doc: initialText,
96
+ doc: model.text(),
99
97
  extensions: [
100
98
  // TODO(burdon): Factor out VIM mode?
101
99
  editorMode === 'vim' && vim(),
102
100
 
103
- // Replication.
104
- collaboration,
105
-
106
101
  // Theme.
107
- slots?.editor?.theme && EditorView.theme(slots?.editor?.theme),
102
+ EditorView.baseTheme(defaultTheme),
103
+ EditorView.theme(slots?.editor?.theme ?? {}),
104
+ // TODO(burdon): themeMode doesn't change in storybooks.
105
+ EditorView.darkTheme.of(themeMode === 'dark'),
106
+
107
+ // Storage and replication.
108
+ model.extension,
108
109
 
109
110
  // Custom.
110
111
  ...extensions,
111
112
  ].filter(Boolean) as Extension[],
112
113
  });
113
114
 
114
- setState(state);
115
-
116
115
  // NOTE: This repaints the editor.
117
116
  // If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
118
117
  // Ideally this should not be hit except when changing between text objects.
119
118
  view?.destroy();
120
- setView(new EditorView({ state, parent }));
119
+ setView(new EditorView({ state, parent: root }));
120
+ setState(state);
121
121
 
122
122
  return () => {
123
123
  view?.destroy();
124
124
  setView(undefined);
125
125
  setState(undefined);
126
126
  };
127
- }, [parent, model.content, themeMode, editorMode]);
127
+ }, [root, model, themeMode, editorMode]);
128
128
 
129
129
  const handleKeyUp = useCallback(
130
130
  (event: KeyboardEvent) => {
@@ -136,7 +136,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
136
136
  }
137
137
 
138
138
  case 'Escape': {
139
- editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && parent?.focus();
139
+ editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && root?.focus();
140
140
  break;
141
141
  }
142
142
  }
@@ -147,7 +147,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
147
147
  return (
148
148
  <div
149
149
  key={model.id}
150
- ref={setParent}
150
+ ref={setRoot}
151
151
  tabIndex={0}
152
152
  {...slots?.root}
153
153
  {...(editorMode !== 'vim' && tabsterDOMAttribute)}
@@ -157,15 +157,26 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
157
157
  },
158
158
  );
159
159
 
160
+ // TODO(burdon): Allow plugins to set extensions (factory).
161
+ const maybeDebug = (): Extension => {
162
+ // TODO(burdon): Parse JSON script format (with key bindings?)
163
+ const items = localStorage.getItem('dxos.composer.demo');
164
+ if (items) {
165
+ return demo({ items: items.split(',') });
166
+ }
167
+
168
+ return [];
169
+ };
170
+
160
171
  export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
161
- ({ extensions: _extensions, slots, ...props }, forwardedRef) => {
172
+ ({ extensions = [], slots: _slots, ...props }, forwardedRef) => {
162
173
  const { themeMode } = useThemeContext();
163
- const extensions = [...(_extensions ?? []), basicBundle({ themeMode, placeholder: slots?.editor?.placeholder })];
174
+ const slots = defaultsDeep({}, _slots, defaultTextSlots);
164
175
  return (
165
176
  <BaseTextEditor
166
177
  ref={forwardedRef}
167
- extensions={extensions}
168
- slots={defaultsDeep({}, slots, { editor: { theme: basicTheme } })}
178
+ extensions={[basicBundle({ themeMode, placeholder: slots?.editor?.placeholder }), maybeDebug(), ...extensions]}
179
+ slots={slots}
169
180
  {...props}
170
181
  />
171
182
  );
@@ -173,16 +184,40 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
173
184
  );
174
185
 
175
186
  export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
176
- ({ extensions: _extensions, slots, ...props }, forwardedRef) => {
187
+ ({ extensions = [], slots: _slots, ...props }, forwardedRef) => {
177
188
  const { themeMode } = useThemeContext();
178
- const extensions = [...(_extensions ?? []), markdownBundle({ themeMode })];
189
+ const slots = defaultsDeep({}, _slots, defaultMarkdownSlots);
179
190
  return (
180
191
  <BaseTextEditor
181
192
  ref={forwardedRef}
182
- extensions={extensions}
183
- slots={defaultsDeep({}, slots, { editor: { theme: markdownTheme } })}
193
+ extensions={[
194
+ markdownBundle({ themeMode, placeholder: slots?.editor?.placeholder }),
195
+ maybeDebug(),
196
+ ...extensions,
197
+ ]}
198
+ slots={slots}
184
199
  {...props}
185
200
  />
186
201
  );
187
202
  },
188
203
  );
204
+
205
+ export const defaultSlots: TextEditorSlots = {
206
+ root: {
207
+ className: mx('p-2', inputSurface),
208
+ },
209
+ };
210
+
211
+ export const defaultTextSlots: TextEditorSlots = {
212
+ ...defaultSlots,
213
+ editor: {
214
+ theme: textTheme,
215
+ },
216
+ };
217
+
218
+ export const defaultMarkdownSlots: TextEditorSlots = {
219
+ ...defaultSlots,
220
+ editor: {
221
+ theme: markdownTheme,
222
+ },
223
+ };
@@ -0,0 +1,74 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ // TODO(burdon): Automcomplete: https://codemirror.net/5/doc/manual.html#addon_runmode
6
+ // TODO(burdon): Modes: parallel parsing and decoration (e.g., associated with language).
7
+ // TODO(burdon): Add-on: runmode: run lexer over content (with rendering codemirror).
8
+ // https://codemirror.net/5/doc/manual.html#addon_runmode
9
+ // TODO(burdon): Add-on: dialog.
10
+ // TODO(burdon): Comments: https://codemirror.net/5/doc/manual.html#setBookmark
11
+ // TODO(burdon): Split view: https://codemirror.net/examples/split
12
+
13
+ // https://codemirror.net/examples/autocompletion
14
+ // https://codemirror.net/docs/ref/#autocomplete.autocompletion
15
+ // https://codemirror.net/docs/ref/#autocomplete.Completion
16
+
17
+ import {
18
+ autocompletion,
19
+ type Completion,
20
+ type CompletionContext,
21
+ completionKeymap,
22
+ type CompletionResult,
23
+ } from '@codemirror/autocomplete';
24
+ import { keymap } from '@codemirror/view';
25
+
26
+ export type AutocompleteResult = Completion;
27
+
28
+ export type AutocompleteOptions = {
29
+ onSearch: (text: string) => Completion[];
30
+ };
31
+
32
+ /**
33
+ * Autocomplete extension.
34
+ */
35
+ export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
36
+ return [
37
+ // https://codemirror.net/docs/ref/#view.keymap
38
+ // https://discuss.codemirror.net/t/how-can-i-replace-the-default-autocompletion-keymap-v6/3322
39
+ keymap.of(completionKeymap),
40
+
41
+ // https://codemirror.net/examples/autocompletion
42
+ // https://codemirror.net/docs/ref/#autocomplete.autocompletion
43
+ autocompletion({
44
+ // TODO(burdon): Set custom keymap.
45
+ // defaultKeymap: false,
46
+
47
+ // Don't start unless key press.
48
+ activateOnTyping: false,
49
+
50
+ // TODO(burdon): Option to create new page?
51
+ // TODO(burdon): Optional decoration via addToOptions
52
+ override: [
53
+ (context: CompletionContext): CompletionResult | null => {
54
+ const word = context.matchBefore(/\w*/);
55
+ if (!word || (word.from === word.to && !context.explicit)) {
56
+ return null;
57
+ }
58
+
59
+ // TODO(burdon): Option to convert to links?
60
+ return {
61
+ from: word.from,
62
+ options: onSearch(word.text.toLowerCase()),
63
+ // options: [
64
+ // { label: 'apple', type: 'keyword' },
65
+ // { label: 'amazon', type: 'keyword' },
66
+ // { label: 'hello', type: 'variable', info: '(World)' },
67
+ // { label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
68
+ // ],
69
+ };
70
+ },
71
+ ],
72
+ }),
73
+ ];
74
+ };
@@ -17,12 +17,14 @@ export type BasicBundleOptions = {
17
17
 
18
18
  export const basicBundle = ({ themeMode, placeholder: _placeholder }: BasicBundleOptions): Extension[] =>
19
19
  [
20
+ // TODO(burdon): Compare with minimalSetup.
21
+ // https://codemirror.net/docs/ref/#codemirror.minimalSetup
20
22
  bracketMatching(),
21
23
  closeBrackets(),
22
- _placeholder && placeholder(_placeholder),
23
-
24
24
  EditorView.lineWrapping,
25
25
 
26
+ _placeholder && placeholder(_placeholder),
27
+
26
28
  // TODO(burdon): Is this required?
27
29
  themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
28
30
  ].filter(Boolean) as Extension[];
@@ -0,0 +1,190 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+ import {
7
+ keymap,
8
+ type DecorationSet,
9
+ type Rect,
10
+ type ViewUpdate,
11
+ Decoration,
12
+ EditorView,
13
+ MatchDecorator,
14
+ ViewPlugin,
15
+ WidgetType,
16
+ } from '@codemirror/view';
17
+
18
+ import { debounce } from '@dxos/async';
19
+ import { invariant } from '@dxos/invariant';
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
37
+
38
+ class BookmarkWidget extends WidgetType {
39
+ constructor(private readonly _pos: number, private readonly _id: string) {
40
+ super();
41
+ invariant(this._id);
42
+ }
43
+
44
+ override eq(other: BookmarkWidget) {
45
+ return this._id === other._id;
46
+ }
47
+
48
+ override toDOM() {
49
+ const span = document.createElement('span');
50
+ span.className = 'cm-bookmark';
51
+ // TODO(burdon): Call out to react?
52
+ // https://emojifinder.com/comment
53
+ span.textContent = '💬';
54
+ return span;
55
+ }
56
+ }
57
+
58
+ // TODO(burdon): Reconcile with theme.
59
+ const styles = EditorView.baseTheme({
60
+ '& .cm-bookmark': {
61
+ cursor: 'pointer',
62
+ margin: '4px',
63
+ padding: '4px',
64
+ backgroundColor: 'yellow',
65
+ },
66
+ '& .cm-bookmark-selected': {
67
+ backgroundColor: 'orange',
68
+ },
69
+ });
70
+
71
+ type CommentsInfo = {
72
+ active?: string;
73
+ items: { id: string; pos: number; location: Rect | null }[];
74
+ };
75
+
76
+ export type CommentsOptions = {
77
+ key?: string;
78
+ onCreate?: () => string | void;
79
+ onUpdate?: (info: CommentsInfo) => void;
80
+ };
81
+
82
+ // https://www.markdownguide.org/extended-syntax/#footnotes
83
+ // TODO(burdon): Record span in automerge model?
84
+ // TODO(burdon): Extended markdown: https://www.markdownguide.org/extended-syntax
85
+ export const comments = (options: CommentsOptions = {}): Extension => {
86
+ let active: string | undefined;
87
+
88
+ const bookmarkMatcher = new MatchDecorator({
89
+ regexp: /\[\^(\w+)\]/g,
90
+ decoration: (match, view, pos) => {
91
+ const id = match[1];
92
+ return Decoration.replace({
93
+ id,
94
+ widget: new BookmarkWidget(pos, id),
95
+ });
96
+ },
97
+ });
98
+
99
+ const bookmarks = ViewPlugin.fromClass(
100
+ class {
101
+ bookmarks: DecorationSet;
102
+ constructor(view: EditorView) {
103
+ this.bookmarks = bookmarkMatcher.createDeco(view);
104
+ }
105
+
106
+ update(update: ViewUpdate) {
107
+ this.bookmarks = bookmarkMatcher.updateDeco(update, this.bookmarks);
108
+ }
109
+ },
110
+ {
111
+ decorations: (instance) => instance.bookmarks,
112
+ provide: (plugin) =>
113
+ EditorView.atomicRanges.of((view) => {
114
+ return view.plugin(plugin)?.bookmarks || Decoration.none;
115
+ }),
116
+ },
117
+ );
118
+
119
+ const doUpdate = debounce((data: CommentsInfo) => {
120
+ options.onUpdate?.(data);
121
+ }, 200);
122
+
123
+ return [
124
+ keymap.of([
125
+ {
126
+ key: options?.key ?? 'shift-meta-c',
127
+ run: (view) => {
128
+ // Insert footnote.
129
+ const id = options.onCreate?.();
130
+ if (id) {
131
+ const pos = view.state.selection.main.head;
132
+ const tag = `[^${id}]`;
133
+ view.dispatch({
134
+ changes: { from: pos, insert: tag },
135
+ selection: { anchor: pos + tag.length },
136
+ });
137
+
138
+ return true;
139
+ }
140
+
141
+ return false;
142
+ },
143
+ },
144
+ ]),
145
+
146
+ bookmarks,
147
+
148
+ // Monitor cursor movement.
149
+ EditorView.updateListener.of((update) => {
150
+ active = undefined;
151
+ if (!options.onUpdate) {
152
+ return;
153
+ }
154
+
155
+ const view = update.view;
156
+ const pos = view.state.selection.main.head;
157
+
158
+ const decorations: { from: number; to: number; value: Decoration }[] = [];
159
+ const rangeSet = view.plugin(bookmarks)?.bookmarks;
160
+ let closest = Infinity;
161
+ // TODO(burdon): Always shows entire document?
162
+ const { from, to } = view.visibleRanges?.[0] ?? { from: 0, to: view.state.doc.length };
163
+ rangeSet?.between(from, to, (from, to, value) => {
164
+ decorations.push({ from, to, value });
165
+ const d = Math.min(Math.abs(pos - from), Math.abs(pos - to));
166
+ if (d < closest) {
167
+ // TODO(burdon): Update class of selected.
168
+ active = value.spec.id;
169
+ closest = d;
170
+ }
171
+ });
172
+
173
+ if (decorations.length) {
174
+ doUpdate({
175
+ active,
176
+ items: decorations.map(
177
+ ({
178
+ from,
179
+ value: {
180
+ spec: { id },
181
+ },
182
+ }) => ({ id: id as string, pos: from, location: view.coordsAtPos(from) }),
183
+ ),
184
+ });
185
+ }
186
+ }),
187
+
188
+ styles,
189
+ ];
190
+ };
@@ -0,0 +1,66 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { keymap } from '@codemirror/view';
6
+
7
+ export type DemoOptions = {
8
+ delay?: number;
9
+ items?: string[];
10
+ };
11
+
12
+ const defaultItems = ['hello world!', 'this is a test.', 'this is [DXOS](https://dxos.org)'];
13
+
14
+ /**
15
+ * Demo script.
16
+ * Configurable plugin that let's user cycle through pre-configured input script.
17
+ */
18
+ export const demo = ({ delay = 75, items = defaultItems }: DemoOptions = {}) => {
19
+ let t: any;
20
+ let idx = 0;
21
+
22
+ return [
23
+ keymap.of([
24
+ {
25
+ // Reset.
26
+ key: "alt-meta-'",
27
+ run: (view) => {
28
+ clearTimeout(t);
29
+ idx = 0;
30
+ return true;
31
+ },
32
+ },
33
+ {
34
+ // Next prompt.
35
+ // TODO(burdon): Press 1-9 to select prompt?
36
+ key: "shift-meta-'",
37
+ run: (view) => {
38
+ clearTimeout(t);
39
+ // TODO(burdon): Add space if needed.
40
+ const text = items[idx++];
41
+ if (idx === items?.length) {
42
+ idx = 0;
43
+ }
44
+
45
+ let i = 0;
46
+ const insert = (d = 0) => {
47
+ t = setTimeout(() => {
48
+ const pos = view.state.selection.main.head;
49
+ view.dispatch({
50
+ changes: { from: pos, insert: text[i++] },
51
+ selection: { anchor: pos + 1 },
52
+ });
53
+
54
+ if (i < text.length) {
55
+ insert(Math.random() * delay * (text[i] === ' ' ? 2 : 1));
56
+ }
57
+ }, d);
58
+ };
59
+
60
+ insert();
61
+ return true;
62
+ },
63
+ },
64
+ ]),
65
+ ];
66
+ };
@@ -13,12 +13,28 @@ 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
+
16
31
  // Notes: Can't edit widget content (or cursor nav through them).
17
32
  // - https://discuss.codemirror.net/t/focusing-inputs-within-widgets/5178/7
18
33
  // Widgets intentionally always get set to contenteditable=false,
19
34
  // or they would become part of CodeMirror’s editable content element.
20
35
  // You should be able to introduce new contenteditable=true child elements inside of them.
21
36
 
37
+ // TODO(burdon): URL match?
22
38
  const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)(!?)/gi;
23
39
 
24
40
  /**
@@ -102,7 +118,7 @@ export function hyperLinkExtension() {
102
118
  );
103
119
  }
104
120
 
105
- export const hyperlinkStyle = EditorView.baseTheme({
121
+ export const styles = EditorView.baseTheme({
106
122
  '.cm-hyperlink-label': {
107
123
  cursor: 'pointer',
108
124
  textDecoration: 'underline',
@@ -116,4 +132,4 @@ export const hyperlinkStyle = EditorView.baseTheme({
116
132
  },
117
133
  });
118
134
 
119
- export const hyperlinkWidget: Extension = [hyperLinkExtension(), hyperlinkStyle];
135
+ export const hyperlinkWidget: Extension = [hyperLinkExtension(), styles];
@@ -2,7 +2,12 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './autocomplete';
5
6
  export * from './basic';
6
- export * from './change';
7
- export * from './hyperlink';
7
+ export * from './demo';
8
+ export * from './comments';
9
+ export * from './link';
10
+ export * from './listener';
8
11
  export * from './markdown';
12
+ export * from './tasklist';
13
+ export * from './tooltip';