@dxos/react-ui-editor 0.3.8-next.f4e0086 → 0.3.9-main.325dd0a

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.
@@ -0,0 +1,16 @@
1
+ import { type EditorView } from '@codemirror/view';
2
+ import { next as automerge } from '@dxos/automerge/automerge';
3
+ import { type Field } from './plugin';
4
+ import { type Peer } from '../demo';
5
+ type Doc<T> = automerge.Doc<T>;
6
+ type Heads = automerge.Heads;
7
+ type ChangeFn = (atHeads: Heads, change: (doc: Doc<unknown>) => void) => Heads | undefined;
8
+ export declare class PatchSemaphore {
9
+ _field: Field;
10
+ _inReconcile: boolean;
11
+ _queue: Array<ChangeFn>;
12
+ constructor(field?: Field);
13
+ reconcile: (handle: Peer, view: EditorView) => void;
14
+ }
15
+ export {};
16
+ //# sourceMappingURL=PatchSemaphore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PatchSemaphore.d.ts","sourceRoot":"","sources":["../../../../../src/automerge/automerge-plugin/PatchSemaphore.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,IAAI,IAAI,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAI9D,OAAO,EAAE,KAAK,KAAK,EAA8E,MAAM,UAAU,CAAC;AAClH,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAEpC,KAAK,GAAG,CAAC,CAAC,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC/B,KAAK,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC;AAE7B,KAAK,QAAQ,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,KAAK,KAAK,GAAG,SAAS,CAAC;AAE3F,qBAAa,cAAc;IACzB,MAAM,EAAG,KAAK,CAAC;IACf,YAAY,UAAS;IACrB,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAM;gBAEjB,KAAK,CAAC,EAAE,KAAK;IAMzB,SAAS,WAAY,IAAI,QAAQ,UAAU,UA2CzC;CACH"}
@@ -0,0 +1,6 @@
1
+ import { type EditorSelection } from '@codemirror/state';
2
+ import { type EditorView } from '@codemirror/view';
3
+ import { type Patch, type Prop } from '@dxos/automerge/automerge';
4
+ declare const _default: (view: EditorView, selection: EditorSelection, target: Prop[], patches: Patch[]) => void;
5
+ export default _default;
6
+ //# sourceMappingURL=amToCodemirror.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"amToCodemirror.d.ts","sourceRoot":"","sources":["../../../../../src/automerge/automerge-plugin/amToCodemirror.ts"],"names":[],"mappings":"AAIA,OAAO,EAA8B,KAAK,eAAe,EAAoB,MAAM,mBAAmB,CAAC;AACvG,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAGL,KAAK,KAAK,EACV,KAAK,IAAI,EAGV,MAAM,2BAA2B,CAAC;+BAIb,UAAU,aAAa,eAAe,UAAU,IAAI,EAAE,WAAW,KAAK,EAAE;AAA9F,wBAgBE"}
@@ -0,0 +1,7 @@
1
+ import { type EditorState, type Transaction } from '@codemirror/state';
2
+ import { type Heads } from '@dxos/automerge/automerge';
3
+ import { type Field } from './plugin';
4
+ import { type Peer } from '../demo';
5
+ declare const _default: (field: Field, handle: Peer, transactions: Transaction[], state: EditorState) => Heads | undefined;
6
+ export default _default;
7
+ //# sourceMappingURL=codeMirrorToAm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codeMirrorToAm.d.ts","sourceRoot":"","sources":["../../../../../src/automerge/automerge-plugin/codeMirrorToAm.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,WAAW,EAAa,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAElF,OAAO,EAAc,KAAK,KAAK,EAAE,MAAM,2BAA2B,CAAC;AAEnE,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;+CAEE,IAAI,gBAAgB,WAAW,EAAE,SAAS,WAAW,KAAG,KAAK,GAAG,SAAS;AAA/G,wBA0BE"}
@@ -0,0 +1,3 @@
1
+ export { plugin } from './plugin';
2
+ export { PatchSemaphore } from './PatchSemaphore';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/automerge/automerge-plugin/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { type EditorState, type Extension, StateEffect, StateField, type Transaction, type TransactionSpec } from '@codemirror/state';
2
+ import { type EditorView } from '@codemirror/view';
3
+ import * as automerge from '@dxos/automerge/automerge';
4
+ import { type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
5
+ import { type Peer } from '../demo';
6
+ export type Value = {
7
+ lastHeads: Heads;
8
+ path: Prop[];
9
+ unreconciledTransactions: Transaction[];
10
+ };
11
+ type UpdateHeads = {
12
+ newHeads: Heads;
13
+ };
14
+ export declare const effectType: import("@codemirror/state").StateEffectType<UpdateHeads>;
15
+ export declare const updateHeads: (newHeads: automerge.next.Heads) => StateEffect<UpdateHeads>;
16
+ export declare const getLastHeads: (state: EditorState, field: Field) => automerge.next.Heads;
17
+ export declare const getPath: (state: EditorState, field: Field) => Prop[];
18
+ export type Field = StateField<Value>;
19
+ export declare const plugin: <T>(doc: automerge.next.Doc<T>, path: Prop[]) => Extension;
20
+ export declare const reconcileAnnotationType: import("@codemirror/state").AnnotationType<unknown>;
21
+ export declare const isReconcileTx: (tr: Transaction) => boolean;
22
+ export declare const makeReconcile: (tr: TransactionSpec) => void;
23
+ export declare const reconcile: (handle: Peer, view: EditorView) => void;
24
+ export {};
25
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../../../src/automerge/automerge-plugin/plugin.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,SAAS,EAEd,WAAW,EACX,UAAU,EACV,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,SAAS,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,KAAK,EAAE,KAAK,IAAI,EAAE,MAAM,2BAA2B,CAAC;AAG5E,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAEpC,MAAM,MAAM,KAAK,GAAG;IAClB,SAAS,EAAE,KAAK,CAAC;IACjB,IAAI,EAAE,IAAI,EAAE,CAAC;IACb,wBAAwB,EAAE,WAAW,EAAE,CAAC;CACzC,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,QAAQ,EAAE,KAAK,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,UAAU,0DAAsC,CAAC;AAE9D,eAAO,MAAM,WAAW,sCAAsB,YAAY,WAAW,CAAgC,CAAC;AAEtG,eAAO,MAAM,YAAY,UAAW,WAAW,uCAAsD,CAAC;AAEtG,eAAO,MAAM,OAAO,UAAW,WAAW,mBAAiB,IAAI,EAA6B,CAAC;AAE7F,MAAM,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;AAMtC,eAAO,MAAM,MAAM,wCAA0B,IAAI,EAAE,KAAG,SAiCrD,CAAC;AAEF,eAAO,MAAM,uBAAuB,qDAA+B,CAAC;AAEpE,eAAO,MAAM,aAAa,OAAQ,WAAW,KAAG,OAAmD,CAAC;AAEpG,eAAO,MAAM,aAAa,OAAQ,eAAe,SAchD,CAAC;AAEF,eAAO,MAAM,SAAS,WAAY,IAAI,QAAQ,UAAU,SAEvD,CAAC"}
@@ -0,0 +1,8 @@
1
+ declare const _default: {
2
+ title: string;
3
+ };
4
+ export default _default;
5
+ export declare const EditorStory: {
6
+ render: () => JSX.Element;
7
+ };
8
+ //# sourceMappingURL=automerge.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"automerge.stories.d.ts","sourceRoot":"","sources":["../../../../src/automerge/automerge.stories.tsx"],"names":[],"mappings":";;;AAkGA,wBAEE;AAEF,eAAO,MAAM,WAAW;;CAEvB,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { Event } from '@dxos/async';
2
+ import { type Doc, next as automerge, type ChangeOptions, type ChangeFn, type Heads } from '@dxos/automerge/automerge';
3
+ export declare class Peer {
4
+ changeEvent: Event<void>;
5
+ storage: Map<string, Uint8Array>;
6
+ stats: {
7
+ messagesSent: number;
8
+ messagesReceived: number;
9
+ bytesSent: number;
10
+ bytesReceived: number;
11
+ snapshotBytes: number;
12
+ incrementalBytes: number;
13
+ };
14
+ doc: Doc<{
15
+ text: string;
16
+ }>;
17
+ change(options: string | ChangeOptions<any> | ChangeFn<any>, callback?: ChangeFn<any>): void;
18
+ changeAt(scope: Heads, options: string | ChangeOptions<any> | ChangeFn<any>, callback?: ChangeFn<any>): automerge.Heads | null;
19
+ replicate(): {
20
+ readable: ReadableStream<Uint8Array>;
21
+ writable: WritableStream<Uint8Array>;
22
+ };
23
+ private _handleChange;
24
+ }
25
+ //# sourceMappingURL=demo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../../../../src/automerge/demo.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,OAAO,EACL,KAAK,GAAG,EACR,IAAI,IAAI,SAAS,EACjB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,KAAK,EAEX,MAAM,2BAA2B,CAAC;AAEnC,qBAAa,IAAI;IACf,WAAW,cAAe;IAE1B,OAAO,0BAAiC;IACxC,KAAK;;;;;;;MAOH;IAEF,GAAG,EAAG,GAAG,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAE5B,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC;IAKrF,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC;IAOrG,SAAS,IAAI;QAAE,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC;QAAC,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,CAAA;KAAE;IAgD3F,OAAO,CAAC,aAAa;CAgBtB"}
@@ -1 +1 @@
1
- {"version":3,"file":"code-mirror.stories.d.ts","sourceRoot":"","sources":["../../../../stories/html/code-mirror.stories.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAMhD,OAAO,EAAE,MAAM,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;;;;AAS5D,wBAEE;AAoEF,eAAO,MAAM,OAAO,EAAE,QAAQ,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,CAoBjF,CAAC"}
1
+ {"version":3,"file":"code-mirror.stories.d.ts","sourceRoot":"","sources":["../../../../stories/html/code-mirror.stories.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAKhD,OAAO,EAAE,MAAM,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;;;;AAS5D,wBAEE;AAoEF,eAAO,MAAM,OAAO,EAAE,QAAQ,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,CAoBjF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-editor",
3
- "version": "0.3.8-next.f4e0086",
3
+ "version": "0.3.9-main.325dd0a",
4
4
  "description": "Document editing experience within a DXOS shell.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -13,20 +13,20 @@
13
13
  "src"
14
14
  ],
15
15
  "dependencies": {
16
- "@codemirror/autocomplete": "^6.4.2",
17
- "@codemirror/commands": "^6.2.2",
18
- "@codemirror/lang-markdown": "^6.1.0",
19
- "@codemirror/language": "^6.6.0",
20
- "@codemirror/language-data": "^6.1.0",
21
- "@codemirror/lint": "^6.2.0",
22
- "@codemirror/search": "^6.3.0",
23
- "@codemirror/state": "^6.2.0",
24
- "@codemirror/theme-one-dark": "^6.1.1",
25
- "@codemirror/view": "^6.9.2",
16
+ "@codemirror/autocomplete": "^6.11.0",
17
+ "@codemirror/commands": "^6.3.0",
18
+ "@codemirror/lang-markdown": "^6.2.2",
19
+ "@codemirror/language": "^6.9.2",
20
+ "@codemirror/language-data": "^6.3.1",
21
+ "@codemirror/lint": "^6.4.2",
22
+ "@codemirror/search": "^6.5.4",
23
+ "@codemirror/state": "^6.3.1",
24
+ "@codemirror/theme-one-dark": "^6.1.2",
25
+ "@codemirror/view": "^6.22.0",
26
26
  "@fluentui/react-tabster": "^9.12.0",
27
- "@lezer/common": "^1.0.2",
28
- "@lezer/highlight": "^1.1.3",
29
- "@lezer/markdown": "^1.0.2",
27
+ "@lezer/common": "^1.1.1",
28
+ "@lezer/highlight": "^1.2.0",
29
+ "@lezer/markdown": "^1.1.1",
30
30
  "@replit/codemirror-vim": "^6.0.14",
31
31
  "@tiptap/core": "2.0.0-beta.220",
32
32
  "@tiptap/extension-collaboration": "2.0.0-beta.220",
@@ -37,7 +37,7 @@
37
37
  "@tiptap/pm": "2.0.0-beta.220",
38
38
  "@tiptap/react": "2.0.0-beta.220",
39
39
  "@tiptap/starter-kit": "2.0.0-beta.220",
40
- "codemirror": "^6.0.1",
40
+ "codemirror": "^6.65.7",
41
41
  "lib0": "^0.2.65",
42
42
  "lodash.get": "^4.4.2",
43
43
  "prosemirror-model": "^1.19.0",
@@ -47,31 +47,34 @@
47
47
  "y-prosemirror": "~1.0.20",
48
48
  "y-protocols": "^1.0.5",
49
49
  "yjs": "^13.6.8",
50
- "@dxos/display-name": "0.3.8-next.f4e0086",
51
- "@dxos/log": "0.3.8-next.f4e0086",
52
- "@dxos/react-async": "0.3.8-next.f4e0086",
53
- "@dxos/debug": "0.3.8-next.f4e0086",
54
- "@dxos/react-ui-theme": "0.3.8-next.f4e0086",
55
- "@dxos/react-ui": "0.3.8-next.f4e0086",
56
- "@dxos/text-model": "0.3.8-next.f4e0086",
57
- "@dxos/util": "0.3.8-next.f4e0086",
58
- "@dxos/protocols": "0.3.8-next.f4e0086"
50
+ "@dxos/debug": "0.3.9-main.325dd0a",
51
+ "@dxos/display-name": "0.3.9-main.325dd0a",
52
+ "@dxos/log": "0.3.9-main.325dd0a",
53
+ "@dxos/protocols": "0.3.9-main.325dd0a",
54
+ "@dxos/react-ui": "0.3.9-main.325dd0a",
55
+ "@dxos/react-ui-theme": "0.3.9-main.325dd0a",
56
+ "@dxos/text-model": "0.3.9-main.325dd0a",
57
+ "@dxos/react-async": "0.3.9-main.325dd0a",
58
+ "@dxos/util": "0.3.9-main.325dd0a"
59
59
  },
60
60
  "devDependencies": {
61
+ "@codemirror/basic-setup": "^0.20.0",
62
+ "@types/codemirror": "^5.60.14",
61
63
  "@types/lodash.get": "^4.4.7",
62
64
  "@types/react": "^18.0.21",
63
65
  "@types/react-dom": "^18.0.6",
64
66
  "react": "^18.2.0",
65
67
  "react-dom": "^18.2.0",
66
- "@dxos/echo-schema": "0.3.8-next.f4e0086",
67
- "@dxos/config": "0.3.8-next.f4e0086",
68
- "@dxos/echo-typegen": "0.3.8-next.f4e0086",
69
- "@dxos/react-client": "0.3.8-next.f4e0086"
68
+ "@dxos/automerge": "0.3.9-main.325dd0a",
69
+ "@dxos/echo-schema": "0.3.9-main.325dd0a",
70
+ "@dxos/config": "0.3.9-main.325dd0a",
71
+ "@dxos/echo-typegen": "0.3.9-main.325dd0a",
72
+ "@dxos/react-client": "0.3.9-main.325dd0a"
70
73
  },
71
74
  "peerDependencies": {
72
75
  "react": "^18.2.0",
73
76
  "react-dom": "^18.2.0",
74
- "@dxos/react-client": "0.3.8-next.f4e0086"
77
+ "@dxos/react-client": "0.3.9-main.325dd0a"
75
78
  },
76
79
  "publishConfig": {
77
80
  "access": "public"
@@ -0,0 +1,74 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type EditorView } from '@codemirror/view';
6
+
7
+ import { next as automerge } from '@dxos/automerge/automerge';
8
+
9
+ import amToCodemirror from './amToCodemirror';
10
+ import codeMirrorToAm from './codeMirrorToAm';
11
+ import { type Field, isReconcileTx, getPath, reconcileAnnotationType, updateHeads, getLastHeads } from './plugin';
12
+ import { type Peer } from '../demo';
13
+
14
+ type Doc<T> = automerge.Doc<T>;
15
+ type Heads = automerge.Heads;
16
+
17
+ type ChangeFn = (atHeads: Heads, change: (doc: Doc<unknown>) => void) => Heads | undefined;
18
+
19
+ export class PatchSemaphore {
20
+ _field!: Field;
21
+ _inReconcile = false;
22
+ _queue: Array<ChangeFn> = [];
23
+
24
+ constructor(field?: Field) {
25
+ if (field !== undefined) {
26
+ this._field = field;
27
+ }
28
+ }
29
+
30
+ reconcile = (handle: Peer, view: EditorView) => {
31
+ if (!this._inReconcile) {
32
+ return;
33
+ }
34
+ this._inReconcile = true;
35
+
36
+ const path = getPath(view.state, this._field);
37
+ const oldHeads = getLastHeads(view.state, this._field);
38
+ let selection = view.state.selection;
39
+
40
+ const transactions = view.state.field(this._field).unreconciledTransactions.filter((tx) => !isReconcileTx(tx));
41
+
42
+ // First undo all the unreconciled transactions
43
+ const toInvert = transactions.slice().reverse();
44
+ for (const tx of toInvert) {
45
+ const inverted = tx.changes.invert(tx.startState.doc);
46
+ selection = selection.map(inverted);
47
+ view.dispatch({
48
+ changes: inverted,
49
+ annotations: reconcileAnnotationType.of(true),
50
+ });
51
+ }
52
+
53
+ // now apply the unreconciled transactions to the document
54
+ let newHeads = codeMirrorToAm(this._field, handle, transactions, view.state);
55
+
56
+ // NOTE: null and undefined each come from automerge and repo respectively
57
+ if (newHeads === null || newHeads === undefined) {
58
+ // TODO: @alexjg this is the call that's resetting the editor state on click
59
+ newHeads = automerge.getHeads(handle.doc);
60
+ }
61
+
62
+ // now get the diff between the updated state of the document and the heads
63
+ // and apply that to the codemirror doc
64
+ const diff = automerge.equals(oldHeads, newHeads) ? [] : automerge.diff(handle.doc, oldHeads, newHeads);
65
+ amToCodemirror(view, selection, path, diff);
66
+
67
+ view.dispatch({
68
+ effects: updateHeads(newHeads),
69
+ annotations: reconcileAnnotationType.of({}),
70
+ });
71
+
72
+ this._inReconcile = false;
73
+ };
74
+ }
@@ -0,0 +1,105 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { ChangeSet, type ChangeSpec, type EditorSelection, type EditorState } from '@codemirror/state';
6
+ import { type EditorView } from '@codemirror/view';
7
+
8
+ import {
9
+ type DelPatch,
10
+ type InsertPatch,
11
+ type Patch,
12
+ type Prop,
13
+ type PutPatch,
14
+ type SpliceTextPatch,
15
+ } from '@dxos/automerge/automerge';
16
+
17
+ import { reconcileAnnotationType } from './plugin';
18
+
19
+ export default (view: EditorView, selection: EditorSelection, target: Prop[], patches: Patch[]) => {
20
+ for (const patch of patches) {
21
+ const changeSpec = handlePatch(patch, target, view.state);
22
+ if (changeSpec != null) {
23
+ const changeSet = ChangeSet.of(changeSpec, view.state.doc.length, '\n');
24
+ selection = selection.map(changeSet, 1);
25
+ view.dispatch({
26
+ changes: changeSet,
27
+ annotations: reconcileAnnotationType.of({}),
28
+ });
29
+ }
30
+ }
31
+ view.dispatch({
32
+ selection,
33
+ annotations: reconcileAnnotationType.of({}),
34
+ });
35
+ };
36
+
37
+ const handlePatch = (patch: Patch, target: Prop[], state: EditorState): ChangeSpec | null => {
38
+ if (patch.action === 'insert') {
39
+ return handleInsert(target, patch);
40
+ } else if (patch.action === 'splice') {
41
+ return handleSplice(target, patch);
42
+ } else if (patch.action === 'del') {
43
+ return handleDel(target, patch);
44
+ } else if (patch.action === 'put') {
45
+ return handlePut(target, patch, state);
46
+ } else {
47
+ return null;
48
+ }
49
+ };
50
+
51
+ const handleInsert = (target: Prop[], patch: InsertPatch): Array<ChangeSpec> => {
52
+ const index = charPath(target, patch.path);
53
+ if (index == null) {
54
+ return [];
55
+ }
56
+ const text = patch.values.map((v) => (v ? v.toString() : '')).join('');
57
+ return [{ from: index, to: index, insert: text }];
58
+ };
59
+
60
+ const handleSplice = (target: Prop[], patch: SpliceTextPatch): Array<ChangeSpec> => {
61
+ const index = charPath(target, patch.path);
62
+ if (index == null) {
63
+ return [];
64
+ }
65
+ return [{ from: index, insert: patch.value }];
66
+ };
67
+
68
+ const handleDel = (target: Prop[], patch: DelPatch): Array<ChangeSpec> => {
69
+ const index = charPath(target, patch.path);
70
+ if (index == null) {
71
+ return [];
72
+ }
73
+ const length = patch.length || 1;
74
+ return [{ from: index, to: index + length }];
75
+ };
76
+
77
+ const handlePut = (target: Prop[], patch: PutPatch, state: EditorState): Array<ChangeSpec> => {
78
+ const index = charPath(target, [...patch.path, 0]);
79
+ if (index == null) {
80
+ return [];
81
+ }
82
+ const length = state.doc.length;
83
+ if (typeof patch.value !== 'string') {
84
+ return []; // TODO(dmaretskyi): How to handle non string values?
85
+ }
86
+ return [{ from: 0, to: length, insert: patch.value as any }];
87
+ };
88
+
89
+ // If the path of the patch is of the form [path, <index>] then we know this is
90
+ // a path to a character within the sequence given by path
91
+ const charPath = (textPath: Prop[], candidatePath: Prop[]): number | null => {
92
+ if (candidatePath.length !== textPath.length + 1) {
93
+ return null;
94
+ }
95
+ for (let i = 0; i < textPath.length; i++) {
96
+ if (textPath[i] !== candidatePath[i]) {
97
+ return null;
98
+ }
99
+ }
100
+ const index = candidatePath[candidatePath.length - 1];
101
+ if (typeof index === 'number') {
102
+ return index;
103
+ }
104
+ return null;
105
+ };
@@ -0,0 +1,38 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type EditorState, type Text, type Transaction } from '@codemirror/state';
6
+
7
+ import { next as am, type Heads } from '@dxos/automerge/automerge';
8
+
9
+ import { type Field } from './plugin';
10
+ import { type Peer } from '../demo';
11
+
12
+ export default (field: Field, handle: Peer, transactions: Transaction[], state: EditorState): Heads | undefined => {
13
+ const { lastHeads, path } = state.field(field);
14
+
15
+ // We don't want to call `automerge.updateAt` if there are no changes.
16
+ // Otherwise later on `automerge.diff` will return empty patches that result in a no-op but still mess up the selection.
17
+ let hasChanges = false;
18
+ for (const tr of transactions) {
19
+ if (tr.changes.length) {
20
+ tr.changes.iterChanges(() => {
21
+ hasChanges = true;
22
+ });
23
+ }
24
+ }
25
+
26
+ if (!hasChanges) {
27
+ return undefined;
28
+ }
29
+
30
+ const newHeads = handle.changeAt(lastHeads, (doc: am.Doc<unknown>) => {
31
+ for (const tr of transactions) {
32
+ tr.changes.iterChanges((fromA: number, toA: number, _fromB: number, _toB: number, inserted: Text) => {
33
+ am.splice(doc, path, fromA, toA - fromA, inserted.toString());
34
+ });
35
+ }
36
+ });
37
+ return newHeads ?? undefined;
38
+ };
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export { plugin } from './plugin';
6
+ export { PatchSemaphore } from './PatchSemaphore';
@@ -0,0 +1,104 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import {
6
+ Annotation,
7
+ type EditorState,
8
+ type Extension,
9
+ Facet,
10
+ StateEffect,
11
+ StateField,
12
+ type Transaction,
13
+ type TransactionSpec,
14
+ } from '@codemirror/state';
15
+ import { type EditorView } from '@codemirror/view';
16
+
17
+ import * as automerge from '@dxos/automerge/automerge';
18
+ import { type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
19
+
20
+ import { PatchSemaphore } from './PatchSemaphore';
21
+ import { type Peer } from '../demo';
22
+
23
+ export type Value = {
24
+ lastHeads: Heads;
25
+ path: Prop[];
26
+ unreconciledTransactions: Transaction[];
27
+ };
28
+
29
+ type UpdateHeads = {
30
+ newHeads: Heads;
31
+ };
32
+
33
+ export const effectType = StateEffect.define<UpdateHeads>({});
34
+
35
+ export const updateHeads = (newHeads: Heads): StateEffect<UpdateHeads> => effectType.of({ newHeads });
36
+
37
+ export const getLastHeads = (state: EditorState, field: Field): Heads => state.field(field).lastHeads;
38
+
39
+ export const getPath = (state: EditorState, field: Field): Prop[] => state.field(field).path;
40
+
41
+ export type Field = StateField<Value>;
42
+
43
+ const semaphoreFacet = Facet.define<PatchSemaphore, PatchSemaphore>({
44
+ combine: (values) => values.at(-1)!, // Take last.
45
+ });
46
+
47
+ export const plugin = <T>(doc: Doc<T>, path: Prop[]): Extension => {
48
+ const stateField: StateField<Value> = StateField.define({
49
+ create: () => ({
50
+ lastHeads: automerge.getHeads(doc),
51
+ unreconciledTransactions: [],
52
+ path: path.slice(),
53
+ }),
54
+ update: (value: Value, tr: Transaction) => {
55
+ const result: Value = {
56
+ lastHeads: value.lastHeads,
57
+ unreconciledTransactions: value.unreconciledTransactions.slice(),
58
+ path: path.slice(),
59
+ };
60
+ let clearUnreconciled = false;
61
+ for (const effect of tr.effects) {
62
+ if (effect.is(effectType)) {
63
+ result.lastHeads = effect.value.newHeads;
64
+ clearUnreconciled = true;
65
+ }
66
+ }
67
+ if (clearUnreconciled) {
68
+ result.unreconciledTransactions = [];
69
+ } else {
70
+ if (!isReconcileTx(tr)) {
71
+ result.unreconciledTransactions.push(tr);
72
+ }
73
+ }
74
+ return result;
75
+ },
76
+ });
77
+ const semaphore = new PatchSemaphore(stateField);
78
+
79
+ return [stateField, semaphoreFacet.of(semaphore)];
80
+ };
81
+
82
+ export const reconcileAnnotationType = Annotation.define<unknown>();
83
+
84
+ export const isReconcileTx = (tr: Transaction): boolean => !!tr.annotation(reconcileAnnotationType);
85
+
86
+ export const makeReconcile = (tr: TransactionSpec) => {
87
+ if (tr.annotations != null) {
88
+ if (tr.annotations instanceof Array) {
89
+ tr.annotations = [...tr.annotations, reconcileAnnotationType.of({})];
90
+ } else {
91
+ tr.annotations = [tr.annotations, reconcileAnnotationType.of({})];
92
+ }
93
+ } else {
94
+ tr.annotations = [reconcileAnnotationType.of({})];
95
+ }
96
+ // return {
97
+ // ...tr,
98
+ // annotations: reconcileAnnotationType.of({})
99
+ // }
100
+ };
101
+
102
+ export const reconcile = (handle: Peer, view: EditorView) => {
103
+ view.state.facet(semaphoreFacet).reconcile(handle, view);
104
+ };
@@ -0,0 +1,105 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { basicSetup } from '@codemirror/basic-setup';
6
+ import { EditorView } from '@codemirror/view';
7
+ import React, { useEffect, useRef, useState } from 'react';
8
+
9
+ import { type Prop, next as automerge } from '@dxos/automerge/automerge';
10
+
11
+ import { plugin as amgPlugin } from './automerge-plugin';
12
+ import { reconcile } from './automerge-plugin/plugin';
13
+ import { Peer } from './demo';
14
+
15
+ type EditorProps = {
16
+ handle: Peer;
17
+ path: Prop[];
18
+ };
19
+
20
+ const Editor = ({ handle, path }: EditorProps) => {
21
+ const containerRef = useRef<HTMLDivElement | null>(null);
22
+ const editorRoot = useRef<EditorView>();
23
+
24
+ useEffect(() => {
25
+ const doc = handle.doc;
26
+ const source = doc.text; // this should use path
27
+ const plugin = amgPlugin(doc, path);
28
+ const view = (editorRoot.current = new EditorView({
29
+ doc: source,
30
+ extensions: [basicSetup, plugin],
31
+ dispatch: (transaction) => {
32
+ view.update([transaction]);
33
+ reconcile(handle, view);
34
+ },
35
+ parent: containerRef.current as any,
36
+ }));
37
+
38
+ const handleChange = () => {
39
+ reconcile(handle, view);
40
+ };
41
+
42
+ handle.changeEvent.on(handleChange);
43
+
44
+ return () => {
45
+ handle.changeEvent.off(handleChange);
46
+ view.destroy();
47
+ };
48
+ }, []);
49
+
50
+ return <div className='codemirror-editor' ref={containerRef} onKeyDown={(evt) => evt.stopPropagation()} />;
51
+ };
52
+
53
+ const Story = () => {
54
+ const [object1, setObject1] = useState<Peer | null>(null);
55
+ const [object2, setObject2] = useState<Peer | null>(null);
56
+ const [stats1, setStats1] = useState<any>({});
57
+ const [stats2, setStats2] = useState<any>({});
58
+
59
+ useEffect(() => {
60
+ const object1 = new Peer();
61
+ object1.doc = automerge.from({ text: 'Hello world!' });
62
+
63
+ const object2 = new Peer();
64
+ object2.doc = automerge.init();
65
+
66
+ const r1 = object1.replicate();
67
+ const r2 = object2.replicate();
68
+
69
+ void r1.readable.pipeTo(r2.writable);
70
+ void r2.readable.pipeTo(r1.writable);
71
+
72
+ setObject1(object1);
73
+ setObject2(object2);
74
+
75
+ setInterval(() => {
76
+ setStats1({ ...object1.stats });
77
+ setStats2({ ...object2.stats });
78
+ }, 500);
79
+ }, []);
80
+
81
+ if (!object1 || !object2) {
82
+ return null;
83
+ }
84
+
85
+ return (
86
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', width: '100vw' }}>
87
+ <div>
88
+ <Editor handle={object1} path={['text']} />
89
+ <div style={{ whiteSpace: 'pre' }}>{JSON.stringify(stats1, null, 2)}</div>
90
+ </div>
91
+ <div>
92
+ <Editor handle={object2} path={['text']} />
93
+ <div style={{ whiteSpace: 'pre' }}>{JSON.stringify(stats2, null, 2)}</div>
94
+ </div>
95
+ </div>
96
+ );
97
+ };
98
+
99
+ export default {
100
+ title: 'Automerge',
101
+ };
102
+
103
+ export const EditorStory = {
104
+ render: () => <Story />,
105
+ };
@@ -0,0 +1,106 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { Event } from '@dxos/async';
6
+ import {
7
+ type Doc,
8
+ next as automerge,
9
+ type ChangeOptions,
10
+ type ChangeFn,
11
+ type Heads,
12
+ type SyncState,
13
+ } from '@dxos/automerge/automerge';
14
+
15
+ export class Peer {
16
+ changeEvent = new Event();
17
+
18
+ storage = new Map<string, Uint8Array>();
19
+ stats = {
20
+ messagesSent: 0,
21
+ messagesReceived: 0,
22
+ bytesSent: 0,
23
+ bytesReceived: 0,
24
+ snapshotBytes: 0,
25
+ incrementalBytes: 0,
26
+ };
27
+
28
+ doc!: Doc<{ text: string }>;
29
+
30
+ change(options: string | ChangeOptions<any> | ChangeFn<any>, callback?: ChangeFn<any>) {
31
+ this.doc = automerge.change(this.doc, options, callback);
32
+ this._handleChange();
33
+ }
34
+
35
+ changeAt(scope: Heads, options: string | ChangeOptions<any> | ChangeFn<any>, callback?: ChangeFn<any>) {
36
+ const { newDoc, newHeads } = automerge.changeAt(this.doc, scope, options, callback);
37
+ this.doc = newDoc;
38
+ this._handleChange();
39
+ return newHeads;
40
+ }
41
+
42
+ replicate(): { readable: ReadableStream<Uint8Array>; writable: WritableStream<Uint8Array> } {
43
+ let syncState: SyncState = automerge.initSyncState();
44
+
45
+ let unsub: (() => void) | undefined;
46
+ const readable = new ReadableStream({
47
+ start: (controller) => {
48
+ unsub = this.changeEvent.on(() => {
49
+ const [newState, msg] = automerge.generateSyncMessage(this.doc, syncState);
50
+ syncState = newState;
51
+
52
+ if (msg) {
53
+ this.stats.messagesSent++;
54
+ this.stats.bytesSent += msg.byteLength;
55
+ controller.enqueue(msg);
56
+ }
57
+ });
58
+ },
59
+ pull: (controller) => {
60
+ const [newState, msg] = automerge.generateSyncMessage(this.doc, syncState);
61
+ syncState = newState;
62
+
63
+ if (msg) {
64
+ this.stats.messagesSent++;
65
+ this.stats.bytesSent += msg.byteLength;
66
+ controller.enqueue(msg);
67
+ }
68
+ },
69
+ cancel: () => {
70
+ unsub?.();
71
+ },
72
+ });
73
+
74
+ const writable = new WritableStream({
75
+ write: (chunk) => {
76
+ this.stats.messagesReceived++;
77
+ this.stats.bytesReceived += chunk.byteLength;
78
+
79
+ const [newDoc, newState] = automerge.receiveSyncMessage(this.doc, syncState, chunk);
80
+ this.doc = newDoc;
81
+ syncState = newState;
82
+
83
+ this._handleChange();
84
+ },
85
+ });
86
+
87
+ return { readable, writable };
88
+ }
89
+
90
+ private _handleChange() {
91
+ // TODO(dmaretskyi): Save since + debounce.
92
+
93
+ if (this.stats.incrementalBytes > 1024 && this.stats.incrementalBytes > this.stats.snapshotBytes) {
94
+ const snapshot = automerge.save(this.doc);
95
+ this.stats.snapshotBytes = snapshot.byteLength;
96
+ this.stats.incrementalBytes = 0;
97
+ console.log('snapshot save', snapshot.byteLength);
98
+ } else {
99
+ const chunk = automerge.saveIncremental(this.doc);
100
+ this.stats.incrementalBytes += chunk.length;
101
+ console.log('incremental save', chunk.length);
102
+ }
103
+
104
+ this.changeEvent.emit();
105
+ }
106
+ }