@@ -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 */
1726export 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