Skip to content

Commit 7aea094

Browse files
committed
feat(json-crdt-extensions): 🎸 improve OverlayPoint marker operations
1 parent 323a98e commit 7aea094

File tree

2 files changed

+147
-16
lines changed

2 files changed

+147
-16
lines changed

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

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode {
2525
public readonly layers: Slice[] = [];
2626

2727
/**
28-
* Collapsed slices - markers/block splits, which represent a single point in
29-
* the text, even if the start and end of the slice are different.
30-
*
31-
* @todo Rename to `markers`?
28+
* Collapsed slices - markers (block splits), which represent a single point
29+
* in the text, even if the start and end of the slice are different.
3230
*/
33-
public readonly points: Slice[] = [];
31+
public readonly markers: Slice[] = [];
3432

35-
/** Hash of text contents until the next {@link OverlayPoint}. */
33+
/**
34+
* Hash of text contents until the next {@link OverlayPoint}. This field is
35+
* modified by the {@link Overlay} tree.
36+
*/
3637
public hash: number = 0;
3738

3839
public removeSlice(slice: Slice): void {
@@ -50,7 +51,7 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode {
5051
}
5152
}
5253
this.removeLayer(slice);
53-
this.removePoint(slice);
54+
this.removeMarker(slice);
5455
}
5556

5657
// ------------------------------------------------------------------- layers
@@ -109,32 +110,49 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode {
109110

110111
// ------------------------------------------------------------------ markers
111112

112-
public addPoint(slice: Slice): void {
113-
const points = this.points;
113+
/**
114+
* Inserts a slice to the list of markers which represent a single point in
115+
* the text, even if the start and end of the slice are different. The
116+
* operation is idempotent, so inserting the same slice twice will not change
117+
* the state of the point. The markers are sorted by the slice ID.
118+
*
119+
* @param slice Slice to add to the marker list.
120+
*/
121+
public addMarker(slice: Slice): void {
122+
const points = this.markers;
114123
const length = points.length;
115124
if (!length) {
116125
points.push(slice);
117126
return;
118127
}
119-
// We attempt to insert from the end of the list, as it is the most likely.
128+
// We attempt to insert from the end of the list, as it is the most likely
129+
// scenario. And `.push()` is more efficient than `.unshift()`.
120130
const lastSlice = points[length - 1];
121131
const sliceId = slice.id;
122-
if (compare(lastSlice.id, sliceId) < 0) {
132+
const cmp = compare(lastSlice.id, sliceId);
133+
if (cmp < 0) {
123134
points.push(slice);
124135
return;
125-
}
136+
} else if (!cmp) return;
126137
for (let i = length - 2; i >= 0; i--) {
127138
const currSlice = points[i];
128-
if (compare(currSlice.id, sliceId) < 0) {
139+
const cmp = compare(currSlice.id, sliceId);
140+
if (cmp < 0) {
129141
points.splice(i + 1, 0, slice);
130142
return;
131-
}
143+
} else if (!cmp) return;
132144
}
133145
points.unshift(slice);
134146
}
135147

136-
public removePoint(slice: Slice): void {
137-
const points = this.points;
148+
/**
149+
* Removes a slice from the list of markers, which represent a single point in
150+
* the text, even if the start and end of the slice are different.
151+
*
152+
* @param slice Slice to remove from the marker list.
153+
*/
154+
public removeMarker(slice: Slice): void {
155+
const points = this.markers;
138156
const length = points.length;
139157
for (let i = 0; i < length; i++) {
140158
if (points[i] === slice) {

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

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,116 @@ describe('layers', () => {
119119
expect(point.layers.length).toBe(0);
120120
});
121121
});
122+
123+
describe('markers', () => {
124+
test('can add a marker', () => {
125+
const {peritext, getPoint} = setupOverlayPoint();
126+
const marker = peritext.slices.insSplit(peritext.rangeAt(5, 0), '<p>');
127+
const point = getPoint(marker.start);
128+
expect(point.markers.length).toBe(0);
129+
point.addMarker(marker);
130+
expect(point.markers.length).toBe(1);
131+
expect(point.markers[0]).toBe(marker);
132+
});
133+
134+
test('inserting same marker twice is a no-op', () => {
135+
const {peritext, getPoint} = setupOverlayPoint();
136+
const marker = peritext.slices.insSplit(peritext.rangeAt(5, 0), '<p>');
137+
const point = getPoint(marker.start);
138+
expect(point.markers.length).toBe(0);
139+
point.addMarker(marker);
140+
point.addMarker(marker);
141+
point.addMarker(marker);
142+
point.addMarker(marker);
143+
expect(point.markers.length).toBe(1);
144+
expect(point.markers[0]).toBe(marker);
145+
});
146+
147+
test('can add two markers with the same start position', () => {
148+
const {peritext, getPoint} = setupOverlayPoint();
149+
const marker1 = peritext.slices.insSplit(peritext.rangeAt(5, 0), '<p>');
150+
const marker2 = peritext.slices.insSplit(peritext.rangeAt(5, 0), '<p>');
151+
const point = getPoint(marker1.start);
152+
expect(point.markers.length).toBe(0);
153+
point.addMarker(marker1);
154+
expect(point.markers.length).toBe(1);
155+
point.addMarker(marker2);
156+
point.addMarker(marker2);
157+
expect(point.markers.length).toBe(2);
158+
expect(point.markers[0]).toBe(marker1);
159+
expect(point.markers[1]).toBe(marker2);
160+
});
161+
162+
test('orders markers by their ID', () => {
163+
const {peritext, getPoint} = setupOverlayPoint();
164+
const marker1 = peritext.slices.insSplit(peritext.rangeAt(5, 0), '<p>');
165+
const marker2 = peritext.slices.insSplit(peritext.rangeAt(5, 0), '<p>');
166+
const point = getPoint(marker1.start);
167+
point.addMarker(marker2);
168+
point.addMarker(marker1);
169+
point.addMarker(marker2);
170+
point.addMarker(marker1);
171+
point.addMarker(marker2);
172+
point.addMarker(marker1);
173+
expect(point.markers[0]).toBe(marker1);
174+
expect(point.markers[1]).toBe(marker2);
175+
});
176+
177+
test('can add tree markers and sort them correctly', () => {
178+
const {peritext, getPoint} = setupOverlayPoint();
179+
const marker1 = peritext.slices.insSplit(peritext.rangeAt(5, 0), '<p>');
180+
const marker2 = peritext.slices.insSplit(peritext.rangeAt(5, 0), '<p>');
181+
const marker3 = peritext.slices.insSplit(peritext.rangeAt(5, 0), '<p>');
182+
const point = getPoint(marker1.start);
183+
point.addMarker(marker3);
184+
point.addMarker(marker3);
185+
point.addMarker(marker2);
186+
point.addMarker(marker2);
187+
point.addMarker(marker3);
188+
point.addMarker(marker1);
189+
point.addMarker(marker3);
190+
point.addMarker(marker3);
191+
expect(point.markers.length).toBe(3);
192+
expect(point.markers[0]).toBe(marker1);
193+
expect(point.markers[1]).toBe(marker2);
194+
expect(point.markers[2]).toBe(marker3);
195+
});
196+
197+
test('can add tree markers by appending them', () => {
198+
const {peritext, getPoint} = setupOverlayPoint();
199+
const marker1 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '<p>');
200+
const marker2 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '<p>');
201+
const marker3 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '<p>');
202+
const point = getPoint(marker2.start);
203+
point.addMarker(marker1);
204+
point.addMarker(marker2);
205+
point.addMarker(marker3);
206+
expect(point.markers[0]).toBe(marker1);
207+
expect(point.markers[1]).toBe(marker2);
208+
expect(point.markers[2]).toBe(marker3);
209+
});
210+
211+
test('can remove markers', () => {
212+
const {peritext, getPoint} = setupOverlayPoint();
213+
const marker1 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '<p>');
214+
const marker2 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '<p>');
215+
const marker3 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '<p>');
216+
const point = getPoint(marker1.start);
217+
point.addMarker(marker2);
218+
point.addMarker(marker1);
219+
point.addMarker(marker1);
220+
point.addMarker(marker1);
221+
point.addMarker(marker3);
222+
expect(point.markers[0]).toBe(marker1);
223+
expect(point.markers[1]).toBe(marker2);
224+
expect(point.markers[2]).toBe(marker3);
225+
point.removeMarker(marker2);
226+
expect(point.markers[0]).toBe(marker1);
227+
expect(point.markers[1]).toBe(marker3);
228+
point.removeMarker(marker1);
229+
expect(point.markers[0]).toBe(marker3);
230+
point.removeMarker(marker1);
231+
point.removeMarker(marker3);
232+
expect(point.markers.length).toBe(0);
233+
});
234+
});

0 commit comments

Comments
 (0)