From 4cff1b7aff55a638db7195e9a04929325a9d5e4e Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 11 Oct 2024 00:02:44 +0200 Subject: [PATCH 1/6] =?UTF-8?q?feat(json-crdt-extensions):=20=F0=9F=8E=B8?= =?UTF-8?q?=20implement=20semantic=20forward=20movement=20in=20the=20edito?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/editor/Editor.ts | 104 ++++++++++++++++++ .../peritext/editor/types.ts | 5 + .../peritext/editor/util.ts | 8 ++ 3 files changed, 117 insertions(+) create mode 100644 src/json-crdt-extensions/peritext/editor/types.ts create mode 100644 src/json-crdt-extensions/peritext/editor/util.ts diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts index 96afe78238..47b84b35a7 100644 --- a/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -3,10 +3,17 @@ import {CursorAnchor, SliceBehavior} from '../slice/constants'; import {PersistedSlice} from '../slice/PersistedSlice'; import {EditorSlices} from './EditorSlices'; import {Chars} from '../constants'; +import {ChunkSlice} from '../util/ChunkSlice'; +import {contains, equal} from '../../../json-crdt-patch/clock'; +import {isLetter} from './util'; +import {Anchor} from '../rga/constants'; import type {ITimestampStruct} from '../../../json-crdt-patch/clock'; import type {Peritext} from '../Peritext'; import type {SliceType} from '../slice/types'; import type {MarkerSlice} from '../slice/MarkerSlice'; +import type {Chunk} from '../../../json-crdt/nodes/rga'; +import type {CharIterator, CharPredicate} from './types'; +import type {Point} from '../rga/Point'; export class Editor { public readonly saved: EditorSlices; @@ -96,6 +103,103 @@ export class Editor { return true; } + /** + * Returns a forward iterator through visible text, one character at a time, + * starting from a given chunk and offset. + * + * @param chunk Chunk to start from. + * @param offset Offset in the chunk to start from. + * @returns The next visible character iterator. + */ + public fwd0(chunk: undefined | Chunk, offset: number): CharIterator { + const str = this.txt.str; + return () => { + if (!chunk) return; + const offsetToReturn = offset; + const chunkToReturn = chunk; + const span = chunk.span; + 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(chunkToReturn, offsetToReturn, 1); + }; + } + + /** + * Returns a forward iterator through visible text, one character at a time, + * starting from a given ID. + * + * @param id ID to start from. + * @param chunk Chunk to start from. + * @returns The next visible character iterator. + */ + public fwd1(id: ITimestampStruct, chunk?: Chunk): CharIterator { + 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); + } + + /** + * Skips a word in an arbitrary direction. A word is defined by the `predicate` + * function, which returns `true` if the character is part of the word. + * + * @param iterator Character iterator. + * @param predicate Predicate function to match characters, returns `true` if + * the character is part of the word. + * @param firstLetterFound Whether the first letter has already been found. If + * not, will skip any characters until the first letter, which is matched + * by the `predicate` is found. + * @returns Point after the last character skipped. + */ + private skipWord(iterator: CharIterator, predicate: CharPredicate, firstLetterFound: boolean): Point | undefined { + let next: ChunkSlice | undefined; + let prev: ChunkSlice | undefined; + while ((next = iterator())) { + const char = (next.view() as string)[0]; + if (firstLetterFound) { + if (!predicate(char)) break; + } else if (predicate(char)) firstLetterFound = true; + prev = next; + } + if (!prev) return; + return this.txt.point(prev.id(), Anchor.After); + } + + /** + * Skips a word forward. A word is defined by the `predicate` function, which + * returns `true` if the character is part of the word. + * + * @param point Point from which to start skipping. + * @param predicate Character class to skip. + * @param firstLetterFound Whether the first letter has already been found. If + * not, will skip any characters until the first letter, which is + * matched by the `predicate` is found. + * @returns Point after the last character skipped. + */ + public fwdSkipWord(point: Point, predicate: CharPredicate = isLetter, firstLetterFound: boolean = false): Point { + const firstChar = point.rightChar(); + if (!firstChar) return point; + const fwd = this.fwd1(firstChar.id(), firstChar.chunk); + return this.skipWord(fwd, predicate, firstLetterFound) || point; + } + /** @deprecated use `.saved.insStack` */ public insStackSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { const range = this.cursor.range(); diff --git a/src/json-crdt-extensions/peritext/editor/types.ts b/src/json-crdt-extensions/peritext/editor/types.ts new file mode 100644 index 0000000000..02c6ae8077 --- /dev/null +++ b/src/json-crdt-extensions/peritext/editor/types.ts @@ -0,0 +1,5 @@ +import type {UndefIterator} from "../../../util/iterator"; +import type {ChunkSlice} from "../util/ChunkSlice"; + +export type CharIterator = UndefIterator>; +export type CharPredicate = (char: T) => boolean; diff --git a/src/json-crdt-extensions/peritext/editor/util.ts b/src/json-crdt-extensions/peritext/editor/util.ts new file mode 100644 index 0000000000..ab7eb49ae5 --- /dev/null +++ b/src/json-crdt-extensions/peritext/editor/util.ts @@ -0,0 +1,8 @@ +import {CharPredicate} from "./types"; + +const LETTER_REGEX = /(\p{Letter}|\d)/u; +const WHITESPACE_REGEX = /\s/; + +export const isLetter: CharPredicate = (char: string) => LETTER_REGEX.test(char[0]); +export const isWhitespace: CharPredicate = (char: string) => WHITESPACE_REGEX.test(char[0]); +export const isPunctuation: CharPredicate = (char: string) => !isLetter(char) && !isWhitespace(char); From 9c2608a0980d4204255b5d575b4c7cda0acefdae Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 11 Oct 2024 00:21:01 +0200 Subject: [PATCH 2/6] =?UTF-8?q?test(json-crdt-extensions):=20=F0=9F=92=8D?= =?UTF-8?q?=20add=20forward=20iteration=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editor/__tests__/Editor-iterators.spec.ts | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/json-crdt-extensions/peritext/editor/__tests__/Editor-iterators.spec.ts diff --git a/src/json-crdt-extensions/peritext/editor/__tests__/Editor-iterators.spec.ts b/src/json-crdt-extensions/peritext/editor/__tests__/Editor-iterators.spec.ts new file mode 100644 index 0000000000..4c8906efed --- /dev/null +++ b/src/json-crdt-extensions/peritext/editor/__tests__/Editor-iterators.spec.ts @@ -0,0 +1,155 @@ +import {Model} from '../../../../json-crdt/model'; +import {Peritext} from '../../Peritext'; +import {Editor} from '../Editor'; + +const setup = ( + insertText = (editor: Editor) => { + editor.insert('abcd'); + editor.cursor.setAt(2); + editor.insert('0123'); + editor.cursor.setAt(7); + editor.insert('4567'); + editor.cursor.setAt(0, 2); + editor.delBwd(); + editor.cursor.setAt(9, 1); + editor.delBwd(); + editor.cursor.setAt(4, 1); + editor.delBwd(); + }, +) => { + const model = Model.create(void 0, 858549494849333); + model.api.root({ + text: '', + slices: [], + }); + const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node); + const editor = peritext.editor; + insertText(editor); + return {model, peritext, editor}; +}; + +describe('.fwd1()', () => { + test('can use string root as initial point', () => { + const {peritext, editor} = setup(); + const iterator = editor.fwd1(peritext.str.id); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('01234567'); + }); + + test('can iterate through the entire string', () => { + const {peritext, editor} = setup(); + const start = peritext.pointStart()!; + const iterator = editor.fwd1(start.id); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('01234567'); + }); + + 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); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('01234567'); + }); + + 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()); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('01234567'); + }); + + 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); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('234567'); + }); + + 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()); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('234567'); + }); + + 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); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('67'); + }); + + 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()); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('67'); + }); + + test('returns true for block split chars', () => { + const {peritext, editor} = setup((editor) => { + editor.insert('ab'); + editor.cursor.setAt(1); + editor.saved.insMarker('p'); + }); + peritext.overlay.refresh(); + const start = peritext.pointAt(0); + const iterator = editor.fwd1(start.id, start.chunk()); + let str = ''; + const bools: boolean[] = []; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + bools.push(peritext.overlay.isMarker(res.id())); + } + expect(str).toBe('a\nb'); + expect(bools).toStrictEqual([false, true, false]); + }); +}); From 0358fdd1f9d0425265279560ea90bda0df656896 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 11 Oct 2024 00:25:02 +0200 Subject: [PATCH 3/6] =?UTF-8?q?feat(json-crdt-extensions):=20=F0=9F=8E=B8?= =?UTF-8?q?=20add=20editor=20backward=20iteration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/editor/Editor.ts | 58 ++++++++ .../editor/__tests__/Editor-iterators.spec.ts | 127 ++++++++++++++++++ 2 files changed, 185 insertions(+) diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts index 47b84b35a7..bb552867bd 100644 --- a/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -156,6 +156,44 @@ export class Editor { return this.fwd0(chunk, 0); } + public bwd0(chunk: undefined | Chunk, offset: number): CharIterator { + const txt = this.txt; + const str = txt.str; + return () => { + if (!chunk || offset < 0) return; + const offsetToReturn = offset; + const chunkToReturn = chunk; + const char = chunkToReturn.view().slice(offsetToReturn, offsetToReturn + 1); + if (!char) return; + 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): CharIterator { + 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); + } + /** * Skips a word in an arbitrary direction. A word is defined by the `predicate` * function, which returns `true` if the character is part of the word. @@ -200,6 +238,26 @@ export class Editor { return this.skipWord(fwd, predicate, firstLetterFound) || point; } + /** + * Skips a word backward. A word is defined by the `predicate` function, which + * returns `true` if the character is part of the word. + * + * @param point Point from which to start skipping. + * @param predicate Character class to skip. + * @param firstLetterFound Whether the first letter has already been found. If + * not, will skip any characters until the first letter, which is + * matched by the `predicate` is found. + * @returns Point after the last character skipped. + */ + public bwdSkipWord(point: Point, predicate: CharPredicate = isLetter, firstLetterFound: boolean = false): Point { + const firstChar = point.leftChar(); + if (!firstChar) return point; + const bwd = this.bwd1(firstChar.id(), firstChar.chunk); + const endPoint = this.skipWord(bwd, predicate, firstLetterFound); + if (endPoint) endPoint.anchor = Anchor.Before; + return endPoint || point; + } + /** @deprecated use `.saved.insStack` */ public insStackSlice(type: SliceType, data?: unknown | ITimestampStruct): PersistedSlice { const range = this.cursor.range(); diff --git a/src/json-crdt-extensions/peritext/editor/__tests__/Editor-iterators.spec.ts b/src/json-crdt-extensions/peritext/editor/__tests__/Editor-iterators.spec.ts index 4c8906efed..deb155b18f 100644 --- a/src/json-crdt-extensions/peritext/editor/__tests__/Editor-iterators.spec.ts +++ b/src/json-crdt-extensions/peritext/editor/__tests__/Editor-iterators.spec.ts @@ -153,3 +153,130 @@ describe('.fwd1()', () => { expect(bools).toStrictEqual([false, true, false]); }); }); + +describe('.bwd1()', () => { + test('can use string root as initial point', () => { + const {peritext, editor} = setup(); + const iterator = editor.bwd1(peritext.str.id); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('76543210'); + }); + + test('can iterate through the entire string', () => { + const {peritext, editor} = setup(); + const end = peritext.pointEnd()!; + const iterator = editor.bwd1(end.id); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('76543210'); + }); + + 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); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('76543210'); + }); + + 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()); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('76543210'); + }); + + 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); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('210'); + }); + + 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()); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('210'); + }); + + 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); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('6543210'); + }); + + 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()); + let str = ''; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + } + expect(str).toBe('6543210'); + }); + + test('returns true for block split chars', () => { + const {peritext, editor} = setup((editor) => { + editor.insert('ab'); + editor.cursor.setAt(1); + editor.saved.insMarker('p'); + }); + peritext.overlay.refresh(); + const start = peritext.pointAt(3); + const iterator = editor.bwd1(start.id, start.chunk()); + let str = ''; + const bools: boolean[] = []; + while (1) { + const res = iterator(); + if (!res) break; + str += res.view(); + bools.push(peritext.overlay.isMarker(res.id())); + } + expect(str).toBe('b\na'); + expect(bools).toStrictEqual([false, true, false]); + }); +}); + From 4f8c968257d8c959625eda45003004d8fc47ae67 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 11 Oct 2024 00:27:15 +0200 Subject: [PATCH 4/6] =?UTF-8?q?perf(json-crdt-extensions):=20=E2=9A=A1?= =?UTF-8?q?=EF=B8=8F=20improve=20backward=20iteration=20end=20condition=20?= =?UTF-8?q?check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/editor/Editor.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts index bb552867bd..535d95a345 100644 --- a/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -115,9 +115,9 @@ export class Editor { const str = this.txt.str; return () => { if (!chunk) return; + const span = chunk.span; const offsetToReturn = offset; const chunkToReturn = chunk; - const span = chunk.span; if (offset >= span) return; offset++; if (offset >= span) { @@ -163,8 +163,6 @@ export class Editor { if (!chunk || offset < 0) return; const offsetToReturn = offset; const chunkToReturn = chunk; - const char = chunkToReturn.view().slice(offsetToReturn, offsetToReturn + 1); - if (!char) return; offset--; if (offset < 0) { chunk = str.prev(chunk); From 95f519dfcaeb1188885373e710dbad0255977e57 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 11 Oct 2024 00:29:30 +0200 Subject: [PATCH 5/6] =?UTF-8?q?test(json-crdt-extensions):=20=F0=9F=92=8D?= =?UTF-8?q?=20implement=20word=20skipping=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editor/__tests__/Editor-cursor.spec.ts | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/json-crdt-extensions/peritext/editor/__tests__/Editor-cursor.spec.ts diff --git a/src/json-crdt-extensions/peritext/editor/__tests__/Editor-cursor.spec.ts b/src/json-crdt-extensions/peritext/editor/__tests__/Editor-cursor.spec.ts new file mode 100644 index 0000000000..d5df6ffde9 --- /dev/null +++ b/src/json-crdt-extensions/peritext/editor/__tests__/Editor-cursor.spec.ts @@ -0,0 +1,109 @@ +import {Model} from '../../../../json-crdt/model'; +import {Peritext} from '../../Peritext'; +import {Point} from '../../rga/Point'; +import {Editor} from '../Editor'; + +const setup = (insert = (editor: Editor) => editor.insert('Hello world!'), sid?: number) => { + const model = Model.withLogicalClock(sid); + model.api.root({ + text: '', + slices: [], + }); + const peritext = new Peritext(model, model.api.str(['text']).node, model.api.arr(['slices']).node); + const editor = peritext.editor; + insert(editor); + return {model, peritext, editor}; +}; + +describe('.fwdSkipWord()', () => { + test('can go to the end of a word', () => { + const {editor} = setup((editor) => editor.insert('Hello world!')); + editor.cursor.setAt(0); + const point = editor.fwdSkipWord(editor.cursor.end); + editor.cursor.end.set(point!); + expect(editor.cursor.text()).toBe('Hello'); + }); + + test('can skip whitespace between words', () => { + const {editor} = setup((editor) => editor.insert('Hello world!')); + editor.cursor.setAt(5); + const point = editor.fwdSkipWord(editor.cursor.end); + editor.cursor.end.set(point!); + expect(editor.cursor.text()).toBe(' world'); + }); + + test('skipping stops before exclamation mark', () => { + const {editor} = setup((editor) => editor.insert('Hello world!')); + editor.cursor.setAt(6); + const point = editor.fwdSkipWord(editor.cursor.end); + editor.cursor.end.set(point!); + expect(editor.cursor.text()).toBe('world'); + }); + + test('can skip to the end of string', () => { + const {editor} = setup((editor) => editor.insert('Hello world!')); + editor.cursor.setAt(11); + const point = editor.fwdSkipWord(editor.cursor.end); + expect(point instanceof Point).toBe(true); + editor.cursor.end.set(point!); + expect(editor.cursor.text()).toBe('!'); + }); + + test('can skip various character classes', () => { + const {editor} = setup((editor) => + editor.insert("const {editor} = setup(editor => editor.insert('Hello world!'));"), + ); + editor.cursor.setAt(0); + const move = (): string => { + const point = editor.fwdSkipWord(editor.cursor.end); + if (point) editor.cursor.end.set(point); + return editor.cursor.text(); + }; + expect(move()).toBe('const'); + expect(move()).toBe('const {editor'); + expect(move()).toBe('const {editor} = setup'); + expect(move()).toBe('const {editor} = setup(editor'); + expect(move()).toBe('const {editor} = setup(editor => editor'); + expect(move()).toBe('const {editor} = setup(editor => editor.insert'); + expect(move()).toBe("const {editor} = setup(editor => editor.insert('Hello"); + expect(move()).toBe("const {editor} = setup(editor => editor.insert('Hello world"); + expect(move()).toBe("const {editor} = setup(editor => editor.insert('Hello world!'));"); + }); +}); + +describe('.bwdSkipWord()', () => { + test('can skip over simple text.', () => { + const {editor} = setup((editor) => editor.insert('Hello world!\nfoo bar baz')); + editor.cursor.setAt(editor.txt.str.length()); + const move = (): string => { + const point = editor.bwdSkipWord(editor.cursor.start); + if (point) editor.cursor.start.set(point); + return editor.cursor.text(); + }; + expect(move()).toBe('baz'); + expect(move()).toBe('bar baz'); + expect(move()).toBe('foo bar baz'); + expect(move()).toBe('world!\nfoo bar baz'); + expect(move()).toBe('Hello world!\nfoo bar baz'); + }); + + test('can skip various character classes', () => { + const {editor} = setup((editor) => + editor.insert("const {editor} = setup(editor => editor.insert('Hello world!'));"), + ); + editor.cursor.setAt(editor.txt.str.length()); + const move = (): string => { + const point = editor.bwdSkipWord(editor.cursor.start); + if (point) editor.cursor.start.set(point); + return editor.cursor.text(); + }; + expect(move()).toBe("world!'));"); + expect(move()).toBe("Hello world!'));"); + expect(move()).toBe("insert('Hello world!'));"); + expect(move()).toBe("editor.insert('Hello world!'));"); + expect(move()).toBe("editor => editor.insert('Hello world!'));"); + expect(move()).toBe("setup(editor => editor.insert('Hello world!'));"); + expect(move()).toBe("editor} = setup(editor => editor.insert('Hello world!'));"); + expect(move()).toBe("const {editor} = setup(editor => editor.insert('Hello world!'));"); + }); +}); From 05af3eb19cf358f4f41f00242c3575eb50f4d29c Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 11 Oct 2024 00:30:05 +0200 Subject: [PATCH 6/6] =?UTF-8?q?style(json-crdt-extensions):=20=F0=9F=92=84?= =?UTF-8?q?=20run=20Prettier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/editor/Editor.ts | 18 +++++++++++++++--- .../editor/__tests__/Editor-iterators.spec.ts | 1 - .../peritext/editor/types.ts | 4 ++-- .../peritext/editor/util.ts | 2 +- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/json-crdt-extensions/peritext/editor/Editor.ts b/src/json-crdt-extensions/peritext/editor/Editor.ts index 535d95a345..b0f4f84ebc 100644 --- a/src/json-crdt-extensions/peritext/editor/Editor.ts +++ b/src/json-crdt-extensions/peritext/editor/Editor.ts @@ -204,7 +204,11 @@ export class Editor { * by the `predicate` is found. * @returns Point after the last character skipped. */ - private skipWord(iterator: CharIterator, predicate: CharPredicate, firstLetterFound: boolean): Point | undefined { + private skipWord( + iterator: CharIterator, + predicate: CharPredicate, + firstLetterFound: boolean, + ): Point | undefined { let next: ChunkSlice | undefined; let prev: ChunkSlice | undefined; while ((next = iterator())) { @@ -229,7 +233,11 @@ export class Editor { * matched by the `predicate` is found. * @returns Point after the last character skipped. */ - public fwdSkipWord(point: Point, predicate: CharPredicate = isLetter, firstLetterFound: boolean = false): Point { + public fwdSkipWord( + point: Point, + predicate: CharPredicate = isLetter, + firstLetterFound: boolean = false, + ): Point { const firstChar = point.rightChar(); if (!firstChar) return point; const fwd = this.fwd1(firstChar.id(), firstChar.chunk); @@ -247,7 +255,11 @@ export class Editor { * matched by the `predicate` is found. * @returns Point after the last character skipped. */ - public bwdSkipWord(point: Point, predicate: CharPredicate = isLetter, firstLetterFound: boolean = false): Point { + public bwdSkipWord( + point: Point, + predicate: CharPredicate = isLetter, + firstLetterFound: boolean = false, + ): Point { const firstChar = point.leftChar(); if (!firstChar) return point; const bwd = this.bwd1(firstChar.id(), firstChar.chunk); diff --git a/src/json-crdt-extensions/peritext/editor/__tests__/Editor-iterators.spec.ts b/src/json-crdt-extensions/peritext/editor/__tests__/Editor-iterators.spec.ts index deb155b18f..bd953c0c6e 100644 --- a/src/json-crdt-extensions/peritext/editor/__tests__/Editor-iterators.spec.ts +++ b/src/json-crdt-extensions/peritext/editor/__tests__/Editor-iterators.spec.ts @@ -279,4 +279,3 @@ describe('.bwd1()', () => { expect(bools).toStrictEqual([false, true, false]); }); }); - diff --git a/src/json-crdt-extensions/peritext/editor/types.ts b/src/json-crdt-extensions/peritext/editor/types.ts index 02c6ae8077..891f2d18e0 100644 --- a/src/json-crdt-extensions/peritext/editor/types.ts +++ b/src/json-crdt-extensions/peritext/editor/types.ts @@ -1,5 +1,5 @@ -import type {UndefIterator} from "../../../util/iterator"; -import type {ChunkSlice} from "../util/ChunkSlice"; +import type {UndefIterator} from '../../../util/iterator'; +import type {ChunkSlice} from '../util/ChunkSlice'; export type CharIterator = UndefIterator>; export type CharPredicate = (char: T) => boolean; diff --git a/src/json-crdt-extensions/peritext/editor/util.ts b/src/json-crdt-extensions/peritext/editor/util.ts index ab7eb49ae5..8010ba888f 100644 --- a/src/json-crdt-extensions/peritext/editor/util.ts +++ b/src/json-crdt-extensions/peritext/editor/util.ts @@ -1,4 +1,4 @@ -import {CharPredicate} from "./types"; +import {CharPredicate} from './types'; const LETTER_REGEX = /(\p{Letter}|\d)/u; const WHITESPACE_REGEX = /\s/;