Skip to content

Commit

Permalink
feat(json-crdt-extensions): 馃幐 improve OverlayPoint ref operations
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Apr 26, 2024
1 parent 7aea094 commit 8a23776
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 52 deletions.
114 changes: 68 additions & 46 deletions src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Point} from '../rga/Point';
import {compare} from '../../../json-crdt-patch/clock';
import {OverlayRef, OverlayRefSliceEnd, OverlayRefSliceStart} from './refs';
import {printTree} from 'sonic-forest/lib/print/printTree';
import type {SplitSlice} from '../slice/SplitSlice';
import type {HeadlessNode} from 'sonic-forest/lib/types';
import type {Printable} from '../../../util/print/types';
import type {Slice} from '../slice/types';
Expand All @@ -13,9 +14,12 @@ import type {Slice} from '../slice/types';
*/
export class OverlayPoint extends Point implements Printable, HeadlessNode {
/**
* Sorted list of all references to rich-text constructs.
* Hash of text contents until the next {@link OverlayPoint}. This field is
* modified by the {@link Overlay} tree.
*/
public readonly refs: OverlayRef[] = [];
public hash: number = 0;

// ------------------------------------------------------------------- layers

/**
* Sorted list of layers, contains the interval from this point to the next
Expand All @@ -24,38 +28,6 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode {
*/
public readonly layers: Slice[] = [];

/**
* Collapsed slices - markers (block splits), which represent a single point
* in the text, even if the start and end of the slice are different.
*/
public readonly markers: Slice[] = [];

/**
* Hash of text contents until the next {@link OverlayPoint}. This field is
* modified by the {@link Overlay} tree.
*/
public hash: number = 0;

public removeSlice(slice: Slice): void {
const refs = this.refs;
const length = refs.length;
for (let i = 0; i < length; i++) {
const ref = refs[i];
if (
ref === slice ||
(ref instanceof OverlayRefSliceStart && ref.slice === slice) ||
(ref instanceof OverlayRefSliceEnd && ref.slice === slice)
) {
refs.splice(i, 1);
break;
}
}
this.removeLayer(slice);
this.removeMarker(slice);
}

// ------------------------------------------------------------------- layers

/**
* Inserts a slice to the list of layers which contains the area from this
* point until the next one. The operation is idempotent, so inserting the
Expand Down Expand Up @@ -110,58 +82,108 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode {

// ------------------------------------------------------------------ markers

/**
* Collapsed slices - markers (block splits), which represent a single point
* in the text, even if the start and end of the slice are different.
* @deprecated This field might happen to be not necessary.
*/
public readonly markers: Slice[] = [];

/**
* Inserts a slice to the list of markers which represent a single point in
* the text, even if the start and end of the slice are different. The
* operation is idempotent, so inserting the same slice twice will not change
* the state of the point. The markers are sorted by the slice ID.
*
* @param slice Slice to add to the marker list.
* @deprecated This method might happen to be not necessary.
*/
public addMarker(slice: Slice): void {
const points = this.markers;
const length = points.length;
/** @deprecated */
const markers = this.markers;
const length = markers.length;
if (!length) {
points.push(slice);
markers.push(slice);
return;
}
// We attempt to insert from the end of the list, as it is the most likely
// scenario. And `.push()` is more efficient than `.unshift()`.
const lastSlice = points[length - 1];
const lastSlice = markers[length - 1];
const sliceId = slice.id;
const cmp = compare(lastSlice.id, sliceId);
if (cmp < 0) {
points.push(slice);
markers.push(slice);
return;
} else if (!cmp) return;
for (let i = length - 2; i >= 0; i--) {
const currSlice = points[i];
const currSlice = markers[i];
const cmp = compare(currSlice.id, sliceId);
if (cmp < 0) {
points.splice(i + 1, 0, slice);
markers.splice(i + 1, 0, slice);
return;
} else if (!cmp) return;
}
points.unshift(slice);
markers.unshift(slice);
}

/**
* Removes a slice from the list of markers, which represent a single point in
* the text, even if the start and end of the slice are different.
*
* @param slice Slice to remove from the marker list.
* @deprecated This method might happen to be not necessary.
*/
public removeMarker(slice: Slice): void {
const points = this.markers;
const length = points.length;
/** @deprecated */
const markers = this.markers;
const length = markers.length;
for (let i = 0; i < length; i++) {
if (points[i] === slice) {
points.splice(i, 1);
if (markers[i] === slice) {
markers.splice(i, 1);
return;
}
}
}

// --------------------------------------------------------------------- refs

/**
* Sorted list of all references to rich-text constructs.
*/
public readonly refs: OverlayRef[] = [];

public addMarkerRef(slice: SplitSlice): void {
this.refs.push(slice);
this.addMarker(slice);
}

public addLayerStartRef(slice: Slice): void {
this.refs.push(new OverlayRefSliceStart(slice));
this.addLayer(slice);
}

public addLayerEndRef(slice: Slice): void {
this.refs.push(new OverlayRefSliceEnd(slice));
}

public removeRef(slice: Slice): void {
const refs = this.refs;
const length = refs.length;
for (let i = 0; i < length; i++) {
const ref = refs[i];
if (
ref === slice ||
(ref instanceof OverlayRefSliceStart && ref.slice === slice) ||
(ref instanceof OverlayRefSliceEnd && ref.slice === slice)
) {
refs.splice(i, 1);
break;
}
}
this.removeLayer(slice);
this.removeMarker(slice);
}

// ---------------------------------------------------------------- Printable

public toStringName(tab: string, lite?: boolean): string {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Point} from '../../rga/Point';
import {setup} from '../../slice/__tests__/setup';
import {OverlayPoint} from '../OverlayPoint';
import {OverlayRefSliceEnd, OverlayRefSliceStart} from '../refs';

const setupOverlayPoint = () => {
const deps = setup();
Expand Down Expand Up @@ -196,9 +197,9 @@ describe('markers', () => {

test('can add tree markers by appending them', () => {
const {peritext, getPoint} = setupOverlayPoint();
const marker1 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '<p>');
const marker2 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '<p>');
const marker3 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '<p>');
const marker1 = peritext.slices.insSplit(peritext.rangeAt(6, 1), '<p>');
const marker2 = peritext.slices.insSplit(peritext.rangeAt(6, 2), '<p>');
const marker3 = peritext.slices.insSplit(peritext.rangeAt(6, 3), '<p>');
const point = getPoint(marker2.start);
point.addMarker(marker1);
point.addMarker(marker2);
Expand All @@ -210,9 +211,9 @@ describe('markers', () => {

test('can remove markers', () => {
const {peritext, getPoint} = setupOverlayPoint();
const marker1 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '<p>');
const marker2 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '<p>');
const marker3 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '<p>');
const marker1 = peritext.slices.insSplit(peritext.rangeAt(6, 1), '<p>');
const marker2 = peritext.slices.insSplit(peritext.rangeAt(6, 1), '<p>');
const marker3 = peritext.slices.insSplit(peritext.rangeAt(6, 2), '<p>');
const point = getPoint(marker1.start);
point.addMarker(marker2);
point.addMarker(marker1);
Expand All @@ -232,3 +233,78 @@ describe('markers', () => {
expect(point.markers.length).toBe(0);
});
});

describe('refs', () => {
test('can add marker ref', () => {
const {peritext, getPoint} = setupOverlayPoint();
const marker = peritext.slices.insSplit(peritext.rangeAt(10, 1), '<p>');
const point = getPoint(marker.start);
expect(point.markers.length).toBe(0);
expect(point.refs.length).toBe(0);
point.addMarkerRef(marker);
expect(point.markers.length).toBe(1);
expect(point.refs.length).toBe(1);
expect(point.markers[0]).toBe(marker);
expect(point.refs[0]).toBe(marker);
});

test('can add layer ref (start)', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice = peritext.slices.insErase(peritext.rangeAt(0, 4), 123);
const point = getPoint(slice.start);
expect(point.layers.length).toBe(0);
expect(point.refs.length).toBe(0);
point.addLayerStartRef(slice);
expect(point.layers.length).toBe(1);
expect(point.refs.length).toBe(1);
expect(point.layers[0]).toBe(slice);
expect((point.refs[0] as OverlayRefSliceStart).slice).toBe(slice);
});

test('can add layer ref (end)', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice = peritext.slices.insErase(peritext.rangeAt(0, 4), 123);
const point = getPoint(slice.end);
expect(point.layers.length).toBe(0);
expect(point.refs.length).toBe(0);
point.addLayerEndRef(slice);
expect(point.layers.length).toBe(0);
expect(point.refs.length).toBe(1);
expect((point.refs[0] as OverlayRefSliceEnd).slice).toBe(slice);
});

test('can add marker and layer start', () => {
const {peritext, getPoint} = setupOverlayPoint();
const marker = peritext.slices.insSplit(peritext.rangeAt(10, 1), '<p>');
const slice = peritext.slices.insErase(peritext.rangeAt(10, 4), 123);
const point = getPoint(slice.end);
expect(point.layers.length).toBe(0);
expect(point.markers.length).toBe(0);
expect(point.refs.length).toBe(0);
point.addMarkerRef(marker);
point.addLayerStartRef(slice);
expect(point.layers.length).toBe(1);
expect(point.markers.length).toBe(1);
expect(point.refs.length).toBe(2);
});

test('can remove marker and layer', () => {
const {peritext, getPoint} = setupOverlayPoint();
const marker = peritext.slices.insSplit(peritext.rangeAt(10, 1), '<p>');
const slice = peritext.slices.insErase(peritext.rangeAt(10, 4), 123);
const point = getPoint(slice.end);
point.addMarkerRef(marker);
point.addLayerStartRef(slice);
expect(point.layers.length).toBe(1);
expect(point.markers.length).toBe(1);
expect(point.refs.length).toBe(2);
point.removeRef(slice);
expect(point.layers.length).toBe(0);
expect(point.markers.length).toBe(1);
expect(point.refs.length).toBe(1);
point.removeRef(marker);
expect(point.layers.length).toBe(0);
expect(point.markers.length).toBe(0);
expect(point.refs.length).toBe(0);
});
});

0 comments on commit 8a23776

Please sign in to comment.