Skip to content

Commit

Permalink
Merge pull request #581 from streamich/slices-5
Browse files Browse the repository at this point in the history
Peritext `Slices` and `Cursor` class improvements
  • Loading branch information
streamich authored Apr 20, 2024
2 parents d14dd8e + 7c270aa commit dcf1086
Show file tree
Hide file tree
Showing 22 changed files with 618 additions and 227 deletions.
26 changes: 3 additions & 23 deletions src/json-crdt-extensions/peritext/Peritext.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import {Anchor, SliceBehavior} from './constants';
import {Point} from './point/Point';
import {Range} from './slice/Range';
import {Anchor} from './rga/constants';
import {Point} from './rga/Point';
import {Range} from './rga/Range';
import {Editor} from './editor/Editor';
import {printTree} from '../../util/print/printTree';
import {ArrNode, StrNode} from '../../json-crdt/nodes';
import {Slices} from './slice/Slices';
import {type ITimestampStruct} from '../../json-crdt-patch/clock';
import type {Model} from '../../json-crdt/model';
import type {Printable} from '../../util/print/types';
import type {SliceType} from './types';
import type {PersistedSlice} from './slice/PersistedSlice';

/**
* Context for a Peritext instance. Contains all the data and methods needed to
Expand Down Expand Up @@ -147,24 +145,6 @@ export class Peritext implements Printable {
return textId;
}

public insSlice(
range: Range,
behavior: SliceBehavior,
type: SliceType,
data?: unknown | ITimestampStruct,
): PersistedSlice {
// if (range.isCollapsed()) throw new Error('INVALID_RANGE');
// TODO: If range is not collapsed, check if there are any visible characters in the range.
const slice = this.slices.ins(range, behavior, type, data);
return slice;
}

// ---------------------------------------------------------------- Deletions

public delSlice(sliceId: ITimestampStruct): void {
this.slices.del(sliceId);
}

/** Select a single character before a point. */
public findCharBefore(point: Point): Range | undefined {
if (point.anchor === Anchor.After) {
Expand Down
13 changes: 7 additions & 6 deletions src/json-crdt-extensions/peritext/editor/Editor.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {Cursor} from '../slice/Cursor';
import {Anchor, SliceBehavior} from '../constants';
import {Anchor} from '../rga/constants';
import {SliceBehavior} from '../slice/constants';
import {tick, type ITimestampStruct} from '../../../json-crdt-patch/clock';
import {PersistedSlice} from '../slice/PersistedSlice';
import type {Range} from '../slice/Range';
import type {Range} from '../rga/Range';
import type {Peritext} from '../Peritext';
import type {Printable} from '../../../util/print/types';
import type {Point} from '../point/Point';
import type {Point} from '../rga/Point';
import type {SliceType} from '../types';

export class Editor implements Printable {
Expand Down Expand Up @@ -121,14 +122,14 @@ export class Editor implements Printable {
}

public insertSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice {
return this.txt.insSlice(this.cursor, SliceBehavior.Stack, type, data);
return this.txt.slices.ins(this.cursor, SliceBehavior.Stack, type, data);
}

public insertOverwriteSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice {
return this.txt.insSlice(this.cursor, SliceBehavior.Overwrite, type, data);
return this.txt.slices.ins(this.cursor, SliceBehavior.Overwrite, type, data);
}

public insertEraseSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice {
return this.txt.insSlice(this.cursor, SliceBehavior.Erase, type, data);
return this.txt.slices.ins(this.cursor, SliceBehavior.Erase, type, data);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {compare, type ITimestampStruct, toDisplayString, equal, tick, containsId} from '../../../json-crdt-patch/clock';
import {Anchor} from '../constants';
import {Anchor} from './constants';
import {ChunkSlice} from '../util/ChunkSlice';
import {updateId} from '../../../json-crdt/hash';
import type {AbstractRga, Chunk} from '../../../json-crdt/nodes/rga';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import {Point} from '../point/Point';
import {Anchor} from '../constants';
import {Point} from './Point';
import {Anchor} from './constants';
import {updateNum} from '../../../json-hash';
import type {ITimestampStruct} from '../../../json-crdt-patch/clock';
import type {Printable} from '../../../util/print/types';
import type {AbstractRga, Chunk} from '../../../json-crdt/nodes/rga';
import type {Stateful} from '../types';

/**
* A range is a pair of points that represent a selection in the text. A range
* can be collapsed to a single point, then it is called a *marker*
* (if it is stored in the text), or *caret* (if it is a cursor position).
*/
export class Range<T = string> implements Printable {
export class Range<T = string> implements Pick<Stateful, 'refresh'>, Printable {
/**
* Creates a range from two points. The points are ordered so that the
* start point is before or equal to the end point.
Expand Down Expand Up @@ -92,6 +94,14 @@ export class Range<T = string> implements Printable {
return new Range(this.rga, this.start.clone(), this.end.clone());
}

public cmp(range: Range<T>): -1 | 0 | 1 {
return this.start.cmp(range.start) || this.end.cmp(range.end);
}

public cmpSpatial(range: Range<T>): number {
return this.start.cmpSpatial(range.start) || this.end.cmpSpatial(range.end);
}

/**
* Determines if the range is collapsed to a single point. Handles special
* cases where the range is collapsed, but the points are not equal, for
Expand Down Expand Up @@ -206,6 +216,14 @@ export class Range<T = string> implements Printable {
return result;
}

// ----------------------------------------------------------------- Stateful

public refresh(): number {
let state = this.start.refresh();
state = updateNum(state, this.end.refresh());
return state;
}

// ---------------------------------------------------------------- Printable

public toString(tab: string = '', lite: boolean = true): string {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Model} from '../../../../json-crdt/model';
import {Peritext} from '../../Peritext';
import {Anchor} from '../../constants';
import {Anchor} from '../constants';
import {tick} from '../../../../json-crdt-patch/clock';

const setup = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Model} from '../../../../json-crdt/model';
import {Peritext} from '../../Peritext';
import {Anchor} from '../../constants';
import {Anchor} from '../constants';

const setup = (insert: (peritext: Peritext) => void = (peritext) => peritext.strApi().ins(0, 'Hello world!')) => {
const model = Model.withLogicalClock();
Expand Down
4 changes: 4 additions & 0 deletions src/json-crdt-extensions/peritext/rga/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const enum Anchor {
Before = 0,
After = 1,
}
70 changes: 34 additions & 36 deletions src/json-crdt-extensions/peritext/slice/Cursor.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {Point} from '../point/Point';
import {Anchor, SliceBehavior, Tags} from '../constants';
import {Range} from './Range';
import {Point} from '../rga/Point';
import {CursorAnchor, SliceBehavior, Tags} from './constants';
import {Range} from '../rga/Range';
import {printTree} from '../../../util/print/printTree';
import {updateNum} from '../../../json-hash';
import type {ITimestampStruct} from '../../../json-crdt-patch/clock';
import type {Peritext} from '../Peritext';
import type {Slice} from './types';

export class Cursor extends Range<string> implements Slice {
export class Cursor<T = string> extends Range<T> implements Slice<T> {
public readonly behavior = SliceBehavior.Overwrite;
public readonly type = Tags.Cursor;

Expand All @@ -15,32 +16,30 @@ export class Cursor extends Range<string> implements Slice {
* the end which does not move when user changes selection. The other
* end is free to move, the moving end of the cursor is "focus". By default
* "anchor" is the start of the cursor.
*
* @todo Create a custom enum for this, instead of using `Anchor`.
*/
public base: Anchor = Anchor.Before;
public anchorSide: CursorAnchor = CursorAnchor.Start;

constructor(
public readonly id: ITimestampStruct,
protected readonly txt: Peritext,
public start: Point,
public end: Point,
public start: Point<T>,
public end: Point<T>,
) {
super(txt.str, start, end);
super(txt.str as any, start, end);
}

public anchor(): Point {
return this.base === Anchor.Before ? this.start : this.end;
public anchor(): Point<T> {
return this.anchorSide === CursorAnchor.Start ? this.start : this.end;
}

public focus(): Point {
return this.base === Anchor.Before ? this.end : this.start;
public focus(): Point<T> {
return this.anchorSide === CursorAnchor.Start ? this.end : this.start;
}

public set(start: Point, end?: Point, base: Anchor = Anchor.Before): void {
public set(start: Point<T>, end?: Point<T>, base: CursorAnchor = CursorAnchor.Start): void {
if (!end || end === start) end = start.clone();
super.set(start, end);
this.base = base;
this.anchorSide = base;
}

public setAt(start: number, length: number = 0): void {
Expand All @@ -51,7 +50,7 @@ export class Cursor extends Range<string> implements Slice {
len = -len;
}
super.setAt(at, len);
this.base = length < 0 ? Anchor.After : Anchor.Before;
this.anchorSide = length < 0 ? CursorAnchor.End : CursorAnchor.Start;
}

/**
Expand All @@ -60,30 +59,25 @@ export class Cursor extends Range<string> implements Slice {
* @param point Point to set the edge to.
* @param edge 0 for "focus", 1 for "anchor."
*/
public setEdge(point: Point, edge: 0 | 1 = 0): void {
public setEdge(point: Point<T>, edge: 0 | 1 = 0): void {
if (this.start === this.end) this.end = this.end.clone();
let anchor = this.anchor();
let focus = this.focus();
if (edge === 0) focus = point;
else anchor = point;
if (focus.cmpSpatial(anchor) < 0) {
this.base = Anchor.After;
this.anchorSide = CursorAnchor.End;
this.start = focus;
this.end = anchor;
} else {
this.base = Anchor.Before;
this.anchorSide = CursorAnchor.Start;
this.start = anchor;
this.end = focus;
}
}

/** @todo Maybe move it to another interface? */
public del(): boolean {
return false;
}

public data(): unknown {
return 1;
public data() {
return undefined;
}

public move(move: number): void {
Expand All @@ -93,19 +87,23 @@ export class Cursor extends Range<string> implements Slice {
end.move(move);
}

public toString(tab: string = ''): string {
const text = JSON.stringify(this.text());
const focusIcon = this.base === Anchor.Before ? '.⇨|' : '|⇦.';
const main = `${this.constructor.name} ${super.toString(tab + ' ', true)} ${focusIcon}`;
return main + printTree(tab, [() => text]);
}

// ----------------------------------------------------------------- Stateful

public hash: number = 0;

public refresh(): number {
// TODO: implement this ...
return this.hash;
let state = super.refresh();
state = updateNum(state, this.anchorSide);
this.hash = state;
return state;
}

// ---------------------------------------------------------------- Printable

public toString(tab: string = ''): string {
const text = JSON.stringify(this.text());
const focusIcon = this.anchorSide === CursorAnchor.Start ? '.→|' : '|←.';
const main = `${this.constructor.name} ${super.toString(tab + ' ', true)} ${focusIcon}`;
return main + printTree(tab, [() => text]);
}
}
Loading

0 comments on commit dcf1086

Please sign in to comment.