@dxos/react-ui-editor 0.3.11-main.2c83f41 → 0.3.11-main.2e1a8e1
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 +748 -753
- 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 +16 -13
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +19 -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/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 +2 -0
- 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 +31 -18
- 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 -0
- 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/extensions/yjs/cursor.d.ts +1 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge.d.ts +2 -1
- package/dist/types/src/hooks/automerge.d.ts.map +1 -1
- package/dist/types/src/hooks/defs.d.ts +28 -0
- package/dist/types/src/hooks/defs.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +2 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- 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/hooks/yjs.d.ts +2 -1
- package/dist/types/src/hooks/yjs.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -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/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 +32 -31
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +77 -79
- package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
- package/src/components/TextEditor/TextEditor.tsx +113 -92
- package/src/components/TextEditor/comments.stories.tsx +357 -0
- package/src/extensions/autocomplete.ts +6 -3
- package/src/extensions/automerge/automerge.ts +24 -49
- 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 +45 -34
- package/src/extensions/basic.ts +22 -17
- package/src/extensions/code.ts +24 -18
- package/src/extensions/comments.ts +242 -234
- 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 -0
- 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/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/automerge.ts +2 -1
- package/src/hooks/defs.ts +47 -0
- package/src/hooks/index.ts +3 -1
- package/src/hooks/useTextModel.ts +3 -49
- package/src/hooks/yjs.ts +2 -23
- package/src/index.ts +1 -0
- package/src/styles/markdown.ts +0 -2
- package/src/themes/default.ts +17 -50
- package/src/util.ts +41 -0
- 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/util/cursor.ts +0 -29
- package/src/util/index.ts +0 -6
- package/src/util/util.ts +0 -18
|
@@ -35,10 +35,13 @@ export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
|
|
|
35
35
|
// https://codemirror.net/examples/autocompletion
|
|
36
36
|
// https://codemirror.net/docs/ref/#autocomplete.autocompletion
|
|
37
37
|
autocompletion({
|
|
38
|
-
|
|
38
|
+
activateOnTyping: true,
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
|
|
40
|
+
// closeOnBlur: false,
|
|
41
|
+
// defaultKeymap: false,
|
|
42
|
+
|
|
43
|
+
// TODO(burdon): Styles/fragments.
|
|
44
|
+
tooltipClass: () => 'shadow rounded',
|
|
42
45
|
}),
|
|
43
46
|
|
|
44
47
|
// TODO(burdon): Option to create new page?
|
|
@@ -4,20 +4,15 @@
|
|
|
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 } from '@codemirror/view';
|
|
9
9
|
|
|
10
|
-
import { next as A } from '@dxos/automerge/automerge';
|
|
11
|
-
import { type Prop } from '@dxos/automerge/automerge';
|
|
10
|
+
import { type Prop, next as A } from '@dxos/automerge/automerge';
|
|
12
11
|
|
|
13
12
|
import { cursorConverter } from './cursor';
|
|
14
|
-
import { effectType, type IDocHandle, isReconcileTx, type
|
|
13
|
+
import { effectType, type IDocHandle, isReconcileTx, type State } from './defs';
|
|
15
14
|
import { PatchSemaphore } from './semaphore';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
const semaphoreFacet = Facet.define<PatchSemaphore, PatchSemaphore>({
|
|
19
|
-
combine: (values) => values.at(-1)!, // Take last.
|
|
20
|
-
});
|
|
15
|
+
import { Cursor } from '../cursor';
|
|
21
16
|
|
|
22
17
|
export type AutomergeOptions = {
|
|
23
18
|
handle: IDocHandle;
|
|
@@ -25,19 +20,20 @@ export type AutomergeOptions = {
|
|
|
25
20
|
};
|
|
26
21
|
|
|
27
22
|
export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
28
|
-
|
|
29
|
-
const stateField: StateField<Value> = StateField.define({
|
|
23
|
+
const state = StateField.define<State>({
|
|
30
24
|
create: () => ({
|
|
25
|
+
path: path.slice(),
|
|
31
26
|
lastHeads: A.getHeads(handle.docSync()!),
|
|
32
27
|
unreconciledTransactions: [],
|
|
33
|
-
path: path.slice(),
|
|
34
28
|
}),
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
|
|
30
|
+
update: (value, tr) => {
|
|
31
|
+
const result: State = {
|
|
32
|
+
path: path.slice(),
|
|
37
33
|
lastHeads: value.lastHeads,
|
|
38
34
|
unreconciledTransactions: value.unreconciledTransactions.slice(),
|
|
39
|
-
path: path.slice(),
|
|
40
35
|
};
|
|
36
|
+
|
|
41
37
|
let clearUnreconciled = false;
|
|
42
38
|
for (const effect of tr.effects) {
|
|
43
39
|
if (effect.is(effectType)) {
|
|
@@ -45,6 +41,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
45
41
|
clearUnreconciled = true;
|
|
46
42
|
}
|
|
47
43
|
}
|
|
44
|
+
|
|
48
45
|
if (clearUnreconciled) {
|
|
49
46
|
result.unreconciledTransactions = [];
|
|
50
47
|
} else {
|
|
@@ -52,43 +49,21 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
52
49
|
result.unreconciledTransactions.push(tr);
|
|
53
50
|
}
|
|
54
51
|
}
|
|
52
|
+
|
|
55
53
|
return result;
|
|
56
54
|
},
|
|
57
55
|
});
|
|
58
56
|
|
|
59
|
-
const semaphore = new PatchSemaphore(
|
|
57
|
+
const semaphore = new PatchSemaphore(handle, state);
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
return [
|
|
60
|
+
Cursor.converter.of(cursorConverter(handle, path)),
|
|
61
|
+
EditorView.updateListener.of(({ view, changes }) => {
|
|
62
|
+
if (!changes.empty) {
|
|
63
|
+
// TODO(burdon): Loses cursor position if auto closing brackets. Call explicitly?
|
|
64
|
+
semaphore.reconcile(view);
|
|
65
65
|
}
|
|
66
|
-
|
|
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
|
-
});
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
destroy() {
|
|
77
|
-
handle.addListener('change', this._handleChange);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private _handleChange = () => {
|
|
81
|
-
this._view.state.facet(semaphoreFacet).reconcile(handle, this._view);
|
|
82
|
-
};
|
|
83
|
-
},
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
extension: [
|
|
88
|
-
CursorConverter.of(cursorConverter(handle, path)),
|
|
89
|
-
semaphoreFacet.of(semaphore),
|
|
90
|
-
stateField,
|
|
91
|
-
viewPlugin,
|
|
92
|
-
],
|
|
93
|
-
};
|
|
66
|
+
}),
|
|
67
|
+
state,
|
|
68
|
+
];
|
|
94
69
|
};
|
|
@@ -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,
|
|
@@ -12,11 +12,16 @@ import {
|
|
|
12
12
|
type ViewUpdate,
|
|
13
13
|
WidgetType,
|
|
14
14
|
} from '@codemirror/view';
|
|
15
|
+
import { useEffect } from 'react';
|
|
15
16
|
|
|
16
17
|
import { Event } from '@dxos/async';
|
|
17
18
|
import { Context } from '@dxos/context';
|
|
19
|
+
import { generateName } from '@dxos/display-name';
|
|
20
|
+
import { useThemeContext } from '@dxos/react-ui';
|
|
21
|
+
import { getColorForValue } from '@dxos/react-ui-theme';
|
|
18
22
|
|
|
19
|
-
import { CursorConverter } from '
|
|
23
|
+
import { Cursor, type CursorConverter } from './cursor';
|
|
24
|
+
import { type EditorModel } from '../hooks';
|
|
20
25
|
|
|
21
26
|
export interface AwarenessProvider {
|
|
22
27
|
remoteStateChange: Event<void>;
|
|
@@ -66,7 +71,6 @@ export type AwarenessState = {
|
|
|
66
71
|
/**
|
|
67
72
|
* Extension provides presence information about other peers.
|
|
68
73
|
*/
|
|
69
|
-
// TODO(burdon): Why provide default?
|
|
70
74
|
export const awareness = (provider = dummyProvider): Extension => {
|
|
71
75
|
return [
|
|
72
76
|
AwarenessProvider.of(provider),
|
|
@@ -77,6 +81,19 @@ export const awareness = (provider = dummyProvider): Extension => {
|
|
|
77
81
|
];
|
|
78
82
|
};
|
|
79
83
|
|
|
84
|
+
export const useAwareness = ({ awareness, peer }: EditorModel) => {
|
|
85
|
+
const { themeMode } = useThemeContext();
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (awareness && peer) {
|
|
88
|
+
awareness.setLocalStateField('user', {
|
|
89
|
+
name: peer.name ?? generateName(peer.id),
|
|
90
|
+
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
91
|
+
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}, [awareness, peer, themeMode]);
|
|
95
|
+
};
|
|
96
|
+
|
|
80
97
|
/**
|
|
81
98
|
* Generates selection decorations from remote peers.
|
|
82
99
|
*/
|
|
@@ -91,7 +108,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
91
108
|
private _lastHead?: number = undefined;
|
|
92
109
|
|
|
93
110
|
constructor(view: EditorView) {
|
|
94
|
-
this._cursorConverter = view.state.facet(
|
|
111
|
+
this._cursorConverter = view.state.facet(Cursor.converter);
|
|
95
112
|
this._provider = view.state.facet(AwarenessProvider);
|
|
96
113
|
this._provider.open();
|
|
97
114
|
this._provider.remoteStateChange.on(this._ctx, () => {
|
|
@@ -156,7 +173,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
156
173
|
to: end,
|
|
157
174
|
value: Decoration.mark({
|
|
158
175
|
attributes: { style: `background-color: ${lightColor}` },
|
|
159
|
-
class: 'cm-
|
|
176
|
+
class: 'cm-collab-selection',
|
|
160
177
|
}),
|
|
161
178
|
});
|
|
162
179
|
} else {
|
|
@@ -166,7 +183,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
166
183
|
to: startLine.from + startLine.length,
|
|
167
184
|
value: Decoration.mark({
|
|
168
185
|
attributes: { style: `background-color: ${color}` },
|
|
169
|
-
class: 'cm-
|
|
186
|
+
class: 'cm-collab-selection',
|
|
170
187
|
}),
|
|
171
188
|
});
|
|
172
189
|
|
|
@@ -176,7 +193,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
176
193
|
to: end,
|
|
177
194
|
value: Decoration.mark({
|
|
178
195
|
attributes: { style: `background-color: ${color}` },
|
|
179
|
-
class: 'cm-
|
|
196
|
+
class: 'cm-collab-selection',
|
|
180
197
|
}),
|
|
181
198
|
});
|
|
182
199
|
|
|
@@ -186,7 +203,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
186
203
|
from: linePos,
|
|
187
204
|
to: linePos,
|
|
188
205
|
value: Decoration.line({
|
|
189
|
-
attributes: { style: `background-color: ${color}`, class: 'cm-
|
|
206
|
+
attributes: { style: `background-color: ${color}`, class: 'cm-collab-selectionLine' },
|
|
190
207
|
}),
|
|
191
208
|
});
|
|
192
209
|
}
|
|
@@ -208,24 +225,22 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
208
225
|
}
|
|
209
226
|
|
|
210
227
|
class RemoteCaretWidget extends WidgetType {
|
|
211
|
-
constructor(
|
|
228
|
+
constructor(private readonly _name: string, private readonly _color: string) {
|
|
212
229
|
super();
|
|
213
|
-
this.name = name;
|
|
214
|
-
this.color = color;
|
|
215
230
|
}
|
|
216
231
|
|
|
217
|
-
toDOM(): HTMLElement {
|
|
232
|
+
override toDOM(): HTMLElement {
|
|
218
233
|
const span = document.createElement('span');
|
|
219
|
-
span.className = 'cm-
|
|
220
|
-
span.style.backgroundColor = this.
|
|
221
|
-
span.style.borderColor = this.
|
|
234
|
+
span.className = 'cm-collab-selectionCaret';
|
|
235
|
+
span.style.backgroundColor = this._color;
|
|
236
|
+
span.style.borderColor = this._color;
|
|
222
237
|
|
|
223
238
|
const dot = document.createElement('div');
|
|
224
|
-
dot.className = 'cm-
|
|
239
|
+
dot.className = 'cm-collab-selectionCaretDot';
|
|
225
240
|
|
|
226
241
|
const info = document.createElement('div');
|
|
227
|
-
info.className = 'cm-
|
|
228
|
-
info.innerText = this.
|
|
242
|
+
info.className = 'cm-collab-selectionInfo';
|
|
243
|
+
info.innerText = this._name;
|
|
229
244
|
|
|
230
245
|
span.appendChild(document.createTextNode('\u2060'));
|
|
231
246
|
span.appendChild(dot);
|
|
@@ -236,18 +251,14 @@ class RemoteCaretWidget extends WidgetType {
|
|
|
236
251
|
return span;
|
|
237
252
|
}
|
|
238
253
|
|
|
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
254
|
override updateDOM() {
|
|
248
255
|
return false;
|
|
249
256
|
}
|
|
250
257
|
|
|
258
|
+
override eq(widget: this) {
|
|
259
|
+
return widget._color === this._color;
|
|
260
|
+
}
|
|
261
|
+
|
|
251
262
|
override get estimatedHeight() {
|
|
252
263
|
return -1;
|
|
253
264
|
}
|
|
@@ -257,14 +268,14 @@ class RemoteCaretWidget extends WidgetType {
|
|
|
257
268
|
}
|
|
258
269
|
}
|
|
259
270
|
|
|
260
|
-
// TODO(burdon):
|
|
271
|
+
// TODO(burdon): Mixed case?
|
|
261
272
|
const styles = EditorView.baseTheme({
|
|
262
|
-
'.cm-
|
|
263
|
-
'.cm-
|
|
273
|
+
'.cm-collab-selection': {},
|
|
274
|
+
'.cm-collab-selectionLine': {
|
|
264
275
|
padding: 0,
|
|
265
276
|
margin: '0px 2px 0px 4px',
|
|
266
277
|
},
|
|
267
|
-
'.cm-
|
|
278
|
+
'.cm-collab-selectionCaret': {
|
|
268
279
|
position: 'relative',
|
|
269
280
|
borderLeft: '1px solid black',
|
|
270
281
|
borderRight: '1px solid black',
|
|
@@ -273,7 +284,7 @@ const styles = EditorView.baseTheme({
|
|
|
273
284
|
boxSizing: 'border-box',
|
|
274
285
|
display: 'inline',
|
|
275
286
|
},
|
|
276
|
-
'.cm-
|
|
287
|
+
'.cm-collab-selectionCaretDot': {
|
|
277
288
|
borderRadius: '50%',
|
|
278
289
|
position: 'absolute',
|
|
279
290
|
width: '.4em',
|
|
@@ -284,11 +295,11 @@ const styles = EditorView.baseTheme({
|
|
|
284
295
|
transition: 'transform .3s ease-in-out',
|
|
285
296
|
boxSizing: 'border-box',
|
|
286
297
|
},
|
|
287
|
-
'.cm-
|
|
298
|
+
'.cm-collab-selectionCaret:hover > .cm-collab-selectionCaretDot': {
|
|
288
299
|
transformOrigin: 'bottom center',
|
|
289
300
|
transform: 'scale(0)',
|
|
290
301
|
},
|
|
291
|
-
'.cm-
|
|
302
|
+
'.cm-collab-selectionInfo': {
|
|
292
303
|
position: 'absolute',
|
|
293
304
|
top: '-1.05em',
|
|
294
305
|
left: '-1px',
|
|
@@ -304,12 +315,12 @@ const styles = EditorView.baseTheme({
|
|
|
304
315
|
zIndex: 101,
|
|
305
316
|
transition: 'opacity .3s ease-in-out',
|
|
306
317
|
backgroundColor: 'inherit',
|
|
307
|
-
//
|
|
318
|
+
// These should be separate.
|
|
308
319
|
opacity: 0,
|
|
309
320
|
transitionDelay: '0s',
|
|
310
321
|
whiteSpace: 'nowrap',
|
|
311
322
|
},
|
|
312
|
-
'.cm-
|
|
323
|
+
'.cm-collab-selectionCaret:hover > .cm-collab-selectionInfo': {
|
|
313
324
|
opacity: 1,
|
|
314
325
|
transitionDelay: '0s',
|
|
315
326
|
},
|
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' | 'multiline'>;
|
|
18
19
|
|
|
19
|
-
export const basicBundle = ({
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
export const basicBundle = ({
|
|
21
|
+
themeMode,
|
|
22
|
+
placeholder: _placeholder,
|
|
23
|
+
multiline = true,
|
|
24
|
+
}: BasicBundleOptions): Extension[] => [
|
|
25
|
+
multiline ? 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
|
},
|