From a76291c7b54a157de11fc6d21bdb17d80da7df4c Mon Sep 17 00:00:00 2001 From: streamich Date: Mon, 6 May 2024 13:16:15 +0200 Subject: [PATCH 01/13] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20improve=20editor=20interfaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/editor/Editor.ts | 5 ++- .../peritext/editor/EditorSlices.ts | 36 +++++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts index 28cf469045..c66d6ac473 100644 --- a/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -77,22 +77,25 @@ export class Editor { return true; } + /** @deprecated use `.saved.insStack` */ public insStackSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { const range = this.cursor.range(); return this.txt.savedSlices.ins(range, SliceBehavior.Stack, type, data); } + /** @deprecated use `.saved.insOverwrite` */ public insOverwriteSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { const range = this.cursor.range(); return this.txt.savedSlices.ins(range, SliceBehavior.Overwrite, type, data); } + /** @deprecated use `.saved.insErase` */ public insEraseSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { const range = this.cursor.range(); return this.txt.savedSlices.ins(range, SliceBehavior.Erase, type, data); } - /** @deprecated */ + /** @deprecated use `.saved.insMarker` */ public insMarker(type: SliceType, data?: unknown): MarkerSlice { return this.saved.insMarker(type, data, Chars.BlockSplitSentinel)[0]; } diff --git a/src/json-crdt-extensions/peritext/editor/EditorSlices.ts b/src/json-crdt-extensions/peritext/editor/EditorSlices.ts index 8066a377da..1f1d38cfa9 100644 --- a/src/json-crdt-extensions/peritext/editor/EditorSlices.ts +++ b/src/json-crdt-extensions/peritext/editor/EditorSlices.ts @@ -2,6 +2,9 @@ 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 { constructor( @@ -9,16 +12,37 @@ export class EditorSlices { protected readonly slices: Slices, ) {} + protected insAtCursors>(callback: (cursor: Cursor) => S): S[] { + const slices: S[] = []; + this.txt.editor.cursors((cursor) => { + const slice = callback(cursor); + slices.push(slice); + }); + return slices; + } + + public insStack(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice[] { + return this.insAtCursors((cursor) => + this.slices.insStack(cursor.range(), type, data)); + } + + public insOverwrite(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice[] { + return this.insAtCursors((cursor) => + this.slices.insOverwrite(cursor.range(), type, data)); + } + + public insErase(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice[] { + return this.insAtCursors((cursor) => + this.slices.insErase(cursor.range(), type, data)); + } + public insMarker(type: SliceType, data?: unknown, separator?: string): MarkerSlice[] { - const {txt, slices} = this; - const markers: MarkerSlice[] = []; - txt.editor.cursors((cursor) => { + return this.insAtCursors((cursor) => { cursor.collapse(); const after = cursor.start.clone(); after.refAfter(); - const marker = slices.insMarkerAfter(after.id, type, data, separator); - markers.push(marker); + const marker = this.slices.insMarkerAfter(after.id, type, data, separator); + return marker; }); - return markers; } } From eda567dbde891b1f342c9274ccd6d940a7f6cc70 Mon Sep 17 00:00:00 2001 From: streamich Date: Mon, 6 May 2024 13:30:58 +0200 Subject: [PATCH 02/13] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20display=20slice=20behaviors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/slice/PersistedSlice.ts | 6 ++++-- src/json-crdt-extensions/peritext/slice/constants.ts | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts index 81e0864ace..95c0d656ce 100644 --- a/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts +++ b/src/json-crdt-extensions/peritext/slice/PersistedSlice.ts @@ -4,7 +4,7 @@ import {Range} from '../rga/Range'; import {updateNode} from '../../../json-crdt/hash'; import {printTree} from 'tree-dump/lib/printTree'; import {Anchor} from '../rga/constants'; -import {SliceHeaderMask, SliceHeaderShift, SliceBehavior, SliceTupleIndex} from './constants'; +import {SliceHeaderMask, SliceHeaderShift, SliceBehavior, SliceTupleIndex, SliceBehaviorName} from './constants'; import {CONST} from '../../../json-hash'; import {Timestamp} from '../../../json-crdt-patch/clock'; import {VecNode} from '../../../json-crdt/nodes'; @@ -168,7 +168,9 @@ export class PersistedSlice extends Range implements MutableSlice const data = this.data(); const dataFormatted = data ? prettyOneLine(data) : '∅'; const dataLengthBreakpoint = 32; - const header = `${this.constructor.name} ${super.toString('', true)}, ${this.behavior}, ${JSON.stringify(this.type)}${dataFormatted.length < dataLengthBreakpoint ? `, ${dataFormatted}` : ''}`; + const header = `${this.constructor.name} ${super.toString('', true)}, ${ + SliceBehaviorName[this.behavior] + }, ${JSON.stringify(this.type)}${dataFormatted.length < dataLengthBreakpoint ? `, ${dataFormatted}` : ''}`; return header; } diff --git a/src/json-crdt-extensions/peritext/slice/constants.ts b/src/json-crdt-extensions/peritext/slice/constants.ts index 9d59aac72a..11c016a3c5 100644 --- a/src/json-crdt-extensions/peritext/slice/constants.ts +++ b/src/json-crdt-extensions/peritext/slice/constants.ts @@ -53,10 +53,20 @@ export const enum SliceBehavior { /** * Used to mark the user's cursor position in the document. + * + * @todo Consider removing this. */ Cursor = 0b100, } +export enum SliceBehaviorName { + Marker = SliceBehavior.Marker, + Stack = SliceBehavior.Stack, + Overwrite = SliceBehavior.Overwrite, + Erase = SliceBehavior.Erase, + Cursor = SliceBehavior.Cursor, +} + /** * Specifies `vec` offsets in the {@link SliceView}. */ From f3784b083392a9fc7e9bd2d64b173f074963ff63 Mon Sep 17 00:00:00 2001 From: streamich Date: Mon, 6 May 2024 13:51:24 +0200 Subject: [PATCH 03/13] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20add=20Overlay.getOrNextHigher()=20impelemntation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/Overlay.ts | 20 ++++ .../__tests__/Overlay.getOrNextLH.spec.ts | 93 +++++++++++++++++++ .../__tests__/Overlay.getOrNextLower.spec.ts | 52 ----------- .../peritext/slice/Slices.ts | 8 +- 4 files changed, 117 insertions(+), 56 deletions(-) create mode 100644 src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts delete mode 100644 src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLower.spec.ts diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 2faf194c60..e4605518ea 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -74,6 +74,26 @@ export class Overlay implements Printable, Stateful { return result; } + /** + * Retrieve overlay point or the next one, measured in spacial dimension. + */ + public getOrNextHigher(point: Point): OverlayPoint | undefined { + let curr: OverlayPoint | undefined = this.root; + let result: OverlayPoint | undefined = undefined; + while (curr) { + const cmp = curr.cmpSpatial(point); + if (cmp === 0) return curr; + if (cmp < 0) curr = curr.r; + else { + const next = curr.l; + result = curr; + if (!next) return result; + curr = next; + } + } + return result; + } + public find(predicate: (point: OverlayPoint) => boolean): OverlayPoint | undefined { let point = this.first(); while (point) { diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts new file mode 100644 index 0000000000..7dd21470ae --- /dev/null +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts @@ -0,0 +1,93 @@ +import {Model} from '../../../../json-crdt/model'; +import {size} from 'sonic-forest/lib/util'; +import {Peritext} from '../../Peritext'; +import {Anchor} from '../../rga/constants'; + +describe('.getOrNextLower()', () => { + test('combines overlay points - right anchor', () => { + const model = Model.create(); + const api = model.api; + api.root({ + text: '1234', + slices: [], + }); + const peritext = new Peritext(model, api.str(['text']).node, api.arr(['slices']).node); + peritext.editor.cursor.setAt(1, 1); + peritext.editor.saved.insStack(2); + peritext.refresh(); + const str = peritext.str; + const id1 = str.find(1)!; + const id2 = str.find(2)!; + const p1 = peritext.point(id1, Anchor.After); + const p2 = peritext.point(id2, Anchor.After); + peritext.editor.cursor.set(p1, p2); + peritext.editor.saved.insStack(3); + peritext.refresh(); + const cnt = size(peritext.overlay.root); + expect(cnt).toBe(3); + }); + + test('combines overlay points - right anchor 2', () => { + const model = Model.create(); + const api = model.api; + api.root({ + text: '1234', + slices: [], + }); + const peritext = new Peritext(model, api.str(['text']).node, api.arr(['slices']).node); + const str = peritext.str; + const id1 = str.find(1)!; + const id2 = str.find(2)!; + const p1 = peritext.point(id1, Anchor.After); + const p2 = peritext.point(id2, Anchor.After); + peritext.editor.cursor.set(p1, p2); + peritext.editor.saved.insStack(3); + peritext.refresh(); + peritext.editor.cursor.setAt(2, 1); + peritext.editor.saved.insStack(33); + peritext.refresh(); + const cnt = size(peritext.overlay.root); + expect(cnt).toBe(3); + }); +}); + +describe('.getOrNextHigher()', () => { + test('can iterate through all character points', () => { + const model = Model.create(); + const api = model.api; + api.root({ + text: '1234', + slices: [], + }); + const peritext = new Peritext(model, api.str(['text']).node, api.arr(['slices']).node); + const editor = peritext.editor; + editor.cursor.setAt(0, 1); + const [slice1] = editor.saved.insStack(1); + editor.cursor.setAt(1, 1); + const [slice2] = editor.saved.insStack(2); + editor.cursor.setAt(2, 1); + const [slice3] = editor.saved.insStack(3); + editor.cursor.setAt(3, 1); + const [slice4] = editor.saved.insStack(4); + peritext.refresh(); + let overlayPoint = peritext.overlay.getOrNextHigher(slice4.end)!; + expect(overlayPoint.layers.length).toBe(0); + overlayPoint = peritext.overlay.getOrNextHigher(slice4.start)!; + expect(overlayPoint.layers.length).toBe(2); + overlayPoint = peritext.overlay.getOrNextHigher(slice3.end)!; + expect(overlayPoint.layers.length).toBe(0); + overlayPoint = peritext.overlay.getOrNextHigher(slice3.start)!; + expect(overlayPoint.layers.length).toBe(1); + expect(overlayPoint.layers[0]).toBe(slice3); + overlayPoint = peritext.overlay.getOrNextHigher(slice2.end)!; + expect(overlayPoint.layers.length).toBe(0); + overlayPoint = peritext.overlay.getOrNextHigher(slice2.start)!; + expect(overlayPoint.layers.length).toBe(1); + expect(overlayPoint.layers[0]).toBe(slice2); + overlayPoint = peritext.overlay.getOrNextHigher(slice1.end)!; + expect(overlayPoint.layers.length).toBe(0); + overlayPoint = peritext.overlay.getOrNextHigher(slice1.start)!; + expect(overlayPoint.layers.length).toBe(1); + expect(overlayPoint.layers[0]).toBe(slice1); + }); +}); diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLower.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLower.spec.ts deleted file mode 100644 index bb117c1ae1..0000000000 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLower.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {Model} from '../../../../json-crdt/model'; -import {size} from 'sonic-forest/lib/util'; -import {Peritext} from '../../Peritext'; -import {Anchor} from '../../rga/constants'; - -describe('.getOrNextLower()', () => { - test('combines overlay points - right anchor', () => { - const model = Model.withLogicalClock(); - const api = model.api; - api.root({ - text: '1234', - slices: [], - }); - const peritext = new Peritext(model, api.str(['text']).node, api.arr(['slices']).node); - peritext.editor.cursor.setAt(1, 1); - peritext.editor.insStackSlice(2); - peritext.refresh(); - const str = peritext.str; - const id1 = str.find(1)!; - const id2 = str.find(2)!; - const p1 = peritext.point(id1, Anchor.After); - const p2 = peritext.point(id2, Anchor.After); - peritext.editor.cursor.set(p1, p2); - peritext.editor.insStackSlice(3); - peritext.refresh(); - const cnt = size(peritext.overlay.root); - expect(cnt).toBe(3); - }); - - test('combines overlay points - right anchor 2', () => { - const model = Model.withLogicalClock(); - const api = model.api; - api.root({ - text: '1234', - slices: [], - }); - const peritext = new Peritext(model, api.str(['text']).node, api.arr(['slices']).node); - const str = peritext.str; - const id1 = str.find(1)!; - const id2 = str.find(2)!; - const p1 = peritext.point(id1, Anchor.After); - const p2 = peritext.point(id2, Anchor.After); - peritext.editor.cursor.set(p1, p2); - peritext.editor.insStackSlice(3); - peritext.refresh(); - peritext.editor.cursor.setAt(2, 1); - peritext.editor.insStackSlice(33); - peritext.refresh(); - const cnt = size(peritext.overlay.root); - expect(cnt).toBe(3); - }); -}); diff --git a/src/json-crdt-extensions/peritext/slice/Slices.ts b/src/json-crdt-extensions/peritext/slice/Slices.ts index 42f4ecab9d..a4cb00a804 100644 --- a/src/json-crdt-extensions/peritext/slice/Slices.ts +++ b/src/json-crdt-extensions/peritext/slice/Slices.ts @@ -76,7 +76,7 @@ export class Slices implements Stateful, Printable { return slice; } - public insMarker(range: Range, type: SliceType, data?: unknown): MarkerSlice { + public insMarker(range: Range, type: SliceType, data?: unknown | ITimestampStruct): MarkerSlice { return this.ins(range, SliceBehavior.Marker, type, data) as MarkerSlice; } @@ -103,15 +103,15 @@ export class Slices implements Stateful, Printable { return this.insMarker(range, type, data); } - public insStack(range: Range, type: SliceType, data?: unknown): PersistedSlice { + public insStack(range: Range, type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { return this.ins(range, SliceBehavior.Stack, type, data); } - public insOverwrite(range: Range, type: SliceType, data?: unknown): PersistedSlice { + public insOverwrite(range: Range, type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { return this.ins(range, SliceBehavior.Overwrite, type, data); } - public insErase(range: Range, type: SliceType, data?: unknown): PersistedSlice { + public insErase(range: Range, type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { return this.ins(range, SliceBehavior.Erase, type, data); } From 7702e9847907828c177d0902dfa9fd0a5b571aea Mon Sep 17 00:00:00 2001 From: streamich Date: Mon, 6 May 2024 16:30:34 +0200 Subject: [PATCH 04/13] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20improve=20how=20Point=20handles=20absolute=20end?= =?UTF-8?q?=20position?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/constants.ts | 15 +++++++++++++++ src/json-crdt-extensions/peritext/rga/Point.ts | 5 +++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/json-crdt-extensions/peritext/constants.ts b/src/json-crdt-extensions/peritext/constants.ts index edf41fd29d..00d2b3e538 100644 --- a/src/json-crdt-extensions/peritext/constants.ts +++ b/src/json-crdt-extensions/peritext/constants.ts @@ -6,6 +6,21 @@ export const enum Chars { BlockSplitSentinel = '\n', } +export const enum Position { + /** + * Specifies the absolute start of the text, i.e. the position before the + * first character. In model space it is defined as string ID and "after" + * anchor. + */ + AbsStart = -1, + + /** + * Specifies the absolute end of the text, i.e. the position after the last + * character. In model space it is defined as string ID and "before" anchor. + */ + AbsEnd = 9007199254740991, // Number.MAX_SAFE_INTEGER +} + export const MNEMONIC = ExtensionName[ExtensionId.peritext]; export const SCHEMA = (text: string) => diff --git a/src/json-crdt-extensions/peritext/rga/Point.ts b/src/json-crdt-extensions/peritext/rga/Point.ts index ed74960797..5b30caad6c 100644 --- a/src/json-crdt-extensions/peritext/rga/Point.ts +++ b/src/json-crdt-extensions/peritext/rga/Point.ts @@ -2,6 +2,7 @@ import {compare, type ITimestampStruct, printTs, equal, tick, containsId} from ' import {Anchor} from './constants'; import {ChunkSlice} from '../util/ChunkSlice'; import {updateId} from '../../../json-crdt/hash'; +import {Position} from '../constants'; import type {AbstractRga, Chunk} from '../../../json-crdt/nodes/rga'; import type {Stateful} from '../types'; import type {Printable} from 'tree-dump/lib/types'; @@ -125,7 +126,7 @@ export class Point implements Pick, Printable { */ public pos(): number { const chunk = this.chunk(); - if (!chunk) return -1; + if (!chunk) return this.isAbsEnd() ? Position.AbsEnd : Position.AbsStart; const pos = this.rga.pos(chunk); if (chunk.del) return pos; return pos + this.id.time - chunk.id.time; @@ -453,6 +454,6 @@ export class Point implements Pick, Printable { const pos = this.pos(); const id = printTs(this.id); const anchor = this.anchor === Anchor.Before ? '.▢' : '▢.'; - return `${name}{ ${pos}, ${id}, ${anchor} }`; + return `${name}{ ${pos === Position.AbsEnd ? '∞' : pos}, ${id}, ${anchor} }`; } } From 0d1254b07dd791ed1bebb5777911c93556568919 Mon Sep 17 00:00:00 2001 From: streamich Date: Mon, 6 May 2024 20:32:02 +0200 Subject: [PATCH 05/13] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20handle=20abs=20end=20in=20getOrNextHigher()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/__tests__/setup.ts | 51 +++++++++++++++++++ .../peritext/overlay/Overlay.ts | 4 ++ .../__tests__/Overlay.getOrNextLH.spec.ts | 40 +++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 src/json-crdt-extensions/peritext/__tests__/setup.ts diff --git a/src/json-crdt-extensions/peritext/__tests__/setup.ts b/src/json-crdt-extensions/peritext/__tests__/setup.ts new file mode 100644 index 0000000000..0d20209ce1 --- /dev/null +++ b/src/json-crdt-extensions/peritext/__tests__/setup.ts @@ -0,0 +1,51 @@ +import {s} from "../../../json-crdt-patch"; +import {ModelWithExt, ext} from "../../ModelWithExt"; + +/** + * Creates a Peritext instance with text "0123456789", with single-char and + * block-wise chunks, as well as with plenty of tombstones. + */ +export const setupNumbersWithTombstones = () => { + const schema = s.obj({ + text: ext.peritext.new('1234'), + }); + const model = ModelWithExt.create(schema); + const str = model.s.text.toExt().text(); + str.ins(1, '234'); + str.ins(2, '345'); + str.ins(3, '456'); + str.ins(4, '567'); + str.ins(5, '678'); + str.ins(6, '789'); + str.del(7, 1); + str.del(8, 1); + str.ins(0, '0'); + str.del(1, 4); + str.del(2, 1); + str.ins(1, '1'); + str.del(0, 1); + str.ins(0, '0'); + str.ins(2, '234'); + str.del(4, 7); + str.del(4, 2); + str.del(7, 3); + str.ins(6, '6789'); + str.del(7, 2); + str.ins(7, '78'); + str.del(10, 2); + str.del(2, 3); + str.ins(2, '234'); + if (str.view() !== '0123456789') throw new Error('Invalid text'); + const api = model.api; + const peritextApi = model.s.text.toExt(); + const peritext = peritextApi.txt; + const editor = peritextApi.editor; + return { + schema, + model, + api, + peritextApi, + peritext, + editor, + }; +}; diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index e4605518ea..54a5159310 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -78,6 +78,10 @@ export class Overlay implements Printable, Stateful { * Retrieve overlay point or the next one, measured in spacial dimension. */ public getOrNextHigher(point: Point): OverlayPoint | undefined { + if (point.isAbsEnd()) point = this.txt.pointEnd()!; + else if (point.isAbsStart()) { + return undefined; + } let curr: OverlayPoint | undefined = this.root; let result: OverlayPoint | undefined = undefined; while (curr) { diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts index 7dd21470ae..06d89c3e4a 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts @@ -2,6 +2,7 @@ import {Model} from '../../../../json-crdt/model'; import {size} from 'sonic-forest/lib/util'; import {Peritext} from '../../Peritext'; import {Anchor} from '../../rga/constants'; +import {setupNumbersWithTombstones} from '../../__tests__/setup'; describe('.getOrNextLower()', () => { test('combines overlay points - right anchor', () => { @@ -90,4 +91,43 @@ describe('.getOrNextHigher()', () => { expect(overlayPoint.layers.length).toBe(1); expect(overlayPoint.layers[0]).toBe(slice1); }); + + test('can find points at the relative end', () => { + const {peritext, editor} = setupNumbersWithTombstones(); + editor.cursor.setAt(9, 1); + peritext.refresh(); + let overlayPoint = peritext.overlay.getOrNextHigher(editor.cursor.end)!; + expect(overlayPoint.layers.length).toBe(0); + overlayPoint = peritext.overlay.getOrNextHigher(editor.cursor.start)!; + expect(overlayPoint.layers.length).toBe(1); + expect(overlayPoint.layers[0]).toBe(editor.cursor); + }); + + test('can find points at the relative end, when absolute end provided', () => { + const {peritext, editor} = setupNumbersWithTombstones(); + editor.cursor.setAt(9, 1); + peritext.refresh(); + const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsEnd())!; + expect(overlayPoint.layers.length).toBe(0); + }); + + test('returns undefined, when absolute start provided', () => { + const {peritext, editor} = setupNumbersWithTombstones(); + editor.cursor.setAt(9, 1); + peritext.refresh(); + const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsStart())!; + expect(overlayPoint).toBe(undefined); + }); + + describe('when all text selected, using absolute range', () => { + test.skip('...', () => { + const {peritext, editor} = setupNumbersWithTombstones(); + const range = peritext.range(peritext.pointAbsStart(), peritext.pointAbsEnd()); + editor.cursor.setRange(range); + peritext.refresh(); + console.log(peritext + ''); + // const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsStart())!; + // expect(overlayPoint).toBe(undefined); + }); + }); }); From cb98052b1d33cba95a9d5aa8c75c5c4ca76cea22 Mon Sep 17 00:00:00 2001 From: streamich Date: Mon, 6 May 2024 20:32:47 +0200 Subject: [PATCH 06/13] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20implement=20Overlay=20traversal=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/Overlay.ts | 240 ++++++++++++++++-- .../overlay/__tests__/Overlay.refresh.spec.ts | 24 ++ 2 files changed, 242 insertions(+), 22 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 54a5159310..55af783ded 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -7,10 +7,11 @@ import {Point} from '../rga/Point'; import {OverlayPoint} from './OverlayPoint'; import {MarkerOverlayPoint} from './MarkerOverlayPoint'; import {OverlayRefSliceEnd, OverlayRefSliceStart} from './refs'; -import {equal, ITimestampStruct} from '../../../json-crdt-patch/clock'; +import {compare, ITimestampStruct, tick} from '../../../json-crdt-patch/clock'; import {CONST, updateNum} from '../../../json-hash'; import {MarkerSlice} from '../slice/MarkerSlice'; -import {firstVis} from '../../../json-crdt/nodes/rga/util'; +import {Range} from '../rga/Range'; +import type {Chunk} from '../../../json-crdt/nodes/rga'; import type {Peritext} from '../Peritext'; import type {Stateful} from '../types'; import type {Printable} from 'tree-dump/lib/types'; @@ -26,8 +27,19 @@ import type {Slices} from '../slice/Slices'; */ export class Overlay implements Printable, Stateful { public root: OverlayPoint | undefined = undefined; + public readonly start: OverlayPoint; - constructor(protected readonly txt: Peritext) {} + constructor(protected readonly txt: Peritext) { + this.start = this.point(this.txt.str.id, Anchor.After); + } + + private point(id: ITimestampStruct, anchor: Anchor): OverlayPoint { + return new OverlayPoint(this.txt.str, id, anchor); + } + + private mPoint(marker: MarkerSlice, anchor: Anchor): MarkerOverlayPoint { + return new MarkerOverlayPoint(this.txt.str, marker.start.id, anchor, marker); + } public first(): OverlayPoint | undefined { return this.root ? first(this.root) : undefined; @@ -42,6 +54,14 @@ export class Overlay implements Printable, Stateful { }; } + public all(): OverlayPoint[] { + const iterator = this.iterator(); + let point: OverlayPoint | undefined; + const points: OverlayPoint[] = []; + while (point = iterator()) points.push(point); + return points; + } + public splitIterator(): () => MarkerOverlayPoint | undefined { let curr = this.first(); return () => { @@ -107,6 +127,201 @@ export class Overlay implements Printable, Stateful { return undefined; } + public chunkSlices0( + chunk: Chunk | undefined, + p1: Point, + p2: Point, + callback: (chunk: Chunk, off: number, len: number) => void, + ): Chunk | undefined { + const rga = this.txt.str; + const strId = rga.id; + let checkFirstAnchor = p1.anchor === Anchor.After; + const adjustForLastAnchor = p2.anchor === Anchor.Before; + let id1 = p1.id; + const id1IsStr = !compare(id1, strId); + if (id1IsStr) { + const first = rga.first(); + if (!first) return; + id1 = first.id; + checkFirstAnchor = false; + } + const id2 = p2.id; + if (!checkFirstAnchor && !adjustForLastAnchor) { + return rga.range0(chunk, id1, id2, callback) as Chunk; + } + const sid1 = id1.sid; + const time1 = id1.time; + const sid2 = id2.sid; + const time2 = id2.time; + return rga.range0(undefined, id1, id2, (chunk: Chunk, off: number, len: number) => { + if (checkFirstAnchor) { + checkFirstAnchor = false; + const chunkId = chunk.id; + if (chunkId.sid === sid1 && chunkId.time + off === time1) { + if (len <= 1) return; + off += 1; + len -= 1; + } + } + if (adjustForLastAnchor) { + const chunkId = chunk.id; + if (chunkId.sid === sid2 && chunkId.time + off + len - 1 === time2) { + if (len <= 1) return; + len -= 1; + } + } + callback(chunk, off, len); + }) as Chunk; + } + + public points0( + start: undefined | OverlayPoint, + end: undefined | ((next: OverlayPoint) => boolean), + callback: (point: OverlayPoint) => void, + ): void { + const txt = this.txt; + const str = txt.str; + const strFirstChunk = str.first(); + if (!strFirstChunk) return; + let point = start || this.first(); + let prev: typeof point; + const pointIsStart = + point && + ((!compare(point.id, str.id) && point.anchor === Anchor.After) || + (!compare(strFirstChunk.id, point.id) && point.anchor === Anchor.Before)); + if (!start && !pointIsStart) { + const startPoint = this.start; + startPoint.id = strFirstChunk.id; + startPoint.anchor = Anchor.Before; + callback(startPoint); + } + while (point) { + if (end && end(point)) return; + callback(point); + prev = point; + point = next(point); + } + const strLastChunk = str.last()!; + const strLastChunkId = strLastChunk.id; + if (prev) { + const prevId = prev.id; + if ( + prev.anchor === Anchor.After && + prevId.time === strLastChunkId.time + strLastChunk.span - 1 && + prevId.sid === strLastChunkId.sid && + prevId.sid === strLastChunkId.sid + ) + return; + } + const endId = strLastChunk.span > 1 ? tick(strLastChunkId, strLastChunk.span - 1) : strLastChunkId; + const ending = this.point(endId!, Anchor.After); + if (end && end(ending)) return; + callback(ending); + } + + public points1( + start: undefined | OverlayPoint, + end: undefined | ((next: OverlayPoint) => boolean), + callback: (p1: OverlayPoint, p2: OverlayPoint) => void, + ): void { + let p1: OverlayPoint | undefined; + let p2: OverlayPoint | undefined; + this.points0(start, end, (point) => { + if (p1) { + p2 = point; + callback(p1, p2); + p1 = p2; + } else { + p1 = point; + } + }); + } + + public findContained(range: Range): Set> { + const result = new Set>(); + let point = this.getOrNextLower(range.start); + if (!point) return result; + do { + if (!range.containsPoint(point)) continue; + const slices = point.layers; + const length = slices.length; + for (let i = 0; i < length; i++) { + const slice = slices[i]; + if (!result.has(slice) && range.contains(slice)) result.add(slice); + } + if (point instanceof MarkerOverlayPoint) { + const marker = point.marker; + if (marker && !result.has(marker) && range.contains(marker)) result.add(marker); + } + } while (point && (point = next(point)) && range.containsPoint(point)); + return result; + } + + public findOverlapping(range: Range): Set> { + const result = new Set>(); + let point = this.getOrNextLower(range.start); + if (!point) return result; + do { + const slices = point.layers; + const length = slices.length; + for (let i = 0; i < length; i++) result.add(slices[i]); + if (point instanceof MarkerOverlayPoint) { + const marker = point.marker; + if (marker) result.add(marker); + } + } while (point && (point = next(point)) && range.containsPoint(point)); + return result; + } + + public leadingTextHash: number = 0; + + protected computeSplitTextHashes(): void { + const txt = this.txt; + const str = txt.str; + const firstChunk = str.first(); + if (!firstChunk) return; + let chunk: Chunk | undefined = firstChunk; + let marker: MarkerOverlayPoint | undefined = undefined; + let state: number = CONST.START_STATE; + this.points1(undefined, undefined, (p1, p2) => { + // 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; + chunk = this.chunkSlices0(chunk || firstChunk, p1, p2, (chunk, off, len) => { + const id = chunk.id; + overlayPointHash = + (overlayPointHash << 5) + overlayPointHash + ((((id.sid >>> 0) + id.time) << 8) + (off << 4) + len); + }); + state = updateNum(state, overlayPointHash); + if (p1) { + p1.hash = overlayPointHash; + } + if (p2 instanceof MarkerOverlayPoint) { + if (marker) { + marker.textHash = state; + } else { + this.leadingTextHash = state; + } + state = CONST.START_STATE; + marker = p2; + } + }); + if ((marker as any) instanceof MarkerOverlayPoint) { + (marker as any as MarkerOverlayPoint).textHash = state; + } else { + this.leadingTextHash = state; + } + } + + public isBlockSplit(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 + ); + } + // ----------------------------------------------------------------- Stateful public hash: number = 0; @@ -156,14 +371,6 @@ export class Overlay implements Printable, Stateful { return state; } - private point(id: ITimestampStruct, anchor: Anchor): OverlayPoint { - return new OverlayPoint(this.txt.str, id, anchor); - } - - private mPoint(marker: MarkerSlice, anchor: Anchor): MarkerOverlayPoint { - return new MarkerOverlayPoint(this.txt.str, marker.start.id, anchor, marker); - } - /** * Retrieve an existing {@link OverlayPoint} or create a new one, inserted * in the tree, sorted by spatial dimension. @@ -217,17 +424,6 @@ export class Overlay implements Printable, Stateful { const str = txt.str; let startPoint = slice.start; let endPoint = slice.end; - const startIsStringRoot = equal(startPoint.id, str.id); - if (startIsStringRoot) { - const firstVisibleChunk = firstVis(txt.str); - if (firstVisibleChunk) { - startPoint = txt.point(firstVisibleChunk.id, Anchor.Before); - const endIsStringRoot = equal(endPoint.id, str.id); - if (endIsStringRoot) { - endPoint = txt.point(firstVisibleChunk.id, Anchor.Before); - } - } - } const [start, isStartNew] = this.upsertPoint(startPoint); const [end, isEndNew] = this.upsertPoint(endPoint); start.refs.push(new OverlayRefSliceStart(slice)); 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 8ef70930d5..15a5ec746a 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 @@ -1,5 +1,6 @@ import {Model, ObjApi} from '../../../../json-crdt/model'; import {Peritext} from '../../Peritext'; +import {setupNumbersWithTombstones} from '../../__tests__/setup'; import {Anchor} from '../../rga/constants'; import {SliceBehavior} from '../../slice/constants'; @@ -22,6 +23,29 @@ const setup = () => { type Kit = ReturnType; describe('Overlay.refresh()', () => { + test('can select all text using relative range', () => { + const {peritext, editor} = setupNumbersWithTombstones(); + const overlay = peritext.overlay; + const range = peritext.range(peritext.pointStart()!, peritext.pointEnd()!); + editor.cursor.setRange(range); + peritext.refresh(); + expect(editor.cursor.text()).toBe('0123456789'); + const overlayPoints = overlay.all(); + expect(overlayPoints.length).toBe(2); + expect(overlayPoints[0].id.time).toBe(editor.cursor.start.id.time); + expect(overlayPoints[1].id.time).toBe(editor.cursor.end.id.time); + }); + + test.only('can select all text using absolute range', () => { + const {peritext, editor} = setupNumbersWithTombstones(); + const range = peritext.range(peritext.pointAbsStart(), peritext.pointAbsEnd()); + editor.cursor.setRange(range); + peritext.refresh(); + console.log(peritext + ''); + // const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsStart())!; + // expect(overlayPoint).toBe(undefined); + }); + const testRefresh = (name: string, update: (kit: Kit, refresh: () => void) => void) => { test(name, () => { const kit = setup(); From a756ff7a947bdadfa81965afc82a6f6eb99ae552 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Tue, 7 May 2024 01:11:02 +0200 Subject: [PATCH 07/13] =?UTF-8?q?refactor(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=92=A1=20move=20around=20refresh=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/Overlay.ts | 108 ++++++++---------- .../overlay/__tests__/Overlay.refresh.spec.ts | 11 +- .../overlay/__tests__/Overlay.spec.ts | 21 ++-- 3 files changed, 68 insertions(+), 72 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 55af783ded..26b24c6f9a 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -332,7 +332,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) this.computeSplitTextHashes(); return (this.hash = hash); } @@ -355,7 +355,7 @@ export class Overlay implements Printable, Stateful { if (positionMoved) this.delSlice(slice, tuple); else return; } - tuple = this.insSlice(slice); + tuple = slice instanceof MarkerSlice ? this.insMarker(slice) : this.insSlice(slice); this.slices.set(slice, tuple); }); if (slices.size() < sliceSet.size) { @@ -371,6 +371,54 @@ export class Overlay implements Printable, Stateful { return state; } + private insSlice(slice: Slice): [start: OverlayPoint, end: OverlayPoint] { + console.log('ins slice', slice+'') + const x0 = slice.start; + const x1 = slice.end; + const [start, isStartNew] = this.upsertPoint(x0); + const [end, isEndNew] = this.upsertPoint(x1); + start.refs.push(new OverlayRefSliceStart(slice)); + end.refs.push(new OverlayRefSliceEnd(slice)); + if (isStartNew) { + const beforeStartPoint = prev(start); + if (beforeStartPoint) start.layers.push(...beforeStartPoint.layers); + } + if (isEndNew) { + const beforeEndPoint = prev(end); + if (beforeEndPoint) end.layers.push(...beforeEndPoint.layers); + } + let curr: OverlayPoint | undefined = start; + do curr.addLayer(slice); while ((curr = next(curr)) && (curr !== end)); + const isCollapsed = x0.cmp(x1) === 0; + if (isCollapsed) start.addMarker(slice); + return [start, end]; + } + + private insMarker(slice: MarkerSlice): [start: OverlayPoint, end: OverlayPoint] { + const point = this.mPoint(slice, Anchor.Before); + const pivot = this.insPoint(point); + if (!pivot) { + point.refs.push(slice); + const prevPoint = prev(point); + if (prevPoint) point.layers.push(...prevPoint.layers); + } + return [point, point]; + } + + private delSlice(slice: Slice, [start, end]: [start: OverlayPoint, end: OverlayPoint]): void { + this.slices.delete(slice); + let curr: OverlayPoint | undefined = start; + do { + curr.removeLayer(slice); + curr.removeMarker(slice); + curr = next(curr); + } while (curr && curr !== end); + start.removeRef(slice); + end.removeRef(slice); + if (!start.refs.length) this.delPoint(start); + if (!end.refs.length && start !== end) this.delPoint(end); + } + /** * Retrieve an existing {@link OverlayPoint} or create a new one, inserted * in the tree, sorted by spatial dimension. @@ -407,62 +455,6 @@ export class Overlay implements Printable, Stateful { this.root = remove(this.root, point); } - private insMarker(slice: MarkerSlice): [start: OverlayPoint, end: OverlayPoint] { - const point = this.mPoint(slice, Anchor.Before); - const pivot = this.insPoint(point); - if (!pivot) { - point.refs.push(slice); - const prevPoint = prev(point); - if (prevPoint) point.layers.push(...prevPoint.layers); - } - return [point, point]; - } - - private insSlice(slice: Slice): [start: OverlayPoint, end: OverlayPoint] { - if (slice instanceof MarkerSlice) return this.insMarker(slice); - const txt = this.txt; - const str = txt.str; - let startPoint = slice.start; - let endPoint = slice.end; - const [start, isStartNew] = this.upsertPoint(startPoint); - const [end, isEndNew] = this.upsertPoint(endPoint); - start.refs.push(new OverlayRefSliceStart(slice)); - end.refs.push(new OverlayRefSliceEnd(slice)); - if (isStartNew) { - const beforeStartPoint = prev(start); - if (beforeStartPoint) start.layers.push(...beforeStartPoint.layers); - } - if (isEndNew) { - const beforeEndPoint = prev(end); - if (beforeEndPoint) end.layers.push(...beforeEndPoint.layers); - } - const isCollapsed = startPoint.cmp(endPoint) === 0; - let curr: OverlayPoint | undefined = start; - while (curr !== end && curr) { - curr.addLayer(slice); - curr = next(curr); - } - if (!isCollapsed) { - } else { - start.addMarker(slice); - } - return [start, end]; - } - - private delSlice(slice: Slice, [start, end]: [start: OverlayPoint, end: OverlayPoint]): void { - this.slices.delete(slice); - let curr: OverlayPoint | undefined = start; - do { - curr.removeLayer(slice); - curr.removeMarker(slice); - curr = next(curr); - } while (curr && curr !== end); - start.removeRef(slice); - end.removeRef(slice); - if (!start.refs.length) this.delPoint(start); - if (!end.refs.length && start !== end) this.delPoint(end); - } - // ---------------------------------------------------------------- Printable public toString(tab: string = ''): string { 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 15a5ec746a..48002b86c7 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 @@ -36,14 +36,17 @@ describe('Overlay.refresh()', () => { expect(overlayPoints[1].id.time).toBe(editor.cursor.end.id.time); }); - test.only('can select all text using absolute range', () => { + test('can select all text using absolute range', () => { const {peritext, editor} = setupNumbersWithTombstones(); + const overlay = peritext.overlay; const range = peritext.range(peritext.pointAbsStart(), peritext.pointAbsEnd()); editor.cursor.setRange(range); peritext.refresh(); - console.log(peritext + ''); - // const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsStart())!; - // expect(overlayPoint).toBe(undefined); + expect(editor.cursor.text()).toBe('0123456789'); + const overlayPoints = overlay.all(); + expect(overlayPoints.length).toBe(2); + expect(overlayPoints[0].id.time).toBe(editor.cursor.start.id.time); + expect(overlayPoints[1].id.time).toBe(editor.cursor.end.id.time); }); const testRefresh = (name: string, update: (kit: Kit, refresh: () => void) => void) => { 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 71ac4585ce..064be1879a 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts @@ -200,21 +200,22 @@ describe('slices', () => { expect(point2.anchor).toBe(Anchor.After); }); - test('intersecting slice before split, should not update the split', () => { + test.only('intersecting slice before split, should not update the split', () => { const {peritext} = setup(); peritext.editor.cursor.setAt(6); - const slice = peritext.editor.insMarker(['p']); + peritext.editor.insMarker(['p']); peritext.refresh(); + console.log(peritext + ''); const point = peritext.overlay.find((point) => point instanceof MarkerOverlayPoint)!; expect(point.layers.length).toBe(0); - peritext.editor.cursor.setAt(2, 2); - peritext.editor.insStackSlice(''); - peritext.refresh(); - expect(point.layers.length).toBe(0); - peritext.editor.cursor.setAt(2, 1); - peritext.editor.insStackSlice(''); - peritext.refresh(); - expect(point.layers.length).toBe(0); + // peritext.editor.cursor.setAt(2, 2); + // peritext.editor.insStackSlice(''); + // peritext.refresh(); + // expect(point.layers.length).toBe(0); + // peritext.editor.cursor.setAt(2, 1); + // peritext.editor.insStackSlice(''); + // peritext.refresh(); + // expect(point.layers.length).toBe(0); }); }); From 32b481dd309c32a2fe689a66e57514d7349819b5 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 7 May 2024 11:30:21 +0200 Subject: [PATCH 08/13] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20improve=20marker=20point=20treatment=20in=20over?= =?UTF-8?q?lay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/Peritext.overlay.spec.ts | 6 +++-- .../peritext/overlay/MarkerOverlayPoint.ts | 6 ++++- .../peritext/overlay/Overlay.ts | 25 +++++++++++-------- .../overlay/__tests__/Overlay.spec.ts | 19 +++++++------- .../peritext/rga/Point.ts | 3 ++- 5 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts index 917aafb421..c1f75e8f51 100644 --- a/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts @@ -19,11 +19,13 @@ const setup = () => { test('can insert markers', () => { const {peritext} = setup(); const {editor} = peritext; - expect(size(peritext.overlay.root)).toBe(0); + expect(peritext.overlay.all().length).toBe(0); editor.cursor.setAt(0); + peritext.refresh(); + expect(peritext.overlay.all().length).toBe(1); editor.insMarker(['p'], '

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

'); peritext.refresh(); diff --git a/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts b/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts index ff9db977a6..90fa2d0dd2 100644 --- a/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts +++ b/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts @@ -46,6 +46,10 @@ export class MarkerOverlayPoint extends OverlayPoint { } public toString(tab: string = '', lite?: boolean): string { - return super.toString(tab, lite) + (lite ? '' : printTree(tab, [(tab) => this.marker.toString(tab)])); + return this.toStringName(tab, lite) + (lite ? '' : printTree(tab, [ + (tab) => this.marker.toString(tab), + ...this.layers.map((slice) => (tab: string) => slice.toString(tab)), + ...this.markers.map((slice) => (tab: string) => slice.toString(tab)), + ])); } } diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 26b24c6f9a..588feb66ce 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -124,7 +124,7 @@ export class Overlay implements Printable, Stateful { if (predicate(point)) return point; point = next(point); } - return undefined; + return; } public chunkSlices0( @@ -372,25 +372,30 @@ export class Overlay implements Printable, Stateful { } private insSlice(slice: Slice): [start: OverlayPoint, end: OverlayPoint] { - console.log('ins slice', slice+'') + // 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); const [end, isEndNew] = this.upsertPoint(x1); + const isCollapsed = x0.cmp(x1) === 0; start.refs.push(new OverlayRefSliceStart(slice)); end.refs.push(new OverlayRefSliceEnd(slice)); if (isStartNew) { const beforeStartPoint = prev(start); if (beforeStartPoint) start.layers.push(...beforeStartPoint.layers); } - if (isEndNew) { - const beforeEndPoint = prev(end); - if (beforeEndPoint) end.layers.push(...beforeEndPoint.layers); + if (!isCollapsed) { + + if (isEndNew) { + const beforeEndPoint = prev(end); + if (beforeEndPoint) end.layers.push(...beforeEndPoint.layers); + } + 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); } - let curr: OverlayPoint | undefined = start; - do curr.addLayer(slice); while ((curr = next(curr)) && (curr !== end)); - const isCollapsed = x0.cmp(x1) === 0; - if (isCollapsed) start.addMarker(slice); return [start, end]; } @@ -448,7 +453,7 @@ export class Overlay implements Printable, Stateful { else insertLeft(point, pivot); } if (this.root !== point) this.root = splay(this.root!, point, 10); - return undefined; + return; } private delPoint(point: OverlayPoint): void { 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 064be1879a..c1d1af857c 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.spec.ts @@ -200,22 +200,21 @@ describe('slices', () => { expect(point2.anchor).toBe(Anchor.After); }); - test.only('intersecting slice before split, should not update the split', () => { + test('intersecting slice before split, should not update the split', () => { const {peritext} = setup(); peritext.editor.cursor.setAt(6); peritext.editor.insMarker(['p']); peritext.refresh(); - console.log(peritext + ''); const point = peritext.overlay.find((point) => point instanceof MarkerOverlayPoint)!; expect(point.layers.length).toBe(0); - // peritext.editor.cursor.setAt(2, 2); - // peritext.editor.insStackSlice(''); - // peritext.refresh(); - // expect(point.layers.length).toBe(0); - // peritext.editor.cursor.setAt(2, 1); - // peritext.editor.insStackSlice(''); - // peritext.refresh(); - // expect(point.layers.length).toBe(0); + peritext.editor.cursor.setAt(2, 2); + peritext.editor.insStackSlice(''); + peritext.refresh(); + expect(point.layers.length).toBe(0); + peritext.editor.cursor.setAt(2, 1); + peritext.editor.insStackSlice(''); + peritext.refresh(); + expect(point.layers.length).toBe(0); }); }); diff --git a/src/json-crdt-extensions/peritext/rga/Point.ts b/src/json-crdt-extensions/peritext/rga/Point.ts index 5b30caad6c..9e5edf94e4 100644 --- a/src/json-crdt-extensions/peritext/rga/Point.ts +++ b/src/json-crdt-extensions/peritext/rga/Point.ts @@ -138,7 +138,8 @@ export class Point implements Pick, Printable { */ public viewPos(): number { const pos = this.pos(); - if (pos < 0) return this.isAbsStart() ? 0 : this.rga.length(); + const isAbs = equal(this.rga.id, this.id); + if (isAbs) return this.anchor === Anchor.After ? 0 : this.rga.length(); return this.anchor === Anchor.Before ? pos : pos + 1; } From fdf07446b08948f53d3a17e8d6b56086afb14fa6 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 7 May 2024 12:37:21 +0200 Subject: [PATCH 09/13] =?UTF-8?q?fix(json-crdt-extensions):=20=F0=9F=90=9B?= =?UTF-8?q?=20improve=20.getOrNextHigher()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/Overlay.ts | 16 +++++++++---- .../__tests__/Overlay.getOrNextLH.spec.ts | 24 +++++++++++++++---- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 588feb66ce..e527632033 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -1,6 +1,6 @@ import {printTree} from 'tree-dump/lib/printTree'; import {printBinary} from 'tree-dump/lib/printBinary'; -import {first, insertLeft, insertRight, next, prev, remove} from 'sonic-forest/lib/util'; +import {first, insertLeft, insertRight, last, next, prev, remove} from 'sonic-forest/lib/util'; import {splay} from 'sonic-forest/lib/splay/util'; import {Anchor} from '../rga/constants'; import {Point} from '../rga/Point'; @@ -45,6 +45,10 @@ export class Overlay implements Printable, Stateful { return this.root ? first(this.root) : undefined; } + public last(): OverlayPoint | undefined { + return this.root ? last(this.root) : undefined; + } + public iterator(): () => OverlayPoint | undefined { let curr = this.first(); return () => { @@ -98,9 +102,11 @@ export class Overlay implements Printable, Stateful { * Retrieve overlay point or the next one, measured in spacial dimension. */ public getOrNextHigher(point: Point): OverlayPoint | undefined { - if (point.isAbsEnd()) point = this.txt.pointEnd()!; - else if (point.isAbsStart()) { - return undefined; + if (point.isAbsEnd()) { + const last = this.last(); + if (!last) return; + if (last.isAbsEnd()) return last; + point = last; } let curr: OverlayPoint | undefined = this.root; let result: OverlayPoint | undefined = undefined; @@ -111,7 +117,7 @@ export class Overlay implements Printable, Stateful { else { const next = curr.l; result = curr; - if (!next) return result; + if (!next) return; curr = next; } } diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts index 06d89c3e4a..b04e9301e4 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts @@ -3,6 +3,8 @@ import {size} from 'sonic-forest/lib/util'; import {Peritext} from '../../Peritext'; import {Anchor} from '../../rga/constants'; import {setupNumbersWithTombstones} from '../../__tests__/setup'; +import {OverlayPoint} from '../OverlayPoint'; +import {OverlayRefSliceEnd, OverlayRefSliceStart} from '../refs'; describe('.getOrNextLower()', () => { test('combines overlay points - right anchor', () => { @@ -120,14 +122,28 @@ describe('.getOrNextHigher()', () => { }); describe('when all text selected, using absolute range', () => { - test.skip('...', () => { + test('can select the ending point', () => { const {peritext, editor} = setupNumbersWithTombstones(); const range = peritext.range(peritext.pointAbsStart(), peritext.pointAbsEnd()); editor.cursor.setRange(range); peritext.refresh(); - console.log(peritext + ''); - // const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsStart())!; - // expect(overlayPoint).toBe(undefined); + const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsEnd())!; + expect(overlayPoint).toBeInstanceOf(OverlayPoint); + expect(overlayPoint.refs.length).toBe(1); + expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceEnd(editor.cursor)); + }); + + test('can select the start point', () => { + const {peritext, editor} = setupNumbersWithTombstones(); + const range = peritext.range(peritext.pointAbsStart(), peritext.pointAbsEnd()); + editor.cursor.setRange(range); + peritext.refresh(); + const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsStart()!)!; + expect(overlayPoint).toBeInstanceOf(OverlayPoint); + expect(overlayPoint.refs.length).toBe(1); + expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceStart(editor.cursor)); + expect(overlayPoint.layers.length).toBe(1); + expect(overlayPoint.layers[0]).toEqual(editor.cursor); }); }); }); From a88f9d8937efad4b2495b6ac029816a92be8aff6 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 7 May 2024 13:10:37 +0200 Subject: [PATCH 10/13] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20support=20absolut=20positions=20in=20higher/lowe?= =?UTF-8?q?r=20iteration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/Overlay.ts | 10 +- .../__tests__/Overlay.getOrNextLH.spec.ts | 140 +++++++++++++++--- 2 files changed, 124 insertions(+), 26 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index e527632033..bbe2321c61 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -82,6 +82,12 @@ export class Overlay implements Printable, Stateful { * Retrieve overlay point or the previous one, measured in spacial dimension. */ public getOrNextLower(point: Point): OverlayPoint | undefined { + if (point.isAbsStart()) { + const first = this.first(); + if (!first) return; + if (first.isAbsStart()) return first; + point = first; + } else if (point.isAbsEnd()) return this.last(); let curr: OverlayPoint | undefined = this.root; let result: OverlayPoint | undefined = undefined; while (curr) { @@ -107,7 +113,7 @@ export class Overlay implements Printable, Stateful { if (!last) return; if (last.isAbsEnd()) return last; point = last; - } + } else if (point.isAbsStart()) return this.first(); let curr: OverlayPoint | undefined = this.root; let result: OverlayPoint | undefined = undefined; while (curr) { @@ -117,7 +123,7 @@ export class Overlay implements Printable, Stateful { else { const next = curr.l; result = curr; - if (!next) return; + if (!next) return result; curr = next; } } diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts index b04e9301e4..6e779ecf56 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/Overlay.getOrNextLH.spec.ts @@ -52,6 +52,99 @@ describe('.getOrNextLower()', () => { const cnt = size(peritext.overlay.root); expect(cnt).toBe(3); }); + + test('can iterate through all character points', () => { + const model = Model.create(); + const api = model.api; + api.root({ + text: '1234', + slices: [], + }); + const peritext = new Peritext(model, api.str(['text']).node, api.arr(['slices']).node); + const editor = peritext.editor; + editor.cursor.setAt(0, 1); + const [slice1] = editor.saved.insStack(1); + editor.cursor.setAt(1, 1); + const [slice2] = editor.saved.insStack(2); + editor.cursor.setAt(2, 1); + const [slice3] = editor.saved.insStack(3); + editor.cursor.setAt(3, 1); + const [slice4] = editor.saved.insStack(4); + peritext.refresh(); + let overlayPoint = peritext.overlay.getOrNextLower(slice1.start)!; + expect(overlayPoint.layers.length).toBe(1); + expect(overlayPoint.layers[0]).toBe(slice1); + overlayPoint = peritext.overlay.getOrNextLower(slice1.end)!; + expect(overlayPoint.layers.length).toBe(0); + overlayPoint = peritext.overlay.getOrNextLower(slice2.start)!; + expect(overlayPoint.layers.length).toBe(1); + expect(overlayPoint.layers[0]).toBe(slice2); + overlayPoint = peritext.overlay.getOrNextLower(slice2.end)!; + expect(overlayPoint.layers.length).toBe(0); + overlayPoint = peritext.overlay.getOrNextLower(slice3.start)!; + expect(overlayPoint.layers.length).toBe(1); + expect(overlayPoint.layers[0]).toBe(slice3); + overlayPoint = peritext.overlay.getOrNextLower(slice3.end)!; + expect(overlayPoint.layers.length).toBe(0); + overlayPoint = peritext.overlay.getOrNextLower(slice4.start)!; + expect(overlayPoint.layers.length).toBe(2); + overlayPoint = peritext.overlay.getOrNextLower(slice4.end)!; + expect(overlayPoint.layers.length).toBe(0); + }); + + describe('when all text selected, using relative range', () => { + test('can select the starting point', () => { + const {peritext, editor} = setupNumbersWithTombstones(); + const range = peritext.range(peritext.pointStart()!, peritext.pointEnd()!); + editor.cursor.setRange(range); + peritext.refresh(); + const overlayPoint = peritext.overlay.getOrNextLower(peritext.pointAbsStart())!; + expect(overlayPoint).toBeInstanceOf(OverlayPoint); + expect(overlayPoint.refs.length).toBe(1); + expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceEnd(editor.cursor)); + expect(overlayPoint.layers.length).toBe(1); + expect(overlayPoint.layers[0]).toEqual(editor.cursor); + }); + + test('can select the ending point', () => { + const {peritext, editor} = setupNumbersWithTombstones(); + const range = peritext.range(peritext.pointStart()!, peritext.pointEnd()!); + editor.cursor.setRange(range); + peritext.refresh(); + const overlayPoint = peritext.overlay.getOrNextLower(peritext.pointAbsEnd())!; + expect(overlayPoint).toBeInstanceOf(OverlayPoint); + expect(overlayPoint.refs.length).toBe(1); + expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceStart(editor.cursor)); + }); + }); + + describe('when all text selected, using absolute range', () => { + test('can select the starting point', () => { + const {peritext, editor} = setupNumbersWithTombstones(); + const range = peritext.range(peritext.pointAbsStart(), peritext.pointAbsEnd()); + editor.cursor.setRange(range); + peritext.refresh(); + const overlayPoint = peritext.overlay.getOrNextLower(peritext.pointAbsStart())!; + expect(overlayPoint).toBeInstanceOf(OverlayPoint); + expect(overlayPoint.refs.length).toBe(1); + expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceEnd(editor.cursor)); + expect(overlayPoint.layers.length).toBe(1); + expect(overlayPoint.layers[0]).toEqual(editor.cursor); + }); + + test('can select the end point', () => { + const {peritext, editor} = setupNumbersWithTombstones(); + const range = peritext.range(peritext.pointAbsStart(), peritext.pointAbsEnd()); + editor.cursor.setRange(range); + peritext.refresh(); + const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsStart()!)!; + expect(overlayPoint).toBeInstanceOf(OverlayPoint); + expect(overlayPoint.refs.length).toBe(1); + expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceStart(editor.cursor)); + expect(overlayPoint.layers.length).toBe(1); + expect(overlayPoint.layers[0]).toEqual(editor.cursor); + }); + }); }); describe('.getOrNextHigher()', () => { @@ -94,31 +187,30 @@ describe('.getOrNextHigher()', () => { expect(overlayPoint.layers[0]).toBe(slice1); }); - test('can find points at the relative end', () => { - const {peritext, editor} = setupNumbersWithTombstones(); - editor.cursor.setAt(9, 1); - peritext.refresh(); - let overlayPoint = peritext.overlay.getOrNextHigher(editor.cursor.end)!; - expect(overlayPoint.layers.length).toBe(0); - overlayPoint = peritext.overlay.getOrNextHigher(editor.cursor.start)!; - expect(overlayPoint.layers.length).toBe(1); - expect(overlayPoint.layers[0]).toBe(editor.cursor); - }); - - test('can find points at the relative end, when absolute end provided', () => { - const {peritext, editor} = setupNumbersWithTombstones(); - editor.cursor.setAt(9, 1); - peritext.refresh(); - const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsEnd())!; - expect(overlayPoint.layers.length).toBe(0); - }); + describe('when all text selected, using relative range', () => { + test('can select the ending point', () => { + const {peritext, editor} = setupNumbersWithTombstones(); + const range = peritext.range(peritext.pointStart()!, peritext.pointEnd()!); + editor.cursor.setRange(range); + peritext.refresh(); + const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsEnd())!; + expect(overlayPoint).toBeInstanceOf(OverlayPoint); + expect(overlayPoint.refs.length).toBe(1); + expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceEnd(editor.cursor)); + }); - test('returns undefined, when absolute start provided', () => { - const {peritext, editor} = setupNumbersWithTombstones(); - editor.cursor.setAt(9, 1); - peritext.refresh(); - const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsStart())!; - expect(overlayPoint).toBe(undefined); + test('can select the start point', () => { + const {peritext, editor} = setupNumbersWithTombstones(); + const range = peritext.range(peritext.pointStart()!, peritext.pointEnd()!); + editor.cursor.setRange(range); + peritext.refresh(); + const overlayPoint = peritext.overlay.getOrNextHigher(peritext.pointAbsStart()!)!; + expect(overlayPoint).toBeInstanceOf(OverlayPoint); + expect(overlayPoint.refs.length).toBe(1); + expect(overlayPoint.refs[0]).toEqual(new OverlayRefSliceStart(editor.cursor)); + expect(overlayPoint.layers.length).toBe(1); + expect(overlayPoint.layers[0]).toEqual(editor.cursor); + }); }); describe('when all text selected, using absolute range', () => { From 810771d4f4618b7f23a9c06bf7ac21d7a64970c0 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 7 May 2024 13:13:16 +0200 Subject: [PATCH 11/13] =?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 --- .../peritext/__tests__/setup.ts | 4 ++-- .../peritext/editor/EditorSlices.ts | 9 +++------ .../peritext/overlay/MarkerOverlayPoint.ts | 15 ++++++++++----- .../peritext/overlay/Overlay.ts | 6 +++--- .../overlay/__tests__/Overlay.refresh.spec.ts | 2 +- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/json-crdt-extensions/peritext/__tests__/setup.ts b/src/json-crdt-extensions/peritext/__tests__/setup.ts index 0d20209ce1..81f4a09713 100644 --- a/src/json-crdt-extensions/peritext/__tests__/setup.ts +++ b/src/json-crdt-extensions/peritext/__tests__/setup.ts @@ -1,5 +1,5 @@ -import {s} from "../../../json-crdt-patch"; -import {ModelWithExt, ext} from "../../ModelWithExt"; +import {s} from '../../../json-crdt-patch'; +import {ModelWithExt, ext} from '../../ModelWithExt'; /** * Creates a Peritext instance with text "0123456789", with single-char and diff --git a/src/json-crdt-extensions/peritext/editor/EditorSlices.ts b/src/json-crdt-extensions/peritext/editor/EditorSlices.ts index 1f1d38cfa9..26a95f8e39 100644 --- a/src/json-crdt-extensions/peritext/editor/EditorSlices.ts +++ b/src/json-crdt-extensions/peritext/editor/EditorSlices.ts @@ -22,18 +22,15 @@ export class EditorSlices { } public insStack(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice[] { - return this.insAtCursors((cursor) => - this.slices.insStack(cursor.range(), type, data)); + return this.insAtCursors((cursor) => this.slices.insStack(cursor.range(), type, data)); } public insOverwrite(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice[] { - return this.insAtCursors((cursor) => - this.slices.insOverwrite(cursor.range(), type, data)); + return this.insAtCursors((cursor) => this.slices.insOverwrite(cursor.range(), type, data)); } public insErase(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice[] { - return this.insAtCursors((cursor) => - this.slices.insErase(cursor.range(), type, data)); + return this.insAtCursors((cursor) => this.slices.insErase(cursor.range(), type, data)); } public insMarker(type: SliceType, data?: unknown, separator?: string): MarkerSlice[] { diff --git a/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts b/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts index 90fa2d0dd2..3b40ea91b3 100644 --- a/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts +++ b/src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts @@ -46,10 +46,15 @@ export class MarkerOverlayPoint extends OverlayPoint { } public toString(tab: string = '', lite?: boolean): string { - return this.toStringName(tab, lite) + (lite ? '' : printTree(tab, [ - (tab) => this.marker.toString(tab), - ...this.layers.map((slice) => (tab: string) => slice.toString(tab)), - ...this.markers.map((slice) => (tab: string) => slice.toString(tab)), - ])); + return ( + this.toStringName(tab, lite) + + (lite + ? '' + : printTree(tab, [ + (tab) => this.marker.toString(tab), + ...this.layers.map((slice) => (tab: string) => slice.toString(tab)), + ...this.markers.map((slice) => (tab: string) => slice.toString(tab)), + ])) + ); } } diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index bbe2321c61..47d5a8d5f5 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -62,7 +62,7 @@ export class Overlay implements Printable, Stateful { const iterator = this.iterator(); let point: OverlayPoint | undefined; const points: OverlayPoint[] = []; - while (point = iterator()) points.push(point); + while ((point = iterator())) points.push(point); return points; } @@ -397,13 +397,13 @@ export class Overlay implements Printable, Stateful { if (beforeStartPoint) start.layers.push(...beforeStartPoint.layers); } if (!isCollapsed) { - if (isEndNew) { const beforeEndPoint = prev(end); if (beforeEndPoint) end.layers.push(...beforeEndPoint.layers); } let curr: OverlayPoint | undefined = start; - do curr.addLayer(slice); while ((curr = next(curr)) && (curr !== end)); + do curr.addLayer(slice); + while ((curr = next(curr)) && curr !== end); } else { // TODO: review if this is needed: start.addMarker(slice); 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 48002b86c7..f7010d32f5 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 @@ -48,7 +48,7 @@ describe('Overlay.refresh()', () => { expect(overlayPoints[0].id.time).toBe(editor.cursor.start.id.time); expect(overlayPoints[1].id.time).toBe(editor.cursor.end.id.time); }); - + const testRefresh = (name: string, update: (kit: Kit, refresh: () => void) => void) => { test(name, () => { const kit = setup(); From 8716cfa901a216037fe0477a09e369e6745f2b84 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 7 May 2024 15:02:01 +0200 Subject: [PATCH 12/13] =?UTF-8?q?feat(json-crdt-extensions):=20?= =?UTF-8?q?=F0=9F=8E=B8=20make=20Overlay=20an=20iterable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/__tests__/Peritext.overlay.spec.ts | 4 ++-- .../peritext/overlay/Overlay.ts | 13 +++++++------ .../overlay/__tests__/Overlay.refresh.spec.ts | 4 ++-- src/util/iterator.ts | 16 ++++++++++++++++ 4 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 src/util/iterator.ts diff --git a/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts b/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts index c1f75e8f51..eaa22f49bd 100644 --- a/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts +++ b/src/json-crdt-extensions/peritext/__tests__/Peritext.overlay.spec.ts @@ -19,10 +19,10 @@ const setup = () => { test('can insert markers', () => { const {peritext} = setup(); const {editor} = peritext; - expect(peritext.overlay.all().length).toBe(0); + expect([...peritext.overlay].length).toBe(0); editor.cursor.setAt(0); peritext.refresh(); - expect(peritext.overlay.all().length).toBe(1); + expect([...peritext.overlay].length).toBe(1); editor.insMarker(['p'], '

'); peritext.refresh(); expect(size(peritext.overlay.root)).toBe(2); diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 47d5a8d5f5..0c6ae01649 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -11,6 +11,7 @@ import {compare, ITimestampStruct, tick} from '../../../json-crdt-patch/clock'; import {CONST, updateNum} from '../../../json-hash'; import {MarkerSlice} from '../slice/MarkerSlice'; import {Range} from '../rga/Range'; +import {UndefEndIter} from '../../../util/iterator'; import type {Chunk} from '../../../json-crdt/nodes/rga'; import type {Peritext} from '../Peritext'; import type {Stateful} from '../types'; @@ -58,12 +59,12 @@ export class Overlay implements Printable, Stateful { }; } - public all(): OverlayPoint[] { - const iterator = this.iterator(); - let point: OverlayPoint | undefined; - const points: OverlayPoint[] = []; - while ((point = iterator())) points.push(point); - return points; + public entries(): IterableIterator> { + return new UndefEndIter(this.iterator()); + } + + [Symbol.iterator]() { + return this.entries(); } public splitIterator(): () => MarkerOverlayPoint | undefined { 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 f7010d32f5..94d8454aae 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 @@ -30,7 +30,7 @@ describe('Overlay.refresh()', () => { editor.cursor.setRange(range); peritext.refresh(); expect(editor.cursor.text()).toBe('0123456789'); - const overlayPoints = overlay.all(); + const overlayPoints = [...overlay]; expect(overlayPoints.length).toBe(2); expect(overlayPoints[0].id.time).toBe(editor.cursor.start.id.time); expect(overlayPoints[1].id.time).toBe(editor.cursor.end.id.time); @@ -43,7 +43,7 @@ describe('Overlay.refresh()', () => { editor.cursor.setRange(range); peritext.refresh(); expect(editor.cursor.text()).toBe('0123456789'); - const overlayPoints = overlay.all(); + const overlayPoints = [...overlay]; expect(overlayPoints.length).toBe(2); expect(overlayPoints[0].id.time).toBe(editor.cursor.start.id.time); expect(overlayPoints[1].id.time).toBe(editor.cursor.end.id.time); diff --git a/src/util/iterator.ts b/src/util/iterator.ts new file mode 100644 index 0000000000..65fd4def9b --- /dev/null +++ b/src/util/iterator.ts @@ -0,0 +1,16 @@ +export class UndefEndIter implements IterableIterator { + constructor(private readonly i: () => T | undefined) {} + + public next(): IteratorResult { + const value = this.i(); + return new IterRes(value, value === undefined) as IteratorResult; + } + + [Symbol.iterator]() { + return this; + } +} + +export class IterRes { + constructor(public readonly value: T, public readonly done: boolean) {} +} From bd738ad03d1c4348a7d61915173f01f6f991d989 Mon Sep 17 00:00:00 2001 From: streamich Date: Tue, 7 May 2024 15:28:38 +0200 Subject: [PATCH 13/13] =?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/overlay/Overlay.ts | 8 ++++++-- .../peritext/overlay/__tests__/Overlay.spec.ts | 4 ++-- src/util/iterator.ts | 5 ++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/Overlay.ts b/src/json-crdt-extensions/peritext/overlay/Overlay.ts index 0c6ae01649..e56f4c5d6f 100644 --- a/src/json-crdt-extensions/peritext/overlay/Overlay.ts +++ b/src/json-crdt-extensions/peritext/overlay/Overlay.ts @@ -67,7 +67,7 @@ export class Overlay implements Printable, Stateful { return this.entries(); } - public splitIterator(): () => MarkerOverlayPoint | undefined { + public markerIterator(): () => MarkerOverlayPoint | undefined { let curr = this.first(); return () => { while (curr) { @@ -75,10 +75,14 @@ export class Overlay implements Printable, Stateful { if (curr) curr = next(curr); if (ret instanceof MarkerOverlayPoint) return ret; } - return undefined; + return; }; } + public markers(): IterableIterator> { + return new UndefEndIter(this.iterator()); + } + /** * Retrieve overlay point or the previous one, measured in spacial dimension. */ 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 c1d1af857c..32d53ea071 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.splitIterator(); + const iterator = overlay.markerIterator(); let count = 0; for (let split = iterator(); split; split = iterator()) { count++; @@ -117,7 +117,7 @@ describe('markers', () => { expect(markerCount(peritext)).toBe(2); const points = []; let point; - for (const iterator = peritext.overlay.splitIterator(); (point = iterator()); ) points.push(point); + for (const iterator = peritext.overlay.markerIterator(); (point = iterator()); ) points.push(point); expect(points.length).toBe(2); expect(points[0].pos()).toBe(2); expect(points[1].pos()).toBe(11); diff --git a/src/util/iterator.ts b/src/util/iterator.ts index 65fd4def9b..143506549d 100644 --- a/src/util/iterator.ts +++ b/src/util/iterator.ts @@ -12,5 +12,8 @@ export class UndefEndIter implements IterableIterator { } export class IterRes { - constructor(public readonly value: T, public readonly done: boolean) {} + constructor( + public readonly value: T, + public readonly done: boolean, + ) {} }