Skip to content

Commit

Permalink
Merge pull request #614 from streamich/overlay-5
Browse files Browse the repository at this point in the history
`Overlay` refresh improvements
  • Loading branch information
streamich authored May 2, 2024
2 parents 7773bec + c0e31c8 commit 5fc8038
Show file tree
Hide file tree
Showing 17 changed files with 514 additions and 166 deletions.
56 changes: 35 additions & 21 deletions src/json-crdt-extensions/peritext/Peritext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@ import {Model} from '../../json-crdt/model';
import {CONST, updateNum} from '../../json-hash';
import {SESSION} from '../../json-crdt-patch/constants';
import {s} from '../../json-crdt-patch';
import {ExtraSlices} from './slice/ExtraSlices';
import type {ITimestampStruct} from '../../json-crdt-patch/clock';
import type {Printable} from 'tree-dump/lib/types';
import type {SliceType} from './types';
import type {MarkerSlice} from './slice/MarkerSlice';
import type {SliceSchema, SliceType} from './slice/types';
import type {SchemaToJsonNode} from '../../json-crdt/schema/types';

const EXTRA_SLICES_SCHEMA = s.vec(s.arr<SliceSchema>([]));

type SlicesModel = Model<SchemaToJsonNode<typeof EXTRA_SLICES_SCHEMA>>;

/**
* Context for a Peritext instance. Contains all the data and methods needed to
Expand Down Expand Up @@ -46,27 +52,33 @@ export class Peritext implements Printable {
public readonly editor: Editor;
public readonly overlay = new Overlay(this);

/**
* Creates a new Peritext context.
*
* @param model JSON CRDT model of the document where the text is stored.
* @param str The {@link StrNode} where the text is stored.
* @param slices The {@link ArrNode} where the slices are stored.
* @param extraSlicesModel The JSON CRDT model for the extra slices, which are
* not persisted in the main document, but are shared with other users.
* @param localSlicesModel The JSON CRDT model for the local slices, which are
* not persisted in the main document and are not shared with other
* users. The local slices capture current-user-only annotations, such
* as the current user's selection.
*/
constructor(
public readonly model: Model,
public readonly str: StrNode,
slices: ArrNode,
extraSlicesModel: SlicesModel = Model.create(EXTRA_SLICES_SCHEMA, model.clock.sid - 1),
localSlicesModel: SlicesModel = Model.create(EXTRA_SLICES_SCHEMA, SESSION.LOCAL),
) {
this.savedSlices = new Slices(this.model, slices, this.str);

const extraModel = Model.withLogicalClock(SESSION.GLOBAL)
.setSchema(s.vec(s.arr([])))
.fork(this.model.clock.sid + 1);
this.extraSlices = new Slices(extraModel, extraModel.root.node().get(0)!, this.str);

// TODO: flush patches
// TODO: remove `arr` tombstones
const localModel = Model.withLogicalClock(SESSION.LOCAL).setSchema(s.vec(s.arr([])));
const localApi = localModel.api;
this.extraSlices = new ExtraSlices(extraSlicesModel, extraSlicesModel.root.node().get(0)!, this.str);
const localApi = localSlicesModel.api;
localApi.onLocalChange.listen(() => {
localApi.flush();
});
this.localSlices = new LocalSlices(localModel, localModel.root.node().get(0)!, this.str);

this.localSlices = new LocalSlices(localSlicesModel, localSlicesModel.root.node().get(0)!, this.str);
this.editor = new Editor(this, this.localSlices);
}

Expand Down Expand Up @@ -99,8 +111,8 @@ export class Peritext implements Printable {
}

/**
* Creates a point at a view position in the text. The `pos` argument specifies
* the position of the character, not the gap between characters.
* Creates a point at a view position in the text. The `pos` argument
* specifies the position of the character, not the gap between characters.
*
* @param pos Position of the character in the text.
* @param anchor Whether the point should attach before or after a character.
Expand Down Expand Up @@ -150,7 +162,8 @@ export class Peritext implements Printable {
}

/**
* Creates a range from two points, the points have to be in the correct order.
* Creates a range from two points, the points have to be in the correct
* order.
*
* @param start Start point of the range, must be before or equal to end.
* @param end End point of the range, must be after or equal to start.
Expand All @@ -161,8 +174,8 @@ export class Peritext implements Printable {
}

/**
* A convenience method for creating a range from a view position and a length.
* See {@link Range.at} for more information.
* A convenience method for creating a range from a view position and a
* length. See {@link Range.at} for more information.
*
* @param start Position in the text.
* @param length Length of the range.
Expand Down Expand Up @@ -238,14 +251,15 @@ export class Peritext implements Printable {

public toString(tab: string = ''): string {
const nl = () => '';
const {savedSlices, extraSlices, localSlices} = this;
return (
this.constructor.name +
printTree(tab, [
(tab) => this.editor.cursor.toString(tab),
nl,
(tab) => this.str.toString(tab),
nl,
(tab) => this.savedSlices.toString(tab),
savedSlices.size() ? (tab) => savedSlices.toString(tab) : null,
extraSlices.size() ? (tab) => extraSlices.toString(tab) : null,
localSlices.size() ? (tab) => localSlices.toString(tab) : null,
nl,
(tab) => this.overlay.toString(tab),
])
Expand Down
35 changes: 9 additions & 26 deletions src/json-crdt-extensions/peritext/editor/Cursor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {Point} from '../rga/Point';
import {Range} from '../rga/Range';
import {CursorAnchor} from '../slice/constants';
import {PersistedSlice} from '../slice/PersistedSlice';

Expand All @@ -8,6 +7,8 @@ export class Cursor<T = string> extends PersistedSlice<T> {
return this.type as CursorAnchor;
}

// ---------------------------------------------------------------- mutations

public set anchorSide(value: CursorAnchor) {
this.update({type: value});
}
Expand All @@ -20,42 +21,26 @@ export class Cursor<T = string> extends PersistedSlice<T> {
return this.anchorSide === CursorAnchor.Start ? this.end : this.start;
}

public set(start: Point<T>, end?: Point<T>, anchorSide: CursorAnchor = this.anchorSide): void {
if (!end || end === start) end = start.clone();
super.set(start, end);
public set(start: Point<T>, end: Point<T> = start, anchorSide: CursorAnchor = this.anchorSide): void {
this.start = start;
this.end = end === start ? end.clone() : end;
this.update({
range: this,
type: anchorSide,
});
}

/** TODO: Move to {@link PersistedSlice}. */
public setAt(start: number, length: number = 0): void {
let at = start;
let len = length;
if (len < 0) {
at += len;
len = -len;
}
const range = Range.at<T>(this.rga, start, length);
const anchorSide = this.anchorSide;
this.update({
range,
type: anchorSide !== this.anchorSide ? anchorSide : undefined,
});
}

/**
* Move one of the edges of the cursor to a new point.
*
* @param point Point to set the edge to.
* @param edge 0 for "focus", 1 for "anchor."
* @param endpoint 0 for "focus", 1 for "anchor."
*/
public setEdge(point: Point<T>, edge: 0 | 1 = 0): void {
public setEndpoint(point: Point<T>, endpoint: 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;
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);
Expand All @@ -64,9 +49,7 @@ export class Cursor<T = string> extends PersistedSlice<T> {
public move(move: number): void {
const {start, end} = this;
start.move(move);
if (start !== end) {
end.move(move);
}
if (start !== end) end.move(move);
this.set(start, end);
}

Expand Down
6 changes: 5 additions & 1 deletion src/json-crdt-extensions/peritext/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import type {Range} from '../rga/Range';
import type {Peritext} from '../Peritext';
import type {Printable} from 'tree-dump/lib/types';
import type {Point} from '../rga/Point';
import type {SliceType} from '../types';
import type {SliceType} from '../slice/types';
import type {MarkerSlice} from '../slice/MarkerSlice';
import type {Slices} from '../slice/Slices';

/**
* Rename to `PeritextApi`.
*/
export class Editor implements Printable {
/**
* Cursor is the the current user selection. It can be a caret or a
Expand All @@ -25,6 +28,7 @@ export class Editor implements Printable {
) {
const point = txt.pointAbsStart();
const range = txt.range(point, point.clone());
// TODO: Add ability to remove cursor.
this.cursor = slices.ins<Cursor, typeof Cursor>(range, SliceBehavior.Cursor, CursorAnchor.Start, undefined, Cursor);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {printTree} from 'tree-dump/lib/printTree';
import {OverlayPoint} from './OverlayPoint';
import {SliceType} from '../types';
import type {SliceType} from '../slice/types';
import type {Anchor} from '../rga/constants';
import type {AbstractRga} from '../../../json-crdt/nodes/rga';
import type {ITimestampStruct} from '../../../json-crdt-patch/clock';
import type {MarkerSlice} from '../slice/MarkerSlice';
import {printTree} from 'tree-dump/lib/printTree';

export class MarkerOverlayPoint extends OverlayPoint {
/**
Expand Down
32 changes: 18 additions & 14 deletions src/json-crdt-extensions/peritext/overlay/Overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,18 @@ import type {Printable} from 'tree-dump/lib/types';
import type {MutableSlice, Slice} from '../slice/types';
import type {Slices} from '../slice/Slices';

/**
* Overlay is a tree structure that represents all the intersections of slices
* in the text. It is used to quickly find all the slices that overlap a
* given point in the text. The overlay is a read-only structure, its state
* is changed only by calling the `refresh` method, which updates the overlay
* based on the current state of the text and slices.
*/
export class Overlay implements Printable, Stateful {
public root: OverlayPoint | undefined = undefined;

constructor(protected readonly txt: Peritext) {}

/**
* @todo Rename to .point().
*/
protected overlayPoint(id: ITimestampStruct, anchor: Anchor): OverlayPoint {
return new OverlayPoint(this.txt.str, id, anchor);
}

protected markerPoint(marker: MarkerSlice, anchor: Anchor): OverlayPoint {
return new MarkerOverlayPoint(this.txt.str, marker.start.id, anchor, marker);
}

public first(): OverlayPoint | undefined {
return this.root ? first(this.root) : undefined;
}
Expand Down Expand Up @@ -136,12 +132,20 @@ export class Overlay implements Printable, Stateful {
return state;
}

private point(id: ITimestampStruct, anchor: Anchor): OverlayPoint {
return new OverlayPoint(this.txt.str, id, anchor);
}

private mPoint(marker: MarkerSlice, anchor: Anchor): MarkerOverlayPoint {
return new MarkerOverlayPoint(this.txt.str, marker.start.id, anchor, marker);
}

/**
* Retrieve an existing {@link OverlayPoint} or create a new one, inserted
* in the tree, sorted by spatial dimension.
*/
protected upsertPoint(point: Point): [point: OverlayPoint, isNew: boolean] {
const newPoint = this.overlayPoint(point.id, point.anchor);
private upsertPoint(point: Point): [point: OverlayPoint, isNew: boolean] {
const newPoint = this.point(point.id, point.anchor);
const pivot = this.insPoint(newPoint);
if (pivot) return [pivot, false];
return [newPoint, true];
Expand Down Expand Up @@ -173,7 +177,7 @@ export class Overlay implements Printable, Stateful {
}

private insMarker(slice: MarkerSlice): [start: OverlayPoint, end: OverlayPoint] {
const point = this.markerPoint(slice, Anchor.Before);
const point = this.mPoint(slice, Anchor.Before);
const pivot = this.insPoint(point);
if (!pivot) {
point.refs.push(slice);
Expand Down
Loading

0 comments on commit 5fc8038

Please sign in to comment.