Skip to content

Commit bc3417d

Browse files
committed
refactor(json-crdt-extensions): 💡 implement Cursor as a local slice
1 parent 7971f21 commit bc3417d

File tree

8 files changed

+116
-126
lines changed

8 files changed

+116
-126
lines changed

src/json-crdt-extensions/peritext/Peritext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export class Peritext implements Printable {
6464
.setSchema(s.vec(s.arr([])));
6565
this.localSlices = new Slices(localModel, localModel.root.node().get(0)!, this.str);
6666

67-
this.editor = new Editor(this);
67+
this.editor = new Editor(this, this.localSlices);
6868
}
6969

7070
public strApi() {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {Point} from '../rga/Point';
2+
import {CursorAnchor} from '../slice/constants';
3+
import {PersistedSlice} from '../slice/PersistedSlice';
4+
5+
export class Cursor<T = string> extends PersistedSlice<T> {
6+
public get anchorSide(): CursorAnchor {
7+
return this.type as CursorAnchor;
8+
}
9+
10+
public set anchorSide(value: CursorAnchor) {
11+
this.update({type: value});
12+
}
13+
14+
public anchor(): Point<T> {
15+
return this.anchorSide === CursorAnchor.Start ? this.start : this.end;
16+
}
17+
18+
public focus(): Point<T> {
19+
return this.anchorSide === CursorAnchor.Start ? this.end : this.start;
20+
}
21+
22+
public set(start: Point<T>, end?: Point<T>, anchorSide: CursorAnchor = this.anchorSide): void {
23+
if (!end || end === start) end = start.clone();
24+
super.set(start, end);
25+
this.update({
26+
range: this,
27+
type: anchorSide,
28+
});
29+
}
30+
31+
public setAt(start: number, length: number = 0): void {
32+
let at = start;
33+
let len = length;
34+
if (len < 0) {
35+
at += len;
36+
len = -len;
37+
}
38+
super.setAt(at, len);
39+
this.anchorSide = length < 0 ? CursorAnchor.End : CursorAnchor.Start;
40+
}
41+
42+
/**
43+
* Move one of the edges of the cursor to a new point.
44+
*
45+
* @param point Point to set the edge to.
46+
* @param edge 0 for "focus", 1 for "anchor."
47+
*/
48+
public setEdge(point: Point<T>, edge: 0 | 1 = 0): void {
49+
if (this.start === this.end) this.end = this.end.clone();
50+
let anchor = this.anchor();
51+
let focus = this.focus();
52+
if (edge === 0) focus = point; else anchor = point;
53+
if (focus.cmpSpatial(anchor) < 0) this.set(focus, anchor, CursorAnchor.End);
54+
else this.set(anchor, focus, CursorAnchor.Start);
55+
}
56+
57+
public move(move: number): void {
58+
const {start, end} = this;
59+
start.move(move);
60+
if (start !== end) {
61+
end.move(move);
62+
}
63+
this.set(start, end);
64+
}
65+
66+
// ---------------------------------------------------------------- Printable
67+
68+
public toStringName(): string {
69+
const focusIcon = this.anchorSide === CursorAnchor.Start ? '.→|' : '|←.';
70+
return `${super.toStringName()}, ${focusIcon}`;
71+
}
72+
}

src/json-crdt-extensions/peritext/editor/Editor.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {Cursor} from '../slice/Cursor';
1+
import {Cursor} from './Cursor';
22
import {Anchor} from '../rga/constants';
3-
import {SliceBehavior} from '../slice/constants';
3+
import {CursorAnchor, SliceBehavior} from '../slice/constants';
44
import {tick, type ITimestampStruct} from '../../../json-crdt-patch/clock';
55
import {PersistedSlice} from '../slice/PersistedSlice';
66
import {Chars} from '../constants';
@@ -10,6 +10,7 @@ import type {Printable} from 'tree-dump/lib/types';
1010
import type {Point} from '../rga/Point';
1111
import type {SliceType} from '../types';
1212
import type {MarkerSlice} from '../slice/MarkerSlice';
13+
import type {Slices} from '../slice/Slices';
1314

1415
export class Editor implements Printable {
1516
/**
@@ -18,10 +19,10 @@ export class Editor implements Printable {
1819
*/
1920
public readonly cursor: Cursor;
2021

21-
constructor(public readonly txt: Peritext) {
22-
const point = txt.point(txt.str.id, Anchor.After);
23-
const cursorId = txt.str.id; // TODO: should be autogenerated to something else
24-
this.cursor = new Cursor(cursorId, txt, point, point.clone());
22+
constructor(public readonly txt: Peritext, slices: Slices) {
23+
const point = txt.pointAbsStart();
24+
const range = txt.range(point, point.clone());
25+
this.cursor = slices.ins<Cursor, typeof Cursor>(range, SliceBehavior.Cursor, CursorAnchor.Start, undefined, Cursor);
2526
}
2627

2728
/** @deprecated */

src/json-crdt-extensions/peritext/slice/Cursor.ts

Lines changed: 0 additions & 109 deletions
This file was deleted.

src/json-crdt-extensions/peritext/slice/PersistedSlice.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ export class PersistedSlice<T = string> extends Range<T> implements MutableSlice
8585
if (range.end.anchor !== end.anchor) updateHeader = true;
8686
if (compare(range.start.id, start.id) !== 0) changes.push([SliceTupleIndex.X1, s.con(range.start.id)]);
8787
if (compare(range.end.id, end.id) !== 0) changes.push([SliceTupleIndex.X2, s.con(range.end.id)]);
88-
this.setRange(range);
88+
this.start = range.start;
89+
this.end = range.start === range.end ? range.end.clone() : range.end;
8990
}
9091
if (params.type !== undefined) {
9192
this.type = params.type;
@@ -137,11 +138,18 @@ export class PersistedSlice<T = string> extends Range<T> implements MutableSlice
137138

138139
// ---------------------------------------------------------------- Printable
139140

141+
protected toStringName(): string {
142+
const data = this.data();
143+
const dataFormatted = data ? prettyOneLine(data) : '∅';
144+
const dataLengthBreakpoint = 32;
145+
const header = `${this.constructor.name} ${super.toString('', true)}, ${this.behavior}, ${JSON.stringify(this.type)}${dataFormatted.length < dataLengthBreakpoint ? `, ${dataFormatted}` : ''}`;
146+
return header;
147+
}
148+
140149
public toString(tab: string = ''): string {
141150
const data = this.data();
142151
const dataFormatted = data ? prettyOneLine(data) : '';
143152
const dataLengthBreakpoint = 32;
144-
const header = `${this.constructor.name} ${super.toString(tab)}, ${this.behavior}, ${JSON.stringify(this.type)}${dataFormatted.length < dataLengthBreakpoint ? `, ${dataFormatted}` : ''}`;
145-
return header + printTree(tab, [dataFormatted.length < dataLengthBreakpoint ? null : (tab) => dataFormatted]);
153+
return this.toStringName() + printTree(tab, [dataFormatted.length < dataLengthBreakpoint ? null : (tab) => dataFormatted]);
146154
}
147155
}

src/json-crdt-extensions/peritext/slice/Slices.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ export class Slices implements Stateful, Printable {
2828
protected readonly rga: AbstractRga<string>,
2929
) {}
3030

31-
public ins(range: Range, behavior: SliceBehavior, type: SliceType, data?: unknown): PersistedSlice {
31+
public ins<S extends PersistedSlice<string>, K extends new (...args: ConstructorParameters<typeof PersistedSlice<string>>) => S>(
32+
range: Range,
33+
behavior: SliceBehavior,
34+
type: SliceType,
35+
data?: unknown,
36+
Klass: K = behavior === SliceBehavior.Marker ? <any>MarkerSlice : PersistedSlice
37+
): S {
3238
const model = this.model;
3339
const set = this.set;
3440
const api = model.api;
@@ -57,10 +63,7 @@ export class Slices implements Stateful, Printable {
5763
const tuple = model.index.get(tupleId) as VecNode;
5864
const chunk = set.findById(chunkId)!;
5965
// TODO: Need to check if split slice text was deleted
60-
const slice =
61-
behavior === SliceBehavior.Marker
62-
? new MarkerSlice(model, this.rga, chunk, tuple, behavior, type, start, end)
63-
: new PersistedSlice(model, this.rga, chunk, tuple, behavior, type, start, end);
66+
const slice = new Klass(model, this.rga, chunk, tuple, behavior, type, start, end);
6467
this.list.set(chunk.id, slice);
6568
return slice;
6669
}

src/json-crdt-extensions/peritext/slice/constants.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/**
2-
* Specifies which cursor end is the "anchor", e.g. the end which does not move
3-
* when user changes selection.
2+
* Specifies whether the start or the end of the cursor is the "anchor", e.g.
3+
* the end which does not move when user changes selection. The other
4+
* end is free to move, the moving end of the cursor is "focus". By default
5+
* "anchor" is usually the start of the cursor.
46
*/
57
export const enum CursorAnchor {
68
Start = 0,
@@ -48,6 +50,11 @@ export const enum SliceBehavior {
4850
* used to re-verse inline formatting, like bold, italic, etc.
4951
*/
5052
Erase = 0b011,
53+
54+
/**
55+
* Used to mark the user's cursor position in the document.
56+
*/
57+
Cursor = 0b100,
5158
}
5259

5360
export const enum SliceTupleIndex {

src/json-crdt-patch/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ export const enum SESSION {
2020
*/
2121
GLOBAL = 2,
2222

23+
/**
24+
* Session ID used for models that are not shared with other users. For
25+
* example, when a user is editing a document in a local editor, these
26+
* documents could capture local information, like the cursor position, which
27+
* is not shared with other users.
28+
*/
29+
LOCAL = 3,
30+
2331
/** Max allowed session ID, they are capped at 53-bits. */
2432
MAX = 9007199254740991,
2533
}

0 commit comments

Comments
 (0)