All files / json-crdt-extensions/peritext/editor Cursor.ts

82.97% Statements 39/47
78.78% Branches 26/33
75% Functions 9/12
82.05% Lines 32/39

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 11464x 64x 64x                                         64x         10217x       22x       12x                   21x       22x       13255x 13255x 13255x 13255x 13255x 10639x 10639x 10639x                         17x 17x 17x 17x 7x 17x 10x       232x 232x 232x 232x               2357x 2357x 2357x 2357x                              
import {printTs} from '../../../json-crdt-patch';
import {CursorAnchor} from '../slice/constants';
import {PersistedSlice} from '../slice/PersistedSlice';
import type {Point} from '../rga/Point';
 
/**
 * Cursor is a slice that represents an explicitly highlighted place in the
 * text to the user. The {@link Cursor} is a {@link Range}, it has a `start`
 * {@link Point} and an `end` {@link Point}.
 *
 * The {@link Cursor} can be a caret (collapsed cursor) or a selection (range
 * expanded cursor). The caret is said to be "collapsed", its `start` and `end`
 * {@link Point}s are the same. When the selection is said to be "expanded", its
 * `start` and `end` {@link Point}s are different.
 *
 * The `start` {@link Point} is always the one that comes first in the text, it
 * is less then or equal to the `end` {@link Point} in the spatial (text) order.
 *
 * An expanded selection cursor has a *focus* and an *anchor* side. The *focus*
 * side is the one that moves when the user presses the arrow keys. The *anchor*
 * side is the one that stays in place when the user presses the arrow keys. The
 * side of the anchor is determined by the {@link Cursor#anchorSide} property.
 */
export class Cursor<T = string> extends PersistedSlice<T> {
  /**
   * @todo Remove getter `get` here.
   */
  public get anchorSide(): CursorAnchor {
    return this.type() as CursorAnchor;
  }
 
  public isStartFocused(): boolean {
    return this.type() === CursorAnchor.End || this.start.cmp(this.end) === 0;
  }
 
  public isEndFocused(): boolean {
    return this.type() === CursorAnchor.Start || this.start.cmp(this.end) === 0;
  }
 
  // ---------------------------------------------------------------- mutations
 
  public set anchorSide(value: CursorAnchor) {
    this.update({type: value});
  }
 
  public anchor(): Point<T> {
    return this.anchorSide === CursorAnchor.Start ? this.start : this.end;
  }
 
  public focus(): Point<T> {
    return this.anchorSide === CursorAnchor.Start ? this.end : this.start;
  }
 
  public set(start: Point<T>, end: Point<T> = start, anchorSide: CursorAnchor = this.anchorSide): void {
    let hasChange = false;
    if (start.cmp(this.start)) hasChange = true;
    if (!hasChange && end.cmp(this.end)) hasChange = true;
    if (!hasChange && anchorSide !== this.anchorSide) hasChange = true;
    if (!hasChange) return;
    this.start = start;
    this.end = end === start ? end.clone() : end;
    this.update({
      range: this,
      type: anchorSide,
    });
  }
 
  /**
   * Move one of the edges of the cursor to a new point.
   *
   * @param point Point to set the edge to.
   * @param endpoint 0 for "focus", 1 for "anchor".
   */
  public setEndpoint(point: Point<T>, endpoint: 0 | 1 = 0): void {
    Iif (this.start === this.end) this.end = this.end.clone();
    let anchor = this.anchor();
    let focus = this.focus();
    if (endpoint === 0) focus = point;
    else anchor = point;
    if (focus.cmpSpatial(anchor) < 0) this.set(focus, anchor, CursorAnchor.End);
    else this.set(anchor, focus, CursorAnchor.Start);
  }
 
  public move(move: number): void {
    const {start, end} = this;
    const isCaret = start.cmp(end) === 0;
    start.step(move);
    if (isCaret) this.set(start);
    else E{
      end.step(move);
      this.set(start, end);
    }
  }
 
  public collapseToStart(anchorSide: CursorAnchor = CursorAnchor.Start): void {
    const start = this.start.clone();
    start.refAfter();
    const end = start.clone();
    this.set(start, end, anchorSide);
  }
 
  // ---------------------------------------------------------------- Printable
 
  public toStringName(): string {
    const focusIcon = this.anchorSide === CursorAnchor.Start ? '.→|' : '|←.';
    return 'Cursor ' + focusIcon + ' ' + printTs(this.chunk.id) + ' #' + this.hash.toString(36);
  }
 
  public toStringHeaderName(): string {
    const focusIcon = this.anchorSide === CursorAnchor.Start ? '.→|' : '|←.';
    return `${super.toStringHeaderName()}, ${focusIcon}`;
  }
}