@dxos/react-ui-editor 0.3.11-main.8d54a30 → 0.3.11-main.8feb6a5
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 +511 -569
- 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 +9 -5
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +12 -17
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -6
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts +1 -1
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/index.d.ts +1 -0
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
- package/dist/types/src/extensions/code.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +6 -2
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/{util → extensions}/cursor.d.ts +9 -1
- package/dist/types/src/extensions/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/hr.d.ts.map +1 -1
- package/dist/types/src/extensions/image.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +1 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/link.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/tasklist.d.ts +5 -1
- package/dist/types/src/extensions/tasklist.d.ts.map +1 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +1 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +0 -5
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/hooks/yjs.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/styles/tokens.d.ts +1 -0
- package/dist/types/src/styles/tokens.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +24 -23
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +25 -23
- package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
- package/src/components/TextEditor/TextEditor.tsx +68 -64
- package/src/components/TextEditor/comments.stories.tsx +329 -0
- package/src/extensions/autocomplete.ts +18 -17
- package/src/extensions/automerge/automerge.ts +2 -2
- package/src/extensions/automerge/cursor.ts +2 -1
- package/src/extensions/automerge/index.ts +1 -0
- package/src/extensions/awareness.ts +25 -32
- package/src/extensions/code.ts +14 -7
- package/src/extensions/comments.ts +89 -120
- package/src/extensions/cursor.ts +46 -0
- package/src/extensions/hr.ts +4 -5
- package/src/extensions/image.ts +40 -22
- package/src/extensions/index.ts +1 -1
- package/src/extensions/link.ts +86 -88
- package/src/extensions/markdown/highlight.ts +3 -12
- package/src/extensions/table.ts +10 -4
- package/src/extensions/tasklist.ts +53 -79
- package/src/extensions/yjs/cursor.ts +2 -1
- package/src/extensions/yjs/yjs.test.ts +1 -1
- package/src/extensions/yjs/yjs.ts +2 -2
- package/src/hooks/useTextModel.ts +4 -7
- package/src/hooks/yjs.ts +1 -23
- package/src/index.ts +3 -0
- package/src/styles/markdown.ts +0 -2
- package/src/styles/tokens.ts +3 -0
- package/src/themes/default.ts +51 -78
- package/dist/types/src/extensions/mermaid.d.ts +0 -3
- package/dist/types/src/extensions/mermaid.d.ts.map +0 -1
- package/dist/types/src/util/cursor.d.ts.map +0 -1
- package/dist/types/src/util/index.d.ts +0 -3
- package/dist/types/src/util/index.d.ts.map +0 -1
- package/dist/types/src/util/util.d.ts.map +0 -1
- package/src/extensions/mermaid.ts +0 -11
- package/src/util/cursor.ts +0 -29
- package/src/util/index.ts +0 -6
- /package/dist/types/src/{util/util.d.ts → util.d.ts} +0 -0
- /package/src/{util/util.ts → util.ts} +0 -0
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { Event } from '@dxos/async';
|
|
17
17
|
import { Context } from '@dxos/context';
|
|
18
18
|
|
|
19
|
-
import { CursorConverter } from '
|
|
19
|
+
import { Cursor, type CursorConverter } from './cursor';
|
|
20
20
|
|
|
21
21
|
export interface AwarenessProvider {
|
|
22
22
|
remoteStateChange: Event<void>;
|
|
@@ -91,7 +91,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
91
91
|
private _lastHead?: number = undefined;
|
|
92
92
|
|
|
93
93
|
constructor(view: EditorView) {
|
|
94
|
-
this._cursorConverter = view.state.facet(
|
|
94
|
+
this._cursorConverter = view.state.facet(Cursor.converter);
|
|
95
95
|
this._provider = view.state.facet(AwarenessProvider);
|
|
96
96
|
this._provider.open();
|
|
97
97
|
this._provider.remoteStateChange.on(this._ctx, () => {
|
|
@@ -156,7 +156,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
156
156
|
to: end,
|
|
157
157
|
value: Decoration.mark({
|
|
158
158
|
attributes: { style: `background-color: ${lightColor}` },
|
|
159
|
-
class: 'cm-
|
|
159
|
+
class: 'cm-collab-selection',
|
|
160
160
|
}),
|
|
161
161
|
});
|
|
162
162
|
} else {
|
|
@@ -166,7 +166,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
166
166
|
to: startLine.from + startLine.length,
|
|
167
167
|
value: Decoration.mark({
|
|
168
168
|
attributes: { style: `background-color: ${color}` },
|
|
169
|
-
class: 'cm-
|
|
169
|
+
class: 'cm-collab-selection',
|
|
170
170
|
}),
|
|
171
171
|
});
|
|
172
172
|
|
|
@@ -176,7 +176,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
176
176
|
to: end,
|
|
177
177
|
value: Decoration.mark({
|
|
178
178
|
attributes: { style: `background-color: ${color}` },
|
|
179
|
-
class: 'cm-
|
|
179
|
+
class: 'cm-collab-selection',
|
|
180
180
|
}),
|
|
181
181
|
});
|
|
182
182
|
|
|
@@ -186,7 +186,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
186
186
|
from: linePos,
|
|
187
187
|
to: linePos,
|
|
188
188
|
value: Decoration.line({
|
|
189
|
-
attributes: { style: `background-color: ${color}`, class: 'cm-
|
|
189
|
+
attributes: { style: `background-color: ${color}`, class: 'cm-collab-selectionLine' },
|
|
190
190
|
}),
|
|
191
191
|
});
|
|
192
192
|
}
|
|
@@ -208,24 +208,22 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
class RemoteCaretWidget extends WidgetType {
|
|
211
|
-
constructor(
|
|
211
|
+
constructor(private readonly _name: string, private readonly _color: string) {
|
|
212
212
|
super();
|
|
213
|
-
this.name = name;
|
|
214
|
-
this.color = color;
|
|
215
213
|
}
|
|
216
214
|
|
|
217
|
-
toDOM(): HTMLElement {
|
|
215
|
+
override toDOM(): HTMLElement {
|
|
218
216
|
const span = document.createElement('span');
|
|
219
|
-
span.className = 'cm-
|
|
220
|
-
span.style.backgroundColor = this.
|
|
221
|
-
span.style.borderColor = this.
|
|
217
|
+
span.className = 'cm-collab-selectionCaret';
|
|
218
|
+
span.style.backgroundColor = this._color;
|
|
219
|
+
span.style.borderColor = this._color;
|
|
222
220
|
|
|
223
221
|
const dot = document.createElement('div');
|
|
224
|
-
dot.className = 'cm-
|
|
222
|
+
dot.className = 'cm-collab-selectionCaretDot';
|
|
225
223
|
|
|
226
224
|
const info = document.createElement('div');
|
|
227
|
-
info.className = 'cm-
|
|
228
|
-
info.innerText = this.
|
|
225
|
+
info.className = 'cm-collab-selectionInfo';
|
|
226
|
+
info.innerText = this._name;
|
|
229
227
|
|
|
230
228
|
span.appendChild(document.createTextNode('\u2060'));
|
|
231
229
|
span.appendChild(dot);
|
|
@@ -236,18 +234,14 @@ class RemoteCaretWidget extends WidgetType {
|
|
|
236
234
|
return span;
|
|
237
235
|
}
|
|
238
236
|
|
|
239
|
-
override eq(widget: this) {
|
|
240
|
-
return widget.color === this.color;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
compare(widget: this) {
|
|
244
|
-
return widget.color === this.color;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
237
|
override updateDOM() {
|
|
248
238
|
return false;
|
|
249
239
|
}
|
|
250
240
|
|
|
241
|
+
override eq(widget: this) {
|
|
242
|
+
return widget._color === this._color;
|
|
243
|
+
}
|
|
244
|
+
|
|
251
245
|
override get estimatedHeight() {
|
|
252
246
|
return -1;
|
|
253
247
|
}
|
|
@@ -257,14 +251,13 @@ class RemoteCaretWidget extends WidgetType {
|
|
|
257
251
|
}
|
|
258
252
|
}
|
|
259
253
|
|
|
260
|
-
// TODO(burdon): Rename prefix (y for yjs?)
|
|
261
254
|
const styles = EditorView.baseTheme({
|
|
262
|
-
'.cm-
|
|
263
|
-
'.cm-
|
|
255
|
+
'.cm-collab-selection': {},
|
|
256
|
+
'.cm-collab-selectionLine': {
|
|
264
257
|
padding: 0,
|
|
265
258
|
margin: '0px 2px 0px 4px',
|
|
266
259
|
},
|
|
267
|
-
'.cm-
|
|
260
|
+
'.cm-collab-selectionCaret': {
|
|
268
261
|
position: 'relative',
|
|
269
262
|
borderLeft: '1px solid black',
|
|
270
263
|
borderRight: '1px solid black',
|
|
@@ -273,7 +266,7 @@ const styles = EditorView.baseTheme({
|
|
|
273
266
|
boxSizing: 'border-box',
|
|
274
267
|
display: 'inline',
|
|
275
268
|
},
|
|
276
|
-
'.cm-
|
|
269
|
+
'.cm-collab-selectionCaretDot': {
|
|
277
270
|
borderRadius: '50%',
|
|
278
271
|
position: 'absolute',
|
|
279
272
|
width: '.4em',
|
|
@@ -284,11 +277,11 @@ const styles = EditorView.baseTheme({
|
|
|
284
277
|
transition: 'transform .3s ease-in-out',
|
|
285
278
|
boxSizing: 'border-box',
|
|
286
279
|
},
|
|
287
|
-
'.cm-
|
|
280
|
+
'.cm-collab-selectionCaret:hover > .cm-collab-selectionCaretDot': {
|
|
288
281
|
transformOrigin: 'bottom center',
|
|
289
282
|
transform: 'scale(0)',
|
|
290
283
|
},
|
|
291
|
-
'.cm-
|
|
284
|
+
'.cm-collab-selectionInfo': {
|
|
292
285
|
position: 'absolute',
|
|
293
286
|
top: '-1.05em',
|
|
294
287
|
left: '-1px',
|
|
@@ -309,7 +302,7 @@ const styles = EditorView.baseTheme({
|
|
|
309
302
|
transitionDelay: '0s',
|
|
310
303
|
whiteSpace: 'nowrap',
|
|
311
304
|
},
|
|
312
|
-
'.cm-
|
|
305
|
+
'.cm-collab-selectionCaret:hover > .cm-collab-selectionInfo': {
|
|
313
306
|
opacity: 1,
|
|
314
307
|
transitionDelay: '0s',
|
|
315
308
|
},
|
package/src/extensions/code.ts
CHANGED
|
@@ -11,24 +11,23 @@ import {
|
|
|
11
11
|
Decoration,
|
|
12
12
|
type BlockInfo,
|
|
13
13
|
} from '@codemirror/view';
|
|
14
|
-
import get from 'lodash.get';
|
|
15
14
|
|
|
16
15
|
import { mx } from '@dxos/react-ui-theme';
|
|
17
16
|
|
|
18
|
-
import {
|
|
17
|
+
import { getToken } from '../styles';
|
|
19
18
|
|
|
20
19
|
const CODE_REGEX = /```[\s\S]*?```/gs;
|
|
21
20
|
|
|
22
21
|
// TODO(burdon): Reconcile with theme.
|
|
23
22
|
const styles = EditorView.baseTheme({
|
|
24
23
|
'& .cm-codeblock': {
|
|
25
|
-
fontFamily:
|
|
24
|
+
fontFamily: getToken('fontFamily.mono', []).join(','),
|
|
26
25
|
},
|
|
27
26
|
'&light [aria-readonly="true"] .cm-codeblock': {
|
|
28
|
-
background:
|
|
27
|
+
background: getToken('extend.colors.neutral.50'),
|
|
29
28
|
},
|
|
30
29
|
'&dark [aria-readonly="true"] .cm-codeblock': {
|
|
31
|
-
background:
|
|
30
|
+
background: getToken('extend.colors.neutral.850'),
|
|
32
31
|
},
|
|
33
32
|
'& [aria-readonly="true"] .cm-codeblock': {
|
|
34
33
|
display: 'block',
|
|
@@ -56,7 +55,7 @@ const getLineRange = (lines: BlockInfo[], from: number, to: number) => {
|
|
|
56
55
|
export const code = () => {
|
|
57
56
|
const getDecorations = (view: EditorView) => {
|
|
58
57
|
const decorations: Range<Decoration>[] = [];
|
|
59
|
-
if (view.state.readOnly) {
|
|
58
|
+
if (!view.hasFocus || view.state.readOnly) {
|
|
60
59
|
const text = view.state.doc.sliceString(0);
|
|
61
60
|
const matches = text.matchAll(CODE_REGEX);
|
|
62
61
|
const blocks = view.viewportLineBlocks;
|
|
@@ -79,13 +78,21 @@ export const code = () => {
|
|
|
79
78
|
return [
|
|
80
79
|
ViewPlugin.fromClass(
|
|
81
80
|
class {
|
|
81
|
+
hasFocus = false;
|
|
82
82
|
decorations: DecorationSet;
|
|
83
83
|
constructor(view: EditorView) {
|
|
84
84
|
this.decorations = getDecorations(view);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
update(update: ViewUpdate) {
|
|
88
|
-
if (
|
|
88
|
+
if (
|
|
89
|
+
// TODO(burdon): Generalize for other extensions.
|
|
90
|
+
this.hasFocus !== update.view.hasFocus ||
|
|
91
|
+
update.startState.readOnly !== update.view.state.readOnly ||
|
|
92
|
+
update.docChanged ||
|
|
93
|
+
update.viewportChanged
|
|
94
|
+
) {
|
|
95
|
+
this.hasFocus = update.view.hasFocus;
|
|
89
96
|
this.decorations = getDecorations(update.view);
|
|
90
97
|
}
|
|
91
98
|
}
|
|
@@ -2,21 +2,8 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Extension, StateEffect, StateField } from '@codemirror/state';
|
|
6
|
-
import {
|
|
7
|
-
hoverTooltip,
|
|
8
|
-
keymap,
|
|
9
|
-
type Command,
|
|
10
|
-
type DecorationSet,
|
|
11
|
-
type ViewUpdate,
|
|
12
|
-
Decoration,
|
|
13
|
-
EditorView,
|
|
14
|
-
MatchDecorator,
|
|
15
|
-
ViewPlugin,
|
|
16
|
-
WidgetType,
|
|
17
|
-
type Rect,
|
|
18
|
-
} from '@codemirror/view';
|
|
19
|
-
import get from 'lodash.get';
|
|
5
|
+
import { type Extension, StateEffect, StateField, type Text } from '@codemirror/state';
|
|
6
|
+
import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
|
|
20
7
|
import sortBy from 'lodash.sortby';
|
|
21
8
|
|
|
22
9
|
import { debounce } from '@dxos/async';
|
|
@@ -24,29 +11,21 @@ import { invariant } from '@dxos/invariant';
|
|
|
24
11
|
import { log } from '@dxos/log';
|
|
25
12
|
import { nonNullable } from '@dxos/util';
|
|
26
13
|
|
|
14
|
+
import { Cursor } from './cursor';
|
|
27
15
|
import { type CommentRange, type EditorModel, type Range } from '../hooks';
|
|
28
|
-
import {
|
|
29
|
-
import { callbackWrapper
|
|
16
|
+
import { getToken } from '../styles';
|
|
17
|
+
import { callbackWrapper } from '../util';
|
|
30
18
|
|
|
31
19
|
// TODO(burdon): Handle delete, cut, copy, and paste (separately) text that includes comment range.
|
|
32
20
|
// TODO(burdon): Consider breaking into separate plugin (since not standalone)? Like mermaid?
|
|
33
21
|
|
|
34
22
|
// TODO(burdon): Reconcile with theme.
|
|
35
23
|
const styles = EditorView.baseTheme({
|
|
36
|
-
'& .cm-bookmark': {
|
|
37
|
-
cursor: 'pointer',
|
|
38
|
-
margin: '4px',
|
|
39
|
-
padding: '4px',
|
|
40
|
-
backgroundColor: 'yellow',
|
|
41
|
-
},
|
|
42
|
-
'& .cm-bookmark-selected': {
|
|
43
|
-
backgroundColor: 'orange',
|
|
44
|
-
},
|
|
45
24
|
'& .cm-comment': {
|
|
46
|
-
backgroundColor:
|
|
25
|
+
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
47
26
|
},
|
|
48
27
|
'& .cm-comment-active': {
|
|
49
|
-
backgroundColor:
|
|
28
|
+
backgroundColor: getToken('extend.colors.yellow.100'),
|
|
50
29
|
},
|
|
51
30
|
});
|
|
52
31
|
|
|
@@ -99,7 +78,7 @@ const setCommentState = StateEffect.define<CommentsState>();
|
|
|
99
78
|
const commentsStateField = StateField.define<CommentsState>({
|
|
100
79
|
create: () => ({ ranges: [] }),
|
|
101
80
|
update: (value, tr) => {
|
|
102
|
-
const cursorConverter = tr.state.facet(
|
|
81
|
+
const cursorConverter = tr.state.facet(Cursor.converter);
|
|
103
82
|
|
|
104
83
|
for (const effect of tr.effects) {
|
|
105
84
|
// Update selection.
|
|
@@ -112,7 +91,7 @@ const commentsStateField = StateField.define<CommentsState>({
|
|
|
112
91
|
const { comments } = effect.value;
|
|
113
92
|
const ranges: ExtendedCommentRange[] = comments
|
|
114
93
|
.map((comment) => {
|
|
115
|
-
const range = getRangeFromCursor(cursorConverter, comment.cursor);
|
|
94
|
+
const range = Cursor.getRangeFromCursor(cursorConverter, comment.cursor);
|
|
116
95
|
return range && { ...comment, ...range };
|
|
117
96
|
})
|
|
118
97
|
.filter(nonNullable);
|
|
@@ -155,42 +134,17 @@ const highlightDecorations = EditorView.decorations.compute([commentsStateField]
|
|
|
155
134
|
return Decoration.set(decorations);
|
|
156
135
|
});
|
|
157
136
|
|
|
158
|
-
/**
|
|
159
|
-
* Optional bookmark widget at the start/end of the selection.
|
|
160
|
-
* May correspond to a markdown bookmark.
|
|
161
|
-
*/
|
|
162
|
-
class BookmarkWidget extends WidgetType {
|
|
163
|
-
constructor(
|
|
164
|
-
private readonly _anchor: number,
|
|
165
|
-
private readonly _id: string,
|
|
166
|
-
private readonly _handleClick?: () => void,
|
|
167
|
-
) {
|
|
168
|
-
super();
|
|
169
|
-
invariant(this._id);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
override eq(other: BookmarkWidget) {
|
|
173
|
-
return this._anchor === other._anchor && this._id === other._id;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
override toDOM() {
|
|
177
|
-
const span = document.createElement('span');
|
|
178
|
-
span.className = 'cm-bookmark';
|
|
179
|
-
span.textContent = '※';
|
|
180
|
-
if (this._handleClick) {
|
|
181
|
-
span.onclick = () => this._handleClick!();
|
|
182
|
-
}
|
|
183
|
-
return span;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
137
|
export type CommentsOptions = {
|
|
188
138
|
key?: string;
|
|
189
139
|
footnote?: boolean;
|
|
190
140
|
/**
|
|
191
141
|
* Called to create a new thread and return the thread id.
|
|
192
142
|
*/
|
|
193
|
-
onCreate?: (
|
|
143
|
+
onCreate?: (cursor: string, location?: Rect | null) => string | undefined;
|
|
144
|
+
/**
|
|
145
|
+
* Called when a comment is moved.
|
|
146
|
+
*/
|
|
147
|
+
onMove?: (threadID: string, cursor: string) => void;
|
|
194
148
|
/**
|
|
195
149
|
* Called to notify which thread is currently closest to the cursor.
|
|
196
150
|
*/
|
|
@@ -198,7 +152,66 @@ export type CommentsOptions = {
|
|
|
198
152
|
/**
|
|
199
153
|
* Called to render tooltip.
|
|
200
154
|
*/
|
|
201
|
-
onHover?: (el: Element) => void;
|
|
155
|
+
onHover?: (el: Element, shortcut: string) => void;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const trackPastedComments = (onMove: (threadID: string, cursor: string) => void) => {
|
|
159
|
+
let tracked: { text: Text; comments: { from: number; to: number; id: string }[] } | null = null;
|
|
160
|
+
|
|
161
|
+
const registerCopy = (event: Event, view: EditorView) => {
|
|
162
|
+
const comments = view.state.field(commentsStateField);
|
|
163
|
+
const { main } = view.state.selection;
|
|
164
|
+
const inSel = comments.ranges.filter(
|
|
165
|
+
(range) => range.from >= main.from && range.to <= main.to && range.from < range.to,
|
|
166
|
+
);
|
|
167
|
+
if (!inSel.length) {
|
|
168
|
+
tracked = null;
|
|
169
|
+
} else {
|
|
170
|
+
tracked = {
|
|
171
|
+
text: view.state.doc.slice(main.from, main.to),
|
|
172
|
+
comments: inSel.map((range) => ({ from: range.from - main.from, to: range.to - main.from, id: range.id })),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return [
|
|
178
|
+
EditorView.domEventHandlers({
|
|
179
|
+
cut: registerCopy,
|
|
180
|
+
copy: registerCopy,
|
|
181
|
+
}),
|
|
182
|
+
|
|
183
|
+
EditorView.updateListener.of((update) => {
|
|
184
|
+
if (tracked) {
|
|
185
|
+
const paste = update.transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
186
|
+
if (paste) {
|
|
187
|
+
let found = -1;
|
|
188
|
+
paste.changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
189
|
+
if (text.eq(tracked!.text)) {
|
|
190
|
+
for (let i = update.transactions.indexOf(paste!) + 1; i < update.transactions.length; i++) {
|
|
191
|
+
fromB = update.transactions[i].changes.mapPos(fromB);
|
|
192
|
+
}
|
|
193
|
+
found = fromB;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
if (found > -1) {
|
|
197
|
+
const active = update.view.state.field(commentsStateField).ranges;
|
|
198
|
+
for (const moved of tracked.comments) {
|
|
199
|
+
if (active.some((range) => range.id === moved.id && range.from === range.to)) {
|
|
200
|
+
onMove(
|
|
201
|
+
moved.id,
|
|
202
|
+
Cursor.getCursorFromRange(update.view.state.facet(Cursor.converter), {
|
|
203
|
+
from: found + moved.from,
|
|
204
|
+
to: found + moved.to,
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
tracked = null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}),
|
|
214
|
+
];
|
|
202
215
|
};
|
|
203
216
|
|
|
204
217
|
/**
|
|
@@ -214,13 +227,15 @@ export type CommentsOptions = {
|
|
|
214
227
|
* 6). Optionally, implements a hoverTooltip to show hints when creating a selection range.
|
|
215
228
|
*/
|
|
216
229
|
export const comments = (options: CommentsOptions = {}): Extension => {
|
|
230
|
+
const { key: shortcut = "meta-'" } = options;
|
|
231
|
+
|
|
217
232
|
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
218
233
|
|
|
219
234
|
/**
|
|
220
235
|
* Create comment thread action.
|
|
221
236
|
*/
|
|
222
237
|
const createCommentThread: Command = (view) => {
|
|
223
|
-
const cursorConverter = view.state.facet(
|
|
238
|
+
const cursorConverter = view.state.facet(Cursor.converter);
|
|
224
239
|
|
|
225
240
|
invariant(options.onCreate);
|
|
226
241
|
const { head, from, to } = view.state.selection.main;
|
|
@@ -228,10 +243,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
228
243
|
return false;
|
|
229
244
|
}
|
|
230
245
|
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
246
|
+
const cursor = Cursor.getCursorFromRange(cursorConverter, { from, to });
|
|
247
|
+
if (cursor) {
|
|
233
248
|
// Create thread via callback.
|
|
234
|
-
const id = callbackWrapper(options.onCreate)(
|
|
249
|
+
const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
|
|
235
250
|
if (id) {
|
|
236
251
|
// Update range.
|
|
237
252
|
view.dispatch({
|
|
@@ -255,44 +270,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
255
270
|
return false;
|
|
256
271
|
};
|
|
257
272
|
|
|
258
|
-
/**
|
|
259
|
-
* Bookmark widget decoration.
|
|
260
|
-
*/
|
|
261
|
-
const bookmarksViewPlugin = ViewPlugin.fromClass(
|
|
262
|
-
class BookmarkViewPlugin {
|
|
263
|
-
// Match markdown footnotes (option).
|
|
264
|
-
// https://www.markdownguide.org/extended-syntax/#footnotes
|
|
265
|
-
static bookmarkMatcher = new MatchDecorator({
|
|
266
|
-
regexp: /\[\^(\w+)\]/g,
|
|
267
|
-
decoration: (match, view, pos) => {
|
|
268
|
-
const id = match[1];
|
|
269
|
-
return Decoration.replace({
|
|
270
|
-
id,
|
|
271
|
-
widget: new BookmarkWidget(pos, id),
|
|
272
|
-
});
|
|
273
|
-
},
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
bookmarks: DecorationSet;
|
|
277
|
-
constructor(view: EditorView) {
|
|
278
|
-
this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.createDeco(view);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
update(update: ViewUpdate) {
|
|
282
|
-
this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.updateDeco(update, this.bookmarks);
|
|
283
|
-
}
|
|
284
|
-
},
|
|
285
|
-
{
|
|
286
|
-
decorations: (instance) => instance.bookmarks,
|
|
287
|
-
provide: (plugin) =>
|
|
288
|
-
EditorView.atomicRanges.of((view) => {
|
|
289
|
-
return view.plugin(plugin)?.bookmarks || Decoration.none;
|
|
290
|
-
}),
|
|
291
|
-
},
|
|
292
|
-
);
|
|
293
|
-
|
|
294
273
|
return [
|
|
295
|
-
bookmarksViewPlugin,
|
|
296
274
|
commentsStateField,
|
|
297
275
|
highlightDecorations,
|
|
298
276
|
styles,
|
|
@@ -303,7 +281,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
303
281
|
options.onCreate
|
|
304
282
|
? keymap.of([
|
|
305
283
|
{
|
|
306
|
-
key:
|
|
284
|
+
key: shortcut,
|
|
307
285
|
run: callbackWrapper(createCommentThread),
|
|
308
286
|
},
|
|
309
287
|
])
|
|
@@ -325,7 +303,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
325
303
|
create: () => {
|
|
326
304
|
// TODO(burdon): Dispatch to react callback to render (or use SSR)?
|
|
327
305
|
const el = document.createElement('div');
|
|
328
|
-
options.onHover?.(el);
|
|
306
|
+
options.onHover?.(el, shortcut);
|
|
329
307
|
return { dom: el, offset: { x: 0, y: 8 } };
|
|
330
308
|
},
|
|
331
309
|
};
|
|
@@ -342,7 +320,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
342
320
|
// TODO(burdon): Is there a better (finer grained) way to do this?
|
|
343
321
|
//
|
|
344
322
|
EditorView.updateListener.of(({ view, state, changes }) => {
|
|
345
|
-
const cursorConverter = view.state.facet(
|
|
323
|
+
const cursorConverter = view.state.facet(Cursor.converter);
|
|
346
324
|
|
|
347
325
|
//
|
|
348
326
|
// Test if need to recompute indexed range if document changes before the end of the range.
|
|
@@ -353,7 +331,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
353
331
|
changes.iterChanges((from, to, from2, to2) => {
|
|
354
332
|
ranges.forEach((range) => {
|
|
355
333
|
if (from2 === to2) {
|
|
356
|
-
const newRange = getRangeFromCursor(cursorConverter, range.cursor);
|
|
334
|
+
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
357
335
|
if (!newRange || newRange.to - newRange.from === 0) {
|
|
358
336
|
// TODO(burdon): Delete range if empty.
|
|
359
337
|
log.info('deleted comment', { thread: range.id });
|
|
@@ -361,7 +339,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
361
339
|
}
|
|
362
340
|
|
|
363
341
|
if (from <= range.to) {
|
|
364
|
-
const newRange = getRangeFromCursor(cursorConverter, range.cursor);
|
|
342
|
+
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
365
343
|
Object.assign(range, newRange);
|
|
366
344
|
mod = true;
|
|
367
345
|
}
|
|
@@ -393,6 +371,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
393
371
|
}
|
|
394
372
|
});
|
|
395
373
|
|
|
374
|
+
// TODO(burdon): Fire debounced callback if range selected (to show hint).
|
|
375
|
+
|
|
396
376
|
if (selected.active !== active || selected.closest !== closest) {
|
|
397
377
|
view.dispatch({ effects: setSelection.of(selected) });
|
|
398
378
|
|
|
@@ -404,18 +384,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
404
384
|
}
|
|
405
385
|
}
|
|
406
386
|
}),
|
|
407
|
-
];
|
|
408
|
-
};
|
|
409
387
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const to = cursorConverter.toCursor(range.to, -1);
|
|
413
|
-
return [from, to].join(':');
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
const getRangeFromCursor = (cursorConverter: CursorConverter, cursor: string) => {
|
|
417
|
-
const parts = cursor.split(':');
|
|
418
|
-
const from = cursorConverter.fromCursor(parts[0]);
|
|
419
|
-
const to = cursorConverter.fromCursor(parts[1]);
|
|
420
|
-
return from && to ? { from, to } : undefined;
|
|
388
|
+
options.onMove ? trackPastedComments(options.onMove) : [],
|
|
389
|
+
];
|
|
421
390
|
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Facet } from '@codemirror/state';
|
|
6
|
+
|
|
7
|
+
import type { Range } from '../hooks';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts indexes into the text document into stable peer-independent cursors.
|
|
11
|
+
*
|
|
12
|
+
* See:
|
|
13
|
+
* - https://automerge.org/automerge/api-docs/js/functions/next.getCursor.html
|
|
14
|
+
* - https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
|
|
15
|
+
*
|
|
16
|
+
* @param {assoc} number Negative values will associate the cursor with the previous character
|
|
17
|
+
* while positive - with the next one.
|
|
18
|
+
*/
|
|
19
|
+
export interface CursorConverter {
|
|
20
|
+
toCursor(position: number, assoc?: -1 | 1 | undefined): string;
|
|
21
|
+
fromCursor(cursor: string): number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const defaultCursorConverter: CursorConverter = {
|
|
25
|
+
toCursor: (position) => position.toString(),
|
|
26
|
+
fromCursor: (cursor) => parseInt(cursor),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class Cursor {
|
|
30
|
+
static readonly converter = Facet.define<CursorConverter, CursorConverter>({
|
|
31
|
+
combine: (providers) => providers[0] ?? defaultCursorConverter,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
static readonly getCursorFromRange = (cursorConverter: CursorConverter, range: Range) => {
|
|
35
|
+
const from = cursorConverter.toCursor(range.from);
|
|
36
|
+
const to = cursorConverter.toCursor(range.to, -1);
|
|
37
|
+
return [from, to].join(':');
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
static readonly getRangeFromCursor = (cursorConverter: CursorConverter, cursor: string) => {
|
|
41
|
+
const parts = cursor.split(':');
|
|
42
|
+
const from = cursorConverter.fromCursor(parts[0]);
|
|
43
|
+
const to = cursorConverter.fromCursor(parts[1]);
|
|
44
|
+
return from !== undefined && to !== undefined ? { from, to } : undefined;
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/extensions/hr.ts
CHANGED
|
@@ -11,16 +11,15 @@ import {
|
|
|
11
11
|
type ViewUpdate,
|
|
12
12
|
WidgetType,
|
|
13
13
|
} from '@codemirror/view';
|
|
14
|
-
import get from 'lodash.get';
|
|
15
14
|
|
|
16
|
-
import {
|
|
15
|
+
import { getToken } from '../styles';
|
|
17
16
|
|
|
18
17
|
// TODO(burdon): Reconcile with theme.
|
|
19
18
|
const styles = EditorView.baseTheme({
|
|
20
19
|
'& .cm-hr': {
|
|
21
20
|
// TODO(burdon): ???
|
|
22
21
|
// Note that block-level decorations should not have vertical margins,
|
|
23
|
-
borderBottom: `1px solid ${
|
|
22
|
+
borderBottom: `1px solid ${getToken('extend.colors.neutral.200')}`,
|
|
24
23
|
},
|
|
25
24
|
});
|
|
26
25
|
|
|
@@ -29,11 +28,11 @@ class HorizontalRuleWidget extends WidgetType {
|
|
|
29
28
|
super();
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
override eq(other:
|
|
31
|
+
override eq(other: this) {
|
|
33
32
|
return this._pos === (other as any as HorizontalRuleWidget)._pos;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
toDOM(view: EditorView) {
|
|
35
|
+
override toDOM(view: EditorView) {
|
|
37
36
|
// TODO(burdon): Create <hr> element? Does this clash with markdown parser?
|
|
38
37
|
const el = document.createElement('div');
|
|
39
38
|
el.className = 'cm-hr';
|