Skip to content

Commit 636a166

Browse files
committed
fix(json-crdt-extensions): 🐛 correctly store extra and local slices
1 parent ed6ce96 commit 636a166

File tree

4 files changed

+104
-13
lines changed

4 files changed

+104
-13
lines changed

src/json-crdt-extensions/peritext/editor/EditorSlices.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import {PersistedSlice} from '../slice/PersistedSlice';
12
import type {Peritext} from '../Peritext';
23
import type {SliceType} from '../slice/types';
34
import type {MarkerSlice} from '../slice/MarkerSlice';
45
import type {Slices} from '../slice/Slices';
56
import type {ITimestampStruct} from '../../../json-crdt-patch';
6-
import type {PersistedSlice} from '../slice/PersistedSlice';
77
import type {Cursor} from './Cursor';
88

99
export class EditorSlices<T = string> {
@@ -42,4 +42,8 @@ export class EditorSlices<T = string> {
4242
return marker;
4343
});
4444
}
45+
46+
public del(sliceOrId: PersistedSlice | ITimestampStruct): void {
47+
this.slices.del(sliceOrId instanceof PersistedSlice ? sliceOrId.id : sliceOrId);
48+
}
4549
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {Kit, setupNumbersKit, setupNumbersWithTombstonesKit} from '../../__tests__/setup';
2+
import {MarkerOverlayPoint} from '../MarkerOverlayPoint';
3+
4+
const runMarkersTests = (setup: () => Kit) => {
5+
describe('.markers()', () => {
6+
test('returns empty set by default', () => {
7+
const {peritext} = setup();
8+
peritext.overlay.refresh();
9+
const list = [...peritext.overlay.markers()];
10+
expect(list.length).toBe(0);
11+
});
12+
13+
test('returns a single marker', () => {
14+
const {peritext, editor} = setup();
15+
editor.cursor.setAt(3);
16+
editor.saved.insMarker('<paragraph>');
17+
peritext.overlay.refresh();
18+
const list = [...peritext.overlay.markers()];
19+
expect(list.length).toBe(1);
20+
expect(list[0] instanceof MarkerOverlayPoint).toBe(true);
21+
});
22+
23+
test('can iterate through multiple markers', () => {
24+
const {peritext, editor} = setup();
25+
editor.cursor.setAt(5);
26+
const [m2] = editor.saved.insMarker('<p2>');
27+
peritext.overlay.refresh();
28+
editor.cursor.setAt(8);
29+
const [m3] = editor.local.insMarker('<p3>');
30+
peritext.overlay.refresh();
31+
editor.cursor.setAt(2);
32+
const [m1] = editor.local.insMarker('<p1>');
33+
peritext.overlay.refresh();
34+
const list = [...peritext.overlay.markers()];
35+
expect(list.length).toBe(3);
36+
list.forEach(m => expect(m instanceof MarkerOverlayPoint).toBe(true));
37+
expect(list[0].marker).toBe(m1);
38+
expect(list[1].marker).toBe(m2);
39+
expect(list[2].marker).toBe(m3);
40+
});
41+
42+
test('can delete markers', () => {
43+
const {peritext, editor} = setup();
44+
editor.cursor.setAt(5);
45+
const [m2] = editor.extra.insMarker('<p2>');
46+
editor.cursor.setAt(8);
47+
const [m3] = editor.local.insMarker('<p3>');
48+
editor.cursor.setAt(2);
49+
const [m1] = editor.local.insMarker('<p1>');
50+
peritext.overlay.refresh();
51+
const list = [...peritext.overlay.markers()];
52+
expect(list.length).toBe(3);
53+
editor.local.del(m3);
54+
peritext.overlay.refresh();
55+
const list2 = [...peritext.overlay.markers()];
56+
expect(list2.length).toBe(2);
57+
expect(list2[0].marker).toBe(m1);
58+
expect(list2[1].marker).toBe(m2);
59+
editor.local.del(m2);
60+
peritext.overlay.refresh();
61+
const list3 = [...peritext.overlay.markers()];
62+
expect(list3.length).toBe(2);
63+
expect(list3[0].marker).toBe(m1);
64+
expect(list3[1].marker).toBe(m2);
65+
editor.extra.del(m2);
66+
peritext.overlay.refresh();
67+
const list4 = [...peritext.overlay.markers()];
68+
expect(list4.length).toBe(1);
69+
expect(list4[0].marker).toBe(m1);
70+
editor.local.del(m1);
71+
editor.local.del(m1);
72+
editor.local.del(m1);
73+
peritext.overlay.refresh();
74+
expect([...peritext.overlay.markers()].length).toBe(0);
75+
});
76+
});
77+
};
78+
79+
describe('numbers "0123456789", no edits', () => {
80+
runMarkersTests(setupNumbersKit);
81+
});
82+
83+
describe('numbers "0123456789", with default schema and tombstones', () => {
84+
runMarkersTests(setupNumbersWithTombstonesKit);
85+
});

src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.points.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {Peritext} from '../../Peritext';
44
import type {OverlayPoint} from '../OverlayPoint';
55

66
const setup = () => {
7-
const model = Model.withLogicalClock();
7+
const model = Model.create();
88
const api = model.api;
99
api.root({
1010
text: '',

src/json-crdt-extensions/peritext/slice/Slices.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ export class Slices<T = string> implements Stateful, Printable {
4242
data?: unknown,
4343
Klass: K = behavior === SliceBehavior.Marker ? <any>MarkerSlice : PersistedSlice,
4444
): S {
45-
const model = this.set.doc;
45+
const slicesModel = this.set.doc;
4646
const set = this.set;
47-
const api = model.api;
47+
const api = slicesModel.api;
4848
const builder = api.builder;
4949
const tupleId = builder.vec();
5050
const start = range.start.clone();
@@ -68,10 +68,10 @@ export class Slices<T = string> implements Stateful, Printable {
6868
const chunkId = builder.insArr(set.id, set.id, [tupleId]);
6969
// TODO: Consider using `s` schema here.
7070
api.apply();
71-
const tuple = model.index.get(tupleId) as VecNode;
71+
const tuple = slicesModel.index.get(tupleId) as VecNode;
7272
const chunk = set.findById(chunkId)!;
7373
// TODO: Need to check if split slice text was deleted
74-
const slice = new Klass(model, this.txt, chunk, tuple, behavior, type, start, end);
74+
const slice = new Klass(slicesModel, this.txt, chunk, tuple, behavior, type, start, end);
7575
this.list.set(chunk.id, slice);
7676
return slice;
7777
}
@@ -87,17 +87,17 @@ export class Slices<T = string> implements Stateful, Printable {
8787
separator: string = Chars.BlockSplitSentinel,
8888
): MarkerSlice<T> {
8989
// TODO: test condition when cursors is at absolute or relative starts
90-
const {txt, set} = this;
91-
const model = set.doc;
92-
const api = model.api;
90+
const txt = this.txt;
91+
const api = txt.model.api;
9392
const builder = api.builder;
94-
const str = txt.str;
9593
/**
9694
* We skip one clock cycle to prevent Block-wise RGA from merging adjacent
9795
* characters. We want the marker chunk to always be its own distinct chunk.
9896
*/
9997
builder.nop(1);
100-
const textId = builder.insStr(str.id, after, separator);
98+
// TODO: Handle case when marker is inserted at the abs start, prevent abs start/end inserts.
99+
const textId = builder.insStr(txt.str.id, after, separator);
100+
api.apply();
101101
const point = txt.point(textId, Anchor.Before);
102102
const range = txt.range(point, point.clone());
103103
return this.insMarker(range, type, data);
@@ -134,8 +134,10 @@ export class Slices<T = string> implements Stateful, Printable {
134134

135135
public del(id: ITimestampStruct): void {
136136
this.list.del(id);
137-
const api = this.set.doc.api;
138-
api.builder.del(this.set.id, [tss(id.sid, id.time, 1)]);
137+
const set = this.set;
138+
const api = set.doc.api;
139+
// TODO: Is it worth checking if the slice is already deleted?
140+
api.builder.del(set.id, [tss(id.sid, id.time, 1)]);
139141
api.apply();
140142
}
141143

0 commit comments

Comments
 (0)