@dxos/react-ui-editor 0.3.11-main.b929e1d → 0.3.11-main.bc6b68b
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 +744 -896
- 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 +13 -14
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +16 -9
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +4 -4
- 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/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.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/defs.d.ts +7 -9
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/index.d.ts +0 -1
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +7 -12
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
- package/dist/types/src/extensions/awareness.d.ts +1 -1
- package/dist/types/src/extensions/awareness.d.ts.map +1 -1
- package/dist/types/src/extensions/basic.d.ts +4 -5
- package/dist/types/src/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/extensions/code.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +32 -19
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/{util → extensions}/cursor.d.ts +10 -2
- 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/index.d.ts +2 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +2 -5
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/mention.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner.d.ts +4 -0
- package/dist/types/src/extensions/outliner.d.ts.map +1 -0
- package/dist/types/src/extensions/tasklist.d.ts.map +1 -1
- package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
- package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/hooks/defs.d.ts +25 -0
- package/dist/types/src/hooks/defs.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +2 -2
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.d.ts +17 -0
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts +2 -32
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +7 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +33 -35
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +75 -80
- package/src/components/TextEditor/TextEditor.stories.tsx +10 -43
- package/src/components/TextEditor/TextEditor.tsx +106 -83
- package/src/components/TextEditor/comments.stories.tsx +357 -0
- package/src/extensions/autocomplete.ts +6 -3
- package/src/extensions/automerge/automerge.ts +40 -46
- package/src/extensions/automerge/cursor.ts +2 -1
- package/src/extensions/automerge/defs.ts +10 -31
- package/src/extensions/automerge/index.ts +0 -1
- package/src/extensions/automerge/semaphore.ts +23 -20
- package/src/extensions/automerge/update-automerge.ts +2 -2
- package/src/extensions/awareness.ts +40 -46
- package/src/extensions/basic.ts +22 -17
- package/src/extensions/code.ts +24 -18
- package/src/extensions/comments.ts +246 -238
- package/src/extensions/cursor.ts +50 -0
- package/src/extensions/hr.ts +49 -50
- package/src/extensions/image.ts +2 -2
- package/src/extensions/index.ts +2 -1
- package/src/extensions/link.ts +7 -7
- package/src/extensions/markdown/bundle.ts +18 -24
- package/src/extensions/markdown/highlight.ts +0 -8
- package/src/extensions/mention.ts +9 -2
- package/src/extensions/outliner.ts +12 -0
- package/src/extensions/table.ts +2 -2
- package/src/extensions/tasklist.ts +78 -86
- package/src/hooks/awareness-provider.ts +24 -20
- package/src/hooks/defs.ts +42 -0
- package/src/hooks/index.ts +3 -2
- package/src/hooks/useTextEditor.ts +29 -0
- package/src/hooks/useTextModel.ts +47 -59
- package/src/styles/markdown.ts +0 -2
- package/src/themes/default.ts +17 -50
- package/src/util.ts +41 -0
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/index.d.ts +0 -2
- package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
- package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
- package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge.d.ts +0 -3
- package/dist/types/src/hooks/automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs.d.ts +0 -27
- package/dist/types/src/hooks/yjs.d.ts.map +0 -1
- package/dist/types/src/testing/index.d.ts +0 -3
- package/dist/types/src/testing/index.d.ts.map +0 -1
- package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
- package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
- package/dist/types/src/testing/proto/index.d.ts +0 -2
- package/dist/types/src/testing/proto/index.d.ts.map +0 -1
- package/dist/types/src/testing/replicator.d.ts +0 -45
- package/dist/types/src/testing/replicator.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 +0 -5
- package/dist/types/src/util/util.d.ts.map +0 -1
- package/src/components/TextEditor/codemirror.stories.ts +0 -115
- package/src/components/TextEditor/yjs.stories.tsx +0 -56
- package/src/extensions/yjs/cursor.ts +0 -21
- package/src/extensions/yjs/index.ts +0 -5
- package/src/extensions/yjs/yjs.test.ts +0 -35
- package/src/extensions/yjs/yjs.ts +0 -16
- package/src/hooks/automerge.ts +0 -52
- package/src/hooks/yjs.ts +0 -178
- package/src/testing/index.ts +0 -6
- package/src/testing/proto/gen/schema.ts +0 -49
- package/src/testing/proto/index.ts +0 -5
- package/src/testing/proto/schema.proto +0 -15
- package/src/testing/replicator.ts +0 -184
- package/src/util/cursor.ts +0 -29
- package/src/util/index.ts +0 -6
- package/src/util/util.ts +0 -18
|
@@ -4,20 +4,16 @@
|
|
|
4
4
|
// Ref: https://github.com/automerge/automerge-codemirror
|
|
5
5
|
//
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { StateField, type Extension } from '@codemirror/state';
|
|
8
|
+
import { EditorView, ViewPlugin } from '@codemirror/view';
|
|
9
9
|
|
|
10
|
-
import { next as A } from '@dxos/automerge/automerge';
|
|
11
|
-
import {
|
|
10
|
+
import { type Prop, next as A } from '@dxos/automerge/automerge';
|
|
11
|
+
import { invariant } from '@dxos/invariant';
|
|
12
12
|
|
|
13
13
|
import { cursorConverter } from './cursor';
|
|
14
|
-
import { effectType, type IDocHandle, isReconcileTx, type
|
|
14
|
+
import { effectType, type IDocHandle, isReconcileTx, type State } from './defs';
|
|
15
15
|
import { PatchSemaphore } from './semaphore';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
const semaphoreFacet = Facet.define<PatchSemaphore, PatchSemaphore>({
|
|
19
|
-
combine: (values) => values.at(-1)!, // Take last.
|
|
20
|
-
});
|
|
16
|
+
import { Cursor } from '../cursor';
|
|
21
17
|
|
|
22
18
|
export type AutomergeOptions = {
|
|
23
19
|
handle: IDocHandle;
|
|
@@ -25,19 +21,20 @@ export type AutomergeOptions = {
|
|
|
25
21
|
};
|
|
26
22
|
|
|
27
23
|
export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
28
|
-
|
|
29
|
-
const stateField: StateField<Value> = StateField.define({
|
|
24
|
+
const state = StateField.define<State>({
|
|
30
25
|
create: () => ({
|
|
26
|
+
path: path.slice(),
|
|
31
27
|
lastHeads: A.getHeads(handle.docSync()!),
|
|
32
28
|
unreconciledTransactions: [],
|
|
33
|
-
path: path.slice(),
|
|
34
29
|
}),
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
|
|
31
|
+
update: (value, tr) => {
|
|
32
|
+
const result: State = {
|
|
33
|
+
path: path.slice(),
|
|
37
34
|
lastHeads: value.lastHeads,
|
|
38
35
|
unreconciledTransactions: value.unreconciledTransactions.slice(),
|
|
39
|
-
path: path.slice(),
|
|
40
36
|
};
|
|
37
|
+
|
|
41
38
|
let clearUnreconciled = false;
|
|
42
39
|
for (const effect of tr.effects) {
|
|
43
40
|
if (effect.is(effectType)) {
|
|
@@ -45,6 +42,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
45
42
|
clearUnreconciled = true;
|
|
46
43
|
}
|
|
47
44
|
}
|
|
45
|
+
|
|
48
46
|
if (clearUnreconciled) {
|
|
49
47
|
result.unreconciledTransactions = [];
|
|
50
48
|
} else {
|
|
@@ -52,43 +50,39 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
52
50
|
result.unreconciledTransactions.push(tr);
|
|
53
51
|
}
|
|
54
52
|
}
|
|
53
|
+
|
|
55
54
|
return result;
|
|
56
55
|
},
|
|
57
56
|
});
|
|
58
57
|
|
|
59
|
-
const semaphore = new PatchSemaphore(
|
|
58
|
+
const semaphore = new PatchSemaphore(handle, state);
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
return [
|
|
61
|
+
Cursor.converter.of(cursorConverter(handle, path)),
|
|
62
|
+
// Reconcile external updates.
|
|
63
|
+
ViewPlugin.fromClass(
|
|
64
|
+
class {
|
|
65
|
+
constructor(private readonly _view: EditorView) {
|
|
66
|
+
invariant(this._view);
|
|
67
|
+
handle.addListener('change', this._handleChange.bind(this));
|
|
68
|
+
}
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// TODO(burdon): This is a hack to ensure that the view is updated after the transaction is applied.
|
|
70
|
-
queueMicrotask(() => {
|
|
71
|
-
this._view.state.facet(semaphoreFacet).reconcile(handle, this._view);
|
|
72
|
-
});
|
|
70
|
+
destroy() {
|
|
71
|
+
handle.removeListener('change', this._handleChange.bind(this));
|
|
73
72
|
}
|
|
74
|
-
}
|
|
75
73
|
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
_handleChange() {
|
|
75
|
+
semaphore.reconcile(this._view);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
),
|
|
79
|
+
// Reconcile local updates.
|
|
80
|
+
EditorView.updateListener.of(({ view, changes }) => {
|
|
81
|
+
if (!changes.empty) {
|
|
82
|
+
// TODO(burdon): Loses cursor position if auto closing brackets. Call explicitly?
|
|
83
|
+
semaphore.reconcile(view);
|
|
78
84
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
};
|
|
83
|
-
},
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
extension: [
|
|
88
|
-
CursorConverter.of(cursorConverter(handle, path)),
|
|
89
|
-
semaphoreFacet.of(semaphore),
|
|
90
|
-
stateField,
|
|
91
|
-
viewPlugin,
|
|
92
|
-
],
|
|
93
|
-
};
|
|
85
|
+
}),
|
|
86
|
+
state,
|
|
87
|
+
];
|
|
94
88
|
};
|
|
@@ -8,7 +8,7 @@ import type { Prop } from '@dxos/automerge/automerge';
|
|
|
8
8
|
import { next as automerge } from '@dxos/automerge/automerge';
|
|
9
9
|
|
|
10
10
|
import { type IDocHandle } from './defs';
|
|
11
|
-
import { type CursorConverter } from '
|
|
11
|
+
import { type CursorConverter } from '../cursor';
|
|
12
12
|
|
|
13
13
|
export const cursorConverter = (handle: IDocHandle, path: Prop[]): CursorConverter => ({
|
|
14
14
|
// TODO(burdon): Handle assoc to associate with a previous character.
|
|
@@ -26,6 +26,7 @@ export const cursorConverter = (handle: IDocHandle, path: Prop[]): CursorConvert
|
|
|
26
26
|
// NOTE: Slice is needed because getCursor mutates the array.
|
|
27
27
|
return automerge.getCursor(doc, path.slice(), pos);
|
|
28
28
|
},
|
|
29
|
+
|
|
29
30
|
fromCursor: (cursor) => {
|
|
30
31
|
if (cursor === '') {
|
|
31
32
|
return 0;
|
|
@@ -1,57 +1,36 @@
|
|
|
1
1
|
//
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
|
+
// Copyright 2024 Automerge
|
|
3
4
|
// Ref: https://github.com/automerge/automerge-codemirror
|
|
4
5
|
//
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
-
Annotation,
|
|
8
|
-
StateEffect,
|
|
9
|
-
type StateField,
|
|
10
|
-
type EditorState,
|
|
11
|
-
type Transaction,
|
|
12
|
-
type TransactionSpec,
|
|
13
|
-
} from '@codemirror/state';
|
|
7
|
+
import { Annotation, StateEffect, type StateField, type EditorState, type Transaction } from '@codemirror/state';
|
|
14
8
|
|
|
15
9
|
import { type ChangeFn, type ChangeOptions, type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
|
|
16
10
|
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
// TODO(burdon): Rename.
|
|
12
|
+
export type State = {
|
|
19
13
|
path: Prop[];
|
|
14
|
+
lastHeads: Heads;
|
|
20
15
|
unreconciledTransactions: Transaction[];
|
|
21
16
|
};
|
|
22
17
|
|
|
23
|
-
type UpdateHeads = {
|
|
18
|
+
export type UpdateHeads = {
|
|
24
19
|
newHeads: Heads;
|
|
25
20
|
};
|
|
26
21
|
|
|
27
22
|
export const effectType = StateEffect.define<UpdateHeads>({});
|
|
28
23
|
|
|
29
|
-
export const
|
|
24
|
+
export const reconcileAnnotationType = Annotation.define<unknown>();
|
|
30
25
|
|
|
31
|
-
export const
|
|
26
|
+
export const getPath = (state: EditorState, field: StateField<State>): Prop[] => state.field(field).path;
|
|
32
27
|
|
|
33
|
-
export const
|
|
28
|
+
export const getLastHeads = (state: EditorState, field: StateField<State>): Heads => state.field(field).lastHeads;
|
|
34
29
|
|
|
35
|
-
export const
|
|
30
|
+
export const updateHeads = (newHeads: Heads): StateEffect<UpdateHeads> => effectType.of({ newHeads });
|
|
36
31
|
|
|
37
32
|
export const isReconcileTx = (tr: Transaction): boolean => !!tr.annotation(reconcileAnnotationType);
|
|
38
33
|
|
|
39
|
-
export const makeReconcile = (tr: TransactionSpec) => {
|
|
40
|
-
if (tr.annotations != null) {
|
|
41
|
-
if (tr.annotations instanceof Array) {
|
|
42
|
-
tr.annotations = [...tr.annotations, reconcileAnnotationType.of({})];
|
|
43
|
-
} else {
|
|
44
|
-
tr.annotations = [tr.annotations, reconcileAnnotationType.of({})];
|
|
45
|
-
}
|
|
46
|
-
} else {
|
|
47
|
-
tr.annotations = [reconcileAnnotationType.of({})];
|
|
48
|
-
}
|
|
49
|
-
// return {
|
|
50
|
-
// ...tr,
|
|
51
|
-
// annotations: reconcileAnnotationType.of({})
|
|
52
|
-
// }
|
|
53
|
-
};
|
|
54
|
-
|
|
55
34
|
export type IDocHandle<T = any> = {
|
|
56
35
|
docSync(): Doc<T> | undefined;
|
|
57
36
|
change(callback: ChangeFn<T>, options?: ChangeOptions<T>): void;
|
|
@@ -16,37 +16,38 @@ import {
|
|
|
16
16
|
reconcileAnnotationType,
|
|
17
17
|
updateHeads,
|
|
18
18
|
type IDocHandle,
|
|
19
|
-
type
|
|
19
|
+
type State,
|
|
20
20
|
} from './defs';
|
|
21
21
|
import { updateAutomerge } from './update-automerge';
|
|
22
22
|
import { updateCodeMirror } from './update-codemirror';
|
|
23
23
|
|
|
24
|
-
type Doc<T> = automerge.Doc<T>;
|
|
25
|
-
type Heads = automerge.Heads;
|
|
26
|
-
|
|
27
|
-
type ChangeFn = (atHeads: Heads, change: (doc: Doc<unknown>) => void) => Heads | undefined;
|
|
28
|
-
|
|
29
24
|
/**
|
|
30
|
-
*
|
|
25
|
+
* Implements three-way merge (on each mutation/keystroke).
|
|
31
26
|
*/
|
|
32
27
|
export class PatchSemaphore {
|
|
33
|
-
_inReconcile = false;
|
|
34
|
-
_queue: Array<ChangeFn> = [];
|
|
28
|
+
private _inReconcile = false;
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
// prettier-ignore
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly _handle: IDocHandle,
|
|
33
|
+
private readonly _state: StateField<State>
|
|
34
|
+
) {}
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
// NOTE: Cannot destruct view.state.
|
|
37
|
+
reconcile(view: EditorView) {
|
|
39
38
|
if (this._inReconcile) {
|
|
40
39
|
return;
|
|
41
40
|
}
|
|
42
41
|
this._inReconcile = true;
|
|
43
42
|
|
|
44
|
-
const path = getPath(view.state, this.
|
|
45
|
-
|
|
43
|
+
const path = getPath(view.state, this._state);
|
|
44
|
+
|
|
45
|
+
// Get the heads before the unreconciled transactions are applied.
|
|
46
|
+
const oldHeads = getLastHeads(view.state, this._state);
|
|
46
47
|
let selection = view.state.selection;
|
|
47
48
|
|
|
48
49
|
// First undo all the unreconciled transactions.
|
|
49
|
-
const transactions = view.state.field(this.
|
|
50
|
+
const transactions = view.state.field(this._state).unreconciledTransactions.filter((tx) => !isReconcileTx(tx));
|
|
50
51
|
const toInvert = transactions.slice().reverse();
|
|
51
52
|
for (const tx of toInvert) {
|
|
52
53
|
const inverted = tx.changes.invert(tx.startState.doc);
|
|
@@ -58,23 +59,25 @@ export class PatchSemaphore {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
// Apply the unreconciled transactions to the document.
|
|
61
|
-
let newHeads = updateAutomerge(this._field, handle, transactions, view.state);
|
|
62
|
-
|
|
63
62
|
// NOTE: null and undefined each come from automerge and repo respectively.
|
|
63
|
+
let newHeads = updateAutomerge(this._state, this._handle, transactions, view.state);
|
|
64
64
|
if (newHeads === null || newHeads === undefined) {
|
|
65
|
-
// TODO:
|
|
66
|
-
newHeads = automerge.getHeads(
|
|
65
|
+
// TODO(alexjg): this is the call that's resetting the editor state on click.
|
|
66
|
+
newHeads = automerge.getHeads(this._handle.docSync()!);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// Now get the diff between the updated state of the document and the heads and apply that to the codemirror doc.
|
|
70
|
-
const diff = automerge.equals(oldHeads, newHeads)
|
|
70
|
+
const diff = automerge.equals(oldHeads, newHeads)
|
|
71
|
+
? []
|
|
72
|
+
: automerge.diff(this._handle.docSync()!, oldHeads, newHeads);
|
|
71
73
|
updateCodeMirror(view, selection, path, diff);
|
|
72
74
|
|
|
75
|
+
// Update automerge state.
|
|
73
76
|
view.dispatch({
|
|
74
77
|
effects: updateHeads(newHeads),
|
|
75
78
|
annotations: reconcileAnnotationType.of({}),
|
|
76
79
|
});
|
|
77
80
|
|
|
78
81
|
this._inReconcile = false;
|
|
79
|
-
}
|
|
82
|
+
}
|
|
80
83
|
}
|
|
@@ -8,10 +8,10 @@ import { type EditorState, type StateField, type Text, type Transaction } from '
|
|
|
8
8
|
|
|
9
9
|
import { next as am, type Heads } from '@dxos/automerge/automerge';
|
|
10
10
|
|
|
11
|
-
import { type IDocHandle, type
|
|
11
|
+
import { type IDocHandle, type State } from './defs';
|
|
12
12
|
|
|
13
13
|
export const updateAutomerge = (
|
|
14
|
-
field: StateField<
|
|
14
|
+
field: StateField<State>,
|
|
15
15
|
handle: IDocHandle,
|
|
16
16
|
transactions: Transaction[],
|
|
17
17
|
state: EditorState,
|
|
@@ -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>;
|
|
@@ -24,8 +24,8 @@ export interface AwarenessProvider {
|
|
|
24
24
|
open(): void;
|
|
25
25
|
close(): void;
|
|
26
26
|
|
|
27
|
-
update(position: AwarenessPosition | undefined): void;
|
|
28
27
|
getRemoteStates(): AwarenessState[];
|
|
28
|
+
update(position: AwarenessPosition | undefined): void;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const dummyProvider: AwarenessProvider = {
|
|
@@ -34,8 +34,8 @@ const dummyProvider: AwarenessProvider = {
|
|
|
34
34
|
open: () => {},
|
|
35
35
|
close: () => {},
|
|
36
36
|
|
|
37
|
-
update: () => {},
|
|
38
37
|
getRemoteStates: () => [],
|
|
38
|
+
update: () => {},
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
export const AwarenessProvider = Facet.define<AwarenessProvider, AwarenessProvider>({
|
|
@@ -66,7 +66,6 @@ export type AwarenessState = {
|
|
|
66
66
|
/**
|
|
67
67
|
* Extension provides presence information about other peers.
|
|
68
68
|
*/
|
|
69
|
-
// TODO(burdon): Why provide default?
|
|
70
69
|
export const awareness = (provider = dummyProvider): Extension => {
|
|
71
70
|
return [
|
|
72
71
|
AwarenessProvider.of(provider),
|
|
@@ -91,7 +90,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
91
90
|
private _lastHead?: number = undefined;
|
|
92
91
|
|
|
93
92
|
constructor(view: EditorView) {
|
|
94
|
-
this._cursorConverter = view.state.facet(
|
|
93
|
+
this._cursorConverter = view.state.facet(Cursor.converter);
|
|
95
94
|
this._provider = view.state.facet(AwarenessProvider);
|
|
96
95
|
this._provider.open();
|
|
97
96
|
this._provider.remoteStateChange.on(this._ctx, () => {
|
|
@@ -156,7 +155,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
156
155
|
to: end,
|
|
157
156
|
value: Decoration.mark({
|
|
158
157
|
attributes: { style: `background-color: ${lightColor}` },
|
|
159
|
-
class: 'cm-
|
|
158
|
+
class: 'cm-collab-selection',
|
|
160
159
|
}),
|
|
161
160
|
});
|
|
162
161
|
} else {
|
|
@@ -166,7 +165,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
166
165
|
to: startLine.from + startLine.length,
|
|
167
166
|
value: Decoration.mark({
|
|
168
167
|
attributes: { style: `background-color: ${color}` },
|
|
169
|
-
class: 'cm-
|
|
168
|
+
class: 'cm-collab-selection',
|
|
170
169
|
}),
|
|
171
170
|
});
|
|
172
171
|
|
|
@@ -176,7 +175,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
176
175
|
to: end,
|
|
177
176
|
value: Decoration.mark({
|
|
178
177
|
attributes: { style: `background-color: ${color}` },
|
|
179
|
-
class: 'cm-
|
|
178
|
+
class: 'cm-collab-selection',
|
|
180
179
|
}),
|
|
181
180
|
});
|
|
182
181
|
|
|
@@ -186,7 +185,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
186
185
|
from: linePos,
|
|
187
186
|
to: linePos,
|
|
188
187
|
value: Decoration.line({
|
|
189
|
-
attributes: { style: `background-color: ${color}`, class: 'cm-
|
|
188
|
+
attributes: { style: `background-color: ${color}`, class: 'cm-collab-selectionLine' },
|
|
190
189
|
}),
|
|
191
190
|
});
|
|
192
191
|
}
|
|
@@ -208,24 +207,22 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
208
207
|
}
|
|
209
208
|
|
|
210
209
|
class RemoteCaretWidget extends WidgetType {
|
|
211
|
-
constructor(
|
|
210
|
+
constructor(private readonly _name: string, private readonly _color: string) {
|
|
212
211
|
super();
|
|
213
|
-
this.name = name;
|
|
214
|
-
this.color = color;
|
|
215
212
|
}
|
|
216
213
|
|
|
217
|
-
toDOM(): HTMLElement {
|
|
214
|
+
override toDOM(): HTMLElement {
|
|
218
215
|
const span = document.createElement('span');
|
|
219
|
-
span.className = 'cm-
|
|
220
|
-
span.style.backgroundColor = this.
|
|
221
|
-
span.style.borderColor = this.
|
|
216
|
+
span.className = 'cm-collab-selectionCaret';
|
|
217
|
+
span.style.backgroundColor = this._color;
|
|
218
|
+
span.style.borderColor = this._color;
|
|
222
219
|
|
|
223
220
|
const dot = document.createElement('div');
|
|
224
|
-
dot.className = 'cm-
|
|
221
|
+
dot.className = 'cm-collab-selectionCaretDot';
|
|
225
222
|
|
|
226
223
|
const info = document.createElement('div');
|
|
227
|
-
info.className = 'cm-
|
|
228
|
-
info.innerText = this.
|
|
224
|
+
info.className = 'cm-collab-selectionInfo';
|
|
225
|
+
info.innerText = this._name;
|
|
229
226
|
|
|
230
227
|
span.appendChild(document.createTextNode('\u2060'));
|
|
231
228
|
span.appendChild(dot);
|
|
@@ -236,18 +233,14 @@ class RemoteCaretWidget extends WidgetType {
|
|
|
236
233
|
return span;
|
|
237
234
|
}
|
|
238
235
|
|
|
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
236
|
override updateDOM() {
|
|
248
237
|
return false;
|
|
249
238
|
}
|
|
250
239
|
|
|
240
|
+
override eq(widget: this) {
|
|
241
|
+
return widget._color === this._color;
|
|
242
|
+
}
|
|
243
|
+
|
|
251
244
|
override get estimatedHeight() {
|
|
252
245
|
return -1;
|
|
253
246
|
}
|
|
@@ -257,14 +250,13 @@ class RemoteCaretWidget extends WidgetType {
|
|
|
257
250
|
}
|
|
258
251
|
}
|
|
259
252
|
|
|
260
|
-
// TODO(burdon): Rename prefix (y for yjs?)
|
|
261
253
|
const styles = EditorView.baseTheme({
|
|
262
|
-
'.cm-
|
|
263
|
-
'.cm-
|
|
254
|
+
'.cm-collab-selection': {},
|
|
255
|
+
'.cm-collab-selectionLine': {
|
|
264
256
|
padding: 0,
|
|
265
257
|
margin: '0px 2px 0px 4px',
|
|
266
258
|
},
|
|
267
|
-
'.cm-
|
|
259
|
+
'.cm-collab-selectionCaret': {
|
|
268
260
|
position: 'relative',
|
|
269
261
|
borderLeft: '1px solid black',
|
|
270
262
|
borderRight: '1px solid black',
|
|
@@ -272,44 +264,46 @@ const styles = EditorView.baseTheme({
|
|
|
272
264
|
marginRight: '-1px',
|
|
273
265
|
boxSizing: 'border-box',
|
|
274
266
|
display: 'inline',
|
|
267
|
+
cursor: 'pointer',
|
|
275
268
|
},
|
|
276
|
-
'.cm-
|
|
269
|
+
'.cm-collab-selectionCaretDot': {
|
|
277
270
|
borderRadius: '50%',
|
|
278
271
|
position: 'absolute',
|
|
279
|
-
width: '.
|
|
280
|
-
height: '.
|
|
281
|
-
top: '-.
|
|
282
|
-
left: '-.
|
|
272
|
+
width: '.5em',
|
|
273
|
+
height: '.5em',
|
|
274
|
+
top: '-.25em',
|
|
275
|
+
left: '-.25em',
|
|
283
276
|
backgroundColor: 'inherit',
|
|
284
277
|
transition: 'transform .3s ease-in-out',
|
|
285
278
|
boxSizing: 'border-box',
|
|
286
279
|
},
|
|
287
|
-
'.cm-
|
|
288
|
-
transformOrigin: 'bottom center',
|
|
280
|
+
'.cm-collab-selectionCaret:hover > .cm-collab-selectionCaretDot': {
|
|
289
281
|
transform: 'scale(0)',
|
|
282
|
+
transformOrigin: 'center',
|
|
290
283
|
},
|
|
291
|
-
'.cm-
|
|
284
|
+
'.cm-collab-selectionInfo': {
|
|
292
285
|
position: 'absolute',
|
|
293
|
-
|
|
294
|
-
|
|
286
|
+
transform: 'translate(-50%, 0)',
|
|
287
|
+
top: '-20px',
|
|
288
|
+
left: 0,
|
|
295
289
|
fontSize: '.75em',
|
|
296
|
-
fontFamily: 'serif',
|
|
290
|
+
fontFamily: 'sans-serif',
|
|
297
291
|
fontStyle: 'normal',
|
|
298
292
|
fontWeight: 'normal',
|
|
299
293
|
lineHeight: 'normal',
|
|
300
294
|
userSelect: 'none',
|
|
301
295
|
color: 'white',
|
|
302
|
-
|
|
303
|
-
paddingRight: '2px',
|
|
296
|
+
padding: '2px',
|
|
304
297
|
zIndex: 101,
|
|
305
298
|
transition: 'opacity .3s ease-in-out',
|
|
306
299
|
backgroundColor: 'inherit',
|
|
307
|
-
|
|
300
|
+
borderRadius: '2px',
|
|
301
|
+
// These should be separate.
|
|
308
302
|
opacity: 0,
|
|
309
303
|
transitionDelay: '0s',
|
|
310
304
|
whiteSpace: 'nowrap',
|
|
311
305
|
},
|
|
312
|
-
'.cm-
|
|
306
|
+
'.cm-collab-selectionCaret:hover > .cm-collab-selectionInfo': {
|
|
313
307
|
opacity: 1,
|
|
314
308
|
transitionDelay: '0s',
|
|
315
309
|
},
|
package/src/extensions/basic.ts
CHANGED
|
@@ -3,30 +3,35 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { closeBrackets } from '@codemirror/autocomplete';
|
|
6
|
+
import { history } from '@codemirror/commands';
|
|
6
7
|
import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
7
8
|
import { type Extension } from '@codemirror/state';
|
|
8
9
|
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
9
|
-
import { drawSelection, EditorView, placeholder } from '@codemirror/view';
|
|
10
|
+
import { drawSelection, EditorView, highlightActiveLine, placeholder } from '@codemirror/view';
|
|
10
11
|
|
|
11
|
-
import type
|
|
12
|
+
import { type ThemeMode } from '@dxos/react-ui';
|
|
13
|
+
|
|
14
|
+
import { type TextEditorProps } from '../components';
|
|
12
15
|
|
|
13
16
|
export type BasicBundleOptions = {
|
|
14
|
-
readonly?: boolean;
|
|
15
17
|
themeMode?: ThemeMode;
|
|
16
|
-
|
|
17
|
-
};
|
|
18
|
+
} & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
|
|
18
19
|
|
|
19
|
-
export const basicBundle = ({
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
export const basicBundle = ({
|
|
21
|
+
themeMode,
|
|
22
|
+
placeholder: _placeholder,
|
|
23
|
+
lineWrapping = true,
|
|
24
|
+
}: BasicBundleOptions): Extension[] => [
|
|
25
|
+
lineWrapping ? EditorView.lineWrapping : [],
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// https://codemirror.net/docs/ref/#codemirror.minimalSetup
|
|
28
|
+
bracketMatching(),
|
|
29
|
+
closeBrackets(),
|
|
30
|
+
drawSelection(),
|
|
31
|
+
highlightActiveLine(),
|
|
32
|
+
history(),
|
|
33
|
+
_placeholder ? placeholder(_placeholder) : [],
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
// https://github.com/codemirror/theme-one-dark
|
|
36
|
+
themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
|
|
37
|
+
];
|
package/src/extensions/code.ts
CHANGED
|
@@ -53,22 +53,20 @@ const getLineRange = (lines: BlockInfo[], from: number, to: number) => {
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
export const code = () => {
|
|
56
|
-
const
|
|
56
|
+
const buildDecorations = (view: EditorView) => {
|
|
57
57
|
const decorations: Range<Decoration>[] = [];
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
);
|
|
71
|
-
}
|
|
58
|
+
const text = view.state.doc.sliceString(0);
|
|
59
|
+
const matches = text.matchAll(CODE_REGEX);
|
|
60
|
+
const blocks = view.viewportLineBlocks;
|
|
61
|
+
for (const match of matches) {
|
|
62
|
+
const range = getLineRange(blocks, match.index!, match.index! + match[0].length);
|
|
63
|
+
for (let i = range[0]; i <= range[1]; i++) {
|
|
64
|
+
const block = blocks[i];
|
|
65
|
+
decorations.push(
|
|
66
|
+
Decoration.line({
|
|
67
|
+
class: mx('cm-codeblock', i === range[0] && 'cm-codeblock-first', i === range[1] && 'cm-codeblock-last'),
|
|
68
|
+
}).range(block.from),
|
|
69
|
+
);
|
|
72
70
|
}
|
|
73
71
|
}
|
|
74
72
|
|
|
@@ -78,14 +76,22 @@ export const code = () => {
|
|
|
78
76
|
return [
|
|
79
77
|
ViewPlugin.fromClass(
|
|
80
78
|
class {
|
|
79
|
+
hasFocus = false;
|
|
81
80
|
decorations: DecorationSet;
|
|
82
81
|
constructor(view: EditorView) {
|
|
83
|
-
this.decorations =
|
|
82
|
+
this.decorations = buildDecorations(view);
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
update(update: ViewUpdate) {
|
|
87
|
-
if (
|
|
88
|
-
|
|
86
|
+
if (
|
|
87
|
+
// TODO(burdon): Generalize for other extensions.
|
|
88
|
+
this.hasFocus !== update.view.hasFocus ||
|
|
89
|
+
update.startState.readOnly !== update.view.state.readOnly ||
|
|
90
|
+
update.docChanged ||
|
|
91
|
+
update.viewportChanged
|
|
92
|
+
) {
|
|
93
|
+
this.hasFocus = update.view.hasFocus;
|
|
94
|
+
this.decorations = buildDecorations(update.view);
|
|
89
95
|
}
|
|
90
96
|
}
|
|
91
97
|
},
|