edytor 3.0.0 → 4.0.0
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/README.md +2 -2
- package/dist/cloudflare/DocumentRoom.d.ts +13 -9
- package/dist/cloudflare/DocumentRoom.js +10 -6
- package/dist/cloudflare/room/context.d.ts +27 -26
- package/dist/cloudflare/room/context.js +2 -2
- package/dist/cloudflare/room/purge.d.ts +6 -3
- package/dist/cloudflare/room/purge.js +44 -19
- package/dist/cloudflare/room/updates.d.ts +3 -4
- package/dist/cloudflare/room/updates.js +6 -8
- package/dist/crdt/attribution/attribution.d.ts +31 -12
- package/dist/crdt/attribution/attribution.js +56 -18
- package/dist/crdt/attribution/block.d.ts +3 -3
- package/dist/crdt/attribution/block.js +7 -8
- package/dist/crdt/attribution/index.d.ts +1 -1
- package/dist/crdt/attribution/index.js +1 -1
- package/dist/crdt/data.d.ts +62 -31
- package/dist/crdt/data.js +211 -103
- package/dist/crdt/doc/delete.d.ts +1 -1
- package/dist/crdt/doc/delete.js +13 -30
- package/dist/crdt/doc/funnel.js +29 -24
- package/dist/crdt/doc/gate.d.ts +3 -3
- package/dist/crdt/doc/history.d.ts +25 -9
- package/dist/crdt/doc/history.js +150 -40
- package/dist/crdt/doc/layout.js +11 -22
- package/dist/crdt/doc/meta.js +12 -17
- package/dist/crdt/doc/moves.d.ts +1 -1
- package/dist/crdt/doc/moves.js +52 -32
- package/dist/crdt/doc/reads.js +2 -2
- package/dist/crdt/doc/seed.js +2 -2
- package/dist/crdt/doc/split-merge.js +12 -15
- package/dist/crdt/doc/steps.d.ts +25 -13
- package/dist/crdt/doc/steps.js +52 -122
- package/dist/crdt/doc/types.d.ts +21 -9
- package/dist/crdt/document.d.ts +16 -5
- package/dist/crdt/document.js +21 -4
- package/dist/crdt/edytor-doc.d.ts +62 -64
- package/dist/crdt/edytor-doc.js +18 -13
- package/dist/crdt/engine-api.d.ts +33 -1
- package/dist/crdt/engine.d.ts +0 -1
- package/dist/crdt/engine.js +1 -4
- package/dist/crdt/flow.d.ts +1 -1
- package/dist/crdt/flow.js +25 -27
- package/dist/crdt/index.d.ts +26 -25
- package/dist/crdt/index.js +1 -1
- package/dist/crdt/placement/display.d.ts +5 -30
- package/dist/crdt/placement/display.js +39 -144
- package/dist/crdt/placement/keys.d.ts +58 -0
- package/dist/crdt/placement/keys.js +90 -0
- package/dist/crdt/placement/model.d.ts +119 -118
- package/dist/crdt/placement/model.js +242 -142
- package/dist/crdt/placement/resolve.d.ts +123 -35
- package/dist/crdt/placement/resolve.js +187 -153
- package/dist/crdt/placement/slots.d.ts +134 -0
- package/dist/crdt/placement/slots.js +152 -0
- package/dist/crdt/providers/room.d.ts +16 -2
- package/dist/crdt/providers/room.js +64 -17
- package/dist/crdt/providers/websocket.js +5 -3
- package/dist/crdt/purge.d.ts +17 -8
- package/dist/crdt/purge.js +236 -126
- package/dist/crdt/restore.d.ts +2 -2
- package/dist/crdt/restore.js +18 -34
- package/dist/crdt/schema.d.ts +22 -21
- package/dist/crdt/schema.js +29 -23
- package/dist/crdt/storage.d.ts +1 -1
- package/dist/crdt/storage.js +1 -1
- package/dist/crdt/structs.d.ts +15 -15
- package/dist/crdt/structs.js +8 -22
- package/dist/crdt/text/index/cache.js +4 -0
- package/dist/crdt/text/index/claims.js +12 -0
- package/dist/crdt/text/index/fold.d.ts +2 -3
- package/dist/crdt/text/index/fold.js +194 -44
- package/dist/crdt/text/index/layout.js +5 -8
- package/dist/crdt/text/index/placement.d.ts +26 -18
- package/dist/crdt/text/index/placement.js +404 -138
- package/dist/crdt/text/index/records.d.ts +3 -3
- package/dist/crdt/text/index/records.js +24 -31
- package/dist/crdt/text/index/shared.d.ts +11 -3
- package/dist/crdt/text/index/shared.js +12 -3
- package/dist/crdt/text/index/state.d.ts +21 -17
- package/dist/crdt/text/index/state.js +15 -17
- package/dist/crdt/text/index/streams.d.ts +10 -5
- package/dist/crdt/text/index/streams.js +78 -27
- package/dist/crdt/text/items.d.ts +10 -3
- package/dist/crdt/text/items.js +7 -0
- package/dist/crdt/text/model.d.ts +77 -62
- package/dist/crdt/text/model.js +84 -169
- package/dist/crdt/text/ranges.d.ts +1 -1
- package/dist/crdt/text/ranges.js +3 -3
- package/dist/crdt/text/rows.d.ts +13 -7
- package/dist/crdt/text/rows.js +24 -7
- package/dist/crdt/text/runs.d.ts +17 -12
- package/dist/crdt/text/runs.js +24 -7
- package/dist/crdt/text/streams.d.ts +26 -13
- package/dist/crdt/text/streams.js +41 -14
- package/dist/crdt/vendor/yjs/UPSTREAM.md +568 -21
- package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
- package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +194 -10
- package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
- package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
- package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
- package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
- package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
- package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
- package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
- package/dist/crdt/vendor/yjs/dts/ynode.d.ts +43 -7
- package/dist/crdt/vendor/yjs/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
- package/dist/crdt/vendor/yjs/src/structs/Item.js +610 -51
- package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
- package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
- package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
- package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
- package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
- package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
- package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
- package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
- package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
- package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
- package/dist/crdt/vendor/yjs/src/ynode.js +177 -12
- package/dist/edytor.svelte.js +4 -1
- package/dist/events/beforeInputCommands.js +2 -2
- package/dist/events/onPageHide.d.ts +3 -0
- package/dist/events/onPageHide.js +16 -0
- package/dist/export/forms.d.ts +25 -25
- package/dist/surface/observer.svelte.js +2 -2
- package/dist/surface/projector.svelte.d.ts +15 -3
- package/dist/surface/projector.svelte.js +59 -8
- package/package.json +2 -2
- package/dist/crdt/placement/rank.d.ts +0 -87
- package/dist/crdt/placement/rank.js +0 -332
- package/dist/crdt/placement/source.d.ts +0 -43
- package/dist/crdt/placement/source.js +0 -66
- package/dist/crdt/text/delete-index.d.ts +0 -129
- package/dist/crdt/text/delete-index.js +0 -365
- package/dist/crdt/text/deletes.d.ts +0 -76
- package/dist/crdt/text/deletes.js +0 -362
- package/dist/crdt/text/index/anchored.d.ts +0 -17
- package/dist/crdt/text/index/anchored.js +0 -197
package/dist/crdt/doc/funnel.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* frame's one attribution pass.
|
|
6
|
-
*/
|
|
7
|
-
import { blockNodeOf, buildInline, materializeSpec, ownText, view as modelView, writePlacement, writeSplit } from '../placement/model.js';
|
|
8
|
-
import { ENTRY_FACET } from '../text/runs.js';
|
|
9
|
-
import { AT, DEL_PREFIX, ID, LAST_CHANGED_ATTR, TYPE } from '../schema.js';
|
|
10
|
-
import { writeLeaves } from '../data.js';
|
|
1
|
+
import { blockNodeOf, buildInline, insertSpecsAt, moveBlocksTo, ownText, view as modelView, writeSplit } from '../placement/model.js';
|
|
2
|
+
import { ENTRY_FACET, SEQ_FACET } from '../text/runs.js';
|
|
3
|
+
import { DEL_PREFIX, ID, LAST_CHANGED_ATTR, TYPE } from '../schema.js';
|
|
4
|
+
import { writeData } from '../data.js';
|
|
11
5
|
import { NOOP } from './plan.js';
|
|
12
6
|
import { claimInto, deleteRange, formatRangeIn, insertIntoText } from '../text/model.js';
|
|
13
7
|
/**
|
|
@@ -55,8 +49,8 @@ export const writeFunnel = (c) => {
|
|
|
55
49
|
// Each apply opens a frame that tracks the index's folds. When it
|
|
56
50
|
// closes, ONE pass over the facets the fold saw stamps attribution: a
|
|
57
51
|
// block whose registry entry was created gets its `createdBy` record; a block whose content, claims, type or data
|
|
58
|
-
// changed gets a contributor stamp; moves
|
|
59
|
-
// nothing. Two intents the effects cannot name are recorded by their
|
|
52
|
+
// changed gets a contributor stamp; moves (a record or a slot in a
|
|
53
|
+
// block's sequence) and delete marks stamp nothing. Two intents the effects cannot name are recorded by their
|
|
60
54
|
// steps: a split-born block inherits its source's contributors, and a
|
|
61
55
|
// merge survivor unions the absorbed block's. With no configured
|
|
62
56
|
// `actor` the funnel writes no attribution at all.
|
|
@@ -74,9 +68,12 @@ export const writeFunnel = (c) => {
|
|
|
74
68
|
else
|
|
75
69
|
for (const facet of facets)
|
|
76
70
|
if (!facet.startsWith(DEL_PREFIX) &&
|
|
77
|
-
![ENTRY_FACET,
|
|
71
|
+
![ENTRY_FACET, SEQ_FACET, ID, LAST_CHANGED_ATTR].includes(facet))
|
|
78
72
|
changed.add(id);
|
|
79
73
|
}
|
|
74
|
+
// A merge changed its survivor (its claim item, written quietly, is no edit of the text holding it).
|
|
75
|
+
for (const [into] of f.unions)
|
|
76
|
+
changed.add(into);
|
|
80
77
|
for (const id of created) {
|
|
81
78
|
const node = blockNodeOf(doc, id);
|
|
82
79
|
if (node !== null)
|
|
@@ -93,38 +90,46 @@ export const writeFunnel = (c) => {
|
|
|
93
90
|
/**
|
|
94
91
|
* A streamless block gets its own text before the first write that
|
|
95
92
|
* needs a stream (`ownText`: a writer derived from its dead
|
|
96
|
-
* incarnation); its attribution record follows the re-minted nonce.
|
|
93
|
+
* incarnation); its attribution record follows the re-minted nonce. A
|
|
94
|
+
* claim item needs the block's own stream (`claim`); text, any piece of
|
|
95
|
+
* its display.
|
|
97
96
|
*/
|
|
98
|
-
const needStream = (id) => {
|
|
99
|
-
|
|
97
|
+
const needStream = (id, claim = false) => {
|
|
98
|
+
const { own } = modelView(doc);
|
|
99
|
+
if (claim ? own.streamOf(id) !== undefined : (own.display(id) ?? []).length > 0)
|
|
100
100
|
return;
|
|
101
101
|
const { from, to } = ownText(doc, id);
|
|
102
102
|
BA.retarget(doc, id, from, to);
|
|
103
103
|
};
|
|
104
104
|
/** One planned step, written (the plan decided it; writers never refuse). */
|
|
105
105
|
const writeStep = (w, f) => {
|
|
106
|
-
//
|
|
106
|
+
// Delete marks need no view: they write one node.
|
|
107
107
|
if (w.op === 'deleteBlock')
|
|
108
108
|
return w.marks.forEach((id) => blockNodeOf(doc, id).setAttr(DEL_PREFIX + doc.clientID, true));
|
|
109
109
|
if (w.op === 'splitBlock' || w.op === 'insertText' || w.op === 'insertInline')
|
|
110
110
|
needStream(w.id);
|
|
111
|
-
|
|
111
|
+
// A claim item goes at the end of the stream of the block merged into.
|
|
112
|
+
if (w.op === 'mergeBlocks')
|
|
113
|
+
needStream(w.into, true);
|
|
114
|
+
const v = modelView(doc);
|
|
115
|
+
const { blocks, own } = v;
|
|
112
116
|
const node = (id) => blockNodeOf(doc, id);
|
|
113
117
|
switch (w.op) {
|
|
114
118
|
case 'insertBlocks':
|
|
115
|
-
return
|
|
119
|
+
return insertSpecsAt(doc, v, w.parent, w.index, w.specs, w.split === undefined ? null : (f.bounds.get(w.split) ?? null));
|
|
116
120
|
case 'moveBlocks':
|
|
117
|
-
return w.ids.
|
|
121
|
+
return moveBlocksTo(doc, v, w.ids, w.parent, w.index, w.side ?? 0, w.part ?? 0, w.at ?? null);
|
|
118
122
|
case 'splitBlock':
|
|
119
123
|
f.inherit.set(w.newId, w.id);
|
|
120
|
-
|
|
124
|
+
f.bounds.set(w.newId, writeSplit(doc, w.id, w.offset, w.newId, w.tail, w.parent, w.index));
|
|
125
|
+
return;
|
|
121
126
|
case 'mergeBlocks':
|
|
122
127
|
f.unions.push([w.into, w.from]);
|
|
123
|
-
return claimInto(
|
|
128
|
+
return f.quiet(() => claimInto(own, w.from, w.into));
|
|
124
129
|
case 'setBlockType':
|
|
125
130
|
return void node(w.id).setAttr(TYPE, w.type);
|
|
126
131
|
case 'patchData':
|
|
127
|
-
return
|
|
132
|
+
return writeData(dataNode(w.id, w.inlineId), w.writes);
|
|
128
133
|
case 'insertText':
|
|
129
134
|
return insertIntoText(doc, blocks, own, w.id, w.offset, w.text, w.marks);
|
|
130
135
|
case 'insertInline':
|
|
@@ -150,7 +155,7 @@ export const writeFunnel = (c) => {
|
|
|
150
155
|
}
|
|
151
156
|
return write(() => doc.transact(() => {
|
|
152
157
|
const frame = runsView.track();
|
|
153
|
-
const f = { inherit: new Map(), unions: [] };
|
|
158
|
+
const f = { inherit: new Map(), bounds: new Map(), unions: [], quiet: frame.quiet };
|
|
154
159
|
try {
|
|
155
160
|
for (const w of p.writes)
|
|
156
161
|
writeStep(w, f);
|
package/dist/crdt/doc/gate.d.ts
CHANGED
|
@@ -17,15 +17,15 @@
|
|
|
17
17
|
*
|
|
18
18
|
* ```
|
|
19
19
|
* doc.get('blocks') registry — flat map, blockId → node('block')
|
|
20
|
-
* └ <blockId>
|
|
21
|
-
* (n: incarnation nonce)
|
|
20
|
+
* └ <blockId> n/type/d/…/del./wd./l + content
|
|
21
|
+
* (n: incarnation nonce; the id is the key)
|
|
22
22
|
* doc.get('meta') version record root
|
|
23
23
|
* ├ v : number SCHEMA_VERSION (LWW attr — concurrent init converges)
|
|
24
24
|
* └ schema : 'edytor-doc' SCHEMA_NAME
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
27
|
import type { EngineDoc } from '../engine-api.js';
|
|
28
|
-
export declare const SCHEMA_VERSION:
|
|
28
|
+
export declare const SCHEMA_VERSION: 10;
|
|
29
29
|
export declare const SCHEMA_NAME: "edytor-doc";
|
|
30
30
|
export declare const META_KEY: "meta";
|
|
31
31
|
/**
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { EngineApi, EngineDoc, EngineNode } from '../engine-api.js';
|
|
2
|
-
import type { TextDeletes } from '../text/deletes.js';
|
|
3
2
|
import { type IdSetOf, type StoreStruct } from '../structs.js';
|
|
4
3
|
import { type WriteFunnel } from './funnel.js';
|
|
5
4
|
import type { DocBase } from './reads.js';
|
|
@@ -33,15 +32,12 @@ export type HistoryOptions = {
|
|
|
33
32
|
}) => boolean;
|
|
34
33
|
};
|
|
35
34
|
/** What a history is made over: the document and the rules of its replay. */
|
|
36
|
-
type Rules = {
|
|
35
|
+
export type Rules = {
|
|
37
36
|
Y: EngineApi;
|
|
38
37
|
doc: EngineDoc;
|
|
39
38
|
scope: EngineNode[];
|
|
40
|
-
/** The
|
|
41
|
-
|
|
42
|
-
restoreFilter(item: unknown): boolean;
|
|
43
|
-
onApply(tr: unknown, step: StackItem): void;
|
|
44
|
-
};
|
|
39
|
+
/** The records list a replay's text holds go to (fork patch YP20). */
|
|
40
|
+
records: EngineNode;
|
|
45
41
|
/** Whether an item a replay would delete is withdrawn instead (`hist.undo.withdraw`). */
|
|
46
42
|
withdraw(item: StoreStruct, step: StackItem): boolean;
|
|
47
43
|
/** Repair the attrs a replay left undefined. */
|
|
@@ -85,16 +81,36 @@ export declare class UndoManager {
|
|
|
85
81
|
redo(): StackItem | null;
|
|
86
82
|
/** Drop every step (undo and redo) and release what they kept for their replay. */
|
|
87
83
|
clear(): void;
|
|
84
|
+
/**
|
|
85
|
+
* The session ends: its page left for good (`hist.release.end`). Every
|
|
86
|
+
* step is dropped and released as a dropped step is (`#release`: what
|
|
87
|
+
* they kept is collected, the text this writer's records hold is
|
|
88
|
+
* deleted for good), in one transaction that, when a step marked a block
|
|
89
|
+
* deleted or withdrawn, also appends one release record (the
|
|
90
|
+
* {@link RELEASE_ROOT} list): its id names this writer and the clock the
|
|
91
|
+
* history ended at, so the room's purge removes what those marks hide at
|
|
92
|
+
* its next tick, past the horizon or not. Then the manager stops
|
|
93
|
+
* capturing (it is destroyed; the document attaches a fresh one if asked).
|
|
94
|
+
* A history that captured nothing writes nothing. Only for a page that
|
|
95
|
+
* cannot come back: a page the back/forward cache keeps, or one that
|
|
96
|
+
* only lost its connection, keeps its history.
|
|
97
|
+
*/
|
|
98
|
+
end(): void;
|
|
88
99
|
destroy: () => void;
|
|
89
100
|
}
|
|
90
101
|
/** What a facade's history is made over: its doc, the write funnel and the bound engine layers. */
|
|
91
102
|
export type HistoryContext = DocBase & Pick<WriteFunnel, 'write' | 'disposed'> & {
|
|
92
|
-
D: TextDeletes;
|
|
93
103
|
/** Stamp the version record before the history attaches. */
|
|
94
104
|
init: (doc: EngineDoc) => void;
|
|
95
105
|
};
|
|
106
|
+
/**
|
|
107
|
+
* The document's history rules over `doc` (the one set every history of an
|
|
108
|
+
* edytor document runs): its scope (the block registry, the text hold
|
|
109
|
+
* records, the document's data), the records a replay's text holds go to,
|
|
110
|
+
* the withdraw rule (`hist.undo.withdraw`) and the attr repair below.
|
|
111
|
+
*/
|
|
112
|
+
export declare const historyRules: (Y: EngineApi, doc: EngineDoc) => Rules;
|
|
96
113
|
/** The history factory of one facade. */
|
|
97
114
|
export declare const docHistory: (c: HistoryContext) => {
|
|
98
115
|
createUndoManager: (options?: HistoryOptions) => UndoManager;
|
|
99
116
|
};
|
|
100
|
-
export {};
|
package/dist/crdt/doc/history.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The document's history: the one undo manager (fork patch YP18 removed the
|
|
3
3
|
* engine's). It captures the tracked local transactions that write in its
|
|
4
|
-
* scope (the block registry, the text
|
|
4
|
+
* scope (the block registry, the text hold records, the document's data)
|
|
5
5
|
* into steps, and an undo or a redo replays a step in one transaction over
|
|
6
|
-
* the engine's struct primitives (`redoItem`, `keepItem`). Its rules: text
|
|
7
|
-
*
|
|
6
|
+
* the engine's struct primitives (`redoItem`, `keepItem`). Its rules: a text
|
|
7
|
+
* delete is a hold record (fork patch YP20), so undoing it deletes the record
|
|
8
|
+
* and the text shows again unless another writer's record holds it
|
|
9
|
+
* (`hist.undo.restore`); undoing typing holds the typed text; an undone
|
|
8
10
|
* creation is withdrawn (`hist.undo.withdraw`), an overwritten attr is
|
|
9
|
-
* repaired, the stack keeps `limit` steps,
|
|
10
|
-
* steps below it (`hist.purge.horizon`)
|
|
11
|
+
* repaired, the stack keeps `limit` steps, the purge horizon drops the
|
|
12
|
+
* steps below it (`hist.purge.horizon`), and a session's end releases them
|
|
13
|
+
* all (`hist.release.end`).
|
|
11
14
|
*/
|
|
12
15
|
import { getUnixTime } from 'lib0-v14/time';
|
|
13
|
-
import { ownTextIds } from '../text/model.js';
|
|
16
|
+
import { openSteps, ownTextIds } from '../text/model.js';
|
|
14
17
|
import { readHorizon } from '../purge.js';
|
|
15
|
-
import { BLOCK_NODE, DATA_LEAF_PREFIX, DOC_DATA_ROOT, HORIZON_ROOT, INLINE_NODE, LAST_CHANGED_ATTR, TYPE } from '../schema.js';
|
|
18
|
+
import { BLOCK_NODE, CONTENT_NODE, DATA_LEAF_PREFIX, DEL_PREFIX, DOC_DATA_ROOT, HORIZON_ROOT, INLINE_NODE, LAST_CHANGED_ATTR, RELEASE_ROOT, TEXT_DELETES_ROOT, TYPE, WITHDRAW_PREFIX } from '../schema.js';
|
|
16
19
|
import { changedUnder, collectable, collectNow, collects, dropSearchMarkers, engineOps, walkIdSet } from '../structs.js';
|
|
17
20
|
import { isInitialized } from './gate.js';
|
|
18
21
|
import { DocumentDestroyedError } from './funnel.js';
|
|
@@ -37,6 +40,13 @@ const REPAIRED = {
|
|
|
37
40
|
[BLOCK_NODE]: [TYPE, LAST_CHANGED_ATTR],
|
|
38
41
|
[INLINE_NODE]: [TYPE]
|
|
39
42
|
};
|
|
43
|
+
/** A unit of a block's text: a character or an inline atom (what a hold record holds). */
|
|
44
|
+
const isUnit = (it) => {
|
|
45
|
+
const content = it.content;
|
|
46
|
+
return (it.parentSub === null &&
|
|
47
|
+
it.parent?.name === CONTENT_NODE &&
|
|
48
|
+
(typeof content?.str === 'string' || content?.type?.name === INLINE_NODE));
|
|
49
|
+
};
|
|
40
50
|
/**
|
|
41
51
|
* A document's undo manager: the undo and redo stacks of the tracked local
|
|
42
52
|
* transactions in its scope. `undo()`/`redo()` replay the top step and
|
|
@@ -64,6 +74,10 @@ export class UndoManager {
|
|
|
64
74
|
/** What dropped steps released (an item is released once every step that deleted it is gone). */
|
|
65
75
|
#released;
|
|
66
76
|
#horizon;
|
|
77
|
+
/** A captured step wrote a block's delete or withdraw mark: the session's end records its release. */
|
|
78
|
+
#marked = false;
|
|
79
|
+
/** The session ended (`end()`): nothing more is released. */
|
|
80
|
+
#ended = false;
|
|
67
81
|
constructor(rules, options = {}) {
|
|
68
82
|
this.#rules = rules;
|
|
69
83
|
this.#ops = engineOps(rules.Y);
|
|
@@ -78,7 +92,19 @@ export class UndoManager {
|
|
|
78
92
|
this.trackedOrigins.add(this);
|
|
79
93
|
this.#ops.on(rules.doc, 'afterTransaction', this.#afterTransaction);
|
|
80
94
|
this.#ops.on(rules.doc, 'destroy', this.destroy);
|
|
95
|
+
const open = openSteps.get(rules.doc) ?? new Set();
|
|
96
|
+
openSteps.set(rules.doc, open.add(this.#open));
|
|
81
97
|
}
|
|
98
|
+
/** What the step still capturing inserted (`null`: none is open): a text delete there removes. */
|
|
99
|
+
#open = () => {
|
|
100
|
+
const top = this.undoStack.at(-1);
|
|
101
|
+
const open = top !== undefined &&
|
|
102
|
+
!this.undoing &&
|
|
103
|
+
!this.redoing &&
|
|
104
|
+
this.lastChange > 0 &&
|
|
105
|
+
getUnixTime() - this.lastChange < this.captureTimeout;
|
|
106
|
+
return open ? top.inserts : null;
|
|
107
|
+
};
|
|
82
108
|
/** Be told of every step taken into a stack (added, or the open one updated). Returns the unsubscribe. */
|
|
83
109
|
observe(f) {
|
|
84
110
|
this.#observers.add(f);
|
|
@@ -118,8 +144,39 @@ export class UndoManager {
|
|
|
118
144
|
clear() {
|
|
119
145
|
this.#release([...this.undoStack.splice(0), ...this.redoStack.splice(0)]);
|
|
120
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* The session ends: its page left for good (`hist.release.end`). Every
|
|
149
|
+
* step is dropped and released as a dropped step is (`#release`: what
|
|
150
|
+
* they kept is collected, the text this writer's records hold is
|
|
151
|
+
* deleted for good), in one transaction that, when a step marked a block
|
|
152
|
+
* deleted or withdrawn, also appends one release record (the
|
|
153
|
+
* {@link RELEASE_ROOT} list): its id names this writer and the clock the
|
|
154
|
+
* history ended at, so the room's purge removes what those marks hide at
|
|
155
|
+
* its next tick, past the horizon or not. Then the manager stops
|
|
156
|
+
* capturing (it is destroyed; the document attaches a fresh one if asked).
|
|
157
|
+
* A history that captured nothing writes nothing. Only for a page that
|
|
158
|
+
* cannot come back: a page the back/forward cache keeps, or one that
|
|
159
|
+
* only lost its connection, keeps its history.
|
|
160
|
+
*/
|
|
161
|
+
end() {
|
|
162
|
+
if (this.#ended)
|
|
163
|
+
return;
|
|
164
|
+
this.#ended = true;
|
|
165
|
+
const dropped = [...this.undoStack.splice(0), ...this.redoStack.splice(0)];
|
|
166
|
+
const marked = this.#marked;
|
|
167
|
+
if (dropped.length > 0 || marked)
|
|
168
|
+
this.doc.transact(() => {
|
|
169
|
+
this.#release(dropped);
|
|
170
|
+
if (marked) {
|
|
171
|
+
const records = this.doc.get(RELEASE_ROOT);
|
|
172
|
+
records.insert(records.length, [true]);
|
|
173
|
+
}
|
|
174
|
+
}, HISTORY_TRIM);
|
|
175
|
+
this.destroy();
|
|
176
|
+
}
|
|
121
177
|
destroy = () => {
|
|
122
178
|
this.trackedOrigins.delete(this);
|
|
179
|
+
openSteps.get(this.doc)?.delete(this.#open);
|
|
123
180
|
this.#ops.off(this.doc, 'afterTransaction', this.#afterTransaction);
|
|
124
181
|
this.#ops.off(this.doc, 'destroy', this.destroy);
|
|
125
182
|
this.#observers.clear();
|
|
@@ -164,6 +221,14 @@ export class UndoManager {
|
|
|
164
221
|
stack.push(new StackItem(tr.insertSet, tr.deleteSet));
|
|
165
222
|
if (!undoing && !redoing)
|
|
166
223
|
this.lastChange = now;
|
|
224
|
+
// A block's delete or withdraw mark: what it hides waits for this
|
|
225
|
+
// history (its end releases it to the purge, `end()`).
|
|
226
|
+
if (!this.#marked)
|
|
227
|
+
for (const keys of tr.changed.values())
|
|
228
|
+
for (const key of keys)
|
|
229
|
+
if (typeof key === 'string' &&
|
|
230
|
+
(key.startsWith(DEL_PREFIX) || key.startsWith(WITHDRAW_PREFIX)))
|
|
231
|
+
this.#marked = true;
|
|
167
232
|
// What the step deleted is kept from collection while a step may bring it back.
|
|
168
233
|
ops.iterate(tr, tr.deleteSet, (item) => {
|
|
169
234
|
if (ops.isItem(item) && this.#inScope(item))
|
|
@@ -190,13 +255,14 @@ export class UndoManager {
|
|
|
190
255
|
}
|
|
191
256
|
/**
|
|
192
257
|
* Replay the top step of `stack` in one transaction (origin: this
|
|
193
|
-
* manager): what it inserted is deleted (
|
|
194
|
-
*
|
|
258
|
+
* manager): what it inserted is deleted (text: held by a record of this
|
|
259
|
+
* writer; a block: withdrawn), what it deleted comes back (a text delete
|
|
260
|
+
* was a record: deleting it shows the text), the overwritten attrs are
|
|
195
261
|
* repaired. A step that changes nothing is skipped for the one below.
|
|
196
262
|
*/
|
|
197
263
|
#pop(stack) {
|
|
198
264
|
const ops = this.#ops;
|
|
199
|
-
const {
|
|
265
|
+
const { records, withdraw, repair } = this.#rules;
|
|
200
266
|
const deletedByHistory = (id) => [this.undoStack, this.redoStack].some((s) => s.some((step) => step.deletes.has(id.client, id.clock)));
|
|
201
267
|
this.doc.transact((transaction) => {
|
|
202
268
|
const tr = transaction;
|
|
@@ -204,11 +270,18 @@ export class UndoManager {
|
|
|
204
270
|
const step = stack.pop();
|
|
205
271
|
const restore = new Set();
|
|
206
272
|
const remove = [];
|
|
273
|
+
const held = [];
|
|
207
274
|
let changed = false;
|
|
208
275
|
ops.iterate(tr, step.inserts, (struct) => {
|
|
209
276
|
if (!ops.isItem(struct))
|
|
210
277
|
return;
|
|
211
278
|
const item = ops.latest(tr, struct);
|
|
279
|
+
// An atom the step inserted is held whole: its attrs stay with it.
|
|
280
|
+
const atom = item.parent?._item;
|
|
281
|
+
if (atom?.id &&
|
|
282
|
+
isUnit(atom) &&
|
|
283
|
+
step.inserts.has(atom.id.client, atom.id.clock))
|
|
284
|
+
return;
|
|
212
285
|
if (!item.deleted && this.#inScope(item))
|
|
213
286
|
remove.push(item);
|
|
214
287
|
});
|
|
@@ -218,25 +291,27 @@ export class UndoManager {
|
|
|
218
291
|
// Created and deleted within the step: nothing to bring back.
|
|
219
292
|
if (step.inserts.has(struct.id.client, struct.id.clock))
|
|
220
293
|
return;
|
|
221
|
-
// Text
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
restore.add(struct);
|
|
294
|
+
// Text a record holds stays hidden: the record decides (removed with its node).
|
|
295
|
+
if ((struct.holds ?? 0) > 0)
|
|
296
|
+
return;
|
|
297
|
+
restore.add(struct);
|
|
226
298
|
});
|
|
227
299
|
for (const item of restore)
|
|
228
300
|
changed = ops.redo(tr, item, restore, step.inserts, deletedByHistory) !== null || changed;
|
|
229
301
|
// Children before parents: more is known when an item is judged.
|
|
230
302
|
for (let i = remove.length - 1; i >= 0; i--) {
|
|
231
|
-
|
|
232
|
-
|
|
303
|
+
const item = remove[i];
|
|
304
|
+
// Text the step wrote is held, never removed: a redo shows it again where it was.
|
|
305
|
+
if (isUnit(item))
|
|
306
|
+
held.push(item.id.client, item.id.clock, item.length);
|
|
307
|
+
else if (!withdraw(item, step))
|
|
308
|
+
ops.remove(tr, item);
|
|
233
309
|
changed = true;
|
|
234
310
|
}
|
|
311
|
+
records.hold(held);
|
|
235
312
|
this.currStackItem = changed ? step : null;
|
|
236
|
-
if (changed)
|
|
237
|
-
marks.onApply(tr, step);
|
|
313
|
+
if (changed)
|
|
238
314
|
repair(step);
|
|
239
|
-
}
|
|
240
315
|
}
|
|
241
316
|
tr.changed.forEach((keys, node) => {
|
|
242
317
|
if (keys.has(null))
|
|
@@ -250,8 +325,8 @@ export class UndoManager {
|
|
|
250
325
|
/**
|
|
251
326
|
* Release what dropped steps kept for their replay: the engine collects
|
|
252
327
|
* that content now, as it would have at the delete without a history
|
|
253
|
-
* (the doc's `gcFilter` still decides
|
|
254
|
-
*
|
|
328
|
+
* (the doc's `gcFilter` still decides), and the text their hold records
|
|
329
|
+
* hold is deleted for good. An item is released only
|
|
255
330
|
* once every step that deleted part of it was dropped (the engine merges
|
|
256
331
|
* deleted items across steps): never split, so the store keeps its
|
|
257
332
|
* merged items. Only the items themselves are released: a kept
|
|
@@ -269,6 +344,8 @@ export class UndoManager {
|
|
|
269
344
|
return (released.clients.get(st.id.client)?.getIds() ?? []).some((r) => r.clock <= st.id.clock && end <= r.clock + r.len);
|
|
270
345
|
};
|
|
271
346
|
const gc = collects(doc);
|
|
347
|
+
const ops = this.#ops;
|
|
348
|
+
const { records } = this.#rules;
|
|
272
349
|
doc.transact((tr) => {
|
|
273
350
|
for (const step of dropped)
|
|
274
351
|
walkIdSet(Y, doc, step.deletes, (st) => {
|
|
@@ -279,6 +356,32 @@ export class UndoManager {
|
|
|
279
356
|
if (gc && it.deleted && collectable(doc, it))
|
|
280
357
|
collectNow(it, tr);
|
|
281
358
|
});
|
|
359
|
+
// A dropped step's text deletes can never be undone here: what this
|
|
360
|
+
// writer's live records hold is deleted for good, as the purge does,
|
|
361
|
+
// and the engine collects it (`hist.delete-marks.release`). A record
|
|
362
|
+
// a captured remote frame wrote (the room's validation) is its
|
|
363
|
+
// writer's to take back.
|
|
364
|
+
const own = ops.idSet();
|
|
365
|
+
for (const step of dropped)
|
|
366
|
+
walkIdSet(Y, doc, step.inserts, (st, from, to) => {
|
|
367
|
+
if (st.parent === records && !st.deleted && st.id.client === doc.clientID)
|
|
368
|
+
own.add(st.id.client, from, to - from);
|
|
369
|
+
});
|
|
370
|
+
const held = ops.idSet();
|
|
371
|
+
ops.iterate(tr, own, (st) => {
|
|
372
|
+
const ids = st.content?.ids;
|
|
373
|
+
if (st.deleted || ids === undefined)
|
|
374
|
+
return;
|
|
375
|
+
for (let i = 0; i < ids.length; i += 3)
|
|
376
|
+
held.add(ids[i], ids[i + 1], ids[i + 2]);
|
|
377
|
+
ops.remove(tr, st);
|
|
378
|
+
});
|
|
379
|
+
ops.iterate(tr, held, (it) => {
|
|
380
|
+
if (it.removed !== false)
|
|
381
|
+
return;
|
|
382
|
+
ops.remove(tr, it);
|
|
383
|
+
dropSearchMarkers(it.parent);
|
|
384
|
+
});
|
|
282
385
|
}, HISTORY_TRIM);
|
|
283
386
|
}
|
|
284
387
|
/**
|
|
@@ -310,9 +413,13 @@ export class UndoManager {
|
|
|
310
413
|
this.#release(dropped);
|
|
311
414
|
}
|
|
312
415
|
}
|
|
313
|
-
/**
|
|
314
|
-
|
|
315
|
-
|
|
416
|
+
/**
|
|
417
|
+
* The document's history rules over `doc` (the one set every history of an
|
|
418
|
+
* edytor document runs): its scope (the block registry, the text hold
|
|
419
|
+
* records, the document's data), the records a replay's text holds go to,
|
|
420
|
+
* the withdraw rule (`hist.undo.withdraw`) and the attr repair below.
|
|
421
|
+
*/
|
|
422
|
+
export const historyRules = (Y, doc) => {
|
|
316
423
|
/**
|
|
317
424
|
* Undo repair, inside the replay's transaction. A replayed step
|
|
318
425
|
* deletes its own attr write and re-creates the value that write
|
|
@@ -350,9 +457,25 @@ export const docHistory = (c) => {
|
|
|
350
457
|
node.setAttr(key, value);
|
|
351
458
|
}
|
|
352
459
|
};
|
|
460
|
+
// The text hold records are in scope (an undo deletes the undoer's
|
|
461
|
+
// own, fork patch YP20). An undone creation withdraws the block instead
|
|
462
|
+
// of deleting it (`hist.undo.withdraw`).
|
|
463
|
+
const records = doc.get(TEXT_DELETES_ROOT);
|
|
464
|
+
return {
|
|
465
|
+
Y,
|
|
466
|
+
doc,
|
|
467
|
+
scope: [registryOf(doc), records, doc.get(DOC_DATA_ROOT)],
|
|
468
|
+
records,
|
|
469
|
+
withdraw: withdrawOnUndo(doc),
|
|
470
|
+
repair: repairAttrs
|
|
471
|
+
};
|
|
472
|
+
};
|
|
473
|
+
/** The history factory of one facade. */
|
|
474
|
+
export const docHistory = (c) => {
|
|
475
|
+
const { Y, doc, init, write, disposed } = c;
|
|
353
476
|
/**
|
|
354
477
|
* The document's undo manager, SCOPED TO THE BLOCK REGISTRY (with the
|
|
355
|
-
* text
|
|
478
|
+
* text hold records and the document's data), attached only after the
|
|
356
479
|
* doc carries the schema version record:
|
|
357
480
|
*
|
|
358
481
|
* - the `meta` root (version stamp, schema name) is outside the scope,
|
|
@@ -373,20 +496,7 @@ export const docHistory = (c) => {
|
|
|
373
496
|
if (!isInitialized(doc))
|
|
374
497
|
init(doc);
|
|
375
498
|
});
|
|
376
|
-
|
|
377
|
-
// removes the undoer's own), and the history restores text only as the
|
|
378
|
-
// marks allow. An undone creation withdraws the block instead of
|
|
379
|
-
// deleting it (`hist.undo.withdraw`).
|
|
380
|
-
const marks = D.history(doc, () => um);
|
|
381
|
-
const um = new UndoManager({
|
|
382
|
-
Y,
|
|
383
|
-
doc,
|
|
384
|
-
scope: [registryOf(doc), D.scope(doc), doc.get(DOC_DATA_ROOT)],
|
|
385
|
-
marks,
|
|
386
|
-
withdraw: withdrawOnUndo(doc),
|
|
387
|
-
repair: repairAttrs
|
|
388
|
-
}, options);
|
|
389
|
-
return um;
|
|
499
|
+
return new UndoManager(historyRules(Y, doc), options);
|
|
390
500
|
};
|
|
391
501
|
return { createUndoManager };
|
|
392
502
|
};
|
package/dist/crdt/doc/layout.js
CHANGED
|
@@ -8,7 +8,7 @@ import { sanitizeSpec } from '../../utils/json.js';
|
|
|
8
8
|
import { ref } from './plan.js';
|
|
9
9
|
/** The layout ops of one facade, prepared. */
|
|
10
10
|
export const layoutOps = (c) => {
|
|
11
|
-
const { roles, view, blockTypeOf, childrenIds, positionOf, ancestorsOf, isIsland, isLines, insideIsland, isLayout, isLayoutItem, holdsLayout, insideItem, live, canPlace, isContainer, fits, fitsIn, REFUSED, plan,
|
|
11
|
+
const { roles, view, blockTypeOf, childrenIds, positionOf, ancestorsOf, isIsland, isLines, insideIsland, isLayout, isLayoutItem, holdsLayout, insideItem, live, canPlace, isContainer, fits, fitsIn, REFUSED, plan, moveTo, landing, emptied } = c;
|
|
12
12
|
/** The layout kind a new layout takes: `kind` when it is one, else the only one the roles declare. */
|
|
13
13
|
const layoutKind = (kind) => {
|
|
14
14
|
if (kind !== undefined)
|
|
@@ -62,8 +62,8 @@ export const layoutOps = (c) => {
|
|
|
62
62
|
* an item, a layout or a block holding one, a block that does not
|
|
63
63
|
* fit an item, a block or a layout inside an island, and, wrapping,
|
|
64
64
|
* when there is no layout kind. The sources are cleaned in the same
|
|
65
|
-
* plan (`emptying`, `dissolving`); plain
|
|
66
|
-
*
|
|
65
|
+
* plan (`emptying`, `dissolving`); a plain move. `ids`: the moved
|
|
66
|
+
* blocks.
|
|
67
67
|
*/
|
|
68
68
|
const placeBeside = (ids, target, side, kind) => {
|
|
69
69
|
const moved = ids.map(ref);
|
|
@@ -98,25 +98,20 @@ export const layoutOps = (c) => {
|
|
|
98
98
|
if (layout !== null && (isIsland(layout) || insideIsland(layout, v)))
|
|
99
99
|
return REFUSED;
|
|
100
100
|
const fresh = spec(item);
|
|
101
|
-
const into = (parent) => moveTo(moved, parent, 0
|
|
101
|
+
const into = (parent) => moveTo(moved, parent, 0);
|
|
102
102
|
let writes;
|
|
103
103
|
let kept;
|
|
104
104
|
if (layout !== null) {
|
|
105
|
-
|
|
106
|
-
writes = [
|
|
107
|
-
{ op: 'insertBlocks', parent: layout, index, specs: [fresh], ranks },
|
|
108
|
-
...into(fresh.id)
|
|
109
|
-
];
|
|
105
|
+
writes = [{ op: 'insertBlocks', parent: layout, index, specs: [fresh] }, ...into(fresh.id)];
|
|
110
106
|
kept = new Set([fresh.id, ...landing(layout)]);
|
|
111
107
|
}
|
|
112
108
|
else {
|
|
113
109
|
const pos = positionOf(at);
|
|
114
110
|
const host = spec(item);
|
|
115
111
|
const wrapper = { ...spec(wrap), children: right ? [host, fresh] : [fresh, host] };
|
|
116
|
-
const ranks = ranksFor(pos.parent, pos.index, 1);
|
|
117
112
|
writes = [
|
|
118
|
-
{ op: 'insertBlocks', parent: pos.parent, index: pos.index, specs: [wrapper]
|
|
119
|
-
...moveTo([at], host.id, 0
|
|
113
|
+
{ op: 'insertBlocks', parent: pos.parent, index: pos.index, specs: [wrapper] },
|
|
114
|
+
...moveTo([at], host.id, 0),
|
|
120
115
|
...into(fresh.id)
|
|
121
116
|
];
|
|
122
117
|
kept = new Set([wrapper.id, host.id, fresh.id, ...landing(pos.parent)]);
|
|
@@ -134,8 +129,8 @@ export const layoutOps = (c) => {
|
|
|
134
129
|
* fewer items than blocks, blocks of different parents, an item, a
|
|
135
130
|
* layout or a block holding one, where the layout does not fit
|
|
136
131
|
* (`fits`: in a list, a code block) or lands inside an item or an
|
|
137
|
-
* island, and for a block that does not fit an item.
|
|
138
|
-
*
|
|
132
|
+
* island, and for a block that does not fit an item. A plain move.
|
|
133
|
+
* `ids`: the new layout.
|
|
139
134
|
*/
|
|
140
135
|
const wrapInLayout = (ids, kind, columns = ids.length) => {
|
|
141
136
|
const blocks = ids.map(ref);
|
|
@@ -165,14 +160,8 @@ export const layoutOps = (c) => {
|
|
|
165
160
|
const wrapper = { ...spec(wrap), children: items };
|
|
166
161
|
const at = positionOf(ordered[0]).index;
|
|
167
162
|
const writes = [
|
|
168
|
-
{
|
|
169
|
-
|
|
170
|
-
parent,
|
|
171
|
-
index: at,
|
|
172
|
-
specs: [wrapper],
|
|
173
|
-
ranks: ranksFor(parent, at, 1)
|
|
174
|
-
},
|
|
175
|
-
...ordered.flatMap((id, i) => moveTo([id], items[i].id, 0, ranksFor(items[i].id, 0, 1)))
|
|
163
|
+
{ op: 'insertBlocks', parent, index: at, specs: [wrapper] },
|
|
164
|
+
...ordered.flatMap((id, i) => moveTo([id], items[i].id, 0))
|
|
176
165
|
];
|
|
177
166
|
const kept = new Set([
|
|
178
167
|
wrapper.id,
|