Skip to content

Commit d10aac9

Browse files
authored
Merge pull request #719 from streamich/perf-editor-iterators
Improve and generalize character iteration in `Editor`
2 parents 83f6a5c + d85b8c0 commit d10aac9

File tree

5 files changed

+74
-117
lines changed

5 files changed

+74
-117
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export class Peritext<T = string> implements Printable {
132132
// TODO: Provide ability to attach to the end of the text?
133133
const str = this.str;
134134
const id = str.find(pos);
135-
if (!id) return this.point(str.id, Anchor.After);
135+
if (!id) return this.point(str.id, pos ? Anchor.Before : Anchor.After);
136136
return this.point(id, anchor);
137137
}
138138

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

Lines changed: 28 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -104,92 +104,47 @@ export class Editor<T = string> {
104104
}
105105

106106
/**
107-
* Returns a forward iterator through visible text, one character at a time,
108-
* starting from a given chunk and offset.
107+
* Returns an iterator through visible text, one `step` characters at a time,
108+
* starting from a given {@link Point}.
109109
*
110-
* @param chunk Chunk to start from.
111-
* @param offset Offset in the chunk to start from.
110+
* @param start The starting point.
111+
* @param step Number of visible characters to skip.
112112
* @returns The next visible character iterator.
113113
*/
114-
public fwd0(chunk: undefined | Chunk<T>, offset: number): CharIterator<T> {
115-
const str = this.txt.str;
114+
public walk(start: Point<T>, step: number = 1): CharIterator<T> {
115+
let point: Point<T> | undefined = start.clone();
116116
return () => {
117-
if (!chunk) return;
118-
const span = chunk.span;
119-
const offsetToReturn = offset;
120-
const chunkToReturn = chunk;
121-
if (offset >= span) return;
122-
offset++;
123-
if (offset >= span) {
124-
offset = 0;
125-
chunk = str.next(chunk);
126-
while (chunk && chunk.del) chunk = str.next(chunk);
127-
}
128-
return new ChunkSlice<T>(chunkToReturn, offsetToReturn, 1);
117+
if (!point) return;
118+
const char = step > 0 ? point.rightChar() : point.leftChar();
119+
if (!char) return (point = undefined);
120+
const end = point.move(step);
121+
if (end) point = undefined;
122+
return char;
129123
};
130124
}
131125

132126
/**
133127
* Returns a forward iterator through visible text, one character at a time,
134-
* starting from a given ID.
128+
* starting from a given {@link Point}.
135129
*
136-
* @param id ID to start from.
130+
* @param start The starting point.
137131
* @param chunk Chunk to start from.
138132
* @returns The next visible character iterator.
139133
*/
140-
public fwd1(id: ITimestampStruct, chunk?: Chunk<T>): CharIterator<T> {
141-
const str = this.txt.str;
142-
const startFromStrRoot = equal(id, str.id);
143-
if (startFromStrRoot) {
144-
chunk = str.first();
145-
while (chunk && chunk.del) chunk = str.next(chunk);
146-
return this.fwd0(chunk, 0);
147-
}
148-
let offset: number = 0;
149-
if (!chunk || !contains(chunk.id, chunk.span, id, 1)) {
150-
chunk = str.findById(id);
151-
if (!chunk) return () => undefined;
152-
offset = id.time - chunk.id.time;
153-
} else offset = id.time - chunk.id.time;
154-
if (!chunk.del) return this.fwd0(chunk, offset);
155-
while (chunk && chunk.del) chunk = str.next(chunk);
156-
return this.fwd0(chunk, 0);
134+
public fwd(start: Point<T>): CharIterator<T> {
135+
return this.walk(start, 1);
157136
}
158137

159-
public bwd0(chunk: undefined | Chunk<T>, offset: number): CharIterator<T> {
160-
const txt = this.txt;
161-
const str = txt.str;
162-
return () => {
163-
if (!chunk || offset < 0) return;
164-
const offsetToReturn = offset;
165-
const chunkToReturn = chunk;
166-
offset--;
167-
if (offset < 0) {
168-
chunk = str.prev(chunk);
169-
while (chunk && chunk.del) chunk = str.prev(chunk);
170-
if (chunk) offset = chunk.span - 1;
171-
}
172-
return new ChunkSlice(chunkToReturn, offsetToReturn, 1);
173-
};
174-
}
175-
176-
public bwd1(id: ITimestampStruct, chunk?: Chunk<T>): CharIterator<T> {
177-
const str = this.txt.str;
178-
const startFromStrRoot = equal(id, str.id);
179-
if (startFromStrRoot) {
180-
chunk = str.last();
181-
while (chunk && chunk.del) chunk = str.prev(chunk);
182-
return this.bwd0(chunk, chunk ? chunk.span - 1 : 0);
183-
}
184-
let offset: number = 0;
185-
if (!chunk || !contains(chunk.id, chunk.span, id, 1)) {
186-
chunk = str.findById(id);
187-
if (!chunk) return () => undefined;
188-
offset = id.time - chunk.id.time;
189-
} else offset = id.time - chunk.id.time;
190-
if (!chunk.del) return this.bwd0(chunk, offset);
191-
while (chunk && chunk.del) chunk = str.prev(chunk);
192-
return this.bwd0(chunk, chunk ? chunk.span - 1 : 0);
138+
/**
139+
* Returns a backward iterator through visible text, one character at a time,
140+
* starting from a given {@link Point}.
141+
*
142+
* @param start The starting point.
143+
* @param chunk Chunk to start from.
144+
* @returns The previous visible character iterator.
145+
*/
146+
public bwd(start: Point<T>): CharIterator<T> {
147+
return this.walk(start, -1);
193148
}
194149

195150
/**
@@ -238,10 +193,7 @@ export class Editor<T = string> {
238193
predicate: CharPredicate<string> = isLetter,
239194
firstLetterFound: boolean = false,
240195
): Point<T> {
241-
const firstChar = point.rightChar();
242-
if (!firstChar) return point;
243-
const fwd = this.fwd1(firstChar.id(), firstChar.chunk);
244-
return this.skipWord(fwd, predicate, firstLetterFound) || point;
196+
return this.skipWord(this.fwd(point), predicate, firstLetterFound) || point;
245197
}
246198

247199
/**
@@ -260,9 +212,7 @@ export class Editor<T = string> {
260212
predicate: CharPredicate<string> = isLetter,
261213
firstLetterFound: boolean = false,
262214
): Point<T> {
263-
const firstChar = point.leftChar();
264-
if (!firstChar) return point;
265-
const bwd = this.bwd1(firstChar.id(), firstChar.chunk);
215+
const bwd = this.bwd(point);
266216
const endPoint = this.skipWord(bwd, predicate, firstLetterFound);
267217
if (endPoint) endPoint.anchor = Anchor.Before;
268218
return endPoint || point;

src/json-crdt-extensions/peritext/editor/__tests__/Editor-iterators.spec.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const setup = (
3131
describe('.fwd1()', () => {
3232
test('can use string root as initial point', () => {
3333
const {peritext, editor} = setup();
34-
const iterator = editor.fwd1(peritext.str.id);
34+
const iterator = editor.fwd(peritext.pointAbsStart());
3535
let str = '';
3636
while (1) {
3737
const res = iterator();
@@ -44,7 +44,7 @@ describe('.fwd1()', () => {
4444
test('can iterate through the entire string', () => {
4545
const {peritext, editor} = setup();
4646
const start = peritext.pointStart()!;
47-
const iterator = editor.fwd1(start.id);
47+
const iterator = editor.fwd(start);
4848
let str = '';
4949
while (1) {
5050
const res = iterator();
@@ -57,7 +57,7 @@ describe('.fwd1()', () => {
5757
test('can iterate through the entire string, starting from ABS start', () => {
5858
const {peritext, editor} = setup();
5959
const start = peritext.pointAbsStart()!;
60-
const iterator = editor.fwd1(start.id);
60+
const iterator = editor.fwd(start);
6161
let str = '';
6262
while (1) {
6363
const res = iterator();
@@ -70,7 +70,7 @@ describe('.fwd1()', () => {
7070
test('can iterate through the entire string, with initial chunk provided', () => {
7171
const {peritext, editor} = setup();
7272
const start = peritext.pointStart()!;
73-
const iterator = editor.fwd1(start.id, start.chunk());
73+
const iterator = editor.fwd(start);
7474
let str = '';
7575
while (1) {
7676
const res = iterator();
@@ -83,7 +83,7 @@ describe('.fwd1()', () => {
8383
test('can iterate starting in the middle of first chunk', () => {
8484
const {peritext, editor} = setup();
8585
const start = peritext.pointAt(2);
86-
const iterator = editor.fwd1(start.id);
86+
const iterator = editor.fwd(start);
8787
let str = '';
8888
while (1) {
8989
const res = iterator();
@@ -96,7 +96,7 @@ describe('.fwd1()', () => {
9696
test('can iterate starting in the middle of first chunk, with initial chunk provided', () => {
9797
const {peritext, editor} = setup();
9898
const start = peritext.pointAt(2);
99-
const iterator = editor.fwd1(start.id, start.chunk());
99+
const iterator = editor.fwd(start);
100100
let str = '';
101101
while (1) {
102102
const res = iterator();
@@ -109,7 +109,7 @@ describe('.fwd1()', () => {
109109
test('can iterate starting in the middle of second chunk', () => {
110110
const {peritext, editor} = setup();
111111
const start = peritext.pointAt(6);
112-
const iterator = editor.fwd1(start.id);
112+
const iterator = editor.fwd(start);
113113
let str = '';
114114
while (1) {
115115
const res = iterator();
@@ -122,7 +122,7 @@ describe('.fwd1()', () => {
122122
test('can iterate starting in the middle of second chunk, with initial chunk provided', () => {
123123
const {peritext, editor} = setup();
124124
const start = peritext.pointAt(6);
125-
const iterator = editor.fwd1(start.id, start.chunk());
125+
const iterator = editor.fwd(start);
126126
let str = '';
127127
while (1) {
128128
const res = iterator();
@@ -140,7 +140,7 @@ describe('.fwd1()', () => {
140140
});
141141
peritext.overlay.refresh();
142142
const start = peritext.pointAt(0);
143-
const iterator = editor.fwd1(start.id, start.chunk());
143+
const iterator = editor.fwd(start);
144144
let str = '';
145145
const bools: boolean[] = [];
146146
while (1) {
@@ -157,7 +157,7 @@ describe('.fwd1()', () => {
157157
describe('.bwd1()', () => {
158158
test('can use string root as initial point', () => {
159159
const {peritext, editor} = setup();
160-
const iterator = editor.bwd1(peritext.str.id);
160+
const iterator = editor.bwd(peritext.pointAbsEnd());
161161
let str = '';
162162
while (1) {
163163
const res = iterator();
@@ -170,7 +170,7 @@ describe('.bwd1()', () => {
170170
test('can iterate through the entire string', () => {
171171
const {peritext, editor} = setup();
172172
const end = peritext.pointEnd()!;
173-
const iterator = editor.bwd1(end.id);
173+
const iterator = editor.bwd(end);
174174
let str = '';
175175
while (1) {
176176
const res = iterator();
@@ -183,7 +183,7 @@ describe('.bwd1()', () => {
183183
test('can iterate through the entire string, starting from ABS end', () => {
184184
const {peritext, editor} = setup();
185185
const end = peritext.pointAbsEnd()!;
186-
const iterator = editor.bwd1(end.id);
186+
const iterator = editor.bwd(end);
187187
let str = '';
188188
while (1) {
189189
const res = iterator();
@@ -196,7 +196,7 @@ describe('.bwd1()', () => {
196196
test('can iterate through the entire string, with initial chunk provided', () => {
197197
const {peritext, editor} = setup();
198198
const end = peritext.pointEnd()!;
199-
const iterator = editor.bwd1(end.id, end.chunk());
199+
const iterator = editor.bwd(end);
200200
let str = '';
201201
while (1) {
202202
const res = iterator();
@@ -208,54 +208,54 @@ describe('.bwd1()', () => {
208208

209209
test('can iterate starting in the middle of first chunk', () => {
210210
const {peritext, editor} = setup();
211-
const end = peritext.pointAt(2);
212-
const iterator = editor.bwd1(end.id);
211+
const point = peritext.pointAt(2);
212+
const iterator = editor.bwd(point);
213213
let str = '';
214214
while (1) {
215215
const res = iterator();
216216
if (!res) break;
217217
str += res.view();
218218
}
219-
expect(str).toBe('210');
219+
expect(str).toBe('10');
220220
});
221221

222222
test('can iterate starting in the middle of first chunk, with initial chunk provided', () => {
223223
const {peritext, editor} = setup();
224-
const end = peritext.pointAt(2);
225-
const iterator = editor.bwd1(end.id, end.chunk());
224+
const point = peritext.pointAt(2);
225+
const iterator = editor.bwd(point);
226226
let str = '';
227227
while (1) {
228228
const res = iterator();
229229
if (!res) break;
230230
str += res.view();
231231
}
232-
expect(str).toBe('210');
232+
expect(str).toBe('10');
233233
});
234234

235235
test('can iterate starting in the middle of second chunk', () => {
236236
const {peritext, editor} = setup();
237-
const end = peritext.pointAt(6);
238-
const iterator = editor.bwd1(end.id);
237+
const point = peritext.pointAt(6);
238+
const iterator = editor.bwd(point);
239239
let str = '';
240240
while (1) {
241241
const res = iterator();
242242
if (!res) break;
243243
str += res.view();
244244
}
245-
expect(str).toBe('6543210');
245+
expect(str).toBe('543210');
246246
});
247247

248248
test('can iterate starting in the middle of second chunk, with initial chunk provided', () => {
249249
const {peritext, editor} = setup();
250-
const end = peritext.pointAt(6);
251-
const iterator = editor.bwd1(end.id, end.chunk());
250+
const point = peritext.pointAt(6);
251+
const iterator = editor.bwd(point);
252252
let str = '';
253253
while (1) {
254254
const res = iterator();
255255
if (!res) break;
256256
str += res.view();
257257
}
258-
expect(str).toBe('6543210');
258+
expect(str).toBe('543210');
259259
});
260260

261261
test('returns true for block split chars', () => {
@@ -266,7 +266,7 @@ describe('.bwd1()', () => {
266266
});
267267
peritext.overlay.refresh();
268268
const start = peritext.pointAt(3);
269-
const iterator = editor.bwd1(start.id, start.chunk());
269+
const iterator = editor.bwd(start);
270270
let str = '';
271271
const bools: boolean[] = [];
272272
while (1) {

0 commit comments

Comments
 (0)