@dxos/ui-editor 0.0.0
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/LICENSE +8 -0
- package/README.md +21 -0
- package/package.json +121 -0
- package/src/defaults.ts +34 -0
- package/src/extensions/annotations.ts +55 -0
- package/src/extensions/autocomplete/autocomplete.ts +151 -0
- package/src/extensions/autocomplete/index.ts +8 -0
- package/src/extensions/autocomplete/match.ts +46 -0
- package/src/extensions/autocomplete/placeholder.ts +117 -0
- package/src/extensions/autocomplete/typeahead.ts +87 -0
- package/src/extensions/automerge/automerge.test.tsx +76 -0
- package/src/extensions/automerge/automerge.ts +105 -0
- package/src/extensions/automerge/cursor.ts +28 -0
- package/src/extensions/automerge/defs.ts +31 -0
- package/src/extensions/automerge/index.ts +5 -0
- package/src/extensions/automerge/sync.ts +79 -0
- package/src/extensions/automerge/update-automerge.ts +50 -0
- package/src/extensions/automerge/update-codemirror.ts +115 -0
- package/src/extensions/autoscroll.ts +165 -0
- package/src/extensions/awareness/awareness-provider.ts +127 -0
- package/src/extensions/awareness/awareness.ts +315 -0
- package/src/extensions/awareness/index.ts +6 -0
- package/src/extensions/blast.ts +363 -0
- package/src/extensions/blocks.ts +131 -0
- package/src/extensions/bookmarks.ts +77 -0
- package/src/extensions/comments.ts +579 -0
- package/src/extensions/debug.ts +15 -0
- package/src/extensions/dnd.ts +39 -0
- package/src/extensions/factories.ts +284 -0
- package/src/extensions/focus.ts +36 -0
- package/src/extensions/folding.ts +63 -0
- package/src/extensions/hashtag.ts +68 -0
- package/src/extensions/index.ts +34 -0
- package/src/extensions/json.ts +57 -0
- package/src/extensions/listener.ts +32 -0
- package/src/extensions/markdown/action.ts +117 -0
- package/src/extensions/markdown/bundle.ts +105 -0
- package/src/extensions/markdown/changes.test.ts +26 -0
- package/src/extensions/markdown/changes.ts +149 -0
- package/src/extensions/markdown/debug.ts +44 -0
- package/src/extensions/markdown/decorate.ts +622 -0
- package/src/extensions/markdown/formatting.test.ts +498 -0
- package/src/extensions/markdown/formatting.ts +1265 -0
- package/src/extensions/markdown/highlight.ts +183 -0
- package/src/extensions/markdown/image.ts +118 -0
- package/src/extensions/markdown/index.ts +13 -0
- package/src/extensions/markdown/link.ts +50 -0
- package/src/extensions/markdown/parser.test.ts +75 -0
- package/src/extensions/markdown/styles.ts +135 -0
- package/src/extensions/markdown/table.ts +150 -0
- package/src/extensions/mention.ts +41 -0
- package/src/extensions/modal.ts +24 -0
- package/src/extensions/modes.ts +41 -0
- package/src/extensions/outliner/commands.ts +270 -0
- package/src/extensions/outliner/editor.test.ts +33 -0
- package/src/extensions/outliner/editor.ts +184 -0
- package/src/extensions/outliner/index.ts +7 -0
- package/src/extensions/outliner/menu.ts +128 -0
- package/src/extensions/outliner/outliner.test.ts +100 -0
- package/src/extensions/outliner/outliner.ts +167 -0
- package/src/extensions/outliner/selection.ts +50 -0
- package/src/extensions/outliner/tree.test.ts +168 -0
- package/src/extensions/outliner/tree.ts +317 -0
- package/src/extensions/preview/index.ts +5 -0
- package/src/extensions/preview/preview.ts +193 -0
- package/src/extensions/replacer.test.ts +75 -0
- package/src/extensions/replacer.ts +93 -0
- package/src/extensions/scrolling.ts +189 -0
- package/src/extensions/selection.ts +100 -0
- package/src/extensions/state.ts +7 -0
- package/src/extensions/submit.ts +62 -0
- package/src/extensions/tags/extended-markdown.test.ts +263 -0
- package/src/extensions/tags/extended-markdown.ts +78 -0
- package/src/extensions/tags/index.ts +7 -0
- package/src/extensions/tags/streamer.ts +243 -0
- package/src/extensions/tags/xml-tags.ts +507 -0
- package/src/extensions/tags/xml-util.test.ts +48 -0
- package/src/extensions/tags/xml-util.ts +93 -0
- package/src/extensions/typewriter.ts +68 -0
- package/src/index.ts +14 -0
- package/src/styles/index.ts +7 -0
- package/src/styles/markdown.ts +26 -0
- package/src/styles/theme.ts +293 -0
- package/src/styles/tokens.ts +17 -0
- package/src/types/index.ts +5 -0
- package/src/types/types.ts +32 -0
- package/src/util/cursor.ts +56 -0
- package/src/util/debug.ts +56 -0
- package/src/util/decorations.ts +21 -0
- package/src/util/dom.ts +36 -0
- package/src/util/facet.ts +13 -0
- package/src/util/index.ts +10 -0
- package/src/util/util.ts +29 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2022 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
export { type Extension, EditorState } from '@codemirror/state';
|
|
6
|
+
export { EditorView, keymap } from '@codemirror/view';
|
|
7
|
+
export { tags } from '@lezer/highlight';
|
|
8
|
+
|
|
9
|
+
export { TextKind } from '@dxos/protocols/proto/dxos/echo/model/text';
|
|
10
|
+
|
|
11
|
+
export * from './defaults';
|
|
12
|
+
export * from './extensions';
|
|
13
|
+
export * from './types';
|
|
14
|
+
export * from './util';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { mx } from '@dxos/ui-theme';
|
|
6
|
+
|
|
7
|
+
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
8
|
+
|
|
9
|
+
// https://tailwindcss.com/docs/font-weight
|
|
10
|
+
const headings: Record<HeadingLevel, string> = {
|
|
11
|
+
1: 'text-4xl',
|
|
12
|
+
2: 'text-3xl',
|
|
13
|
+
3: 'text-2xl',
|
|
14
|
+
4: 'text-xl',
|
|
15
|
+
5: 'text-lg',
|
|
16
|
+
6: '', // TODO(burdon): Should be text-base, but that's a color in our system.
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const markdownTheme = {
|
|
20
|
+
code: 'font-mono !no-underline text-neutral-700 dark:text-neutral-300',
|
|
21
|
+
codeMark: 'font-mono text-primary-500',
|
|
22
|
+
mark: 'opacity-50',
|
|
23
|
+
heading: (level: HeadingLevel) => {
|
|
24
|
+
return mx(headings[level], 'dark:text-neutral-400');
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import { EditorView } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
import { fontBody, fontMono } from './tokens';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Global base theme.
|
|
12
|
+
*
|
|
13
|
+
* NOTE: The base theme is GLOBAL and is applied to ALL editors.
|
|
14
|
+
* NOTE: `light` and `dark` selectors are preprocessed by CodeMirror and can only be in the base theme.
|
|
15
|
+
* NOTE: Use 'unset' to remove default CM style.
|
|
16
|
+
*
|
|
17
|
+
* Examples:
|
|
18
|
+
* - https://codemirror.net/examples/styling
|
|
19
|
+
* - https://github.com/codemirror/view/blob/main/src/theme.ts
|
|
20
|
+
* - https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts
|
|
21
|
+
*
|
|
22
|
+
* Main layout:
|
|
23
|
+
* - https://codemirror.net/examples/styling
|
|
24
|
+
* - https://codemirror.net/docs/guide (DOM Structure).
|
|
25
|
+
*
|
|
26
|
+
* <div class="cm-editor [cm-focused] [generated classes]">
|
|
27
|
+
* <div class="cm-scroller">
|
|
28
|
+
* <div class="cm-gutters">
|
|
29
|
+
* <div class="cm-gutter [...]">
|
|
30
|
+
* <div class="cm-gutterElement">...</div>
|
|
31
|
+
* </div>
|
|
32
|
+
* </div>
|
|
33
|
+
* <div class="cm-content" role="textbox" contenteditable="true">
|
|
34
|
+
* <div class="cm-line"></div>
|
|
35
|
+
* </div>
|
|
36
|
+
* <div class="cm-selectionLayer">
|
|
37
|
+
* <div class="cm-selectionBackground"></div>
|
|
38
|
+
* </div>
|
|
39
|
+
* <div class="cm-cursorLayer">
|
|
40
|
+
* <div class="cm-cursor"></div>
|
|
41
|
+
* </div>
|
|
42
|
+
* </div>
|
|
43
|
+
* </div>
|
|
44
|
+
*/
|
|
45
|
+
export const baseTheme = EditorView.baseTheme({
|
|
46
|
+
'&': {},
|
|
47
|
+
'&.cm-focused': {
|
|
48
|
+
outline: 'none',
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Scroller
|
|
53
|
+
*/
|
|
54
|
+
'.cm-scroller': {
|
|
55
|
+
overflowY: 'auto',
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Content
|
|
60
|
+
* NOTE: Apply margins to content so that scrollbar is at the edge of the container.
|
|
61
|
+
*/
|
|
62
|
+
'.cm-content': {
|
|
63
|
+
padding: 'unset',
|
|
64
|
+
lineHeight: '24px',
|
|
65
|
+
color: 'unset',
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Gutters
|
|
70
|
+
* NOTE: Gutters should have the same top margin as the content.
|
|
71
|
+
*/
|
|
72
|
+
'.cm-gutters': {
|
|
73
|
+
background: 'transparent',
|
|
74
|
+
borderRight: 'none',
|
|
75
|
+
},
|
|
76
|
+
'.cm-gutter': {},
|
|
77
|
+
'.cm-gutter.cm-lineNumbers': {
|
|
78
|
+
paddingRight: '4px',
|
|
79
|
+
borderRight: '1px solid var(--dx-subduedSeparator)',
|
|
80
|
+
color: 'var(--dx-subduedText)',
|
|
81
|
+
},
|
|
82
|
+
'.cm-gutter.cm-lineNumbers .cm-gutterElement': {
|
|
83
|
+
minWidth: '40px',
|
|
84
|
+
},
|
|
85
|
+
/**
|
|
86
|
+
* Height is set to match the corresponding line (which may have wrapped).
|
|
87
|
+
*/
|
|
88
|
+
'.cm-gutterElement': {
|
|
89
|
+
lineHeight: '24px',
|
|
90
|
+
fontSize: '12px',
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Line.
|
|
95
|
+
*/
|
|
96
|
+
'.cm-line': {
|
|
97
|
+
lineHeight: '24px',
|
|
98
|
+
paddingInline: 0,
|
|
99
|
+
},
|
|
100
|
+
'.cm-activeLine': {
|
|
101
|
+
background: 'var(--dx-cmActiveLine)',
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Cursor (layer).
|
|
106
|
+
*/
|
|
107
|
+
'.cm-cursor, .cm-dropCursor': {
|
|
108
|
+
borderLeft: '2px solid var(--dx-cmCursor)',
|
|
109
|
+
},
|
|
110
|
+
'.cm-placeholder': {
|
|
111
|
+
color: 'var(--dx-placeholder)',
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Selection (layer).
|
|
116
|
+
*/
|
|
117
|
+
'.cm-selectionBackground': {
|
|
118
|
+
background: 'var(--dx-cmSelection)',
|
|
119
|
+
},
|
|
120
|
+
'&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
|
|
121
|
+
background: 'var(--dx-cmFocusedSelection)',
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Search.
|
|
126
|
+
* NOTE: Matches comment.
|
|
127
|
+
*/
|
|
128
|
+
'.cm-searchMatch': {
|
|
129
|
+
margin: '0 -3px',
|
|
130
|
+
padding: '3px',
|
|
131
|
+
borderRadius: '3px',
|
|
132
|
+
background: 'var(--dx-cmHighlightSurface)',
|
|
133
|
+
color: 'var(--dx-cmHighlight)',
|
|
134
|
+
},
|
|
135
|
+
'.cm-searchMatch-selected': {
|
|
136
|
+
textDecoration: 'underline',
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Link.
|
|
141
|
+
*/
|
|
142
|
+
'.cm-link': {
|
|
143
|
+
textDecorationLine: 'underline',
|
|
144
|
+
textDecorationThickness: '1px',
|
|
145
|
+
textDecorationColor: 'var(--dx-separator)',
|
|
146
|
+
textUnderlineOffset: '2px',
|
|
147
|
+
borderRadius: '.125rem',
|
|
148
|
+
},
|
|
149
|
+
'.cm-link > span': {
|
|
150
|
+
color: 'var(--dx-accentText)',
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Tooltip.
|
|
155
|
+
*/
|
|
156
|
+
'.cm-tooltip': {
|
|
157
|
+
background: 'var(--dx-baseSurface)',
|
|
158
|
+
},
|
|
159
|
+
'.cm-tooltip-below': {},
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Autocomplete.
|
|
163
|
+
* https://github.com/codemirror/autocomplete/blob/main/src/completion.ts
|
|
164
|
+
*/
|
|
165
|
+
'.cm-tooltip.cm-tooltip-autocomplete': {
|
|
166
|
+
marginTop: '6px',
|
|
167
|
+
marginLeft: '-10px',
|
|
168
|
+
border: '2px solid var(--dx-separator)',
|
|
169
|
+
borderRadius: '4px',
|
|
170
|
+
},
|
|
171
|
+
'.cm-tooltip.cm-tooltip-autocomplete > ul': {
|
|
172
|
+
maxHeight: '20em',
|
|
173
|
+
},
|
|
174
|
+
'.cm-tooltip.cm-tooltip-autocomplete > ul > li': {
|
|
175
|
+
padding: '4px',
|
|
176
|
+
},
|
|
177
|
+
'.cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected]': {
|
|
178
|
+
background: 'var(--dx-activeSurface)',
|
|
179
|
+
color: 'var(--dx-activeSurfaceText)',
|
|
180
|
+
},
|
|
181
|
+
'.cm-tooltip.cm-tooltip-autocomplete > ul > completion-section': {
|
|
182
|
+
paddingLeft: '4px !important',
|
|
183
|
+
color: 'var(--dx-hoverSurfaceText)',
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Completion info.
|
|
188
|
+
*/
|
|
189
|
+
'.cm-completionInfo': {
|
|
190
|
+
width: '360px !important',
|
|
191
|
+
margin: '-10px 1px 0 1px',
|
|
192
|
+
padding: '8px !important',
|
|
193
|
+
borderColor: 'var(--dx-separator)',
|
|
194
|
+
},
|
|
195
|
+
'.cm-completionIcon': {
|
|
196
|
+
display: 'none',
|
|
197
|
+
},
|
|
198
|
+
'.cm-completionLabel': {
|
|
199
|
+
color: 'var(--dx-description)',
|
|
200
|
+
padding: '0 4px',
|
|
201
|
+
},
|
|
202
|
+
'.cm-completionMatchedText': {
|
|
203
|
+
color: 'var(--dx-baseText)',
|
|
204
|
+
textDecoration: 'none !important',
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Panels
|
|
209
|
+
* https://github.com/codemirror/search/blob/main/src/search.ts#L745
|
|
210
|
+
*
|
|
211
|
+
* Find/replace panel.
|
|
212
|
+
* <div class="cm-announced">...</div>
|
|
213
|
+
* <div class="cm-scroller">...</div>
|
|
214
|
+
* <div class="cm-panels cm-panels-bottom">
|
|
215
|
+
* <div class="cm-search cm-panel">
|
|
216
|
+
* <input class="cm-textfield" />
|
|
217
|
+
* <button class="cm-button">...</button>
|
|
218
|
+
* <label><input type="checkbox" />...</label>
|
|
219
|
+
* </div>
|
|
220
|
+
* </div
|
|
221
|
+
*/
|
|
222
|
+
// TODO(burdon): Implement custom panel (with icon buttons).
|
|
223
|
+
'.cm-panels': {},
|
|
224
|
+
'.cm-panel': {
|
|
225
|
+
backgroundColor: 'var(--surface-bg)',
|
|
226
|
+
},
|
|
227
|
+
'.cm-panel input, .cm-panel button, .cm-panel label': {
|
|
228
|
+
color: 'var(--dx-subdued)',
|
|
229
|
+
fontSize: '14px',
|
|
230
|
+
all: 'unset',
|
|
231
|
+
margin: '3px !important',
|
|
232
|
+
padding: '2px 6px !important',
|
|
233
|
+
outline: '1px solid transparent',
|
|
234
|
+
},
|
|
235
|
+
'.cm-panel input, .cm-panel button': {
|
|
236
|
+
backgroundColor: 'var(--dx-inputSurface)',
|
|
237
|
+
},
|
|
238
|
+
'.cm-panel input:focus, .cm-panel button:focus': {
|
|
239
|
+
outline: '1px solid var(--dx-neutralFocusIndicator)',
|
|
240
|
+
},
|
|
241
|
+
'.cm-panel label': {
|
|
242
|
+
display: 'inline-flex',
|
|
243
|
+
alignItems: 'center',
|
|
244
|
+
cursor: 'pointer',
|
|
245
|
+
},
|
|
246
|
+
'.cm-panel input.cm-textfield': {},
|
|
247
|
+
'.cm-panel input[type=checkbox]': {
|
|
248
|
+
width: '8px',
|
|
249
|
+
height: '8px',
|
|
250
|
+
marginRight: '6px !important',
|
|
251
|
+
padding: '2px !important',
|
|
252
|
+
color: 'var(--dx-neutralFocusIndicator)',
|
|
253
|
+
},
|
|
254
|
+
'.cm-panel button': {
|
|
255
|
+
'&:hover': {
|
|
256
|
+
backgroundColor: 'var(--dx-accentSurfaceHover) !important',
|
|
257
|
+
},
|
|
258
|
+
'&:active': {
|
|
259
|
+
backgroundColor: 'var(--dx-accentSurfaceHover)',
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
'.cm-panel.cm-search': {
|
|
263
|
+
padding: '4px',
|
|
264
|
+
borderTop: '1px solid var(--dx-separator)',
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
export const editorGutter: Extension = EditorView.theme({
|
|
269
|
+
'.cm-gutters': {
|
|
270
|
+
// NOTE: Non-transparent background required to cover content if scrolling horizontally.
|
|
271
|
+
background: 'var(--dx-baseSurface) !important',
|
|
272
|
+
paddingRight: '1rem',
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
export type FontOptions = {
|
|
277
|
+
monospace?: boolean;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export const createFontTheme = ({ monospace }: FontOptions = {}) =>
|
|
281
|
+
EditorView.theme({
|
|
282
|
+
// Set metrics on the scroller (this is often what CM uses for layout).
|
|
283
|
+
'.cm-scroller': {
|
|
284
|
+
fontFamily: monospace ? fontMono : fontBody,
|
|
285
|
+
fontSize: '16px',
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// Maintain defaults for UI components.
|
|
289
|
+
'.cm-content, .cm-gutters, .cm-panel': {
|
|
290
|
+
fontFamily: 'inherit',
|
|
291
|
+
fontSize: 'inherit',
|
|
292
|
+
},
|
|
293
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { tokens } from '@dxos/ui-theme';
|
|
6
|
+
import { get } from '@dxos/util';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns the tailwind token value.
|
|
10
|
+
*/
|
|
11
|
+
const getToken = (path: string, defaultValue?: string | string[]): string => {
|
|
12
|
+
const value = get(tokens, path, defaultValue);
|
|
13
|
+
return value?.toString() ?? '';
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const fontBody = getToken('fontFamily.body');
|
|
17
|
+
export const fontMono = getToken('fontFamily.mono');
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type EditorView } from '@codemirror/view';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
7
|
+
|
|
8
|
+
// Runtime data structure.
|
|
9
|
+
export type Range = {
|
|
10
|
+
from: number;
|
|
11
|
+
to: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Persistent data structure.
|
|
15
|
+
// TODO(burdon): Rename annotation?
|
|
16
|
+
export type Comment = {
|
|
17
|
+
id: string;
|
|
18
|
+
cursor?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Callback that renders into a DOM element within the editor.
|
|
23
|
+
*/
|
|
24
|
+
export type RenderCallback<Props extends object> = (el: HTMLElement, props: Props, view: EditorView) => void;
|
|
25
|
+
|
|
26
|
+
export const EditorViewModes = ['preview', 'readonly', 'source'] as const;
|
|
27
|
+
export const EditorViewMode = Schema.Union(...EditorViewModes.map((mode) => Schema.Literal(mode)));
|
|
28
|
+
export type EditorViewMode = Schema.Schema.Type<typeof EditorViewMode>;
|
|
29
|
+
|
|
30
|
+
export const EditorInputModes = ['default', 'vim', 'vscode'] as const;
|
|
31
|
+
export const EditorInputMode = Schema.Union(...EditorInputModes.map((mode) => Schema.Literal(mode)));
|
|
32
|
+
export type EditorInputMode = Schema.Schema.Type<typeof EditorInputMode>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type EditorState } from '@codemirror/state';
|
|
6
|
+
|
|
7
|
+
import { type Range } from '../types';
|
|
8
|
+
|
|
9
|
+
import { singleValueFacet } from './facet';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Determines if two ranges overlap.
|
|
13
|
+
* A range is considered to overlap if there is any intersection
|
|
14
|
+
* between the two ranges, inclusive of their boundaries.
|
|
15
|
+
*/
|
|
16
|
+
export const overlap = (a: Range, b: Range): boolean => a.from <= b.to && a.to >= b.from;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Converts indexes into the text document into stable peer-independent cursors.
|
|
20
|
+
*
|
|
21
|
+
* See:
|
|
22
|
+
* - https://automerge.org/automerge/api-docs/js/functions/next.getCursor.html
|
|
23
|
+
* - https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
|
|
24
|
+
*
|
|
25
|
+
* @param {assoc} number Negative values will associate the cursor with the previous character
|
|
26
|
+
* while positive - with the next one.
|
|
27
|
+
*/
|
|
28
|
+
export interface CursorConverter {
|
|
29
|
+
toCursor(position: number, assoc?: -1 | 1 | undefined): string;
|
|
30
|
+
fromCursor(cursor: string): number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const defaultCursorConverter: CursorConverter = {
|
|
34
|
+
toCursor: (position) => position.toString(),
|
|
35
|
+
fromCursor: (cursor) => parseInt(cursor),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export class Cursor {
|
|
39
|
+
static readonly converter = singleValueFacet(defaultCursorConverter);
|
|
40
|
+
|
|
41
|
+
static readonly getCursorFromRange = (state: EditorState, range: Range) => {
|
|
42
|
+
const cursorConverter = state.facet(Cursor.converter);
|
|
43
|
+
const from = cursorConverter.toCursor(range.from);
|
|
44
|
+
const to = cursorConverter.toCursor(range.to, -1);
|
|
45
|
+
return [from, to].join(':');
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
static readonly getRangeFromCursor = (state: EditorState, cursor: string) => {
|
|
49
|
+
const cursorConverter = state.facet(Cursor.converter);
|
|
50
|
+
|
|
51
|
+
const parts = cursor.split(':');
|
|
52
|
+
const from = cursorConverter.fromCursor(parts[0]);
|
|
53
|
+
const to = cursorConverter.fromCursor(parts[1]);
|
|
54
|
+
return from !== undefined && to !== undefined ? { from, to } : undefined;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Transaction } from '@codemirror/state';
|
|
6
|
+
import { type EditorView } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
import { log } from '@dxos/log';
|
|
9
|
+
|
|
10
|
+
export const join = (...lines: string[]) => lines.join('\n');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* CodeMirror callbacks swallow errors so wrap handlers.
|
|
14
|
+
*/
|
|
15
|
+
export const wrapWithCatch = (fn: (...args: any[]) => any, label?: string) => {
|
|
16
|
+
return (...args: any[]) => {
|
|
17
|
+
try {
|
|
18
|
+
return fn(...args);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
log.catch(err, { label });
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Log all changes before dispatching them to the view.
|
|
27
|
+
* https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
28
|
+
*/
|
|
29
|
+
export const debugDispatcher = (trs: readonly Transaction[], view: EditorView) => {
|
|
30
|
+
logChanges(trs);
|
|
31
|
+
view.update(trs);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Util to log transactions in update listener.
|
|
36
|
+
*/
|
|
37
|
+
export const logChanges = (trs: readonly Transaction[]) => {
|
|
38
|
+
const changes = trs
|
|
39
|
+
.flatMap((tr) => {
|
|
40
|
+
if (tr.changes.empty) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const changes: any[] = [];
|
|
45
|
+
tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) =>
|
|
46
|
+
changes.push(JSON.stringify({ fromA, toA, fromB, toB, inserted: inserted.toString() })),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return changes;
|
|
50
|
+
})
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
|
|
53
|
+
if (changes.length) {
|
|
54
|
+
log('changes', { changes });
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Range } from '@codemirror/state';
|
|
6
|
+
import { type Decoration, type DecorationSet } from '@codemirror/view';
|
|
7
|
+
|
|
8
|
+
export const decorationSetToArray = (deco: DecorationSet): readonly Range<Decoration>[] => {
|
|
9
|
+
const ranges: Range<Decoration>[] = [];
|
|
10
|
+
const iter = deco.iter();
|
|
11
|
+
while (iter.value) {
|
|
12
|
+
ranges.push({
|
|
13
|
+
from: iter.from,
|
|
14
|
+
to: iter.to,
|
|
15
|
+
value: iter.value,
|
|
16
|
+
});
|
|
17
|
+
iter.next();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return ranges;
|
|
21
|
+
};
|
package/src/util/dom.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// TODO(burdon): Factor out to @dxos/ui
|
|
6
|
+
|
|
7
|
+
export type Rect = {
|
|
8
|
+
readonly left: number;
|
|
9
|
+
readonly right: number;
|
|
10
|
+
readonly top: number;
|
|
11
|
+
readonly bottom: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const flattenRect = (rect: Rect, left: boolean): Rect => {
|
|
15
|
+
const x = left ? rect.left : rect.right;
|
|
16
|
+
return { left: x, right: x, top: rect.top, bottom: rect.bottom };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let scratchRange: Range | null;
|
|
20
|
+
|
|
21
|
+
export const textRange = (node: Text, from: number, to = from): Range => {
|
|
22
|
+
const range = scratchRange || (scratchRange = document.createRange());
|
|
23
|
+
range.setEnd(node, to);
|
|
24
|
+
range.setStart(node, from);
|
|
25
|
+
return range;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const clientRectsFor = (dom: Node): DOMRectList => {
|
|
29
|
+
if (dom.nodeType === 3) {
|
|
30
|
+
return textRange(dom as Text, 0, dom.nodeValue!.length).getClientRects();
|
|
31
|
+
} else if (dom.nodeType === 1) {
|
|
32
|
+
return (dom as HTMLElement).getClientRects();
|
|
33
|
+
} else {
|
|
34
|
+
return [] as any as DOMRectList;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Facet } from '@codemirror/state';
|
|
6
|
+
|
|
7
|
+
export const singleValueFacet = <I, O = I>(defaultValue?: O) =>
|
|
8
|
+
Facet.define<I, O>({
|
|
9
|
+
// Called immediately.
|
|
10
|
+
combine: (providers) => {
|
|
11
|
+
return (providers[0] ?? defaultValue) as O;
|
|
12
|
+
},
|
|
13
|
+
});
|
package/src/util/util.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type EditorView } from '@codemirror/view';
|
|
6
|
+
|
|
7
|
+
export const insertAtCursor = (view: EditorView, from: number, insert: string) => {
|
|
8
|
+
view.dispatch({
|
|
9
|
+
changes: { from, to: from, insert },
|
|
10
|
+
selection: { anchor: from + insert.length, head: from + insert.length },
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* If the cursor is at the start of a line, insert the text at the cursor.
|
|
16
|
+
* Otherwise, insert the text on a new line.
|
|
17
|
+
*/
|
|
18
|
+
export const insertAtLineStart = (view: EditorView, from: number, insert: string) => {
|
|
19
|
+
const line = view.state.doc.lineAt(from);
|
|
20
|
+
if (line.from === from) {
|
|
21
|
+
insertAtCursor(view, from, insert);
|
|
22
|
+
} else {
|
|
23
|
+
insert = '\n' + insert;
|
|
24
|
+
view.dispatch({
|
|
25
|
+
changes: { from: line.to, to: line.to, insert },
|
|
26
|
+
selection: { anchor: line.to + insert.length, head: line.to + insert.length },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|