From c6614d9b337c5870cafa02aa8784b2fd145fdd0f Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 8 May 2024 18:23:44 +0200 Subject: [PATCH 01/10] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20add=20.findContained()=20and=20.findOverlapping(?= =?UTF-8?q?)=20overlay=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/MarkerOverlayPoint.ts | 2 +- .../peritext/overlay/Overlay.ts | 19 ++- .../overlay/__tests__/Overlay.findX.spec.ts | 141 ++++++++++++++++++ .../overlay/__tests__/Overlay.tuples.spec.ts | 4 +- 4 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.findX.spec.ts diff --git a/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts b/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts index 3b40ea91b3..81983b04d4 100644 --- a/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts +++ b/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts @@ -8,7 +8,7 @@ import type {MarkerSlice} from '../slice/MarkerSlice'; export class MarkerOverlayPoint extends OverlayPoint { /** - * Hash value of the preceding text contents, up until the next marker. + * Hash value of the following text contents, up until the next marker. */ public textHash: number = 0; diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 016c6e7896..f9e8f4ab65 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -118,7 +118,8 @@ export class Overlay implements Printable, Stateful { } return; } - + + /** @todo Rename to `chunks()`. */ public chunkSlices0( chunk: Chunk | undefined, p1: Point, @@ -245,6 +246,14 @@ export class Overlay implements Printable, Stateful { return new UndefEndIter(this.tuples0(after)); } + /** + * Finds all slices that are contained within the given range. A slice is + * considered contained if its start and end points are within the range, + * inclusive (uses {@link Range#contains} method to check containment). + * + * @param range The range to search for contained slices. + * @returns A set of slices that are contained within the given range. + */ public findContained(range: Range): Set> { const result = new Set>(); let point = this.getOrNextLower(range.start); @@ -265,6 +274,14 @@ export class Overlay implements Printable, Stateful { return result; } + /** + * Finds all slices that overlap with the given range. A slice is considered + * overlapping if its start or end point is within the range, inclusive + * (uses {@link Range#containsPoint} method to check overlap). + * + * @param range The range to search for overlapping slices. + * @returns A set of slices that overlap with the given range. + */ public findOverlapping(range: Range): Set> { const result = new Set>(); let point = this.getOrNextLower(range.start); diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.findX.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.findX.spec.ts new file mode 100644 index 0000000000..1e9678fa37 --- /dev/null +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.findX.spec.ts @@ -0,0 +1,141 @@ +import {Kit, setupHelloWorldKit, setupHelloWorldWithFewEditsKit} from '../../__tests__/setup'; + +const runFindContainedTests = (setup: () => Kit) => { + describe('.findContained()', () => { + test('returns empty set by default', () => { + const {peritext} = setup(); + peritext.overlay.refresh(); + const slices = peritext.overlay.findContained(peritext.rangeAt(3, 4)); + expect(slices.size).toBe(0); + }); + + test('returns a single contained slice', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(3, 2); + editor.saved.insStack('em'); + editor.cursor.setAt(0); + peritext.overlay.refresh(); + const slices = peritext.overlay.findContained(peritext.rangeAt(3, 4)); + expect(slices.size).toBe(1); + }); + + test('returns two contained slice', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(3, 1); + editor.saved.insStack('em'); + editor.cursor.setAt(5, 2); + editor.saved.insStack('bold'); + editor.cursor.setAt(0); + peritext.overlay.refresh(); + const slices = peritext.overlay.findContained(peritext.rangeAt(2, 8)); + expect(slices.size).toBe(2); + }); + + test('does not return overlapping slice', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(3, 4); + editor.saved.insStack('em'); + editor.cursor.setAt(5, 2); + editor.saved.insStack('bold'); + editor.cursor.setAt(0); + peritext.overlay.refresh(); + const slices = peritext.overlay.findContained(peritext.rangeAt(4, 8)); + expect(slices.size).toBe(1); + }); + + test('returns split blocks', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(3, 4); + editor.saved.insStack('em'); + editor.cursor.setAt(5, 2); + editor.saved.insStack('bold'); + editor.cursor.setAt(8); + editor.saved.insMarker('p'); + editor.cursor.setAt(0); + peritext.overlay.refresh(); + const slices = peritext.overlay.findContained(peritext.rangeAt(4, 8)); + expect(slices.size).toBe(2); + }); + }); +}; + +const runFindOverlappingTests = (setup: () => Kit) => { + describe('.findOverlapping()', () => { + test('returns empty set by default', () => { + const {peritext} = setup(); + peritext.overlay.refresh(); + const slices = peritext.overlay.findOverlapping(peritext.rangeAt(3, 4)); + expect(slices.size).toBe(0); + }); + + test('returns a single contained slice', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(3, 2); + editor.saved.insStack('em'); + editor.cursor.setAt(0); + peritext.overlay.refresh(); + const slices = peritext.overlay.findOverlapping(peritext.rangeAt(3, 4)); + expect(slices.size).toBe(1); + }); + + test('returns two contained slice', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(3, 1); + editor.saved.insStack('em'); + editor.cursor.setAt(5, 2); + editor.saved.insStack('bold'); + editor.cursor.setAt(0); + peritext.overlay.refresh(); + const slices = peritext.overlay.findOverlapping(peritext.rangeAt(2, 8)); + expect(slices.size).toBe(2); + }); + + test('returns overlapping slices', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(3, 4); + editor.saved.insStack('em'); + editor.cursor.setAt(5, 2); + editor.saved.insStack('bold'); + editor.cursor.setAt(0); + peritext.overlay.refresh(); + const slices = peritext.overlay.findOverlapping(peritext.rangeAt(4, 8)); + expect(slices.size).toBe(2); + }); + + test('returns overlapping slices from both ends', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(3, 2); + editor.saved.insStack('em'); + editor.cursor.setAt(8, 2); + editor.saved.insStack('bold'); + editor.cursor.setAt(0); + peritext.overlay.refresh(); + const slices = peritext.overlay.findOverlapping(peritext.rangeAt(4, 5)); + expect(slices.size).toBe(2); + }); + + test('returns split blocks', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(3, 4); + editor.saved.insStack('em'); + editor.cursor.setAt(5, 2); + editor.saved.insStack('bold'); + editor.cursor.setAt(8); + editor.saved.insMarker('p'); + editor.cursor.setAt(0); + peritext.overlay.refresh(); + const slices = peritext.overlay.findOverlapping(peritext.rangeAt(4, 8)); + expect(slices.size).toBe(3); + }); + }); +}; + +describe('text "hello world", no edits', () => { + runFindContainedTests(setupHelloWorldKit); + runFindOverlappingTests(setupHelloWorldKit); +}); + +describe('text "hello world", with few edits', () => { + runFindContainedTests(setupHelloWorldWithFewEditsKit); + runFindOverlappingTests(setupHelloWorldWithFewEditsKit); +}); diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.tuples.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.tuples.spec.ts index 6174e7a26a..e61c3db95f 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.tuples.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.tuples.spec.ts @@ -152,10 +152,10 @@ const runPairsTests = (setup: () => Kit) => { }); }; -describe('numbers "hello world", no edits', () => { +describe('text "hello world", no edits', () => { runPairsTests(setupHelloWorldKit); }); -describe('numbers "hello world", with default schema and tombstones', () => { +describe('text "hello world", with few edits', () => { runPairsTests(setupHelloWorldWithFewEditsKit); }); From 6fa5d41cb3569c0e50b2ef14b24af57b4488becc Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 8 May 2024 18:41:46 +0200 Subject: [PATCH 02/10] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20add=20Overlay.find()=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/Overlay.ts | 27 ++++++--- .../overlay/__tests__/Overlay.findX.spec.ts | 58 +++++++++++++++++-- 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index f9e8f4ab65..dfc4fb79c7 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -109,15 +109,6 @@ export class Overlay implements Printable, Stateful { } return result; } - - public find(predicate: (point: OverlayPoint) => boolean): OverlayPoint | undefined { - let point = this.first(); - while (point) { - if (predicate(point)) return point; - point = next(point); - } - return; - } /** @todo Rename to `chunks()`. */ public chunkSlices0( @@ -246,6 +237,24 @@ export class Overlay implements Printable, Stateful { return new UndefEndIter(this.tuples0(after)); } + + /** + * Finds the first point that satisfies the given predicate function. + * + * @param predicate Predicate function to find the point, returns true if the + * point is found. + * @returns The first point that satisfies the predicate, or undefined if no + * point is found. + */ + public find(predicate: (point: OverlayPoint) => boolean): OverlayPoint | undefined { + let point = this.first(); + while (point) { + if (predicate(point)) return point; + point = next(point); + } + return; + } + /** * Finds all slices that are contained within the given range. A slice is * considered contained if its start and end points are within the range, diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.findX.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.findX.spec.ts index 1e9678fa37..3ca529e648 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.findX.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.findX.spec.ts @@ -1,4 +1,50 @@ import {Kit, setupHelloWorldKit, setupHelloWorldWithFewEditsKit} from '../../__tests__/setup'; +import {Cursor} from '../../editor/Cursor'; +import {OverlayRefSliceEnd} from '../refs'; + +const runFind = (setup: () => Kit) => { + describe('.find()', () => { + test('can find nothing', () => { + const {peritext} = setup(); + peritext.overlay.refresh(); + const point = peritext.overlay.find(() => false); + expect(point).toBe(undefined); + }); + + test('can find a single caret cursor', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3); + peritext.overlay.refresh(); + const point = peritext.overlay.find((point) => { + return point.markers[0] instanceof Cursor; + })!; + expect(point.markers[0]).toBe(peritext.editor.cursor); + }); + + test('can find the cursor by selection start', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3, 3); + peritext.overlay.refresh(); + const point = peritext.overlay.find((point) => { + return point.layers[0] === peritext.editor.cursor; + })!; + expect(point.layers[0]).toBe(peritext.editor.cursor); + }); + + test('can find the cursor by selection end', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3, 3); + peritext.overlay.refresh(); + const point = peritext.overlay.find((point) => { + if (point.refs[0] instanceof OverlayRefSliceEnd) { + return point.refs[0].slice === peritext.editor.cursor; + } + return false; + })!; + expect((point.refs[0] as OverlayRefSliceEnd).slice).toBe(peritext.editor.cursor); + }); + }); +}; const runFindContainedTests = (setup: () => Kit) => { describe('.findContained()', () => { @@ -130,12 +176,16 @@ const runFindOverlappingTests = (setup: () => Kit) => { }); }; +const runTestSuite = (setup: () => Kit) => { + runFind(setup); + runFindContainedTests(setup); + runFindOverlappingTests(setup); +}; + describe('text "hello world", no edits', () => { - runFindContainedTests(setupHelloWorldKit); - runFindOverlappingTests(setupHelloWorldKit); + runTestSuite(setupHelloWorldKit); }); describe('text "hello world", with few edits', () => { - runFindContainedTests(setupHelloWorldWithFewEditsKit); - runFindOverlappingTests(setupHelloWorldWithFewEditsKit); + runTestSuite(setupHelloWorldWithFewEditsKit); }); From d64b8ab977bcd42b07da02471d5b554cde5d8703 Mon Sep 17 00:00:00 2001 From: streamich Date: Wed, 8 May 2024 19:01:13 +0200 Subject: [PATCH 03/10] =?UTF-8?q?fix(json-crdt-extensions):=20=F0=9F=90=9B?= =?UTF-8?q?=20recompute=20different=20`Overlay`=20state=20hash=20when=20te?= =?UTF-8?q?xt=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/Overlay.ts | 19 +++++++++++----- .../overlay/__tests__/Overlay.refresh.spec.ts | 22 ++++++++++++++++++- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index dfc4fb79c7..5a763ac716 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -307,7 +307,13 @@ export class Overlay implements Printable, Stateful { return result; } - public isBlockSplit(id: ITimestampStruct): boolean { + /** + * Returns `true` if the current character is a marker sentinel. + * + * @param id ID of the point to check. + * @returns Whether the point is a marker point. + */ + public isMarker(id: ITimestampStruct): boolean { const point = this.txt.point(id, Anchor.Before); const overlayPoint = this.getOrNextLower(point); return ( @@ -325,7 +331,7 @@ export class Overlay implements Printable, Stateful { hash = this.refreshSlices(hash, txt.savedSlices); hash = this.refreshSlices(hash, txt.extraSlices); hash = this.refreshSlices(hash, txt.localSlices); - if (!slicesOnly) this.computeSplitTextHashes(); + if (!slicesOnly) hash = this.computeSplitTextHashes(hash); return (this.hash = hash); } @@ -455,15 +461,15 @@ export class Overlay implements Printable, Stateful { public leadingTextHash: number = 0; - protected computeSplitTextHashes(): void { + protected computeSplitTextHashes(stateTotal: number): number { const txt = this.txt; const str = txt.str; const firstChunk = str.first(); - if (!firstChunk) return; + if (!firstChunk) return stateTotal; let chunk: Chunk | undefined = firstChunk; let marker: MarkerOverlayPoint | undefined = undefined; - let state: number = CONST.START_STATE; const i = this.tuples0(undefined); + let state: number = CONST.START_STATE; for (let pair = i(); pair; pair = i()) { const [p1, p2] = pair; // TODO: need to incorporate slice attribute hash here? @@ -478,6 +484,7 @@ export class Overlay implements Printable, Stateful { state = updateNum(state, overlayPointHash); if (p1) { p1.hash = overlayPointHash; + stateTotal = updateNum(stateTotal, overlayPointHash); } if (p2 instanceof MarkerOverlayPoint) { if (marker) { @@ -485,6 +492,7 @@ export class Overlay implements Printable, Stateful { } else { this.leadingTextHash = state; } + stateTotal = updateNum(stateTotal, state); state = CONST.START_STATE; marker = p2; } @@ -494,6 +502,7 @@ export class Overlay implements Printable, Stateful { } else { this.leadingTextHash = state; } + return stateTotal; } // ---------------------------------------------------------------- Printable 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 8b5fcbb122..539252b059 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 @@ -266,7 +266,7 @@ describe('Overlay.refresh()', () => { }); }); - describe('cursor', () => { + describe('local slices - cursor', () => { describe('updates hash', () => { testRefresh('when cursor char ID changes', (kit, refresh) => { kit.peritext.editor.cursor.setAt(1); @@ -302,4 +302,24 @@ describe('Overlay.refresh()', () => { }); }); }); + + describe('text contents', () => { + describe('updates hash', () => { + testRefresh('when the first character is deleted and reinserted', (kit, refresh) => { + const index = 0; + const char = kit.peritext.strApi().view()[index]; + refresh(); + kit.peritext.strApi().del(index, 1); + kit.peritext.strApi().ins(index, char); + }); + + testRefresh('when the last character is deleted and reinserted', (kit, refresh) => { + const index = kit.peritext.strApi().view().length - 1; + const char = kit.peritext.strApi().view()[index]; + refresh(); + kit.peritext.strApi().del(index, 1); + kit.peritext.strApi().ins(index, char); + }); + }); + }); }); From ed6ce960851bc8f57f3135c3b2329656ff13a56c Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Thu, 9 May 2024 00:18:41 +0200 Subject: [PATCH 04/10] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20improve=20text=20hash=20calculation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/Overlay.ts | 16 +++++++------ .../overlay/__tests__/Overlay.refresh.spec.ts | 23 ++++++++++++++++--- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 5a763ac716..44f4a0043e 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -331,7 +331,12 @@ export class Overlay implements Printable, Stateful { hash = this.refreshSlices(hash, txt.savedSlices); hash = this.refreshSlices(hash, txt.extraSlices); hash = this.refreshSlices(hash, txt.localSlices); - if (!slicesOnly) hash = this.computeSplitTextHashes(hash); + + // TODO: Move test hash calculation out of the overlay. + if (!slicesOnly) { + // hash = updateRga(hash, txt.str); + hash = this.refreshTextSlices(hash); + } return (this.hash = hash); } @@ -461,7 +466,7 @@ export class Overlay implements Printable, Stateful { public leadingTextHash: number = 0; - protected computeSplitTextHashes(stateTotal: number): number { + protected refreshTextSlices(stateTotal: number): number { const txt = this.txt; const str = txt.str; const firstChunk = str.first(); @@ -472,7 +477,6 @@ export class Overlay implements Printable, Stateful { let state: number = CONST.START_STATE; for (let pair = i(); pair; pair = i()) { const [p1, p2] = pair; - // TODO: need to incorporate slice attribute hash here? const id1 = p1.id; state = (state << 5) + state + (id1.sid >>> 0) + id1.time; let overlayPointHash = CONST.START_STATE; @@ -482,10 +486,8 @@ export class Overlay implements Printable, Stateful { (overlayPointHash << 5) + overlayPointHash + ((((id.sid >>> 0) + id.time) << 8) + (off << 4) + len); }); state = updateNum(state, overlayPointHash); - if (p1) { - p1.hash = overlayPointHash; - stateTotal = updateNum(stateTotal, overlayPointHash); - } + p1.hash = overlayPointHash; + stateTotal = updateNum(stateTotal, overlayPointHash); if (p2 instanceof MarkerOverlayPoint) { if (marker) { marker.textHash = state; 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 539252b059..59fa1c2c96 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 @@ -6,7 +6,7 @@ import {SliceBehavior} from '../../slice/constants'; const setup = () => { const sid = 123456789; - const model = Model.withLogicalClock(sid); + const model = Model.create(undefined, sid); model.api.root({ text: '', slices: [], @@ -307,18 +307,35 @@ describe('Overlay.refresh()', () => { describe('updates hash', () => { testRefresh('when the first character is deleted and reinserted', (kit, refresh) => { const index = 0; - const char = kit.peritext.strApi().view()[index]; + const str = kit.peritext.strApi(); + const char = str.view()[index]; + const view = str.view(); refresh(); kit.peritext.strApi().del(index, 1); kit.peritext.strApi().ins(index, char); + expect(str.view()).toEqual(view); }); testRefresh('when the last character is deleted and reinserted', (kit, refresh) => { const index = kit.peritext.strApi().view().length - 1; - const char = kit.peritext.strApi().view()[index]; + const str = kit.peritext.strApi(); + const char = str.view()[index]; + const view = str.view(); refresh(); kit.peritext.strApi().del(index, 1); kit.peritext.strApi().ins(index, char); + expect(str.view()).toEqual(view); + }); + + testRefresh('when the third character is reinserted', (kit, refresh) => { + const index = 3; + const str = kit.peritext.strApi(); + const char = str.view()[index]; + const view = str.view(); + refresh(); + kit.peritext.strApi().del(index, 1); + kit.peritext.strApi().ins(index, char); + expect(str.view()).toEqual(view); }); }); }); From 636a16622f8b9037cca18eee008b99dc9e375cde Mon Sep 17 00:00:00 2001 From: streamich Date: Thu, 9 May 2024 12:23:22 +0200 Subject: [PATCH 05/10] =?UTF-8?q?fix(json-crdt-extensions):=20=F0=9F=90=9B?= =?UTF-8?q?=20correctly=20store=20extra=20and=20local=20slices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/editor/EditorSlices.ts | 6 +- .../overlay/__tests__/Overlay.markers.spec.ts | 85 +++++++++++++++++++ .../overlay/__tests__/Overlay.points.spec.ts | 2 +- .../peritext/slice/Slices.ts | 24 +++--- 4 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts diff --git a/src/json-crdt-extensions/peritext/editor/EditorSlices.ts b/src/json-crdt-extensions/peritext/editor/EditorSlices.ts index 26a95f8e39..e1faa18b26 100644 --- a/src/json-crdt-extensions/peritext/editor/EditorSlices.ts +++ b/src/json-crdt-extensions/peritext/editor/EditorSlices.ts @@ -1,9 +1,9 @@ +import {PersistedSlice} from '../slice/PersistedSlice'; import type {Peritext} from '../Peritext'; import type {SliceType} from '../slice/types'; import type {MarkerSlice} from '../slice/MarkerSlice'; import type {Slices} from '../slice/Slices'; import type {ITimestampStruct} from '../../../json-crdt-patch'; -import type {PersistedSlice} from '../slice/PersistedSlice'; import type {Cursor} from './Cursor'; export class EditorSlices { @@ -42,4 +42,8 @@ export class EditorSlices { return marker; }); } + + public del(sliceOrId: PersistedSlice | ITimestampStruct): void { + this.slices.del(sliceOrId instanceof PersistedSlice ? sliceOrId.id : sliceOrId); + } } diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts new file mode 100644 index 0000000000..3838b859e9 --- /dev/null +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts @@ -0,0 +1,85 @@ +import {Kit, setupNumbersKit, setupNumbersWithTombstonesKit} from '../../__tests__/setup'; +import {MarkerOverlayPoint} from '../MarkerOverlayPoint'; + +const runMarkersTests = (setup: () => Kit) => { + describe('.markers()', () => { + test('returns empty set by default', () => { + const {peritext} = setup(); + peritext.overlay.refresh(); + const list = [...peritext.overlay.markers()]; + expect(list.length).toBe(0); + }); + + test('returns a single marker', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(3); + editor.saved.insMarker(''); + peritext.overlay.refresh(); + const list = [...peritext.overlay.markers()]; + expect(list.length).toBe(1); + expect(list[0] instanceof MarkerOverlayPoint).toBe(true); + }); + + test('can iterate through multiple markers', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(5); + const [m2] = editor.saved.insMarker(''); + peritext.overlay.refresh(); + editor.cursor.setAt(8); + const [m3] = editor.local.insMarker(''); + peritext.overlay.refresh(); + editor.cursor.setAt(2); + const [m1] = editor.local.insMarker(''); + peritext.overlay.refresh(); + const list = [...peritext.overlay.markers()]; + expect(list.length).toBe(3); + list.forEach(m => expect(m instanceof MarkerOverlayPoint).toBe(true)); + expect(list[0].marker).toBe(m1); + expect(list[1].marker).toBe(m2); + expect(list[2].marker).toBe(m3); + }); + + test('can delete markers', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(5); + const [m2] = editor.extra.insMarker(''); + editor.cursor.setAt(8); + const [m3] = editor.local.insMarker(''); + editor.cursor.setAt(2); + const [m1] = editor.local.insMarker(''); + peritext.overlay.refresh(); + const list = [...peritext.overlay.markers()]; + expect(list.length).toBe(3); + editor.local.del(m3); + peritext.overlay.refresh(); + const list2 = [...peritext.overlay.markers()]; + expect(list2.length).toBe(2); + expect(list2[0].marker).toBe(m1); + expect(list2[1].marker).toBe(m2); + editor.local.del(m2); + peritext.overlay.refresh(); + const list3 = [...peritext.overlay.markers()]; + expect(list3.length).toBe(2); + expect(list3[0].marker).toBe(m1); + expect(list3[1].marker).toBe(m2); + editor.extra.del(m2); + peritext.overlay.refresh(); + const list4 = [...peritext.overlay.markers()]; + expect(list4.length).toBe(1); + expect(list4[0].marker).toBe(m1); + editor.local.del(m1); + editor.local.del(m1); + editor.local.del(m1); + peritext.overlay.refresh(); + expect([...peritext.overlay.markers()].length).toBe(0); + }); + }); +}; + +describe('numbers "0123456789", no edits', () => { + runMarkersTests(setupNumbersKit); +}); + +describe('numbers "0123456789", with default schema and tombstones', () => { + runMarkersTests(setupNumbersWithTombstonesKit); +}); diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.points.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.points.spec.ts index d341897e5a..822feb5d46 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.points.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.points.spec.ts @@ -4,7 +4,7 @@ import {Peritext} from '../../Peritext'; import type {OverlayPoint} from '../OverlayPoint'; const setup = () => { - const model = Model.withLogicalClock(); + const model = Model.create(); const api = model.api; api.root({ text: '', diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index a4cb00a804..466b3688d1 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -42,9 +42,9 @@ export class Slices implements Stateful, Printable { data?: unknown, Klass: K = behavior === SliceBehavior.Marker ? MarkerSlice : PersistedSlice, ): S { - const model = this.set.doc; + const slicesModel = this.set.doc; const set = this.set; - const api = model.api; + const api = slicesModel.api; const builder = api.builder; const tupleId = builder.vec(); const start = range.start.clone(); @@ -68,10 +68,10 @@ export class Slices implements Stateful, Printable { const chunkId = builder.insArr(set.id, set.id, [tupleId]); // TODO: Consider using `s` schema here. api.apply(); - const tuple = model.index.get(tupleId) as VecNode; + const tuple = slicesModel.index.get(tupleId) as VecNode; const chunk = set.findById(chunkId)!; // TODO: Need to check if split slice text was deleted - const slice = new Klass(model, this.txt, chunk, tuple, behavior, type, start, end); + const slice = new Klass(slicesModel, this.txt, chunk, tuple, behavior, type, start, end); this.list.set(chunk.id, slice); return slice; } @@ -87,17 +87,17 @@ export class Slices implements Stateful, Printable { separator: string = Chars.BlockSplitSentinel, ): MarkerSlice { // TODO: test condition when cursors is at absolute or relative starts - const {txt, set} = this; - const model = set.doc; - const api = model.api; + const txt = this.txt; + const api = txt.model.api; const builder = api.builder; - const str = txt.str; /** * We skip one clock cycle to prevent Block-wise RGA from merging adjacent * characters. We want the marker chunk to always be its own distinct chunk. */ builder.nop(1); - const textId = builder.insStr(str.id, after, separator); + // TODO: Handle case when marker is inserted at the abs start, prevent abs start/end inserts. + const textId = builder.insStr(txt.str.id, after, separator); + api.apply(); const point = txt.point(textId, Anchor.Before); const range = txt.range(point, point.clone()); return this.insMarker(range, type, data); @@ -134,8 +134,10 @@ export class Slices implements Stateful, Printable { public del(id: ITimestampStruct): void { this.list.del(id); - const api = this.set.doc.api; - api.builder.del(this.set.id, [tss(id.sid, id.time, 1)]); + const set = this.set; + const api = set.doc.api; + // TODO: Is it worth checking if the slice is already deleted? + api.builder.del(set.id, [tss(id.sid, id.time, 1)]); api.apply(); } From 9f090f71b18010bb71d0f5ea82bd1303957bb414 Mon Sep 17 00:00:00 2001 From: streamich Date: Thu, 9 May 2024 12:52:32 +0200 Subject: [PATCH 06/10] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20add=20markers=20only=20overlay=20tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/MarkerOverlayPoint.ts | 9 +++- .../peritext/overlay/Overlay.ts | 41 ++++++++++++++----- .../peritext/overlay/OverlayPoint.ts | 14 ++++--- .../overlay/__tests__/Overlay.markers.spec.ts | 26 ++++++++++++ .../overlay/__tests__/Overlay.spec.ts | 4 +- 5 files changed, 76 insertions(+), 18 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts b/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts index 81983b04d4..588c03c8c9 100644 --- a/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts +++ b/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts @@ -1,12 +1,13 @@ import {printTree} from 'tree-dump/lib/printTree'; import {OverlayPoint} from './OverlayPoint'; +import type {HeadlessNode2} from 'sonic-forest/lib/types2'; import type {SliceType} from '../slice/types'; import type {Anchor} from '../rga/constants'; import type {AbstractRga} from '../../../json-crdt/nodes/rga'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {MarkerSlice} from '../slice/MarkerSlice'; -export class MarkerOverlayPoint extends OverlayPoint { +export class MarkerOverlayPoint extends OverlayPoint implements HeadlessNode2 { /** * Hash value of the following text contents, up until the next marker. */ @@ -57,4 +58,10 @@ export class MarkerOverlayPoint extends OverlayPoint { ])) ); } + + // ---------------------------------------------------------------- Printable + + public p2: MarkerOverlayPoint | undefined; + public l2: MarkerOverlayPoint | undefined; + public r2: MarkerOverlayPoint | undefined; } diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 44f4a0043e..893b4ced9d 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -1,6 +1,7 @@ import {printTree} from 'tree-dump/lib/printTree'; import {printBinary} from 'tree-dump/lib/printBinary'; import {first, insertLeft, insertRight, last, next, prev, remove} from 'sonic-forest/lib/util'; +import {first2, insert2, next2, remove2} from 'sonic-forest/lib/util2'; import {splay} from 'sonic-forest/lib/splay/util'; import {Anchor} from '../rga/constants'; import {Point} from '../rga/Point'; @@ -19,6 +20,9 @@ import type {Printable} from 'tree-dump/lib/types'; import type {MutableSlice, Slice} from '../slice/types'; import type {Slices} from '../slice/Slices'; import type {OverlayPair, OverlayTuple} from './types'; +import type {Comparator} from 'sonic-forest/lib/types'; + +const spatialComparator: Comparator = (a: OverlayPoint, b: OverlayPoint) => a.cmpSpatial(b); /** * Overlay is a tree structure that represents all the intersections of slices @@ -29,6 +33,7 @@ import type {OverlayPair, OverlayTuple} from './types'; */ export class Overlay implements Printable, Stateful { public root: OverlayPoint | undefined = undefined; + public root2: MarkerOverlayPoint | undefined = undefined; /** A virtual absolute start point, used when the absolute start is missing. */ public readonly START: OverlayPoint; @@ -171,20 +176,17 @@ export class Overlay implements Printable, Stateful { return new UndefEndIter(this.points0(after)); } - public markers0(): UndefIterator> { - let curr = this.first(); + public markers0(after: undefined | MarkerOverlayPoint): UndefIterator> { + let curr = after ? next2(after) : first2(this.root2); return () => { - while (curr) { - const ret = curr; - if (curr) curr = next(curr); - if (ret instanceof MarkerOverlayPoint) return ret; - } - return; + const ret = curr; + if (curr) curr = next2(curr); + return ret; }; } public markers(): IterableIterator> { - return new UndefEndIter(this.markers0()); + return new UndefEndIter(this.markers0(undefined)); } public pairs0(after: undefined | OverlayPoint): UndefIterator> { @@ -404,6 +406,7 @@ export class Overlay implements Printable, Stateful { } private insMarker(slice: MarkerSlice): [start: OverlayPoint, end: OverlayPoint] { + // TODO: When marker is at rel start, and cursor too, the overlay point should have reference to the cursor. const point = this.mPoint(slice, Anchor.Before); const pivot = this.insPoint(point); if (!pivot) { @@ -445,6 +448,10 @@ export class Overlay implements Printable, Stateful { * @returns Returns the existing point if it was already in the tree. */ private insPoint(point: OverlayPoint): OverlayPoint | undefined { + if (point instanceof MarkerOverlayPoint) { + this.root2 = insert2(this.root2, point, spatialComparator); + // if (this.root2 !== point) this.root2 = splay2(this.root2!, point, 10); + } let pivot = this.getOrNextLower(point); if (!pivot) pivot = first(this.root); if (!pivot) { @@ -461,6 +468,8 @@ export class Overlay implements Printable, Stateful { } private delPoint(point: OverlayPoint): void { + if (point instanceof MarkerOverlayPoint) + this.root2 = remove2(this.root2, point); this.root = remove(this.root, point); } @@ -519,9 +528,21 @@ export class Overlay implements Printable, Stateful { ]) ); }; + const printMarkerPoint = (tab: string, point: MarkerOverlayPoint): string => { + return ( + point.toString(tab) + + printBinary(tab, [ + !point.l2 ? null : (tab) => printMarkerPoint(tab, point.l2!), + !point.r2 ? null : (tab) => printMarkerPoint(tab, point.r2!), + ]) + ); + }; return ( `${this.constructor.name} #${this.hash.toString(36)}` + - printTree(tab, [!this.root ? null : (tab) => printPoint(tab, this.root!)]) + printTree(tab, [ + !this.root ? null : (tab) => printPoint(tab, this.root!), + !this.root2 ? null : (tab) => printMarkerPoint(tab, this.root2!), + ]) ); } } diff --git a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts index 6ace500f22..fd5c78cf05 100644 --- a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts +++ b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts @@ -4,7 +4,7 @@ import {OverlayRef, OverlayRefSliceEnd, OverlayRefSliceStart} from './refs'; import {printTree} from 'tree-dump/lib/printTree'; import type {MarkerSlice} from '../slice/MarkerSlice'; import type {HeadlessNode} from 'sonic-forest/lib/types'; -import type {Printable} from 'tree-dump/lib/types'; +import type {PrintChild, Printable} from 'tree-dump/lib/types'; import type {Slice} from '../slice/types'; /** @@ -218,12 +218,16 @@ export class OverlayPoint extends Point implements Printable, Hea const refs = lite ? '' : `, refs = ${this.refs.length}`; const header = this.toStringName(tab, lite) + refs; if (lite) return header; + const children: PrintChild[] = []; + const layers = this.layers; + const layerLength = layers.length; + for (let i = 0; i < layerLength; i++) children.push((tab) => layers[i].toString(tab)); + const markers = this.markers; + const markerLength = markers.length; + for (let i = 0; i < markerLength; i++) children.push((tab) => markers[i].toString(tab)); return ( header + - printTree( - tab, - this.layers.map((slice) => (tab) => slice.toString(tab)), - ) + printTree(tab, children) ); } diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts index 3838b859e9..0beb7d98ab 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts @@ -73,6 +73,32 @@ const runMarkersTests = (setup: () => Kit) => { peritext.overlay.refresh(); expect([...peritext.overlay.markers()].length).toBe(0); }); + + test('can add marker at the start of text', () => { + const {peritext, editor} = setup(); + editor.cursor.setAt(0); + const [marker] = editor.extra.insMarker(0); + peritext.overlay.refresh(); + const list = [...peritext.overlay.markers()]; + expect(list.length).toBe(1); + expect(list[0].marker).toBe(marker); + editor.extra.del(marker); + peritext.overlay.refresh(); + expect([...peritext.overlay.markers()].length).toBe(0); + }); + + test('can add marker at the end of text', () => { + const {peritext, editor} = setup(); + editor.cursor.set(peritext.pointEnd()!); + const [marker] = editor.extra.insMarker('0'); + peritext.overlay.refresh(); + const list = [...peritext.overlay.markers()]; + expect(list.length).toBe(1); + expect(list[0].marker).toBe(marker); + editor.extra.del(marker); + peritext.overlay.refresh(); + expect([...peritext.overlay.markers()].length).toBe(0); + }); }); }; 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 c6d4fe8d9b..9da416ac79 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts @@ -21,7 +21,7 @@ const setup = () => { const markerCount = (peritext: Peritext): number => { const overlay = peritext.overlay; - const iterator = overlay.markers0(); + const iterator = overlay.markers0(void 0); let count = 0; for (let split = iterator(); split; split = iterator()) { count++; @@ -106,7 +106,7 @@ describe('markers', () => { expect(markerCount(peritext)).toBe(2); const points = []; let point; - for (const iterator = peritext.overlay.markers0(); (point = iterator()); ) points.push(point); + for (const iterator = peritext.overlay.markers0(void 0); (point = iterator()); ) points.push(point); expect(points.length).toBe(2); expect(points[0].pos()).toBe(2); expect(points[1].pos()).toBe(11); From eb7fc09797f0750fdeef6382e610ee52c0f9a6a2 Mon Sep 17 00:00:00 2001 From: streamich Date: Thu, 9 May 2024 12:52:54 +0200 Subject: [PATCH 07/10] =?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/overlay/Overlay.ts | 6 ++---- src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts | 5 +---- .../peritext/overlay/__tests__/Overlay.markers.spec.ts | 4 ++-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 893b4ced9d..ddbfa419b8 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -114,7 +114,7 @@ export class Overlay implements Printable, Stateful { } return result; } - + /** @todo Rename to `chunks()`. */ public chunkSlices0( chunk: Chunk | undefined, @@ -239,7 +239,6 @@ export class Overlay implements Printable, Stateful { return new UndefEndIter(this.tuples0(after)); } - /** * Finds the first point that satisfies the given predicate function. * @@ -468,8 +467,7 @@ export class Overlay implements Printable, Stateful { } private delPoint(point: OverlayPoint): void { - if (point instanceof MarkerOverlayPoint) - this.root2 = remove2(this.root2, point); + if (point instanceof MarkerOverlayPoint) this.root2 = remove2(this.root2, point); this.root = remove(this.root, point); } diff --git a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts index fd5c78cf05..61e35acb86 100644 --- a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts +++ b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts @@ -225,10 +225,7 @@ export class OverlayPoint extends Point implements Printable, Hea const markers = this.markers; const markerLength = markers.length; for (let i = 0; i < markerLength; i++) children.push((tab) => markers[i].toString(tab)); - return ( - header + - printTree(tab, children) - ); + return header + printTree(tab, children); } // ------------------------------------------------------------- HeadlessNode diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts index 0beb7d98ab..b454f60ff5 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.markers.spec.ts @@ -33,7 +33,7 @@ const runMarkersTests = (setup: () => Kit) => { peritext.overlay.refresh(); const list = [...peritext.overlay.markers()]; expect(list.length).toBe(3); - list.forEach(m => expect(m instanceof MarkerOverlayPoint).toBe(true)); + list.forEach((m) => expect(m instanceof MarkerOverlayPoint).toBe(true)); expect(list[0].marker).toBe(m1); expect(list[1].marker).toBe(m2); expect(list[2].marker).toBe(m3); @@ -86,7 +86,7 @@ const runMarkersTests = (setup: () => Kit) => { peritext.overlay.refresh(); expect([...peritext.overlay.markers()].length).toBe(0); }); - + test('can add marker at the end of text', () => { const {peritext, editor} = setup(); editor.cursor.set(peritext.pointEnd()!); From 975c95f244f502e278dbf1c3a7da9194dd1ca52b Mon Sep 17 00:00:00 2001 From: streamich Date: Thu, 9 May 2024 13:01:26 +0200 Subject: [PATCH 08/10] =?UTF-8?q?chore(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=A4=96=20cleanup=20marker=20overlay=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/MarkerOverlayPoint.ts | 10 +--------- .../peritext/overlay/OverlayPoint.ts | 5 ----- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts b/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts index 588c03c8c9..63126c6c63 100644 --- a/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts +++ b/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts @@ -22,14 +22,6 @@ export class MarkerOverlayPoint extends OverlayPoint implements H super(rga, id, anchor); } - /** - * @todo Rename or access it directly. - * @deprecated - */ - public markerHash(): number { - return this.marker ? this.marker.hash : 0; - } - public type(): SliceType { return this.marker && this.marker.type; } @@ -59,7 +51,7 @@ export class MarkerOverlayPoint extends OverlayPoint implements H ); } - // ---------------------------------------------------------------- Printable + // ------------------------------------------------------------ HeadlessNode2 public p2: MarkerOverlayPoint | undefined; public l2: MarkerOverlayPoint | undefined; diff --git a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts index 61e35acb86..d1c5557533 100644 --- a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts +++ b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts @@ -85,7 +85,6 @@ export class OverlayPoint extends Point implements Printable, Hea /** * Collapsed slices - markers (block splits), which represent a single point * in the text, even if the start and end of the slice are different. - * @deprecated This field might happen to be not necessary. */ public readonly markers: Slice[] = []; @@ -96,10 +95,8 @@ export class OverlayPoint extends Point implements Printable, Hea * the state of the point. The markers are sorted by the slice ID. * * @param slice Slice to add to the marker list. - * @deprecated This method might happen to be not necessary. */ public addMarker(slice: Slice): void { - /** @deprecated */ const markers = this.markers; const length = markers.length; if (!length) { @@ -131,10 +128,8 @@ export class OverlayPoint extends Point implements Printable, Hea * the text, even if the start and end of the slice are different. * * @param slice Slice to remove from the marker list. - * @deprecated This method might happen to be not necessary. */ public removeMarker(slice: Slice): void { - /** @deprecated */ const markers = this.markers; const length = markers.length; for (let i = 0; i < length; i++) { From 178381a253b00ac48cdfeb7142e958ccee218d69 Mon Sep 17 00:00:00 2001 From: streamich Date: Thu, 9 May 2024 13:40:41 +0200 Subject: [PATCH 09/10] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20add=20test=20for=20cursor=20in=20same=20position?= =?UTF-8?q?=20as=20marker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/overlay/Overlay.ts | 9 +++------ .../peritext/overlay/__tests__/Overlay.spec.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index ddbfa419b8..8c3c01a652 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -315,11 +315,9 @@ export class Overlay implements Printable, Stateful { * @returns Whether the point is a marker point. */ public isMarker(id: ITimestampStruct): boolean { - const point = this.txt.point(id, Anchor.Before); - const overlayPoint = this.getOrNextLower(point); - return ( - overlayPoint instanceof MarkerOverlayPoint && overlayPoint.id.time === id.time && overlayPoint.id.sid === id.sid - ); + const p = this.txt.point(id, Anchor.Before); + const op = this.getOrNextLower(p); + return op instanceof MarkerOverlayPoint && op.id.time === id.time && op.id.sid === id.sid; } // ----------------------------------------------------------------- Stateful @@ -405,7 +403,6 @@ export class Overlay implements Printable, Stateful { } private insMarker(slice: MarkerSlice): [start: OverlayPoint, end: OverlayPoint] { - // TODO: When marker is at rel start, and cursor too, the overlay point should have reference to the cursor. const point = this.mPoint(slice, Anchor.Before); const pivot = this.insPoint(point); if (!pivot) { 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 9da416ac79..99d9488453 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts @@ -65,6 +65,18 @@ describe('markers', () => { peritext.overlay.refresh(); expect(markerCount(peritext)).toBe(2); }); + + test('does reference cursor, when marker and cursor are at the same position', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3); + const [marker] = peritext.editor.saved.insMarker(['p'], 'ΒΆ'); + peritext.editor.cursor.set(marker.start.clone()); + peritext.overlay.refresh(); + const overlayMarkerPoint = peritext.overlay.root2!; + expect(overlayMarkerPoint instanceof MarkerOverlayPoint).toBe(true); + expect(overlayMarkerPoint.markers.length).toBe(1); + expect(overlayMarkerPoint.markers.find((m) => m === peritext.editor.cursor)).toBe(peritext.editor.cursor); + }); }); describe('deletes', () => { From c5962d6c942231cff63d4c851234825abd32b367 Mon Sep 17 00:00:00 2001 From: streamich Date: Thu, 9 May 2024 13:47:46 +0200 Subject: [PATCH 10/10] =?UTF-8?q?test(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=8D=20add=20test=20for=20collapsed=20slice=20insertion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/overlay/Overlay.ts | 6 +----- .../peritext/overlay/__tests__/Overlay.spec.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 8c3c01a652..645c8ca229 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -375,7 +375,6 @@ export class Overlay implements Printable, Stateful { } private insSlice(slice: Slice): [start: OverlayPoint, end: OverlayPoint] { - // TODO: Test cases where the inserted slice is collapsed to one point. const x0 = slice.start; const x1 = slice.end; const [start, isStartNew] = this.upsertPoint(x0); @@ -395,10 +394,7 @@ export class Overlay implements Printable, Stateful { let curr: OverlayPoint | undefined = start; do curr.addLayer(slice); while ((curr = next(curr)) && curr !== end); - } else { - // TODO: review if this is needed: - start.addMarker(slice); - } + } else start.addMarker(slice); return [start, end]; } 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 99d9488453..6a717dc99a 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts @@ -161,6 +161,18 @@ describe('slices', () => { expect(points.length).toBe(4); }); + test('can insert a slice, which is collapsed to a point', () => { + const {peritext} = setup(); + peritext.editor.cursor.setAt(3); + const [slice] = peritext.editor.saved.insStack('em', {emphasis: true}); + peritext.overlay.refresh(); + const [point] = [...peritext.overlay.points()]; + expect(point.layers.length).toBe(0); + expect(point.markers.length).toBe(2); + expect(point.markers.find((m) => m === peritext.editor.cursor)).toBe(peritext.editor.cursor); + expect(point.markers.find((m) => m === slice)).toBe(slice); + }); + test('intersecting slice chunks point to two slices', () => { const {peritext} = setup(); peritext.editor.cursor.setAt(2, 2);