Skip to content

Commit

Permalink
feat(json-crdt-extensions): 馃幐 add initial Overlay implementatin
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Apr 29, 2024
1 parent 8bffa22 commit 2cd0174
Show file tree
Hide file tree
Showing 2 changed files with 269 additions and 3 deletions.
19 changes: 16 additions & 3 deletions src/json-crdt-extensions/peritext/Peritext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {Slices} from './slice/Slices';
import {type ITimestampStruct} from '../../json-crdt-patch/clock';
import type {Model} from '../../json-crdt/model';
import type {Printable} from '../../util/print/types';
import type {StringChunk} from './util/types';

/**
* Context for a Peritext instance. Contains all the data and methods needed to
Expand All @@ -30,7 +31,19 @@ export class Peritext implements Printable {
return this.model.api.wrap(this.str);
}

// ------------------------------------------------------------------- Points
/** @todo Find a better place for this function. */
public firstVisChunk(): StringChunk | undefined {
const str = this.str;
let curr = str.first();
if (!curr) return;
while (curr.del) {
curr = str.next(curr);
if (!curr) return;
}
return curr;
}

// ------------------------------------------------------------------- points

/**
* Creates a point at a character ID.
Expand Down Expand Up @@ -81,7 +94,7 @@ export class Peritext implements Printable {
return this.point(this.str.id, Anchor.Before);
}

// ------------------------------------------------------------------- Ranges
// ------------------------------------------------------------------- ranges

/**
* Creates a range from two points. The points can be in any order.
Expand Down Expand Up @@ -117,7 +130,7 @@ export class Peritext implements Printable {
return Range.at(this.str, start, length);
}

// --------------------------------------------------------------- Insertions
// --------------------------------------------------------------- insertions

/**
* Insert plain text at a view position in the text.
Expand Down
253 changes: 253 additions & 0 deletions src/json-crdt-extensions/peritext/overlay/Overlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import {first, insertLeft, insertRight, next, prev, remove} from 'sonic-forest/lib/util';
import {splay} from 'sonic-forest/lib/splay/util';
import {Anchor} from '../rga/constants';
import {Point} from '../rga/Point';
import {OverlayPoint} from './OverlayPoint';
import {MarkerOverlayPoint} from './MarkerOverlayPoint';
import {OverlayRefSliceEnd, OverlayRefSliceStart} from './refs';
import {equal, ITimestampStruct} from '../../../json-crdt-patch/clock';
import {CONST, updateNum} from '../../../json-hash';
import {printBinary} from '../../../util/print/printBinary';
import {printTree} from '../../../util/print/printTree';
import {MarkerSlice} from '../slice/MarkerSlice';
import type {Peritext} from '../Peritext';
import type {Stateful} from '../types';
import type {Printable} from '../../../util/print/types';
import type {MutableSlice, Slice} from '../slice/types';

export class Overlay implements Printable, Stateful {
public root: OverlayPoint | undefined = undefined;

constructor(protected readonly txt: Peritext) {}

/**
* @todo Rename to .point().
*/
protected overlayPoint(id: ITimestampStruct, anchor: Anchor): OverlayPoint {
return new OverlayPoint(this.txt.str, id, anchor);
}

protected markerPoint(marker: MarkerSlice, anchor: Anchor): OverlayPoint {
return new MarkerOverlayPoint(this.txt.str, marker.start.id, anchor, marker);
}

public first(): OverlayPoint | undefined {
return this.root ? first(this.root) : undefined;
}

public iterator(): () => OverlayPoint | undefined {
let curr = this.first();
return () => {
const ret = curr;
if (curr) curr = next(curr);
return ret;
};
}

public splitIterator(): () => MarkerOverlayPoint | undefined {
let curr = this.first();
return () => {
while (curr) {
const ret = curr;
if (curr) curr = next(curr);
if (ret instanceof MarkerOverlayPoint) return ret;
}
return undefined;
};
}

/**
* Retrieve overlay point or the previous one, measured in spacial dimension.
*/
public getOrNextLower(point: Point): OverlayPoint | undefined {
let curr: OverlayPoint | undefined = this.root;
let result: OverlayPoint | undefined = undefined;
while (curr) {
const cmp = curr.cmpSpatial(point);
if (cmp === 0) return curr;
if (cmp > 0) curr = curr.l;
else {
const next = curr.r;
result = curr;
if (!next) return result;
curr = next;
}
}
return result;
}

// ----------------------------------------------------------------- Stateful

public hash: number = 0;

public refresh(slicesOnly: boolean = false): number {
let hash: number = CONST.START_STATE;
hash = this.refreshSlices(hash);
// if (!slicesOnly) this.computeSplitTextHashes();
return (this.hash = hash);
}

/**
* Retrieve an existing {@link OverlayPoint} or create a new one, inserted
* in the tree, sorted by spatial dimension.
*/
protected upsertPoint(point: Point): [point: OverlayPoint, isNew: boolean] {
const newPoint = this.overlayPoint(point.id, point.anchor);
const pivot = this.insertPoint(newPoint);
if (pivot) return [pivot, false];
return [newPoint, true];
}

/**
* Inserts a point into the tree, sorted by spatial dimension.
* @param point Point to insert.
* @returns Returns the existing point if it was already in the tree.
*/
protected insertPoint(point: OverlayPoint): OverlayPoint | undefined {
let pivot = this.getOrNextLower(point);
if (!pivot) pivot = first(this.root);
if (!pivot) {
this.root = point;
return;
} else {
if (pivot.cmp(point) === 0) return pivot;
const cmp = pivot.cmpSpatial(point);
if (cmp < 0) insertRight(point, pivot);
else insertLeft(point, pivot);
}
if (this.root !== point) this.root = splay(this.root!, point, 10);
return undefined;
}

protected delPoint(point: OverlayPoint): void {
this.root = remove(this.root, point);
}

public slices = new Map<Slice, [start: OverlayPoint, end: OverlayPoint]>();

private refreshSlices(state: number): number {
const slices = this.txt.slices;
const changed = slices.refresh();
const sliceSet = this.slices;
state = updateNum(state, slices.hash);
if (changed) {
slices.forEach((slice) => {
let tuple: [start: OverlayPoint, end: OverlayPoint] | undefined = sliceSet.get(slice);
if (tuple) {
if (slice.isDel()) {
this.delSlice(slice, tuple);
return;
}
const positionMoved = tuple[0].cmp(slice.start) !== 0 || tuple[1].cmp(slice.end) !== 0;
if (positionMoved) this.delSlice(slice, tuple);
else return;
}
tuple = this.insSlice(slice);
this.slices.set(slice, tuple);
});
if (slices.size() < sliceSet.size) {
sliceSet.forEach((tuple, slice) => {
const mutSlice = slice as Slice | MutableSlice;
if ((<MutableSlice>mutSlice).isDel) {
if (!(<MutableSlice>mutSlice).isDel()) return;
this.delSlice(slice, tuple);
}
});
}
}
const cursor = this.txt.editor.cursor;
let tuple: [start: OverlayPoint, end: OverlayPoint] | undefined = sliceSet.get(cursor);
const positionMoved = tuple && (tuple[0].cmp(cursor.start) !== 0 || tuple[1].cmp(cursor.end) !== 0);
if (tuple && positionMoved) {
this.delSlice(cursor, tuple!);
}
if (!tuple || positionMoved) {
tuple = this.insSlice(cursor);
this.slices.set(cursor, tuple);
}
return state;
}

protected insSplit(slice: MarkerSlice): [start: OverlayPoint, end: OverlayPoint] {
// const point = new MarkerOverlayPoint(this.txt, slice.start.id, Anchor.Before, slice);
const point = this.markerPoint(slice, Anchor.Before);
const pivot = this.insertPoint(point);
if (!pivot) {
point.refs.push(slice);
const prevPoint = prev(point);
if (prevPoint) point.layers.push(...prevPoint.layers);
}
return [point, point];
}

private insSlice(slice: Slice): [start: OverlayPoint, end: OverlayPoint] {
if (slice instanceof MarkerSlice) return this.insSplit(slice);
const txt = this.txt;
const str = txt.str;
let startPoint = slice.start;
let endPoint = slice.end;
const startIsStringRoot = equal(startPoint.id, str.id);
if (startIsStringRoot) {
const firstVisibleChunk = txt.firstVisChunk();
if (firstVisibleChunk) {
startPoint = txt.point(firstVisibleChunk.id, Anchor.Before);
const endIsStringRoot = equal(endPoint.id, str.id);
if (endIsStringRoot) {
endPoint = txt.point(firstVisibleChunk.id, Anchor.Before);
}
}
}
const [start, isStartNew] = this.upsertPoint(startPoint);
const [end, isEndNew] = this.upsertPoint(endPoint);
start.refs.push(new OverlayRefSliceStart(slice));
end.refs.push(new OverlayRefSliceEnd(slice));
if (isStartNew) {
const beforeStartPoint = prev(start);
if (beforeStartPoint) start.layers.push(...beforeStartPoint.layers);
}
if (isEndNew) {
const beforeEndPoint = prev(end);
if (beforeEndPoint) end.layers.push(...beforeEndPoint.layers);
}
const isCollapsed = startPoint.cmp(endPoint) === 0;
let curr: OverlayPoint | undefined = start;
while (curr !== end && curr) {
curr.addLayer(slice);
curr = next(curr);
}
if (!isCollapsed) {
} else {
start.addMarker(slice);
}
return [start, end];
}

private delSlice(slice: Slice, [start, end]: [start: OverlayPoint, end: OverlayPoint]): void {
this.slices.delete(slice);
let curr: OverlayPoint | undefined = start;
do {
curr.removeLayer(slice);
curr.removeMarker(slice);
curr = next(curr);
} while (curr && curr !== end);
start.removeRef(slice);
end.removeRef(slice);
if (!start.refs.length) this.delPoint(start);
if (!end.refs.length && start !== end) this.delPoint(end);
}

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

public toString(tab: string = ''): string {
const printPoint = (tab: string, point: OverlayPoint): string => {
return (
point.toString(tab) +
printBinary(tab, [
!point.l ? null : (tab) => printPoint(tab, point.l!),
!point.r ? null : (tab) => printPoint(tab, point.r!),
])
);
};
return this.constructor.name + printTree(tab, [!this.root ? null : (tab) => printPoint(tab, this.root!)]);
}
}

0 comments on commit 2cd0174

Please sign in to comment.