@@ -3,10 +3,17 @@ import {CursorAnchor, SliceBehavior} from '../slice/constants';
3
3
import { PersistedSlice } from '../slice/PersistedSlice' ;
4
4
import { EditorSlices } from './EditorSlices' ;
5
5
import { 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' ;
6
10
import type { ITimestampStruct } from '../../../json-crdt-patch/clock' ;
7
11
import type { Peritext } from '../Peritext' ;
8
12
import type { SliceType } from '../slice/types' ;
9
13
import 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' ;
10
17
11
18
export class Editor < T = string > {
12
19
public readonly saved : EditorSlices < T > ;
@@ -96,6 +103,171 @@ export class Editor<T = string> {
96
103
return true ;
97
104
}
98
105
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
+
99
271
/** @deprecated use `.saved.insStack` */
100
272
public insStackSlice ( type : SliceType , data ?: unknown | ITimestampStruct ) : PersistedSlice < T > {
101
273
const range = this . cursor . range ( ) ;
0 commit comments