@@ -3,10 +3,17 @@ import {CursorAnchor, SliceBehavior} from '../slice/constants';
33import { PersistedSlice } from '../slice/PersistedSlice' ;
44import { EditorSlices } from './EditorSlices' ;
55import { Chars } from '../constants' ;
6+ import { ChunkSlice } from '../util/ChunkSlice' ;
7+ import { contains , equal } from '../../../json-crdt-patch/clock' ;
8+ import { isLetter } from './util' ;
9+ import { Anchor } from '../rga/constants' ;
610import type { ITimestampStruct } from '../../../json-crdt-patch/clock' ;
711import type { Peritext } from '../Peritext' ;
812import type { SliceType } from '../slice/types' ;
913import type { MarkerSlice } from '../slice/MarkerSlice' ;
14+ import type { Chunk } from '../../../json-crdt/nodes/rga' ;
15+ import type { CharIterator , CharPredicate } from './types' ;
16+ import type { Point } from '../rga/Point' ;
1017
1118export class Editor < T = string > {
1219 public readonly saved : EditorSlices < T > ;
@@ -96,6 +103,171 @@ export class Editor<T = string> {
96103 return true ;
97104 }
98105
106+ /**
107+ * Returns a forward iterator through visible text, one character at a time,
108+ * starting from a given chunk and offset.
109+ *
110+ * @param chunk Chunk to start from.
111+ * @param offset Offset in the chunk to start from.
112+ * @returns The next visible character iterator.
113+ */
114+ public fwd0 ( chunk : undefined | Chunk < T > , offset : number ) : CharIterator < T > {
115+ const str = this . txt . str ;
116+ 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 ) ;
129+ } ;
130+ }
131+
132+ /**
133+ * Returns a forward iterator through visible text, one character at a time,
134+ * starting from a given ID.
135+ *
136+ * @param id ID to start from.
137+ * @param chunk Chunk to start from.
138+ * @returns The next visible character iterator.
139+ */
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 ) ;
157+ }
158+
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 ) ;
193+ }
194+
195+ /**
196+ * Skips a word in an arbitrary direction. A word is defined by the `predicate`
197+ * function, which returns `true` if the character is part of the word.
198+ *
199+ * @param iterator Character iterator.
200+ * @param predicate Predicate function to match characters, returns `true` if
201+ * the character is part of the word.
202+ * @param firstLetterFound Whether the first letter has already been found. If
203+ * not, will skip any characters until the first letter, which is matched
204+ * by the `predicate` is found.
205+ * @returns Point after the last character skipped.
206+ */
207+ private skipWord (
208+ iterator : CharIterator < T > ,
209+ predicate : CharPredicate < string > ,
210+ firstLetterFound : boolean ,
211+ ) : Point < T > | undefined {
212+ let next : ChunkSlice < T > | undefined ;
213+ let prev : ChunkSlice < T > | undefined ;
214+ while ( ( next = iterator ( ) ) ) {
215+ const char = ( next . view ( ) as string ) [ 0 ] ;
216+ if ( firstLetterFound ) {
217+ if ( ! predicate ( char ) ) break ;
218+ } else if ( predicate ( char ) ) firstLetterFound = true ;
219+ prev = next ;
220+ }
221+ if ( ! prev ) return ;
222+ return this . txt . point ( prev . id ( ) , Anchor . After ) ;
223+ }
224+
225+ /**
226+ * Skips a word forward. A word is defined by the `predicate` function, which
227+ * returns `true` if the character is part of the word.
228+ *
229+ * @param point Point from which to start skipping.
230+ * @param predicate Character class to skip.
231+ * @param firstLetterFound Whether the first letter has already been found. If
232+ * not, will skip any characters until the first letter, which is
233+ * matched by the `predicate` is found.
234+ * @returns Point after the last character skipped.
235+ */
236+ public fwdSkipWord (
237+ point : Point < T > ,
238+ predicate : CharPredicate < string > = isLetter ,
239+ firstLetterFound : boolean = false ,
240+ ) : 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 ;
245+ }
246+
247+ /**
248+ * Skips a word backward. A word is defined by the `predicate` function, which
249+ * returns `true` if the character is part of the word.
250+ *
251+ * @param point Point from which to start skipping.
252+ * @param predicate Character class to skip.
253+ * @param firstLetterFound Whether the first letter has already been found. If
254+ * not, will skip any characters until the first letter, which is
255+ * matched by the `predicate` is found.
256+ * @returns Point after the last character skipped.
257+ */
258+ public bwdSkipWord (
259+ point : Point < T > ,
260+ predicate : CharPredicate < string > = isLetter ,
261+ firstLetterFound : boolean = false ,
262+ ) : Point < T > {
263+ const firstChar = point . leftChar ( ) ;
264+ if ( ! firstChar ) return point ;
265+ const bwd = this . bwd1 ( firstChar . id ( ) , firstChar . chunk ) ;
266+ const endPoint = this . skipWord ( bwd , predicate , firstLetterFound ) ;
267+ if ( endPoint ) endPoint . anchor = Anchor . Before ;
268+ return endPoint || point ;
269+ }
270+
99271 /** @deprecated use `.saved.insStack` */
100272 public insStackSlice ( type : SliceType , data ?: unknown | ITimestampStruct ) : PersistedSlice < T > {
101273 const range = this . cursor . range ( ) ;
0 commit comments