From 94ac96bfcc3cb6a718ca27abf1780eb7935b4812 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 30 Apr 2024 10:23:42 +0200 Subject: [PATCH 01/19] =?UTF-8?q?refactor(json-crdt):=20=F0=9F=92=A1=20imp?= =?UTF-8?q?lement=20method=20which=20find=20first=20visible=20chunk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 12 ------------ src/json-crdt-extensions/peritext/overlay/Overlay.ts | 3 ++- src/json-crdt/nodes/rga/util.ts | 12 ++++++++++++ 3 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 src/json-crdt/nodes/rga/util.ts diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index 443a097e9c..d320449bd5 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -38,18 +38,6 @@ export class Peritext implements Printable { return this.model.api.wrap(this.str); } - /** @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; - } - /** Select a single character before a point. */ public findCharBefore(point: Point): Range | undefined { if (point.anchor === Anchor.After) { diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 21216ff46a..954650e035 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -10,6 +10,7 @@ import {OverlayRefSliceEnd, OverlayRefSliceStart} from './refs'; import {equal, ITimestampStruct} 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 type {Peritext} from '../Peritext'; import type {Stateful} from '../types'; import type {Printable} from '../../../util/print/types'; @@ -197,7 +198,7 @@ export class Overlay implements Printable, Stateful { let endPoint = slice.end; const startIsStringRoot = equal(startPoint.id, str.id); if (startIsStringRoot) { - const firstVisibleChunk = txt.firstVisChunk(); + const firstVisibleChunk = firstVis(txt.str); if (firstVisibleChunk) { startPoint = txt.point(firstVisibleChunk.id, Anchor.Before); const endIsStringRoot = equal(endPoint.id, str.id); diff --git a/src/json-crdt/nodes/rga/util.ts b/src/json-crdt/nodes/rga/util.ts new file mode 100644 index 0000000000..a6807d23e9 --- /dev/null +++ b/src/json-crdt/nodes/rga/util.ts @@ -0,0 +1,12 @@ +import type {AbstractRga, Chunk} from "./AbstractRga"; + +/** Find the first visible chunk, if any. */ +export const firstVis = (rga: AbstractRga): Chunk | undefined => { + let curr = rga.first(); + if (!curr) return; + while (curr.del) { + curr = rga.next(curr); + if (!curr) return; + } + return curr; +}; From 5e7e66120233120d5fc8f9fc0dab3da0d7396299 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 30 Apr 2024 10:27:53 +0200 Subject: [PATCH 02/19] =?UTF-8?q?perf(json-crdt):=20=E2=9A=A1=EF=B8=8F=20i?= =?UTF-8?q?mprove=20first=20chunk=20finding=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt/nodes/rga/util.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/json-crdt/nodes/rga/util.ts b/src/json-crdt/nodes/rga/util.ts index a6807d23e9..6bbbf3c2ae 100644 --- a/src/json-crdt/nodes/rga/util.ts +++ b/src/json-crdt/nodes/rga/util.ts @@ -3,10 +3,6 @@ import type {AbstractRga, Chunk} from "./AbstractRga"; /** Find the first visible chunk, if any. */ export const firstVis = (rga: AbstractRga): Chunk | undefined => { let curr = rga.first(); - if (!curr) return; - while (curr.del) { - curr = rga.next(curr); - if (!curr) return; - } + while (curr && curr.del) curr = rga.next(curr); return curr; }; From be724c397f2ead8433e27ea3d5cf39cb4f8afc91 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 30 Apr 2024 11:03:50 +0200 Subject: [PATCH 03/19] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20add=20Peritext=20overlay=20smoke=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/Peritext.overlay.spec.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts new file mode 100644 index 0000000000..1353299cf3 --- /dev/null +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts @@ -0,0 +1,49 @@ +import {Model} from '../../../json-crdt/model'; +import {size} from 'sonic-forest/lib/util'; +import {Peritext} from '../Peritext'; + +const setup = () => { + const model = Model.withLogicalClock(); + model.api.root({ + text: '', + slices: [], + }); + model.api.str(['text']).ins(0, 'wworld'); + model.api.str(['text']).ins(0, 'helo '); + model.api.str(['text']).ins(2, 'l'); + model.api.str(['text']).del(7, 1); + const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node); + return {model, peritext}; +}; + +test('can insert markers', () => { + const {peritext} = setup(); + const {editor} = peritext; + expect(size(peritext.overlay.root)).toBe(0); + editor.setCursor(0); + editor.insMarker(['p'], '

'); + peritext.refresh(); + expect(size(peritext.overlay.root)).toBe(1); + editor.setCursor(9); + editor.insMarker(['p'], '

'); + peritext.refresh(); + expect(size(peritext.overlay.root)).toBe(3); +}); + +test('can insert slices', () => { + const {peritext} = setup(); + const {editor} = peritext; + expect(size(peritext.overlay.root)).toBe(0); + editor.setCursor(2, 2); + editor.insertSlice('bold'); + peritext.refresh(); + expect(size(peritext.overlay.root)).toBe(2); + editor.setCursor(6, 5); + editor.insertSlice('italic'); + peritext.refresh(); + expect(size(peritext.overlay.root)).toBe(4); + editor.setCursor(0, 5); + editor.insertSlice('underline'); + peritext.refresh(); + expect(size(peritext.overlay.root)).toBe(6); +}); From 554a0fdc14d54fb95e2ff0590698fd5af01bc451 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 30 Apr 2024 11:10:22 +0200 Subject: [PATCH 04/19] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20add=20Overlay.getOrNextLower()=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/Overlay.getOrNextLower.spec.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLower.spec.ts 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 new file mode 100644 index 0000000000..3a797d7e15 --- /dev/null +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLower.spec.ts @@ -0,0 +1,52 @@ +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.insertSlice(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.insertSlice(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.insertSlice(3); + peritext.refresh(); + peritext.editor.cursor.setAt(2, 1); + peritext.editor.insertSlice(33); + peritext.refresh(); + const cnt = size(peritext.overlay.root); + expect(cnt).toBe(3); + }); +}); From 4391dbe6a847fc49522360db253e991a4ae846c8 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 30 Apr 2024 11:10:46 +0200 Subject: [PATCH 05/19] =?UTF-8?q?refactor(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=A1=20use=20non-deprecated=20cursor=20setting=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/Peritext.overlay.spec.ts | 10 ++--- .../overlay/__tests__/Overlay.spec.ts | 38 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) 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 1353299cf3..35ebb9caf6 100644 --- a/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts @@ -20,11 +20,11 @@ test('can insert markers', () => { const {peritext} = setup(); const {editor} = peritext; expect(size(peritext.overlay.root)).toBe(0); - editor.setCursor(0); + editor.cursor.setAt(0); editor.insMarker(['p'], '

'); peritext.refresh(); expect(size(peritext.overlay.root)).toBe(1); - editor.setCursor(9); + editor.cursor.setAt(9); editor.insMarker(['p'], '

'); peritext.refresh(); expect(size(peritext.overlay.root)).toBe(3); @@ -34,15 +34,15 @@ test('can insert slices', () => { const {peritext} = setup(); const {editor} = peritext; expect(size(peritext.overlay.root)).toBe(0); - editor.setCursor(2, 2); + editor.cursor.setAt(2, 2); editor.insertSlice('bold'); peritext.refresh(); expect(size(peritext.overlay.root)).toBe(2); - editor.setCursor(6, 5); + editor.cursor.setAt(6, 5); editor.insertSlice('italic'); peritext.refresh(); expect(size(peritext.overlay.root)).toBe(4); - editor.setCursor(0, 5); + editor.cursor.setAt(0, 5); editor.insertSlice('underline'); peritext.refresh(); expect(size(peritext.overlay.root)).toBe(6); 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 a770d0cb27..e96ba81bc1 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts @@ -38,7 +38,7 @@ describe('markers', () => { test('can insert one marker in the middle of text', () => { const {peritext} = setup(); - peritext.editor.setCursor(6); + peritext.editor.cursor.setAt(6); peritext.editor.insMarker(['p'], '¶'); expect(splitCount(peritext)).toBe(0); peritext.overlay.refresh(); @@ -54,14 +54,14 @@ describe('markers', () => { test('can insert two markers', () => { const {peritext} = setup(); - peritext.editor.setCursor(3); + peritext.editor.cursor.setAt(3); peritext.editor.insMarker(['p'], '¶'); expect(splitCount(peritext)).toBe(0); peritext.overlay.refresh(); expect(splitCount(peritext)).toBe(1); peritext.overlay.refresh(); expect(splitCount(peritext)).toBe(1); - peritext.editor.setCursor(9); + peritext.editor.cursor.setAt(9); peritext.editor.insMarker(['li'], '- '); expect(splitCount(peritext)).toBe(1); peritext.overlay.refresh(); @@ -74,7 +74,7 @@ describe('markers', () => { describe('deletes', () => { test('can delete a marker', () => { const {peritext} = setup(); - peritext.editor.setCursor(6); + peritext.editor.cursor.setAt(6); const slice = peritext.editor.insMarker(['p'], '¶'); peritext.refresh(); expect(splitCount(peritext)).toBe(1); @@ -89,9 +89,9 @@ describe('markers', () => { test('can delete one of two splits', () => { const {peritext} = setup(); - peritext.editor.setCursor(2); + peritext.editor.cursor.setAt(2); peritext.editor.insMarker(['p'], '¶'); - peritext.editor.setCursor(11); + peritext.editor.cursor.setAt(11); const slice = peritext.editor.insMarker(['p'], '¶'); peritext.refresh(); expect(splitCount(peritext)).toBe(2); @@ -108,11 +108,11 @@ describe('markers', () => { describe('iterates', () => { test('can iterate over markers', () => { const {peritext} = setup(); - peritext.editor.setCursor(1, 6); + peritext.editor.cursor.setAt(1, 6); peritext.editor.insertSlice('a', {a: 'b'}); - peritext.editor.setCursor(2); + peritext.editor.cursor.setAt(2); peritext.editor.insMarker(['p'], '¶'); - peritext.editor.setCursor(11); + peritext.editor.cursor.setAt(11); peritext.editor.insMarker(['p'], '¶'); peritext.refresh(); expect(splitCount(peritext)).toBe(2); @@ -135,7 +135,7 @@ describe('slices', () => { test('can insert one slice in the middle of text', () => { const {peritext} = setup(); - peritext.editor.setCursor(6, 2); + peritext.editor.cursor.setAt(6, 2); peritext.editor.insertSlice('em', {emphasis: true}); expect(peritext.overlay.slices.size).toBe(0); peritext.overlay.refresh(); @@ -152,9 +152,9 @@ describe('slices', () => { test('can insert two slices', () => { const {peritext} = setup(); - peritext.editor.setCursor(2, 8); + peritext.editor.cursor.setAt(2, 8); peritext.editor.insertSlice('em', {emphasis: true}); - peritext.editor.setCursor(4, 8); + peritext.editor.cursor.setAt(4, 8); peritext.editor.insertSlice('strong', {bold: true}); expect(peritext.overlay.slices.size).toBe(0); peritext.overlay.refresh(); @@ -167,9 +167,9 @@ describe('slices', () => { test('intersecting slice chunks point to two slices', () => { const {peritext} = setup(); - peritext.editor.setCursor(2, 2); + peritext.editor.cursor.setAt(2, 2); peritext.editor.insertSlice('em', {emphasis: true}); - peritext.editor.setCursor(3, 2); + peritext.editor.cursor.setAt(3, 2); peritext.editor.insertSlice('strong', {bold: true}); peritext.refresh(); const point1 = first(peritext.overlay.root)!; @@ -190,7 +190,7 @@ describe('slices', () => { test('one char slice should correctly sort overlay points', () => { const {peritext} = setup(); - peritext.editor.setCursor(0, 1); + peritext.editor.cursor.setAt(0, 1); peritext.editor.insertSlice('em', {emphasis: true}); peritext.refresh(); const point1 = peritext.overlay.first()!; @@ -203,16 +203,16 @@ describe('slices', () => { test('intersecting slice before split, should not update the split', () => { const {peritext} = setup(); - peritext.editor.setCursor(6); + peritext.editor.cursor.setAt(6); const slice = peritext.editor.insMarker(['p']); peritext.refresh(); const point = peritext.overlay.find((point) => point instanceof MarkerOverlayPoint)!; expect(point.layers.length).toBe(0); - peritext.editor.setCursor(2, 2); + peritext.editor.cursor.setAt(2, 2); peritext.editor.insertSlice(''); peritext.refresh(); expect(point.layers.length).toBe(0); - peritext.editor.setCursor(2, 1); + peritext.editor.cursor.setAt(2, 1); peritext.editor.insertSlice(''); peritext.refresh(); expect(point.layers.length).toBe(0); @@ -222,7 +222,7 @@ describe('slices', () => { describe('deletes', () => { test('can remove a slice', () => { const {peritext} = setup(); - peritext.editor.setCursor(6, 2); + peritext.editor.cursor.setAt(6, 2); const slice = peritext.editor.insertSlice('em', {emphasis: true}); expect(peritext.overlay.slices.size).toBe(0); peritext.overlay.refresh(); From 2774c3d84ea55a89b29bbb4b48ff33384b192889 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 30 Apr 2024 11:25:19 +0200 Subject: [PATCH 06/19] =?UTF-8?q?chore:=20=F0=9F=A4=96=20update=20yarn.loc?= =?UTF-8?q?k?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yarn.lock | 3 --- 1 file changed, 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 383cabecbd..3f8bf2914f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -955,7 +955,6 @@ concat-map@0.0.1: "config-housekeeping@https://github.com/streamich/housekeeping#3532d2abeac159315ddf403d70517859d079c801": version "0.0.0" - uid "3532d2abeac159315ddf403d70517859d079c801" resolved "https://github.com/streamich/housekeeping#3532d2abeac159315ddf403d70517859d079c801" convert-source-map@^2.0.0: @@ -1019,7 +1018,6 @@ diff@^4.0.1: "editing-traces@https://github.com/streamich/editing-traces#6494020428530a6e382378b98d1d7e31334e2d7b": version "0.0.0" - uid "6494020428530a6e382378b98d1d7e31334e2d7b" resolved "https://github.com/streamich/editing-traces#6494020428530a6e382378b98d1d7e31334e2d7b" electron-to-chromium@^1.4.668: @@ -1710,7 +1708,6 @@ jsesc@^2.5.1: "json-crdt-traces@https://github.com/streamich/json-crdt-traces#ec825401dc05cbb74b9e0b3c4d6527399f54d54d": version "0.0.1" - uid ec825401dc05cbb74b9e0b3c4d6527399f54d54d resolved "https://github.com/streamich/json-crdt-traces#ec825401dc05cbb74b9e0b3c4d6527399f54d54d" json-logic-js@^2.0.2: From 65440fe18efc7bbb49ca09a23b807336de59a266 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 30 Apr 2024 11:31:43 +0200 Subject: [PATCH 07/19] =?UTF-8?q?chore:=20=F0=9F=A4=96=20simplify=20Jest?= =?UTF-8?q?=20config=20and=20reduce=20test=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 7 ------- src/__tests__/setup.js | 2 -- .../__tests__/guide/1-JsonPatch.spec.ts | 20 +++++++++---------- .../__tests__/guide/2-ModelApi.spec.ts | 20 +++++++++---------- .../__tests__/guide/3-PatchBuilder.spec.ts | 20 +++++++++---------- src/json-crdt/__tests__/guide/4-Patch.spec.ts | 20 +++++++++---------- 6 files changed, 40 insertions(+), 49 deletions(-) delete mode 100644 src/__tests__/setup.js diff --git a/package.json b/package.json index 725db1a753..deee526458 100644 --- a/package.json +++ b/package.json @@ -148,13 +148,6 @@ "typescript": "^5.4.5" }, "jest": { - "verbose": true, - "testEnvironmentOptions": { - "url": "http://localhost/" - }, - "setupFiles": [ - "/src/__tests__/setup.js" - ], "moduleFileExtensions": [ "ts", "js" diff --git a/src/__tests__/setup.js b/src/__tests__/setup.js deleted file mode 100644 index e265fa1747..0000000000 --- a/src/__tests__/setup.js +++ /dev/null @@ -1,2 +0,0 @@ -// Jest setup. -process.env.JEST = true; diff --git a/src/json-crdt/__tests__/guide/1-JsonPatch.spec.ts b/src/json-crdt/__tests__/guide/1-JsonPatch.spec.ts index 7016e1cf3b..7159d3c16c 100644 --- a/src/json-crdt/__tests__/guide/1-JsonPatch.spec.ts +++ b/src/json-crdt/__tests__/guide/1-JsonPatch.spec.ts @@ -7,30 +7,30 @@ const doc = Model.withLogicalClock(); const jsonPatch = new JsonPatch(doc); test('can edit document using JSON Patch operations', () => { - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); jsonPatch.apply([{op: 'add', path: '', value: {foo: 'bar'}}]); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); jsonPatch.apply([{op: 'str_ins', path: '/foo', pos: 3, str: '!'}]); jsonPatch.apply([{op: 'str_ins', path: '/foo', pos: 4, str: ' baz!'}]); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); jsonPatch.apply([{op: 'str_ins', path: '/foo', pos: 5, str: 'qux! '}]); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); jsonPatch.apply([{op: 'add', path: '/list', value: [{title: 'To the dishes!'}, {title: 'Write more tests!'}]}]); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); expect(doc.view()).toStrictEqual({ foo: 'bar! qux! baz!', diff --git a/src/json-crdt/__tests__/guide/2-ModelApi.spec.ts b/src/json-crdt/__tests__/guide/2-ModelApi.spec.ts index 67a284c0f1..a81c4c9032 100644 --- a/src/json-crdt/__tests__/guide/2-ModelApi.spec.ts +++ b/src/json-crdt/__tests__/guide/2-ModelApi.spec.ts @@ -5,31 +5,31 @@ import {Model} from '../..'; const doc = Model.withLogicalClock(); test('can edit document using JSON Patch operations', () => { - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); doc.api.root({foo: 'bar'}); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); doc.api.str('/foo').ins(3, '!'); doc.api.str(['foo']).ins(4, ' baz!'); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); doc.api.str('/foo').ins(5, 'qux! '); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); doc.api.obj('').set({ list: [{title: 'To the dishes!'}, {title: 'Write more tests!'}], }); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); expect(doc.view()).toStrictEqual({ foo: 'bar! qux! baz!', diff --git a/src/json-crdt/__tests__/guide/3-PatchBuilder.spec.ts b/src/json-crdt/__tests__/guide/3-PatchBuilder.spec.ts index 9516428997..b7445b27c6 100644 --- a/src/json-crdt/__tests__/guide/3-PatchBuilder.spec.ts +++ b/src/json-crdt/__tests__/guide/3-PatchBuilder.spec.ts @@ -7,8 +7,8 @@ const doc = Model.withLogicalClock(); const builder = doc.api.builder; test('can edit document using JSON Patch operations', () => { - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); const obj = builder.obj(); const str = builder.str(); @@ -17,28 +17,28 @@ test('can edit document using JSON Patch operations', () => { builder.root(obj); doc.api.apply(); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); const insert2 = builder.insStr(str, tick(insert1, 2), '!'); doc.api.apply(); const insert3 = builder.insStr(str, insert2, ' baz!'); doc.api.apply(); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); const insert4 = builder.insStr(str, insert3, 'qux! '); doc.api.apply(); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); builder.insObj(obj, [['list', builder.json([{title: 'To the dishes!'}, {title: 'Write more tests!'}])]]); doc.api.apply(); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); expect(doc.view()).toStrictEqual({ foo: 'bar! qux! baz!', diff --git a/src/json-crdt/__tests__/guide/4-Patch.spec.ts b/src/json-crdt/__tests__/guide/4-Patch.spec.ts index adea47f6e5..697df567aa 100644 --- a/src/json-crdt/__tests__/guide/4-Patch.spec.ts +++ b/src/json-crdt/__tests__/guide/4-Patch.spec.ts @@ -12,8 +12,8 @@ const clock = doc.clock; const patch = new Patch(); test('can edit document using JSON Patch operations', () => { - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); const obj = clock.tick(1); patch.ops.push(new NewObjOp(obj)); @@ -32,8 +32,8 @@ test('can edit document using JSON Patch operations', () => { doc.applyPatch(patch); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); const insert2 = clock.tick(1); patch.ops.push(new InsStrOp(insert2, str, tick(insert1, 2), '!')); @@ -43,16 +43,16 @@ test('can edit document using JSON Patch operations', () => { doc.applyPatch(patch); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); const insert4 = clock.tick(5); patch.ops.push(new InsStrOp(insert4, str, insert3, 'qux! ')); doc.applyPatch(patch); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); const builder = new PatchBuilder(clock); const list = builder.json([{title: 'To the dishes!'}, {title: 'Write more tests!'}]); @@ -63,8 +63,8 @@ test('can edit document using JSON Patch operations', () => { doc.applyPatch(patch); - console.log(doc.view()); - console.log(doc.toString()); + // console.log(doc.view()); + // console.log(doc.toString()); expect(doc.view()).toStrictEqual({ foo: 'bar! qux! baz!', From de8116983e6cc29c52837c7563bfecb4de4e98a1 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 30 Apr 2024 13:54:59 +0200 Subject: [PATCH 08/19] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20add=20Overlay.refresh()=20slices=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../overlay/__tests__/Overlay.refresh.spec.ts | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.refresh.spec.ts 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 new file mode 100644 index 0000000000..a4c484d3a1 --- /dev/null +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.refresh.spec.ts @@ -0,0 +1,156 @@ +import {Model, ObjApi} from '../../../../json-crdt/model'; +import {Peritext} from '../../Peritext'; +import {Anchor} from '../../rga/constants'; +import {SliceBehavior} from '../../slice/constants'; + +const setup = () => { + const model = Model.withLogicalClock(); + model.api.root({ + text: '', + slices: [], + markers: [], + }); + model.api.str(['text']).ins(0, 'wworld'); + model.api.str(['text']).ins(0, 'helo '); + model.api.str(['text']).ins(2, 'l'); + model.api.str(['text']).del(7, 1); + const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node); + return {model, peritext}; +}; + +type Kit = ReturnType; + +describe('Overlay.refresh()', () => { + describe('slices', () => { + describe('updates hash', () => { + const testRefresh = (name: string, update: (kit: Kit, refresh: () => void) => void) => { + test(name, () => { + const kit = setup(); + const overlay = kit.peritext.overlay; + let hash1: number | undefined, hash2: number | undefined, hash3: number | undefined; + update(kit, () => { + hash1 = overlay.refresh(); + hash2 = overlay.refresh(); + hash3 = overlay.refresh(); + }); + const hash4 = overlay.refresh(); + const hash5 = overlay.refresh(); + const hash6 = overlay.refresh(); + expect(hash1).toBe(hash2); + expect(hash2).toBe(hash3); + expect(hash3).not.toBe(hash4); + expect(hash4).toBe(hash5); + expect(hash5).toBe(hash6); + }); + }; + + testRefresh('when a slice is inserted', (kit, refresh) => { + kit.peritext.editor.cursor.setAt(1, 4); + refresh(); + kit.peritext.editor.insStackSlice('bold'); + }); + + testRefresh('when a collapsed slice is inserted', (kit, refresh) => { + kit.peritext.editor.cursor.setAt(5); + refresh(); + kit.peritext.editor.insStackSlice(''); + }); + + testRefresh('when a marker is inserted', (kit, refresh) => { + kit.peritext.editor.cursor.setAt(0); + refresh(); + kit.peritext.editor.insMarker(''); + }); + + testRefresh('when a marker is inserted at the same position', (kit, refresh) => { + kit.peritext.editor.cursor.setAt(0); + kit.peritext.editor.insMarker(''); + refresh(); + kit.peritext.editor.insMarker(''); + }); + + testRefresh('when slice is deleted', (kit, refresh) => { + kit.peritext.editor.cursor.setAt(0, 1); + const slice = kit.peritext.editor.insStackSlice(''); + refresh(); + slice.del(); + }); + + testRefresh('when slice type is changed', (kit, refresh) => { + kit.peritext.editor.cursor.setAt(0, 1); + const slice = kit.peritext.editor.insStackSlice(''); + refresh(); + slice.update({type: ''}); + }); + + testRefresh('when slice behavior is changed', (kit, refresh) => { + kit.peritext.editor.cursor.setAt(0, 1); + const slice = kit.peritext.editor.insStackSlice(123); + refresh(); + slice.update({behavior: SliceBehavior.Erase}); + }); + + testRefresh('when slice data is overwritten', (kit, refresh) => { + kit.peritext.editor.cursor.setAt(0, 1); + const slice = kit.peritext.editor.insStackSlice(123, 'a'); + refresh(); + slice.update({data: 'b'}); + }); + + testRefresh('when slice data is updated inline', (kit, refresh) => { + kit.peritext.editor.cursor.setAt(0, 1); + const slice = kit.peritext.editor.insStackSlice(123, {foo: 'bar'}); + refresh(); + const api = slice.dataNode()! as ObjApi; + api.set({foo: 'baz'}); + }); + + testRefresh('when slice start point anchor is changed', (kit, refresh) => { + kit.peritext.editor.cursor.setAt(0, 1); + const slice = kit.peritext.editor.insStackSlice(123, 456); + expect(slice.start.anchor).toBe(Anchor.Before); + refresh(); + const range = slice.range(); + range.start.anchor = Anchor.After; + slice.update({range}); + }); + + testRefresh('when slice end point anchor is changed', (kit, refresh) => { + kit.peritext.editor.cursor.setAt(3, 3); + const slice = kit.peritext.editor.insStackSlice(0, 0); + expect(slice.end.anchor).toBe(Anchor.After); + refresh(); + const range = slice.range(); + range.end.anchor = Anchor.Before; + slice.update({range}); + }); + + testRefresh('when slice range changes', (kit, refresh) => { + kit.peritext.editor.cursor.setAt(3, 3); + kit.peritext.editor.insStackSlice(0, 0); + 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'); + expect(slice.end.anchor).toBe(Anchor.After); + refresh(); + const range2 = kit.peritext.rangeAt(2, 2); + slice.update({range: range2}); + }); + }); + }); + + describe('cursor', () => { + test.skip('updates state hash, when cursor char ID changes', () => { + const {peritext} = setup(); + const overlay = peritext.overlay; + peritext.editor.cursor.setAt(1); + const hash1 = overlay.refresh(); + peritext.editor.cursor.setAt(2); + const hash2 = overlay.refresh(); + const hash3 = overlay.refresh(); + expect(hash1).not.toBe(hash2); + expect(hash2).toBe(hash3); + }); + }); +}); From bdf99be619afaaa8bf308cb817df6d3530b73101 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 30 Apr 2024 15:05:52 +0200 Subject: [PATCH 09/19] =?UTF-8?q?refactor(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=A1=20improve=20method=20naming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/Peritext.overlay.spec.ts | 6 +-- .../peritext/editor/Editor.ts | 15 +++--- .../__tests__/Overlay.getOrNextLower.spec.ts | 8 +-- .../overlay/__tests__/Overlay.spec.ts | 50 +++++++++---------- .../peritext/rga/Range.ts | 2 +- .../peritext/rga/__tests__/Range.spec.ts | 6 +-- .../peritext/slice/__tests__/Slices.spec.ts | 14 +++--- 7 files changed, 52 insertions(+), 49 deletions(-) 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 35ebb9caf6..917aafb421 100644 --- a/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts @@ -35,15 +35,15 @@ test('can insert slices', () => { const {editor} = peritext; expect(size(peritext.overlay.root)).toBe(0); editor.cursor.setAt(2, 2); - editor.insertSlice('bold'); + editor.insStackSlice('bold'); peritext.refresh(); expect(size(peritext.overlay.root)).toBe(2); editor.cursor.setAt(6, 5); - editor.insertSlice('italic'); + editor.insStackSlice('italic'); peritext.refresh(); expect(size(peritext.overlay.root)).toBe(4); editor.cursor.setAt(0, 5); - editor.insertSlice('underline'); + editor.insStackSlice('underline'); peritext.refresh(); expect(size(peritext.overlay.root)).toBe(6); }); diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts index 5f5f654d99..eec6dec283 100644 --- a/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -123,16 +123,19 @@ export class Editor implements Printable { if (range) this.cursor.setRange(range); } - public insertSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { - return this.txt.slices.ins(this.cursor, SliceBehavior.Stack, type, data); + public insStackSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { + const range = this.cursor.range(); + return this.txt.slices.ins(range, SliceBehavior.Stack, type, data); } - public insertOverwriteSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { - return this.txt.slices.ins(this.cursor, SliceBehavior.Overwrite, 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); } - public insertEraseSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { - return this.txt.slices.ins(this.cursor, SliceBehavior.Erase, 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); } public insMarker(type: SliceType, data?: unknown): MarkerSlice { 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 index 3a797d7e15..bb117c1ae1 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLower.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLower.spec.ts @@ -13,7 +13,7 @@ describe('.getOrNextLower()', () => { }); const peritext = new Peritext(model, api.str(['text']).node, api.arr(['slices']).node); peritext.editor.cursor.setAt(1, 1); - peritext.editor.insertSlice(2); + peritext.editor.insStackSlice(2); peritext.refresh(); const str = peritext.str; const id1 = str.find(1)!; @@ -21,7 +21,7 @@ describe('.getOrNextLower()', () => { const p1 = peritext.point(id1, Anchor.After); const p2 = peritext.point(id2, Anchor.After); peritext.editor.cursor.set(p1, p2); - peritext.editor.insertSlice(3); + peritext.editor.insStackSlice(3); peritext.refresh(); const cnt = size(peritext.overlay.root); expect(cnt).toBe(3); @@ -41,10 +41,10 @@ describe('.getOrNextLower()', () => { const p1 = peritext.point(id1, Anchor.After); const p2 = peritext.point(id2, Anchor.After); peritext.editor.cursor.set(p1, p2); - peritext.editor.insertSlice(3); + peritext.editor.insStackSlice(3); peritext.refresh(); peritext.editor.cursor.setAt(2, 1); - peritext.editor.insertSlice(33); + 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.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts index e96ba81bc1..fe98e523e2 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts @@ -19,7 +19,7 @@ const setup = () => { return {model, peritext}; }; -const splitCount = (peritext: Peritext): number => { +const markerCount = (peritext: Peritext): number => { const overlay = peritext.overlay; const iterator = overlay.splitIterator(); let count = 0; @@ -33,16 +33,16 @@ describe('markers', () => { describe('inserts', () => { test('overlays starts with no markers', () => { const {peritext} = setup(); - expect(splitCount(peritext)).toBe(0); + expect(markerCount(peritext)).toBe(0); }); test('can insert one marker in the middle of text', () => { const {peritext} = setup(); peritext.editor.cursor.setAt(6); peritext.editor.insMarker(['p'], '¶'); - expect(splitCount(peritext)).toBe(0); + expect(markerCount(peritext)).toBe(0); peritext.overlay.refresh(); - expect(splitCount(peritext)).toBe(1); + expect(markerCount(peritext)).toBe(1); const points = []; let point; for (const iterator = peritext.overlay.iterator(); (point = iterator()); ) points.push(point); @@ -56,18 +56,18 @@ describe('markers', () => { const {peritext} = setup(); peritext.editor.cursor.setAt(3); peritext.editor.insMarker(['p'], '¶'); - expect(splitCount(peritext)).toBe(0); + expect(markerCount(peritext)).toBe(0); peritext.overlay.refresh(); - expect(splitCount(peritext)).toBe(1); + expect(markerCount(peritext)).toBe(1); peritext.overlay.refresh(); - expect(splitCount(peritext)).toBe(1); + expect(markerCount(peritext)).toBe(1); peritext.editor.cursor.setAt(9); peritext.editor.insMarker(['li'], '- '); - expect(splitCount(peritext)).toBe(1); + expect(markerCount(peritext)).toBe(1); peritext.overlay.refresh(); - expect(splitCount(peritext)).toBe(2); + expect(markerCount(peritext)).toBe(2); peritext.overlay.refresh(); - expect(splitCount(peritext)).toBe(2); + expect(markerCount(peritext)).toBe(2); }); }); @@ -77,14 +77,14 @@ describe('markers', () => { peritext.editor.cursor.setAt(6); const slice = peritext.editor.insMarker(['p'], '¶'); peritext.refresh(); - expect(splitCount(peritext)).toBe(1); + expect(markerCount(peritext)).toBe(1); const points = []; let point; for (const iterator = peritext.overlay.iterator(); (point = iterator()); ) points.push(point); point = points[0]; peritext.delMarker(slice); peritext.refresh(); - expect(splitCount(peritext)).toBe(0); + expect(markerCount(peritext)).toBe(0); }); test('can delete one of two splits', () => { @@ -94,14 +94,14 @@ describe('markers', () => { peritext.editor.cursor.setAt(11); const slice = peritext.editor.insMarker(['p'], '¶'); peritext.refresh(); - expect(splitCount(peritext)).toBe(2); + expect(markerCount(peritext)).toBe(2); const points = []; let point; for (const iterator = peritext.overlay.iterator(); (point = iterator()); ) points.push(point); point = points[0]; peritext.delMarker(slice); peritext.refresh(); - expect(splitCount(peritext)).toBe(1); + expect(markerCount(peritext)).toBe(1); }); }); @@ -109,13 +109,13 @@ describe('markers', () => { test('can iterate over markers', () => { const {peritext} = setup(); peritext.editor.cursor.setAt(1, 6); - peritext.editor.insertSlice('a', {a: 'b'}); + peritext.editor.insStackSlice('a', {a: 'b'}); peritext.editor.cursor.setAt(2); peritext.editor.insMarker(['p'], '¶'); peritext.editor.cursor.setAt(11); peritext.editor.insMarker(['p'], '¶'); peritext.refresh(); - expect(splitCount(peritext)).toBe(2); + expect(markerCount(peritext)).toBe(2); const points = []; let point; for (const iterator = peritext.overlay.splitIterator(); (point = iterator()); ) points.push(point); @@ -136,7 +136,7 @@ describe('slices', () => { test('can insert one slice in the middle of text', () => { const {peritext} = setup(); peritext.editor.cursor.setAt(6, 2); - peritext.editor.insertSlice('em', {emphasis: true}); + peritext.editor.insStackSlice('em', {emphasis: true}); expect(peritext.overlay.slices.size).toBe(0); peritext.overlay.refresh(); expect(peritext.overlay.slices.size).toBe(2); @@ -153,9 +153,9 @@ describe('slices', () => { test('can insert two slices', () => { const {peritext} = setup(); peritext.editor.cursor.setAt(2, 8); - peritext.editor.insertSlice('em', {emphasis: true}); + peritext.editor.insStackSlice('em', {emphasis: true}); peritext.editor.cursor.setAt(4, 8); - peritext.editor.insertSlice('strong', {bold: true}); + peritext.editor.insStackSlice('strong', {bold: true}); expect(peritext.overlay.slices.size).toBe(0); peritext.overlay.refresh(); expect(peritext.overlay.slices.size).toBe(3); @@ -168,9 +168,9 @@ describe('slices', () => { test('intersecting slice chunks point to two slices', () => { const {peritext} = setup(); peritext.editor.cursor.setAt(2, 2); - peritext.editor.insertSlice('em', {emphasis: true}); + peritext.editor.insStackSlice('em', {emphasis: true}); peritext.editor.cursor.setAt(3, 2); - peritext.editor.insertSlice('strong', {bold: true}); + peritext.editor.insStackSlice('strong', {bold: true}); peritext.refresh(); const point1 = first(peritext.overlay.root)!; expect(point1.layers.length).toBe(1); @@ -191,7 +191,7 @@ describe('slices', () => { test('one char slice should correctly sort overlay points', () => { const {peritext} = setup(); peritext.editor.cursor.setAt(0, 1); - peritext.editor.insertSlice('em', {emphasis: true}); + peritext.editor.insStackSlice('em', {emphasis: true}); peritext.refresh(); const point1 = peritext.overlay.first()!; const point2 = next(point1)!; @@ -209,11 +209,11 @@ describe('slices', () => { const point = peritext.overlay.find((point) => point instanceof MarkerOverlayPoint)!; expect(point.layers.length).toBe(0); peritext.editor.cursor.setAt(2, 2); - peritext.editor.insertSlice(''); + peritext.editor.insStackSlice(''); peritext.refresh(); expect(point.layers.length).toBe(0); peritext.editor.cursor.setAt(2, 1); - peritext.editor.insertSlice(''); + peritext.editor.insStackSlice(''); peritext.refresh(); expect(point.layers.length).toBe(0); }); @@ -223,7 +223,7 @@ describe('slices', () => { test('can remove a slice', () => { const {peritext} = setup(); peritext.editor.cursor.setAt(6, 2); - const slice = peritext.editor.insertSlice('em', {emphasis: true}); + const slice = peritext.editor.insStackSlice('em', {emphasis: true}); expect(peritext.overlay.slices.size).toBe(0); peritext.overlay.refresh(); expect(peritext.overlay.slices.size).toBe(2); diff --git a/src/json-crdt-extensions/peritext/rga/Range.ts b/src/json-crdt-extensions/peritext/rga/Range.ts index 3759215c35..a480f479f8 100644 --- a/src/json-crdt-extensions/peritext/rga/Range.ts +++ b/src/json-crdt-extensions/peritext/rga/Range.ts @@ -90,7 +90,7 @@ export class Range implements Pick, Printable { * * @returns A new range with the same start and end points. */ - public clone(): Range { + public range(): Range { return new Range(this.rga, this.start.clone(), this.end.clone()); } diff --git a/src/json-crdt-extensions/peritext/rga/__tests__/Range.spec.ts b/src/json-crdt-extensions/peritext/rga/__tests__/Range.spec.ts index 29bf962b4d..0cddf96b07 100644 --- a/src/json-crdt-extensions/peritext/rga/__tests__/Range.spec.ts +++ b/src/json-crdt-extensions/peritext/rga/__tests__/Range.spec.ts @@ -186,7 +186,7 @@ describe('.clone()', () => { test('can clone a range', () => { const {peritext} = setup(); const range1 = peritext.rangeAt(2, 3); - const range2 = range1.clone(); + const range2 = range1.range(); expect(range2).not.toBe(range1); expect(range1.text()).toBe(range2.text()); expect(range2.start).not.toBe(range1.start); @@ -322,7 +322,7 @@ describe('.contains()', () => { test('returns true if slice is contained', () => { const {peritext} = setup(); peritext.editor.setCursor(3, 2); - const slice = peritext.editor.insertOverwriteSlice('b'); + const slice = peritext.editor.insOverwriteSlice('b'); peritext.editor.setCursor(0); peritext.refresh(); expect(peritext.rangeAt(2, 4).contains(slice)).toBe(true); @@ -334,7 +334,7 @@ describe('.contains()', () => { test('returns false if slice is not contained', () => { const {peritext} = setup(); peritext.editor.setCursor(3, 2); - const slice = peritext.editor.insertOverwriteSlice('b'); + const slice = peritext.editor.insOverwriteSlice('b'); peritext.editor.setCursor(0); peritext.refresh(); expect(peritext.rangeAt(3, 1).contains(slice)).toBe(false); diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts index bd062a8692..9c035156ac 100644 --- a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts +++ b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts @@ -30,9 +30,9 @@ describe('.ins()', () => { const {peritext} = setup(); const {editor} = peritext; editor.cursor.setAt(6, 5); - const slice1 = editor.insertSlice('strong', {bold: true}); + const slice1 = editor.insStackSlice('strong', {bold: true}); editor.cursor.setAt(12, 4); - const slice2 = editor.insertSlice('i', {italic: true}); + const slice2 = editor.insStackSlice('i', {italic: true}); peritext.refresh(); expect(peritext.slices.size()).toBe(2); expect(slice1.data()).toStrictEqual({bold: true}); @@ -50,7 +50,7 @@ describe('.ins()', () => { expect(changed2).toBe(false); expect(hash1).toBe(hash2); editor.cursor.setAt(12, 7); - editor.insertSlice('b', {bold: true}); + editor.insStackSlice('b', {bold: true}); const changed3 = peritext.slices.hash !== peritext.slices.refresh(); const hash3 = peritext.slices.hash; const changed4 = peritext.slices.hash !== peritext.slices.refresh(); @@ -60,7 +60,7 @@ describe('.ins()', () => { expect(hash1).not.toStrictEqual(hash3); expect(hash3).toBe(hash4); editor.cursor.setAt(12, 4); - editor.insertSlice('em', {italic: true}); + editor.insStackSlice('em', {italic: true}); const changed5 = peritext.slices.hash !== peritext.slices.refresh(); const hash5 = peritext.slices.hash; const changed6 = peritext.slices.hash !== peritext.slices.refresh(); @@ -124,7 +124,7 @@ describe('.del()', () => { const {peritext} = setup(); const {editor} = peritext; editor.cursor.setAt(6, 5); - const slice1 = editor.insertSlice('b', {bold: true}); + const slice1 = editor.insStackSlice('b', {bold: true}); peritext.refresh(); const hash1 = peritext.slices.hash; expect(peritext.slices.size()).toBe(1); @@ -141,7 +141,7 @@ describe('.delSlices()', () => { const {peritext} = setup(); const {editor} = peritext; editor.cursor.setAt(6, 5); - const slice1 = editor.insertSlice('b', {bold: true}); + const slice1 = editor.insStackSlice('b', {bold: true}); peritext.refresh(); const hash1 = peritext.slices.hash; expect(peritext.slices.size()).toBe(1); @@ -214,7 +214,7 @@ describe('.refresh()', () => { const {peritext} = setup(); const {editor} = peritext; editor.cursor.setAt(6, 5); - const slice1 = editor.insertSlice('b', {bold: true}); + const slice1 = editor.insStackSlice('b', {bold: true}); peritext.refresh(); const hash1 = peritext.slices.hash; peritext.model.api.obj(['slices', 0, 4]).set({bold: false}); From bcd0cbf49103ca26d5c7f9547ee6fd4447c428f6 Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 1 May 2024 10:37:11 +0200 Subject: [PATCH 10/19] =?UTF-8?q?chore(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=A4=96=20start=20refresh=20re-implementation=20rething?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/Overlay.ts | 91 ++++++++++--------- .../overlay/__tests__/Overlay.refresh.spec.ts | 62 +++++++------ .../peritext/slice/Slices.ts | 4 +- 3 files changed, 85 insertions(+), 72 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 954650e035..0edcfe7b6b 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -93,58 +93,26 @@ export class Overlay implements Printable, Stateful { public refresh(slicesOnly: boolean = false): number { let hash: number = CONST.START_STATE; hash = this.refreshSlices(hash); + // hash = this.refreshCursor(hash); + // TODO: refresh ephemeral slices // 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(); + private slices = new Map(); private refreshSlices(state: number): number { const slices = this.txt.slices; - const changed = slices.refresh(); + 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 + ''); let tuple: [start: OverlayPoint, end: OverlayPoint] | undefined = sliceSet.get(slice); if (tuple) { - if (slice.isDel()) { + if ((slice as any).isDel && (slice as any).isDel()) { this.delSlice(slice, tuple); return; } @@ -165,8 +133,12 @@ export class Overlay implements Printable, Stateful { }); } } + return state; + } + + private refreshCursor(state: number): number { const cursor = this.txt.editor.cursor; - let tuple: [start: OverlayPoint, end: OverlayPoint] | undefined = sliceSet.get(cursor); + let tuple: [start: OverlayPoint, end: OverlayPoint] | undefined = this.slices.get(cursor); const positionMoved = tuple && (tuple[0].cmp(cursor.start) !== 0 || tuple[1].cmp(cursor.end) !== 0); if (tuple && positionMoved) { this.delSlice(cursor, tuple!); @@ -178,8 +150,43 @@ export class Overlay implements Printable, Stateful { return state; } + /** + * 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); + } + 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) { @@ -258,6 +265,6 @@ export class Overlay implements Printable, Stateful { ]) ); }; - return this.constructor.name + printTree(tab, [!this.root ? null : (tab) => printPoint(tab, this.root!)]); + return `${this.constructor.name} #${this.hash.toString(36)}` + printTree(tab, [!this.root ? null : (tab) => printPoint(tab, this.root!)]); } } 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 a4c484d3a1..ad747d5ce1 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 @@ -4,7 +4,8 @@ import {Anchor} from '../../rga/constants'; import {SliceBehavior} from '../../slice/constants'; const setup = () => { - const model = Model.withLogicalClock(); + const sid = 123456789; + const model = Model.withLogicalClock(sid); model.api.root({ text: '', slices: [], @@ -21,29 +22,29 @@ const setup = () => { type Kit = ReturnType; describe('Overlay.refresh()', () => { + const testRefresh = (name: string, update: (kit: Kit, refresh: () => void) => void) => { + test(name, () => { + const kit = setup(); + const overlay = kit.peritext.overlay; + let hash1: number | undefined, hash2: number | undefined, hash3: number | undefined; + update(kit, () => { + hash1 = overlay.refresh(); + hash2 = overlay.refresh(); + hash3 = overlay.refresh(); + }); + const hash4 = overlay.refresh(); + const hash5 = overlay.refresh(); + const hash6 = overlay.refresh(); + expect(hash1).toBe(hash2); + expect(hash2).toBe(hash3); + expect(hash3).not.toBe(hash4); + expect(hash4).toBe(hash5); + expect(hash5).toBe(hash6); + }); + }; + describe('slices', () => { describe('updates hash', () => { - const testRefresh = (name: string, update: (kit: Kit, refresh: () => void) => void) => { - test(name, () => { - const kit = setup(); - const overlay = kit.peritext.overlay; - let hash1: number | undefined, hash2: number | undefined, hash3: number | undefined; - update(kit, () => { - hash1 = overlay.refresh(); - hash2 = overlay.refresh(); - hash3 = overlay.refresh(); - }); - const hash4 = overlay.refresh(); - const hash5 = overlay.refresh(); - const hash6 = overlay.refresh(); - expect(hash1).toBe(hash2); - expect(hash2).toBe(hash3); - expect(hash3).not.toBe(hash4); - expect(hash4).toBe(hash5); - expect(hash5).toBe(hash6); - }); - }; - testRefresh('when a slice is inserted', (kit, refresh) => { kit.peritext.editor.cursor.setAt(1, 4); refresh(); @@ -141,16 +142,21 @@ describe('Overlay.refresh()', () => { }); describe('cursor', () => { - test.skip('updates state hash, when cursor char ID changes', () => { + test.only('updates state hash, when cursor char ID changes', () => { const {peritext} = setup(); const overlay = peritext.overlay; peritext.editor.cursor.setAt(1); - const hash1 = overlay.refresh(); + overlay.refresh(); + console.log(peritext + ''); peritext.editor.cursor.setAt(2); - const hash2 = overlay.refresh(); - const hash3 = overlay.refresh(); - expect(hash1).not.toBe(hash2); - expect(hash2).toBe(hash3); + overlay.refresh(); + console.log(peritext + ''); + // const hash1 = overlay.refresh(); + // peritext.editor.cursor.setAt(2); + // const hash2 = overlay.refresh(); + // const hash3 = overlay.refresh(); + // expect(hash1).not.toBe(hash2); + // expect(hash2).toBe(hash3); }); }); }); diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index 0c4f3f0a7f..e7aa074273 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -87,7 +87,6 @@ export class Slices implements Stateful, Printable { const tuple = model.index.get(tupleId); if (!(tuple instanceof VecNode)) throw new Error('NOT_TUPLE'); let slice = PersistedSlice.deserialize(txt, rga, chunk, tuple); - // TODO: Simplify, remove `SplitSlice` class. if (slice.isSplit()) slice = new MarkerSlice(txt, rga, chunk, tuple, slice.behavior, slice.type, slice.start, slice.end); return slice; @@ -123,8 +122,9 @@ export class Slices implements Stateful, Printable { return this.list._size; } - public forEach(callback: (item: PersistedSlice) => void): void { + public forEach(callback: (item: Slice) => void): void { this.list.forEach((node) => callback(node.v)); + callback(this.txt.editor.cursor); } // ----------------------------------------------------------------- Stateful From e7a21c8889289af367fc35bb6c77a2d9116853dc Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 1 May 2024 12:04:28 +0200 Subject: [PATCH 11/19] =?UTF-8?q?refactor(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=A1=20decouple=20slices=20from=20Peritext=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 3 +- .../peritext/slice/PersistedSlice.ts | 18 +++++------ .../peritext/slice/Slices.ts | 31 ++++++++++--------- .../slice/__tests__/PersistedSlice.spec.ts | 2 +- .../peritext/slice/types.ts | 2 -- 5 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index 5c7cee3865..4167337cda 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -12,7 +12,6 @@ import {CONST, updateNum} from '../../json-hash'; 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 {StringChunk} from './util/types'; import type {SliceType} from './types'; import type {MarkerSlice} from './slice/MarkerSlice'; @@ -30,7 +29,7 @@ export class Peritext implements Printable { public readonly str: StrNode, slices: ArrNode, ) { - this.slices = new Slices(this, slices); + this.slices = new Slices(this.model, slices, this.str); this.editor = new Editor(this); } diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index f4c05c76c1..0aa3c17392 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -13,13 +13,13 @@ import {s} from '../../../json-crdt-patch'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {ArrChunk} from '../../../json-crdt/nodes'; import type {MutableSlice, SliceUpdateParams} from './types'; -import type {Peritext} from '../Peritext'; import type {SliceDto, SliceType, Stateful} from '../types'; import type {Printable} from 'tree-dump/lib/types'; import type {AbstractRga} from '../../../json-crdt/nodes/rga'; +import type {Model} from '../../../json-crdt/model'; export class PersistedSlice extends Range implements MutableSlice, Stateful, Printable { - public static deserialize(txt: Peritext, rga: AbstractRga, chunk: ArrChunk, tuple: VecNode): PersistedSlice { + public static deserialize(model: Model, rga: AbstractRga, chunk: ArrChunk, tuple: VecNode): PersistedSlice { const header = +(tuple.get(0)!.view() as SliceDto[0]); const id1 = tuple.get(1)!.view() as ITimestampStruct; const id2 = (tuple.get(2)!.view() || id1) as ITimestampStruct; @@ -33,13 +33,13 @@ export class PersistedSlice extends Range implements MutableSlice const behavior: SliceBehavior = (header & SliceHeaderMask.Behavior) >>> SliceHeaderShift.Behavior; const p1 = new Point(rga, id1, anchor1); const p2 = new Point(rga, id2, anchor2); - const slice = new PersistedSlice(txt, rga, chunk, tuple, behavior, type, p1, p2); + const slice = new PersistedSlice(model, rga, chunk, tuple, behavior, type, p1, p2); return slice; } constructor( /** The Peritext context. */ - protected readonly txt: Peritext, + protected readonly model: Model, /** The text RGA. */ protected readonly rga: AbstractRga, /** The `arr` chunk of `arr` where the slice is stored. */ @@ -62,7 +62,7 @@ export class PersistedSlice extends Range implements MutableSlice } protected tupleApi() { - return this.txt.model.api.wrap(this.tuple); + return this.model.api.wrap(this.tuple); } // ------------------------------------------------------------- MutableSlice @@ -108,11 +108,7 @@ export class PersistedSlice extends Range implements MutableSlice public dataNode() { const node = this.tuple.get(SliceTupleIndex.Data); - return node && this.txt.model.api.wrap(node); - } - - public del(): void { - this.txt.slices.del(this.id); + return node && this.model.api.wrap(node); } public isDel(): boolean { @@ -130,7 +126,7 @@ export class PersistedSlice extends Range implements MutableSlice this.hash = state; if (changed) { const tuple = this.tuple; - const slice = PersistedSlice.deserialize(this.txt, this.rga, this.chunk, tuple); + const slice = PersistedSlice.deserialize(this.model, this.rga, this.chunk, tuple); this.behavior = slice.behavior; this.type = slice.type; this.start = slice.start; diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index fcd1607723..88995a2205 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -11,21 +11,25 @@ import {VecNode} from '../../../json-crdt/nodes'; import type {Slice} from './types'; import type {ITimespanStruct, ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {SliceType, Stateful} from '../types'; -import type {Peritext} from '../Peritext'; import type {Printable} from 'tree-dump/lib/types'; import type {ArrChunk, ArrNode} from '../../../json-crdt/nodes'; +import type {Model} from '../../../json-crdt/model'; +import type {AbstractRga} from '../../../json-crdt/nodes/rga'; export class Slices implements Stateful, Printable { private list = new AvlMap(compare); constructor( - public readonly txt: Peritext, + /** The model, which powers the CRDT nodes. */ + public readonly model: Model, + /** The `arr` node, used as a set, where slices are stored. */ public readonly set: ArrNode, + /** The text RGA. */ + protected readonly rga: AbstractRga, ) {} public ins(range: Range, behavior: SliceBehavior, type: SliceType, data?: unknown): PersistedSlice { - const peritext = this.txt; - const model = peritext.model; + const model = this.model; const set = this.set; const api = model.api; const builder = api.builder; @@ -53,11 +57,10 @@ export class Slices implements Stateful, Printable { const tuple = model.index.get(tupleId) as VecNode; const chunk = set.findById(chunkId)!; // TODO: Need to check if split slice text was deleted - const txt = this.txt; const slice = behavior === SliceBehavior.Marker - ? new MarkerSlice(txt, txt.str, chunk, tuple, behavior, type, start, end) - : new PersistedSlice(txt, txt.str, chunk, tuple, behavior, type, start, end); + ? new MarkerSlice(model, this.rga, chunk, tuple, behavior, type, start, end) + : new PersistedSlice(model, this.rga, chunk, tuple, behavior, type, start, end); this.list.set(chunk.id, slice); return slice; } @@ -79,16 +82,15 @@ export class Slices implements Stateful, Printable { } protected unpack(chunk: ArrChunk): PersistedSlice { - const txt = this.txt; - const rga = txt.str; - const model = txt.model; + const rga = this.rga; + const model = this.model; const tupleId = chunk.data ? chunk.data[0] : undefined; if (!tupleId) throw new Error('SLICE_NOT_FOUND'); const tuple = model.index.get(tupleId); if (!(tuple instanceof VecNode)) throw new Error('NOT_TUPLE'); - let slice = PersistedSlice.deserialize(txt, rga, chunk, tuple); + let slice = PersistedSlice.deserialize(model, rga, chunk, tuple); if (slice.isSplit()) - slice = new MarkerSlice(txt, rga, chunk, tuple, slice.behavior, slice.type, slice.start, slice.end); + slice = new MarkerSlice(model, rga, chunk, tuple, slice.behavior, slice.type, slice.start, slice.end); return slice; } @@ -98,13 +100,13 @@ export class Slices implements Stateful, Printable { public del(id: ITimestampStruct): void { this.list.del(id); - const api = this.txt.model.api; + const api = this.model.api; api.builder.del(this.set.id, [tss(id.sid, id.time, 1)]); api.apply(); } public delSlices(slices: Slice[]): void { - const api = this.txt.model.api; + const api = this.model.api; const spans: ITimespanStruct[] = []; const length = slices.length; for (let i = 0; i < length; i++) { @@ -124,7 +126,6 @@ export class Slices implements Stateful, Printable { public forEach(callback: (item: Slice) => void): void { this.list.forEach((node) => callback(node.v)); - callback(this.txt.editor.cursor); } // ----------------------------------------------------------------- Stateful diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts b/src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts index 42bdb2bc2e..dc3a9215aa 100644 --- a/src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts +++ b/src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts @@ -65,7 +65,7 @@ describe('.del() and .isDel()', () => { expect(peritext.model.view().slices.length).toBe(1); expect(slice2.isDel()).toBe(false); expect(slice2).toBe(slice); - slice.del(); + peritext.slices.del(slice.id); expect(peritext.model.view().slices.length).toBe(0); expect(slice.isDel()).toBe(true); expect(slice2.isDel()).toBe(true); diff --git a/src/json-crdt-extensions/peritext/slice/types.ts b/src/json-crdt-extensions/peritext/slice/types.ts index 6afdd511de..5d32d8a0c3 100644 --- a/src/json-crdt-extensions/peritext/slice/types.ts +++ b/src/json-crdt-extensions/peritext/slice/types.ts @@ -33,8 +33,6 @@ export interface Slice extends Range, Stateful { export interface MutableSlice extends Slice { update(params: SliceUpdateParams): void; - del(): void; - /** * Whether the slice is deleted. */ From 7971f217c7c27e70a76e95220144ef6892a1371c Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 1 May 2024 12:52:06 +0200 Subject: [PATCH 12/19] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20add=20more=20slice=20layers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 46 +++++++++-- .../peritext/editor/Editor.ts | 6 +- .../peritext/overlay/Overlay.ts | 6 +- .../overlay/__tests__/Overlay.refresh.spec.ts | 4 +- .../overlay/__tests__/Overlay.spec.ts | 2 +- .../overlay/__tests__/OverlayPoint.spec.ts | 74 +++++++++--------- .../peritext/slice/Slices.ts | 2 +- .../slice/__tests__/PersistedSlice.spec.ts | 8 +- .../peritext/slice/__tests__/Slices.spec.ts | 78 +++++++++---------- .../peritext/slice/__tests__/setup.ts | 2 +- 10 files changed, 131 insertions(+), 97 deletions(-) diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index 4167337cda..97f8e7b38c 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -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'; @@ -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); @@ -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); } @@ -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() */ @@ -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(); } @@ -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), ]) diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts index 222a0f223f..e5c450b692 100644 --- a/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -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 { diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 747d5f84cc..67ee11f5ef 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -99,17 +99,17 @@ export class Overlay implements Printable, Stateful { return (this.hash = hash); } - private slices = new Map(); + public readonly slices = new Map(); 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()) { 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 ad747d5ce1..024e7b1bd6 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 @@ -74,7 +74,7 @@ describe('Overlay.refresh()', () => { kit.peritext.editor.cursor.setAt(0, 1); const slice = kit.peritext.editor.insStackSlice(''); refresh(); - slice.del(); + kit.peritext.savedSlices.del(slice.id); }); testRefresh('when slice type is changed', (kit, refresh) => { @@ -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); 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 fe98e523e2..8cbae5c6b1 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts @@ -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); diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts index 75a56352ca..fbeec727e6 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts @@ -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), ''); + const slice = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 5), ''); const point = getPoint(slice.start); expect(point.layers.length).toBe(0); point.addLayer(slice); @@ -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), ''); + const slice = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 5), ''); const point = getPoint(slice.start); expect(point.layers.length).toBe(0); point.addLayer(slice); @@ -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), ''); - const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), ''); + const slice1 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 5), ''); + const slice2 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 3), ''); const point = getPoint(slice1.start); expect(point.layers.length).toBe(0); point.addLayer(slice1); @@ -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), ''); - const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), ''); + const slice1 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 5), ''); + const slice2 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 3), ''); const point = getPoint(slice1.start); point.addLayer(slice2); point.addLayer(slice1); @@ -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), ''); - const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), ''); - const slice3 = peritext.slices.insOverwrite(peritext.rangeAt(2, 10), ''); + const slice1 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 5), ''); + const slice2 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 3), ''); + const slice3 = peritext.savedSlices.insOverwrite(peritext.rangeAt(2, 10), ''); const point = getPoint(slice1.start); point.addLayer(slice3); point.addLayer(slice3); @@ -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), ''); - const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), ''); - const slice3 = peritext.slices.insOverwrite(peritext.rangeAt(2, 10), ''); + const slice1 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 5), ''); + const slice2 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 3), ''); + const slice3 = peritext.savedSlices.insOverwrite(peritext.rangeAt(2, 10), ''); const point = getPoint(slice1.start); point.addLayer(slice1); point.addLayer(slice2); @@ -98,9 +98,9 @@ describe('layers', () => { test('can remove layers', () => { const {peritext, getPoint} = setupOverlayPoint(); - const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), ''); - const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), ''); - const slice3 = peritext.slices.insOverwrite(peritext.rangeAt(2, 10), ''); + const slice1 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 5), ''); + const slice2 = peritext.savedSlices.insOverwrite(peritext.rangeAt(5, 3), ''); + const slice3 = peritext.savedSlices.insOverwrite(peritext.rangeAt(2, 10), ''); const point = getPoint(slice1.start); point.addLayer(slice2); point.addLayer(slice1); @@ -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), '

'); + const marker = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '

'); const point = getPoint(marker.start); expect(point.markers.length).toBe(0); point.addMarker(marker); @@ -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), '

'); + const marker = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '

'); const point = getPoint(marker.start); expect(point.markers.length).toBe(0); point.addMarker(marker); @@ -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), '

'); - const marker2 = peritext.slices.insMarker(peritext.rangeAt(5, 0), '

'); + const marker1 = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '

'); + const marker2 = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '

'); const point = getPoint(marker1.start); expect(point.markers.length).toBe(0); point.addMarker(marker1); @@ -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), '

'); - const marker2 = peritext.slices.insMarker(peritext.rangeAt(5, 0), '

'); + const marker1 = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '

'); + const marker2 = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '

'); const point = getPoint(marker1.start); point.addMarker(marker2); point.addMarker(marker1); @@ -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), '

'); - const marker2 = peritext.slices.insMarker(peritext.rangeAt(5, 0), '

'); - const marker3 = peritext.slices.insMarker(peritext.rangeAt(5, 0), '

'); + const marker1 = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '

'); + const marker2 = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '

'); + const marker3 = peritext.savedSlices.insMarker(peritext.rangeAt(5, 0), '

'); const point = getPoint(marker1.start); point.addMarker(marker3); point.addMarker(marker3); @@ -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), '

'); - const marker2 = peritext.slices.insMarker(peritext.rangeAt(6, 2), '

'); - const marker3 = peritext.slices.insMarker(peritext.rangeAt(6, 3), '

'); + const marker1 = peritext.savedSlices.insMarker(peritext.rangeAt(6, 1), '

'); + const marker2 = peritext.savedSlices.insMarker(peritext.rangeAt(6, 2), '

'); + const marker3 = peritext.savedSlices.insMarker(peritext.rangeAt(6, 3), '

'); const point = getPoint(marker2.start); point.addMarker(marker1); point.addMarker(marker2); @@ -211,9 +211,9 @@ describe('markers', () => { test('can remove markers', () => { const {peritext, getPoint} = setupOverlayPoint(); - const marker1 = peritext.slices.insMarker(peritext.rangeAt(6, 1), '

'); - const marker2 = peritext.slices.insMarker(peritext.rangeAt(6, 1), '

'); - const marker3 = peritext.slices.insMarker(peritext.rangeAt(6, 2), '

'); + const marker1 = peritext.savedSlices.insMarker(peritext.rangeAt(6, 1), '

'); + const marker2 = peritext.savedSlices.insMarker(peritext.rangeAt(6, 1), '

'); + const marker3 = peritext.savedSlices.insMarker(peritext.rangeAt(6, 2), '

'); const point = getPoint(marker1.start); point.addMarker(marker2); point.addMarker(marker1); @@ -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), '

'); + const marker = peritext.savedSlices.insMarker(peritext.rangeAt(10, 1), '

'); const point = getPoint(marker.start); expect(point.markers.length).toBe(0); expect(point.refs.length).toBe(0); @@ -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); @@ -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); @@ -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), '

'); - const slice = peritext.slices.insErase(peritext.rangeAt(10, 4), 123); + const marker = peritext.savedSlices.insMarker(peritext.rangeAt(10, 1), '

'); + 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); @@ -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), '

'); - const slice = peritext.slices.insErase(peritext.rangeAt(10, 4), 123); + const marker = peritext.savedSlices.insMarker(peritext.rangeAt(10, 1), '

'); + const slice = peritext.savedSlices.insErase(peritext.rangeAt(10, 4), 123); const point = getPoint(slice.end); point.addMarkerRef(marker); point.addLayerStartRef(slice); diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index 88995a2205..08a1084166 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -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, /** The `arr` node, used as a set, where slices are stored. */ public readonly set: ArrNode, /** The text RGA. */ diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts b/src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts index dc3a9215aa..bb33b387fd 100644 --- a/src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts +++ b/src/json-crdt-extensions/peritext/slice/__tests__/PersistedSlice.spec.ts @@ -4,7 +4,7 @@ import {setup} from './setup'; const setupSlice = () => { const deps = setup(); const range = deps.peritext.rangeAt(2, 3); - const slice = deps.peritext.slices.insMarker(range, 0); + const slice = deps.peritext.savedSlices.insMarker(range, 0); return {...deps, range, slice}; }; @@ -61,15 +61,15 @@ describe('.del() and .isDel()', () => { const {peritext, slice} = setupSlice(); expect(peritext.model.view().slices.length).toBe(1); expect(slice.isDel()).toBe(false); - const slice2 = peritext.slices.get(slice.id)!; + const slice2 = peritext.savedSlices.get(slice.id)!; expect(peritext.model.view().slices.length).toBe(1); expect(slice2.isDel()).toBe(false); expect(slice2).toBe(slice); - peritext.slices.del(slice.id); + peritext.savedSlices.del(slice.id); expect(peritext.model.view().slices.length).toBe(0); expect(slice.isDel()).toBe(true); expect(slice2.isDel()).toBe(true); - const slice3 = peritext.slices.get(slice.id); + const slice3 = peritext.savedSlices.get(slice.id); expect(slice3).toBe(undefined); }); }); diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts index 9c035156ac..c47794d222 100644 --- a/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts +++ b/src/json-crdt-extensions/peritext/slice/__tests__/Slices.spec.ts @@ -8,9 +8,9 @@ import {setup} from './setup'; test('initially slice list is empty', () => { const {peritext} = setup(); - expect(peritext.slices.size()).toBe(0); + expect(peritext.savedSlices.size()).toBe(0); peritext.refresh(); - expect(peritext.slices.size()).toBe(0); + expect(peritext.savedSlices.size()).toBe(0); }); describe('.ins()', () => { @@ -18,7 +18,7 @@ describe('.ins()', () => { const {peritext, slices} = setup(); const range = peritext.rangeAt(12, 7); const slice = slices.ins(range, SliceBehavior.Stack, 'b', {bold: true}); - expect(peritext.slices.size()).toBe(1); + expect(peritext.savedSlices.size()).toBe(1); expect(slice.start).toStrictEqual(range.start); expect(slice.end).toStrictEqual(range.end); expect(slice.behavior).toBe(SliceBehavior.Stack); @@ -34,7 +34,7 @@ describe('.ins()', () => { editor.cursor.setAt(12, 4); const slice2 = editor.insStackSlice('i', {italic: true}); peritext.refresh(); - expect(peritext.slices.size()).toBe(2); + expect(peritext.savedSlices.size()).toBe(2); expect(slice1.data()).toStrictEqual({bold: true}); expect(slice2.data()).toStrictEqual({italic: true}); }); @@ -42,29 +42,29 @@ describe('.ins()', () => { test('updates hash on slice insert', () => { const {peritext} = setup(); const {editor} = peritext; - const changed1 = peritext.slices.hash !== peritext.slices.refresh(); - const hash1 = peritext.slices.hash; - const changed2 = peritext.slices.hash !== peritext.slices.refresh(); - const hash2 = peritext.slices.hash; + const changed1 = peritext.savedSlices.hash !== peritext.savedSlices.refresh(); + const hash1 = peritext.savedSlices.hash; + const changed2 = peritext.savedSlices.hash !== peritext.savedSlices.refresh(); + const hash2 = peritext.savedSlices.hash; expect(changed1).toBe(true); expect(changed2).toBe(false); expect(hash1).toBe(hash2); editor.cursor.setAt(12, 7); editor.insStackSlice('b', {bold: true}); - const changed3 = peritext.slices.hash !== peritext.slices.refresh(); - const hash3 = peritext.slices.hash; - const changed4 = peritext.slices.hash !== peritext.slices.refresh(); - const hash4 = peritext.slices.hash; + const changed3 = peritext.savedSlices.hash !== peritext.savedSlices.refresh(); + const hash3 = peritext.savedSlices.hash; + const changed4 = peritext.savedSlices.hash !== peritext.savedSlices.refresh(); + const hash4 = peritext.savedSlices.hash; expect(changed3).toBe(true); expect(changed4).toBe(false); expect(hash1).not.toStrictEqual(hash3); expect(hash3).toBe(hash4); editor.cursor.setAt(12, 4); editor.insStackSlice('em', {italic: true}); - const changed5 = peritext.slices.hash !== peritext.slices.refresh(); - const hash5 = peritext.slices.hash; - const changed6 = peritext.slices.hash !== peritext.slices.refresh(); - const hash6 = peritext.slices.hash; + const changed5 = peritext.savedSlices.hash !== peritext.savedSlices.refresh(); + const hash5 = peritext.savedSlices.hash; + const changed6 = peritext.savedSlices.hash !== peritext.savedSlices.refresh(); + const hash6 = peritext.savedSlices.hash; expect(changed5).toBe(true); expect(changed6).toBe(false); expect(hash3).not.toBe(hash5); @@ -86,7 +86,7 @@ describe('.ins()', () => { for (const data of datas) { for (const behavior of behaviors) { const {peritext, model} = setup(); - const slice = peritext.slices.ins(range, behavior, type, data); + const slice = peritext.savedSlices.ins(range, behavior, type, data); expect(slice.start.cmp(range.start)).toBe(0); expect(slice.end.cmp(range.end)).toBe(0); expect(slice.behavior).toBe(behavior); @@ -96,7 +96,7 @@ describe('.ins()', () => { const model2 = Model.fromBinary(buf); const peritext2 = new Peritext(model2, model2.api.str(['text']).node, model2.api.arr(['slices']).node); peritext2.refresh(); - const slice2 = peritext2.slices.get(slice.id)!; + const slice2 = peritext2.savedSlices.get(slice.id)!; expect(slice2.start.cmp(range.start)).toBe(0); expect(slice2.end.cmp(range.end)).toBe(0); expect(slice2.behavior).toBe(behavior); @@ -113,8 +113,8 @@ describe('.get()', () => { test('can retrieve slice by id', () => { const {peritext} = setup(); const range = peritext.rangeAt(6, 5); - const slice = peritext.slices.insOverwrite(range, 'italic'); - const slice2 = peritext.slices.get(slice.id); + const slice = peritext.savedSlices.insOverwrite(range, 'italic'); + const slice2 = peritext.savedSlices.get(slice.id); expect(slice2).toBe(slice); }); }); @@ -126,12 +126,12 @@ describe('.del()', () => { editor.cursor.setAt(6, 5); const slice1 = editor.insStackSlice('b', {bold: true}); peritext.refresh(); - const hash1 = peritext.slices.hash; - expect(peritext.slices.size()).toBe(1); - peritext.slices.del(slice1.id); + const hash1 = peritext.savedSlices.hash; + expect(peritext.savedSlices.size()).toBe(1); + peritext.savedSlices.del(slice1.id); peritext.refresh(); - const hash2 = peritext.slices.hash; - expect(peritext.slices.size()).toBe(0); + const hash2 = peritext.savedSlices.hash; + expect(peritext.savedSlices.size()).toBe(0); expect(hash1).not.toBe(hash2); }); }); @@ -143,12 +143,12 @@ describe('.delSlices()', () => { editor.cursor.setAt(6, 5); const slice1 = editor.insStackSlice('b', {bold: true}); peritext.refresh(); - const hash1 = peritext.slices.hash; - expect(peritext.slices.size()).toBe(1); - peritext.slices.delSlices([slice1]); + const hash1 = peritext.savedSlices.hash; + expect(peritext.savedSlices.size()).toBe(1); + peritext.savedSlices.delSlices([slice1]); peritext.refresh(); - const hash2 = peritext.slices.hash; - expect(peritext.slices.size()).toBe(0); + const hash2 = peritext.savedSlices.hash; + expect(peritext.savedSlices.size()).toBe(0); expect(hash1).not.toBe(hash2); }); }); @@ -158,19 +158,19 @@ describe('.refresh()', () => { test('changes hash on: ' + name, () => { const {peritext, encodeAndDecode} = setup(); const range = peritext.rangeAt(6, 5); - const slice = peritext.slices.insOverwrite(range, 'b', {howBold: 'very'}); - const hash1 = peritext.slices.refresh(); - const hash2 = peritext.slices.refresh(); + const slice = peritext.savedSlices.insOverwrite(range, 'b', {howBold: 'very'}); + const hash1 = peritext.savedSlices.refresh(); + const hash2 = peritext.savedSlices.refresh(); expect(hash1).toBe(hash2); expect(slice.type).toBe('b'); update({range, slice}); - const hash3 = peritext.slices.refresh(); - const hash4 = peritext.slices.refresh(); + const hash3 = peritext.savedSlices.refresh(); + const hash4 = peritext.savedSlices.refresh(); expect(hash3).not.toBe(hash2); expect(hash4).toBe(hash3); const {peritext2} = encodeAndDecode(); peritext2.refresh(); - const slice2 = peritext2.slices.get(slice.id)!; + const slice2 = peritext2.savedSlices.get(slice.id)!; expect(slice2.cmp(slice)).toBe(0); }); }; @@ -216,12 +216,12 @@ describe('.refresh()', () => { editor.cursor.setAt(6, 5); const slice1 = editor.insStackSlice('b', {bold: true}); peritext.refresh(); - const hash1 = peritext.slices.hash; + const hash1 = peritext.savedSlices.hash; peritext.model.api.obj(['slices', 0, 4]).set({bold: false}); peritext.refresh(); - const hash2 = peritext.slices.hash; + const hash2 = peritext.savedSlices.hash; peritext.refresh(); - const hash3 = peritext.slices.hash; + const hash3 = peritext.savedSlices.hash; expect(hash1).not.toBe(hash2); expect(hash2).toBe(hash3); }); diff --git a/src/json-crdt-extensions/peritext/slice/__tests__/setup.ts b/src/json-crdt-extensions/peritext/slice/__tests__/setup.ts index f31bb4c04b..0b7e556cb4 100644 --- a/src/json-crdt-extensions/peritext/slice/__tests__/setup.ts +++ b/src/json-crdt-extensions/peritext/slice/__tests__/setup.ts @@ -13,7 +13,7 @@ export const setup = () => { model.api.str(['text']).del(7, 1); model.api.str(['text']).ins(11, ' this game is awesome'); const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node); - const slices = peritext.slices; + const slices = peritext.savedSlices; const encodeAndDecode = () => { const buf = model.toBinary(); const model2 = Model.fromBinary(buf); From bc3417da85bd6fcefa29a938c006abe96d1d90f8 Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 1 May 2024 13:57:22 +0200 Subject: [PATCH 13/19] =?UTF-8?q?refactor(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=A1=20implement=20Cursor=20as=20a=20local=20slice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 2 +- .../peritext/editor/Cursor.ts | 72 ++++++++++++ .../peritext/editor/Editor.ts | 13 ++- .../peritext/slice/Cursor.ts | 109 ------------------ .../peritext/slice/PersistedSlice.ts | 14 ++- .../peritext/slice/Slices.ts | 13 ++- .../peritext/slice/constants.ts | 11 +- src/json-crdt-patch/constants.ts | 8 ++ 8 files changed, 116 insertions(+), 126 deletions(-) create mode 100644 src/json-crdt-extensions/peritext/editor/Cursor.ts delete mode 100644 src/json-crdt-extensions/peritext/slice/Cursor.ts diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index 97f8e7b38c..2f3bc48361 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -64,7 +64,7 @@ export class Peritext implements Printable { .setSchema(s.vec(s.arr([]))); this.localSlices = new Slices(localModel, localModel.root.node().get(0)!, this.str); - this.editor = new Editor(this); + this.editor = new Editor(this, this.localSlices); } public strApi() { diff --git a/src/json-crdt-extensions/peritext/editor/Cursor.ts b/src/json-crdt-extensions/peritext/editor/Cursor.ts new file mode 100644 index 0000000000..fd9f6fc9dd --- /dev/null +++ b/src/json-crdt-extensions/peritext/editor/Cursor.ts @@ -0,0 +1,72 @@ +import {Point} from '../rga/Point'; +import {CursorAnchor} from '../slice/constants'; +import {PersistedSlice} from '../slice/PersistedSlice'; + +export class Cursor extends PersistedSlice { + public get anchorSide(): CursorAnchor { + return this.type as CursorAnchor; + } + + public set anchorSide(value: CursorAnchor) { + this.update({type: value}); + } + + public anchor(): Point { + return this.anchorSide === CursorAnchor.Start ? this.start : this.end; + } + + public focus(): Point { + return this.anchorSide === CursorAnchor.Start ? this.end : this.start; + } + + public set(start: Point, end?: Point, anchorSide: CursorAnchor = this.anchorSide): void { + if (!end || end === start) end = start.clone(); + super.set(start, end); + this.update({ + range: this, + type: anchorSide, + }); + } + + public setAt(start: number, length: number = 0): void { + let at = start; + let len = length; + if (len < 0) { + at += len; + len = -len; + } + super.setAt(at, len); + this.anchorSide = length < 0 ? CursorAnchor.End : CursorAnchor.Start; + } + + /** + * Move one of the edges of the cursor to a new point. + * + * @param point Point to set the edge to. + * @param edge 0 for "focus", 1 for "anchor." + */ + public setEdge(point: Point, edge: 0 | 1 = 0): void { + if (this.start === this.end) this.end = this.end.clone(); + let anchor = this.anchor(); + let focus = this.focus(); + if (edge === 0) focus = point; else anchor = point; + if (focus.cmpSpatial(anchor) < 0) this.set(focus, anchor, CursorAnchor.End); + else this.set(anchor, focus, CursorAnchor.Start); + } + + public move(move: number): void { + const {start, end} = this; + start.move(move); + if (start !== end) { + end.move(move); + } + this.set(start, end); + } + + // ---------------------------------------------------------------- Printable + + public toStringName(): string { + const focusIcon = this.anchorSide === CursorAnchor.Start ? '.→|' : '|←.'; + return `${super.toStringName()}, ${focusIcon}`; + } +} diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts index e5c450b692..206201b00d 100644 --- a/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -1,6 +1,6 @@ -import {Cursor} from '../slice/Cursor'; +import {Cursor} from './Cursor'; import {Anchor} from '../rga/constants'; -import {SliceBehavior} from '../slice/constants'; +import {CursorAnchor, SliceBehavior} from '../slice/constants'; import {tick, type ITimestampStruct} from '../../../json-crdt-patch/clock'; import {PersistedSlice} from '../slice/PersistedSlice'; import {Chars} from '../constants'; @@ -10,6 +10,7 @@ import type {Printable} from 'tree-dump/lib/types'; import type {Point} from '../rga/Point'; import type {SliceType} from '../types'; import type {MarkerSlice} from '../slice/MarkerSlice'; +import type {Slices} from '../slice/Slices'; export class Editor implements Printable { /** @@ -18,10 +19,10 @@ export class Editor implements Printable { */ public readonly cursor: Cursor; - constructor(public readonly txt: Peritext) { - const point = txt.point(txt.str.id, Anchor.After); - const cursorId = txt.str.id; // TODO: should be autogenerated to something else - this.cursor = new Cursor(cursorId, txt, point, point.clone()); + constructor(public readonly txt: Peritext, slices: Slices) { + const point = txt.pointAbsStart(); + const range = txt.range(point, point.clone()); + this.cursor = slices.ins(range, SliceBehavior.Cursor, CursorAnchor.Start, undefined, Cursor); } /** @deprecated */ diff --git a/src/json-crdt-extensions/peritext/slice/Cursor.ts b/src/json-crdt-extensions/peritext/slice/Cursor.ts deleted file mode 100644 index 250499b4d8..0000000000 --- a/src/json-crdt-extensions/peritext/slice/Cursor.ts +++ /dev/null @@ -1,109 +0,0 @@ -import {Point} from '../rga/Point'; -import {CursorAnchor, SliceBehavior, Tags} from './constants'; -import {Range} from '../rga/Range'; -import {printTree} from 'tree-dump/lib/printTree'; -import {updateNum} from '../../../json-hash'; -import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; -import type {Peritext} from '../Peritext'; -import type {Slice} from './types'; - -export class Cursor extends Range implements Slice { - public readonly behavior = SliceBehavior.Overwrite; - public readonly type = Tags.Cursor; - - /** - * Specifies whether the start or the end of the cursor is the "anchor", e.g. - * the end which does not move when user changes selection. The other - * end is free to move, the moving end of the cursor is "focus". By default - * "anchor" is the start of the cursor. - */ - public anchorSide: CursorAnchor = CursorAnchor.Start; - - constructor( - public readonly id: ITimestampStruct, - protected readonly txt: Peritext, - public start: Point, - public end: Point, - ) { - super(txt.str as any, start, end); - } - - public anchor(): Point { - return this.anchorSide === CursorAnchor.Start ? this.start : this.end; - } - - public focus(): Point { - return this.anchorSide === CursorAnchor.Start ? this.end : this.start; - } - - public set(start: Point, end?: Point, base: CursorAnchor = CursorAnchor.Start): void { - if (!end || end === start) end = start.clone(); - super.set(start, end); - this.anchorSide = base; - } - - public setAt(start: number, length: number = 0): void { - let at = start; - let len = length; - if (len < 0) { - at += len; - len = -len; - } - super.setAt(at, len); - this.anchorSide = length < 0 ? CursorAnchor.End : CursorAnchor.Start; - } - - /** - * Move one of the edges of the cursor to a new point. - * - * @param point Point to set the edge to. - * @param edge 0 for "focus", 1 for "anchor." - */ - public setEdge(point: Point, edge: 0 | 1 = 0): void { - if (this.start === this.end) this.end = this.end.clone(); - let anchor = this.anchor(); - let focus = this.focus(); - if (edge === 0) focus = point; - else anchor = point; - if (focus.cmpSpatial(anchor) < 0) { - this.anchorSide = CursorAnchor.End; - this.start = focus; - this.end = anchor; - } else { - this.anchorSide = CursorAnchor.Start; - this.start = anchor; - this.end = focus; - } - } - - public data() { - return undefined; - } - - public move(move: number): void { - const {start, end} = this; - start.move(move); - if (start === end) return; - end.move(move); - } - - // ----------------------------------------------------------------- Stateful - - public hash: number = 0; - - public refresh(): number { - let state = super.refresh(); - state = updateNum(state, this.anchorSide); - this.hash = state; - return state; - } - - // ---------------------------------------------------------------- Printable - - public toString(tab: string = ''): string { - const text = this.text(); - const focusIcon = this.anchorSide === CursorAnchor.Start ? '.→|' : '|←.'; - const main = `${this.constructor.name} ${super.toString(tab + ' ', true)} ${focusIcon}`; - return main + (text.length > 32 ? printTree(tab, [() => JSON.stringify(text)]) : ''); - } -} diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index 0aa3c17392..099032c057 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -85,7 +85,8 @@ export class PersistedSlice extends Range implements MutableSlice if (range.end.anchor !== end.anchor) updateHeader = true; if (compare(range.start.id, start.id) !== 0) changes.push([SliceTupleIndex.X1, s.con(range.start.id)]); if (compare(range.end.id, end.id) !== 0) changes.push([SliceTupleIndex.X2, s.con(range.end.id)]); - this.setRange(range); + this.start = range.start; + this.end = range.start === range.end ? range.end.clone() : range.end; } if (params.type !== undefined) { this.type = params.type; @@ -137,11 +138,18 @@ export class PersistedSlice extends Range implements MutableSlice // ---------------------------------------------------------------- Printable + protected toStringName(): string { + 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}` : ''}`; + return header; + } + public toString(tab: string = ''): string { const data = this.data(); const dataFormatted = data ? prettyOneLine(data) : ''; const dataLengthBreakpoint = 32; - const header = `${this.constructor.name} ${super.toString(tab)}, ${this.behavior}, ${JSON.stringify(this.type)}${dataFormatted.length < dataLengthBreakpoint ? `, ${dataFormatted}` : ''}`; - return header + printTree(tab, [dataFormatted.length < dataLengthBreakpoint ? null : (tab) => dataFormatted]); + return this.toStringName() + printTree(tab, [dataFormatted.length < dataLengthBreakpoint ? null : (tab) => dataFormatted]); } } diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index 08a1084166..5a0a2a8109 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -28,7 +28,13 @@ export class Slices implements Stateful, Printable { protected readonly rga: AbstractRga, ) {} - public ins(range: Range, behavior: SliceBehavior, type: SliceType, data?: unknown): PersistedSlice { + public ins, K extends new (...args: ConstructorParameters>) => S>( + range: Range, + behavior: SliceBehavior, + type: SliceType, + data?: unknown, + Klass: K = behavior === SliceBehavior.Marker ? MarkerSlice : PersistedSlice + ): S { const model = this.model; const set = this.set; const api = model.api; @@ -57,10 +63,7 @@ export class Slices implements Stateful, Printable { const tuple = model.index.get(tupleId) as VecNode; const chunk = set.findById(chunkId)!; // TODO: Need to check if split slice text was deleted - const slice = - behavior === SliceBehavior.Marker - ? new MarkerSlice(model, this.rga, chunk, tuple, behavior, type, start, end) - : new PersistedSlice(model, this.rga, chunk, tuple, behavior, type, start, end); + const slice = new Klass(model, this.rga, chunk, tuple, behavior, type, start, end); this.list.set(chunk.id, slice); return slice; } diff --git a/src/json-crdt-extensions/peritext/slice/constants.ts b/src/json-crdt-extensions/peritext/slice/constants.ts index 6ed172066a..97724b4d5a 100644 --- a/src/json-crdt-extensions/peritext/slice/constants.ts +++ b/src/json-crdt-extensions/peritext/slice/constants.ts @@ -1,6 +1,8 @@ /** - * Specifies which cursor end is the "anchor", e.g. the end which does not move - * when user changes selection. + * Specifies whether the start or the end of the cursor is the "anchor", e.g. + * the end which does not move when user changes selection. The other + * end is free to move, the moving end of the cursor is "focus". By default + * "anchor" is usually the start of the cursor. */ export const enum CursorAnchor { Start = 0, @@ -48,6 +50,11 @@ export const enum SliceBehavior { * used to re-verse inline formatting, like bold, italic, etc. */ Erase = 0b011, + + /** + * Used to mark the user's cursor position in the document. + */ + Cursor = 0b100, } export const enum SliceTupleIndex { diff --git a/src/json-crdt-patch/constants.ts b/src/json-crdt-patch/constants.ts index 39762ab764..e62c3fd595 100644 --- a/src/json-crdt-patch/constants.ts +++ b/src/json-crdt-patch/constants.ts @@ -20,6 +20,14 @@ export const enum SESSION { */ GLOBAL = 2, + /** + * Session ID used for models that are not shared with other users. For + * example, when a user is editing a document in a local editor, these + * documents could capture local information, like the cursor position, which + * is not shared with other users. + */ + LOCAL = 3, + /** Max allowed session ID, they are capped at 53-bits. */ MAX = 9007199254740991, } From 553c9ee6b93e0b6515954ffe884423d2163146d0 Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 1 May 2024 17:45:31 +0200 Subject: [PATCH 14/19] =?UTF-8?q?fix:=20=F0=9F=90=9B=20bump=20tree-dump=20?= =?UTF-8?q?dependency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- yarn.lock | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8341d032f7..3b8b17f76f 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "hyperdyperid": "^1.2.0", "sonic-forest": "^1.0.2", "thingies": "^2.0.0", - "tree-dump": "^1.0.0" + "tree-dump": "^1.0.1" }, "devDependencies": { "@types/benchmark": "^2.1.5", diff --git a/yarn.lock b/yarn.lock index c63e597a4d..376daa94d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2203,6 +2203,11 @@ tree-dump@^1.0.0: resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.0.tgz#bd5fdece2b36d888ae0d1cf316e653af3de656ea" integrity sha512-gDLjiHO2JTBf8JtRNCq/tUYZMdI5EFOA3UKWZJddwqVxRjC8jj/tI/pJEocV0hPtJeztEcF2RqufJZYbF/rKEw== +tree-dump@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.1.tgz#b448758da7495580e6b7830d6b7834fca4c45b96" + integrity sha512-WCkcRBVPSlHHq1dc/px9iOfqklvzCbdRwvlNfxGZsrHqf6aZttfPrd7DJTt6oR10dwUfpFFQeVTkPbBIZxX/YA== + ts-jest@^29.1.2: version "29.1.2" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.2.tgz#7613d8c81c43c8cb312c6904027257e814c40e09" From 52cf2b9e3cb35402d57e0a0e5ca48556d891ce08 Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 1 May 2024 17:46:25 +0200 Subject: [PATCH 15/19] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20pass=20Overlay=20smoke=20tests=20after=20refacto?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 1 + .../peritext/editor/Cursor.ts | 10 ++++++++-- .../peritext/overlay/Overlay.ts | 9 +++++---- .../peritext/overlay/__tests__/Overlay.spec.ts | 1 - .../peritext/slice/PersistedSlice.ts | 18 +++++++++--------- 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index 2f3bc48361..3d2e27700d 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -59,6 +59,7 @@ export class Peritext implements Printable { this.extraSlices = new Slices(extraModel, extraModel.root.node().get(0)!, this.str); // TODO: flush patches + // TODO: remove `arr` tombstones const localModel = Model .withLogicalClock(SESSION.LOCAL) .setSchema(s.vec(s.arr([]))); diff --git a/src/json-crdt-extensions/peritext/editor/Cursor.ts b/src/json-crdt-extensions/peritext/editor/Cursor.ts index fd9f6fc9dd..2aff0b1f72 100644 --- a/src/json-crdt-extensions/peritext/editor/Cursor.ts +++ b/src/json-crdt-extensions/peritext/editor/Cursor.ts @@ -1,4 +1,5 @@ import {Point} from '../rga/Point'; +import {Range} from '../rga/Range'; import {CursorAnchor} from '../slice/constants'; import {PersistedSlice} from '../slice/PersistedSlice'; @@ -28,6 +29,7 @@ export class Cursor extends PersistedSlice { }); } + /** Move to persisted slice. */ public setAt(start: number, length: number = 0): void { let at = start; let len = length; @@ -35,8 +37,12 @@ export class Cursor extends PersistedSlice { at += len; len = -len; } - super.setAt(at, len); - this.anchorSide = length < 0 ? CursorAnchor.End : CursorAnchor.Start; + const range = Range.at(this.rga, start, length); + const anchorSide = this.anchorSide; + this.update({ + range, + type: anchorSide !== this.anchorSide ? anchorSide : undefined, + }); } /** diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 67ee11f5ef..3dce9bc0e1 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -15,6 +15,7 @@ import type {Peritext} from '../Peritext'; import type {Stateful} from '../types'; import type {Printable} from 'tree-dump/lib/types'; import type {MutableSlice, Slice} from '../slice/types'; +import type {Slices} from '../slice/Slices'; export class Overlay implements Printable, Stateful { public root: OverlayPoint | undefined = undefined; @@ -91,8 +92,10 @@ export class Overlay implements Printable, Stateful { public hash: number = 0; public refresh(slicesOnly: boolean = false): number { + const txt = this.txt; let hash: number = CONST.START_STATE; - hash = this.refreshSlices(hash); + hash = this.refreshSlices(hash, txt.savedSlices); + hash = this.refreshSlices(hash, txt.localSlices); // hash = this.refreshCursor(hash); // TODO: refresh ephemeral slices // if (!slicesOnly) this.computeSplitTextHashes(); @@ -101,15 +104,13 @@ export class Overlay implements Printable, Stateful { public readonly slices = new Map(); - private refreshSlices(state: number): number { - const slices = this.txt.savedSlices; + private refreshSlices(state: number, slices: Slices): number { 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 + ''); let tuple: [start: OverlayPoint, end: OverlayPoint] | undefined = sliceSet.get(slice); if (tuple) { if ((slice as any).isDel && (slice as any).isDel()) { 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 8cbae5c6b1..71ac4585ce 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts @@ -46,7 +46,6 @@ describe('markers', () => { const points = []; let point; for (const iterator = peritext.overlay.iterator(); (point = iterator()); ) points.push(point); - // console.log(peritext + ''); expect(points.length).toBe(2); point = points[0]; expect(point.pos()).toBe(5); diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index 099032c057..55be33a089 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -1,3 +1,4 @@ +import {hasOwnProperty} from '@jsonjoy.com/util/lib/hasOwnProperty'; import {Point} from '../rga/Point'; import {Range} from '../rga/Range'; import {updateNode} from '../../../json-crdt/hash'; @@ -73,7 +74,6 @@ export class PersistedSlice extends Range implements MutableSlice public update(params: SliceUpdateParams): void { let updateHeader = false; - const {start, end} = this; const changes: [number, unknown][] = []; if (params.behavior !== undefined) { this.behavior = params.behavior; @@ -81,10 +81,10 @@ export class PersistedSlice extends Range implements MutableSlice } if (params.range) { const range = params.range; - if (range.start.anchor !== start.anchor) updateHeader = true; - if (range.end.anchor !== end.anchor) updateHeader = true; - if (compare(range.start.id, start.id) !== 0) changes.push([SliceTupleIndex.X1, s.con(range.start.id)]); - if (compare(range.end.id, end.id) !== 0) changes.push([SliceTupleIndex.X2, s.con(range.end.id)]); + updateHeader = true; + changes.push( + [SliceTupleIndex.X1, s.con(range.start.id)], + [SliceTupleIndex.X2, s.con(range.end.id)]); this.start = range.start; this.end = range.start === range.end ? range.end.clone() : range.end; } @@ -92,12 +92,12 @@ export class PersistedSlice extends Range implements MutableSlice this.type = params.type; changes.push([SliceTupleIndex.Type, s.con(this.type)]); } - if (params.data !== undefined) changes.push([SliceTupleIndex.Data, s.con(params.data)]); + if (hasOwnProperty(params, 'data')) changes.push([SliceTupleIndex.Data, s.con(params.data)]); if (updateHeader) { const header = - (this.behavior << SliceHeaderShift.Behavior) + - (this.start.anchor << SliceHeaderShift.X1Anchor) + - (this.end.anchor << SliceHeaderShift.X2Anchor); + (this.behavior << SliceHeaderShift.Behavior) + + (this.start.anchor << SliceHeaderShift.X1Anchor) + + (this.end.anchor << SliceHeaderShift.X2Anchor); changes.push([SliceTupleIndex.Header, s.con(header)]); } this.tupleApi().set(changes); From 232457bc3db6839df9130f1cbae9e27284d548f4 Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 1 May 2024 18:29:26 +0200 Subject: [PATCH 16/19] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20cleanup=20internal=20data=20structures=20after?= =?UTF-8?q?=20local=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 5 +- .../__tests__/Peritext.localSlices.spec.ts | 46 +++++++++++++++++++ .../peritext/editor/Cursor.ts | 2 +- .../overlay/__tests__/Overlay.refresh.spec.ts | 40 ++++++++++------ .../peritext/slice/LocalSlices.ts | 9 ++++ 5 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 src/json-crdt-extensions/peritext/__tests__/Peritext.localSlices.spec.ts create mode 100644 src/json-crdt-extensions/peritext/slice/LocalSlices.ts diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index 3d2e27700d..123b43eb09 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -5,6 +5,7 @@ import {Range} from './rga/Range'; import {Editor} from './editor/Editor'; import {ArrNode, StrNode} from '../../json-crdt/nodes'; import {Slices} from './slice/Slices'; +import {LocalSlices} from './slice/LocalSlices'; import {Overlay} from './overlay/Overlay'; import {Chars} from './constants'; import {interval} from '../../json-crdt-patch/clock'; @@ -63,7 +64,9 @@ export class Peritext implements Printable { const localModel = Model .withLogicalClock(SESSION.LOCAL) .setSchema(s.vec(s.arr([]))); - this.localSlices = new Slices(localModel, localModel.root.node().get(0)!, this.str); + const localApi = localModel.api; + localApi.onLocalChange.listen(() => { localApi.flush(); }); + this.localSlices = new LocalSlices(localModel, localModel.root.node().get(0)!, this.str); this.editor = new Editor(this, this.localSlices); } diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.localSlices.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.localSlices.spec.ts new file mode 100644 index 0000000000..91df0367b1 --- /dev/null +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.localSlices.spec.ts @@ -0,0 +1,46 @@ +import {Model} from '../../../json-crdt/model'; +import {Peritext} from '../Peritext'; + +const setup = () => { + const model = Model.withLogicalClock(); + model.api.root({ + text: '', + slices: [], + }); + model.api.str(['text']).ins(0, 'wworld'); + model.api.str(['text']).ins(0, 'helo '); + model.api.str(['text']).ins(2, 'l'); + model.api.str(['text']).del(7, 1); + const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node); + return {model, peritext}; +}; + +test('clears change history', () => { + const {peritext} = setup(); + const {editor} = peritext; + editor.cursor.setAt(0); + editor.cursor.setAt(1); + editor.cursor.setAt(2); + editor.cursor.setAt(3); + expect(peritext.localSlices.model.api.flush().ops.length).toBe(0); +}); + +test('clears slice set tombstones', () => { + const _random = Math.random; + // It is probabilistic, if we set `Math.random` to 0 it will always remove tombstones. + Math.random = () => 0; + const {peritext} = setup(); + const slicesRga = peritext.localSlices.model.root.node()!.get(0)!; + const count = slicesRga.size(); + const slice1 = peritext.localSlices.insOverwrite(peritext.rangeAt(1, 2), 1); + const slice2 = peritext.localSlices.insOverwrite(peritext.rangeAt(1, 2), 3); + const slice3 = peritext.localSlices.insOverwrite(peritext.rangeAt(1, 2), 2); + expect(slicesRga.size()).toBe(count + 3); + peritext.localSlices.del(slice2.id); + expect(slicesRga.size()).toBe(count + 2); + peritext.localSlices.del(slice1.id); + expect(slicesRga.size()).toBe(count + 1); + peritext.localSlices.del(slice3.id); + expect(slicesRga.size()).toBe(count); + Math.random = _random; +}); diff --git a/src/json-crdt-extensions/peritext/editor/Cursor.ts b/src/json-crdt-extensions/peritext/editor/Cursor.ts index 2aff0b1f72..3e63a8a980 100644 --- a/src/json-crdt-extensions/peritext/editor/Cursor.ts +++ b/src/json-crdt-extensions/peritext/editor/Cursor.ts @@ -29,7 +29,7 @@ export class Cursor extends PersistedSlice { }); } - /** Move to persisted slice. */ + /** TODO: Move to {@link PersistedSlice}. */ public setAt(start: number, length: number = 0): void { let at = start; let len = length; 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 024e7b1bd6..1b303e531e 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 @@ -142,21 +142,31 @@ describe('Overlay.refresh()', () => { }); describe('cursor', () => { - test.only('updates state hash, when cursor char ID changes', () => { - const {peritext} = setup(); - const overlay = peritext.overlay; - peritext.editor.cursor.setAt(1); - overlay.refresh(); - console.log(peritext + ''); - peritext.editor.cursor.setAt(2); - overlay.refresh(); - console.log(peritext + ''); - // const hash1 = overlay.refresh(); - // peritext.editor.cursor.setAt(2); - // const hash2 = overlay.refresh(); - // const hash3 = overlay.refresh(); - // expect(hash1).not.toBe(hash2); - // expect(hash2).toBe(hash3); + describe('updates hash', () => { + testRefresh('when cursor char ID changes', (kit, refresh) => { + kit.peritext.editor.cursor.setAt(1); + refresh(); + kit.peritext.editor.cursor.setAt(1); + }); + + testRefresh('when cursor start anchor changes', (kit, refresh) => { + kit.peritext.editor.cursor.setAt(3, 3); + expect(kit.peritext.editor.cursor.start.anchor).toBe(Anchor.Before); + refresh(); + const start = kit.peritext.editor.cursor.start.clone(); + start.anchor = Anchor.After; + kit.peritext.editor.cursor.setRange(kit.peritext.range(start, kit.peritext.editor.cursor.end)); + }); + + testRefresh('when cursor end anchor changes', (kit, refresh) => { + kit.peritext.editor.cursor.setAt(3, 3); + expect(kit.peritext.editor.cursor.end.anchor).toBe(Anchor.After); + refresh(); + const end = kit.peritext.editor.cursor.start.clone(); + end.anchor = Anchor.Before; + kit.peritext.editor.cursor.setRange(kit.peritext.range(kit.peritext.editor.cursor.start, end)); + console.log(kit.peritext.localSlices.model.api.flush()); + }); }); }); }); diff --git a/src/json-crdt-extensions/peritext/slice/LocalSlices.ts b/src/json-crdt-extensions/peritext/slice/LocalSlices.ts new file mode 100644 index 0000000000..5bb5c23f67 --- /dev/null +++ b/src/json-crdt-extensions/peritext/slice/LocalSlices.ts @@ -0,0 +1,9 @@ +import {Slices} from './Slices'; +import type {ITimestampStruct} from '../../../json-crdt-patch'; + +export class LocalSlices extends Slices { + public del(id: ITimestampStruct): void { + super.del(id); + if (Math.random() < 0.1) this.set.removeTombstones(); + } +} From abbac3a20a4d7d52af514f2c29dbf0d3456522b3 Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 1 May 2024 18:30:04 +0200 Subject: [PATCH 17/19] =?UTF-8?q?style(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=84=20run=20Prettier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 13 ++++++------- .../peritext/editor/Cursor.ts | 3 ++- .../peritext/editor/Editor.ts | 5 ++++- .../peritext/overlay/Overlay.ts | 5 ++++- .../peritext/slice/LocalSlices.ts | 2 +- .../peritext/slice/PersistedSlice.ts | 15 ++++++++------- src/json-crdt-extensions/peritext/slice/Slices.ts | 7 +++++-- src/json-crdt/nodes/rga/util.ts | 2 +- 8 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index 123b43eb09..141d04850a 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -42,7 +42,7 @@ export class Peritext implements Printable { * the current user. */ public readonly localSlices: Slices; - + public readonly editor: Editor; public readonly overlay = new Overlay(this); @@ -53,19 +53,18 @@ export class Peritext implements Printable { ) { this.savedSlices = new Slices(this.model, slices, this.str); - const extraModel = Model - .withLogicalClock(SESSION.GLOBAL) + 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 // TODO: remove `arr` tombstones - const localModel = Model - .withLogicalClock(SESSION.LOCAL) - .setSchema(s.vec(s.arr([]))); + const localModel = Model.withLogicalClock(SESSION.LOCAL).setSchema(s.vec(s.arr([]))); const localApi = localModel.api; - localApi.onLocalChange.listen(() => { localApi.flush(); }); + localApi.onLocalChange.listen(() => { + localApi.flush(); + }); this.localSlices = new LocalSlices(localModel, localModel.root.node().get(0)!, this.str); this.editor = new Editor(this, this.localSlices); diff --git a/src/json-crdt-extensions/peritext/editor/Cursor.ts b/src/json-crdt-extensions/peritext/editor/Cursor.ts index 3e63a8a980..d18498ca48 100644 --- a/src/json-crdt-extensions/peritext/editor/Cursor.ts +++ b/src/json-crdt-extensions/peritext/editor/Cursor.ts @@ -55,7 +55,8 @@ export class Cursor extends PersistedSlice { if (this.start === this.end) this.end = this.end.clone(); let anchor = this.anchor(); let focus = this.focus(); - if (edge === 0) focus = point; else anchor = point; + if (edge === 0) focus = point; + else anchor = point; if (focus.cmpSpatial(anchor) < 0) this.set(focus, anchor, CursorAnchor.End); else this.set(anchor, focus, CursorAnchor.Start); } diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts index 206201b00d..d111e0d645 100644 --- a/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -19,7 +19,10 @@ export class Editor implements Printable { */ public readonly cursor: Cursor; - constructor(public readonly txt: Peritext, slices: Slices) { + constructor( + public readonly txt: Peritext, + slices: Slices, + ) { const point = txt.pointAbsStart(); const range = txt.range(point, point.clone()); this.cursor = slices.ins(range, SliceBehavior.Cursor, CursorAnchor.Start, undefined, Cursor); diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 3dce9bc0e1..6f5d6432b8 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -266,6 +266,9 @@ export class Overlay implements Printable, Stateful { ]) ); }; - return `${this.constructor.name} #${this.hash.toString(36)}` + printTree(tab, [!this.root ? null : (tab) => printPoint(tab, this.root!)]); + return ( + `${this.constructor.name} #${this.hash.toString(36)}` + + printTree(tab, [!this.root ? null : (tab) => printPoint(tab, this.root!)]) + ); } } diff --git a/src/json-crdt-extensions/peritext/slice/LocalSlices.ts b/src/json-crdt-extensions/peritext/slice/LocalSlices.ts index 5bb5c23f67..fbb1eb2148 100644 --- a/src/json-crdt-extensions/peritext/slice/LocalSlices.ts +++ b/src/json-crdt-extensions/peritext/slice/LocalSlices.ts @@ -5,5 +5,5 @@ export class LocalSlices extends Slices { public del(id: ITimestampStruct): void { super.del(id); if (Math.random() < 0.1) this.set.removeTombstones(); - } + } } diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index 55be33a089..1c70d46ce5 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -82,9 +82,7 @@ export class PersistedSlice extends Range implements MutableSlice if (params.range) { const range = params.range; updateHeader = true; - changes.push( - [SliceTupleIndex.X1, s.con(range.start.id)], - [SliceTupleIndex.X2, s.con(range.end.id)]); + changes.push([SliceTupleIndex.X1, s.con(range.start.id)], [SliceTupleIndex.X2, s.con(range.end.id)]); this.start = range.start; this.end = range.start === range.end ? range.end.clone() : range.end; } @@ -95,9 +93,9 @@ export class PersistedSlice extends Range implements MutableSlice if (hasOwnProperty(params, 'data')) changes.push([SliceTupleIndex.Data, s.con(params.data)]); if (updateHeader) { const header = - (this.behavior << SliceHeaderShift.Behavior) + - (this.start.anchor << SliceHeaderShift.X1Anchor) + - (this.end.anchor << SliceHeaderShift.X2Anchor); + (this.behavior << SliceHeaderShift.Behavior) + + (this.start.anchor << SliceHeaderShift.X1Anchor) + + (this.end.anchor << SliceHeaderShift.X2Anchor); changes.push([SliceTupleIndex.Header, s.con(header)]); } this.tupleApi().set(changes); @@ -150,6 +148,9 @@ export class PersistedSlice extends Range implements MutableSlice const data = this.data(); const dataFormatted = data ? prettyOneLine(data) : ''; const dataLengthBreakpoint = 32; - return this.toStringName() + printTree(tab, [dataFormatted.length < dataLengthBreakpoint ? null : (tab) => dataFormatted]); + return ( + this.toStringName() + + printTree(tab, [dataFormatted.length < dataLengthBreakpoint ? null : (tab) => dataFormatted]) + ); } } diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index 5a0a2a8109..852bca4ee6 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -28,12 +28,15 @@ export class Slices implements Stateful, Printable { protected readonly rga: AbstractRga, ) {} - public ins, K extends new (...args: ConstructorParameters>) => S>( + public ins< + S extends PersistedSlice, + K extends new (...args: ConstructorParameters>) => S, + >( range: Range, behavior: SliceBehavior, type: SliceType, data?: unknown, - Klass: K = behavior === SliceBehavior.Marker ? MarkerSlice : PersistedSlice + Klass: K = behavior === SliceBehavior.Marker ? MarkerSlice : PersistedSlice, ): S { const model = this.model; const set = this.set; diff --git a/src/json-crdt/nodes/rga/util.ts b/src/json-crdt/nodes/rga/util.ts index 6bbbf3c2ae..5aa4315859 100644 --- a/src/json-crdt/nodes/rga/util.ts +++ b/src/json-crdt/nodes/rga/util.ts @@ -1,4 +1,4 @@ -import type {AbstractRga, Chunk} from "./AbstractRga"; +import type {AbstractRga, Chunk} from './AbstractRga'; /** Find the first visible chunk, if any. */ export const firstVis = (rga: AbstractRga): Chunk | undefined => { From 576bb2c2d566cc363e1304ea13cf73125fe0f5f8 Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 1 May 2024 18:31:00 +0200 Subject: [PATCH 18/19] =?UTF-8?q?style(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=84=20remove=20unnecessary=20console.log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/__tests__/Overlay.refresh.spec.ts | 1 - 1 file changed, 1 deletion(-) 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 1b303e531e..d061e5e577 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 @@ -165,7 +165,6 @@ describe('Overlay.refresh()', () => { const end = kit.peritext.editor.cursor.start.clone(); end.anchor = Anchor.Before; kit.peritext.editor.cursor.setRange(kit.peritext.range(kit.peritext.editor.cursor.start, end)); - console.log(kit.peritext.localSlices.model.api.flush()); }); }); }); From ee76f283645ae112267160442327bd0fbd8a9203 Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 1 May 2024 18:35:13 +0200 Subject: [PATCH 19/19] =?UTF-8?q?refactor(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=A1=20cleanup=20Overlay=20refresh=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/Overlay.ts | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 6f5d6432b8..4ed30f12f3 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -95,9 +95,8 @@ export class Overlay implements Printable, Stateful { const txt = this.txt; let hash: number = CONST.START_STATE; hash = this.refreshSlices(hash, txt.savedSlices); + hash = this.refreshSlices(hash, txt.extraSlices); hash = this.refreshSlices(hash, txt.localSlices); - // hash = this.refreshCursor(hash); - // TODO: refresh ephemeral slices // if (!slicesOnly) this.computeSplitTextHashes(); return (this.hash = hash); } @@ -137,27 +136,13 @@ export class Overlay implements Printable, Stateful { return state; } - private refreshCursor(state: number): number { - const cursor = this.txt.editor.cursor; - let tuple: [start: OverlayPoint, end: OverlayPoint] | undefined = this.slices.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; - } - /** * 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); + const pivot = this.insPoint(newPoint); if (pivot) return [pivot, false]; return [newPoint, true]; } @@ -167,7 +152,7 @@ export class Overlay implements Printable, Stateful { * @param point Point to insert. * @returns Returns the existing point if it was already in the tree. */ - protected insertPoint(point: OverlayPoint): OverlayPoint | undefined { + private insPoint(point: OverlayPoint): OverlayPoint | undefined { let pivot = this.getOrNextLower(point); if (!pivot) pivot = first(this.root); if (!pivot) { @@ -183,13 +168,13 @@ export class Overlay implements Printable, Stateful { return undefined; } - protected delPoint(point: OverlayPoint): void { + private delPoint(point: OverlayPoint): void { this.root = remove(this.root, point); } - protected insSplit(slice: MarkerSlice): [start: OverlayPoint, end: OverlayPoint] { + private insMarker(slice: MarkerSlice): [start: OverlayPoint, end: OverlayPoint] { const point = this.markerPoint(slice, Anchor.Before); - const pivot = this.insertPoint(point); + const pivot = this.insPoint(point); if (!pivot) { point.refs.push(slice); const prevPoint = prev(point); @@ -199,7 +184,7 @@ export class Overlay implements Printable, Stateful { } private insSlice(slice: Slice): [start: OverlayPoint, end: OverlayPoint] { - if (slice instanceof MarkerSlice) return this.insSplit(slice); + if (slice instanceof MarkerSlice) return this.insMarker(slice); const txt = this.txt; const str = txt.str; let startPoint = slice.start;