Skip to content

Commit 216bd9e

Browse files
committed
feat(json-crdt-extensions): 🎸 refactor Peritext implementation, improve perf
1 parent aed68d0 commit 216bd9e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+2048
-2336
lines changed

‎packages/json-joy/src/json-crdt-extensions/cnt/__demos__/usage.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* npx nodemon -q -x ts-node src/json-crdt-extensions/cnt/__demos__/usage.ts
77
*/
88

9-
import {Model} from '../../../json-crdt';
9+
import {Model, s} from '../../../json-crdt';
1010
import {cnt} from '..';
1111

1212
console.clear();

‎packages/json-joy/src/json-crdt-extensions/peritext/Peritext.ts‎

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import {type ArrNode, StrNode} from '../../json-crdt/nodes';
77
import {Slices} from './slice/Slices';
88
import {LocalSlices} from './slice/LocalSlices';
99
import {Overlay} from './overlay/Overlay';
10-
import {Chars} from './constants';
11-
import {interval, tick} from '../../json-crdt-patch/clock';
10+
import {tick} from '../../json-crdt-patch/clock';
1211
import {Model, type StrApi} from '../../json-crdt/model';
1312
import {CONST, updateNum} from '../../json-hash/hash';
1413
import {SESSION} from '../../json-crdt-patch/constants';
@@ -18,13 +17,12 @@ import {Fragment} from './block/Fragment';
1817
import {updateRga} from '../../json-crdt/hash';
1918
import type {ITimestampStruct} from '../../json-crdt-patch/clock';
2019
import type {Printable} from 'tree-dump/lib/types';
21-
import type {MarkerSlice} from './slice/MarkerSlice';
2220
import type {SliceSchema, SliceTypeSteps} from './slice/types';
2321
import type {SchemaToJsonNode} from '../../json-crdt/schema/types';
2422
import type {AbstractRga} from '../../json-crdt/nodes/rga';
2523
import type {ChunkSlice} from './util/ChunkSlice';
2624
import type {Stateful} from './types';
27-
import type {PersistedSlice} from './slice/PersistedSlice';
25+
import type {Slice} from './slice/Slice';
2826

2927
const EXTRA_SLICES_SCHEMA = s.vec(s.arr<SliceSchema>([]));
3028
const LOCAL_DATA_SCHEMA = EXTRA_SLICES_SCHEMA;
@@ -75,7 +73,8 @@ export class Peritext<T = string> implements Printable, Stateful {
7573
}
7674

7775
public strApi(): StrApi {
78-
if (this.str instanceof StrNode) return this.model.api.wrap(this.str);
76+
const str = this.str;
77+
if (str instanceof StrNode) return this.model.api.wrap(str);
7978
throw new Error('INVALID_STR');
8079
}
8180

@@ -98,7 +97,7 @@ export class Peritext<T = string> implements Printable, Stateful {
9897
*
9998
* @param pos Position of the character in the text.
10099
* @param anchor Whether the point should attach before or after a character.
101-
* Defaults to "before".
100+
* Defaults to "before".
102101
* @returns The point.
103102
*/
104103
public pointAt(pos: number, anchor: Anchor = Anchor.Before): Point<T> {
@@ -110,6 +109,27 @@ export class Peritext<T = string> implements Printable, Stateful {
110109
return this.point(id, anchor);
111110
}
112111

112+
/**
113+
* Creates a point at a view position in the text, between characters.
114+
*
115+
* @param pos Position between characters in the text.
116+
* @param anchor Whether the point should attach before or after a character.
117+
* Defaults to "after".
118+
* @returns The point.
119+
*/
120+
public pointIn(pos: number, anchor: Anchor = Anchor.After): Point<T> {
121+
const str = this.str;
122+
if (anchor === Anchor.After) {
123+
if (!pos) return this.pointAbsStart();
124+
const id = str.find(pos - 1);
125+
if (!id) return this.pointEnd() ?? this.pointAbsStart();
126+
return this.point(id, Anchor.After);
127+
} else {
128+
const id = str.find(pos);
129+
return id ? this.point(id, Anchor.Before) : this.pointAbsEnd();
130+
}
131+
}
132+
113133
/**
114134
* Creates a point which is attached to the start of the text, before the
115135
* first character.
@@ -299,31 +319,15 @@ export class Peritext<T = string> implements Printable, Stateful {
299319
*/
300320
public readonly localSlices: Slices<T>;
301321

302-
public getSlice(id: ITimestampStruct): PersistedSlice<T> | undefined {
322+
public getSlice(id: ITimestampStruct): Slice<T> | undefined {
303323
return this.savedSlices.get(id) || this.localSlices.get(id) || this.extraSlices.get(id);
304324
}
305325

306326
// ------------------------------------------------------------------ markers
307327

308328
/** @deprecated Use the method in `Editor` and `Cursor` instead. */
309-
public insMarker(
310-
after: ITimestampStruct,
311-
type: SliceTypeSteps,
312-
data?: unknown,
313-
char: string = Chars.BlockSplitSentinel,
314-
): MarkerSlice<T> {
315-
return this.savedSlices.insMarkerAfter(after, type, data, char);
316-
}
317-
318-
/** @todo This can probably use .del() */
319-
public delMarker(split: MarkerSlice<T>): void {
320-
const str = this.str;
321-
const api = this.model.api;
322-
const builder = api.builder;
323-
const strChunk = split.start.chunk();
324-
if (strChunk) builder.del(str.id, [interval(strChunk.id, 0, 1)]);
325-
builder.del(this.savedSlices.set.id, [interval(split.id, 0, 1)]);
326-
api.apply();
329+
public insMarker(after: ITimestampStruct, type: SliceTypeSteps, data?: unknown): Slice<T> {
330+
return this.savedSlices.insMarkerAfter(after, type, data);
327331
}
328332

329333
/** ----------------------------------------------------- {@link Printable} */

‎packages/json-joy/src/json-crdt-extensions/peritext/__tests__/Peritext.cursor.spec.ts‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ test('cursor can move across block boundary forwards', () => {
9797
expect([...peritext.blocks.root.children[0].texts()][0].attr()[SliceTypeName.Cursor][0]).toBeInstanceOf(
9898
InlineAttrStartPoint,
9999
);
100-
101100
editor.cursor.move(1);
102101
peritext.refresh();
103102
expect(peritext.blocks.root.children.length).toBe(2);
@@ -113,15 +112,14 @@ test('cursor can move across block boundary forwards', () => {
113112
expect([...peritext.blocks.root.children[1].texts()][0].attr()).toEqual({});
114113
editor.cursor.move(1);
115114
peritext.refresh();
115+
// console.log(peritext + '');
116116
expect(peritext.blocks.root.children.length).toBe(2);
117117
expect([...peritext.blocks.root.children[0].texts()].length).toBe(1);
118118
expect([...peritext.blocks.root.children[0].texts()][0].text()).toBe('a');
119119
expect([...peritext.blocks.root.children[0].texts()][0].attr()).toEqual({});
120-
expect([...peritext.blocks.root.children[1].texts()].length).toBe(2);
121-
expect([...peritext.blocks.root.children[1].texts()][0].text()).toBe('');
122-
expect([...peritext.blocks.root.children[1].texts()][0].attr()).toEqual({});
123-
expect([...peritext.blocks.root.children[1].texts()][1].text()).toBe('b');
124-
expect([...peritext.blocks.root.children[1].texts()][1].attr()[SliceTypeName.Cursor][0]).toBeInstanceOf(
120+
expect([...peritext.blocks.root.children[1].texts()].length).toBe(1);
121+
expect([...peritext.blocks.root.children[1].texts()][0].text()).toBe('b');
122+
expect([...peritext.blocks.root.children[1].texts()][0].attr()[SliceTypeName.Cursor][0]).toBeInstanceOf(
125123
InlineAttrStartPoint,
126124
);
127125
editor.cursor.move(1);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {type Kit, runNumbersKitTestSuite} from './setup';
2+
import {Anchor} from '../rga/constants';
3+
4+
const testSuite = (setup: () => Kit): void => {
5+
describe('.pointIn()', () => {
6+
test('can infer point between characters', () => {
7+
const {peritext} = setup();
8+
for (let i = 0; i <= 9; i++) {
9+
const p0 = peritext.pointIn(i);
10+
expect(p0.anchor).toBe(Anchor.After);
11+
expect(p0.viewPos()).toBe(i);
12+
expect(p0.rightChar()?.view()).toBe(i.toString());
13+
const p1 = peritext.pointIn(i, Anchor.Before);
14+
expect(p1.anchor).toBe(Anchor.Before);
15+
expect(p1.viewPos()).toBe(i);
16+
expect(p1.rightChar()?.view()).toBe(i.toString());
17+
}
18+
});
19+
});
20+
};
21+
22+
runNumbersKitTestSuite(testSuite);

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

Lines changed: 115 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,26 @@ const runInlineSlicesTests = (
3131
const {view, editor} = setup();
3232
editor.cursor.setAt(10);
3333
editor.saved.insMarker([['p', 0, {foo: 'bar'}]]);
34-
expect(view()).toMatchSnapshot();
34+
expect(view()).toMatchInlineSnapshot(`
35+
"<>
36+
<0>
37+
"abcdefghij" {}
38+
<p> { foo = "bar" }
39+
"klmnopqrstuvwxyz" {}
40+
"
41+
`);
3542
});
3643

3744
test('can insert at the beginning of text', () => {
3845
const {view, editor} = setup();
3946
editor.cursor.setAt(0);
4047
editor.saved.insMarker([['p', 0, {foo: 'bar'}]]);
41-
expect(view()).toMatchSnapshot();
48+
expect(view()).toMatchInlineSnapshot(`
49+
"<>
50+
<p> { foo = "bar" }
51+
"abcdefghijklmnopqrstuvwxyz" {}
52+
"
53+
`);
4254
});
4355

4456
test('nested block data', () => {
@@ -53,7 +65,17 @@ const runInlineSlicesTests = (
5365
['ul', 0, {type: 'tasks'}],
5466
['li', 1, {completed: true}],
5567
]);
56-
expect(view()).toMatchSnapshot();
68+
expect(view()).toMatchInlineSnapshot(`
69+
"<>
70+
<0>
71+
"abcde" {}
72+
<ul> { type = "tasks" }
73+
<li> { completed = !f }
74+
"fghi" {}
75+
<li> { completed = !t }
76+
"jklmnopqrstuvwxyz" {}
77+
"
78+
`);
5779
});
5880

5981
test('nested block data - 2', () => {
@@ -68,14 +90,31 @@ const runInlineSlicesTests = (
6890
['ul', 1, {type: 'tasks'}],
6991
['li', 0, {completed: true}],
7092
]);
71-
expect(view()).toMatchSnapshot();
93+
expect(view()).toMatchInlineSnapshot(`
94+
"<>
95+
<0>
96+
"abcde" {}
97+
<ul> { type = "tasks" }
98+
<li> { completed = !f }
99+
"fghi" {}
100+
<ul> { type = "tasks" }
101+
<li> { completed = !t }
102+
"jklmnopqrstuvwxyz" {}
103+
"
104+
`);
72105
});
73106

74107
test('can insert at the end of text', () => {
75108
const {view, editor} = setup();
76109
editor.cursor.setAt(26);
77110
editor.saved.insMarker([['unfurl', 0, {link: 'foobar'}]]);
78-
expect(view()).toMatchSnapshot();
111+
expect(view()).toMatchInlineSnapshot(`
112+
"<>
113+
<0>
114+
"abcdefghijklmnopqrstuvwxyz" {}
115+
<unfurl> { link = "foobar" }
116+
"
117+
`);
79118
});
80119

81120
test('can split text after slice', () => {
@@ -84,7 +123,16 @@ const runInlineSlicesTests = (
84123
editor.saved.insOne('BOLD');
85124
editor.cursor.setAt(15);
86125
editor.saved.insMarker(['paragraph']);
87-
expect(view()).toMatchSnapshot();
126+
expect(view()).toMatchInlineSnapshot(`
127+
"<>
128+
<0>
129+
"abcde" {}
130+
"fghij" { BOLD = [ !u ] }
131+
"klmno" {}
132+
<paragraph>
133+
"pqrstuvwxyz" {}
134+
"
135+
`);
88136
});
89137

90138
test('can split text right after slice', () => {
@@ -93,7 +141,16 @@ const runInlineSlicesTests = (
93141
editor.saved.insOne('BOLD');
94142
editor.cursor.setAt(10);
95143
editor.saved.insMarker(['paragraph']);
96-
expect(view()).toMatchSnapshot();
144+
expect(view()).toMatchInlineSnapshot(`
145+
"<>
146+
<0>
147+
"abcde" {}
148+
"fghij" { BOLD = [ !u ] }
149+
"" {}
150+
<paragraph>
151+
"klmnopqrstuvwxyz" {}
152+
"
153+
`);
97154
});
98155

99156
test('can split text before slice', () => {
@@ -102,7 +159,16 @@ const runInlineSlicesTests = (
102159
editor.saved.insOne('BOLD');
103160
editor.cursor.setAt(10);
104161
editor.saved.insMarker(['paragraph']);
105-
expect(view()).toMatchSnapshot();
162+
expect(view()).toMatchInlineSnapshot(`
163+
"<>
164+
<0>
165+
"abcdefghij" {}
166+
<paragraph>
167+
"klmno" {}
168+
"pqrst" { BOLD = [ !u ] }
169+
"uvwxyz" {}
170+
"
171+
`);
106172
});
107173

108174
test('can split text right before slice', () => {
@@ -111,7 +177,15 @@ const runInlineSlicesTests = (
111177
editor.saved.insOne('BOLD');
112178
editor.cursor.setAt(15);
113179
editor.saved.insMarker(['paragraph']);
114-
expect(view()).toMatchSnapshot();
180+
expect(view()).toMatchInlineSnapshot(`
181+
"<>
182+
<0>
183+
"abcdefghijklmno" {}
184+
<paragraph>
185+
"pqrst" { BOLD = [ !u ] }
186+
"uvwxyz" {}
187+
"
188+
`);
115189
});
116190

117191
test('can split text in the middle of a slice', () => {
@@ -120,7 +194,16 @@ const runInlineSlicesTests = (
120194
editor.saved.insOne('BOLD');
121195
editor.cursor.setAt(10);
122196
editor.saved.insMarker(['paragraph']);
123-
expect(view()).toMatchSnapshot();
197+
expect(view()).toMatchInlineSnapshot(`
198+
"<>
199+
<0>
200+
"abcde" {}
201+
"fghij" { BOLD = [ !u ] }
202+
<paragraph>
203+
"klmno" { BOLD = [ !u ] }
204+
"pqrstuvwxyz" {}
205+
"
206+
`);
124207
});
125208

126209
test('can annotate with slice over two block splits', () => {
@@ -131,7 +214,18 @@ const runInlineSlicesTests = (
131214
editor.saved.insMarker(['p']);
132215
editor.cursor.setAt(8, 15);
133216
editor.saved.insOne('BOLD');
134-
expect(view()).toMatchSnapshot();
217+
expect(view()).toMatchInlineSnapshot(`
218+
"<>
219+
<0>
220+
"abcdefgh" {}
221+
"ij" { BOLD = [ !u ] }
222+
<p>
223+
"klmn" { BOLD = [ !u ] }
224+
<p>
225+
"opqrstu" { BOLD = [ !u ] }
226+
"vwxyz" {}
227+
"
228+
`);
135229
});
136230

137231
test('can insert two blocks', () => {
@@ -140,7 +234,16 @@ const runInlineSlicesTests = (
140234
editor.saved.insMarker('p');
141235
editor.cursor.setAt(10 + 10 + 1);
142236
editor.saved.insMarker('p');
143-
expect(view()).toMatchSnapshot();
237+
expect(view()).toMatchInlineSnapshot(`
238+
"<>
239+
<0>
240+
"abcdefghij" {}
241+
<p>
242+
"klmnopqrst" {}
243+
<p>
244+
"uvwxyz" {}
245+
"
246+
`);
144247
});
145248
});
146249
};

0 commit comments

Comments
 (0)