json-joy
Version:
Collection of libraries for building collaborative editing apps.
728 lines (727 loc) • 26 kB
JavaScript
import { printTree } from 'tree-dump/lib/printTree';
import { printBinary } from 'tree-dump/lib/printBinary';
import { first, insertLeft, insertRight, last, next, prev, remove } from 'sonic-forest/lib/util';
import { first2, insert2, last2, next2, prev2, remove2 } from 'sonic-forest/lib/util2';
import { splay } from 'sonic-forest/lib/splay/util';
import { Anchor } from '../rga/constants';
import { OverlayPoint } from './OverlayPoint';
import { MarkerOverlayPoint } from './MarkerOverlayPoint';
import { OverlayRefSliceEnd, OverlayRefSliceStart } from './refs';
import { compare } from '../../../json-crdt-patch/clock';
import { CONST, updateNum } from '../../../json-hash/hash';
import { MarkerSlice } from '../slice/MarkerSlice';
import { UndefEndIter } from '../../../util/iterator';
import { SliceStacking } from '../slice/constants';
const spatialComparator = (a, b) => a.cmpSpatial(b);
/**
* Overlay is a tree structure that represents all the intersections of slices
* in the text. It is used to quickly find all the slices that overlap a
* given point in the text. The overlay is a read-only structure, its state
* is changed only by calling the `refresh` method, which updates the overlay
* based on the current state of the text and slices.
*/
export class Overlay {
txt;
root = undefined;
root2 = undefined;
/** A virtual absolute start point, used when the absolute start is missing. */
START;
/** A virtual absolute end point, used when the absolute end is missing. */
END;
constructor(txt) {
this.txt = txt;
const id = txt.str.id;
this.START = this.point(id, Anchor.After);
this.END = this.point(id, Anchor.Before);
}
point(id, anchor) {
return new OverlayPoint(this.txt.str, id, anchor);
}
mPoint(marker, anchor) {
return new MarkerOverlayPoint(this.txt.str, marker.start.id, anchor, marker);
}
first() {
return this.root ? first(this.root) : undefined;
}
last() {
return this.root ? last(this.root) : undefined;
}
firstMarker() {
return this.root2 ? first2(this.root2) : undefined;
}
lastMarker() {
return this.root2 ? last2(this.root2) : undefined;
}
/**
* Retrieve overlay point or the previous one, measured in spacial dimension.
*/
getOrNextLower(point) {
if (point.isAbsStart()) {
const first = this.first();
if (!first)
return;
return first.isAbsStart() ? first : void 0;
}
else if (point.isAbsEnd())
return this.last();
let curr = this.root;
let result = 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;
}
/**
* Retrieve overlay point or the next one, measured in spacial dimension.
*/
getOrNextHigher(point) {
if (point.isAbsEnd()) {
const last = this.last();
if (!last)
return;
return last.isAbsEnd() ? last : void 0;
}
else if (point.isAbsStart())
return this.first();
let curr = this.root;
let result = undefined;
while (curr) {
const cmp = curr.cmpSpatial(point);
if (cmp === 0)
return curr;
if (cmp < 0)
curr = curr.r;
else {
const next = curr.l;
result = curr;
if (!next)
return result;
curr = next;
}
}
return result;
}
/**
* Retrieve a {@link MarkerOverlayPoint} at the specified point or the
* previous one, measured in spacial dimension.
*/
getOrNextLowerMarker(point) {
if (point.isAbsStart()) {
const first = this.firstMarker();
if (!first)
return;
return first.isAbsStart() ? first : void 0;
}
else if (point.isAbsEnd())
return this.lastMarker();
let curr = this.root2;
let result = undefined;
while (curr) {
const cmp = curr.cmpSpatial(point);
if (cmp === 0)
return curr;
if (cmp > 0)
curr = curr.l2;
else {
const next = curr.r2;
result = curr;
if (!next)
return result;
curr = next;
}
}
return result;
}
/** @todo Rename to `chunks()`. */
chunkSlices0(chunk, p1, p2, callback) {
const rga = this.txt.str;
const strId = rga.id;
let checkFirstAnchor = p1.anchor === Anchor.After;
const adjustForLastAnchor = p2.anchor === Anchor.Before;
let id1 = p1.id;
const id1IsStr = !compare(id1, strId);
if (id1IsStr) {
const first = rga.first();
if (!first)
return;
id1 = first.id;
checkFirstAnchor = false;
}
const id2 = p2.id;
if (!checkFirstAnchor && !adjustForLastAnchor) {
return rga.range0(chunk, id1, id2, callback);
}
const sid1 = id1.sid;
const time1 = id1.time;
const sid2 = id2.sid;
const time2 = id2.time;
return rga.range0(undefined, id1, id2, (chunk, off, len) => {
if (checkFirstAnchor) {
checkFirstAnchor = false;
const chunkId = chunk.id;
if (chunkId.sid === sid1 && chunkId.time + off === time1) {
if (len <= 1)
return;
off += 1;
len -= 1;
}
}
if (adjustForLastAnchor) {
const chunkId = chunk.id;
if (chunkId.sid === sid2 && chunkId.time + off + len - 1 === time2) {
if (len <= 1)
return;
len -= 1;
}
}
if (callback(chunk, off, len))
return true;
});
}
points0(after, inclusive) {
let curr = after ? (inclusive ? after : next(after)) : this.first();
return () => {
const ret = curr;
if (curr)
curr = next(curr);
return ret;
};
}
points(after, inclusive) {
return new UndefEndIter(this.points0(after, inclusive));
}
/**
* Returns all {@link MarkerOverlayPoint} instances in the overlay, starting
* from the given marker point, not including the marker point itself.
*
* If the `after` parameter is not provided, the iteration starts from the
* first marker point in the overlay.
*
* @param after The marker point after which to start the iteration.
* @returns All marker points in the overlay, starting from the given marker
* point.
*/
markers0(after) {
let curr = after ? next2(after) : first2(this.root2);
return () => {
const ret = curr;
if (curr)
curr = next2(curr);
return ret;
};
}
markers(after) {
return new UndefEndIter(this.markers0(after));
}
/**
* Returns all {@link MarkerOverlayPoint} instances in the overlay, starting
* from a give {@link Point}, including any marker overlay points that are
* at the same position as the given point.
*
* @param point Point (inclusive) from which to return all markers.
* @returns All marker points in the overlay, starting from the given marker
* point.
*/
markersFrom0(point) {
if (point.isAbsStart())
return this.markers0(undefined);
let after = this.getOrNextLowerMarker(point);
if (after && after.cmp(point) === 0)
after = prev2(after);
return this.markers0(after);
}
/**
* Returns a pair of overlay marker points for each pair of adjacent marker
* points in the overlay, starting from a given point (which may not be a
* marker). The very first point in the first pair might be `undefined`, if
* the given point is not a marker. Similarly, the very last point in the last
* pair might be `undefined`, if the iteration end point is not a marker.
*
* @param start Start point of the iteration, inclusive.
* @param end End point of the iteration. If not provided, the iteration
* continues until the end of the overlay.
* @returns Iterator that returns pairs of overlay points.
*/
markerPairs0(start, end) {
const i = this.markersFrom0(start);
let closed = false;
let p1;
let p2 = i();
if (p2) {
if (p2.isAbsStart() || !p2.cmp(start)) {
p1 = p2;
p2 = i();
}
if (end && p2) {
const cmp = end.cmpSpatial(p2);
if (cmp <= 0)
return () => (closed ? void 0 : ((closed = true), [p1, cmp ? void 0 : p2]));
}
}
return () => {
if (closed)
return;
if (!p2 || p2.isAbsEnd())
return (closed = true), [p1, p2];
else if (p2 && end) {
const cmp = end.cmpSpatial(p2);
if (cmp <= 0) {
closed = true;
return [p1, cmp ? void 0 : p2];
}
}
const result = [p1, p2];
p1 = p2;
p2 = i();
return result;
};
}
pairs0(after) {
const isEmpty = !this.root;
if (isEmpty) {
const u = undefined;
let closed = false;
return () => (closed ? u : ((closed = true), [u, u]));
}
let p1;
let p2 = after;
const iterator = this.points0(after);
return () => {
const next = iterator();
const isEnd = !next;
if (isEnd) {
if (!p2 || p2.isAbsEnd())
return;
p1 = p2;
p2 = undefined;
return [p1, p2];
}
p1 = p2;
p2 = next;
if (!p1) {
if (p2 && p2.isAbsStart()) {
p1 = p2;
p2 = iterator();
}
}
return p1 || p2 ? [p1, p2] : undefined;
};
}
pairs(after) {
return new UndefEndIter(this.pairs0(after));
}
tuples0(after) {
const iterator = this.pairs0(after);
return () => {
const pair = iterator();
if (!pair)
return;
pair[0] ??= this.START;
pair[1] ??= this.END;
return pair;
};
}
tuples(after) {
return new UndefEndIter(this.tuples0(after));
}
/**
* Finds the first point that satisfies the given predicate function.
*
* @param predicate Predicate function to find the point, returns true if the
* point is found.
* @returns The first point that satisfies the predicate, or undefined if no
* point is found.
*/
find(predicate) {
let point = this.first();
while (point) {
if (predicate(point))
return point;
point = next(point);
}
return;
}
/**
* Finds all slices that are contained within the given range. A slice is
* considered contained if its start and end points are within the range,
* inclusive (uses {@link Range#contains} method to check containment).
*
* @param range The range to search for contained slices.
* @returns A set of slices that are contained within the given range.
*/
findContained(range) {
const result = new Set();
let point = this.getOrNextLower(range.start) ?? this.first();
if (!point)
return result;
do {
if (!range.containsPoint(point))
continue;
const slices = point.layers;
const length = slices.length;
for (let i = 0; i < length; i++) {
const slice = slices[i];
if (!result.has(slice) && range.contains(slice))
result.add(slice);
}
if (point instanceof MarkerOverlayPoint) {
const marker = point.marker;
if (marker && !result.has(marker) && range.contains(marker))
result.add(marker);
}
} while (point && (point = next(point)) && range.containsPoint(point));
return result;
}
/**
* Finds all slices that overlap with the given range. A slice is considered
* overlapping if its start or end point is within the range, inclusive
* (uses {@link Range#containsPoint} method to check overlap).
*
* @param range The range to search for overlapping slices.
* @returns A set of slices that overlap with the given range.
*/
findOverlapping(range) {
const result = new Set();
let point = this.getOrNextLower(range.start) ?? this.first();
if (!point)
return result;
do {
const slices = point.layers;
const length = slices.length;
for (let i = 0; i < length; i++)
result.add(slices[i]);
if (point instanceof MarkerOverlayPoint) {
const marker = point.marker;
if (marker)
result.add(marker);
}
} while (point && (point = next(point)) && range.containsPoint(point));
return result;
}
/**
* Returns a summary of how different slice types overlap with the given range.
*
* @param range Range over which to search for slices.
* @param endOnMarker If set to a positive number, the search will stop after
* the given number of marker points have been observed.
* @returns Summary of the slices in this range. `complete` contains all
* "Overwrite" slice types, which overlay the full range, which have not
* been removed by "Erase" slice type. `partial` contains all "Overwrite"
* slice types, which mark a part of the range, and have not been removed
* by "Erase" slice type.
*/
stat(range, endOnMarker = 10) {
const { start, end: end_ } = range;
let end = end_;
const isSamePoint = start.cmp(end_) === 0;
if (isSamePoint) {
end = end.clone();
end.halfstep(1);
}
const after = this.getOrNextLower(start);
const hasLeadingPoint = !!after;
const iterator = this.points0(after, true);
let complete = new Set();
let partial = new Set();
let isFirst = true;
let markerCount = 0;
OVERLAY: for (let point = iterator(); point && point.cmpSpatial(end) < 0; point = iterator()) {
if (point instanceof MarkerOverlayPoint) {
markerCount++;
if (markerCount >= endOnMarker)
break;
continue OVERLAY;
}
const current = new Set();
const layers = point.layers;
const length = layers.length;
LAYERS: for (let i = 0; i < length; i++) {
const slice = layers[i];
const type = slice.type;
if (typeof type === 'object')
continue LAYERS;
const stacking = slice.stacking;
STACKING: switch (stacking) {
case SliceStacking.One:
current.add(type);
break STACKING;
case SliceStacking.Erase:
current.delete(type);
break STACKING;
}
}
if (isFirst) {
isFirst = false;
if (hasLeadingPoint)
complete = current;
else
partial = current;
continue OVERLAY;
}
for (const type of complete)
if (!current.has(type)) {
complete.delete(type);
partial.add(type);
}
for (const type of current)
if (!complete.has(type))
partial.add(type);
}
return [complete, partial, markerCount];
}
/**
* Returns `true` if the current character is a marker sentinel.
*
* @param id ID of the point to check.
* @returns Whether the point is a marker point.
*/
isMarker(id) {
const p = this.txt.point(id, Anchor.Before);
const op = this.getOrNextLower(p);
return op instanceof MarkerOverlayPoint && op.id.time === id.time && op.id.sid === id.sid;
}
skipMarkers(point, direction) {
while (true) {
const isMarker = this.isMarker(point.id);
if (!isMarker)
return true;
const end = point.step(direction);
if (end)
break;
}
return false;
}
// ----------------------------------------------------------------- Stateful
hash = 0;
refresh(slicesOnly = false) {
const txt = this.txt;
let hash = CONST.START_STATE;
hash = this.refreshSlices(hash, txt.savedSlices);
hash = this.refreshSlices(hash, txt.extraSlices);
hash = this.refreshSlices(hash, txt.localSlices);
// TODO: Move test hash calculation out of the overlay.
if (!slicesOnly) {
// hash = updateRga(hash, txt.str);
hash = this.refreshTextSlices(hash);
}
return (this.hash = hash);
}
slices = new Map();
refreshSlices(state, slices) {
const oldSlicesHash = slices.hash;
const changed = oldSlicesHash !== slices.refresh();
const sliceSet = this.slices;
state = updateNum(state, slices.hash);
if (changed) {
// biome-ignore lint: slices is not iterable
slices.forEach((slice) => {
let tuple = sliceSet.get(slice);
if (tuple) {
if (slice.isDel && 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 = slice instanceof MarkerSlice ? this.insMarker(slice) : this.insSlice(slice);
this.slices.set(slice, tuple);
});
if (slices.size() < sliceSet.size) {
sliceSet.forEach((tuple, slice) => {
const mutSlice = slice;
if (mutSlice.isDel) {
if (!mutSlice.isDel())
return;
this.delSlice(slice, tuple);
}
});
}
}
return state;
}
insSlice(slice) {
const x0 = slice.start;
const x1 = slice.end;
const [start, isStartNew] = this.upsertPoint(x0);
const [end, isEndNew] = this.upsertPoint(x1);
const isCollapsed = x0.cmp(x1) === 0;
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 (!isCollapsed) {
if (isEndNew) {
const beforeEndPoint = prev(end);
if (beforeEndPoint)
end.layers.push(...beforeEndPoint.layers);
}
let curr = start;
do
curr.addLayer(slice);
while ((curr = next(curr)) && curr !== end);
}
else
start.addMarker(slice);
return [start, end];
}
insMarker(slice) {
const point = this.mPoint(slice, Anchor.Before);
const pivot = this.insPoint(point);
if (!pivot) {
point.refs.push(slice);
const prevPoint = prev(point);
if (prevPoint)
point.layers.push(...prevPoint.layers);
}
return [point, point];
}
delSlice(slice, [start, end]) {
this.slices.delete(slice);
let curr = 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);
}
/**
* Retrieve an existing {@link OverlayPoint} or create a new one, inserted
* in the tree, sorted by spatial dimension.
*/
upsertPoint(point) {
const newPoint = this.point(point.id, point.anchor);
const pivot = this.insPoint(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.
*/
insPoint(point) {
if (point instanceof MarkerOverlayPoint) {
this.root2 = insert2(this.root2, point, spatialComparator);
// if (this.root2 !== point) this.root2 = splay2(this.root2!, point, 10);
}
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;
}
delPoint(point) {
if (point instanceof MarkerOverlayPoint)
this.root2 = remove2(this.root2, point);
this.root = remove(this.root, point);
}
leadingTextHash = 0;
refreshTextSlices(stateTotal) {
const txt = this.txt;
const str = txt.str;
const firstChunk = str.first();
if (!firstChunk)
return stateTotal;
let chunk = firstChunk;
let marker = undefined;
const i = this.tuples0(undefined);
let state = CONST.START_STATE;
for (let pair = i(); pair; pair = i()) {
const [p1, p2] = pair;
const id1 = p1.id;
state = (state << 5) + state + (id1.sid >>> 0) + id1.time;
let overlayPointHash = CONST.START_STATE;
chunk = this.chunkSlices0(chunk || firstChunk, p1, p2, (chunk, off, len) => {
const id = chunk.id;
overlayPointHash =
(overlayPointHash << 5) + overlayPointHash + ((((id.sid >>> 0) + id.time) << 8) + (off << 4) + len);
});
state = updateNum(state, overlayPointHash);
for (const slice of p1.layers)
state = updateNum(state, slice.hash);
for (const slice of p1.markers)
state = updateNum(state, slice.hash);
p1.hash = overlayPointHash;
stateTotal = updateNum(stateTotal, overlayPointHash);
if (p2 instanceof MarkerOverlayPoint) {
if (marker) {
marker.textHash = state;
}
else {
this.leadingTextHash = state;
}
stateTotal = updateNum(stateTotal, state);
state = CONST.START_STATE;
marker = p2;
}
}
if (marker instanceof MarkerOverlayPoint) {
marker.textHash = state;
}
else {
this.leadingTextHash = state;
}
return stateTotal;
}
// ---------------------------------------------------------------- Printable
toString(tab = '') {
const printPoint = (tab, point) => {
return (point.toString(tab) +
printBinary(tab, [
!point.l ? null : (tab) => printPoint(tab, point.l),
!point.r ? null : (tab) => printPoint(tab, point.r),
]));
};
const printMarkerPoint = (tab, point) => {
return (point.toString(tab) +
printBinary(tab, [
!point.l2 ? null : (tab) => printMarkerPoint(tab, point.l2),
!point.r2 ? null : (tab) => printMarkerPoint(tab, point.r2),
]));
};
return (`Overlay #${this.hash.toString(36)}` +
printTree(tab, [
!this.root ? null : (tab) => printPoint(tab, this.root),
!this.root2 ? null : (tab) => printMarkerPoint(tab, this.root2),
]));
}
}