Skip to content

Commit 8206d8c

Browse files
authored
Merge pull request #764 from streamich/peritext-stacked-annotations
Peritext block commands
2 parents acdef97 + 9b46a4d commit 8206d8c

File tree

31 files changed

+520
-178
lines changed

31 files changed

+520
-178
lines changed

src/json-crdt-extensions/peritext/__tests__/Peritext.render-block.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,17 +220,17 @@ runInlineSlicesTests('text with block split', (editor: Editor) => {
220220
runInlineSlicesTests('text with deletes', (editor: Editor) => {
221221
editor.insert('lmXXXnwYxyz');
222222
editor.cursor.setAt(2, 3);
223-
editor.cursor.del();
223+
editor.del();
224224
editor.cursor.setAt(3);
225225
editor.insert('opqrstuv');
226226
editor.cursor.setAt(12, 1);
227-
editor.cursor.del();
227+
editor.del();
228228
editor.cursor.setAt(0);
229229
editor.insert('ab1c3defghijk4444');
230230
editor.cursor.setAt(2, 1);
231-
editor.cursor.del();
231+
editor.del();
232232
editor.cursor.setAt(3, 1);
233-
editor.cursor.del();
233+
editor.del();
234234
editor.cursor.setAt(11, 4);
235-
editor.cursor.del();
235+
editor.del();
236236
});

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ const run = (setup: () => Kit) => {
109109
const {peritext, model} = setup();
110110
const {editor} = peritext;
111111
expect(editor.cursor.isCollapsed()).toBe(true);
112-
editor.cursor.collapse();
112+
editor.collapseCursors();
113113
expect(editor.cursor.isCollapsed()).toBe(true);
114114
expect((model.view() as any).text).toBe('hello world');
115115
});
@@ -119,7 +119,7 @@ const run = (setup: () => Kit) => {
119119
const {editor} = peritext;
120120
editor.cursor.setAt(2, 3);
121121
expect(editor.cursor.isCollapsed()).toBe(false);
122-
editor.cursor.collapse();
122+
editor.collapseCursors();
123123
expect(editor.cursor.isCollapsed()).toBe(true);
124124
expect((model.view() as any).text).toBe('he world');
125125
});
@@ -129,12 +129,12 @@ const run = (setup: () => Kit) => {
129129
const {editor} = peritext;
130130
peritext.editor.cursor.setAt(0, 1);
131131
expect(editor.cursor.isCollapsed()).toBe(false);
132-
editor.cursor.collapse();
132+
editor.collapseCursors();
133133
expect(editor.cursor.isCollapsed()).toBe(true);
134134
expect((model.view() as any).text).toBe('ello world');
135135
editor.cursor.setAt(0, 1);
136136
expect(editor.cursor.isCollapsed()).toBe(false);
137-
editor.cursor.collapse();
137+
editor.collapseCursors();
138138
expect(editor.cursor.isCollapsed()).toBe(true);
139139
expect((model.view() as any).text).toBe('llo world');
140140
});
@@ -144,12 +144,12 @@ const run = (setup: () => Kit) => {
144144
const {editor} = peritext;
145145
editor.cursor.setAt(peritext.str.length() - 1, 1);
146146
expect(editor.cursor.isCollapsed()).toBe(false);
147-
editor.cursor.collapse();
147+
editor.collapseCursors();
148148
expect(editor.cursor.isCollapsed()).toBe(true);
149149
expect((model.view() as any).text).toBe('hello worl');
150150
peritext.editor.cursor.setAt(peritext.str.length() - 1, 1);
151151
expect(editor.cursor.isCollapsed()).toBe(false);
152-
editor.cursor.collapse();
152+
editor.collapseCursors();
153153
expect(editor.cursor.isCollapsed()).toBe(true);
154154
expect((model.view() as any).text).toBe('hello wor');
155155
});
@@ -159,7 +159,7 @@ const run = (setup: () => Kit) => {
159159
const {editor} = peritext;
160160
editor.cursor.setAt(0, peritext.str.length());
161161
expect(editor.cursor.isCollapsed()).toBe(false);
162-
editor.cursor.collapse();
162+
editor.collapseCursors();
163163
expect(editor.cursor.isCollapsed()).toBe(true);
164164
expect((model.view() as any).text).toBe('');
165165
editor.insert('abc');

src/json-crdt-extensions/peritext/block/Block.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {MarkerOverlayPoint} from '../overlay/MarkerOverlayPoint';
44
import type {OverlayPoint} from '../overlay/OverlayPoint';
55
import {UndefEndIter, type UndefIterator} from '../../../util/iterator';
66
import {Inline} from './Inline';
7+
import {formatType} from '../slice/util';
78
import type {Path} from '@jsonjoy.com/json-pointer';
89
import type {Printable} from 'tree-dump';
910
import type {Peritext} from '../Peritext';
@@ -144,7 +145,7 @@ export class Block<Attr = unknown> implements IBlock, Printable, Stateful {
144145
}
145146
protected toStringHeader(): string {
146147
const hash = `#${this.hash.toString(36).slice(-4)}`;
147-
const tag = `<${this.path.join('.')}>`;
148+
const tag = this.path.map((step) => formatType(step)).join('.');
148149
const header = `${this.toStringName()} ${hash} ${tag}`;
149150
return header;
150151
}

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

Lines changed: 5 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -71,60 +71,11 @@ export class Cursor<T = string> extends PersistedSlice<T> {
7171
}
7272
}
7373

74-
/**
75-
* Ensures there is no range selection. If user has selected a range,
76-
* the contents is removed and the cursor is set at the start of the range as cursor.
77-
*
78-
* @todo If block boundaries are withing the range, remove the blocks.
79-
* @todo Stress test this method.
80-
*
81-
* @returns Returns the cursor position after the operation.
82-
*/
83-
public collapse(): void {
84-
const deleted = this.txt.delStr(this);
85-
if (deleted) this.collapseToStart();
86-
}
87-
88-
/**
89-
* Insert inline text at current cursor position. If cursor selects a range,
90-
* the range is removed and the text is inserted at the start of the range.
91-
*/
92-
public insert(text: string): void {
93-
if (!text) return;
94-
this.collapse();
95-
const after = this.start.clone();
96-
after.refAfter();
97-
const textId = this.txt.ins(after.id, text);
98-
const shift = text.length - 1;
99-
this.setAfter(shift ? tick(textId, shift) : textId);
100-
}
101-
102-
/**
103-
* Deletes the given number of characters from the current caret position.
104-
* Negative values delete backwards. If the cursor selects a range, the
105-
* range is removed and the cursor is set at the start of the range.
106-
*
107-
* @param step Number of characters to delete. Negative values delete
108-
* backwards.
109-
*/
110-
public del(step: number = -1): void {
111-
if (!this.isCollapsed()) {
112-
this.collapse();
113-
return;
114-
}
115-
const point1 = this.start.clone();
116-
const point2 = point1.clone();
117-
if (step > 0) point2.step(1);
118-
else if (step < 0) point1.step(-1);
119-
else if (step === 0) {
120-
point1.step(-1);
121-
point2.step(1);
122-
}
123-
const txt = this.txt;
124-
const range = txt.range(point1, point2);
125-
txt.delStr(range);
126-
point1.refAfter();
127-
this.set(point1);
74+
public collapseToStart(anchorSide: CursorAnchor = CursorAnchor.Start): void {
75+
const start = this.start.clone();
76+
start.refAfter();
77+
const end = start.clone();
78+
this.set(start, end, anchorSide);
12879
}
12980

13081
// ---------------------------------------------------------------- Printable

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

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ import {UndefEndIter, type UndefIterator} from '../../../util/iterator';
1111
import {PersistedSlice} from '../slice/PersistedSlice';
1212
import {ValueSyncStore} from '../../../util/events/sync-store';
1313
import {formatType} from '../slice/util';
14-
import type {CommonSliceType} from '../slice';
14+
import {CommonSliceType, type SliceType} from '../slice';
1515
import type {ChunkSlice} from '../util/ChunkSlice';
1616
import type {Peritext} from '../Peritext';
1717
import type {Point} from '../rga/Point';
1818
import type {Range} from '../rga/Range';
1919
import type {CharIterator, CharPredicate, Position, TextRangeUnit} from './types';
2020
import type {Printable} from 'tree-dump';
21+
import {tick} from '../../../json-crdt-patch';
2122

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

127+
/**
128+
* Ensures there is no range selection. If user has selected a range,
129+
* the contents is removed and the cursor is set at the start of the range
130+
* as caret.
131+
*/
132+
public collapseCursor(cursor: Cursor<T>): void {
133+
this.delRange(cursor);
134+
cursor.collapseToStart();
135+
}
136+
137+
public collapseCursors(): void {
138+
for (let i = this.cursors0(), cursor = i(); cursor; cursor = i()) this.collapseCursor(cursor);
139+
}
140+
126141
// ------------------------------------------------------------- text editing
127142

128143
/**
129144
* Insert inline text at current cursor position. If cursor selects a range,
130145
* the range is removed and the text is inserted at the start of the range.
131146
*/
147+
public insert0(cursor: Cursor<T>, text: string): void {
148+
if (!text) return;
149+
if (!cursor.isCollapsed()) this.delRange(cursor);
150+
const after = cursor.start.clone();
151+
after.refAfter();
152+
const txt = this.txt;
153+
const textId = txt.ins(after.id, text);
154+
const shift = text.length - 1;
155+
const point = txt.point(shift ? tick(textId, shift) : textId, Anchor.After);
156+
cursor.set(point, point, CursorAnchor.Start);
157+
}
158+
159+
/**
160+
* Inserts text at the cursor positions and collapses cursors, if necessary.
161+
* The applies any pending inline formatting to the inserted text.
162+
*/
132163
public insert(text: string): void {
133164
if (!this.hasCursor()) this.addCursor();
134165
for (let cursor: Cursor<T> | undefined, i = this.cursors0(); (cursor = i()); ) {
135-
cursor.insert(text);
166+
this.insert0(cursor, text);
136167
const pending = this.pending.value;
137168
if (pending.size) {
138169
this.pending.next(new Map());
@@ -149,7 +180,16 @@ export class Editor<T = string> implements Printable {
149180
* select a range, deletes the whole range.
150181
*/
151182
public del(step: number = -1): void {
152-
this.forCursor((cursor) => cursor.del(step));
183+
this.delete(step, 'char');
184+
}
185+
186+
public delRange(range: Range<T>): void {
187+
const txt = this.txt;
188+
const overlay = txt.overlay;
189+
const contained = overlay.findContained(range);
190+
for (const slice of contained)
191+
if (slice instanceof PersistedSlice && slice.behavior !== SliceBehavior.Cursor) slice.del();
192+
txt.delStr(range);
153193
}
154194

155195
/**
@@ -160,9 +200,10 @@ export class Editor<T = string> implements Printable {
160200
* @param unit A unit of deletion: "char", "word", "line".
161201
*/
162202
public delete(step: number, unit: 'char' | 'word' | 'line'): void {
163-
this.forCursor((cursor) => {
203+
const txt = this.txt;
204+
for (let i = this.cursors0(), cursor = i(); cursor; cursor = i()) {
164205
if (!cursor.isCollapsed()) {
165-
cursor.collapse();
206+
this.collapseCursor(cursor);
166207
return;
167208
}
168209
let point1 = cursor.start.clone();
@@ -173,12 +214,11 @@ export class Editor<T = string> implements Printable {
173214
point1 = this.skip(point1, -1, unit);
174215
point2 = this.skip(point2, 1, unit);
175216
}
176-
const txt = this.txt;
177217
const range = txt.range(point1, point2);
178-
txt.delStr(range);
218+
this.delRange(range);
179219
point1.refAfter();
180220
cursor.set(point1);
181-
});
221+
}
182222
}
183223

184224
// ----------------------------------------------------------------- movement
@@ -494,7 +534,7 @@ export class Editor<T = string> implements Printable {
494534
public select(unit: TextRangeUnit): void {
495535
this.forCursor((cursor) => {
496536
const range = this.range(cursor.start, unit);
497-
if (range) cursor.setRange(range);
537+
if (range) cursor.set(range.start, range.end, CursorAnchor.Start);
498538
else this.delCursors;
499539
});
500540
}
@@ -506,14 +546,6 @@ export class Editor<T = string> implements Printable {
506546

507547
// --------------------------------------------------------------- formatting
508548

509-
protected getSliceStore(slice: PersistedSlice<T>): EditorSlices<T> | undefined {
510-
const sid = slice.id.sid;
511-
if (sid === this.saved.slices.set.doc.clock.sid) return this.saved;
512-
if (sid === this.extra.slices.set.doc.clock.sid) return this.extra;
513-
if (sid === this.local.slices.set.doc.clock.sid) return this.local;
514-
return;
515-
}
516-
517549
protected toggleRangeExclFmt(
518550
range: Range<T>,
519551
type: CommonSliceType | string | number,
@@ -527,12 +559,7 @@ export class Editor<T = string> implements Printable {
527559
const needToRemoveFormatting = complete.has(type);
528560
makeRangeExtendable(range);
529561
const contained = overlay.findContained(range);
530-
for (const slice of contained) {
531-
if (slice instanceof PersistedSlice && slice.type === type) {
532-
const deletionStore = this.getSliceStore(slice);
533-
if (deletionStore) deletionStore.del(slice.id);
534-
}
535-
}
562+
for (const slice of contained) if (slice instanceof PersistedSlice && slice.type === type) slice.del();
536563
if (needToRemoveFormatting) {
537564
overlay.refresh();
538565
const [complete2, partial2] = overlay.stat(range, 1e6);
@@ -584,10 +611,8 @@ export class Editor<T = string> implements Printable {
584611
switch (slice.behavior) {
585612
case SliceBehavior.One:
586613
case SliceBehavior.Many:
587-
case SliceBehavior.Erase: {
588-
const deletionStore = this.getSliceStore(slice);
589-
if (deletionStore) deletionStore.del(slice.id);
590-
}
614+
case SliceBehavior.Erase:
615+
slice.del();
591616
}
592617
}
593618
}
@@ -613,6 +638,18 @@ export class Editor<T = string> implements Printable {
613638
}
614639
}
615640

641+
public split(type?: SliceType, data?: unknown, slices: EditorSlices<T> = this.saved): void {
642+
for (let i = this.cursors0(), cursor = i(); cursor; cursor = i()) {
643+
this.collapseCursor(cursor);
644+
if (type === void 0) {
645+
// TODO: detect current block type
646+
type = CommonSliceType.p;
647+
}
648+
slices.insMarker(type, data);
649+
cursor.move(1);
650+
}
651+
}
652+
616653
// ------------------------------------------------------------------ various
617654

618655
public point(at: Position<T>): Point<T> {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export class EditorSlices<T = string> {
3535

3636
public insMarker(type: SliceType, data?: unknown, separator?: string): MarkerSlice<T>[] {
3737
return this.insAtCursors((cursor) => {
38-
cursor.collapse();
38+
this.txt.editor.collapseCursor(cursor);
3939
const after = cursor.start.clone();
4040
after.refAfter();
4141
const marker = this.slices.insMarkerAfter(after.id, type, data, separator);

src/json-crdt-extensions/peritext/overlay/MarkerOverlayPoint.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class MarkerOverlayPoint<T = string> extends OverlayPoint<T> implements H
3333
// ---------------------------------------------------------------- Printable
3434

3535
public toStringName(): string {
36-
return 'OverlayPoint';
36+
return 'MarkerOverlayPoint';
3737
}
3838

3939
public toStringHeader(tab: string, lite?: boolean): string {

0 commit comments

Comments
 (0)