Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
42d3820
feat(json-crdt-extensions): 🎸 improve Slice and PersistedSlice interf…
streamich Apr 19, 2024
4d55317
refactor(json-crdt-extensions): 💡 move Point to /rga folder
streamich Apr 19, 2024
c2bde73
refactor(json-crdt-extensions): 💡 move Range to /rga folder
streamich Apr 19, 2024
a0332b0
feat(json-crdt-extensions): 🎸 improve unserialization of slices
streamich Apr 19, 2024
fde38b3
refactor(json-crdt-extensions): 💡 improve constants
streamich Apr 19, 2024
dfc3e03
chore(json-crdt-extensions): 🤖 update error message
streamich Apr 19, 2024
c99fa95
perf(util): ⚡️ walk tree less when computing its size
streamich Apr 19, 2024
690f91a
perf(util): ⚡️ store size of the map in AvlMap
streamich Apr 19, 2024
97b60c2
feat(json-crdt-extensions): 🎸 improve .toString() presentation
streamich Apr 19, 2024
5acce0f
perf(json-crdt-extensions): ⚡️ store slice list in AvlMap
streamich Apr 19, 2024
abd04a6
test(json-crdt-extensions): 💍 improve Slices.ins() tests
streamich Apr 19, 2024
12e4129
style: 💄 run Prettier
streamich Apr 19, 2024
c482e64
feat(json-crdt-extensions): 🎸 improve slice state refresh and print l…
streamich Apr 19, 2024
423fe16
feat(json-crdt-extensions): 🎸 make Cursor generic
streamich Apr 20, 2024
894cddd
feat(json-crdt-extensions): 🎸 pretty print one-line JSON
streamich Apr 20, 2024
1ed13b8
feat(json-crdt-extensions): 🎸 improve slices interface
streamich Apr 20, 2024
ba77d20
feat(json-crdt-extensions): 🎸 add slice update method
streamich Apr 20, 2024
3c50c72
feat(json-crdt-extensions): 🎸 improve method setup and constants
streamich Apr 20, 2024
bebfbbb
style(json-crdt-extensions): 💄 run Prettier
streamich Apr 20, 2024
f6c55b3
test(json-crdt-extensions): 💍 improve slice refresh tests
streamich Apr 20, 2024
db7f10e
fix(json-crdt-extensions): 🐛 delete slices from index on deletion
streamich Apr 20, 2024
7c270aa
style(json-crdt-extensions): 💄 fix linter errors
streamich Apr 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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