Skip to content

Commit 70748ac

Browse files
committed
feat(json-crdt-extensions): 🎸 improve overlay point layer insertion
1 parent 00fa557 commit 70748ac

File tree

3 files changed

+155
-9
lines changed

3 files changed

+155
-9
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export class Peritext implements Printable {
4949
*
5050
* @param pos Position of the character in the text.
5151
* @param anchor Whether the point should attach before or after a character.
52+
* Defaults to "before".
5253
* @returns The point.
5354
*/
5455
public pointAt(pos: number, anchor: Anchor = Anchor.Before): Point {

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

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,27 @@ import type {HeadlessNode} from 'sonic-forest/lib/types';
66
import type {Printable} from '../../../util/print/types';
77
import type {Slice} from '../slice/types';
88

9+
/**
10+
* A {@link Point} which is indexed in the {@link Overlay} tree. Represents
11+
* sparse locations in the string of the places where annotation slices start,
12+
* end, or are broken down by other intersecting slices.
13+
*/
914
export class OverlayPoint extends Point implements Printable, HeadlessNode {
1015
/**
11-
* Sorted list of references to rich-text constructs.
16+
* Sorted list of all references to rich-text constructs.
1217
*/
1318
public readonly refs: OverlayRef[] = [];
1419

1520
/**
16-
* Sorted list of layers, contain the interval from this point to the next one.
21+
* Sorted list of layers, contains the interval from this point to the next
22+
* one. A *layer* is a part of a slice from the current point to the next one.
23+
* This interval can contain many layers, as the slices can be overlap.
1724
*/
1825
public readonly layers: Slice[] = [];
1926

2027
/**
21-
* Collapsed slices.
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.
2230
*
2331
* @todo Rename to `markers`?
2432
*/
@@ -45,9 +53,14 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode {
4553
this.removePoint(slice);
4654
}
4755

56+
// ------------------------------------------------------------------- layers
57+
4858
/**
4959
* Inserts a slice to the list of layers which contains the area from this
50-
* point to the next one.
60+
* point until the next one. The operation is idempotent, so inserting the
61+
* same slice twice will not change the state of the point. The layers are
62+
* sorted by the slice ID.
63+
*
5164
* @param slice Slice to add to the layer list.
5265
*/
5366
public addLayer(slice: Slice): void {
@@ -57,23 +70,32 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode {
5770
layers.push(slice);
5871
return;
5972
}
60-
// We attempt to insert from the end of the list, as it is the most likely.
73+
// We attempt to insert from the end of the list, as it is the most likely
74+
// scenario. And `.push()` is more efficient than `.unshift()`.
6175
const lastSlice = layers[length - 1];
6276
const sliceId = slice.id;
63-
if (compare(lastSlice.id, sliceId) < 0) {
77+
const cmp = compare(lastSlice.id, sliceId);
78+
if (cmp < 0) {
6479
layers.push(slice);
6580
return;
66-
}
81+
} else if (!cmp) return;
6782
for (let i = length - 2; i >= 0; i--) {
6883
const currSlice = layers[i];
69-
if (compare(currSlice.id, sliceId) < 0) {
84+
const cmp = compare(currSlice.id, sliceId);
85+
if (cmp < 0) {
7086
layers.splice(i + 1, 0, slice);
7187
return;
72-
}
88+
} else if (!cmp) return;
7389
}
7490
layers.unshift(slice);
7591
}
7692

93+
/**
94+
* Removes a slice from the list of layers, which start from this overlay
95+
* point.
96+
*
97+
* @param slice Slice to remove from the layer list.
98+
*/
7799
public removeLayer(slice: Slice): void {
78100
const layers = this.layers;
79101
const length = layers.length;
@@ -85,6 +107,8 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode {
85107
}
86108
}
87109

110+
// ------------------------------------------------------------------ markers
111+
88112
public addPoint(slice: Slice): void {
89113
const points = this.points;
90114
const length = points.length;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import {Point} from "../../rga/Point";
2+
import {setup} from "../../slice/__tests__/setup";
3+
import {OverlayPoint} from "../OverlayPoint";
4+
5+
const setupOverlayPoint = () => {
6+
const deps = setup();
7+
const getPoint = (point: Point) => {
8+
return new OverlayPoint(deps.peritext.str, point.id, point.anchor);
9+
};
10+
return {
11+
...deps,
12+
getPoint,
13+
};
14+
};
15+
16+
describe('layers', () => {
17+
test('can add a layer', () => {
18+
const {peritext, getPoint} = setupOverlayPoint();
19+
const slice = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
20+
const point = getPoint(slice.start);
21+
expect(point.layers.length).toBe(0);
22+
point.addLayer(slice);
23+
expect(point.layers.length).toBe(1);
24+
expect(point.layers[0]).toBe(slice);
25+
});
26+
27+
test('inserting same slice twice is a no-op', () => {
28+
const {peritext, getPoint} = setupOverlayPoint();
29+
const slice = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
30+
const point = getPoint(slice.start);
31+
expect(point.layers.length).toBe(0);
32+
point.addLayer(slice);
33+
point.addLayer(slice);
34+
point.addLayer(slice);
35+
expect(point.layers.length).toBe(1);
36+
expect(point.layers[0]).toBe(slice);
37+
});
38+
39+
test('can add two layers with the same start position', () => {
40+
const {peritext, getPoint} = setupOverlayPoint();
41+
const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
42+
const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
43+
const point = getPoint(slice1.start);
44+
expect(point.layers.length).toBe(0);
45+
point.addLayer(slice1);
46+
expect(point.layers.length).toBe(1);
47+
point.addLayer(slice2);
48+
point.addLayer(slice2);
49+
expect(point.layers.length).toBe(2);
50+
expect(point.layers[0]).toBe(slice1);
51+
expect(point.layers[1]).toBe(slice2);
52+
});
53+
54+
test('orders slices by their ID', () => {
55+
const {peritext, getPoint} = setupOverlayPoint();
56+
const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
57+
const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
58+
const point = getPoint(slice1.start);
59+
point.addLayer(slice2);
60+
point.addLayer(slice1);
61+
expect(point.layers[0]).toBe(slice1);
62+
expect(point.layers[1]).toBe(slice2);
63+
});
64+
65+
test('can add tree layers and sort them correctly', () => {
66+
const {peritext, getPoint} = setupOverlayPoint();
67+
const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
68+
const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
69+
const slice3 = peritext.slices.insOverwrite(peritext.rangeAt(2, 10), '<u>');
70+
const point = getPoint(slice1.start);
71+
point.addLayer(slice3);
72+
point.addLayer(slice3);
73+
point.addLayer(slice2);
74+
point.addLayer(slice3);
75+
point.addLayer(slice1);
76+
point.addLayer(slice3);
77+
point.addLayer(slice3);
78+
expect(point.layers.length).toBe(3);
79+
expect(point.layers[0]).toBe(slice1);
80+
expect(point.layers[1]).toBe(slice2);
81+
expect(point.layers[2]).toBe(slice3);
82+
});
83+
84+
test('can add tree layers by appending them', () => {
85+
const {peritext, getPoint} = setupOverlayPoint();
86+
const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
87+
const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
88+
const slice3 = peritext.slices.insOverwrite(peritext.rangeAt(2, 10), '<u>');
89+
const point = getPoint(slice1.start);
90+
point.addLayer(slice1);
91+
point.addLayer(slice2);
92+
point.addLayer(slice3);
93+
expect(point.layers[0]).toBe(slice1);
94+
expect(point.layers[1]).toBe(slice2);
95+
expect(point.layers[2]).toBe(slice3);
96+
});
97+
98+
test('can remove layers', () => {
99+
const {peritext, getPoint} = setupOverlayPoint();
100+
const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
101+
const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
102+
const slice3 = peritext.slices.insOverwrite(peritext.rangeAt(2, 10), '<u>');
103+
const point = getPoint(slice1.start);
104+
point.addLayer(slice2);
105+
point.addLayer(slice1);
106+
point.addLayer(slice1);
107+
point.addLayer(slice1);
108+
point.addLayer(slice3);
109+
expect(point.layers[0]).toBe(slice1);
110+
expect(point.layers[1]).toBe(slice2);
111+
expect(point.layers[2]).toBe(slice3);
112+
point.removeLayer(slice2);
113+
expect(point.layers[0]).toBe(slice1);
114+
expect(point.layers[1]).toBe(slice3);
115+
point.removeLayer(slice1);
116+
expect(point.layers[0]).toBe(slice3);
117+
point.removeLayer(slice1);
118+
point.removeLayer(slice3);
119+
expect(point.layers.length).toBe(0);
120+
});
121+
});

0 commit comments

Comments
 (0)