Skip to content

Commit

Permalink
Merge pull request #719 from streamich/perf-editor-iterators
Browse files Browse the repository at this point in the history
Improve and generalize character iteration in `Editor`
  • Loading branch information
streamich authored Oct 10, 2024
2 parents 83f6a5c + d85b8c0 commit d10aac9
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 117 deletions.
2 changes: 1 addition & 1 deletion src/json-crdt-extensions/peritext/Peritext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export class Peritext<T = string> implements Printable {
// TODO: Provide ability to attach to the end of the text?
const str = this.str;
const id = str.find(pos);
if (!id) return this.point(str.id, Anchor.After);
if (!id) return this.point(str.id, pos ? Anchor.Before : Anchor.After);
return this.point(id, anchor);
}

Expand Down
106 changes: 28 additions & 78 deletions src/json-crdt-extensions/peritext/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,92 +104,47 @@ export class Editor<T = string> {
}

/**
* Returns a forward iterator through visible text, one character at a time,
* starting from a given chunk and offset.
* Returns an iterator through visible text, one `step` characters at a time,
* starting from a given {@link Point}.
*
* @param chunk Chunk to start from.
* @param offset Offset in the chunk to start from.
* @param start The starting point.
* @param step Number of visible characters to skip.
* @returns The next visible character iterator.
*/
public fwd0(chunk: undefined | Chunk<T>, offset: number): CharIterator<T> {
const str = this.txt.str;
public walk(start: Point<T>, step: number = 1): CharIterator<T> {
let point: Point<T> | undefined = start.clone();
return () => {
if (!chunk) return;
const span = chunk.span;
const offsetToReturn = offset;
const chunkToReturn = chunk;
if (offset >= span) return;
offset++;
if (offset >= span) {
offset = 0;
chunk = str.next(chunk);
while (chunk && chunk.del) chunk = str.next(chunk);
}
return new ChunkSlice<T>(chunkToReturn, offsetToReturn, 1);
if (!point) return;
const char = step > 0 ? point.rightChar() : point.leftChar();
if (!char) return (point = undefined);
const end = point.move(step);
if (end) point = undefined;
return char;
};
}

/**
* Returns a forward iterator through visible text, one character at a time,
* starting from a given ID.
* starting from a given {@link Point}.
*
* @param id ID to start from.
* @param start The starting point.
* @param chunk Chunk to start from.
* @returns The next visible character iterator.
*/
public fwd1(id: ITimestampStruct, chunk?: Chunk<T>): CharIterator<T> {
const str = this.txt.str;
const startFromStrRoot = equal(id, str.id);
if (startFromStrRoot) {
chunk = str.first();
while (chunk && chunk.del) chunk = str.next(chunk);
return this.fwd0(chunk, 0);
}
let offset: number = 0;
if (!chunk || !contains(chunk.id, chunk.span, id, 1)) {
chunk = str.findById(id);
if (!chunk) return () => undefined;
offset = id.time - chunk.id.time;
} else offset = id.time - chunk.id.time;
if (!chunk.del) return this.fwd0(chunk, offset);
while (chunk && chunk.del) chunk = str.next(chunk);
return this.fwd0(chunk, 0);
public fwd(start: Point<T>): CharIterator<T> {
return this.walk(start, 1);
}

public bwd0(chunk: undefined | Chunk<T>, offset: number): CharIterator<T> {
const txt = this.txt;
const str = txt.str;
return () => {
if (!chunk || offset < 0) return;
const offsetToReturn = offset;
const chunkToReturn = chunk;
offset--;
if (offset < 0) {
chunk = str.prev(chunk);
while (chunk && chunk.del) chunk = str.prev(chunk);
if (chunk) offset = chunk.span - 1;
}
return new ChunkSlice(chunkToReturn, offsetToReturn, 1);
};
}

public bwd1(id: ITimestampStruct, chunk?: Chunk<T>): CharIterator<T> {
const str = this.txt.str;
const startFromStrRoot = equal(id, str.id);
if (startFromStrRoot) {
chunk = str.last();
while (chunk && chunk.del) chunk = str.prev(chunk);
return this.bwd0(chunk, chunk ? chunk.span - 1 : 0);
}
let offset: number = 0;
if (!chunk || !contains(chunk.id, chunk.span, id, 1)) {
chunk = str.findById(id);
if (!chunk) return () => undefined;
offset = id.time - chunk.id.time;
} else offset = id.time - chunk.id.time;
if (!chunk.del) return this.bwd0(chunk, offset);
while (chunk && chunk.del) chunk = str.prev(chunk);
return this.bwd0(chunk, chunk ? chunk.span - 1 : 0);
/**
* Returns a backward iterator through visible text, one character at a time,
* starting from a given {@link Point}.
*
* @param start The starting point.
* @param chunk Chunk to start from.
* @returns The previous visible character iterator.
*/
public bwd(start: Point<T>): CharIterator<T> {
return this.walk(start, -1);
}

/**
Expand Down Expand Up @@ -238,10 +193,7 @@ export class Editor<T = string> {
predicate: CharPredicate<string> = isLetter,
firstLetterFound: boolean = false,
): Point<T> {
const firstChar = point.rightChar();
if (!firstChar) return point;
const fwd = this.fwd1(firstChar.id(), firstChar.chunk);
return this.skipWord(fwd, predicate, firstLetterFound) || point;
return this.skipWord(this.fwd(point), predicate, firstLetterFound) || point;
}

/**
Expand All @@ -260,9 +212,7 @@ export class Editor<T = string> {
predicate: CharPredicate<string> = isLetter,
firstLetterFound: boolean = false,
): Point<T> {
const firstChar = point.leftChar();
if (!firstChar) return point;
const bwd = this.bwd1(firstChar.id(), firstChar.chunk);
const bwd = this.bwd(point);
const endPoint = this.skipWord(bwd, predicate, firstLetterFound);
if (endPoint) endPoint.anchor = Anchor.Before;
return endPoint || point;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const setup = (
describe('.fwd1()', () => {
test('can use string root as initial point', () => {
const {peritext, editor} = setup();
const iterator = editor.fwd1(peritext.str.id);
const iterator = editor.fwd(peritext.pointAbsStart());
let str = '';
while (1) {
const res = iterator();
Expand All @@ -44,7 +44,7 @@ describe('.fwd1()', () => {
test('can iterate through the entire string', () => {
const {peritext, editor} = setup();
const start = peritext.pointStart()!;
const iterator = editor.fwd1(start.id);
const iterator = editor.fwd(start);
let str = '';
while (1) {
const res = iterator();
Expand All @@ -57,7 +57,7 @@ describe('.fwd1()', () => {
test('can iterate through the entire string, starting from ABS start', () => {
const {peritext, editor} = setup();
const start = peritext.pointAbsStart()!;
const iterator = editor.fwd1(start.id);
const iterator = editor.fwd(start);
let str = '';
while (1) {
const res = iterator();
Expand All @@ -70,7 +70,7 @@ describe('.fwd1()', () => {
test('can iterate through the entire string, with initial chunk provided', () => {
const {peritext, editor} = setup();
const start = peritext.pointStart()!;
const iterator = editor.fwd1(start.id, start.chunk());
const iterator = editor.fwd(start);
let str = '';
while (1) {
const res = iterator();
Expand All @@ -83,7 +83,7 @@ describe('.fwd1()', () => {
test('can iterate starting in the middle of first chunk', () => {
const {peritext, editor} = setup();
const start = peritext.pointAt(2);
const iterator = editor.fwd1(start.id);
const iterator = editor.fwd(start);
let str = '';
while (1) {
const res = iterator();
Expand All @@ -96,7 +96,7 @@ describe('.fwd1()', () => {
test('can iterate starting in the middle of first chunk, with initial chunk provided', () => {
const {peritext, editor} = setup();
const start = peritext.pointAt(2);
const iterator = editor.fwd1(start.id, start.chunk());
const iterator = editor.fwd(start);
let str = '';
while (1) {
const res = iterator();
Expand All @@ -109,7 +109,7 @@ describe('.fwd1()', () => {
test('can iterate starting in the middle of second chunk', () => {
const {peritext, editor} = setup();
const start = peritext.pointAt(6);
const iterator = editor.fwd1(start.id);
const iterator = editor.fwd(start);
let str = '';
while (1) {
const res = iterator();
Expand All @@ -122,7 +122,7 @@ describe('.fwd1()', () => {
test('can iterate starting in the middle of second chunk, with initial chunk provided', () => {
const {peritext, editor} = setup();
const start = peritext.pointAt(6);
const iterator = editor.fwd1(start.id, start.chunk());
const iterator = editor.fwd(start);
let str = '';
while (1) {
const res = iterator();
Expand All @@ -140,7 +140,7 @@ describe('.fwd1()', () => {
});
peritext.overlay.refresh();
const start = peritext.pointAt(0);
const iterator = editor.fwd1(start.id, start.chunk());
const iterator = editor.fwd(start);
let str = '';
const bools: boolean[] = [];
while (1) {
Expand All @@ -157,7 +157,7 @@ describe('.fwd1()', () => {
describe('.bwd1()', () => {
test('can use string root as initial point', () => {
const {peritext, editor} = setup();
const iterator = editor.bwd1(peritext.str.id);
const iterator = editor.bwd(peritext.pointAbsEnd());
let str = '';
while (1) {
const res = iterator();
Expand All @@ -170,7 +170,7 @@ describe('.bwd1()', () => {
test('can iterate through the entire string', () => {
const {peritext, editor} = setup();
const end = peritext.pointEnd()!;
const iterator = editor.bwd1(end.id);
const iterator = editor.bwd(end);
let str = '';
while (1) {
const res = iterator();
Expand All @@ -183,7 +183,7 @@ describe('.bwd1()', () => {
test('can iterate through the entire string, starting from ABS end', () => {
const {peritext, editor} = setup();
const end = peritext.pointAbsEnd()!;
const iterator = editor.bwd1(end.id);
const iterator = editor.bwd(end);
let str = '';
while (1) {
const res = iterator();
Expand All @@ -196,7 +196,7 @@ describe('.bwd1()', () => {
test('can iterate through the entire string, with initial chunk provided', () => {
const {peritext, editor} = setup();
const end = peritext.pointEnd()!;
const iterator = editor.bwd1(end.id, end.chunk());
const iterator = editor.bwd(end);
let str = '';
while (1) {
const res = iterator();
Expand All @@ -208,54 +208,54 @@ describe('.bwd1()', () => {

test('can iterate starting in the middle of first chunk', () => {
const {peritext, editor} = setup();
const end = peritext.pointAt(2);
const iterator = editor.bwd1(end.id);
const point = peritext.pointAt(2);
const iterator = editor.bwd(point);
let str = '';
while (1) {
const res = iterator();
if (!res) break;
str += res.view();
}
expect(str).toBe('210');
expect(str).toBe('10');
});

test('can iterate starting in the middle of first chunk, with initial chunk provided', () => {
const {peritext, editor} = setup();
const end = peritext.pointAt(2);
const iterator = editor.bwd1(end.id, end.chunk());
const point = peritext.pointAt(2);
const iterator = editor.bwd(point);
let str = '';
while (1) {
const res = iterator();
if (!res) break;
str += res.view();
}
expect(str).toBe('210');
expect(str).toBe('10');
});

test('can iterate starting in the middle of second chunk', () => {
const {peritext, editor} = setup();
const end = peritext.pointAt(6);
const iterator = editor.bwd1(end.id);
const point = peritext.pointAt(6);
const iterator = editor.bwd(point);
let str = '';
while (1) {
const res = iterator();
if (!res) break;
str += res.view();
}
expect(str).toBe('6543210');
expect(str).toBe('543210');
});

test('can iterate starting in the middle of second chunk, with initial chunk provided', () => {
const {peritext, editor} = setup();
const end = peritext.pointAt(6);
const iterator = editor.bwd1(end.id, end.chunk());
const point = peritext.pointAt(6);
const iterator = editor.bwd(point);
let str = '';
while (1) {
const res = iterator();
if (!res) break;
str += res.view();
}
expect(str).toBe('6543210');
expect(str).toBe('543210');
});

test('returns true for block split chars', () => {
Expand All @@ -266,7 +266,7 @@ describe('.bwd1()', () => {
});
peritext.overlay.refresh();
const start = peritext.pointAt(3);
const iterator = editor.bwd1(start.id, start.chunk());
const iterator = editor.bwd(start);
let str = '';
const bools: boolean[] = [];
while (1) {
Expand Down
Loading

0 comments on commit d10aac9

Please sign in to comment.