Skip to content

Commit cefd3c5

Browse files
committed
feat(json-crdt-extensions): 🎸 improve the Point class
1 parent 9be59fe commit cefd3c5

File tree

2 files changed

+348
-40
lines changed

2 files changed

+348
-40
lines changed

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

Lines changed: 150 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,16 @@ import type {StringChunk} from '../util/types';
1212
* character ID and an anchor. Anchor specifies the side of the character to
1313
* which the point is attached. For example, a point with an anchor "before" .▢
1414
* points just before the character, while a point with an anchor "after" ▢.
15-
* points just after the character.
15+
* points just after the character. Points attached to string characters are
16+
* referred to as *relative* points, while points attached to the beginning or
17+
* end of the string are referred to as *absolute* points.
18+
*
19+
* The *absolute* points are reference the string itself, by using the string's
20+
* ID as the character ID. The *absolute (abs) start* references the very start
21+
* of the string, before the first character, and even before any deleted
22+
* characters. The *absolute (abs) end* references the very end of the string,
23+
* after the last character, and even after any deleted characters at the end
24+
* of the string.
1625
*/
1726
export class Point implements Pick<Stateful, 'refresh'>, Printable {
1827
constructor(
@@ -42,6 +51,9 @@ export class Point implements Pick<Stateful, 'refresh'>, Printable {
4251
}
4352

4453
/**
54+
* Compares two points by their character IDs and anchors. First, the character
55+
* IDs are compared. If they are equal, the anchors are compared. The anchor
56+
* "before" is considered less than the anchor "after".
4557
*
4658
* @param other The other point to compare to.
4759
* @returns Returns 0 if the two points are equal, -1 if this point is less
@@ -54,9 +66,30 @@ export class Point implements Pick<Stateful, 'refresh'>, Printable {
5466
return (this.anchor - other.anchor) as -1 | 0 | 1;
5567
}
5668

69+
/**
70+
* Compares two points by their spatial (view) location in the string. Takes
71+
* into account not only the character position in the view, but also handles
72+
* deleted characters and absolute points.
73+
*
74+
* @param other The other point to compare to.
75+
* @returns Returns 0 if the two points are equal, negative if this point is
76+
* less than the other point, and positive if this point is greater
77+
* than the other point.
78+
*/
5779
public compareSpatial(other: Point): number {
5880
const thisId = this.id;
5981
const otherId = other.id;
82+
if (this.isAbs()) {
83+
const isStart = this.anchor === Anchor.After;
84+
return isStart
85+
? other.isAbsStart() ? 0 : -1
86+
: other.isAbsEnd() ? 0 : 1;
87+
} else if (other.isAbs()) {
88+
const isStart = other.anchor === Anchor.After;
89+
return isStart
90+
? this.isAbsStart() ? 0 : 1
91+
: this.isAbsEnd() ? 0 : -1;
92+
}
6093
const cmp0 = compare(thisId, otherId);
6194
if (!cmp0) return this.anchor - other.anchor;
6295
const cmp1 = this.pos() - other.pos();
@@ -74,6 +107,11 @@ export class Point implements Pick<Stateful, 'refresh'>, Printable {
74107
}
75108

76109
private _chunk: StringChunk | undefined;
110+
111+
/**
112+
* @returns Returns the chunk that contains the character referenced by the
113+
* point, or `undefined` if the chunk is not found.
114+
*/
77115
public chunk(): StringChunk | undefined {
78116
let chunk = this._chunk;
79117
const id = this.id;
@@ -99,6 +137,7 @@ export class Point implements Pick<Stateful, 'refresh'>, Printable {
99137
}
100138

101139
private _pos: number = -1;
140+
102141
/** @todo Is this needed? */
103142
public posCached(): number {
104143
if (this._pos >= 0) return this._pos;
@@ -107,12 +146,12 @@ export class Point implements Pick<Stateful, 'refresh'>, Printable {
107146
}
108147

109148
/**
110-
* @returns Returns position of the point, as if it is a cursor in a text
111-
* pointing between characters.
149+
* @returns Returns the view position of the point, as if it is a caret in
150+
* the text pointing between characters.
112151
*/
113152
public viewPos(): number {
114153
const pos = this.pos();
115-
if (pos < 0) return this.isStartOfStr() ? 0 : this.txt.str.length();
154+
if (pos < 0) return this.isAbsStart() ? 0 : this.txt.str.length();
116155
return this.anchor === Anchor.Before ? pos : pos + 1;
117156
}
118157

@@ -125,12 +164,12 @@ export class Point implements Pick<Stateful, 'refresh'>, Printable {
125164
* @returns Next visible ID in string.
126165
*/
127166
public nextId(move: number = 1): ITimestampStruct | undefined {
128-
if (this.isEndOfStr()) return;
167+
if (this.isAbsEnd()) return;
129168
let remaining: number = move;
130169
const {id, txt} = this;
131170
const str = txt.str;
132171
let chunk: StringChunk | undefined;
133-
if (this.isStartOfStr()) {
172+
if (this.isAbsStart()) {
134173
chunk = str.first();
135174
while (chunk && chunk.del) chunk = str.next(chunk);
136175
if (!chunk) return;
@@ -171,7 +210,7 @@ export class Point implements Pick<Stateful, 'refresh'>, Printable {
171210
* such character.
172211
*/
173212
public prevId(move: number = 1): ITimestampStruct | undefined {
174-
if (this.isStartOfStr()) return;
213+
if (this.isAbsStart()) return;
175214
let remaining: number = move;
176215
const {id, txt} = this;
177216
const str = txt.str;
@@ -200,7 +239,7 @@ export class Point implements Pick<Stateful, 'refresh'>, Printable {
200239

201240
public leftChar(): ChunkSlice | undefined {
202241
const str = this.txt.str;
203-
if (this.isEndOfStr()) {
242+
if (this.isAbsEnd()) {
204243
let chunk = str.last();
205244
while (chunk && chunk.del) chunk = str.prev(chunk);
206245
return chunk ? new ChunkSlice(chunk, chunk.span - 1, 1) : undefined;
@@ -227,7 +266,7 @@ export class Point implements Pick<Stateful, 'refresh'>, Printable {
227266

228267
public rightChar(): ChunkSlice | undefined {
229268
const str = this.txt.str;
230-
if (this.isStartOfStr()) {
269+
if (this.isAbsStart()) {
231270
let chunk = str.first();
232271
while (chunk && chunk.del) chunk = str.next(chunk);
233272
return chunk ? new ChunkSlice(chunk, 0, 1) : undefined;
@@ -252,12 +291,56 @@ export class Point implements Pick<Stateful, 'refresh'>, Printable {
252291
return new ChunkSlice(chunk, 0, 1);
253292
}
254293

255-
public isStartOfStr(): boolean {
256-
return equal(this.id, this.txt.str.id) && this.anchor === Anchor.After;
294+
/**
295+
* Checks if the point is an absolute point. An absolute point is a point that
296+
* references the string itself, rather than a character in the string. It can
297+
* be either the very start or the very end of the string.
298+
*
299+
* @returns Returns `true` if the point is an absolute point.
300+
*/
301+
public isAbs(): boolean {
302+
return equal(this.id, this.txt.str.id);
257303
}
258304

259-
public isEndOfStr(): boolean {
260-
return equal(this.id, this.txt.str.id) && this.anchor === Anchor.Before;
305+
/**
306+
* @returns Returns `true` if the point is an absolute point and is anchored
307+
* before the first character in the string.
308+
*/
309+
public isAbsStart(): boolean {
310+
return this.isAbs() && this.anchor === Anchor.After;
311+
}
312+
313+
/**
314+
* @returns Returns `true` if the point is an absolute point and is anchored
315+
* after the last character in the string.
316+
*/
317+
public isAbsEnd(): boolean {
318+
return this.isAbs() && this.anchor === Anchor.Before;
319+
}
320+
321+
/**
322+
* @returns Returns `true` if the point is exactly the relative start, i.e.
323+
* it is attached to the first visible character in the string and
324+
* anchored "before".
325+
*/
326+
public isRelStart(): boolean {
327+
if (this.anchor !== Anchor.Before) return false;
328+
const id = this.txt.str.find(0);
329+
return !!id && equal(this.id, id);
330+
}
331+
332+
/**
333+
* @returns Returns `true` if the point is exactly the relative end, i.e. it
334+
* is attached to the last visible character in the string and
335+
* anchored "after".
336+
*/
337+
public isRelEnd(): boolean {
338+
if (this.anchor !== Anchor.After) return false;
339+
const str = this.txt.str;
340+
const length = str.length();
341+
if (length === 0) return false;
342+
const id = str.find(length - 1);
343+
return !!id && equal(this.id, id);
261344
}
262345

263346
/**
@@ -270,39 +353,85 @@ export class Point implements Pick<Stateful, 'refresh'>, Printable {
270353
else this.refAfter();
271354
}
272355

273-
public refStart(): void {
356+
/**
357+
* Sets the point to the absolute start of the string.
358+
*/
359+
public refAbsStart(): void {
274360
this.id = this.txt.str.id;
275361
this.anchor = Anchor.After;
276362
}
277363

278-
public refEnd(): void {
364+
/**
365+
* Sets the point to the absolute end of the string.
366+
*/
367+
public refAbsEnd(): void {
279368
this.id = this.txt.str.id;
280369
this.anchor = Anchor.Before;
281370
}
282371

283372
/**
284-
* Modifies the location of the point, such that the spatial location remains
373+
* Sets the point to the relative start of the string.
374+
*/
375+
public refStart(): void {
376+
this.refAbsStart();
377+
this.refBefore();
378+
}
379+
380+
/**
381+
* Sets the point to the relative end of the string.
382+
*/
383+
public refEnd(): void {
384+
this.refAbsEnd();
385+
this.refAfter();
386+
}
387+
388+
/**
389+
* Modifies the location of the point, such that the view location remains
285390
* the same, but ensures that it is anchored before a character. Skips any
286391
* deleted characters (chunks), attaching the point to the next visible
287392
* character.
288393
*/
289394
public refBefore(): void {
290395
const chunk = this.chunk();
291-
if (!chunk) return this.refEnd();
396+
if (!chunk) {
397+
if (this.isAbsStart()) {
398+
const id = this.txt.str.find(0);
399+
if (id) {
400+
this.id = id;
401+
this.anchor = Anchor.Before;
402+
return;
403+
}
404+
}
405+
return this.refAbsEnd();
406+
}
292407
if (!chunk.del && this.anchor === Anchor.Before) return;
293408
this.anchor = Anchor.Before;
294409
this.id = this.nextId() || this.txt.str.id;
295410
}
296411

297412
/**
298-
* Modifies the location of the point, such that the spatial location remains
413+
* Modifies the location of the point, such that the view location remains
299414
* the same, but ensures that it is anchored after a character. Skips any
300415
* deleted characters (chunks), attaching the point to the next visible
301416
* character.
302417
*/
303418
public refAfter(): void {
304419
const chunk = this.chunk();
305-
if (!chunk) return this.refStart();
420+
if (!chunk) {
421+
if (this.isAbsEnd()) {
422+
const str = this.txt.str;
423+
const length = str.length();
424+
if (length !== 0) {
425+
const id = str.find(length - 1);
426+
if (id) {
427+
this.id = id;
428+
this.anchor = Anchor.After;
429+
return;
430+
}
431+
}
432+
}
433+
return this.refAbsStart();
434+
}
306435
if (!chunk.del && this.anchor === Anchor.After) return;
307436
this.anchor = Anchor.After;
308437
this.id = this.prevId() || this.txt.str.id;
@@ -318,14 +447,14 @@ export class Point implements Pick<Stateful, 'refresh'>, Printable {
318447
if (anchor !== Anchor.After) this.refAfter();
319448
if (skip > 0) {
320449
const nextId = this.nextId(skip);
321-
if (!nextId) this.refEnd();
450+
if (!nextId) this.refAbsEnd();
322451
else {
323452
this.id = nextId;
324453
if (anchor !== Anchor.After) this.refBefore();
325454
}
326455
} else {
327456
const prevId = this.prevId(-skip);
328-
if (!prevId) this.refStart();
457+
if (!prevId) this.refAbsStart();
329458
else {
330459
this.id = prevId;
331460
if (anchor !== Anchor.After) this.refBefore();

0 commit comments

Comments
 (0)