diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts
index 917aafb421..eaa22f49bd 100644
--- a/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts
+++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts
@@ -19,11 +19,13 @@ const setup = () => {
test('can insert markers', () => {
const {peritext} = setup();
const {editor} = peritext;
- expect(size(peritext.overlay.root)).toBe(0);
+ expect([...peritext.overlay].length).toBe(0);
editor.cursor.setAt(0);
+ peritext.refresh();
+ expect([...peritext.overlay].length).toBe(1);
editor.insMarker(['p'], '
');
peritext.refresh();
- expect(size(peritext.overlay.root)).toBe(1);
+ expect(size(peritext.overlay.root)).toBe(2);
editor.cursor.setAt(9);
editor.insMarker(['p'], '
');
peritext.refresh();
diff --git a/src/json-crdt-extensions/peritext/__tests__/setup.ts b/src/json-crdt-extensions/peritext/__tests__/setup.ts
new file mode 100644
index 0000000000..81f4a09713
--- /dev/null
+++ b/src/json-crdt-extensions/peritext/__tests__/setup.ts
@@ -0,0 +1,51 @@
+import {s} from '../../../json-crdt-patch';
+import {ModelWithExt, ext} from '../../ModelWithExt';
+
+/**
+ * Creates a Peritext instance with text "0123456789", with single-char and
+ * block-wise chunks, as well as with plenty of tombstones.
+ */
+export const setupNumbersWithTombstones = () => {
+ const schema = s.obj({
+ text: ext.peritext.new('1234'),
+ });
+ const model = ModelWithExt.create(schema);
+ const str = model.s.text.toExt().text();
+ str.ins(1, '234');
+ str.ins(2, '345');
+ str.ins(3, '456');
+ str.ins(4, '567');
+ str.ins(5, '678');
+ str.ins(6, '789');
+ str.del(7, 1);
+ str.del(8, 1);
+ str.ins(0, '0');
+ str.del(1, 4);
+ str.del(2, 1);
+ str.ins(1, '1');
+ str.del(0, 1);
+ str.ins(0, '0');
+ str.ins(2, '234');
+ str.del(4, 7);
+ str.del(4, 2);
+ str.del(7, 3);
+ str.ins(6, '6789');
+ str.del(7, 2);
+ str.ins(7, '78');
+ str.del(10, 2);
+ str.del(2, 3);
+ str.ins(2, '234');
+ if (str.view() !== '0123456789') throw new Error('Invalid text');
+ const api = model.api;
+ const peritextApi = model.s.text.toExt();
+ const peritext = peritextApi.txt;
+ const editor = peritextApi.editor;
+ return {
+ schema,
+ model,
+ api,
+ peritextApi,
+ peritext,
+ editor,
+ };
+};
diff --git a/src/json-crdt-extensions/peritext/constants.ts b/src/json-crdt-extensions/peritext/constants.ts
index edf41fd29d..00d2b3e538 100644
--- a/src/json-crdt-extensions/peritext/constants.ts
+++ b/src/json-crdt-extensions/peritext/constants.ts
@@ -6,6 +6,21 @@ export const enum Chars {
BlockSplitSentinel = '\n',
}
+export const enum Position {
+ /**
+ * Specifies the absolute start of the text, i.e. the position before the
+ * first character. In model space it is defined as string ID and "after"
+ * anchor.
+ */
+ AbsStart = -1,
+
+ /**
+ * Specifies the absolute end of the text, i.e. the position after the last
+ * character. In model space it is defined as string ID and "before" anchor.
+ */
+ AbsEnd = 9007199254740991, // Number.MAX_SAFE_INTEGER
+}
+
export const MNEMONIC = ExtensionName[ExtensionId.peritext];
export const SCHEMA = (text: string) =>
diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts
index 28cf469045..c66d6ac473 100644
--- a/src/json-crdt-extensions/peritext/editor/Editor.ts
+++ b/src/json-crdt-extensions/peritext/editor/Editor.ts
@@ -77,22 +77,25 @@ export class Editor {
return true;
}
+ /** @deprecated use `.saved.insStack` */
public insStackSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice {
const range = this.cursor.range();
return this.txt.savedSlices.ins(range, SliceBehavior.Stack, type, data);
}
+ /** @deprecated use `.saved.insOverwrite` */
public insOverwriteSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice {
const range = this.cursor.range();
return this.txt.savedSlices.ins(range, SliceBehavior.Overwrite, type, data);
}
+ /** @deprecated use `.saved.insErase` */
public insEraseSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice {
const range = this.cursor.range();
return this.txt.savedSlices.ins(range, SliceBehavior.Erase, type, data);
}
- /** @deprecated */
+ /** @deprecated use `.saved.insMarker` */
public insMarker(type: SliceType, data?: unknown): MarkerSlice {
return this.saved.insMarker(type, data, Chars.BlockSplitSentinel)[0];
}
diff --git a/src/json-crdt-extensions/peritext/editor/EditorSlices.ts b/src/json-crdt-extensions/peritext/editor/EditorSlices.ts
index 8066a377da..26a95f8e39 100644
--- a/src/json-crdt-extensions/peritext/editor/EditorSlices.ts
+++ b/src/json-crdt-extensions/peritext/editor/EditorSlices.ts
@@ -2,6 +2,9 @@ import type {Peritext} from '../Peritext';
import type {SliceType} from '../slice/types';
import type {MarkerSlice} from '../slice/MarkerSlice';
import type {Slices} from '../slice/Slices';
+import type {ITimestampStruct} from '../../../json-crdt-patch';
+import type {PersistedSlice} from '../slice/PersistedSlice';
+import type {Cursor} from './Cursor';
export class EditorSlices {
constructor(
@@ -9,16 +12,34 @@ export class EditorSlices {
protected readonly slices: Slices,
) {}
+ protected insAtCursors>(callback: (cursor: Cursor) => S): S[] {
+ const slices: S[] = [];
+ this.txt.editor.cursors((cursor) => {
+ const slice = callback(cursor);
+ slices.push(slice);
+ });
+ return slices;
+ }
+
+ public insStack(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice[] {
+ return this.insAtCursors((cursor) => this.slices.insStack(cursor.range(), type, data));
+ }
+
+ public insOverwrite(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice[] {
+ return this.insAtCursors((cursor) => this.slices.insOverwrite(cursor.range(), type, data));
+ }
+
+ public insErase(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice[] {
+ return this.insAtCursors((cursor) => this.slices.insErase(cursor.range(), type, data));
+ }
+
public insMarker(type: SliceType, data?: unknown, separator?: string): MarkerSlice[] {
- const {txt, slices} = this;
- const markers: MarkerSlice[] = [];
- txt.editor.cursors((cursor) => {
+ return this.insAtCursors((cursor) => {
cursor.collapse();
const after = cursor.start.clone();
after.refAfter();
- const marker = slices.insMarkerAfter(after.id, type, data, separator);
- markers.push(marker);
+ const marker = this.slices.insMarkerAfter(after.id, type, data, separator);
+ return marker;
});
- return markers;
}
}
diff --git a/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts b/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts
index ff9db977a6..3b40ea91b3 100644
--- a/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts
+++ b/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts
@@ -46,6 +46,15 @@ export class MarkerOverlayPoint extends OverlayPoint {
}
public toString(tab: string = '', lite?: boolean): string {
- return super.toString(tab, lite) + (lite ? '' : printTree(tab, [(tab) => this.marker.toString(tab)]));
+ return (
+ this.toStringName(tab, lite) +
+ (lite
+ ? ''
+ : printTree(tab, [
+ (tab) => this.marker.toString(tab),
+ ...this.layers.map((slice) => (tab: string) => slice.toString(tab)),
+ ...this.markers.map((slice) => (tab: string) => slice.toString(tab)),
+ ]))
+ );
}
}
diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts
index 2faf194c60..e56f4c5d6f 100644
--- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts
+++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts
@@ -1,16 +1,18 @@
import {printTree} from 'tree-dump/lib/printTree';
import {printBinary} from 'tree-dump/lib/printBinary';
-import {first, insertLeft, insertRight, next, prev, remove} from 'sonic-forest/lib/util';
+import {first, insertLeft, insertRight, last, 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 {compare, ITimestampStruct, tick} from '../../../json-crdt-patch/clock';
import {CONST, updateNum} from '../../../json-hash';
import {MarkerSlice} from '../slice/MarkerSlice';
-import {firstVis} from '../../../json-crdt/nodes/rga/util';
+import {Range} from '../rga/Range';
+import {UndefEndIter} from '../../../util/iterator';
+import type {Chunk} from '../../../json-crdt/nodes/rga';
import type {Peritext} from '../Peritext';
import type {Stateful} from '../types';
import type {Printable} from 'tree-dump/lib/types';
@@ -26,13 +28,28 @@ import type {Slices} from '../slice/Slices';
*/
export class Overlay implements Printable, Stateful {
public root: OverlayPoint | undefined = undefined;
+ public readonly start: OverlayPoint;
- constructor(protected readonly txt: Peritext) {}
+ constructor(protected readonly txt: Peritext) {
+ this.start = this.point(this.txt.str.id, Anchor.After);
+ }
+
+ private point(id: ITimestampStruct, anchor: Anchor): OverlayPoint {
+ return new OverlayPoint(this.txt.str, id, anchor);
+ }
+
+ private mPoint(marker: MarkerSlice, anchor: Anchor): MarkerOverlayPoint {
+ return new MarkerOverlayPoint(this.txt.str, marker.start.id, anchor, marker);
+ }
public first(): OverlayPoint | undefined {
return this.root ? first(this.root) : undefined;
}
+ public last(): OverlayPoint | undefined {
+ return this.root ? last(this.root) : undefined;
+ }
+
public iterator(): () => OverlayPoint | undefined {
let curr = this.first();
return () => {
@@ -42,7 +59,15 @@ export class Overlay implements Printable, Stateful {
};
}
- public splitIterator(): () => MarkerOverlayPoint | undefined {
+ public entries(): IterableIterator> {
+ return new UndefEndIter(this.iterator());
+ }
+
+ [Symbol.iterator]() {
+ return this.entries();
+ }
+
+ public markerIterator(): () => MarkerOverlayPoint | undefined {
let curr = this.first();
return () => {
while (curr) {
@@ -50,14 +75,24 @@ export class Overlay implements Printable, Stateful {
if (curr) curr = next(curr);
if (ret instanceof MarkerOverlayPoint) return ret;
}
- return undefined;
+ return;
};
}
+ public markers(): IterableIterator> {
+ return new UndefEndIter(this.iterator());
+ }
+
/**
* Retrieve overlay point or the previous one, measured in spacial dimension.
*/
public getOrNextLower(point: Point): OverlayPoint | undefined {
+ if (point.isAbsStart()) {
+ const first = this.first();
+ if (!first) return;
+ if (first.isAbsStart()) return first;
+ point = first;
+ } else if (point.isAbsEnd()) return this.last();
let curr: OverlayPoint | undefined = this.root;
let result: OverlayPoint | undefined = undefined;
while (curr) {
@@ -74,13 +109,234 @@ export class Overlay implements Printable, Stateful {
return result;
}
+ /**
+ * Retrieve overlay point or the next one, measured in spacial dimension.
+ */
+ public getOrNextHigher(point: Point): OverlayPoint | undefined {
+ if (point.isAbsEnd()) {
+ const last = this.last();
+ if (!last) return;
+ if (last.isAbsEnd()) return last;
+ point = last;
+ } else if (point.isAbsStart()) return this.first();
+ 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.r;
+ else {
+ const next = curr.l;
+ result = curr;
+ if (!next) return result;
+ curr = next;
+ }
+ }
+ return result;
+ }
+
public find(predicate: (point: OverlayPoint) => boolean): OverlayPoint | undefined {
let point = this.first();
while (point) {
if (predicate(point)) return point;
point = next(point);
}
- return undefined;
+ return;
+ }
+
+ public chunkSlices0(
+ chunk: Chunk | undefined,
+ p1: Point,
+ p2: Point,
+ callback: (chunk: Chunk, off: number, len: number) => void,
+ ): Chunk | undefined {
+ 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) as Chunk;
+ }
+ const sid1 = id1.sid;
+ const time1 = id1.time;
+ const sid2 = id2.sid;
+ const time2 = id2.time;
+ return rga.range0(undefined, id1, id2, (chunk: Chunk, off: number, len: number) => {
+ 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;
+ }
+ }
+ callback(chunk, off, len);
+ }) as Chunk;
+ }
+
+ public points0(
+ start: undefined | OverlayPoint,
+ end: undefined | ((next: OverlayPoint) => boolean),
+ callback: (point: OverlayPoint) => void,
+ ): void {
+ const txt = this.txt;
+ const str = txt.str;
+ const strFirstChunk = str.first();
+ if (!strFirstChunk) return;
+ let point = start || this.first();
+ let prev: typeof point;
+ const pointIsStart =
+ point &&
+ ((!compare(point.id, str.id) && point.anchor === Anchor.After) ||
+ (!compare(strFirstChunk.id, point.id) && point.anchor === Anchor.Before));
+ if (!start && !pointIsStart) {
+ const startPoint = this.start;
+ startPoint.id = strFirstChunk.id;
+ startPoint.anchor = Anchor.Before;
+ callback(startPoint);
+ }
+ while (point) {
+ if (end && end(point)) return;
+ callback(point);
+ prev = point;
+ point = next(point);
+ }
+ const strLastChunk = str.last()!;
+ const strLastChunkId = strLastChunk.id;
+ if (prev) {
+ const prevId = prev.id;
+ if (
+ prev.anchor === Anchor.After &&
+ prevId.time === strLastChunkId.time + strLastChunk.span - 1 &&
+ prevId.sid === strLastChunkId.sid &&
+ prevId.sid === strLastChunkId.sid
+ )
+ return;
+ }
+ const endId = strLastChunk.span > 1 ? tick(strLastChunkId, strLastChunk.span - 1) : strLastChunkId;
+ const ending = this.point(endId!, Anchor.After);
+ if (end && end(ending)) return;
+ callback(ending);
+ }
+
+ public points1(
+ start: undefined | OverlayPoint,
+ end: undefined | ((next: OverlayPoint) => boolean),
+ callback: (p1: OverlayPoint, p2: OverlayPoint) => void,
+ ): void {
+ let p1: OverlayPoint | undefined;
+ let p2: OverlayPoint | undefined;
+ this.points0(start, end, (point) => {
+ if (p1) {
+ p2 = point;
+ callback(p1, p2);
+ p1 = p2;
+ } else {
+ p1 = point;
+ }
+ });
+ }
+
+ public findContained(range: Range): Set> {
+ const result = new Set>();
+ let point = this.getOrNextLower(range.start);
+ 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;
+ }
+
+ public findOverlapping(range: Range): Set> {
+ const result = new Set>();
+ let point = this.getOrNextLower(range.start);
+ 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;
+ }
+
+ public leadingTextHash: number = 0;
+
+ protected computeSplitTextHashes(): void {
+ const txt = this.txt;
+ const str = txt.str;
+ const firstChunk = str.first();
+ if (!firstChunk) return;
+ let chunk: Chunk | undefined = firstChunk;
+ let marker: MarkerOverlayPoint | undefined = undefined;
+ let state: number = CONST.START_STATE;
+ this.points1(undefined, undefined, (p1, p2) => {
+ // TODO: need to incorporate slice attribute hash here?
+ 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);
+ if (p1) {
+ p1.hash = overlayPointHash;
+ }
+ if (p2 instanceof MarkerOverlayPoint) {
+ if (marker) {
+ marker.textHash = state;
+ } else {
+ this.leadingTextHash = state;
+ }
+ state = CONST.START_STATE;
+ marker = p2;
+ }
+ });
+ if ((marker as any) instanceof MarkerOverlayPoint) {
+ (marker as any as MarkerOverlayPoint).textHash = state;
+ } else {
+ this.leadingTextHash = state;
+ }
+ }
+
+ public isBlockSplit(id: ITimestampStruct): boolean {
+ const point = this.txt.point(id, Anchor.Before);
+ const overlayPoint = this.getOrNextLower(point);
+ return (
+ overlayPoint instanceof MarkerOverlayPoint && overlayPoint.id.time === id.time && overlayPoint.id.sid === id.sid
+ );
}
// ----------------------------------------------------------------- Stateful
@@ -93,7 +349,7 @@ export class Overlay implements Printable, Stateful {
hash = this.refreshSlices(hash, txt.savedSlices);
hash = this.refreshSlices(hash, txt.extraSlices);
hash = this.refreshSlices(hash, txt.localSlices);
- // if (!slicesOnly) this.computeSplitTextHashes();
+ if (!slicesOnly) this.computeSplitTextHashes();
return (this.hash = hash);
}
@@ -116,7 +372,7 @@ export class Overlay implements Printable, Stateful {
if (positionMoved) this.delSlice(slice, tuple);
else return;
}
- tuple = this.insSlice(slice);
+ tuple = slice instanceof MarkerSlice ? this.insMarker(slice) : this.insSlice(slice);
this.slices.set(slice, tuple);
});
if (slices.size() < sliceSet.size) {
@@ -132,12 +388,57 @@ export class Overlay implements Printable, Stateful {
return state;
}
- private point(id: ITimestampStruct, anchor: Anchor): OverlayPoint {
- return new OverlayPoint(this.txt.str, id, anchor);
+ private insSlice(slice: Slice): [start: OverlayPoint, end: OverlayPoint] {
+ // TODO: Test cases where the inserted slice is collapsed to one point.
+ 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: OverlayPoint | undefined = start;
+ do curr.addLayer(slice);
+ while ((curr = next(curr)) && curr !== end);
+ } else {
+ // TODO: review if this is needed:
+ start.addMarker(slice);
+ }
+ return [start, end];
}
- private mPoint(marker: MarkerSlice, anchor: Anchor): MarkerOverlayPoint {
- return new MarkerOverlayPoint(this.txt.str, marker.start.id, anchor, marker);
+ private insMarker(slice: MarkerSlice): [start: OverlayPoint, end: OverlayPoint] {
+ 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];
+ }
+
+ 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);
}
/**
@@ -169,80 +470,13 @@ export class Overlay implements Printable, Stateful {
else insertLeft(point, pivot);
}
if (this.root !== point) this.root = splay(this.root!, point, 10);
- return undefined;
+ return;
}
private delPoint(point: OverlayPoint): void {
this.root = remove(this.root, point);
}
- private insMarker(slice: MarkerSlice): [start: OverlayPoint, end: OverlayPoint] {
- 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];
- }
-
- private insSlice(slice: Slice): [start: OverlayPoint, end: OverlayPoint] {
- if (slice instanceof MarkerSlice) return this.insMarker(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 = firstVis(txt.str);
- 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 {
diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts
new file mode 100644
index 0000000000..6e779ecf56
--- /dev/null
+++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts
@@ -0,0 +1,241 @@
+import {Model} from '../../../../json-crdt/model';
+import {size} from 'sonic-forest/lib/util';
+import {Peritext} from '../../Peritext';
+import {Anchor} from '../../rga/constants';
+import {setupNumbersWithTombstones} from '../../__tests__/setup';
+import {OverlayPoint} from '../OverlayPoint';
+import {OverlayRefSliceEnd, OverlayRefSliceStart} from '../refs';
+
+describe('.getOrNextLower()', () => {
+ test('combines overlay points - right anchor', () => {
+ const model = Model.create();
+ const api = model.api;
+ api.root({
+ text: '1234',
+ slices: [],
+ });
+ const peritext = new Peritext(model, api.str(['text']).node, api.arr(['slices']).node);
+ peritext.editor.cursor.setAt(1, 1);
+ peritext.editor.saved.insStack(2);
+ peritext.refresh();
+ const str = peritext.str;
+ const id1 = str.find(1)!;
+ const id2 = str.find(2)!;
+ const p1 = peritext.point(id1, Anchor.After);
+ const p2 = peritext.point(id2, Anchor.After);
+ peritext.editor.cursor.set(p1, p2);
+ peritext.editor.saved.insStack(3);
+ peritext.refresh();
+ const cnt = size(peritext.overlay.root);
+ expect(cnt).toBe(3);
+ });
+
+ test('combines overlay points - right anchor 2', () => {
+ const model = Model.create();
+ const api = model.api;
+ api.root({
+ text: '1234',
+ slices: [],
+ });
+ const peritext = new Peritext(model, api.str(['text']).node, api.arr(['slices']).node);
+ const str = peritext.str;
+ const id1 = str.find(1)!;
+ const id2 = str.find(2)!;
+ const p1 = peritext.point(id1, Anchor.After);
+ const p2 = peritext.point(id2, Anchor.After);
+ peritext.editor.cursor.set(p1, p2);
+ peritext.editor.saved.insStack(3);
+ peritext.refresh();
+ peritext.editor.cursor.setAt(2, 1);
+ peritext.editor.saved.insStack(33);
+ peritext.refresh();
+ const cnt = size(peritext.overlay.root);
+ expect(cnt).toBe(3);
+ });
+
+ test('can iterate through all character points', () => {
+ const model = Model.create();
+ const api = model.api;
+ api.root({
+ text: '1234',
+ slices: [],
+ });
+ const peritext = new Peritext(model, api.str(['text']).node, api.arr(['slices']).node);
+ const editor = peritext.editor;
+ editor.cursor.setAt(0, 1);
+ const [slice1] = editor.saved.insStack(1);
+ editor.cursor.setAt(1, 1);
+ const [slice2] = editor.saved.insStack(2);
+ editor.cursor.setAt(2, 1);
+ const [slice3] = editor.saved.insStack(3);
+ editor.cursor.setAt(3, 1);
+ const [slice4] = editor.saved.insStack(4);
+ peritext.refresh();
+ let overlayPoint = peritext.overlay.getOrNextLower(slice1.start)!;
+ expect(overlayPoint.layers.length).toBe(1);
+ expect(overlayPoint.layers[0]).toBe(slice1);
+ overlayPoint = peritext.overlay.getOrNextLower(slice1.end)!;
+ expect(overlayPoint.layers.length).toBe(0);
+ overlayPoint = peritext.overlay.getOrNextLower(slice2.start)!;
+ expect(overlayPoint.layers.length).toBe(1);
+ expect(overlayPoint.layers[0]).toBe(slice2);
+ overlayPoint = peritext.overlay.getOrNextLower(slice2.end)!;
+ expect(overlayPoint.layers.length).toBe(0);
+ overlayPoint = peritext.overlay.getOrNextLower(slice3.start)!;
+ expect(overlayPoint.layers.length).toBe(1);
+ expect(overlayPoint.layers[0]).toBe(slice3);
+ overlayPoint = peritext.overlay.getOrNextLower(slice3.end)!;
+ expect(overlayPoint.layers.length).toBe(0);
+ overlayPoint = peritext.overlay.getOrNextLower(slice4.start)!;
+ expect(overlayPoint.layers.length).toBe(2);
+ overlayPoint = peritext.overlay.getOrNextLower(slice4.end)!;
+ expect(overlayPoint.layers.length).toBe(0);
+ });
+
+ describe('when all text selected, using relative range', () => {
+ test('can select the starting point', () => {
+ const {peritext, editor} = setupNumbersWithTombstones();
+ const range = peritext.range(peritext.pointStart()!, peritext.pointEnd()!);
+ editor.cursor.setRange(range);
+ peritext.refresh();
+ const overlayPoint = peritext.overlay.getOrNextLower(peritext.pointAbsStart())!;
+ expect(overlayPoint).toBeInstanceOf(OverlayPoint);
+ expect(overlayPoint.refs.length).toBe(1);
+ expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceEnd(editor.cursor));
+ expect(overlayPoint.layers.length).toBe(1);
+ expect(overlayPoint.layers[0]).toEqual(editor.cursor);
+ });
+
+ test('can select the ending point', () => {
+ const {peritext, editor} = setupNumbersWithTombstones();
+ const range = peritext.range(peritext.pointStart()!, peritext.pointEnd()!);
+ editor.cursor.setRange(range);
+ peritext.refresh();
+ const overlayPoint = peritext.overlay.getOrNextLower(peritext.pointAbsEnd())!;
+ expect(overlayPoint).toBeInstanceOf(OverlayPoint);
+ expect(overlayPoint.refs.length).toBe(1);
+ expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceStart(editor.cursor));
+ });
+ });
+
+ describe('when all text selected, using absolute range', () => {
+ test('can select the starting point', () => {
+ const {peritext, editor} = setupNumbersWithTombstones();
+ const range = peritext.range(peritext.pointAbsStart(), peritext.pointAbsEnd());
+ editor.cursor.setRange(range);
+ peritext.refresh();
+ const overlayPoint = peritext.overlay.getOrNextLower(peritext.pointAbsStart())!;
+ expect(overlayPoint).toBeInstanceOf(OverlayPoint);
+ expect(overlayPoint.refs.length).toBe(1);
+ expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceEnd(editor.cursor));
+ expect(overlayPoint.layers.length).toBe(1);
+ expect(overlayPoint.layers[0]).toEqual(editor.cursor);
+ });
+
+ test('can select the end point', () => {
+ const {peritext, editor} = setupNumbersWithTombstones();
+ const range = peritext.range(peritext.pointAbsStart(), peritext.pointAbsEnd());
+ editor.cursor.setRange(range);
+ peritext.refresh();
+ const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsStart()!)!;
+ expect(overlayPoint).toBeInstanceOf(OverlayPoint);
+ expect(overlayPoint.refs.length).toBe(1);
+ expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceStart(editor.cursor));
+ expect(overlayPoint.layers.length).toBe(1);
+ expect(overlayPoint.layers[0]).toEqual(editor.cursor);
+ });
+ });
+});
+
+describe('.getOrNextHigher()', () => {
+ test('can iterate through all character points', () => {
+ const model = Model.create();
+ const api = model.api;
+ api.root({
+ text: '1234',
+ slices: [],
+ });
+ const peritext = new Peritext(model, api.str(['text']).node, api.arr(['slices']).node);
+ const editor = peritext.editor;
+ editor.cursor.setAt(0, 1);
+ const [slice1] = editor.saved.insStack(1);
+ editor.cursor.setAt(1, 1);
+ const [slice2] = editor.saved.insStack(2);
+ editor.cursor.setAt(2, 1);
+ const [slice3] = editor.saved.insStack(3);
+ editor.cursor.setAt(3, 1);
+ const [slice4] = editor.saved.insStack(4);
+ peritext.refresh();
+ let overlayPoint = peritext.overlay.getOrNextHigher(slice4.end)!;
+ expect(overlayPoint.layers.length).toBe(0);
+ overlayPoint = peritext.overlay.getOrNextHigher(slice4.start)!;
+ expect(overlayPoint.layers.length).toBe(2);
+ overlayPoint = peritext.overlay.getOrNextHigher(slice3.end)!;
+ expect(overlayPoint.layers.length).toBe(0);
+ overlayPoint = peritext.overlay.getOrNextHigher(slice3.start)!;
+ expect(overlayPoint.layers.length).toBe(1);
+ expect(overlayPoint.layers[0]).toBe(slice3);
+ overlayPoint = peritext.overlay.getOrNextHigher(slice2.end)!;
+ expect(overlayPoint.layers.length).toBe(0);
+ overlayPoint = peritext.overlay.getOrNextHigher(slice2.start)!;
+ expect(overlayPoint.layers.length).toBe(1);
+ expect(overlayPoint.layers[0]).toBe(slice2);
+ overlayPoint = peritext.overlay.getOrNextHigher(slice1.end)!;
+ expect(overlayPoint.layers.length).toBe(0);
+ overlayPoint = peritext.overlay.getOrNextHigher(slice1.start)!;
+ expect(overlayPoint.layers.length).toBe(1);
+ expect(overlayPoint.layers[0]).toBe(slice1);
+ });
+
+ describe('when all text selected, using relative range', () => {
+ test('can select the ending point', () => {
+ const {peritext, editor} = setupNumbersWithTombstones();
+ const range = peritext.range(peritext.pointStart()!, peritext.pointEnd()!);
+ editor.cursor.setRange(range);
+ peritext.refresh();
+ const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsEnd())!;
+ expect(overlayPoint).toBeInstanceOf(OverlayPoint);
+ expect(overlayPoint.refs.length).toBe(1);
+ expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceEnd(editor.cursor));
+ });
+
+ test('can select the start point', () => {
+ const {peritext, editor} = setupNumbersWithTombstones();
+ const range = peritext.range(peritext.pointStart()!, peritext.pointEnd()!);
+ editor.cursor.setRange(range);
+ peritext.refresh();
+ const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsStart()!)!;
+ expect(overlayPoint).toBeInstanceOf(OverlayPoint);
+ expect(overlayPoint.refs.length).toBe(1);
+ expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceStart(editor.cursor));
+ expect(overlayPoint.layers.length).toBe(1);
+ expect(overlayPoint.layers[0]).toEqual(editor.cursor);
+ });
+ });
+
+ describe('when all text selected, using absolute range', () => {
+ test('can select the ending point', () => {
+ const {peritext, editor} = setupNumbersWithTombstones();
+ const range = peritext.range(peritext.pointAbsStart(), peritext.pointAbsEnd());
+ editor.cursor.setRange(range);
+ peritext.refresh();
+ const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsEnd())!;
+ expect(overlayPoint).toBeInstanceOf(OverlayPoint);
+ expect(overlayPoint.refs.length).toBe(1);
+ expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceEnd(editor.cursor));
+ });
+
+ test('can select the start point', () => {
+ const {peritext, editor} = setupNumbersWithTombstones();
+ const range = peritext.range(peritext.pointAbsStart(), peritext.pointAbsEnd());
+ editor.cursor.setRange(range);
+ peritext.refresh();
+ const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsStart()!)!;
+ expect(overlayPoint).toBeInstanceOf(OverlayPoint);
+ expect(overlayPoint.refs.length).toBe(1);
+ expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceStart(editor.cursor));
+ expect(overlayPoint.layers.length).toBe(1);
+ expect(overlayPoint.layers[0]).toEqual(editor.cursor);
+ });
+ });
+});
diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLower.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLower.spec.ts
deleted file mode 100644
index bb117c1ae1..0000000000
--- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLower.spec.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import {Model} from '../../../../json-crdt/model';
-import {size} from 'sonic-forest/lib/util';
-import {Peritext} from '../../Peritext';
-import {Anchor} from '../../rga/constants';
-
-describe('.getOrNextLower()', () => {
- test('combines overlay points - right anchor', () => {
- const model = Model.withLogicalClock();
- const api = model.api;
- api.root({
- text: '1234',
- slices: [],
- });
- const peritext = new Peritext(model, api.str(['text']).node, api.arr(['slices']).node);
- peritext.editor.cursor.setAt(1, 1);
- peritext.editor.insStackSlice(2);
- peritext.refresh();
- const str = peritext.str;
- const id1 = str.find(1)!;
- const id2 = str.find(2)!;
- const p1 = peritext.point(id1, Anchor.After);
- const p2 = peritext.point(id2, Anchor.After);
- peritext.editor.cursor.set(p1, p2);
- peritext.editor.insStackSlice(3);
- peritext.refresh();
- const cnt = size(peritext.overlay.root);
- expect(cnt).toBe(3);
- });
-
- test('combines overlay points - right anchor 2', () => {
- const model = Model.withLogicalClock();
- const api = model.api;
- api.root({
- text: '1234',
- slices: [],
- });
- const peritext = new Peritext(model, api.str(['text']).node, api.arr(['slices']).node);
- const str = peritext.str;
- const id1 = str.find(1)!;
- const id2 = str.find(2)!;
- const p1 = peritext.point(id1, Anchor.After);
- const p2 = peritext.point(id2, Anchor.After);
- peritext.editor.cursor.set(p1, p2);
- peritext.editor.insStackSlice(3);
- peritext.refresh();
- peritext.editor.cursor.setAt(2, 1);
- peritext.editor.insStackSlice(33);
- peritext.refresh();
- const cnt = size(peritext.overlay.root);
- expect(cnt).toBe(3);
- });
-});
diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.refresh.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.refresh.spec.ts
index 8ef70930d5..94d8454aae 100644
--- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.refresh.spec.ts
+++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.refresh.spec.ts
@@ -1,5 +1,6 @@
import {Model, ObjApi} from '../../../../json-crdt/model';
import {Peritext} from '../../Peritext';
+import {setupNumbersWithTombstones} from '../../__tests__/setup';
import {Anchor} from '../../rga/constants';
import {SliceBehavior} from '../../slice/constants';
@@ -22,6 +23,32 @@ const setup = () => {
type Kit = ReturnType;
describe('Overlay.refresh()', () => {
+ test('can select all text using relative range', () => {
+ const {peritext, editor} = setupNumbersWithTombstones();
+ const overlay = peritext.overlay;
+ const range = peritext.range(peritext.pointStart()!, peritext.pointEnd()!);
+ editor.cursor.setRange(range);
+ peritext.refresh();
+ expect(editor.cursor.text()).toBe('0123456789');
+ const overlayPoints = [...overlay];
+ expect(overlayPoints.length).toBe(2);
+ expect(overlayPoints[0].id.time).toBe(editor.cursor.start.id.time);
+ expect(overlayPoints[1].id.time).toBe(editor.cursor.end.id.time);
+ });
+
+ test('can select all text using absolute range', () => {
+ const {peritext, editor} = setupNumbersWithTombstones();
+ const overlay = peritext.overlay;
+ const range = peritext.range(peritext.pointAbsStart(), peritext.pointAbsEnd());
+ editor.cursor.setRange(range);
+ peritext.refresh();
+ expect(editor.cursor.text()).toBe('0123456789');
+ const overlayPoints = [...overlay];
+ expect(overlayPoints.length).toBe(2);
+ expect(overlayPoints[0].id.time).toBe(editor.cursor.start.id.time);
+ expect(overlayPoints[1].id.time).toBe(editor.cursor.end.id.time);
+ });
+
const testRefresh = (name: string, update: (kit: Kit, refresh: () => void) => void) => {
test(name, () => {
const kit = setup();
diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts
index 71ac4585ce..32d53ea071 100644
--- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts
+++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts
@@ -21,7 +21,7 @@ const setup = () => {
const markerCount = (peritext: Peritext): number => {
const overlay = peritext.overlay;
- const iterator = overlay.splitIterator();
+ const iterator = overlay.markerIterator();
let count = 0;
for (let split = iterator(); split; split = iterator()) {
count++;
@@ -117,7 +117,7 @@ describe('markers', () => {
expect(markerCount(peritext)).toBe(2);
const points = [];
let point;
- for (const iterator = peritext.overlay.splitIterator(); (point = iterator()); ) points.push(point);
+ for (const iterator = peritext.overlay.markerIterator(); (point = iterator()); ) points.push(point);
expect(points.length).toBe(2);
expect(points[0].pos()).toBe(2);
expect(points[1].pos()).toBe(11);
@@ -203,7 +203,7 @@ describe('slices', () => {
test('intersecting slice before split, should not update the split', () => {
const {peritext} = setup();
peritext.editor.cursor.setAt(6);
- const slice = peritext.editor.insMarker(['p']);
+ peritext.editor.insMarker(['p']);
peritext.refresh();
const point = peritext.overlay.find((point) => point instanceof MarkerOverlayPoint)!;
expect(point.layers.length).toBe(0);
diff --git a/src/json-crdt-extensions/peritext/rga/Point.ts b/src/json-crdt-extensions/peritext/rga/Point.ts
index ed74960797..9e5edf94e4 100644
--- a/src/json-crdt-extensions/peritext/rga/Point.ts
+++ b/src/json-crdt-extensions/peritext/rga/Point.ts
@@ -2,6 +2,7 @@ import {compare, type ITimestampStruct, printTs, equal, tick, containsId} from '
import {Anchor} from './constants';
import {ChunkSlice} from '../util/ChunkSlice';
import {updateId} from '../../../json-crdt/hash';
+import {Position} from '../constants';
import type {AbstractRga, Chunk} from '../../../json-crdt/nodes/rga';
import type {Stateful} from '../types';
import type {Printable} from 'tree-dump/lib/types';
@@ -125,7 +126,7 @@ export class Point implements Pick, Printable {
*/
public pos(): number {
const chunk = this.chunk();
- if (!chunk) return -1;
+ if (!chunk) return this.isAbsEnd() ? Position.AbsEnd : Position.AbsStart;
const pos = this.rga.pos(chunk);
if (chunk.del) return pos;
return pos + this.id.time - chunk.id.time;
@@ -137,7 +138,8 @@ export class Point implements Pick, Printable {
*/
public viewPos(): number {
const pos = this.pos();
- if (pos < 0) return this.isAbsStart() ? 0 : this.rga.length();
+ const isAbs = equal(this.rga.id, this.id);
+ if (isAbs) return this.anchor === Anchor.After ? 0 : this.rga.length();
return this.anchor === Anchor.Before ? pos : pos + 1;
}
@@ -453,6 +455,6 @@ export class Point implements Pick, Printable {
const pos = this.pos();
const id = printTs(this.id);
const anchor = this.anchor === Anchor.Before ? '.▢' : '▢.';
- return `${name}{ ${pos}, ${id}, ${anchor} }`;
+ return `${name}{ ${pos === Position.AbsEnd ? '∞' : pos}, ${id}, ${anchor} }`;
}
}
diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts
index 81e0864ace..95c0d656ce 100644
--- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts
+++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts
@@ -4,7 +4,7 @@ import {Range} from '../rga/Range';
import {updateNode} from '../../../json-crdt/hash';
import {printTree} from 'tree-dump/lib/printTree';
import {Anchor} from '../rga/constants';
-import {SliceHeaderMask, SliceHeaderShift, SliceBehavior, SliceTupleIndex} from './constants';
+import {SliceHeaderMask, SliceHeaderShift, SliceBehavior, SliceTupleIndex, SliceBehaviorName} from './constants';
import {CONST} from '../../../json-hash';
import {Timestamp} from '../../../json-crdt-patch/clock';
import {VecNode} from '../../../json-crdt/nodes';
@@ -168,7 +168,9 @@ export class PersistedSlice extends Range implements MutableSlice
const data = this.data();
const dataFormatted = data ? prettyOneLine(data) : '∅';
const dataLengthBreakpoint = 32;
- const header = `${this.constructor.name} ${super.toString('', true)}, ${this.behavior}, ${JSON.stringify(this.type)}${dataFormatted.length < dataLengthBreakpoint ? `, ${dataFormatted}` : ''}`;
+ const header = `${this.constructor.name} ${super.toString('', true)}, ${
+ SliceBehaviorName[this.behavior]
+ }, ${JSON.stringify(this.type)}${dataFormatted.length < dataLengthBreakpoint ? `, ${dataFormatted}` : ''}`;
return header;
}
diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts
index 42f4ecab9d..a4cb00a804 100644
--- a/src/json-crdt-extensions/peritext/slice/Slices.ts
+++ b/src/json-crdt-extensions/peritext/slice/Slices.ts
@@ -76,7 +76,7 @@ export class Slices implements Stateful, Printable {
return slice;
}
- public insMarker(range: Range, type: SliceType, data?: unknown): MarkerSlice {
+ public insMarker(range: Range, type: SliceType, data?: unknown | ITimestampStruct): MarkerSlice {
return this.ins(range, SliceBehavior.Marker, type, data) as MarkerSlice;
}
@@ -103,15 +103,15 @@ export class Slices implements Stateful, Printable {
return this.insMarker(range, type, data);
}
- public insStack(range: Range, type: SliceType, data?: unknown): PersistedSlice {
+ public insStack(range: Range, type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice {
return this.ins(range, SliceBehavior.Stack, type, data);
}
- public insOverwrite(range: Range, type: SliceType, data?: unknown): PersistedSlice {
+ public insOverwrite(range: Range, type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice {
return this.ins(range, SliceBehavior.Overwrite, type, data);
}
- public insErase(range: Range, type: SliceType, data?: unknown): PersistedSlice {
+ public insErase(range: Range, type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice {
return this.ins(range, SliceBehavior.Erase, type, data);
}
diff --git a/src/json-crdt-extensions/peritext/slice/constants.ts b/src/json-crdt-extensions/peritext/slice/constants.ts
index 9d59aac72a..11c016a3c5 100644
--- a/src/json-crdt-extensions/peritext/slice/constants.ts
+++ b/src/json-crdt-extensions/peritext/slice/constants.ts
@@ -53,10 +53,20 @@ export const enum SliceBehavior {
/**
* Used to mark the user's cursor position in the document.
+ *
+ * @todo Consider removing this.
*/
Cursor = 0b100,
}
+export enum SliceBehaviorName {
+ Marker = SliceBehavior.Marker,
+ Stack = SliceBehavior.Stack,
+ Overwrite = SliceBehavior.Overwrite,
+ Erase = SliceBehavior.Erase,
+ Cursor = SliceBehavior.Cursor,
+}
+
/**
* Specifies `vec` offsets in the {@link SliceView}.
*/
diff --git a/src/util/iterator.ts b/src/util/iterator.ts
new file mode 100644
index 0000000000..143506549d
--- /dev/null
+++ b/src/util/iterator.ts
@@ -0,0 +1,19 @@
+export class UndefEndIter implements IterableIterator {
+ constructor(private readonly i: () => T | undefined) {}
+
+ public next(): IteratorResult {
+ const value = this.i();
+ return new IterRes(value, value === undefined) as IteratorResult;
+ }
+
+ [Symbol.iterator]() {
+ return this;
+ }
+}
+
+export class IterRes {
+ constructor(
+ public readonly value: T,
+ public readonly done: boolean,
+ ) {}
+}