Skip to content

Commit

Permalink
feat(json-crdt-extensions): 馃幐 add more slice layers
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed May 1, 2024
1 parent e7a21c8 commit 7971f21
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 97 deletions.
46 changes: 40 additions & 6 deletions src/json-crdt-extensions/peritext/Peritext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {Slices} from './slice/Slices';
import {Overlay} from './overlay/Overlay';
import {Chars} from './constants';
import {interval} from '../../json-crdt-patch/clock';
import {Model} from '../../json-crdt/model';
import {CONST, updateNum} from '../../json-hash';
import {SESSION} from '../../json-crdt-patch/constants';
import {s} from '../../json-crdt-patch';
import type {ITimestampStruct} from '../../json-crdt-patch/clock';
import type {Model} from '../../json-crdt/model';
import type {Printable} from 'tree-dump/lib/types';
import type {SliceType} from './types';
import type {MarkerSlice} from './slice/MarkerSlice';
Expand All @@ -20,7 +22,26 @@ import type {MarkerSlice} from './slice/MarkerSlice';
* interact with the text.
*/
export class Peritext implements Printable {
public readonly slices: Slices;
/**
* *Slices* are rich-text annotations that appear in the text. The "saved"
* slices are the ones that are persisted in the document.
*/
public readonly savedSlices: Slices;

/**
* *Extra slices* are slices that are not persisted in the document. However,
* they are still shared across users, i.e. they are ephemerally persisted
* during the editing session.
*/
public readonly extraSlices: Slices;

/**
* *Local slices* are slices that are not persisted in the document and are
* not shared with other users. They are used only for local annotations for
* the current user.
*/
public readonly localSlices: Slices;

public readonly editor: Editor;
public readonly overlay = new Overlay(this);

Expand All @@ -29,7 +50,20 @@ export class Peritext implements Printable {
public readonly str: StrNode,
slices: ArrNode,
) {
this.slices = new Slices(this.model, slices, this.str);
this.savedSlices = new Slices(this.model, slices, this.str);

const extraModel = Model
.withLogicalClock(SESSION.GLOBAL)
.setSchema(s.vec(s.arr([])))
.fork(this.model.clock.sid + 1);
this.extraSlices = new Slices(extraModel, extraModel.root.node().get(0)!, this.str);

// TODO: flush patches
const localModel = Model
.withLogicalClock(SESSION.LOCAL)
.setSchema(s.vec(s.arr([])));
this.localSlices = new Slices(localModel, localModel.root.node().get(0)!, this.str);

this.editor = new Editor(this);
}

Expand Down Expand Up @@ -183,7 +217,7 @@ export class Peritext implements Printable {
const textId = builder.insStr(str.id, after, char[0]);
const point = this.point(textId, Anchor.Before);
const range = this.range(point, point);
return this.slices.insMarker(range, type, data);
return this.savedSlices.insMarker(range, type, data);
}

/** @todo This can probably use .del() */
Expand All @@ -193,7 +227,7 @@ export class Peritext implements Printable {
const builder = api.builder;
const strChunk = split.start.chunk();
if (strChunk) builder.del(str.id, [interval(strChunk.id, 0, 1)]);
builder.del(this.slices.set.id, [interval(split.id, 0, 1)]);
builder.del(this.savedSlices.set.id, [interval(split.id, 0, 1)]);
api.apply();
}

Expand All @@ -208,7 +242,7 @@ export class Peritext implements Printable {
nl,
(tab) => this.str.toString(tab),
nl,
(tab) => this.slices.toString(tab),
(tab) => this.savedSlices.toString(tab),
nl,
(tab) => this.overlay.toString(tab),
])
Expand Down
6 changes: 3 additions & 3 deletions src/json-crdt-extensions/peritext/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,17 @@ export class Editor implements Printable {

public insStackSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice {
const range = this.cursor.range();
return this.txt.slices.ins(range, SliceBehavior.Stack, type, data);
return this.txt.savedSlices.ins(range, SliceBehavior.Stack, type, data);
}

public insOverwriteSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice {
const range = this.cursor.range();
return this.txt.slices.ins(range, SliceBehavior.Overwrite, type, data);
return this.txt.savedSlices.ins(range, SliceBehavior.Overwrite, type, data);
}

public insEraseSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice {
const range = this.cursor.range();
return this.txt.slices.ins(range, SliceBehavior.Erase, type, data);
return this.txt.savedSlices.ins(range, SliceBehavior.Erase, type, data);
}

public insMarker(type: SliceType, data?: unknown): MarkerSlice {
Expand Down
6 changes: 3 additions & 3 deletions src/json-crdt-extensions/peritext/overlay/Overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,17 @@ export class Overlay implements Printable, Stateful {
return (this.hash = hash);
}

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

private refreshSlices(state: number): number {
const slices = this.txt.slices;
const slices = this.txt.savedSlices;
const oldSlicesHash = slices.hash;
const changed = oldSlicesHash !== slices.refresh();
const sliceSet = this.slices;
state = updateNum(state, slices.hash);
if (changed) {
slices.forEach((slice) => {
console.log('slice', slice + '');
// console.log('slice', slice + '');
let tuple: [start: OverlayPoint, end: OverlayPoint] | undefined = sliceSet.get(slice);
if (tuple) {
if ((slice as any).isDel && (slice as any).isDel()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe('Overlay.refresh()', () => {
kit.peritext.editor.cursor.setAt(0, 1);
const slice = kit.peritext.editor.insStackSlice('<b>');
refresh();
slice.del();
kit.peritext.savedSlices.del(slice.id);
});

testRefresh('when slice type is changed', (kit, refresh) => {
Expand Down Expand Up @@ -132,7 +132,7 @@ describe('Overlay.refresh()', () => {
kit.peritext.editor.insStackSlice(1, 1);
kit.peritext.editor.insStackSlice(3, 3);
const range1 = kit.peritext.rangeAt(1, 2);
const slice = kit.peritext.slices.insErase(range1, 'gg');
const slice = kit.peritext.savedSlices.insErase(range1, 'gg');
expect(slice.end.anchor).toBe(Anchor.After);
refresh();
const range2 = kit.peritext.rangeAt(2, 2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ describe('slices', () => {
expect(peritext.overlay.slices.size).toBe(0);
peritext.overlay.refresh();
expect(peritext.overlay.slices.size).toBe(2);
peritext.slices.del(slice.id);
peritext.savedSlices.del(slice.id);
expect(peritext.overlay.slices.size).toBe(2);
peritext.overlay.refresh();
expect(peritext.overlay.slices.size).toBe(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const setupOverlayPoint = () => {
describe('layers', () => {
test('can add a layer', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const point = getPoint(slice.start);
expect(point.layers.length).toBe(0);
point.addLayer(slice);
Expand All @@ -27,7 +27,7 @@ describe('layers', () => {

test('inserting same slice twice is a no-op', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const point = getPoint(slice.start);
expect(point.layers.length).toBe(0);
point.addLayer(slice);
Expand All @@ -39,8 +39,8 @@ describe('layers', () => {

test('can add two layers with the same start position', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
const slice1 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice2 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
const point = getPoint(slice1.start);
expect(point.layers.length).toBe(0);
point.addLayer(slice1);
Expand All @@ -54,8 +54,8 @@ describe('layers', () => {

test('orders slices by their ID', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
const slice1 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice2 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
const point = getPoint(slice1.start);
point.addLayer(slice2);
point.addLayer(slice1);
Expand All @@ -65,9 +65,9 @@ describe('layers', () => {

test('can add tree layers and sort them correctly', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
const slice3 = peritext.slices.insOverwrite(peritext.rangeAt(2, 10), '<u>');
const slice1 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice2 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
const slice3 = peritext.savedSlices.insOverwrite(peritext.rangeAt(2, 10), '<u>');
const point = getPoint(slice1.start);
point.addLayer(slice3);
point.addLayer(slice3);
Expand All @@ -84,9 +84,9 @@ describe('layers', () => {

test('can add tree layers by appending them', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
const slice3 = peritext.slices.insOverwrite(peritext.rangeAt(2, 10), '<u>');
const slice1 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice2 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
const slice3 = peritext.savedSlices.insOverwrite(peritext.rangeAt(2, 10), '<u>');
const point = getPoint(slice1.start);
point.addLayer(slice1);
point.addLayer(slice2);
Expand All @@ -98,9 +98,9 @@ describe('layers', () => {

test('can remove layers', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
const slice3 = peritext.slices.insOverwrite(peritext.rangeAt(2, 10), '<u>');
const slice1 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 5), '<b>');
const slice2 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 3), '<i>');
const slice3 = peritext.savedSlices.insOverwrite(peritext.rangeAt(2, 10), '<u>');
const point = getPoint(slice1.start);
point.addLayer(slice2);
point.addLayer(slice1);
Expand All @@ -124,7 +124,7 @@ describe('layers', () => {
describe('markers', () => {
test('can add a marker', () => {
const {peritext, getPoint} = setupOverlayPoint();
const marker = peritext.slices.insMarker(peritext.rangeAt(5, 0), '<p>');
const marker = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '<p>');
const point = getPoint(marker.start);
expect(point.markers.length).toBe(0);
point.addMarker(marker);
Expand All @@ -134,7 +134,7 @@ describe('markers', () => {

test('inserting same marker twice is a no-op', () => {
const {peritext, getPoint} = setupOverlayPoint();
const marker = peritext.slices.insMarker(peritext.rangeAt(5, 0), '<p>');
const marker = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '<p>');
const point = getPoint(marker.start);
expect(point.markers.length).toBe(0);
point.addMarker(marker);
Expand All @@ -147,8 +147,8 @@ describe('markers', () => {

test('can add two markers with the same start position', () => {
const {peritext, getPoint} = setupOverlayPoint();
const marker1 = peritext.slices.insMarker(peritext.rangeAt(5, 0), '<p>');
const marker2 = peritext.slices.insMarker(peritext.rangeAt(5, 0), '<p>');
const marker1 = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '<p>');
const marker2 = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '<p>');
const point = getPoint(marker1.start);
expect(point.markers.length).toBe(0);
point.addMarker(marker1);
Expand All @@ -162,8 +162,8 @@ describe('markers', () => {

test('orders markers by their ID', () => {
const {peritext, getPoint} = setupOverlayPoint();
const marker1 = peritext.slices.insMarker(peritext.rangeAt(5, 0), '<p>');
const marker2 = peritext.slices.insMarker(peritext.rangeAt(5, 0), '<p>');
const marker1 = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '<p>');
const marker2 = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '<p>');
const point = getPoint(marker1.start);
point.addMarker(marker2);
point.addMarker(marker1);
Expand All @@ -177,9 +177,9 @@ describe('markers', () => {

test('can add tree markers and sort them correctly', () => {
const {peritext, getPoint} = setupOverlayPoint();
const marker1 = peritext.slices.insMarker(peritext.rangeAt(5, 0), '<p>');
const marker2 = peritext.slices.insMarker(peritext.rangeAt(5, 0), '<p>');
const marker3 = peritext.slices.insMarker(peritext.rangeAt(5, 0), '<p>');
const marker1 = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '<p>');
const marker2 = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '<p>');
const marker3 = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '<p>');
const point = getPoint(marker1.start);
point.addMarker(marker3);
point.addMarker(marker3);
Expand All @@ -197,9 +197,9 @@ describe('markers', () => {

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

test('can remove markers', () => {
const {peritext, getPoint} = setupOverlayPoint();
const marker1 = peritext.slices.insMarker(peritext.rangeAt(6, 1), '<p>');
const marker2 = peritext.slices.insMarker(peritext.rangeAt(6, 1), '<p>');
const marker3 = peritext.slices.insMarker(peritext.rangeAt(6, 2), '<p>');
const marker1 = peritext.savedSlices.insMarker(peritext.rangeAt(6, 1), '<p>');
const marker2 = peritext.savedSlices.insMarker(peritext.rangeAt(6, 1), '<p>');
const marker3 = peritext.savedSlices.insMarker(peritext.rangeAt(6, 2), '<p>');
const point = getPoint(marker1.start);
point.addMarker(marker2);
point.addMarker(marker1);
Expand All @@ -237,7 +237,7 @@ describe('markers', () => {
describe('refs', () => {
test('can add marker ref', () => {
const {peritext, getPoint} = setupOverlayPoint();
const marker = peritext.slices.insMarker(peritext.rangeAt(10, 1), '<p>');
const marker = peritext.savedSlices.insMarker(peritext.rangeAt(10, 1), '<p>');
const point = getPoint(marker.start);
expect(point.markers.length).toBe(0);
expect(point.refs.length).toBe(0);
Expand All @@ -250,7 +250,7 @@ describe('refs', () => {

test('can add layer ref (start)', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice = peritext.slices.insErase(peritext.rangeAt(0, 4), 123);
const slice = peritext.savedSlices.insErase(peritext.rangeAt(0, 4), 123);
const point = getPoint(slice.start);
expect(point.layers.length).toBe(0);
expect(point.refs.length).toBe(0);
Expand All @@ -263,7 +263,7 @@ describe('refs', () => {

test('can add layer ref (end)', () => {
const {peritext, getPoint} = setupOverlayPoint();
const slice = peritext.slices.insErase(peritext.rangeAt(0, 4), 123);
const slice = peritext.savedSlices.insErase(peritext.rangeAt(0, 4), 123);
const point = getPoint(slice.end);
expect(point.layers.length).toBe(0);
expect(point.refs.length).toBe(0);
Expand All @@ -275,8 +275,8 @@ describe('refs', () => {

test('can add marker and layer start', () => {
const {peritext, getPoint} = setupOverlayPoint();
const marker = peritext.slices.insMarker(peritext.rangeAt(10, 1), '<p>');
const slice = peritext.slices.insErase(peritext.rangeAt(10, 4), 123);
const marker = peritext.savedSlices.insMarker(peritext.rangeAt(10, 1), '<p>');
const slice = peritext.savedSlices.insErase(peritext.rangeAt(10, 4), 123);
const point = getPoint(slice.end);
expect(point.layers.length).toBe(0);
expect(point.markers.length).toBe(0);
Expand All @@ -290,8 +290,8 @@ describe('refs', () => {

test('can remove marker and layer', () => {
const {peritext, getPoint} = setupOverlayPoint();
const marker = peritext.slices.insMarker(peritext.rangeAt(10, 1), '<p>');
const slice = peritext.slices.insErase(peritext.rangeAt(10, 4), 123);
const marker = peritext.savedSlices.insMarker(peritext.rangeAt(10, 1), '<p>');
const slice = peritext.savedSlices.insErase(peritext.rangeAt(10, 4), 123);
const point = getPoint(slice.end);
point.addMarkerRef(marker);
point.addLayerStartRef(slice);
Expand Down
2 changes: 1 addition & 1 deletion src/json-crdt-extensions/peritext/slice/Slices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class Slices implements Stateful, Printable {

constructor(
/** The model, which powers the CRDT nodes. */
public readonly model: Model,
public readonly model: Model<any>,
/** The `arr` node, used as a set, where slices are stored. */
public readonly set: ArrNode,
/** The text RGA. */
Expand Down

0 comments on commit 7971f21

Please sign in to comment.