@dxos/react-ui-editor 0.3.11-main.c95bc86 → 0.3.11-main.cb5abf5
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 +808 -252
- 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 +1 -2
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -1
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/{comments.stories.d.ts → hooks.stories.d.ts} +9 -12
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +25 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +23 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/index.d.ts +2 -0
- package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/defs.d.ts +5 -5
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +2 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness.d.ts +1 -1
- package/dist/types/src/extensions/basic.d.ts +1 -1
- package/dist/types/src/extensions/basic.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/extensions/markdown/bundle.d.ts +2 -2
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/code.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +13 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useEditorView.d.ts +17 -0
- package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +36 -14
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +12 -0
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/package.json +38 -25
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +10 -16
- package/src/components/TextEditor/TextEditor.tsx +27 -16
- package/src/components/TextEditor/automerge.stories.tsx +1 -2
- package/src/components/TextEditor/hooks.stories.tsx +106 -0
- package/src/components/Toolbar/Toolbar.stories.tsx +90 -0
- package/src/components/Toolbar/Toolbar.tsx +166 -0
- package/src/components/Toolbar/index.ts +5 -0
- package/src/components/index.ts +1 -0
- package/src/extensions/automerge/automerge.spec.tsx +76 -0
- package/src/extensions/automerge/automerge.test.ts +13 -0
- package/src/extensions/automerge/automerge.ts +26 -11
- package/src/extensions/automerge/cursor.ts +3 -3
- package/src/extensions/automerge/defs.ts +9 -9
- package/src/extensions/automerge/semaphore.ts +17 -19
- package/src/extensions/automerge/update-automerge.ts +12 -6
- package/src/extensions/automerge/update-codemirror.ts +5 -5
- package/src/extensions/awareness.ts +7 -4
- package/src/extensions/basic.ts +15 -13
- package/src/extensions/blast.ts +4 -1
- package/src/extensions/comments.ts +113 -65
- package/src/extensions/markdown/bundle.ts +10 -5
- package/src/extensions/markdown/code.ts +36 -13
- package/src/extensions/markdown/formatting.ts +188 -0
- package/src/extensions/markdown/heading.ts +1 -1
- package/src/extensions/markdown/index.ts +1 -0
- package/src/extensions/markdown/link.ts +12 -2
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useActionHandler.ts +62 -0
- package/src/hooks/useEditorView.ts +29 -0
- package/src/hooks/useTextEditor.ts +143 -19
- package/src/hooks/useTextModel.ts +28 -9
- package/src/index.ts +1 -1
- package/src/themes/default.ts +9 -4
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +0 -1
- package/src/components/TextEditor/comments.stories.tsx +0 -357
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
import { type StateField } from '@codemirror/state';
|
|
8
8
|
import { type EditorView } from '@codemirror/view';
|
|
9
9
|
|
|
10
|
-
import { next as
|
|
10
|
+
import { next as A } from '@dxos/automerge/automerge';
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
getLastHeads,
|
|
14
14
|
getPath,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
isReconcile,
|
|
16
|
+
reconcileAnnotation,
|
|
17
17
|
updateHeads,
|
|
18
18
|
type IDocHandle,
|
|
19
19
|
type State,
|
|
@@ -22,7 +22,7 @@ import { updateAutomerge } from './update-automerge';
|
|
|
22
22
|
import { updateCodeMirror } from './update-codemirror';
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Implements three-way merge (on each mutation
|
|
25
|
+
* Implements three-way merge (on each mutation).
|
|
26
26
|
*/
|
|
27
27
|
export class PatchSemaphore {
|
|
28
28
|
private _inReconcile = false;
|
|
@@ -33,13 +33,15 @@ export class PatchSemaphore {
|
|
|
33
33
|
private readonly _state: StateField<State>
|
|
34
34
|
) {}
|
|
35
35
|
|
|
36
|
-
// NOTE: Cannot destruct view.state.
|
|
37
36
|
reconcile(view: EditorView) {
|
|
38
|
-
if (this._inReconcile) {
|
|
39
|
-
|
|
37
|
+
if (!this._inReconcile) {
|
|
38
|
+
this._inReconcile = true;
|
|
39
|
+
this.doReconcile(view);
|
|
40
|
+
this._inReconcile = false;
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
+
}
|
|
42
43
|
|
|
44
|
+
private doReconcile(view: EditorView) {
|
|
43
45
|
const path = getPath(view.state, this._state);
|
|
44
46
|
|
|
45
47
|
// Get the heads before the unreconciled transactions are applied.
|
|
@@ -47,14 +49,14 @@ export class PatchSemaphore {
|
|
|
47
49
|
let selection = view.state.selection;
|
|
48
50
|
|
|
49
51
|
// First undo all the unreconciled transactions.
|
|
50
|
-
const transactions = view.state.field(this._state).unreconciledTransactions.filter((tx) => !
|
|
52
|
+
const transactions = view.state.field(this._state).unreconciledTransactions.filter((tx) => !isReconcile(tx));
|
|
51
53
|
const toInvert = transactions.slice().reverse();
|
|
52
|
-
for (const
|
|
53
|
-
const inverted =
|
|
54
|
+
for (const tr of toInvert) {
|
|
55
|
+
const inverted = tr.changes.invert(tr.startState.doc);
|
|
54
56
|
selection = selection.map(inverted);
|
|
55
57
|
view.dispatch({
|
|
56
58
|
changes: inverted,
|
|
57
|
-
annotations:
|
|
59
|
+
annotations: reconcileAnnotation.of(true),
|
|
58
60
|
});
|
|
59
61
|
}
|
|
60
62
|
|
|
@@ -63,21 +65,17 @@ export class PatchSemaphore {
|
|
|
63
65
|
let newHeads = updateAutomerge(this._state, this._handle, transactions, view.state);
|
|
64
66
|
if (newHeads === null || newHeads === undefined) {
|
|
65
67
|
// TODO(alexjg): this is the call that's resetting the editor state on click.
|
|
66
|
-
newHeads =
|
|
68
|
+
newHeads = A.getHeads(this._handle.docSync()!);
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
// Now get the diff between the updated state of the document and the heads and apply that to the codemirror doc.
|
|
70
|
-
const diff =
|
|
71
|
-
? []
|
|
72
|
-
: automerge.diff(this._handle.docSync()!, oldHeads, newHeads);
|
|
72
|
+
const diff = A.equals(oldHeads, newHeads) ? [] : A.diff(this._handle.docSync()!, oldHeads, newHeads);
|
|
73
73
|
updateCodeMirror(view, selection, path, diff);
|
|
74
74
|
|
|
75
75
|
// Update automerge state.
|
|
76
76
|
view.dispatch({
|
|
77
77
|
effects: updateHeads(newHeads),
|
|
78
|
-
annotations:
|
|
78
|
+
annotations: reconcileAnnotation.of(false),
|
|
79
79
|
});
|
|
80
|
-
|
|
81
|
-
this._inReconcile = false;
|
|
82
80
|
}
|
|
83
81
|
}
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// Ref: https://github.com/automerge/automerge-codemirror
|
|
5
5
|
//
|
|
6
6
|
|
|
7
|
-
import { type EditorState, type StateField, type
|
|
7
|
+
import { type EditorState, type StateField, type Transaction, type Text } from '@codemirror/state';
|
|
8
8
|
|
|
9
|
-
import { next as
|
|
9
|
+
import { next as A, type Heads } from '@dxos/automerge/automerge';
|
|
10
10
|
|
|
11
11
|
import { type IDocHandle, type State } from './defs';
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@ export const updateAutomerge = (
|
|
|
14
14
|
field: StateField<State>,
|
|
15
15
|
handle: IDocHandle,
|
|
16
16
|
transactions: Transaction[],
|
|
17
|
-
state: EditorState,
|
|
17
|
+
state: EditorState, // TODO(burdon): Just pass in the state field value?
|
|
18
18
|
): Heads | undefined => {
|
|
19
19
|
const { lastHeads, path } = state.field(field);
|
|
20
20
|
|
|
@@ -31,12 +31,18 @@ export const updateAutomerge = (
|
|
|
31
31
|
return undefined;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const newHeads = handle.changeAt(lastHeads, (doc:
|
|
34
|
+
const newHeads = handle.changeAt(lastHeads, (doc: A.Doc<unknown>) => {
|
|
35
|
+
const invertedTransactions: { from: number; del: number; insert: Text }[] = [];
|
|
35
36
|
for (const tr of transactions) {
|
|
36
|
-
tr.changes.iterChanges((fromA
|
|
37
|
-
|
|
37
|
+
tr.changes.iterChanges((fromA, toA, _fromB, _toB, insert) => {
|
|
38
|
+
invertedTransactions.push({ from: fromA, del: toA - fromA, insert });
|
|
38
39
|
});
|
|
39
40
|
}
|
|
41
|
+
|
|
42
|
+
// TODO(burdon): Hack to apply in reverse order to properly apply range.
|
|
43
|
+
invertedTransactions.reverse().forEach(({ from, del, insert }) => {
|
|
44
|
+
A.splice(doc, path.slice(), from, del, insert.toString());
|
|
45
|
+
});
|
|
40
46
|
});
|
|
41
47
|
|
|
42
48
|
return newHeads ?? undefined;
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
type SpliceTextPatch,
|
|
17
17
|
} from '@dxos/automerge/automerge';
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import { reconcileAnnotation } from './defs';
|
|
20
20
|
|
|
21
21
|
export const updateCodeMirror = (view: EditorView, selection: EditorSelection, target: Prop[], patches: Patch[]) => {
|
|
22
22
|
for (const patch of patches) {
|
|
@@ -26,14 +26,14 @@ export const updateCodeMirror = (view: EditorView, selection: EditorSelection, t
|
|
|
26
26
|
selection = selection.map(changeSet, 1);
|
|
27
27
|
view.dispatch({
|
|
28
28
|
changes: changeSet,
|
|
29
|
-
annotations:
|
|
29
|
+
annotations: reconcileAnnotation.of(false),
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
view.dispatch({
|
|
35
35
|
selection,
|
|
36
|
-
annotations:
|
|
36
|
+
annotations: reconcileAnnotation.of(false),
|
|
37
37
|
});
|
|
38
38
|
};
|
|
39
39
|
|
|
@@ -57,7 +57,7 @@ const handleInsert = (target: Prop[], patch: InsertPatch): Array<ChangeSpec> =>
|
|
|
57
57
|
return [];
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
const text = patch.values.map((
|
|
60
|
+
const text = patch.values.map((value) => (value ? value.toString() : '')).join('');
|
|
61
61
|
return [{ from: index, to: index, insert: text }];
|
|
62
62
|
};
|
|
63
63
|
|
|
@@ -88,7 +88,7 @@ const handlePut = (target: Prop[], patch: PutPatch, state: EditorState): Array<C
|
|
|
88
88
|
|
|
89
89
|
const length = state.doc.length;
|
|
90
90
|
if (typeof patch.value !== 'string') {
|
|
91
|
-
return []; // TODO(dmaretskyi): How to handle non
|
|
91
|
+
return []; // TODO(dmaretskyi): How to handle non-string values?
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
return [{ from: 0, to: length, insert: patch.value as any }];
|
|
@@ -38,7 +38,7 @@ const dummyProvider: AwarenessProvider = {
|
|
|
38
38
|
update: () => {},
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
export const
|
|
41
|
+
export const awarenessProvider = Facet.define<AwarenessProvider, AwarenessProvider>({
|
|
42
42
|
combine: (providers) => providers[0] ?? dummyProvider,
|
|
43
43
|
});
|
|
44
44
|
|
|
@@ -68,7 +68,7 @@ export type AwarenessState = {
|
|
|
68
68
|
*/
|
|
69
69
|
export const awareness = (provider = dummyProvider): Extension => {
|
|
70
70
|
return [
|
|
71
|
-
|
|
71
|
+
awarenessProvider.of(provider),
|
|
72
72
|
ViewPlugin.fromClass(RemoteSelectionsDecorator, {
|
|
73
73
|
decorations: (value) => value.decorations,
|
|
74
74
|
}),
|
|
@@ -91,7 +91,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
91
91
|
|
|
92
92
|
constructor(view: EditorView) {
|
|
93
93
|
this._cursorConverter = view.state.facet(Cursor.converter);
|
|
94
|
-
this._provider = view.state.facet(
|
|
94
|
+
this._provider = view.state.facet(awarenessProvider);
|
|
95
95
|
this._provider.open();
|
|
96
96
|
this._provider.remoteStateChange.on(this._ctx, () => {
|
|
97
97
|
view.dispatch({ annotations: [RemoteSelectionChangedAnnotation.of([])] });
|
|
@@ -207,7 +207,10 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
class RemoteCaretWidget extends WidgetType {
|
|
210
|
-
constructor(
|
|
210
|
+
constructor(
|
|
211
|
+
private readonly _name: string,
|
|
212
|
+
private readonly _color: string,
|
|
213
|
+
) {
|
|
211
214
|
super();
|
|
212
215
|
}
|
|
213
216
|
|
package/src/extensions/basic.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
|
10
10
|
import { drawSelection, EditorView, highlightActiveLine, placeholder } from '@codemirror/view';
|
|
11
11
|
|
|
12
12
|
import { type ThemeMode } from '@dxos/react-ui';
|
|
13
|
+
import { isNotFalsy } from '@dxos/util';
|
|
13
14
|
|
|
14
15
|
import { type TextEditorProps } from '../components';
|
|
15
16
|
|
|
@@ -17,21 +18,22 @@ export type BasicBundleOptions = {
|
|
|
17
18
|
themeMode?: ThemeMode;
|
|
18
19
|
} & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
|
|
19
20
|
|
|
20
|
-
export const
|
|
21
|
+
export const createBasicBundle = ({
|
|
21
22
|
themeMode,
|
|
22
23
|
placeholder: _placeholder,
|
|
23
24
|
lineWrapping = true,
|
|
24
|
-
}: BasicBundleOptions): Extension[] =>
|
|
25
|
-
|
|
25
|
+
}: BasicBundleOptions = {}): Extension[] =>
|
|
26
|
+
[
|
|
27
|
+
lineWrapping && EditorView.lineWrapping,
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
// https://codemirror.net/docs/ref/#codemirror.minimalSetup
|
|
30
|
+
bracketMatching(),
|
|
31
|
+
closeBrackets(),
|
|
32
|
+
drawSelection(),
|
|
33
|
+
highlightActiveLine(),
|
|
34
|
+
history(),
|
|
35
|
+
_placeholder && placeholder(_placeholder),
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
];
|
|
37
|
+
// https://github.com/codemirror/theme-one-dark
|
|
38
|
+
themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
|
|
39
|
+
].filter(isNotFalsy);
|
package/src/extensions/blast.ts
CHANGED
|
@@ -125,7 +125,10 @@ class Blaster {
|
|
|
125
125
|
|
|
126
126
|
_lastPoint = { x: 0, y: 0 };
|
|
127
127
|
|
|
128
|
-
constructor(
|
|
128
|
+
constructor(
|
|
129
|
+
private readonly _node: HTMLElement,
|
|
130
|
+
private readonly _options: BlastOptions,
|
|
131
|
+
) {
|
|
129
132
|
this._effect = this._options.effect === 1 ? new Effect1(_options) : new Effect2(_options);
|
|
130
133
|
}
|
|
131
134
|
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { invertedEffects } from '@codemirror/commands';
|
|
6
|
+
import { type Extension, Facet, StateEffect, StateField, type Text, type ChangeDesc } from '@codemirror/state';
|
|
6
7
|
import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
|
|
7
8
|
import sortBy from 'lodash.sortby';
|
|
8
9
|
import { useEffect } from 'react';
|
|
9
10
|
|
|
10
11
|
import { debounce } from '@dxos/async';
|
|
11
|
-
import { invariant } from '@dxos/invariant';
|
|
12
12
|
import { nonNullable } from '@dxos/util';
|
|
13
13
|
|
|
14
14
|
import { Cursor } from './cursor';
|
|
@@ -26,10 +26,8 @@ const styles = EditorView.baseTheme({
|
|
|
26
26
|
},
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
highlightActive: Decoration.mark({ class: 'cm-comment-current' }),
|
|
32
|
-
};
|
|
29
|
+
const commentMark = Decoration.mark({ class: 'cm-comment' });
|
|
30
|
+
const commentCurrentMark = Decoration.mark({ class: 'cm-comment-current' });
|
|
33
31
|
|
|
34
32
|
type CommentState = {
|
|
35
33
|
comment: Comment;
|
|
@@ -47,7 +45,7 @@ export type CommentsState = {
|
|
|
47
45
|
selection: SelectionState;
|
|
48
46
|
};
|
|
49
47
|
|
|
50
|
-
export const
|
|
48
|
+
export const focusComment = (view: EditorView, id: string, center = true) => {
|
|
51
49
|
const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === id);
|
|
52
50
|
if (!comment?.comment.cursor) {
|
|
53
51
|
return;
|
|
@@ -115,7 +113,7 @@ const commentsState = StateField.define<CommentsState>({
|
|
|
115
113
|
/**
|
|
116
114
|
* Decorate ranges.
|
|
117
115
|
*/
|
|
118
|
-
const
|
|
116
|
+
const commentsDecorations = EditorView.decorations.compute([commentsState], (state) => {
|
|
119
117
|
const {
|
|
120
118
|
selection: { current },
|
|
121
119
|
comments,
|
|
@@ -130,9 +128,9 @@ const highlightDecorations = EditorView.decorations.compute([commentsState], (st
|
|
|
130
128
|
}
|
|
131
129
|
|
|
132
130
|
if (comment.comment.id === current) {
|
|
133
|
-
return
|
|
131
|
+
return commentCurrentMark.range(range.from, range.to);
|
|
134
132
|
} else {
|
|
135
|
-
return
|
|
133
|
+
return commentMark.range(range.from, range.to);
|
|
136
134
|
}
|
|
137
135
|
})
|
|
138
136
|
.filter(nonNullable);
|
|
@@ -167,11 +165,11 @@ export type CommentsOptions = {
|
|
|
167
165
|
onHover?: (el: Element, shortcut: string) => void;
|
|
168
166
|
};
|
|
169
167
|
|
|
170
|
-
|
|
168
|
+
type TrackedComment = { id: string; from: number; to: number };
|
|
169
|
+
|
|
171
170
|
const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>) => {
|
|
172
171
|
// Tracks indexed selections within text.
|
|
173
|
-
|
|
174
|
-
let tracked: { text: Text; comments: { id: string; from: number; to: number }[] } | null = null;
|
|
172
|
+
let tracked: { text: Text; comments: TrackedComment[] } | null = null;
|
|
175
173
|
|
|
176
174
|
// Track cut or copy (enables cut-and-paste and copy-delete-paste to restore comment selection).
|
|
177
175
|
const handleTrack = (event: Event, view: EditorView) => {
|
|
@@ -201,16 +199,48 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
|
|
|
201
199
|
copy: handleTrack,
|
|
202
200
|
}),
|
|
203
201
|
|
|
204
|
-
//
|
|
205
|
-
|
|
202
|
+
// Track deleted comments.
|
|
203
|
+
invertedEffects.of((tr) => {
|
|
204
|
+
const { comments } = tr.startState.field(commentsState);
|
|
205
|
+
const effects: StateEffect<any>[] = [];
|
|
206
|
+
tr.changes.iterChangedRanges((fromA, toA) => {
|
|
207
|
+
for (const {
|
|
208
|
+
comment: { id },
|
|
209
|
+
range: { from, to },
|
|
210
|
+
} of comments) {
|
|
211
|
+
if (from < to && from >= fromA && to <= toA) {
|
|
212
|
+
effects.push(restoreCommentEffect.of({ id, from, to }));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return effects;
|
|
218
|
+
}),
|
|
219
|
+
|
|
220
|
+
// Handle paste or the undo of comment deletion.
|
|
221
|
+
EditorView.updateListener.of((update) => {
|
|
222
|
+
const restore: TrackedComment[] = [];
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < update.transactions.length; i++) {
|
|
225
|
+
const tr = update.transactions[i];
|
|
226
|
+
for (let j = 0; j < restore.length; j++) {
|
|
227
|
+
restore[j] = mapTrackedComment(restore[j], tr.changes);
|
|
228
|
+
}
|
|
229
|
+
for (const effect of tr.effects) {
|
|
230
|
+
if (effect.is(restoreCommentEffect)) {
|
|
231
|
+
restore.push(effect.value);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
206
236
|
if (tracked) {
|
|
207
|
-
const paste = transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
237
|
+
const paste = update.transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
208
238
|
if (paste) {
|
|
209
239
|
let found = -1;
|
|
210
240
|
paste.changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
211
241
|
if (text.eq(tracked!.text)) {
|
|
212
|
-
for (let i = transactions.indexOf(paste!) + 1; i < transactions.length; i++) {
|
|
213
|
-
fromB = transactions[i].changes.mapPos(fromB);
|
|
242
|
+
for (let i = update.transactions.indexOf(paste!) + 1; i < update.transactions.length; i++) {
|
|
243
|
+
fromB = update.transactions[i].changes.mapPos(fromB);
|
|
214
244
|
}
|
|
215
245
|
|
|
216
246
|
found = fromB;
|
|
@@ -219,23 +249,78 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
|
|
|
219
249
|
|
|
220
250
|
if (found > -1) {
|
|
221
251
|
for (const moved of tracked.comments) {
|
|
222
|
-
|
|
223
|
-
from: found + moved.from,
|
|
224
|
-
to: found + moved.to,
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
const cursor = Cursor.getCursorFromRange(state, range);
|
|
228
|
-
onUpdate(moved.id, cursor);
|
|
252
|
+
restore.push({ id: moved.id, from: found + moved.from, to: found + moved.to });
|
|
229
253
|
}
|
|
230
254
|
}
|
|
231
255
|
|
|
232
256
|
tracked = null;
|
|
233
257
|
}
|
|
234
258
|
}
|
|
259
|
+
|
|
260
|
+
for (const comment of restore) {
|
|
261
|
+
const { comments } = update.startState.field(commentsState);
|
|
262
|
+
const exists = comments.some((c) => c.comment.id === comment.id && c.range.from < c.range.to);
|
|
263
|
+
if (!exists) {
|
|
264
|
+
const cursor = Cursor.getCursorFromRange(update.state, comment);
|
|
265
|
+
onUpdate(comment.id, cursor);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
235
268
|
}),
|
|
236
269
|
];
|
|
237
270
|
};
|
|
238
271
|
|
|
272
|
+
const mapTrackedComment = (comment: TrackedComment, changes: ChangeDesc) => ({
|
|
273
|
+
id: comment.id,
|
|
274
|
+
from: changes.mapPos(comment.from, 1),
|
|
275
|
+
to: changes.mapPos(comment.to, 1),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// These are attached to undone/redone transactions in the editor for the purpose of restoring comments
|
|
279
|
+
// that were deleted by the original changes.
|
|
280
|
+
const restoreCommentEffect = StateEffect.define<TrackedComment>({ map: mapTrackedComment });
|
|
281
|
+
|
|
282
|
+
const optionsFacet = Facet.define<CommentsOptions, CommentsOptions>({
|
|
283
|
+
combine: (providers) => providers[0],
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Create comment thread action.
|
|
288
|
+
*/
|
|
289
|
+
export const createComment: Command = (view) => {
|
|
290
|
+
const options = view.state.facet(optionsFacet);
|
|
291
|
+
const { from, to } = view.state.selection.main;
|
|
292
|
+
if (from === to) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Don't allow selection at end of document.
|
|
297
|
+
if (to === view.state.doc.length) {
|
|
298
|
+
view.dispatch({
|
|
299
|
+
changes: {
|
|
300
|
+
from: to,
|
|
301
|
+
insert: '\n',
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const cursor = Cursor.getCursorFromRange(view.state, { from, to });
|
|
307
|
+
if (cursor) {
|
|
308
|
+
// Create thread via callback.
|
|
309
|
+
const id = options.onCreate?.(cursor, view.coordsAtPos(from));
|
|
310
|
+
if (id) {
|
|
311
|
+
// Update range.
|
|
312
|
+
view.dispatch({
|
|
313
|
+
effects: setSelection.of({ current: id }),
|
|
314
|
+
selection: { anchor: to },
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return false;
|
|
322
|
+
};
|
|
323
|
+
|
|
239
324
|
/**
|
|
240
325
|
* Comment threads.
|
|
241
326
|
* 1). Updates the EditorModel to store relative selections for a set of comments threads.
|
|
@@ -252,47 +337,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
252
337
|
|
|
253
338
|
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
254
339
|
|
|
255
|
-
/**
|
|
256
|
-
* Create comment thread action.
|
|
257
|
-
*/
|
|
258
|
-
const createCommentThread: Command = (view) => {
|
|
259
|
-
invariant(options.onCreate);
|
|
260
|
-
const { from, to } = view.state.selection.main;
|
|
261
|
-
if (from === to) {
|
|
262
|
-
return false;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Don't allow selection at end of document.
|
|
266
|
-
if (to === view.state.doc.length) {
|
|
267
|
-
view.dispatch({
|
|
268
|
-
changes: {
|
|
269
|
-
from: to,
|
|
270
|
-
insert: '\n',
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const cursor = Cursor.getCursorFromRange(view.state, { from, to });
|
|
276
|
-
if (cursor) {
|
|
277
|
-
// Create thread via callback.
|
|
278
|
-
const id = options.onCreate?.(cursor, view.coordsAtPos(from));
|
|
279
|
-
if (id) {
|
|
280
|
-
// Update range.
|
|
281
|
-
view.dispatch({
|
|
282
|
-
effects: setSelection.of({ current: id }),
|
|
283
|
-
selection: { anchor: from },
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
return true;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return false;
|
|
291
|
-
};
|
|
292
|
-
|
|
293
340
|
return [
|
|
341
|
+
optionsFacet.of(options),
|
|
294
342
|
commentsState,
|
|
295
|
-
|
|
343
|
+
commentsDecorations,
|
|
296
344
|
styles,
|
|
297
345
|
|
|
298
346
|
//
|
|
@@ -302,7 +350,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
302
350
|
? keymap.of([
|
|
303
351
|
{
|
|
304
352
|
key: shortcut,
|
|
305
|
-
run: callbackWrapper(
|
|
353
|
+
run: callbackWrapper(createComment),
|
|
306
354
|
},
|
|
307
355
|
])
|
|
308
356
|
: [],
|
|
@@ -21,13 +21,14 @@ import {
|
|
|
21
21
|
} from '@codemirror/view';
|
|
22
22
|
|
|
23
23
|
import type { ThemeMode } from '@dxos/react-ui';
|
|
24
|
+
import { isNotFalsy } from '@dxos/util';
|
|
24
25
|
|
|
25
26
|
import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
|
|
26
27
|
import { type TextEditorProps } from '../../components';
|
|
27
28
|
|
|
28
29
|
export type MarkdownBundleOptions = {
|
|
29
30
|
themeMode?: ThemeMode;
|
|
30
|
-
} & Pick<TextEditorProps, 'placeholder'>;
|
|
31
|
+
} & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Markdown bundle.
|
|
@@ -36,11 +37,15 @@ export type MarkdownBundleOptions = {
|
|
|
36
37
|
* https://codemirror.net/docs/ref/#codemirror.basicSetup
|
|
37
38
|
*/
|
|
38
39
|
// TODO(burdon): Add Composer here: https://codemirror.net/docs/community
|
|
39
|
-
export const
|
|
40
|
+
export const createMarkdownExtensions = ({
|
|
41
|
+
themeMode,
|
|
42
|
+
placeholder: _placeholder,
|
|
43
|
+
lineWrapping = true,
|
|
44
|
+
}: MarkdownBundleOptions = {}): Extension[] => {
|
|
40
45
|
return [
|
|
46
|
+
lineWrapping && EditorView.lineWrapping,
|
|
41
47
|
EditorState.allowMultipleSelections.of(true),
|
|
42
48
|
EditorState.tabSize.of(2),
|
|
43
|
-
EditorView.lineWrapping,
|
|
44
49
|
|
|
45
50
|
// https://github.com/codemirror/basic-setup
|
|
46
51
|
bracketMatching(),
|
|
@@ -51,7 +56,7 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
|
|
|
51
56
|
highlightActiveLine(),
|
|
52
57
|
history(),
|
|
53
58
|
indentOnInput(),
|
|
54
|
-
_placeholder
|
|
59
|
+
_placeholder && placeholder(_placeholder),
|
|
55
60
|
|
|
56
61
|
// Main extension.
|
|
57
62
|
// https://github.com/codemirror/lang-markdown
|
|
@@ -92,5 +97,5 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
|
|
|
92
97
|
// https://codemirror.net/docs/ref/#commands.standardKeymap
|
|
93
98
|
...standardKeymap,
|
|
94
99
|
]),
|
|
95
|
-
];
|
|
100
|
+
].filter(isNotFalsy);
|
|
96
101
|
};
|