Skip to content

Commit

Permalink
fix(json-crdt-extensions): 🐛 correctly store extra and local slices
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed May 9, 2024
1 parent ed6ce96 commit 636a166
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 13 deletions.
6 changes: 5 additions & 1 deletion src/json-crdt-extensions/peritext/editor/EditorSlices.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {PersistedSlice} from '../slice/PersistedSlice';
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<T = string> {
Expand Down Expand Up @@ -42,4 +42,8 @@ export class EditorSlices<T = string> {
return marker;
});
}

public del(sliceOrId: PersistedSlice | ITimestampStruct): void {
this.slices.del(sliceOrId instanceof PersistedSlice ? sliceOrId.id : sliceOrId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {Kit, setupNumbersKit, setupNumbersWithTombstonesKit} from '../../__tests__/setup';
import {MarkerOverlayPoint} from '../MarkerOverlayPoint';

const runMarkersTests = (setup: () => Kit) => {
describe('.markers()', () => {
test('returns empty set by default', () => {
const {peritext} = setup();
peritext.overlay.refresh();
const list = [...peritext.overlay.markers()];
expect(list.length).toBe(0);
});

test('returns a single marker', () => {
const {peritext, editor} = setup();
editor.cursor.setAt(3);
editor.saved.insMarker('<paragraph>');
peritext.overlay.refresh();
const list = [...peritext.overlay.markers()];
expect(list.length).toBe(1);
expect(list[0] instanceof MarkerOverlayPoint).toBe(true);
});

test('can iterate through multiple markers', () => {
const {peritext, editor} = setup();
editor.cursor.setAt(5);
const [m2] = editor.saved.insMarker('<p2>');
peritext.overlay.refresh();
editor.cursor.setAt(8);
const [m3] = editor.local.insMarker('<p3>');
peritext.overlay.refresh();
editor.cursor.setAt(2);
const [m1] = editor.local.insMarker('<p1>');
peritext.overlay.refresh();
const list = [...peritext.overlay.markers()];
expect(list.length).toBe(3);
list.forEach(m => expect(m instanceof MarkerOverlayPoint).toBe(true));
expect(list[0].marker).toBe(m1);
expect(list[1].marker).toBe(m2);
expect(list[2].marker).toBe(m3);
});

test('can delete markers', () => {
const {peritext, editor} = setup();
editor.cursor.setAt(5);
const [m2] = editor.extra.insMarker('<p2>');
editor.cursor.setAt(8);
const [m3] = editor.local.insMarker('<p3>');
editor.cursor.setAt(2);
const [m1] = editor.local.insMarker('<p1>');
peritext.overlay.refresh();
const list = [...peritext.overlay.markers()];
expect(list.length).toBe(3);
editor.local.del(m3);
peritext.overlay.refresh();
const list2 = [...peritext.overlay.markers()];
expect(list2.length).toBe(2);
expect(list2[0].marker).toBe(m1);
expect(list2[1].marker).toBe(m2);
editor.local.del(m2);
peritext.overlay.refresh();
const list3 = [...peritext.overlay.markers()];
expect(list3.length).toBe(2);
expect(list3[0].marker).toBe(m1);
expect(list3[1].marker).toBe(m2);
editor.extra.del(m2);
peritext.overlay.refresh();
const list4 = [...peritext.overlay.markers()];
expect(list4.length).toBe(1);
expect(list4[0].marker).toBe(m1);
editor.local.del(m1);
editor.local.del(m1);
editor.local.del(m1);
peritext.overlay.refresh();
expect([...peritext.overlay.markers()].length).toBe(0);
});
});
};

describe('numbers "0123456789", no edits', () => {
runMarkersTests(setupNumbersKit);
});

describe('numbers "0123456789", with default schema and tombstones', () => {
runMarkersTests(setupNumbersWithTombstonesKit);
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {Peritext} from '../../Peritext';
import type {OverlayPoint} from '../OverlayPoint';

const setup = () => {
const model = Model.withLogicalClock();
const model = Model.create();
const api = model.api;
api.root({
text: '',
Expand Down
24 changes: 13 additions & 11 deletions src/json-crdt-extensions/peritext/slice/Slices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ export class Slices<T = string> implements Stateful, Printable {
data?: unknown,
Klass: K = behavior === SliceBehavior.Marker ? <any>MarkerSlice : PersistedSlice,
): S {
const model = this.set.doc;
const slicesModel = this.set.doc;
const set = this.set;
const api = model.api;
const api = slicesModel.api;
const builder = api.builder;
const tupleId = builder.vec();
const start = range.start.clone();
Expand All @@ -68,10 +68,10 @@ export class Slices<T = string> implements Stateful, Printable {
const chunkId = builder.insArr(set.id, set.id, [tupleId]);
// TODO: Consider using `s` schema here.
api.apply();
const tuple = model.index.get(tupleId) as VecNode;
const tuple = slicesModel.index.get(tupleId) as VecNode;
const chunk = set.findById(chunkId)!;
// TODO: Need to check if split slice text was deleted
const slice = new Klass(model, this.txt, chunk, tuple, behavior, type, start, end);
const slice = new Klass(slicesModel, this.txt, chunk, tuple, behavior, type, start, end);
this.list.set(chunk.id, slice);
return slice;
}
Expand All @@ -87,17 +87,17 @@ export class Slices<T = string> implements Stateful, Printable {
separator: string = Chars.BlockSplitSentinel,
): MarkerSlice<T> {
// TODO: test condition when cursors is at absolute or relative starts
const {txt, set} = this;
const model = set.doc;
const api = model.api;
const txt = this.txt;
const api = txt.model.api;
const builder = api.builder;
const str = txt.str;
/**
* We skip one clock cycle to prevent Block-wise RGA from merging adjacent
* characters. We want the marker chunk to always be its own distinct chunk.
*/
builder.nop(1);
const textId = builder.insStr(str.id, after, separator);
// TODO: Handle case when marker is inserted at the abs start, prevent abs start/end inserts.
const textId = builder.insStr(txt.str.id, after, separator);
api.apply();
const point = txt.point(textId, Anchor.Before);
const range = txt.range(point, point.clone());
return this.insMarker(range, type, data);
Expand Down Expand Up @@ -134,8 +134,10 @@ export class Slices<T = string> implements Stateful, Printable {

public del(id: ITimestampStruct): void {
this.list.del(id);
const api = this.set.doc.api;
api.builder.del(this.set.id, [tss(id.sid, id.time, 1)]);
const set = this.set;
const api = set.doc.api;
// TODO: Is it worth checking if the slice is already deleted?
api.builder.del(set.id, [tss(id.sid, id.time, 1)]);
api.apply();
}

Expand Down

0 comments on commit 636a166

Please sign in to comment.