@dxos/react-ui-editor 0.3.10-main.3c0616c → 0.3.10-main.3edbcce
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.
- package/dist/lib/browser/index.mjs +410 -215
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +3 -0
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +1 -0
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts +3 -2
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts +18 -0
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/demo.d.ts +10 -0
- package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/index.d.ts +2 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts +4 -2
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +1 -4
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/themes/default.d.ts +3 -0
- package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
- package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.d.ts +3 -0
- package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/handle.d.ts.map +1 -0
- package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/index.d.ts +1 -1
- package/dist/types/src/hooks/automerge/index.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts +5 -0
- package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts +6 -0
- package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/translation/index.d.ts +3 -0
- package/dist/types/src/hooks/automerge/translation/index.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +0 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +3 -4
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/testing/replicator.d.ts.map +1 -1
- package/package.json +20 -19
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +41 -8
- package/src/components/TextEditor/TextEditor.tsx +39 -21
- package/src/components/TextEditor/extensions/autocomplete.ts +5 -3
- package/src/components/TextEditor/extensions/comments.ts +190 -0
- package/src/components/TextEditor/extensions/demo.ts +66 -0
- package/src/components/TextEditor/extensions/experimental.tsx +1 -0
- package/src/components/TextEditor/extensions/index.ts +2 -0
- package/src/components/TextEditor/extensions/link.ts +10 -2
- package/src/components/TextEditor/extensions/listener.ts +3 -2
- package/src/components/TextEditor/extensions/markdown/bundle.ts +6 -14
- package/src/components/TextEditor/extensions/markdown/highlight.ts +2 -1
- package/src/components/TextEditor/extensions/tasklist.ts +73 -79
- package/src/components/TextEditor/themes/default.ts +15 -0
- package/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.ts +12 -9
- package/src/{automerge → hooks/automerge}/automerge.stories.tsx +2 -1
- package/src/{automerge/automerge-plugin → hooks/automerge}/plugin.ts +1 -0
- package/src/{automerge/automerge-plugin/amToCodemirror.ts → hooks/automerge/translation/automerge-to-codemirror.ts} +10 -2
- package/src/{automerge/automerge-plugin/codeMirrorToAm.ts → hooks/automerge/translation/codemirror-to-automerge.ts} +7 -4
- package/src/hooks/automerge/translation/index.ts +6 -0
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useTextModel.ts +49 -41
- package/src/testing/replicator.ts +6 -7
- package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts +0 -6
- package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts +0 -7
- package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge.stories.d.ts.map +0 -1
- package/dist/types/src/hooks/useCollaboration.d.ts +0 -9
- package/dist/types/src/hooks/useCollaboration.d.ts.map +0 -1
- package/src/hooks/useCollaboration.ts +0 -45
- package/dist/types/src/{automerge → hooks/automerge}/automerge.stories.d.ts +0 -0
- package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/handle.d.ts +0 -0
- package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/plugin.d.ts +0 -0
- package/src/{automerge/automerge-plugin → hooks/automerge}/handle.ts +0 -0
- package/src/{automerge/automerge-plugin → hooks/automerge}/index.ts +1 -1
|
@@ -10,11 +10,21 @@ import React, { StrictMode, useRef, useState } from 'react';
|
|
|
10
10
|
import { createRoot } from 'react-dom/client';
|
|
11
11
|
|
|
12
12
|
import { TextObject } from '@dxos/echo-schema';
|
|
13
|
+
import { PublicKey } from '@dxos/keys';
|
|
13
14
|
import { fixedInsetFlexLayout, getSize, groupSurface, mx } from '@dxos/react-ui-theme';
|
|
14
15
|
import { withTheme } from '@dxos/storybook-utils';
|
|
15
16
|
|
|
16
17
|
import { MarkdownEditor, type TextEditorProps, type TextEditorRef } from './TextEditor';
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
autocomplete,
|
|
20
|
+
listener,
|
|
21
|
+
link,
|
|
22
|
+
tasklist,
|
|
23
|
+
tooltip,
|
|
24
|
+
type TooltipOptions,
|
|
25
|
+
type LinkOptions,
|
|
26
|
+
comments,
|
|
27
|
+
} from './extensions';
|
|
18
28
|
import { useTextModel } from '../../hooks';
|
|
19
29
|
|
|
20
30
|
// TODO(burdon): Read-only render mode (presentation).
|
|
@@ -31,9 +41,11 @@ const text = {
|
|
|
31
41
|
tasks: str(
|
|
32
42
|
'## Tasks',
|
|
33
43
|
'',
|
|
34
|
-
'- [x]
|
|
35
|
-
'- [ ]
|
|
36
|
-
'- [ ]
|
|
44
|
+
'- [x] decorator',
|
|
45
|
+
'- [ ] checkbox',
|
|
46
|
+
' - [ ] state',
|
|
47
|
+
' - [ ] indent',
|
|
48
|
+
' - [x] style',
|
|
37
49
|
),
|
|
38
50
|
|
|
39
51
|
list: str(
|
|
@@ -80,6 +92,8 @@ const text = {
|
|
|
80
92
|
headings: str(
|
|
81
93
|
...[1, 2, 3, 4, 5, 6].map((level) => ['#'.repeat(level) + ` Heading ${level}`, faker.lorem.sentences(), '']).flat(),
|
|
82
94
|
),
|
|
95
|
+
|
|
96
|
+
paragraphs: str(...faker.helpers.multiple(() => [faker.lorem.paragraph(), ''], { count: 3 }).flat())
|
|
83
97
|
};
|
|
84
98
|
|
|
85
99
|
const document = str(
|
|
@@ -180,7 +194,7 @@ export const Default = {
|
|
|
180
194
|
tooltip({ onHover }),
|
|
181
195
|
tasklist(),
|
|
182
196
|
autocomplete({
|
|
183
|
-
|
|
197
|
+
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
|
184
198
|
}),
|
|
185
199
|
]}
|
|
186
200
|
/>
|
|
@@ -205,8 +219,10 @@ export const TaskList = {
|
|
|
205
219
|
text={str(text.tasks, '', text.list)}
|
|
206
220
|
extensions={[
|
|
207
221
|
tasklist(),
|
|
208
|
-
listener(
|
|
209
|
-
|
|
222
|
+
listener({
|
|
223
|
+
onChange: (text) => {
|
|
224
|
+
console.log(text);
|
|
225
|
+
},
|
|
210
226
|
}),
|
|
211
227
|
]}
|
|
212
228
|
/>
|
|
@@ -220,7 +236,24 @@ export const Autocomplete = {
|
|
|
220
236
|
extensions={[
|
|
221
237
|
link({ onRender }),
|
|
222
238
|
autocomplete({
|
|
223
|
-
|
|
239
|
+
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
|
240
|
+
}),
|
|
241
|
+
]}
|
|
242
|
+
/>
|
|
243
|
+
),
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const mark = () => `[^${PublicKey.random().toHex()}]`;
|
|
247
|
+
export const Comments = {
|
|
248
|
+
render: () => (
|
|
249
|
+
<Story
|
|
250
|
+
text={str(mark(), '', text.paragraphs, mark(), '', mark(), '')}
|
|
251
|
+
extensions={[
|
|
252
|
+
comments({
|
|
253
|
+
onCreate: () => PublicKey.random().toHex(),
|
|
254
|
+
onUpdate: (info) => {
|
|
255
|
+
console.log('update', info);
|
|
256
|
+
},
|
|
224
257
|
}),
|
|
225
258
|
]}
|
|
226
259
|
/>
|
|
@@ -16,15 +16,13 @@ import React, {
|
|
|
16
16
|
useImperativeHandle,
|
|
17
17
|
useState,
|
|
18
18
|
} from 'react';
|
|
19
|
-
import { yCollab } from 'y-codemirror.next';
|
|
20
19
|
|
|
21
20
|
import { generateName } from '@dxos/display-name';
|
|
22
21
|
import { useThemeContext } from '@dxos/react-ui';
|
|
23
22
|
import { getColorForValue, inputSurface, mx } from '@dxos/react-ui-theme';
|
|
24
|
-
import { type YText } from '@dxos/text-model';
|
|
25
23
|
|
|
26
|
-
import { basicBundle, markdownBundle } from './extensions';
|
|
27
|
-
import { defaultTheme, textTheme } from './themes';
|
|
24
|
+
import { basicBundle, demo, markdownBundle } from './extensions';
|
|
25
|
+
import { defaultTheme, markdownTheme, textTheme } from './themes';
|
|
28
26
|
import { type EditorModel } from '../../hooks';
|
|
29
27
|
import { type ThemeStyles } from '../../styles';
|
|
30
28
|
|
|
@@ -72,23 +70,22 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
72
70
|
const { themeMode } = useThemeContext();
|
|
73
71
|
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
73
|
+
const [root, setRoot] = useState<HTMLDivElement | null>(null);
|
|
74
|
+
const [state, setState] = useState<EditorState>();
|
|
75
|
+
const [view, setView] = useState<EditorView>();
|
|
76
|
+
useImperativeHandle(forwardedRef, () => ({ root, state, view }), [view, state, root]);
|
|
77
|
+
|
|
78
|
+
// TODO(burdon): Factor out?
|
|
79
|
+
const { awareness, peer } = model;
|
|
78
80
|
useEffect(() => {
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
+
if (awareness && peer) {
|
|
82
|
+
awareness.setLocalStateField('user', {
|
|
81
83
|
name: peer.name ?? generateName(peer.id),
|
|
82
84
|
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
83
85
|
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
84
86
|
});
|
|
85
87
|
}
|
|
86
|
-
}, [
|
|
87
|
-
|
|
88
|
-
const [root, setRoot] = useState<HTMLDivElement | null>(null);
|
|
89
|
-
const [state, setState] = useState<EditorState>();
|
|
90
|
-
const [view, setView] = useState<EditorView>();
|
|
91
|
-
useImperativeHandle(forwardedRef, () => ({ root, state, view }), [view, state, root]);
|
|
88
|
+
}, [awareness, peer, themeMode]);
|
|
92
89
|
|
|
93
90
|
useEffect(() => {
|
|
94
91
|
if (!root) {
|
|
@@ -108,7 +105,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
108
105
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
109
106
|
|
|
110
107
|
// Storage and replication.
|
|
111
|
-
|
|
108
|
+
model.extension,
|
|
112
109
|
|
|
113
110
|
// Custom.
|
|
114
111
|
...extensions,
|
|
@@ -127,7 +124,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
127
124
|
setView(undefined);
|
|
128
125
|
setState(undefined);
|
|
129
126
|
};
|
|
130
|
-
}, [root, model
|
|
127
|
+
}, [root, model, themeMode, editorMode]);
|
|
131
128
|
|
|
132
129
|
const handleKeyUp = useCallback(
|
|
133
130
|
(event: KeyboardEvent) => {
|
|
@@ -160,7 +157,17 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
160
157
|
},
|
|
161
158
|
);
|
|
162
159
|
|
|
163
|
-
// TODO(burdon):
|
|
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
|
+
|
|
164
171
|
export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
165
172
|
({ extensions = [], slots: _slots, ...props }, forwardedRef) => {
|
|
166
173
|
const { themeMode } = useThemeContext();
|
|
@@ -168,7 +175,7 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
168
175
|
return (
|
|
169
176
|
<BaseTextEditor
|
|
170
177
|
ref={forwardedRef}
|
|
171
|
-
extensions={[basicBundle({ themeMode, placeholder: slots?.editor?.placeholder }), ...extensions]}
|
|
178
|
+
extensions={[basicBundle({ themeMode, placeholder: slots?.editor?.placeholder }), maybeDebug(), ...extensions]}
|
|
172
179
|
slots={slots}
|
|
173
180
|
{...props}
|
|
174
181
|
/>
|
|
@@ -179,11 +186,15 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
179
186
|
export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
180
187
|
({ extensions = [], slots: _slots, ...props }, forwardedRef) => {
|
|
181
188
|
const { themeMode } = useThemeContext();
|
|
182
|
-
const slots = defaultsDeep({}, _slots,
|
|
189
|
+
const slots = defaultsDeep({}, _slots, defaultMarkdownSlots);
|
|
183
190
|
return (
|
|
184
191
|
<BaseTextEditor
|
|
185
192
|
ref={forwardedRef}
|
|
186
|
-
extensions={[
|
|
193
|
+
extensions={[
|
|
194
|
+
markdownBundle({ themeMode, placeholder: slots?.editor?.placeholder }),
|
|
195
|
+
maybeDebug(),
|
|
196
|
+
...extensions,
|
|
197
|
+
]}
|
|
187
198
|
slots={slots}
|
|
188
199
|
{...props}
|
|
189
200
|
/>
|
|
@@ -203,3 +214,10 @@ export const defaultTextSlots: TextEditorSlots = {
|
|
|
203
214
|
theme: textTheme,
|
|
204
215
|
},
|
|
205
216
|
};
|
|
217
|
+
|
|
218
|
+
export const defaultMarkdownSlots: TextEditorSlots = {
|
|
219
|
+
...defaultSlots,
|
|
220
|
+
editor: {
|
|
221
|
+
theme: markdownTheme,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
@@ -23,14 +23,16 @@ import {
|
|
|
23
23
|
} from '@codemirror/autocomplete';
|
|
24
24
|
import { keymap } from '@codemirror/view';
|
|
25
25
|
|
|
26
|
+
export type AutocompleteResult = Completion;
|
|
27
|
+
|
|
26
28
|
export type AutocompleteOptions = {
|
|
27
|
-
|
|
29
|
+
onSearch: (text: string) => Completion[];
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
33
|
* Autocomplete extension.
|
|
32
34
|
*/
|
|
33
|
-
export const autocomplete = ({
|
|
35
|
+
export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
|
|
34
36
|
return [
|
|
35
37
|
// https://codemirror.net/docs/ref/#view.keymap
|
|
36
38
|
// https://discuss.codemirror.net/t/how-can-i-replace-the-default-autocompletion-keymap-v6/3322
|
|
@@ -57,7 +59,7 @@ export const autocomplete = ({ getOptions }: AutocompleteOptions) => {
|
|
|
57
59
|
// TODO(burdon): Option to convert to links?
|
|
58
60
|
return {
|
|
59
61
|
from: word.from,
|
|
60
|
-
options:
|
|
62
|
+
options: onSearch(word.text.toLowerCase()),
|
|
61
63
|
// options: [
|
|
62
64
|
// { label: 'apple', type: 'keyword' },
|
|
63
65
|
// { label: 'amazon', type: 'keyword' },
|
|
@@ -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
|
+
};
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
// TODO(burdon): Add-on: dialog.
|
|
20
20
|
// TODO(burdon): Comments: https://codemirror.net/5/doc/manual.html#setBookmark
|
|
21
21
|
// TODO(burdon): Split view: https://codemirror.net/examples/split
|
|
22
|
+
// TODO(burdon): https://codemirror.net/5/demo/simplemode.html
|
|
22
23
|
|
|
23
24
|
// https://codemirror.net/examples/autocompletion
|
|
24
25
|
// https://codemirror.net/docs/ref/#autocomplete.autocompletion
|
|
@@ -77,6 +77,11 @@ class LinkText extends WidgetType {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Range sets provide a data structure that can hold a collection of tagged,
|
|
82
|
+
* possibly overlapping ranges in such a way that they can efficiently be mapped though document changes.
|
|
83
|
+
* https://codemirror.net/docs/ref/#state
|
|
84
|
+
*/
|
|
80
85
|
const update = (state: EditorState, options: LinkOptions) => {
|
|
81
86
|
const builder = new RangeSetBuilder();
|
|
82
87
|
const cursor = state.selection.main.head;
|
|
@@ -89,6 +94,9 @@ const update = (state: EditorState, options: LinkOptions) => {
|
|
|
89
94
|
|
|
90
95
|
const urlNode = node.node.getChild('URL');
|
|
91
96
|
const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
|
|
97
|
+
if (!url) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
92
100
|
|
|
93
101
|
if (cursor < node.from || cursor > node.to) {
|
|
94
102
|
builder.add(
|
|
@@ -109,9 +117,9 @@ const update = (state: EditorState, options: LinkOptions) => {
|
|
|
109
117
|
}),
|
|
110
118
|
);
|
|
111
119
|
}
|
|
112
|
-
}
|
|
113
120
|
|
|
114
|
-
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
115
123
|
},
|
|
116
124
|
});
|
|
117
125
|
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import { StateField } from '@codemirror/state';
|
|
6
6
|
|
|
7
|
-
export type
|
|
7
|
+
export type ListenerOptions = { onChange: (text: string) => void };
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Based on https://github.com/codemirror/dev/issues/44#issuecomment-789093799
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
// TODO(burdon): Expose options.
|
|
13
|
+
export const listener = ({ onChange }: ListenerOptions) =>
|
|
13
14
|
StateField.define({
|
|
14
15
|
create: () => null,
|
|
15
16
|
update: (_value, transaction) => {
|
|
@@ -3,17 +3,10 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
|
6
|
-
import { defaultKeymap, history,
|
|
6
|
+
import { defaultKeymap, history, indentWithTab } from '@codemirror/commands';
|
|
7
7
|
import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
|
|
8
|
-
import {
|
|
9
|
-
bracketMatching,
|
|
10
|
-
defaultHighlightStyle,
|
|
11
|
-
foldKeymap,
|
|
12
|
-
indentOnInput,
|
|
13
|
-
syntaxHighlighting,
|
|
14
|
-
} from '@codemirror/language';
|
|
8
|
+
import { bracketMatching, defaultHighlightStyle, indentOnInput, syntaxHighlighting } from '@codemirror/language';
|
|
15
9
|
import { languages } from '@codemirror/language-data';
|
|
16
|
-
import { lintKeymap } from '@codemirror/lint';
|
|
17
10
|
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
|
|
18
11
|
import { EditorState, type Extension } from '@codemirror/state';
|
|
19
12
|
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
@@ -50,7 +43,6 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
|
|
|
50
43
|
EditorState.allowMultipleSelections.of(true),
|
|
51
44
|
EditorView.lineWrapping,
|
|
52
45
|
|
|
53
|
-
// autocompletion(),
|
|
54
46
|
crosshairCursor(),
|
|
55
47
|
dropCursor(),
|
|
56
48
|
drawSelection(),
|
|
@@ -62,13 +54,13 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
|
|
|
62
54
|
indentOnInput(),
|
|
63
55
|
rectangularSelection(),
|
|
64
56
|
|
|
57
|
+
// TODO(burdon): Review.
|
|
65
58
|
keymap.of([
|
|
66
59
|
...closeBracketsKeymap,
|
|
67
|
-
// ...completionKeymap,
|
|
68
60
|
...defaultKeymap,
|
|
69
|
-
...foldKeymap,
|
|
70
|
-
...historyKeymap,
|
|
71
|
-
...lintKeymap,
|
|
61
|
+
// ...foldKeymap,
|
|
62
|
+
// ...historyKeymap,
|
|
63
|
+
// ...lintKeymap,
|
|
72
64
|
...searchKeymap,
|
|
73
65
|
indentWithTab,
|
|
74
66
|
]),
|
|
@@ -134,10 +134,11 @@ export const markdownHighlightStyle = HighlightStyle.define(
|
|
|
134
134
|
class: codeMark,
|
|
135
135
|
},
|
|
136
136
|
|
|
137
|
-
// The `markdown` extension configures extensions for `lezer` to parse markdown tokens (incl. below).
|
|
137
|
+
// NOTE: The `markdown` extension configures extensions for `lezer` to parse markdown tokens (incl. below).
|
|
138
138
|
// However, since `codeLanguages` is also defined, the `lezer` will not parse fenced code blocks,
|
|
139
139
|
// when a language is specified. In this case, the syntax highlighting extensions will colorize
|
|
140
140
|
// the code, but all other CSS properties will be inherited.
|
|
141
|
+
// IMPORTANT: Therefore, the fenced code block will use the base editor font.
|
|
141
142
|
{
|
|
142
143
|
tag: [markdownTags.CodeText, markdownTags.InlineCode],
|
|
143
144
|
class: code,
|