Skip to content

Commit 75e2620

Browse files
committed
feat(json-crdt-extensions): 🎸 improve overlay layer insertions
1 parent a83518d commit 75e2620

File tree

4 files changed

+288
-22
lines changed

4 files changed

+288
-22
lines changed

src/json-crdt-extensions/peritext/Peritext.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {ArrNode, StrNode} from '../../json-crdt/nodes';
77
import {Slices} from './slice/Slices';
88
import {Overlay} from './overlay/Overlay';
99
import {Chars} from './constants';
10+
import {interval} from '../../json-crdt-patch/clock';
11+
import {CONST, updateNum} from '../../json-hash';
1012
import type {ITimestampStruct} from '../../json-crdt-patch/clock';
1113
import type {Model} from '../../json-crdt/model';
1214
import type {Printable} from '../../util/print/types';
@@ -146,7 +148,7 @@ export class Peritext implements Printable {
146148
return Range.at(this.str, start, length);
147149
}
148150

149-
// --------------------------------------------------------------- insertions
151+
// --------------------------------------------------------------------- text
150152

151153
/**
152154
* Insert plain text at a view position in the text.
@@ -175,6 +177,8 @@ export class Peritext implements Printable {
175177
return textId;
176178
}
177179

180+
// ------------------------------------------------------------------ markers
181+
178182
public insMarker(after: ITimestampStruct, type: SliceType, data?: unknown, char: string = Chars.BlockSplitSentinel): MarkerSlice {
179183
const api = this.model.api;
180184
const builder = api.builder;
@@ -190,6 +194,17 @@ export class Peritext implements Printable {
190194
return this.slices.insMarker(range, type, data);
191195
}
192196

197+
/** @todo This can probably use .del() */
198+
public delMarker(split: MarkerSlice): void {
199+
const str = this.str;
200+
const api = this.model.api;
201+
const builder = api.builder;
202+
const strChunk = split.start.chunk();
203+
if (strChunk) builder.del(str.id, [interval(strChunk.id, 0, 1)]);
204+
builder.del(this.slices.set.id, [interval(split.id, 0, 1)]);
205+
api.apply();
206+
}
207+
193208
// ---------------------------------------------------------------- Printable
194209

195210
public toString(tab: string = ''): string {
@@ -202,6 +217,8 @@ export class Peritext implements Printable {
202217
(tab) => this.str.toString(tab),
203218
nl,
204219
(tab) => this.slices.toString(tab),
220+
nl,
221+
(tab) => this.overlay.toString(tab),
205222
])
206223
);
207224
}
@@ -211,6 +228,9 @@ export class Peritext implements Printable {
211228
public hash: number = 0;
212229

213230
public refresh(): number {
214-
return this.slices.refresh();
231+
let state: number = CONST.START_STATE;
232+
this.overlay.refresh();
233+
state = updateNum(state, this.overlay.hash);
234+
return (this.hash = state);
215235
}
216236
}

src/json-crdt-extensions/peritext/overlay/Overlay.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {printTree} from 'sonic-forest/lib/print/printTree';
2+
import {printBinary} from 'sonic-forest/lib/print/printBinary';
13
import {first, insertLeft, insertRight, next, prev, remove} from 'sonic-forest/lib/util';
24
import {splay} from 'sonic-forest/lib/splay/util';
35
import {Anchor} from '../rga/constants';
@@ -7,8 +9,6 @@ import {MarkerOverlayPoint} from './MarkerOverlayPoint';
79
import {OverlayRefSliceEnd, OverlayRefSliceStart} from './refs';
810
import {equal, ITimestampStruct} from '../../../json-crdt-patch/clock';
911
import {CONST, updateNum} from '../../../json-hash';
10-
import {printBinary} from '../../../util/print/printBinary';
11-
import {printTree} from '../../../util/print/printTree';
1212
import {MarkerSlice} from '../slice/MarkerSlice';
1313
import type {Peritext} from '../Peritext';
1414
import type {Stateful} from '../types';
@@ -76,6 +76,15 @@ export class Overlay implements Printable, Stateful {
7676
return result;
7777
}
7878

79+
public find(predicate: (point: OverlayPoint) => boolean): OverlayPoint | undefined {
80+
let point = this.first();
81+
while (point) {
82+
if (predicate(point)) return point;
83+
point = next(point);
84+
}
85+
return undefined;
86+
}
87+
7988
// ----------------------------------------------------------------- Stateful
8089

8190
public hash: number = 0;
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import {Model} from '../../../../json-crdt/model';
2+
import {first, next} from 'sonic-forest/lib/util';
3+
import {Peritext} from '../../Peritext';
4+
import {Anchor} from '../../rga/constants';
5+
import {MarkerOverlayPoint} from '../MarkerOverlayPoint';
6+
7+
const setup = () => {
8+
const model = Model.withLogicalClock();
9+
model.api.root({
10+
text: '',
11+
slices: [],
12+
markers: [],
13+
});
14+
model.api.str(['text']).ins(0, 'wworld');
15+
model.api.str(['text']).ins(0, 'helo ');
16+
model.api.str(['text']).ins(2, 'l');
17+
model.api.str(['text']).del(7, 1);
18+
const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node);
19+
return {model, peritext};
20+
};
21+
22+
const splitCount = (peritext: Peritext): number => {
23+
const overlay = peritext.overlay;
24+
const iterator = overlay.splitIterator();
25+
let count = 0;
26+
for (let split = iterator(); split; split = iterator()) {
27+
count++;
28+
}
29+
return count;
30+
};
31+
32+
describe('markers', () => {
33+
describe('inserts', () => {
34+
test('overlays starts with no markers', () => {
35+
const {peritext} = setup();
36+
expect(splitCount(peritext)).toBe(0);
37+
});
38+
39+
test('can insert one marker in the middle of text', () => {
40+
const {peritext} = setup();
41+
peritext.editor.setCursor(6);
42+
peritext.editor.insMarker(['p'], '¶');
43+
expect(splitCount(peritext)).toBe(0);
44+
peritext.overlay.refresh();
45+
expect(splitCount(peritext)).toBe(1);
46+
const points = [];
47+
let point;
48+
for (const iterator = peritext.overlay.iterator(); (point = iterator()); ) points.push(point);
49+
// console.log(peritext + '');
50+
expect(points.length).toBe(2);
51+
point = points[0];
52+
expect(point.pos()).toBe(5);
53+
});
54+
55+
test('can insert two markers', () => {
56+
const {peritext} = setup();
57+
peritext.editor.setCursor(3);
58+
peritext.editor.insMarker(['p'], '¶');
59+
expect(splitCount(peritext)).toBe(0);
60+
peritext.overlay.refresh();
61+
expect(splitCount(peritext)).toBe(1);
62+
peritext.overlay.refresh();
63+
expect(splitCount(peritext)).toBe(1);
64+
peritext.editor.setCursor(9);
65+
peritext.editor.insMarker(['li'], '- ');
66+
expect(splitCount(peritext)).toBe(1);
67+
peritext.overlay.refresh();
68+
expect(splitCount(peritext)).toBe(2);
69+
peritext.overlay.refresh();
70+
expect(splitCount(peritext)).toBe(2);
71+
});
72+
});
73+
74+
describe('deletes', () => {
75+
test('can delete a marker', () => {
76+
const {peritext} = setup();
77+
peritext.editor.setCursor(6);
78+
const slice = peritext.editor.insMarker(['p'], '¶');
79+
peritext.refresh();
80+
expect(splitCount(peritext)).toBe(1);
81+
const points = [];
82+
let point;
83+
for (const iterator = peritext.overlay.iterator(); (point = iterator()); ) points.push(point);
84+
point = points[0];
85+
peritext.delMarker(slice);
86+
peritext.refresh();
87+
expect(splitCount(peritext)).toBe(0);
88+
});
89+
90+
test('can delete one of two splits', () => {
91+
const {peritext} = setup();
92+
peritext.editor.setCursor(2);
93+
peritext.editor.insMarker(['p'], '¶');
94+
peritext.editor.setCursor(11);
95+
const slice = peritext.editor.insMarker(['p'], '¶');
96+
peritext.refresh();
97+
expect(splitCount(peritext)).toBe(2);
98+
const points = [];
99+
let point;
100+
for (const iterator = peritext.overlay.iterator(); (point = iterator()); ) points.push(point);
101+
point = points[0];
102+
peritext.delMarker(slice);
103+
peritext.refresh();
104+
expect(splitCount(peritext)).toBe(1);
105+
});
106+
});
107+
108+
describe('iterates', () => {
109+
test('can iterate over markers', () => {
110+
const {peritext} = setup();
111+
peritext.editor.setCursor(1, 6);
112+
peritext.editor.insertSlice('a', {a: 'b'});
113+
peritext.editor.setCursor(2);
114+
peritext.editor.insMarker(['p'], '¶');
115+
peritext.editor.setCursor(11);
116+
peritext.editor.insMarker(['p'], '¶');
117+
peritext.refresh();
118+
expect(splitCount(peritext)).toBe(2);
119+
const points = [];
120+
let point;
121+
for (const iterator = peritext.overlay.splitIterator(); (point = iterator()); ) points.push(point);
122+
expect(points.length).toBe(2);
123+
expect(points[0].pos()).toBe(2);
124+
expect(points[1].pos()).toBe(11);
125+
});
126+
});
127+
});
128+
129+
describe('slices', () => {
130+
describe('inserts', () => {
131+
test('overlays starts with no slices', () => {
132+
const {peritext} = setup();
133+
expect(peritext.overlay.slices.size).toBe(0);
134+
});
135+
136+
test('can insert one slice in the middle of text', () => {
137+
const {peritext} = setup();
138+
peritext.editor.setCursor(6, 2);
139+
peritext.editor.insertSlice('em', {emphasis: true});
140+
expect(peritext.overlay.slices.size).toBe(0);
141+
peritext.overlay.refresh();
142+
expect(peritext.overlay.slices.size).toBe(2);
143+
const points = [];
144+
let point;
145+
for (const iterator = peritext.overlay.iterator(); (point = iterator()); ) points.push(point);
146+
expect(points.length).toBe(2);
147+
expect(points[0].pos()).toBe(6);
148+
expect(points[0].anchor).toBe(Anchor.Before);
149+
expect(points[1].pos()).toBe(7);
150+
expect(points[1].anchor).toBe(Anchor.After);
151+
});
152+
153+
test('can insert two slices', () => {
154+
const {peritext} = setup();
155+
peritext.editor.setCursor(2, 8);
156+
peritext.editor.insertSlice('em', {emphasis: true});
157+
peritext.editor.setCursor(4, 8);
158+
peritext.editor.insertSlice('strong', {bold: true});
159+
expect(peritext.overlay.slices.size).toBe(0);
160+
peritext.overlay.refresh();
161+
expect(peritext.overlay.slices.size).toBe(3);
162+
const points = [];
163+
let point;
164+
for (const iterator = peritext.overlay.iterator(); (point = iterator()); ) points.push(point);
165+
expect(points.length).toBe(4);
166+
});
167+
168+
test('intersecting slice chunks point to two slices', () => {
169+
const {peritext} = setup();
170+
peritext.editor.setCursor(2, 2);
171+
peritext.editor.insertSlice('em', {emphasis: true});
172+
peritext.editor.setCursor(3, 2);
173+
peritext.editor.insertSlice('strong', {bold: true});
174+
peritext.refresh();
175+
const point1 = first(peritext.overlay.root)!;
176+
expect(point1.layers.length).toBe(1);
177+
expect(point1.layers[0].data()).toStrictEqual({emphasis: true});
178+
const point2 = next(point1)!;
179+
expect(point2.layers.length).toBe(3);
180+
expect(point2.layers[0].data()).toStrictEqual(undefined);
181+
expect(point2.layers[1].data()).toStrictEqual({emphasis: true});
182+
expect(point2.layers[2].data()).toStrictEqual({bold: true});
183+
const point3 = next(point2)!;
184+
expect(point3.layers.length).toBe(2);
185+
expect(point3.layers[0].data()).toStrictEqual(undefined);
186+
expect(point3.layers[1].data()).toStrictEqual({bold: true});
187+
const point4 = next(point3)!;
188+
expect(point4.layers.length).toBe(0);
189+
console.log(peritext + '');
190+
});
191+
192+
test('one char slice should correctly sort overlay points', () => {
193+
const {peritext} = setup();
194+
peritext.editor.setCursor(0, 1);
195+
peritext.editor.insertSlice('em', {emphasis: true});
196+
peritext.refresh();
197+
const point1 = peritext.overlay.first()!;
198+
const point2 = next(point1)!;
199+
expect(point1.pos()).toBe(0);
200+
expect(point2.pos()).toBe(0);
201+
expect(point1.anchor).toBe(Anchor.Before);
202+
expect(point2.anchor).toBe(Anchor.After);
203+
});
204+
205+
test('intersecting slice before split, should not update the split', () => {
206+
const {peritext} = setup();
207+
peritext.editor.setCursor(6);
208+
const slice = peritext.editor.insMarker(['p']);
209+
peritext.refresh();
210+
const point = peritext.overlay.find((point) => point instanceof MarkerOverlayPoint)!;
211+
expect(point.layers.length).toBe(0);
212+
peritext.editor.setCursor(2, 2);
213+
peritext.editor.insertSlice('<i>');
214+
peritext.refresh();
215+
expect(point.layers.length).toBe(0);
216+
peritext.editor.setCursor(2, 1);
217+
peritext.editor.insertSlice('<b>');
218+
peritext.refresh();
219+
expect(point.layers.length).toBe(0);
220+
});
221+
});
222+
223+
describe('deletes', () => {
224+
test('can remove a slice', () => {
225+
const {peritext} = setup();
226+
peritext.editor.setCursor(6, 2);
227+
const slice = peritext.editor.insertSlice('em', {emphasis: true});
228+
expect(peritext.overlay.slices.size).toBe(0);
229+
peritext.overlay.refresh();
230+
expect(peritext.overlay.slices.size).toBe(2);
231+
peritext.slices.del(slice.id);
232+
expect(peritext.overlay.slices.size).toBe(2);
233+
peritext.overlay.refresh();
234+
expect(peritext.overlay.slices.size).toBe(1);
235+
});
236+
});
237+
});

0 commit comments

Comments
 (0)