Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overlay refresh improvements #614

Merged
merged 17 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
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
Loading