From 96fb4024e9fba22f5c6b494b9c43d3405b358080 Mon Sep 17 00:00:00 2001 From: streamich Date: Thu, 27 Jun 2024 23:39:07 +0200 Subject: [PATCH 01/10] =?UTF-8?q?refactor(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=A1=20move=20text=20deletion=20code=20to=20Peritext=20?= =?UTF-8?q?main=20object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 45 +++++++++++++++++++ .../peritext/editor/Cursor.ts | 22 ++------- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index 49c26f5c69..d5fd1e7cfd 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -241,6 +241,51 @@ export class Peritext implements Printable { return textId; } + public delAt(pos: number, len: number): void { + const range = this.rangeAt(pos, len); + this.del(range); + } + + public del(range: Range): void { + // this.delSlices(range); + this.delStr(range); + } + + public delStr(range: Range): boolean { + const isCaret = range.isCollapsed(); + if (isCaret) return false; + const {start, end} = range; + const delStartId = start.isAbsStart() + ? this.point().refStart().id + : start.anchor === Anchor.Before + ? start.id + : start.nextId(); + const delEndId = end.isAbsEnd() + ? this.point().refEnd().id + : end.anchor === Anchor.After + ? end.id + : end.prevId(); + if (!delStartId || !delEndId) throw new Error('INVALID_RANGE'); + const rga = this.str; + const spans = rga.findInterval2(delStartId, delEndId); + const api = this.model.api; + api.builder.del(rga.id, spans); + api.apply(); + return true; + } + + // public delSlices(range: Range): void { + // this.overlay.refresh(); + // range = range.clone(); + // range.expand(); + // const slices = this.overlay.findContained(range); + // this.slices.delMany(Array.from(slices)); + // } + + // public delSlice(sliceId: ITimestampStruct): void { + // this.slices.del(sliceId); + // } + // ------------------------------------------------------------------ markers /** @deprecated Use the method in `Editor` and `Cursor` instead. */ diff --git a/src/json-crdt-extensions/peritext/editor/Cursor.ts b/src/json-crdt-extensions/peritext/editor/Cursor.ts index 94c2e80b26..9686806580 100644 --- a/src/json-crdt-extensions/peritext/editor/Cursor.ts +++ b/src/json-crdt-extensions/peritext/editor/Cursor.ts @@ -65,25 +65,9 @@ export class Cursor extends PersistedSlice { * @returns Returns the cursor position after the operation. */ public collapse(): void { - const isCaret = this.isCollapsed(); - if (!isCaret) { - const {start, end} = this; - const delStartId = start.isAbsStart() - ? this.txt.point().refStart().id - : start.anchor === Anchor.Before - ? start.id - : start.nextId(); - const delEndId = end.isAbsEnd() - ? this.txt.point().refEnd().id - : end.anchor === Anchor.After - ? end.id - : end.prevId(); - if (!delStartId || !delEndId) throw new Error('INVALID_RANGE'); - const rga = this.rga; - const spans = rga.findInterval2(delStartId, delEndId); - const api = this.txt.model.api; - api.builder.del(rga.id, spans); - api.apply(); + const deleted = this.txt.delStr(this); + if (deleted) { + const {start, rga} = this; if (start.anchor === Anchor.After) this.setAfter(start.id); else this.setAfter(start.prevId() || rga.id); } From 6359951aa9a5f1d06d9b3e4ee69b71a1451ea635 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 28 Jun 2024 00:22:14 +0200 Subject: [PATCH 02/10] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20improve=20slice=20deletions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 21 +++++---- .../__tests__/Peritext.cursor.spec.ts | 2 - .../__tests__/Peritext.deletions.spec.ts | 47 +++++++++++++++++++ .../peritext/editor/Editor.ts | 7 ++- .../peritext/editor/EditorSlices.ts | 2 +- .../peritext/slice/Slices.ts | 7 ++- 6 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 src/json-crdt-extensions/peritext/__tests__/Peritext.deletions.spec.ts diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index d5fd1e7cfd..7e857bbb46 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -212,7 +212,7 @@ export class Peritext implements Printable { return this.range(start, end); } - // --------------------------------------------------------------------- text + // ---------------------------------------------------------- text (& slices) /** * Insert plain text at a view position in the text. @@ -247,7 +247,7 @@ export class Peritext implements Printable { } public del(range: Range): void { - // this.delSlices(range); + this.delSlices(range); this.delStr(range); } @@ -274,13 +274,16 @@ export class Peritext implements Printable { return true; } - // public delSlices(range: Range): void { - // this.overlay.refresh(); - // range = range.clone(); - // range.expand(); - // const slices = this.overlay.findContained(range); - // this.slices.delMany(Array.from(slices)); - // } + public delSlices(range: Range): void { + this.overlay.refresh(); + range = range.range(); + range.expand(); + const slices = this.overlay.findContained(range); + if (!slices.size) return; + this.savedSlices.delSlices(slices); + this.extraSlices.delSlices(slices); + this.localSlices.delSlices(slices); + } // public delSlice(sliceId: ITimestampStruct): void { // this.slices.del(sliceId); diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.cursor.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.cursor.spec.ts index fe7856fc31..ed18cf6ae0 100644 --- a/src/json-crdt-extensions/peritext/__tests__/Peritext.cursor.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.cursor.spec.ts @@ -135,5 +135,3 @@ test('cursor can move across block boundary forwards', () => { [SliceTypes.Cursor]: [[[CursorAnchor.Start, void 0]], InlineAttrPos.Collapsed], }); }); - -test.todo('moving past text end keeps cursor at text end'); diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.deletions.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.deletions.spec.ts new file mode 100644 index 0000000000..93f42c1fb7 --- /dev/null +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.deletions.spec.ts @@ -0,0 +1,47 @@ +import {setupAlphabetChunkSplitKit, setupAlphabetKit, Kit, setupAlphabetWithDeletesKit, setupAlphabetWithTwoChunksKit} from './setup'; + +const run = (setup: () => Kit) => { + describe('.delAt()', () => { + test('can delete text', () => { + const {peritext} = setup(); + peritext.delAt(0, 1); + peritext.delAt(0, 2); + peritext.delAt(1, 2); + peritext.delAt(3, 15); + peritext.delAt(4, 2); + expect(peritext.str.view()).toBe('dghx'); + }); + + test('deletes slice if it is contained in deletion range', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(2, 2); + editor.saved.insOverwrite('underline'); + editor.cursor.setAt(0); + peritext.refresh(); + expect(editor.saved.slices.size()).toBe(1); + peritext.delAt(1, 3); + peritext.refresh(); + expect(editor.saved.slices.size()).toBe(0); + }); + + test('does not delete slice if it is only partially contained', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(2, 10); + editor.extra.insOverwrite('underline'); + editor.cursor.setAt(0); + peritext.refresh(); + expect(peritext.extraSlices.size()).toBe(1); + peritext.delAt(1, 10); + peritext.refresh(); + expect(peritext.extraSlices.size()).toBe(1); + peritext.delAt(1, 10); + peritext.refresh(); + expect(peritext.extraSlices.size()).toBe(0); + }); + }); +}; + +describe('basic alphabet', () => run(setupAlphabetKit)); +describe('alphabet with chunk splits', () => run(setupAlphabetChunkSplitKit)); +describe('alphabet with deletes', () => run(setupAlphabetWithDeletesKit)); +describe('alphabet with two chunks', () => run(setupAlphabetWithTwoChunksKit)); diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts index 7ef2549156..96afe78238 100644 --- a/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -70,7 +70,12 @@ export class Editor { * the range is removed and the text is inserted at the start of the range. */ public insert(text: string): void { - this.cursors((cursor) => cursor.insert(text)); + let cnt = 0; + this.cursors((cursor) => { + cnt++; + cursor.insert(text); + }); + if (!cnt) this.cursor.insert(text); } /** diff --git a/src/json-crdt-extensions/peritext/editor/EditorSlices.ts b/src/json-crdt-extensions/peritext/editor/EditorSlices.ts index 95387a7cde..4b3ce95457 100644 --- a/src/json-crdt-extensions/peritext/editor/EditorSlices.ts +++ b/src/json-crdt-extensions/peritext/editor/EditorSlices.ts @@ -9,7 +9,7 @@ import type {Cursor} from './Cursor'; export class EditorSlices { constructor( protected readonly txt: Peritext, - protected readonly slices: Slices, + public readonly slices: Slices, ) {} protected insAtCursors>(callback: (cursor: Cursor) => S): S[] { diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index 466b3688d1..a239d83813 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -141,17 +141,16 @@ export class Slices implements Stateful, Printable { api.apply(); } - public delSlices(slices: Slice[]): void { + public delSlices(slices: Iterable>): void { const api = this.set.doc.api; const spans: ITimespanStruct[] = []; - const length = slices.length; - for (let i = 0; i < length; i++) { - const slice = slices[i]; + for (const slice of slices) { if (slice instanceof PersistedSlice) { const id = slice.id; spans.push(new Timespan(id.sid, id.time, 1)); } } + if (!spans.length) return; api.builder.del(this.set.id, spans); api.apply(); } From c76994544b125538ec01a41768ba0555068e98c8 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 29 Jun 2024 13:39:34 +0200 Subject: [PATCH 03/10] =?UTF-8?q?fix(json-crdt-extensions):=20=F0=9F=90=9B?= =?UTF-8?q?=20do=20not=20create=20stray=20slice=20deletion=20patches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 17 +++--- .../__tests__/Peritext.deletions.spec.ts | 52 ++++++++++++++++++- .../peritext/slice/Slices.ts | 14 +++-- 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index 7e857bbb46..b7f1575c80 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -73,6 +73,7 @@ export class Peritext implements Printable { // TODO: Rename `str` to `rga`. public readonly str: AbstractRga, slices: ArrNode, + // TODO: Add test that verifies that SIDs are different across all three models. extraSlicesModel: SlicesModel = Model.create(EXTRA_SLICES_SCHEMA, model.clock.sid - 1), localSlicesModel: SlicesModel = Model.create(EXTRA_SLICES_SCHEMA, SESSION.LOCAL), ) { @@ -274,19 +275,23 @@ export class Peritext implements Printable { return true; } - public delSlices(range: Range): void { + public delSlices(range: Range): boolean { + // TODO: PERF: do we need this refresh? this.overlay.refresh(); range = range.range(); range.expand(); const slices = this.overlay.findContained(range); - if (!slices.size) return; - this.savedSlices.delSlices(slices); - this.extraSlices.delSlices(slices); - this.localSlices.delSlices(slices); + let deleted = false; + if (!slices.size) return deleted; + if (this.savedSlices.delSlices(slices)) deleted = true; + if (this.extraSlices.delSlices(slices)) deleted = true; + if (this.localSlices.delSlices(slices)) deleted = true; + return deleted; } // public delSlice(sliceId: ITimestampStruct): void { - // this.slices.del(sliceId); + + // this.savedSlices.del(sliceId); // } // ------------------------------------------------------------------ markers diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.deletions.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.deletions.spec.ts index 93f42c1fb7..c45b548542 100644 --- a/src/json-crdt-extensions/peritext/__tests__/Peritext.deletions.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.deletions.spec.ts @@ -11,7 +11,7 @@ const run = (setup: () => Kit) => { peritext.delAt(4, 2); expect(peritext.str.view()).toBe('dghx'); }); - + test('deletes slice if it is contained in deletion range', () => { const {peritext, editor} = setup(); editor.cursor.setAt(2, 2); @@ -23,7 +23,7 @@ const run = (setup: () => Kit) => { peritext.refresh(); expect(editor.saved.slices.size()).toBe(0); }); - + test('does not delete slice if it is only partially contained', () => { const {peritext, editor} = setup(); editor.cursor.setAt(2, 10); @@ -39,6 +39,54 @@ const run = (setup: () => Kit) => { expect(peritext.extraSlices.size()).toBe(0); }); }); + + describe('.delSlices()', () => { + test('does not delete already deleted slice', () => { + const {peritext, editor, model} = setup(); + editor.cursor.setAt(15, 2); + editor.saved.insOverwrite('bold'); + editor.cursor.setAt(2, 2); + editor.saved.insOverwrite('underline'); + editor.cursor.setAt(0); + peritext.refresh(); + const tick1 = model.tick; + expect(editor.saved.slices.size()).toBe(2); + const range = peritext.rangeAt(1, 5); + const deleted = peritext.delSlices(range); + const tick2 = model.tick; + expect(deleted).toBe(true); + expect(tick2).not.toBe(tick1); + peritext.refresh(); + const deleted2 = peritext.delSlices(range); + const tick3 = model.tick; + expect(editor.saved.slices.size()).toBe(1); + expect(deleted2).toBe(false); + expect(tick3).toBe(tick2); + }); + + test('does not attempt to delete slices in the wrong models', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(2, 2); + editor.extra.insStack('something...'); + editor.cursor.setAt(0); + peritext.refresh(); + const tick1 = peritext.savedSlices.set.doc.tick; + const tick2 = peritext.extraSlices.set.doc.tick; + const tick3 = peritext.localSlices.set.doc.tick; + expect(editor.extra.slices.size()).toBe(1); + const range = peritext.rangeAt(1, 5); + const deleted = peritext.delSlices(range); + peritext.refresh(); + const tick4 = peritext.savedSlices.set.doc.tick; + const tick5 = peritext.extraSlices.set.doc.tick; + const tick6 = peritext.localSlices.set.doc.tick; + expect(deleted).toBe(true); + expect(editor.extra.slices.size()).toBe(0); + expect(tick1 === tick4).toBe(true); + expect(tick2 < tick5).toBe(true); + expect(tick3 === tick6).toBe(true); + }); + }); }; describe('basic alphabet', () => run(setupAlphabetKit)); diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index a239d83813..ecbcffefb3 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -8,6 +8,8 @@ import {CONST, updateNum} from '../../../json-hash'; import {SliceBehavior, SliceHeaderShift, SliceTupleIndex} from './constants'; import {MarkerSlice} from './MarkerSlice'; import {VecNode} from '../../../json-crdt/nodes'; +import {Chars} from '../constants'; +import {Anchor} from '../rga/constants'; import type {Slice, SliceType} from './types'; import type {ITimespanStruct, ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {Stateful} from '../types'; @@ -15,8 +17,6 @@ import type {Printable} from 'tree-dump/lib/types'; import type {ArrChunk, ArrNode} from '../../../json-crdt/nodes'; import type {AbstractRga} from '../../../json-crdt/nodes/rga'; import type {Peritext} from '../Peritext'; -import {Chars} from '../constants'; -import {Anchor} from '../rga/constants'; export class Slices implements Stateful, Printable { private list = new AvlMap>(compare); @@ -141,18 +141,22 @@ export class Slices implements Stateful, Printable { api.apply(); } - public delSlices(slices: Iterable>): void { - const api = this.set.doc.api; + public delSlices(slices: Iterable>): boolean { + const set = this.set; + const doc = set.doc; + const api = doc.api; const spans: ITimespanStruct[] = []; for (const slice of slices) { if (slice instanceof PersistedSlice) { const id = slice.id; + if (!set.findById(id)) continue; spans.push(new Timespan(id.sid, id.time, 1)); } } - if (!spans.length) return; + if (!spans.length) return false; api.builder.del(this.set.id, spans); api.apply(); + return true; } public size(): number { From f476f98ea1a0d0244cba265f934b4c481de67a6c Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 29 Jun 2024 14:21:23 +0200 Subject: [PATCH 04/10] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20add=20block=20hash=20recalculation=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/Peritext.hashes.spec.ts | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/json-crdt-extensions/peritext/__tests__/Peritext.hashes.spec.ts diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.hashes.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.hashes.spec.ts new file mode 100644 index 0000000000..76e78d467d --- /dev/null +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.hashes.spec.ts @@ -0,0 +1,116 @@ +import {Model} from '../../../json-crdt/model'; +import {Peritext} from '../Peritext'; +import {Editor} from '../editor/Editor'; +import {render} from './render'; + +const setup = (insertNumbers = (editor: Editor) => editor.insert('Hello world!'), sid?: number) => { + const model = Model.withLogicalClock(sid); + model.api.root({ + text: '', + slices: [], + }); + const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node); + const editor = peritext.editor; + insertNumbers(editor); + const view = () => { + peritext.refresh(); + return render(peritext.blocks.root, '', true); + }; + // console.log(peritext.str + ''); + return {model, peritext, editor, view}; +}; + +test('updates block hash on text input', () => { + const {editor, peritext} = setup(); + // const {editor, peritext} = setup(undefined, 6015966450700167); + let prevHash = 0; + let hash = 0; + editor.cursor.setAt(2); + editor.insert('1'); + peritext.refresh(); + hash = peritext.blocks.root.children[0].hash; + expect(hash).not.toBe(prevHash); + prevHash = hash; + editor.cursor.setAt(1); + editor.insert('2'); + editor.insert('2'); + editor.insert('2'); + editor.insert('2'); + peritext.refresh(); + hash = peritext.blocks.root.children[0].hash; + expect(hash).not.toBe(prevHash); + prevHash = hash; + editor.cursor.setAt(2); + editor.insert('3'); + peritext.refresh(); + hash = peritext.blocks.root.children[0].hash; + expect(hash).not.toBe(prevHash); + prevHash = hash; + editor.insert('3'); + peritext.refresh(); + hash = peritext.blocks.root.children[0].hash; + expect(hash).not.toBe(prevHash); + prevHash = hash; + editor.insert('3'); + peritext.refresh(); + hash = peritext.blocks.root.children[0].hash; + expect(hash).not.toBe(prevHash); + prevHash = hash; + editor.insert('3'); + peritext.refresh(); + hash = peritext.blocks.root.children[0].hash; + expect(hash).not.toBe(prevHash); + prevHash = hash; + editor.insert('3'); + peritext.refresh(); + hash = peritext.blocks.root.children[0].hash; + expect(hash).not.toBe(prevHash); + prevHash = hash; + editor.insert('3'); + peritext.refresh(); + hash = peritext.blocks.root.children[0].hash; + expect(hash).not.toBe(prevHash); + prevHash = hash; + editor.insert('3'); + peritext.refresh(); + hash = peritext.blocks.root.children[0].hash; + expect(hash).not.toBe(prevHash); + prevHash = hash; + editor.insert('3'); + peritext.refresh(); + hash = peritext.blocks.root.children[0].hash; + expect(hash).not.toBe(prevHash); + prevHash = hash; + editor.insert('3'); + peritext.refresh(); + hash = peritext.blocks.root.children[0].hash; + expect(hash).not.toBe(prevHash); + prevHash = hash; +}); + +test('updates block hash when moving across deleted char', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(5); + editor.insert('1'); + peritext.refresh(); + const hash1 = peritext.blocks.root.children[0].hash; + editor.cursor.move(-1); + peritext.refresh(); + const hash2 = peritext.blocks.root.children[0].hash; + expect(hash1).not.toBe(hash2); +}); + +test('updates block after moving over new block boundary', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(6); + editor.saved.insMarker(['p'], '\n'); + peritext.refresh(); + const hash11 = peritext.blocks.root.children[0].hash; + const hash12 = peritext.blocks.root.children[1].hash; + editor.cursor.move(1); + peritext.refresh(); + const hash21 = peritext.blocks.root.children[0].hash; + const hash22 = peritext.blocks.root.children[1].hash; + expect(hash11).not.toBe(hash21); + expect(hash12).not.toBe(hash22); +}); From 3367392abb2675f0077eff4b67432b970e4c875a Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 29 Jun 2024 14:31:53 +0200 Subject: [PATCH 05/10] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20add=20basic=20text=20insertion=20smoke=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/__tests__/Peritext.insetions.spec.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/json-crdt-extensions/peritext/__tests__/Peritext.insetions.spec.ts diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.insetions.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.insetions.spec.ts new file mode 100644 index 0000000000..f95e6bd094 --- /dev/null +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.insetions.spec.ts @@ -0,0 +1,11 @@ +import {setupKit} from './setup'; + +describe('.insAt()', () => { + test('can insert text', () => { + const {peritext} = setupKit(); + peritext.insAt(0, 'ac'); + peritext.insAt(1, 'b'); + peritext.refresh(); + expect(peritext.str.view()).toBe('abc'); + }); +}); From 2d97f5b58670a5cf482731878dc123389762c317 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 29 Jun 2024 14:33:25 +0200 Subject: [PATCH 06/10] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20add=20movement=20across=20block=20boundary=20tes?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/__tests__/Peritext.movement.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/json-crdt-extensions/peritext/__tests__/Peritext.movement.spec.ts diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.movement.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.movement.spec.ts new file mode 100644 index 0000000000..9a133ff193 --- /dev/null +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.movement.spec.ts @@ -0,0 +1,16 @@ +import {setupKit} from './setup'; + +test('can move back across single char block marker', () => { + const {editor, peritext} = setupKit(); + editor.insert('ab'); + editor.cursor.setAt(1); + editor.saved.insMarker(['p'], '\n'); + peritext.refresh(); + expect(peritext.blocks.root.children.length).toBe(2); + editor.cursor.setAt(2); + peritext.refresh(); + expect(peritext.blocks.root.children.length).toBe(2); + editor.cursor.move(-1); + peritext.refresh(); + expect(peritext.blocks.root.children.length).toBe(2); +}); From fdd47339551ae5887d3629ad349cb96c863d1bd0 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 29 Jun 2024 14:41:54 +0200 Subject: [PATCH 07/10] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20add=20cursor=20movement=20and=20rendering=20test?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Peritext.render-cursor-movement.spec.ts | 300 ++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 src/json-crdt-extensions/peritext/__tests__/Peritext.render-cursor-movement.spec.ts diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.render-cursor-movement.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.render-cursor-movement.spec.ts new file mode 100644 index 0000000000..d2b7a224a7 --- /dev/null +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.render-cursor-movement.spec.ts @@ -0,0 +1,300 @@ +import {Model} from '../../../json-crdt/model'; +import {Peritext} from '../Peritext'; +import {Editor} from '../editor/Editor'; +import {Anchor} from '../rga/constants'; +import {render} from './render'; + +const runInlineSlicesTests = ( + desc: string, + insertNumbers = (editor: Editor) => editor.insert('abcdefghijklmnopqrstuvwxyz'), +) => { + const setup = () => { + const model = Model.withLogicalClock(123); + model.api.root({ + text: '', + slices: [], + }); + const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node); + const editor = peritext.editor; + insertNumbers(editor); + const view = () => { + peritext.refresh(); + return render(peritext.blocks.root, '', false); + }; + // console.log(peritext.str + ''); + return {model, peritext, editor, view}; + }; + + describe(desc, () => { + test('can move cursor forward - starting from middle', () => { + const {editor, peritext, view} = setup(); + editor.cursor.setAt(1); + peritext.refresh(); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "a" { } + "bcdefghijklmnopqrstuvwxyz" { -1 = [ [ [ 0, !u ] ], 4 ] } +" +`); + editor.cursor.move(1); + peritext.refresh(); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "ab" { } + "cdefghijklmnopqrstuvwxyz" { -1 = [ [ [ 0, !u ] ], 4 ] } +" +`); + editor.cursor.move(2); + peritext.refresh(); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "abcd" { } + "efghijklmnopqrstuvwxyz" { -1 = [ [ [ 0, !u ] ], 4 ] } +" +`); + }); + + test('can move cursor forward - starting the beginning of the string', () => { + const {editor, peritext, view} = setup(); + editor.cursor.setAt(0); + peritext.refresh(); + // console.log(view()); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "abcdefghijklmnopqrstuvwxyz" { -1 = [ [ [ 0, !u ] ], 4 ] } +" +`); + editor.cursor.move(1); + peritext.refresh(); + // console.log(view()); + // console.log(peritext + ''); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "a" { } + "bcdefghijklmnopqrstuvwxyz" { -1 = [ [ [ 0, !u ] ], 4 ] } +" +`); + editor.cursor.move(2); + peritext.refresh(); + // console.log(view()); + expect(view()).toMatchInlineSnapshot(` +"<> + <0> + "abc" { } + "defghijklmnopqrstuvwxyz" { -1 = [ [ [ 0, !u ] ], 4 ] } +" +`); + }); + + test('can move cursor backward - starting from middle', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(5); + peritext.refresh(); + expect(editor.cursor.start.anchor).toBe(Anchor.After); + expect(editor.cursor.start.pos()).toEqual(4); + editor.cursor.move(-1); + peritext.refresh(); + expect(editor.cursor.start.pos()).toEqual(3); + editor.cursor.move(-2); + peritext.refresh(); + expect(editor.cursor.start.pos()).toEqual(1); + editor.cursor.move(-1); + peritext.refresh(); + expect(editor.cursor.start.pos()).toEqual(0); + }); + + test('can move cursor backward - starting from end of string', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(26); + peritext.refresh(); + expect(editor.cursor.start.anchor).toBe(Anchor.After); + expect(editor.cursor.start.pos()).toEqual(25); + editor.cursor.move(-1); + peritext.refresh(); + expect(editor.cursor.start.pos()).toEqual(24); + editor.cursor.move(-2); + peritext.refresh(); + expect(editor.cursor.start.pos()).toEqual(22); + }); + + test('can move cursor backward - until the beginning of the string', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(5); + peritext.refresh(); + expect(editor.cursor.start.anchor).toBe(Anchor.After); + expect(editor.cursor.start.pos()).toEqual(4); + editor.cursor.move(-2); + peritext.refresh(); + expect(editor.cursor.start.pos()).toEqual(2); + editor.cursor.move(-3); + peritext.refresh(); + expect(editor.cursor.start.pos()).toEqual(-1); + }); + + test('can move cursor backward - more than there is space', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(5); + peritext.refresh(); + expect(editor.cursor.start.anchor).toBe(Anchor.After); + expect(editor.cursor.start.pos()).toEqual(4); + editor.cursor.move(-111111); + peritext.refresh(); + expect(editor.cursor.start.pos()).toEqual(-1); + }); + }); +}; + +runInlineSlicesTests('single text chunk'); + +runInlineSlicesTests('two text chunks', (editor: Editor) => { + editor.insert('lmnopqrstuvwxyz'); + editor.cursor.setAt(0); + editor.insert('abcdefghijk'); +}); + +runInlineSlicesTests('text with block split', (editor: Editor) => { + editor.insert('lmnwxyz'); + editor.cursor.setAt(3); + editor.insert('opqrstuv'); + editor.cursor.setAt(0); + editor.insert('abcdefghijk'); +}); + +runInlineSlicesTests('text with deletes', (editor: Editor) => { + editor.insert('lmXXXnwYxyz'); + editor.cursor.setAt(2, 3); + editor.delBwd(); + editor.cursor.setAt(3); + editor.insert('opqrstuv'); + editor.cursor.setAt(12, 1); + editor.delBwd(); + editor.cursor.setAt(0); + editor.insert('ab1c3defghijk4444'); + editor.cursor.setAt(2, 1); + editor.delBwd(); + editor.cursor.setAt(3, 1); + editor.delBwd(); + editor.cursor.setAt(11, 4); + editor.delBwd(); +}); + +runInlineSlicesTests('written in reverse', (editor: Editor) => { + editor.insert('z'); + editor.cursor.setAt(0); + editor.insert('y'); + editor.cursor.setAt(0); + editor.insert('x'); + editor.cursor.setAt(0); + editor.insert('w'); + editor.cursor.setAt(0); + editor.insert('v'); + editor.cursor.setAt(0); + editor.insert('u'); + editor.cursor.setAt(0); + editor.insert('t'); + editor.cursor.setAt(0); + editor.insert('s'); + editor.cursor.setAt(0); + editor.insert('r'); + editor.cursor.setAt(0); + editor.insert('q'); + editor.cursor.setAt(0); + editor.insert('p'); + editor.cursor.setAt(0); + editor.insert('o'); + editor.cursor.setAt(0); + editor.insert('n'); + editor.cursor.setAt(0); + editor.insert('m'); + editor.cursor.setAt(0); + editor.insert('l'); + editor.cursor.setAt(0); + editor.insert('k'); + editor.cursor.setAt(0); + editor.insert('j'); + editor.cursor.setAt(0); + editor.insert('i'); + editor.cursor.setAt(0); + editor.insert('h'); + editor.cursor.setAt(0); + editor.insert('g'); + editor.cursor.setAt(0); + editor.insert('f'); + editor.cursor.setAt(0); + editor.insert('e'); + editor.cursor.setAt(0); + editor.insert('d'); + editor.cursor.setAt(0); + editor.insert('c'); + editor.cursor.setAt(0); + editor.insert('b'); + editor.cursor.setAt(0); + editor.insert('a'); +}); + +runInlineSlicesTests('written in reverse with deletes', (editor: Editor) => { + editor.insert('z'); + editor.cursor.setAt(0); + editor.insert('y'); + editor.cursor.setAt(0); + editor.insert('x'); + editor.cursor.setAt(0); + editor.insert('w'); + editor.cursor.setAt(0); + editor.insert('v'); + editor.cursor.setAt(0); + editor.insert('u'); + editor.cursor.setAt(0); + editor.insert('t'); + editor.cursor.setAt(0); + editor.insert('s'); + editor.cursor.setAt(0); + editor.insert('r'); + editor.cursor.setAt(0); + editor.insert('q'); + editor.cursor.setAt(0); + editor.insert('p'); + editor.cursor.setAt(0); + editor.insert('o'); + editor.cursor.setAt(0); + editor.insert('n'); + editor.cursor.setAt(0); + editor.insert('m'); + editor.cursor.setAt(0); + editor.insert('l'); + editor.cursor.setAt(0); + editor.insert('k'); + editor.cursor.setAt(0); + editor.insert('j'); + editor.cursor.setAt(0); + editor.insert('i'); + editor.cursor.setAt(0); + editor.insert('h'); + editor.cursor.setAt(0); + editor.insert('g'); + editor.cursor.setAt(0); + editor.insert('f'); + editor.cursor.setAt(0); + editor.insert('e'); + editor.cursor.setAt(0); + editor.insert('d'); + editor.cursor.setAt(0); + editor.insert('c'); + editor.cursor.setAt(0); + editor.insert('b'); + editor.cursor.setAt(0); + editor.insert('a'); + editor.cursor.setAt(0); + editor.insert('123'); + editor.cursor.setAt(0, 3); + editor.delBwd(); + editor.cursor.setAt(3); + editor.insert('1'); + editor.cursor.setAt(3, 1); + editor.delBwd(); +}); From 703d3b8ae607726c511e329104b9a25425f58cb1 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 29 Jun 2024 15:35:21 +0200 Subject: [PATCH 08/10] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20standartize=20reverse=20alphabet=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Peritext.render-cursor-movement.spec.ts | 176 ++---------------- .../peritext/__tests__/setup.ts | 66 ++++++- 2 files changed, 74 insertions(+), 168 deletions(-) diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.render-cursor-movement.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.render-cursor-movement.spec.ts index d2b7a224a7..241681adcc 100644 --- a/src/json-crdt-extensions/peritext/__tests__/Peritext.render-cursor-movement.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.render-cursor-movement.spec.ts @@ -1,28 +1,19 @@ -import {Model} from '../../../json-crdt/model'; -import {Peritext} from '../Peritext'; -import {Editor} from '../editor/Editor'; import {Anchor} from '../rga/constants'; import {render} from './render'; +import {Kit, setupAlphabetChunkSplitKit, setupAlphabetKit, setupAlphabetWithDeletesKit, setupAlphabetWithTwoChunksKit, setupAlphabetWrittenInReverse, setupAlphabetWrittenInReverseWithDeletes} from './setup'; const runInlineSlicesTests = ( desc: string, - insertNumbers = (editor: Editor) => editor.insert('abcdefghijklmnopqrstuvwxyz'), + getKit: () => Kit, ) => { const setup = () => { - const model = Model.withLogicalClock(123); - model.api.root({ - text: '', - slices: [], - }); - const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node); - const editor = peritext.editor; - insertNumbers(editor); + const kit = getKit(); const view = () => { - peritext.refresh(); - return render(peritext.blocks.root, '', false); + kit.peritext.refresh(); + return render(kit.peritext.blocks.root, '', false); }; // console.log(peritext.str + ''); - return {model, peritext, editor, view}; + return {...kit, view}; }; describe(desc, () => { @@ -149,152 +140,9 @@ const runInlineSlicesTests = ( }); }; -runInlineSlicesTests('single text chunk'); - -runInlineSlicesTests('two text chunks', (editor: Editor) => { - editor.insert('lmnopqrstuvwxyz'); - editor.cursor.setAt(0); - editor.insert('abcdefghijk'); -}); - -runInlineSlicesTests('text with block split', (editor: Editor) => { - editor.insert('lmnwxyz'); - editor.cursor.setAt(3); - editor.insert('opqrstuv'); - editor.cursor.setAt(0); - editor.insert('abcdefghijk'); -}); - -runInlineSlicesTests('text with deletes', (editor: Editor) => { - editor.insert('lmXXXnwYxyz'); - editor.cursor.setAt(2, 3); - editor.delBwd(); - editor.cursor.setAt(3); - editor.insert('opqrstuv'); - editor.cursor.setAt(12, 1); - editor.delBwd(); - editor.cursor.setAt(0); - editor.insert('ab1c3defghijk4444'); - editor.cursor.setAt(2, 1); - editor.delBwd(); - editor.cursor.setAt(3, 1); - editor.delBwd(); - editor.cursor.setAt(11, 4); - editor.delBwd(); -}); - -runInlineSlicesTests('written in reverse', (editor: Editor) => { - editor.insert('z'); - editor.cursor.setAt(0); - editor.insert('y'); - editor.cursor.setAt(0); - editor.insert('x'); - editor.cursor.setAt(0); - editor.insert('w'); - editor.cursor.setAt(0); - editor.insert('v'); - editor.cursor.setAt(0); - editor.insert('u'); - editor.cursor.setAt(0); - editor.insert('t'); - editor.cursor.setAt(0); - editor.insert('s'); - editor.cursor.setAt(0); - editor.insert('r'); - editor.cursor.setAt(0); - editor.insert('q'); - editor.cursor.setAt(0); - editor.insert('p'); - editor.cursor.setAt(0); - editor.insert('o'); - editor.cursor.setAt(0); - editor.insert('n'); - editor.cursor.setAt(0); - editor.insert('m'); - editor.cursor.setAt(0); - editor.insert('l'); - editor.cursor.setAt(0); - editor.insert('k'); - editor.cursor.setAt(0); - editor.insert('j'); - editor.cursor.setAt(0); - editor.insert('i'); - editor.cursor.setAt(0); - editor.insert('h'); - editor.cursor.setAt(0); - editor.insert('g'); - editor.cursor.setAt(0); - editor.insert('f'); - editor.cursor.setAt(0); - editor.insert('e'); - editor.cursor.setAt(0); - editor.insert('d'); - editor.cursor.setAt(0); - editor.insert('c'); - editor.cursor.setAt(0); - editor.insert('b'); - editor.cursor.setAt(0); - editor.insert('a'); -}); - -runInlineSlicesTests('written in reverse with deletes', (editor: Editor) => { - editor.insert('z'); - editor.cursor.setAt(0); - editor.insert('y'); - editor.cursor.setAt(0); - editor.insert('x'); - editor.cursor.setAt(0); - editor.insert('w'); - editor.cursor.setAt(0); - editor.insert('v'); - editor.cursor.setAt(0); - editor.insert('u'); - editor.cursor.setAt(0); - editor.insert('t'); - editor.cursor.setAt(0); - editor.insert('s'); - editor.cursor.setAt(0); - editor.insert('r'); - editor.cursor.setAt(0); - editor.insert('q'); - editor.cursor.setAt(0); - editor.insert('p'); - editor.cursor.setAt(0); - editor.insert('o'); - editor.cursor.setAt(0); - editor.insert('n'); - editor.cursor.setAt(0); - editor.insert('m'); - editor.cursor.setAt(0); - editor.insert('l'); - editor.cursor.setAt(0); - editor.insert('k'); - editor.cursor.setAt(0); - editor.insert('j'); - editor.cursor.setAt(0); - editor.insert('i'); - editor.cursor.setAt(0); - editor.insert('h'); - editor.cursor.setAt(0); - editor.insert('g'); - editor.cursor.setAt(0); - editor.insert('f'); - editor.cursor.setAt(0); - editor.insert('e'); - editor.cursor.setAt(0); - editor.insert('d'); - editor.cursor.setAt(0); - editor.insert('c'); - editor.cursor.setAt(0); - editor.insert('b'); - editor.cursor.setAt(0); - editor.insert('a'); - editor.cursor.setAt(0); - editor.insert('123'); - editor.cursor.setAt(0, 3); - editor.delBwd(); - editor.cursor.setAt(3); - editor.insert('1'); - editor.cursor.setAt(3, 1); - editor.delBwd(); -}); +runInlineSlicesTests('single text chunk', setupAlphabetKit); +runInlineSlicesTests('two chunks', setupAlphabetWithTwoChunksKit); +runInlineSlicesTests('with chunk split', setupAlphabetChunkSplitKit); +runInlineSlicesTests('with deletes', setupAlphabetWithDeletesKit); +runInlineSlicesTests('written in reverse', setupAlphabetWrittenInReverse); +runInlineSlicesTests('written in reverse with deletes', setupAlphabetWrittenInReverseWithDeletes); diff --git a/src/json-crdt-extensions/peritext/__tests__/setup.ts b/src/json-crdt-extensions/peritext/__tests__/setup.ts index 38180b8fd0..323c7d19a1 100644 --- a/src/json-crdt-extensions/peritext/__tests__/setup.ts +++ b/src/json-crdt-extensions/peritext/__tests__/setup.ts @@ -153,7 +153,7 @@ export const setupAlphabetKit = (): Kit => { return setupKit('', (model) => { const str = model.s.text.toExt().text(); str.ins(0, 'abcdefghijklmnopqrstuvwxyz'); - if (str.view() !== 'abcdefghijklmnopqrstuvwxyz') throw new Error('Invalid text'); + if (str.view() !== 'abcdefghijklmnopqrstuvwxyz') throw new Error('Invalid text: ' + str.view()); }); }; @@ -165,7 +165,7 @@ export const setupAlphabetWithTwoChunksKit = (): Kit => { const str = model.s.text.toExt().text(); str.ins(0, 'lmnopqrstuvwxyz'); str.ins(0, 'abcdefghijk'); - if (str.view() !== 'abcdefghijklmnopqrstuvwxyz') throw new Error('Invalid text'); + if (str.view() !== 'abcdefghijklmnopqrstuvwxyz') throw new Error('Invalid text: ' + str.view()); }); }; @@ -178,7 +178,7 @@ export const setupAlphabetChunkSplitKit = (): Kit => { str.ins(0, 'lmnwxyz'); str.ins(3, 'opqrstuv'); str.ins(0, 'abcdefghijk'); - if (str.view() !== 'abcdefghijklmnopqrstuvwxyz') throw new Error('Invalid text'); + if (str.view() !== 'abcdefghijklmnopqrstuvwxyz') throw new Error('Invalid text: ' + str.view()); }); }; @@ -196,6 +196,64 @@ export const setupAlphabetWithDeletesKit = (): Kit => { str.del(2, 1); str.del(3, 1); str.del(11, 4); - if (str.view() !== 'abcdefghijklmnopqrstuvwxyz') throw new Error('Invalid text'); + if (str.view() !== 'abcdefghijklmnopqrstuvwxyz') throw new Error('Invalid text: ' + str.view()); }); }; + +/** + * Creates a Peritext instance with text "abcdefghijklmnopqrstuvwxyz" written in + * reverse. + */ +export const setupAlphabetWrittenInReverse = (): Kit => { + return setupKit('', (model) => { + const str = model.s.text.toExt().text(); + str.ins(0, 'z'); + str.ins(0, 'y'); + str.ins(0, 'x'); + str.ins(0, 'w'); + str.ins(0, 'v'); + str.ins(0, 'u'); + str.ins(0, 't'); + str.ins(0, 's'); + str.ins(0, 'r'); + str.ins(0, 'q'); + str.ins(0, 'p'); + str.ins(0, 'o'); + str.ins(0, 'n'); + str.ins(0, 'm'); + str.ins(0, 'l'); + str.ins(0, 'k'); + str.ins(0, 'j'); + str.ins(0, 'i'); + str.ins(0, 'h'); + str.ins(0, 'g'); + str.ins(0, 'f'); + str.ins(0, 'e'); + str.ins(0, 'd'); + str.ins(0, 'c'); + str.ins(0, 'b'); + str.ins(0, 'a'); + if (str.view() !== 'abcdefghijklmnopqrstuvwxyz') throw new Error('Invalid text: ' + str.view()); + }); +}; + +/** + * Creates a Peritext instance with text "abcdefghijklmnopqrstuvwxyz" written in + * reverse and contains deletes. + */ +export const setupAlphabetWrittenInReverseWithDeletes = (): Kit => { + const kit = setupAlphabetWrittenInReverse(); + const str = kit.model.s.text.toExt().text(); + str.ins(0, '123'); + str.del(0, 3); + str.ins(3, '1'); + str.del(3, 1); + str.del(2, 2); + str.ins(2, 'cd'); + str.del(3, 5); + str.ins(3, 'defgh'); + str.del(7, 8); + str.ins(7, 'hijklmno'); + if (str.view() !== 'abcdefghijklmnopqrstuvwxyz') throw new Error('Invalid text: ' + str.view()); + return kit; +}; From f4895ffbca4a1a829c2d85f3dfb6aaf7e99b83ca Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 29 Jun 2024 15:39:08 +0200 Subject: [PATCH 09/10] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20add=20hash=20smoke=20tests=20using=20rendering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/Peritext.render-hash.spec.ts | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/json-crdt-extensions/peritext/__tests__/Peritext.render-hash.spec.ts diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.render-hash.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.render-hash.spec.ts new file mode 100644 index 0000000000..9bb42a4afa --- /dev/null +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.render-hash.spec.ts @@ -0,0 +1,74 @@ +import {render} from './render'; +import { + Kit, + setupAlphabetChunkSplitKit, + setupAlphabetKit, + setupAlphabetWithDeletesKit, + setupAlphabetWithTwoChunksKit, + setupAlphabetWrittenInReverse, + setupAlphabetWrittenInReverseWithDeletes, +} from './setup'; + +const runInlineSlicesTests = (desc: string, getKit: () => Kit) => { + const setup = () => { + const kit = getKit(); + const view = () => { + kit.peritext.refresh(); + return render(kit.peritext.blocks.root, '', false); + }; + // console.log(peritext.str + ''); + return {...kit, view}; + }; + + describe(desc, () => { + test('updates block hash only where something was changed - leading block', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(10); + editor.saved.insMarker(['p'], 'p1'); + editor.cursor.setAt(22); + editor.saved.insMarker(['p'], 'p2'); + editor.cursor.setAt(editor.txt.str.length()); + peritext.refresh(); + const rootHash1 = peritext.blocks.root.hash; + const firstBlockHash1 = peritext.blocks.root.children[0].hash; + const secondBlockHash1 = peritext.blocks.root.children[1].hash; + editor.cursor.setAt(2); + editor.insert('___'); + peritext.refresh(); + const rootHash2 = peritext.blocks.root.hash; + const firstBlockHash2 = peritext.blocks.root.children[0].hash; + const secondBlockHash2 = peritext.blocks.root.children[1].hash; + expect(rootHash1).not.toBe(rootHash2); + expect(firstBlockHash1).not.toBe(firstBlockHash2); + expect(secondBlockHash1).toBe(secondBlockHash2); + }); + + test('updates block hash only where hash has changed - middle block', () => { + const {editor, peritext} = setup(); + editor.cursor.setAt(10); + editor.saved.insMarker(['p', 'p1']); + editor.cursor.setAt(22); + editor.saved.insMarker(['p'], 'p2'); + peritext.refresh(); + const rootHash1 = peritext.blocks.root.hash; + const firstBlockHash1 = peritext.blocks.root.children[0].hash; + const secondBlockHash1 = peritext.blocks.root.children[1].hash; + editor.cursor.setAt(13); + editor.insert('___'); + peritext.refresh(); + const rootHash2 = peritext.blocks.root.hash; + const firstBlockHash2 = peritext.blocks.root.children[0].hash; + const secondBlockHash2 = peritext.blocks.root.children[1].hash; + expect(rootHash1).not.toBe(rootHash2); + expect(firstBlockHash1).toBe(firstBlockHash2); + expect(secondBlockHash1).not.toBe(secondBlockHash2); + }); + }); +}; + +runInlineSlicesTests('single text chunk', setupAlphabetKit); +runInlineSlicesTests('two chunks', setupAlphabetWithTwoChunksKit); +runInlineSlicesTests('with chunk split', setupAlphabetChunkSplitKit); +runInlineSlicesTests('with deletes', setupAlphabetWithDeletesKit); +runInlineSlicesTests('written in reverse', setupAlphabetWrittenInReverse); +runInlineSlicesTests('written in reverse with deletes', setupAlphabetWrittenInReverseWithDeletes); From 70df740544d8a87b538a51cb07558f6b0a107827 Mon Sep 17 00:00:00 2001 From: streamich Date: Sat, 29 Jun 2024 15:39:56 +0200 Subject: [PATCH 10/10] =?UTF-8?q?style:=20=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 | 6 +----- .../peritext/__tests__/Peritext.deletions.spec.ts | 12 +++++++++--- .../Peritext.render-cursor-movement.spec.ts | 15 ++++++++++----- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index b7f1575c80..1868407bcb 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -261,11 +261,7 @@ export class Peritext implements Printable { : start.anchor === Anchor.Before ? start.id : start.nextId(); - const delEndId = end.isAbsEnd() - ? this.point().refEnd().id - : end.anchor === Anchor.After - ? end.id - : end.prevId(); + const delEndId = end.isAbsEnd() ? this.point().refEnd().id : end.anchor === Anchor.After ? end.id : end.prevId(); if (!delStartId || !delEndId) throw new Error('INVALID_RANGE'); const rga = this.str; const spans = rga.findInterval2(delStartId, delEndId); diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.deletions.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.deletions.spec.ts index c45b548542..6df896d3e1 100644 --- a/src/json-crdt-extensions/peritext/__tests__/Peritext.deletions.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.deletions.spec.ts @@ -1,4 +1,10 @@ -import {setupAlphabetChunkSplitKit, setupAlphabetKit, Kit, setupAlphabetWithDeletesKit, setupAlphabetWithTwoChunksKit} from './setup'; +import { + setupAlphabetChunkSplitKit, + setupAlphabetKit, + Kit, + setupAlphabetWithDeletesKit, + setupAlphabetWithTwoChunksKit, +} from './setup'; const run = (setup: () => Kit) => { describe('.delAt()', () => { @@ -11,7 +17,7 @@ const run = (setup: () => Kit) => { peritext.delAt(4, 2); expect(peritext.str.view()).toBe('dghx'); }); - + test('deletes slice if it is contained in deletion range', () => { const {peritext, editor} = setup(); editor.cursor.setAt(2, 2); @@ -23,7 +29,7 @@ const run = (setup: () => Kit) => { peritext.refresh(); expect(editor.saved.slices.size()).toBe(0); }); - + test('does not delete slice if it is only partially contained', () => { const {peritext, editor} = setup(); editor.cursor.setAt(2, 10); diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.render-cursor-movement.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.render-cursor-movement.spec.ts index 241681adcc..fa9bb95f0d 100644 --- a/src/json-crdt-extensions/peritext/__tests__/Peritext.render-cursor-movement.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.render-cursor-movement.spec.ts @@ -1,11 +1,16 @@ import {Anchor} from '../rga/constants'; import {render} from './render'; -import {Kit, setupAlphabetChunkSplitKit, setupAlphabetKit, setupAlphabetWithDeletesKit, setupAlphabetWithTwoChunksKit, setupAlphabetWrittenInReverse, setupAlphabetWrittenInReverseWithDeletes} from './setup'; +import { + Kit, + setupAlphabetChunkSplitKit, + setupAlphabetKit, + setupAlphabetWithDeletesKit, + setupAlphabetWithTwoChunksKit, + setupAlphabetWrittenInReverse, + setupAlphabetWrittenInReverseWithDeletes, +} from './setup'; -const runInlineSlicesTests = ( - desc: string, - getKit: () => Kit, -) => { +const runInlineSlicesTests = (desc: string, getKit: () => Kit) => { const setup = () => { const kit = getKit(); const view = () => {