@dxos/ui-editor 0.9.0 → 0.9.1-staging.ee54ba693a
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 +2533 -1744
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +2533 -1744
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/defaults.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts +6 -2
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts +6 -2
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts +2 -2
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/blocks.d.ts.map +1 -1
- package/dist/types/src/extensions/busy.d.ts +13 -0
- package/dist/types/src/extensions/busy.d.ts.map +1 -0
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/dnd.d.ts +5 -1
- package/dist/types/src/extensions/dnd.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +2 -2
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/fader.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +6 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -1
- package/dist/types/src/extensions/marker.d.ts +40 -0
- package/dist/types/src/extensions/marker.d.ts.map +1 -0
- package/dist/types/src/extensions/pending/index.d.ts +3 -0
- package/dist/types/src/extensions/pending/index.d.ts.map +1 -0
- package/dist/types/src/extensions/pending/pending-text-stream.d.ts +75 -0
- package/dist/types/src/extensions/pending/pending-text-stream.d.ts.map +1 -0
- package/dist/types/src/extensions/pending/pending-text-stream.test.d.ts +2 -0
- package/dist/types/src/extensions/pending/pending-text-stream.test.d.ts.map +1 -0
- package/dist/types/src/extensions/pending/pending-text.d.ts +46 -0
- package/dist/types/src/extensions/pending/pending-text.d.ts.map +1 -0
- package/dist/types/src/extensions/pending/pending-text.test.d.ts +2 -0
- package/dist/types/src/extensions/pending/pending-text.test.d.ts.map +1 -0
- package/dist/types/src/extensions/pos.d.ts +49 -0
- package/dist/types/src/extensions/pos.d.ts.map +1 -0
- package/dist/types/src/extensions/pos.test.d.ts +2 -0
- package/dist/types/src/extensions/pos.test.d.ts.map +1 -0
- package/dist/types/src/extensions/scrolling/auto-scroll.d.ts.map +1 -1
- package/dist/types/src/extensions/selection.d.ts +12 -1
- package/dist/types/src/extensions/selection.d.ts.map +1 -1
- package/dist/types/src/extensions/selection.test.d.ts +2 -0
- package/dist/types/src/extensions/selection.test.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/index.d.ts +2 -2
- package/dist/types/src/extensions/tags/index.d.ts.map +1 -1
- package/dist/types/src/extensions/tags/stub.d.ts +35 -0
- package/dist/types/src/extensions/tags/stub.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/widgets/anchor.d.ts +13 -0
- package/dist/types/src/extensions/tags/widgets/anchor.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/widgets/index.d.ts +2 -0
- package/dist/types/src/extensions/tags/widgets/index.d.ts.map +1 -0
- package/dist/types/src/extensions/tags/xml-tags.d.ts +9 -5
- package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
- package/dist/types/src/extensions/typewriter.d.ts.map +1 -0
- package/dist/types/src/extensions/typewriter.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +37 -35
- package/src/defaults.ts +3 -1
- package/src/extensions/autocomplete/placeholder.ts +11 -1
- package/src/extensions/automerge/automerge.test.tsx +25 -53
- package/src/extensions/automerge/automerge.ts +7 -3
- package/src/extensions/automerge/cursor.ts +7 -2
- package/src/extensions/automerge/sync.ts +2 -2
- package/src/extensions/automerge/update-automerge.ts +2 -2
- package/src/extensions/blocks.ts +36 -36
- package/src/extensions/busy.ts +32 -0
- package/src/extensions/comments.ts +5 -9
- package/src/extensions/dnd.ts +127 -3
- package/src/extensions/factories.test.ts +1 -1
- package/src/extensions/factories.ts +2 -2
- package/src/extensions/index.ts +6 -1
- package/src/extensions/markdown/styles.ts +7 -6
- package/src/extensions/marker.ts +143 -0
- package/src/extensions/pending/index.ts +6 -0
- package/src/extensions/pending/pending-text-stream.test.ts +133 -0
- package/src/extensions/pending/pending-text-stream.ts +269 -0
- package/src/extensions/pending/pending-text.test.ts +134 -0
- package/src/extensions/pending/pending-text.ts +264 -0
- package/src/extensions/pos.test.ts +97 -0
- package/src/extensions/pos.ts +206 -0
- package/src/extensions/scrolling/auto-scroll.ts +12 -8
- package/src/extensions/selection.test.ts +26 -0
- package/src/extensions/selection.ts +14 -15
- package/src/extensions/tags/DESIGN.md +58 -0
- package/src/extensions/tags/index.ts +2 -2
- package/src/extensions/tags/stub.ts +80 -0
- package/src/extensions/tags/widgets/anchor.ts +30 -0
- package/src/extensions/{preview → tags/widgets}/index.ts +1 -1
- package/src/extensions/tags/xml-tags.ts +116 -74
- package/src/extensions/tags/xml-util.test.ts +1 -1
- package/src/index.ts +1 -1
- package/src/styles/theme.ts +0 -1
- package/dist/types/src/extensions/preview/index.d.ts +0 -2
- package/dist/types/src/extensions/preview/index.d.ts.map +0 -1
- package/dist/types/src/extensions/preview/preview.d.ts +0 -34
- package/dist/types/src/extensions/preview/preview.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/fader.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/typewriter.d.ts.map +0 -1
- package/dist/types/src/extensions/tags/typewriter.test.d.ts.map +0 -1
- package/src/extensions/preview/preview.ts +0 -242
- /package/dist/types/src/extensions/{tags/fader.d.ts → fader.d.ts} +0 -0
- /package/dist/types/src/extensions/{tags/typewriter.d.ts → typewriter.d.ts} +0 -0
- /package/dist/types/src/extensions/{tags/typewriter.test.d.ts → typewriter.test.d.ts} +0 -0
- /package/src/extensions/{tags/fader.ts → fader.ts} +0 -0
- /package/src/extensions/{tags/typewriter.test.ts → typewriter.test.ts} +0 -0
- /package/src/extensions/{tags/typewriter.ts → typewriter.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/ui-editor",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1-staging.ee54ba693a",
|
|
4
4
|
"description": "Text editor components.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"src"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@automerge/automerge": "3.3.0-fragments.
|
|
35
|
+
"@automerge/automerge": "3.3.0-fragments.2",
|
|
36
36
|
"@codemirror/autocomplete": "^6.20.2",
|
|
37
37
|
"@codemirror/commands": "^6.10.3",
|
|
38
38
|
"@codemirror/lang-html": "^6.4.11",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@codemirror/lang-yaml": "^6.1.3",
|
|
44
44
|
"@codemirror/language": "^6.12.3",
|
|
45
45
|
"@codemirror/language-data": "^6.5.2",
|
|
46
|
-
"@codemirror/lint": "6.9.
|
|
46
|
+
"@codemirror/lint": "6.9.7",
|
|
47
47
|
"@codemirror/search": "^6.7.0",
|
|
48
48
|
"@codemirror/state": "^6.6.0",
|
|
49
49
|
"@codemirror/theme-one-dark": "^6.1.3",
|
|
@@ -62,29 +62,31 @@
|
|
|
62
62
|
"lodash.merge": "^4.6.2",
|
|
63
63
|
"lodash.sortby": "^4.7.0",
|
|
64
64
|
"style-mod": "^4.1.0",
|
|
65
|
-
"@dxos/
|
|
66
|
-
"@dxos/
|
|
67
|
-
"@dxos/
|
|
68
|
-
"@dxos/
|
|
69
|
-
"@dxos/
|
|
70
|
-
"@dxos/
|
|
71
|
-
"@dxos/echo-client": "0.9.
|
|
72
|
-
"@dxos/
|
|
73
|
-
"@dxos/
|
|
74
|
-
"@dxos/
|
|
75
|
-
"@dxos/
|
|
76
|
-
"@dxos/
|
|
77
|
-
"@dxos/
|
|
78
|
-
"@dxos/
|
|
79
|
-
"@dxos/ui-theme": "0.9.
|
|
80
|
-
"@dxos/
|
|
81
|
-
"@dxos/
|
|
65
|
+
"@dxos/app-graph": "0.9.1-staging.ee54ba693a",
|
|
66
|
+
"@dxos/async": "0.9.1-staging.ee54ba693a",
|
|
67
|
+
"@dxos/client": "0.9.1-staging.ee54ba693a",
|
|
68
|
+
"@dxos/context": "0.9.1-staging.ee54ba693a",
|
|
69
|
+
"@dxos/debug": "0.9.1-staging.ee54ba693a",
|
|
70
|
+
"@dxos/display-name": "0.9.1-staging.ee54ba693a",
|
|
71
|
+
"@dxos/echo-client": "0.9.1-staging.ee54ba693a",
|
|
72
|
+
"@dxos/echo": "0.9.1-staging.ee54ba693a",
|
|
73
|
+
"@dxos/echo-doc": "0.9.1-staging.ee54ba693a",
|
|
74
|
+
"@dxos/keys": "0.9.1-staging.ee54ba693a",
|
|
75
|
+
"@dxos/invariant": "0.9.1-staging.ee54ba693a",
|
|
76
|
+
"@dxos/lit-ui": "0.9.1-staging.ee54ba693a",
|
|
77
|
+
"@dxos/nlp": "0.9.1-staging.ee54ba693a",
|
|
78
|
+
"@dxos/ui": "0.9.1-staging.ee54ba693a",
|
|
79
|
+
"@dxos/ui-theme": "0.9.1-staging.ee54ba693a",
|
|
80
|
+
"@dxos/protocols": "0.9.1-staging.ee54ba693a",
|
|
81
|
+
"@dxos/ui-types": "0.9.1-staging.ee54ba693a",
|
|
82
|
+
"@dxos/util": "0.9.1-staging.ee54ba693a",
|
|
83
|
+
"@dxos/log": "0.9.1-staging.ee54ba693a"
|
|
82
84
|
},
|
|
83
85
|
"devDependencies": {
|
|
84
|
-
"@automerge/automerge": "3.3.0-fragments.
|
|
85
|
-
"@automerge/automerge-repo": "2.6.0-subduction.
|
|
86
|
-
"@automerge/automerge-repo-network-broadcastchannel": "2.6.0-subduction.
|
|
87
|
-
"@effect/platform": "0.96.
|
|
86
|
+
"@automerge/automerge": "3.3.0-fragments.2",
|
|
87
|
+
"@automerge/automerge-repo": "2.6.0-subduction.37",
|
|
88
|
+
"@automerge/automerge-repo-network-broadcastchannel": "2.6.0-subduction.37",
|
|
89
|
+
"@effect/platform": "0.96.2",
|
|
88
90
|
"@types/chai": "^4.2.15",
|
|
89
91
|
"@types/chai-dom": "^1.11.0",
|
|
90
92
|
"@types/lodash.defaultsdeep": "^4.6.6",
|
|
@@ -92,25 +94,25 @@
|
|
|
92
94
|
"@types/lodash.sortby": "^4.7.7",
|
|
93
95
|
"chai": "^4.4.1",
|
|
94
96
|
"chai-dom": "^1.11.0",
|
|
95
|
-
"effect": "3.21.
|
|
97
|
+
"effect": "3.21.4",
|
|
96
98
|
"happy-dom": "^20.0.0",
|
|
97
99
|
"jsdom": "^29.1.1",
|
|
98
100
|
"mocha": "^10.6.0",
|
|
99
101
|
"vite": "^8.0.16",
|
|
100
102
|
"vite-plugin-top-level-await": "^1.6.0",
|
|
101
103
|
"vite-plugin-wasm": "^3.6.0",
|
|
102
|
-
"@dxos/
|
|
103
|
-
"@dxos/
|
|
104
|
-
"@dxos/keyboard": "0.9.
|
|
105
|
-
"@dxos/random": "0.9.
|
|
106
|
-
"@dxos/schema": "0.9.
|
|
107
|
-
"@dxos/
|
|
108
|
-
"@dxos/
|
|
104
|
+
"@dxos/echo": "0.9.1-staging.ee54ba693a",
|
|
105
|
+
"@dxos/config": "0.9.1-staging.ee54ba693a",
|
|
106
|
+
"@dxos/keyboard": "0.9.1-staging.ee54ba693a",
|
|
107
|
+
"@dxos/random": "0.9.1-staging.ee54ba693a",
|
|
108
|
+
"@dxos/schema": "0.9.1-staging.ee54ba693a",
|
|
109
|
+
"@dxos/ui-theme": "0.9.1-staging.ee54ba693a",
|
|
110
|
+
"@dxos/storybook-utils": "0.9.1-staging.ee54ba693a"
|
|
109
111
|
},
|
|
110
112
|
"peerDependencies": {
|
|
111
|
-
"@effect/platform": "0.96.
|
|
112
|
-
"effect": "3.21.
|
|
113
|
-
"@dxos/ui-theme": "0.9.
|
|
113
|
+
"@effect/platform": "0.96.2",
|
|
114
|
+
"effect": "3.21.4",
|
|
115
|
+
"@dxos/ui-theme": "0.9.1-staging.ee54ba693a"
|
|
114
116
|
},
|
|
115
117
|
"publishConfig": {
|
|
116
118
|
"access": "public"
|
package/src/defaults.ts
CHANGED
|
@@ -9,7 +9,9 @@ import { type ThemeExtensionsOptions } from './extensions';
|
|
|
9
9
|
export const editorClassNames = (role?: string) =>
|
|
10
10
|
mx(
|
|
11
11
|
'dx-attention-surface data-[toolbar=disabled]:pt-2 dx-focus-ring-inset',
|
|
12
|
-
role === '
|
|
12
|
+
role === 'org.dxos.role.section'
|
|
13
|
+
? '[&_.cm-scroller]:overflow-hidden [&_.cm-scroller]:min-h-24'
|
|
14
|
+
: 'dx-container overflow-hidden',
|
|
13
15
|
);
|
|
14
16
|
|
|
15
17
|
export const documentSlots: ThemeExtensionsOptions['slots'] = {
|
|
@@ -7,6 +7,7 @@ import { type Extension } from '@codemirror/state';
|
|
|
7
7
|
import { Decoration, EditorView, ViewPlugin, type ViewUpdate, WidgetType } from '@codemirror/view';
|
|
8
8
|
|
|
9
9
|
import { clientRectsFor, flattenRect } from '../../util';
|
|
10
|
+
import { isBusy } from '../busy';
|
|
10
11
|
|
|
11
12
|
type Content = string | HTMLElement | ((view: EditorView) => HTMLElement);
|
|
12
13
|
|
|
@@ -28,6 +29,10 @@ export const placeholder = ({ content, delay = 3_000, focusOnly = false }: Place
|
|
|
28
29
|
_decorations = Decoration.none;
|
|
29
30
|
|
|
30
31
|
update(update: ViewUpdate) {
|
|
32
|
+
// A busy-state toggle (e.g. transcription starting) carries no doc/selection/focus change,
|
|
33
|
+
// so it must be detected explicitly to clear an already-scheduled placeholder.
|
|
34
|
+
const busyChanged = isBusy(update.startState) !== isBusy(update.view.state);
|
|
35
|
+
|
|
31
36
|
// React to actual user activity only. The empty `view.update([])`
|
|
32
37
|
// dispatched from the timeout below carries no doc/selection/focus
|
|
33
38
|
// change, so it falls through here as a no-op — that's how the
|
|
@@ -35,7 +40,7 @@ export const placeholder = ({ content, delay = 3_000, focusOnly = false }: Place
|
|
|
35
40
|
// provider to read it. Without this gate, the unconditional reset
|
|
36
41
|
// would clobber the decoration in the same tick and the placeholder
|
|
37
42
|
// would never visibly render.
|
|
38
|
-
if (!update.docChanged && !update.selectionSet && !update.focusChanged) {
|
|
43
|
+
if (!update.docChanged && !update.selectionSet && !update.focusChanged && !busyChanged) {
|
|
39
44
|
return;
|
|
40
45
|
}
|
|
41
46
|
|
|
@@ -52,6 +57,11 @@ export const placeholder = ({ content, delay = 3_000, focusOnly = false }: Place
|
|
|
52
57
|
return;
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
// Suppress the hint while another extension owns the caret region (e.g. transcription).
|
|
61
|
+
if (isBusy(update.view.state)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
const activeLine = update.view.state.doc.lineAt(update.view.state.selection.main.head);
|
|
56
66
|
if (activeLine.text.trim() !== '') {
|
|
57
67
|
return;
|
|
@@ -2,62 +2,43 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { type DocHandle, Repo, decodeHeads, encodeHeads, initSubduction } from '@automerge/automerge-repo';
|
|
5
|
+
import { next as A } from '@automerge/automerge';
|
|
7
6
|
import { EditorState } from '@codemirror/state';
|
|
8
7
|
import { EditorView } from '@codemirror/view';
|
|
9
8
|
import { render, screen } from '@testing-library/react';
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
9
|
+
import * as Schema from 'effect/Schema';
|
|
10
|
+
import React, { useEffect, useRef } from 'react';
|
|
11
|
+
import { describe, test } from 'vitest';
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { DXN, Obj, Type } from '@dxos/echo';
|
|
14
|
+
import { Doc } from '@dxos/echo-doc';
|
|
15
15
|
|
|
16
16
|
import { automerge } from './automerge';
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const adaptRepoHandle = <T,>(handle: DocHandle<T>): IDocHandle<T> => ({
|
|
23
|
-
doc: () => handle.doc() as Doc<T> | undefined,
|
|
24
|
-
change: (callback: ChangeFn<T>, options?: ChangeOptions<T>) =>
|
|
25
|
-
options ? handle.change(callback, options) : handle.change(callback),
|
|
26
|
-
changeAt: (heads: Heads, callback: ChangeFn<T>, options?: ChangeOptions<T>): Heads | undefined => {
|
|
27
|
-
const encoded = options
|
|
28
|
-
? handle.changeAt(encodeHeads(heads), callback, options)
|
|
29
|
-
: handle.changeAt(encodeHeads(heads), callback);
|
|
30
|
-
return encoded ? decodeHeads(encoded) : undefined;
|
|
31
|
-
},
|
|
32
|
-
addListener: (event, listener) => handle.on(event, listener),
|
|
33
|
-
removeListener: (event, listener) => handle.off(event, listener),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const path = ['text'];
|
|
37
|
-
|
|
38
|
-
type TestObject = {
|
|
39
|
-
text: string;
|
|
40
|
-
};
|
|
18
|
+
const TestType = Schema.Struct({ text: Schema.String }).pipe(
|
|
19
|
+
Type.makeObject(DXN.make('com.example.test.editor', '0.1.0')),
|
|
20
|
+
);
|
|
21
|
+
type TestType = Type.InstanceType<typeof TestType>;
|
|
41
22
|
|
|
42
23
|
class Generator {
|
|
43
|
-
constructor(private readonly
|
|
24
|
+
constructor(private readonly _accessor: Doc.Accessor<TestType>) {}
|
|
25
|
+
|
|
44
26
|
update(text: string): void {
|
|
45
|
-
this.
|
|
46
|
-
doc.
|
|
27
|
+
this._accessor.handle.change((doc) => {
|
|
28
|
+
A.splice(doc, [...this._accessor.path], 0, 0, text);
|
|
47
29
|
});
|
|
48
30
|
}
|
|
49
31
|
}
|
|
50
32
|
|
|
51
|
-
const Test
|
|
33
|
+
const Test = ({ accessor }: { accessor: Doc.Accessor<TestType> }) => {
|
|
52
34
|
const ref = useRef<HTMLDivElement>(null);
|
|
53
|
-
const [view, setView] = useState<EditorView>();
|
|
54
35
|
useEffect(() => {
|
|
55
36
|
const extensions = [
|
|
56
|
-
automerge(
|
|
57
|
-
EditorView.updateListener.of((
|
|
37
|
+
automerge(accessor),
|
|
38
|
+
EditorView.updateListener.of(() => {
|
|
58
39
|
if (view.state.doc.toString() === 'hello!') {
|
|
59
40
|
// Update editor.
|
|
60
|
-
|
|
41
|
+
view.dispatch({
|
|
61
42
|
changes: { from: view.state.doc.length - 1, insert: ' world' },
|
|
62
43
|
});
|
|
63
44
|
}
|
|
@@ -65,33 +46,24 @@ const Test: FC<{ handle: DocHandle<TestObject>; generator: Generator }> = ({ han
|
|
|
65
46
|
];
|
|
66
47
|
|
|
67
48
|
const view = new EditorView({
|
|
68
|
-
state: EditorState.create({ doc:
|
|
49
|
+
state: EditorState.create({ doc: Doc.getValue<string>(accessor) ?? '', extensions }),
|
|
69
50
|
parent: ref.current!,
|
|
70
51
|
});
|
|
71
52
|
|
|
72
|
-
|
|
73
|
-
}, []);
|
|
74
|
-
|
|
75
|
-
useEffect(() => {}, [view]);
|
|
53
|
+
return () => view.destroy();
|
|
54
|
+
}, [accessor]);
|
|
76
55
|
|
|
77
56
|
return <div ref={ref} data-testid='editor' />;
|
|
78
57
|
};
|
|
79
58
|
|
|
80
59
|
// TODO(burdon): Test history/undo.
|
|
81
|
-
// TODO(burdon): https://testing-library.com/docs/react-testing-library/example-intro/
|
|
82
60
|
|
|
83
61
|
describe('Automerge', () => {
|
|
84
|
-
// Subduction-fork `Repo` constructs a `MemorySigner` internally; WASM must be
|
|
85
|
-
// initialized first or the constructor throws `'set_subduction_logger' of undefined`.
|
|
86
|
-
beforeAll(async () => {
|
|
87
|
-
await initSubduction();
|
|
88
|
-
});
|
|
89
|
-
|
|
90
62
|
test('basic sync', ({ expect }) => {
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const generator = new Generator(
|
|
94
|
-
render(<Test
|
|
63
|
+
const obj = Obj.make(TestType, { text: '' });
|
|
64
|
+
const accessor = Doc.createAccessor(obj, ['text']);
|
|
65
|
+
const generator = new Generator(accessor);
|
|
66
|
+
render(<Test accessor={accessor} />);
|
|
95
67
|
|
|
96
68
|
const editor = screen.getByTestId('editor');
|
|
97
69
|
expect(editor.textContent).toBe('');
|
|
@@ -8,7 +8,7 @@ import { next as A } from '@automerge/automerge';
|
|
|
8
8
|
import { type Extension, StateField, Transaction } from '@codemirror/state';
|
|
9
9
|
import { EditorView, ViewPlugin } from '@codemirror/view';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { Doc } from '@dxos/echo-doc';
|
|
12
12
|
|
|
13
13
|
import { Cursor } from '../../util';
|
|
14
14
|
import { initialSync } from '../state';
|
|
@@ -16,7 +16,11 @@ import { cursorConverter } from './cursor';
|
|
|
16
16
|
import { type State, isReconcile, reconcileAnnotation, updateHeadsEffect } from './defs';
|
|
17
17
|
import { Syncer } from './sync';
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* CodeMirror extension that two-way syncs the editor with the string the {@link Doc.Accessor} points
|
|
21
|
+
* at, reconciling local edits and remote document mutations via Automerge.
|
|
22
|
+
*/
|
|
23
|
+
export const automerge = (accessor: Doc.Accessor): Extension => {
|
|
20
24
|
const syncState = StateField.define<State>({
|
|
21
25
|
create: () => {
|
|
22
26
|
return {
|
|
@@ -68,7 +72,7 @@ export const automerge = (accessor: DocAccessor): Extension => {
|
|
|
68
72
|
accessor.handle.addListener('change', this._handleChange);
|
|
69
73
|
|
|
70
74
|
requestAnimationFrame(() => {
|
|
71
|
-
const value =
|
|
75
|
+
const value = Doc.getValue<string>(accessor);
|
|
72
76
|
const current = this._view.state.doc.toString();
|
|
73
77
|
if (value !== current) {
|
|
74
78
|
this._view.dispatch({
|
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { fromCursor, toCursor } from '@dxos/echo-client';
|
|
6
|
+
import { Doc } from '@dxos/echo-doc';
|
|
6
7
|
import { log } from '@dxos/log';
|
|
7
8
|
|
|
8
9
|
import { type CursorConverter } from '../../util';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Maps between editor offsets and stable Automerge cursors for the text the {@link Doc.Accessor}
|
|
13
|
+
* points at; falls back to empty/zero positions when the document is unavailable.
|
|
14
|
+
*/
|
|
15
|
+
export const cursorConverter = (accessor: Doc.Accessor): CursorConverter => ({
|
|
11
16
|
toCursor: (pos, assoc) => {
|
|
12
17
|
try {
|
|
13
18
|
return toCursor(accessor, pos, assoc);
|
|
@@ -8,7 +8,7 @@ import { next as A } from '@automerge/automerge';
|
|
|
8
8
|
import { type StateField } from '@codemirror/state';
|
|
9
9
|
import { type EditorView } from '@codemirror/view';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { Doc } from '@dxos/echo-doc';
|
|
12
12
|
import { log } from '@dxos/log';
|
|
13
13
|
|
|
14
14
|
import { type State, getLastHeads, getPath, isReconcile, reconcileAnnotation, updateHeads } from './defs';
|
|
@@ -23,7 +23,7 @@ export class Syncer {
|
|
|
23
23
|
|
|
24
24
|
// prettier-ignore
|
|
25
25
|
constructor(
|
|
26
|
-
private readonly _handle:
|
|
26
|
+
private readonly _handle: Doc.Handle,
|
|
27
27
|
private readonly _state: StateField<State>
|
|
28
28
|
) {}
|
|
29
29
|
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
import { next as A, type Heads } from '@automerge/automerge';
|
|
8
8
|
import { type EditorState, type StateField, type Text, type Transaction } from '@codemirror/state';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { Doc } from '@dxos/echo-doc';
|
|
11
11
|
|
|
12
12
|
import { type State } from './defs';
|
|
13
13
|
|
|
14
14
|
export const updateAutomerge = (
|
|
15
15
|
field: StateField<State>,
|
|
16
|
-
handle:
|
|
16
|
+
handle: Doc.Handle,
|
|
17
17
|
transactions: Transaction[],
|
|
18
18
|
state: EditorState, // TODO(burdon): Just pass in the state field value?
|
|
19
19
|
): Heads | undefined => {
|
package/src/extensions/blocks.ts
CHANGED
|
@@ -7,7 +7,42 @@ import { Decoration, type DecorationSet, EditorView, ViewPlugin, type ViewUpdate
|
|
|
7
7
|
|
|
8
8
|
import { mx } from '@dxos/ui-theme';
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
export const blocks = () => [
|
|
11
|
+
paragraphBlock,
|
|
12
|
+
EditorView.baseTheme({
|
|
13
|
+
'.cm-line.block-line': {
|
|
14
|
+
paddingLeft: '0.75rem',
|
|
15
|
+
paddingRight: '0.75rem',
|
|
16
|
+
borderLeft: '1px solid var(--color-subdued-separator)',
|
|
17
|
+
borderRight: '1px solid var(--color-subdued-separator)',
|
|
18
|
+
},
|
|
19
|
+
'.cm-line.block-single': {
|
|
20
|
+
border: '1px solid var(--color-subdued-separator)',
|
|
21
|
+
borderRadius: '6px',
|
|
22
|
+
paddingTop: '0.5rem',
|
|
23
|
+
paddingBottom: '0.5rem',
|
|
24
|
+
marginTop: '0.5rem',
|
|
25
|
+
marginBottom: '0.5rem',
|
|
26
|
+
},
|
|
27
|
+
'.cm-line.block-first': {
|
|
28
|
+
borderTop: '1px solid var(--color-subdued-separator)',
|
|
29
|
+
borderTopLeftRadius: '6px',
|
|
30
|
+
borderTopRightRadius: '6px',
|
|
31
|
+
paddingTop: '0.5rem',
|
|
32
|
+
marginTop: '0.5rem',
|
|
33
|
+
},
|
|
34
|
+
'.cm-line.block-middle': {},
|
|
35
|
+
'.cm-line.block-last': {
|
|
36
|
+
borderBottom: '1px solid var(--color-subdued-separator)',
|
|
37
|
+
borderBottomLeftRadius: '6px',
|
|
38
|
+
borderBottomRightRadius: '6px',
|
|
39
|
+
paddingBottom: '0.5rem',
|
|
40
|
+
marginBottom: '0.5rem',
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const paragraphBlock = ViewPlugin.fromClass(
|
|
11
46
|
class {
|
|
12
47
|
decorations: DecorationSet;
|
|
13
48
|
|
|
@@ -94,38 +129,3 @@ const paragraphBlockPlugin = ViewPlugin.fromClass(
|
|
|
94
129
|
decorations: (v) => v.decorations,
|
|
95
130
|
},
|
|
96
131
|
);
|
|
97
|
-
|
|
98
|
-
export const blocks = () => [
|
|
99
|
-
paragraphBlockPlugin,
|
|
100
|
-
EditorView.baseTheme({
|
|
101
|
-
'.cm-line.block-line': {
|
|
102
|
-
paddingLeft: '0.75rem',
|
|
103
|
-
paddingRight: '0.75rem',
|
|
104
|
-
borderLeft: '1px solid var(--color-subdued-separator)',
|
|
105
|
-
borderRight: '1px solid var(--color-subdued-separator)',
|
|
106
|
-
},
|
|
107
|
-
'.cm-line.block-single': {
|
|
108
|
-
border: '1px solid var(--color-subdued-separator)',
|
|
109
|
-
borderRadius: '6px',
|
|
110
|
-
paddingTop: '0.5rem',
|
|
111
|
-
paddingBottom: '0.5rem',
|
|
112
|
-
marginTop: '0.5rem',
|
|
113
|
-
marginBottom: '0.5rem',
|
|
114
|
-
},
|
|
115
|
-
'.cm-line.block-first': {
|
|
116
|
-
borderTop: '1px solid var(--color-subdued-separator)',
|
|
117
|
-
borderTopLeftRadius: '6px',
|
|
118
|
-
borderTopRightRadius: '6px',
|
|
119
|
-
paddingTop: '0.5rem',
|
|
120
|
-
marginTop: '0.5rem',
|
|
121
|
-
},
|
|
122
|
-
'.cm-line.block-middle': {},
|
|
123
|
-
'.cm-line.block-last': {
|
|
124
|
-
borderBottom: '1px solid var(--color-subdued-separator)',
|
|
125
|
-
borderBottomLeftRadius: '6px',
|
|
126
|
-
borderBottomRightRadius: '6px',
|
|
127
|
-
paddingBottom: '0.5rem',
|
|
128
|
-
marginBottom: '0.5rem',
|
|
129
|
-
},
|
|
130
|
-
}),
|
|
131
|
-
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type EditorState, type Extension, StateEffect, StateField } from '@codemirror/state';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generic shared "busy" flag that extensions use to coordinate exclusive use of the cursor region.
|
|
9
|
+
* One extension sets it (e.g. transcription while a pending block is active); others read it (e.g. a
|
|
10
|
+
* placeholder hint) to suppress themselves while something else owns the caret.
|
|
11
|
+
*/
|
|
12
|
+
export const setBusy = StateEffect.define<boolean>();
|
|
13
|
+
|
|
14
|
+
export const busyState = StateField.define<boolean>({
|
|
15
|
+
create: () => false,
|
|
16
|
+
update: (value, tr) => {
|
|
17
|
+
let next = value;
|
|
18
|
+
for (const effect of tr.effects) {
|
|
19
|
+
if (effect.is(setBusy)) {
|
|
20
|
+
next = effect.value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return next;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/** Whether any extension has flagged the editor busy. Safe when the field is absent (returns false). */
|
|
29
|
+
export const isBusy = (state: EditorState): boolean => state.field(busyState, false) ?? false;
|
|
30
|
+
|
|
31
|
+
/** Contributes the shared busy flag. Include once; setters/readers reference the same field. */
|
|
32
|
+
export const busy = (): Extension => busyState;
|
|
@@ -22,6 +22,7 @@ import { isNonNullable } from '@dxos/util';
|
|
|
22
22
|
|
|
23
23
|
import { type Comment, type Range, type RenderCallback } from '../types';
|
|
24
24
|
import { Cursor, singleValueFacet, wrapWithCatch } from '../util';
|
|
25
|
+
import { markerMark, markerTheme } from './marker';
|
|
25
26
|
import { documentId } from './selection';
|
|
26
27
|
|
|
27
28
|
//
|
|
@@ -99,22 +100,16 @@ export const commentsState = StateField.define<CommentsState>({
|
|
|
99
100
|
/**
|
|
100
101
|
* NOTE: Matches search.
|
|
101
102
|
*/
|
|
103
|
+
// Surface/box-shadow come from the shared `markerTheme`, tinted by the mark's `data-hue`; the comment
|
|
104
|
+
// just adds the pointer affordance.
|
|
102
105
|
const styles = EditorView.theme({
|
|
103
106
|
'.cm-comment > span': {
|
|
104
|
-
boxDecorationBreak: 'clone',
|
|
105
|
-
boxShadow: '0 0 0 3px var(--color-cm-comment-surface)',
|
|
106
|
-
backgroundColor: 'var(--color-cm-comment-surface)',
|
|
107
|
-
color: 'var(--color-cm-comment-text) !important',
|
|
108
107
|
cursor: 'pointer',
|
|
109
108
|
},
|
|
110
|
-
'.cm-comment[data-current="1"] > span': {
|
|
111
|
-
boxShadow: '0 0 0 3px var(--color-cm-comment-current-surface)',
|
|
112
|
-
backgroundColor: 'var(--color-cm-comment-current-surface)',
|
|
113
|
-
},
|
|
114
109
|
});
|
|
115
110
|
|
|
116
111
|
const createCommentMark = (id: string, isCurrent: boolean) =>
|
|
117
|
-
|
|
112
|
+
markerMark(isCurrent ? 'orange' : 'teal', {
|
|
118
113
|
class: 'cm-comment',
|
|
119
114
|
attributes: {
|
|
120
115
|
'data-testid': 'cm-comment',
|
|
@@ -386,6 +381,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
386
381
|
commentsState,
|
|
387
382
|
commentsDecorations,
|
|
388
383
|
handleCommentClick,
|
|
384
|
+
markerTheme(),
|
|
389
385
|
styles,
|
|
390
386
|
|
|
391
387
|
//
|