Skip to content

Commit

Permalink
feat(json-crdt-extensions): 馃幐 improve multi-cursor support
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed May 5, 2024
1 parent 6a14127 commit 7a6850b
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 16 deletions.
51 changes: 35 additions & 16 deletions src/json-crdt-extensions/peritext/editor/Editor.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,70 @@
import {Cursor} from './Cursor';
import {Anchor} from '../rga/constants';
import {CursorAnchor, SliceBehavior} from '../slice/constants';
import {tick, type ITimestampStruct} from '../../../json-crdt-patch/clock';
import {PersistedSlice} from '../slice/PersistedSlice';
import {Chars} from '../constants';
import type {Range} from '../rga/Range';
import type {ITimestampStruct} from '../../../json-crdt-patch/clock';
import type {Peritext} from '../Peritext';
import type {Point} from '../rga/Point';
import type {SliceType} from '../slice/types';
import type {MarkerSlice} from '../slice/MarkerSlice';

export class Editor<T = string> {
constructor(public readonly txt: Peritext<T>) {}

public firstCursor(): Cursor<T> | undefined {
const iterator = this.txt.localSlices.iterator0();
let cursor = iterator();
while (cursor) {
if (cursor instanceof Cursor) return cursor;
cursor = iterator();
}
return;
}

/**
* Cursor is the the current user selection. It can be a caret or a
* range. If range is collapsed to a single point, it is a caret.
* Returns the first cursor in the text. If there is no cursor, creates one
* and inserts it at the start of the text. To work with multiple cursors, use
* `.cursors()` method.
*
* Cursor is the the current user selection. It can be a caret or a range. If
* range is collapsed to a single point, it is a *caret*.
*/
public readonly cursor: Cursor<T>;

constructor(public readonly txt: Peritext<T>) {
const point = txt.pointAbsStart();
const range = txt.range(point, point.clone());
// TODO: Add ability to remove cursor.
this.cursor = txt.localSlices.ins<Cursor<T>, typeof Cursor>(
range,
public get cursor(): Cursor<T> {
const maybeCursor = this.firstCursor();
if (maybeCursor) return maybeCursor;
const txt = this.txt;
const cursor = txt.localSlices.ins<Cursor<T>, typeof Cursor>(
txt.rangeAt(0),
SliceBehavior.Cursor,
CursorAnchor.Start,
undefined,
Cursor,
);
return cursor;
}

public cursors(callback: (cursor: Cursor<T>) => void): void {
this.txt.localSlices.forEach((slice) => {
if (slice instanceof Cursor) callback(slice);
});
}

/**
* Insert inline text at current cursor position. If cursor selects a range,
* the range is removed and the text is inserted at the start of the range.
*/
public insert(text: string): void {
this.cursor.insert(text);
this.cursors(cursor => cursor.insert(text));
}

/**
* Deletes the previous character at current cursor position. If cursor
* selects a range, deletes the whole range.
*/
public delBwd(): void {
this.cursor.delBwd();
this.cursors(cursor => cursor.delBwd());
}

/** @todo Add main impl details of this to `Cursor`, but here ensure there is only one cursor. */
public selectAll(): boolean {
const range = this.txt.rangeAll();
if (!range) return false;
Expand Down
5 changes: 5 additions & 0 deletions src/json-crdt-extensions/peritext/slice/Slices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ export class Slices<T = string> implements Stateful, Printable {
return this.list._size;
}

public iterator0(): (() => Slice<T> | undefined) {
const iterator = this.list.iterator0();
return () => iterator()?.v;
}

public forEach(callback: (item: Slice<T>) => void): void {
this.list.forEach((node) => callback(node.v));
}
Expand Down

0 comments on commit 7a6850b

Please sign in to comment.