Skip to content

Commit 2e48abf

Browse files
committed
fix(json-crdt-extensions): 🐛 correct check for .refBefore() and .refAfter()
1 parent b1be984 commit 2e48abf

File tree

2 files changed

+98
-4
lines changed

2 files changed

+98
-4
lines changed

src/json-crdt-extensions/peritext/point/Point.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,24 +282,28 @@ export class Point implements Pick<Stateful, 'refresh'>, Printable {
282282

283283
/**
284284
* Modifies the location of the point, such that the spatial location remains
285-
* the same, but ensures that it is anchored before a character.
285+
* the same, but ensures that it is anchored before a character. Skips any
286+
* deleted characters (chunks), attaching the point to the next visible
287+
* character.
286288
*/
287289
public refBefore(): void {
288290
const chunk = this.chunk();
289291
if (!chunk) return this.refEnd();
290-
if (!chunk.del || this.anchor === Anchor.Before) return;
292+
if (!chunk.del && this.anchor === Anchor.Before) return;
291293
this.anchor = Anchor.Before;
292294
this.id = this.nextId() || this.txt.str.id;
293295
}
294296

295297
/**
296298
* Modifies the location of the point, such that the spatial location remains
297-
* the same, but ensures that it is anchored after a character.
299+
* the same, but ensures that it is anchored after a character. Skips any
300+
* deleted characters (chunks), attaching the point to the next visible
301+
* character.
298302
*/
299303
public refAfter(): void {
300304
const chunk = this.chunk();
301305
if (!chunk) return this.refStart();
302-
if (!chunk.del || this.anchor === Anchor.After) return;
306+
if (!chunk.del && this.anchor === Anchor.After) return;
303307
this.anchor = Anchor.After;
304308
this.id = this.prevId() || this.txt.str.id;
305309
}

src/json-crdt-extensions/peritext/point/__tests__/Point.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,96 @@ describe('.rightChar()', () => {
729729
});
730730
});
731731

732+
describe('.isStartOfStr()', () => {
733+
test('returns true if is start of string', () => {
734+
const {peritext} = setupWithChunkedText();
735+
const p1 = peritext.pointAtStart();
736+
const p2 = peritext.pointAt(0, Anchor.Before);
737+
expect(p1.isStartOfStr()).toBe(true);
738+
expect(p2.isStartOfStr()).toBe(false);
739+
});
740+
});
741+
742+
describe('.isEndOfStr()', () => {
743+
test('returns true if is end of string', () => {
744+
const {peritext} = setupWithChunkedText();
745+
const p1 = peritext.pointAtEnd();
746+
const p2 = peritext.pointAt(8, Anchor.After);
747+
expect(p1.isEndOfStr()).toBe(true);
748+
expect(p2.isEndOfStr()).toBe(false);
749+
});
750+
});
751+
752+
describe('.refBefore()', () => {
753+
test('goes to next character, when anchor is switched', () => {
754+
const {peritext} = setupWithChunkedText();
755+
const p1 = peritext.pointAt(0, Anchor.After);
756+
expect(p1.rightChar()!.view()).toBe('2');
757+
const p2 = p1.clone();
758+
p2.refBefore();
759+
expect(p2.rightChar()!.view()).toBe('2');
760+
expect(p1.anchor).toBe(Anchor.After);
761+
expect(p2.anchor).toBe(Anchor.Before);
762+
expect(p1.id.time + 1).toBe(p2.id.time);
763+
});
764+
765+
test('skips deleted chars', () => {
766+
const {peritext} = setupWithChunkedText();
767+
const p1 = peritext.pointAt(2, Anchor.After);
768+
expect(p1.rightChar()!.view()).toBe('4');
769+
const p2 = p1.clone();
770+
p2.refBefore();
771+
expect(p2.rightChar()!.view()).toBe('4');
772+
expect(p1.anchor).toBe(Anchor.After);
773+
expect(p2.anchor).toBe(Anchor.Before);
774+
expect(p1.id.time).not.toBe(p2.id.time);
775+
});
776+
777+
test('when on last character, attaches to end of str', () => {
778+
const {peritext} = setupWithChunkedText();
779+
const p1 = peritext.pointAt(8, Anchor.After);
780+
expect(p1.leftChar()!.view()).toBe('9');
781+
const p2 = p1.clone();
782+
p2.refBefore();
783+
expect(p2.isEndOfStr()).toBe(true);
784+
});
785+
});
786+
787+
describe('.refAfter()', () => {
788+
test('goes to next character, when anchor is switched', () => {
789+
const {peritext} = setupWithChunkedText();
790+
const p1 = peritext.pointAt(4, Anchor.Before);
791+
expect(p1.leftChar()!.view()).toBe('4');
792+
const p2 = p1.clone();
793+
p2.refAfter();
794+
expect(p2.leftChar()!.view()).toBe('4');
795+
expect(p1.anchor).toBe(Anchor.Before);
796+
expect(p2.anchor).toBe(Anchor.After);
797+
expect(p1.id.time - 1).toBe(p2.id.time);
798+
});
799+
800+
test('skips deleted chars', () => {
801+
const {peritext} = setupWithChunkedText();
802+
const p1 = peritext.pointAt(7, Anchor.Before);
803+
expect(p1.leftChar()!.view()).toBe('7');
804+
const p2 = p1.clone();
805+
p2.refAfter();
806+
expect(p2.leftChar()!.view()).toBe('7');
807+
expect(p1.anchor).toBe(Anchor.Before);
808+
expect(p2.anchor).toBe(Anchor.After);
809+
expect(p2.chunk()!.del).toBe(false);
810+
});
811+
812+
test('when on first character, attaches to start of str', () => {
813+
const {peritext} = setupWithChunkedText();
814+
const p1 = peritext.pointAt(0, Anchor.Before);
815+
expect(p1.rightChar()!.view()).toBe('1');
816+
const p2 = p1.clone();
817+
p2.refAfter();
818+
expect(p2.isStartOfStr()).toBe(true);
819+
});
820+
});
821+
732822
describe('.move()', () => {
733823
test('can move forward', () => {
734824
const {peritext, model} = setupWithChunkedText();

0 commit comments

Comments
 (0)