Skip to content

Commit

Permalink
Merge pull request #764 from streamich/peritext-stacked-annotations
Browse files Browse the repository at this point in the history
Peritext block commands
  • Loading branch information
streamich authored Nov 11, 2024
2 parents acdef97 + 9b46a4d commit 8206d8c
Show file tree
Hide file tree
Showing 31 changed files with 520 additions and 178 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -220,17 +220,17 @@ runInlineSlicesTests('text with block split', (editor: Editor) => {
runInlineSlicesTests('text with deletes', (editor: Editor) => {
editor.insert('lmXXXnwYxyz');
editor.cursor.setAt(2, 3);
editor.cursor.del();
editor.del();
editor.cursor.setAt(3);
editor.insert('opqrstuv');
editor.cursor.setAt(12, 1);
editor.cursor.del();
editor.del();
editor.cursor.setAt(0);
editor.insert('ab1c3defghijk4444');
editor.cursor.setAt(2, 1);
editor.cursor.del();
editor.del();
editor.cursor.setAt(3, 1);
editor.cursor.del();
editor.del();
editor.cursor.setAt(11, 4);
editor.cursor.del();
editor.del();
});
14 changes: 7 additions & 7 deletions src/json-crdt-extensions/peritext/__tests__/Peritext.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const run = (setup: () => Kit) => {
const {peritext, model} = setup();
const {editor} = peritext;
expect(editor.cursor.isCollapsed()).toBe(true);
editor.cursor.collapse();
editor.collapseCursors();
expect(editor.cursor.isCollapsed()).toBe(true);
expect((model.view() as any).text).toBe('hello world');
});
Expand All @@ -119,7 +119,7 @@ const run = (setup: () => Kit) => {
const {editor} = peritext;
editor.cursor.setAt(2, 3);
expect(editor.cursor.isCollapsed()).toBe(false);
editor.cursor.collapse();
editor.collapseCursors();
expect(editor.cursor.isCollapsed()).toBe(true);
expect((model.view() as any).text).toBe('he world');
});
Expand All @@ -129,12 +129,12 @@ const run = (setup: () => Kit) => {
const {editor} = peritext;
peritext.editor.cursor.setAt(0, 1);
expect(editor.cursor.isCollapsed()).toBe(false);
editor.cursor.collapse();
editor.collapseCursors();
expect(editor.cursor.isCollapsed()).toBe(true);
expect((model.view() as any).text).toBe('ello world');
editor.cursor.setAt(0, 1);
expect(editor.cursor.isCollapsed()).toBe(false);
editor.cursor.collapse();
editor.collapseCursors();
expect(editor.cursor.isCollapsed()).toBe(true);
expect((model.view() as any).text).toBe('llo world');
});
Expand All @@ -144,12 +144,12 @@ const run = (setup: () => Kit) => {
const {editor} = peritext;
editor.cursor.setAt(peritext.str.length() - 1, 1);
expect(editor.cursor.isCollapsed()).toBe(false);
editor.cursor.collapse();
editor.collapseCursors();
expect(editor.cursor.isCollapsed()).toBe(true);
expect((model.view() as any).text).toBe('hello worl');
peritext.editor.cursor.setAt(peritext.str.length() - 1, 1);
expect(editor.cursor.isCollapsed()).toBe(false);
editor.cursor.collapse();
editor.collapseCursors();
expect(editor.cursor.isCollapsed()).toBe(true);
expect((model.view() as any).text).toBe('hello wor');
});
Expand All @@ -159,7 +159,7 @@ const run = (setup: () => Kit) => {
const {editor} = peritext;
editor.cursor.setAt(0, peritext.str.length());
expect(editor.cursor.isCollapsed()).toBe(false);
editor.cursor.collapse();
editor.collapseCursors();
expect(editor.cursor.isCollapsed()).toBe(true);
expect((model.view() as any).text).toBe('');
editor.insert('abc');
Expand Down
3 changes: 2 additions & 1 deletion src/json-crdt-extensions/peritext/block/Block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {MarkerOverlayPoint} from '../overlay/MarkerOverlayPoint';
import type {OverlayPoint} from '../overlay/OverlayPoint';
import {UndefEndIter, type UndefIterator} from '../../../util/iterator';
import {Inline} from './Inline';
import {formatType} from '../slice/util';
import type {Path} from '@jsonjoy.com/json-pointer';
import type {Printable} from 'tree-dump';
import type {Peritext} from '../Peritext';
Expand Down Expand Up @@ -144,7 +145,7 @@ export class Block<Attr = unknown> implements IBlock, Printable, Stateful {
}
protected toStringHeader(): string {
const hash = `#${this.hash.toString(36).slice(-4)}`;
const tag = `<${this.path.join('.')}>`;
const tag = this.path.map((step) => formatType(step)).join('.');
const header = `${this.toStringName()} ${hash} ${tag}`;
return header;
}
Expand Down
59 changes: 5 additions & 54 deletions src/json-crdt-extensions/peritext/editor/Cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,60 +71,11 @@ export class Cursor<T = string> extends PersistedSlice<T> {
}
}

/**
* Ensures there is no range selection. If user has selected a range,
* the contents is removed and the cursor is set at the start of the range as cursor.
*
* @todo If block boundaries are withing the range, remove the blocks.
* @todo Stress test this method.
*
* @returns Returns the cursor position after the operation.
*/
public collapse(): void {
const deleted = this.txt.delStr(this);
if (deleted) this.collapseToStart();
}

/**
* 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 {
if (!text) return;
this.collapse();
const after = this.start.clone();
after.refAfter();
const textId = this.txt.ins(after.id, text);
const shift = text.length - 1;
this.setAfter(shift ? tick(textId, shift) : textId);
}

/**
* Deletes the given number of characters from the current caret position.
* Negative values delete backwards. If the cursor selects a range, the
* range is removed and the cursor is set at the start of the range.
*
* @param step Number of characters to delete. Negative values delete
* backwards.
*/
public del(step: number = -1): void {
if (!this.isCollapsed()) {
this.collapse();
return;
}
const point1 = this.start.clone();
const point2 = point1.clone();
if (step > 0) point2.step(1);
else if (step < 0) point1.step(-1);
else if (step === 0) {
point1.step(-1);
point2.step(1);
}
const txt = this.txt;
const range = txt.range(point1, point2);
txt.delStr(range);
point1.refAfter();
this.set(point1);
public collapseToStart(anchorSide: CursorAnchor = CursorAnchor.Start): void {
const start = this.start.clone();
start.refAfter();
const end = start.clone();
this.set(start, end, anchorSide);
}

// ---------------------------------------------------------------- Printable
Expand Down
91 changes: 64 additions & 27 deletions src/json-crdt-extensions/peritext/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import {UndefEndIter, type UndefIterator} from '../../../util/iterator';
import {PersistedSlice} from '../slice/PersistedSlice';
import {ValueSyncStore} from '../../../util/events/sync-store';
import {formatType} from '../slice/util';
import type {CommonSliceType} from '../slice';
import {CommonSliceType, type SliceType} from '../slice';
import type {ChunkSlice} from '../util/ChunkSlice';
import type {Peritext} from '../Peritext';
import type {Point} from '../rga/Point';
import type {Range} from '../rga/Range';
import type {CharIterator, CharPredicate, Position, TextRangeUnit} from './types';
import type {Printable} from 'tree-dump';
import {tick} from '../../../json-crdt-patch';

/**
* For inline boolean ("Overwrite") slices, both range endpoints should be
Expand Down Expand Up @@ -123,16 +124,46 @@ export class Editor<T = string> implements Printable {
for (let cursor: Cursor<T> | undefined, i = this.cursors0(); (cursor = i()); ) this.delCursor(cursor);
}

/**
* Ensures there is no range selection. If user has selected a range,
* the contents is removed and the cursor is set at the start of the range
* as caret.
*/
public collapseCursor(cursor: Cursor<T>): void {
this.delRange(cursor);
cursor.collapseToStart();
}

public collapseCursors(): void {
for (let i = this.cursors0(), cursor = i(); cursor; cursor = i()) this.collapseCursor(cursor);
}

// ------------------------------------------------------------- text editing

/**
* 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 insert0(cursor: Cursor<T>, text: string): void {
if (!text) return;
if (!cursor.isCollapsed()) this.delRange(cursor);
const after = cursor.start.clone();
after.refAfter();
const txt = this.txt;
const textId = txt.ins(after.id, text);
const shift = text.length - 1;
const point = txt.point(shift ? tick(textId, shift) : textId, Anchor.After);
cursor.set(point, point, CursorAnchor.Start);
}

/**
* Inserts text at the cursor positions and collapses cursors, if necessary.
* The applies any pending inline formatting to the inserted text.
*/
public insert(text: string): void {
if (!this.hasCursor()) this.addCursor();
for (let cursor: Cursor<T> | undefined, i = this.cursors0(); (cursor = i()); ) {
cursor.insert(text);
this.insert0(cursor, text);
const pending = this.pending.value;
if (pending.size) {
this.pending.next(new Map());
Expand All @@ -149,7 +180,16 @@ export class Editor<T = string> implements Printable {
* select a range, deletes the whole range.
*/
public del(step: number = -1): void {
this.forCursor((cursor) => cursor.del(step));
this.delete(step, 'char');
}

public delRange(range: Range<T>): void {
const txt = this.txt;
const overlay = txt.overlay;
const contained = overlay.findContained(range);
for (const slice of contained)
if (slice instanceof PersistedSlice && slice.behavior !== SliceBehavior.Cursor) slice.del();
txt.delStr(range);
}

/**
Expand All @@ -160,9 +200,10 @@ export class Editor<T = string> implements Printable {
* @param unit A unit of deletion: "char", "word", "line".
*/
public delete(step: number, unit: 'char' | 'word' | 'line'): void {
this.forCursor((cursor) => {
const txt = this.txt;
for (let i = this.cursors0(), cursor = i(); cursor; cursor = i()) {
if (!cursor.isCollapsed()) {
cursor.collapse();
this.collapseCursor(cursor);
return;
}
let point1 = cursor.start.clone();
Expand All @@ -173,12 +214,11 @@ export class Editor<T = string> implements Printable {
point1 = this.skip(point1, -1, unit);
point2 = this.skip(point2, 1, unit);
}
const txt = this.txt;
const range = txt.range(point1, point2);
txt.delStr(range);
this.delRange(range);
point1.refAfter();
cursor.set(point1);
});
}
}

// ----------------------------------------------------------------- movement
Expand Down Expand Up @@ -494,7 +534,7 @@ export class Editor<T = string> implements Printable {
public select(unit: TextRangeUnit): void {
this.forCursor((cursor) => {
const range = this.range(cursor.start, unit);
if (range) cursor.setRange(range);
if (range) cursor.set(range.start, range.end, CursorAnchor.Start);
else this.delCursors;
});
}
Expand All @@ -506,14 +546,6 @@ export class Editor<T = string> implements Printable {

// --------------------------------------------------------------- formatting

protected getSliceStore(slice: PersistedSlice<T>): EditorSlices<T> | undefined {
const sid = slice.id.sid;
if (sid === this.saved.slices.set.doc.clock.sid) return this.saved;
if (sid === this.extra.slices.set.doc.clock.sid) return this.extra;
if (sid === this.local.slices.set.doc.clock.sid) return this.local;
return;
}

protected toggleRangeExclFmt(
range: Range<T>,
type: CommonSliceType | string | number,
Expand All @@ -527,12 +559,7 @@ export class Editor<T = string> implements Printable {
const needToRemoveFormatting = complete.has(type);
makeRangeExtendable(range);
const contained = overlay.findContained(range);
for (const slice of contained) {
if (slice instanceof PersistedSlice && slice.type === type) {
const deletionStore = this.getSliceStore(slice);
if (deletionStore) deletionStore.del(slice.id);
}
}
for (const slice of contained) if (slice instanceof PersistedSlice && slice.type === type) slice.del();
if (needToRemoveFormatting) {
overlay.refresh();
const [complete2, partial2] = overlay.stat(range, 1e6);
Expand Down Expand Up @@ -584,10 +611,8 @@ export class Editor<T = string> implements Printable {
switch (slice.behavior) {
case SliceBehavior.One:
case SliceBehavior.Many:
case SliceBehavior.Erase: {
const deletionStore = this.getSliceStore(slice);
if (deletionStore) deletionStore.del(slice.id);
}
case SliceBehavior.Erase:
slice.del();
}
}
}
Expand All @@ -613,6 +638,18 @@ export class Editor<T = string> implements Printable {
}
}

public split(type?: SliceType, data?: unknown, slices: EditorSlices<T> = this.saved): void {
for (let i = this.cursors0(), cursor = i(); cursor; cursor = i()) {
this.collapseCursor(cursor);
if (type === void 0) {
// TODO: detect current block type
type = CommonSliceType.p;
}
slices.insMarker(type, data);
cursor.move(1);
}
}

// ------------------------------------------------------------------ various

public point(at: Position<T>): Point<T> {
Expand Down
2 changes: 1 addition & 1 deletion src/json-crdt-extensions/peritext/editor/EditorSlices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class EditorSlices<T = string> {

public insMarker(type: SliceType, data?: unknown, separator?: string): MarkerSlice<T>[] {
return this.insAtCursors((cursor) => {
cursor.collapse();
this.txt.editor.collapseCursor(cursor);
const after = cursor.start.clone();
after.refAfter();
const marker = this.slices.insMarkerAfter(after.id, type, data, separator);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class MarkerOverlayPoint<T = string> extends OverlayPoint<T> implements H
// ---------------------------------------------------------------- Printable

public toStringName(): string {
return 'OverlayPoint';
return 'MarkerOverlayPoint';
}

public toStringHeader(tab: string, lite?: boolean): string {
Expand Down
Loading

0 comments on commit 8206d8c

Please sign in to comment.