1 /*
<lambda>null2 * Copyright 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.foundation.text.input
18
19 import androidx.compose.foundation.ExperimentalFoundationApi
20 import androidx.compose.foundation.internal.requirePrecondition
21 import androidx.compose.foundation.text.input.TextFieldBuffer.ChangeList
22 import androidx.compose.foundation.text.input.internal.ChangeTracker
23 import androidx.compose.foundation.text.input.internal.OffsetMappingCalculator
24 import androidx.compose.foundation.text.input.internal.PartialGapBuffer
25 import androidx.compose.runtime.collection.MutableVector
26 import androidx.compose.runtime.collection.mutableVectorOf
27 import androidx.compose.ui.text.AnnotatedString
28 import androidx.compose.ui.text.TextRange
29 import androidx.compose.ui.text.coerceIn
30 import androidx.compose.ui.util.fastForEach
31 import kotlin.jvm.JvmName
32
33 /**
34 * A text buffer that can be edited, similar to [StringBuilder].
35 *
36 * This class provides methods for changing the text, such as:
37 * - [replace]
38 * - [append]
39 * - [insert]
40 * - [delete]
41 *
42 * This class also stores and tracks the cursor position or selection range. The cursor position is
43 * just a selection range with zero length. The cursor and selection can be changed using methods
44 * such as:
45 * - [placeCursorAfterCharAt]
46 * - [placeCursorBeforeCharAt]
47 * - [placeCursorAtEnd]
48 * - [selectAll]
49 *
50 * To get one of these, and for usage samples, see [TextFieldState.edit]. Every change to the buffer
51 * is tracked in a [ChangeList] which you can access via the [changes] property.
52 */
53 class TextFieldBuffer
54 internal constructor(
55 initialValue: TextFieldCharSequence,
56 initialChanges: ChangeTracker? = null,
57 internal val originalValue: TextFieldCharSequence = initialValue,
58 private val offsetMappingCalculator: OffsetMappingCalculator? = null,
59 ) : Appendable {
60
61 private val buffer = PartialGapBuffer(initialValue)
62
63 private var backingChangeTracker: ChangeTracker? =
64 initialChanges?.let { ChangeTracker(initialChanges) }
65
66 /** Lazily-allocated [ChangeTracker], initialized on the first access. */
67 internal val changeTracker: ChangeTracker
68 get() = backingChangeTracker ?: ChangeTracker().also { backingChangeTracker = it }
69
70 /** The number of characters in the text field. */
71 val length: Int
72 get() = buffer.length
73
74 /**
75 * Original text content of the buffer before any changes were applied. Calling
76 * [revertAllChanges] will set the contents of this buffer to this value.
77 */
78 val originalText: CharSequence
79 get() = originalValue.text
80
81 /**
82 * Original selection before the changes. Calling [revertAllChanges] will set the selection to
83 * this value.
84 */
85 val originalSelection: TextRange
86 get() = originalValue.selection
87
88 /**
89 * The [ChangeList] represents the changes made to this value and is inherently mutable. This
90 * means that the returned [ChangeList] always reflects the complete list of changes made to
91 * this value at any given time, even those made after reading this property.
92 *
93 * @sample androidx.compose.foundation.samples.BasicTextFieldChangeIterationSample
94 * @sample androidx.compose.foundation.samples.BasicTextFieldChangeReverseIterationSample
95 */
96 @ExperimentalFoundationApi
97 val changes: ChangeList
98 get() = changeTracker
99
100 // region selection
101
102 /**
103 * True if the selection range has non-zero length. If this is false, then the selection
104 * represents the cursor.
105 *
106 * @see selection
107 */
108 @get:JvmName("hasSelection")
109 val hasSelection: Boolean
110 get() = !selection.collapsed
111
112 /**
113 * Backing TextRange for [selection]. Each method that updates selection has its own validation.
114 * This backing field does not further validate its own state.
115 */
116 private var selectionInChars: TextRange = initialValue.selection
117
118 /**
119 * The selected range of characters.
120 *
121 * Places the selection around the given range in characters.
122 *
123 * If the start or end of TextRange fall inside surrogate pairs or other invalid runs, the
124 * values will be adjusted to the nearest earlier and later characters, respectively.
125 *
126 * To place the start of the selection at the beginning of the field, set this value to
127 * [TextRange.Zero]. To place the end of the selection at the end of the field, after the last
128 * character, pass [TextFieldBuffer.length]. Passing a zero-length range is the same as calling
129 * [placeCursorBeforeCharAt].
130 */
131 var selection: TextRange
132 get() = selectionInChars
133 set(value) {
134 requireValidRange(value)
135 selectionInChars = value
136 highlight = null
137 }
138
139 // endregion
140
141 // region composition
142
143 /**
144 * Returns the composition information as TextRange. Returns null if no composition is set.
145 *
146 * Evaluates to null if it is set to a collapsed TextRange. Clears [composingAnnotations] when
147 * set to null, including collapsed TextRange.
148 */
149 internal var composition: TextRange? = initialValue.composition
150 private set(value) {
151 // collapsed composition region is equivalent to no composition
152 if (value == null || value.collapsed) {
153 field = null
154 // Do not deallocate an existing list. We will probably use it again.
155 composingAnnotations?.clear()
156 } else {
157 field = value
158 }
159 }
160
161 /**
162 * List of annotations that are attached to the composing region. These are usually styling cues
163 * like underline or different background colors.
164 */
165 internal var composingAnnotations:
166 MutableVector<AnnotatedString.Range<AnnotatedString.Annotation>>? =
167 if (!initialValue.composingAnnotations.isNullOrEmpty()) {
168 MutableVector(initialValue.composingAnnotations.size) {
169 initialValue.composingAnnotations[it]
170 }
171 } else {
172 null
173 }
174 private set
175
176 /** Helper function that returns true if the buffer has composing region */
177 internal fun hasComposition(): Boolean = composition != null
178
179 /** Clears current composition. */
180 internal fun commitComposition() {
181 composition = null
182 }
183
184 /**
185 * Mark the specified area of the text as composition text.
186 *
187 * The empty range or reversed range is not allowed. Use [commitComposition] in case if you want
188 * to clear composition.
189 *
190 * @param start the inclusive start offset of the composition
191 * @param end the exclusive end offset of the composition
192 * @param annotations Annotations that are attached to the composing region of text. This
193 * function does not check whether the given annotations are inside the composing region. It
194 * simply adds them to the current buffer while adjusting their range according to where the
195 * new composition region is set.
196 * @throws IndexOutOfBoundsException if start or end offset is outside of current buffer
197 * @throws IllegalArgumentException if start is larger than or equal to end. (reversed or
198 * collapsed range)
199 */
200 internal fun setComposition(start: Int, end: Int, annotations: List<PlacedAnnotation>? = null) {
201 if (start < 0 || start > buffer.length) {
202 throw IndexOutOfBoundsException(
203 "start ($start) offset is outside of text region ${buffer.length}"
204 )
205 }
206 if (end < 0 || end > buffer.length) {
207 throw IndexOutOfBoundsException(
208 "end ($end) offset is outside of text region ${buffer.length}"
209 )
210 }
211 if (start >= end) {
212 throw IllegalArgumentException("Do not set reversed or empty range: $start > $end")
213 }
214
215 composition = TextRange(start, end)
216
217 this.composingAnnotations?.clear()
218 if (!annotations.isNullOrEmpty()) {
219 if (this.composingAnnotations == null) {
220 this.composingAnnotations = mutableVectorOf()
221 }
222 annotations.fastForEach {
223 // place the annotations at the correct indices in the buffer.
224 this.composingAnnotations?.add(
225 it.copy(start = it.start + start, end = it.end + start)
226 )
227 }
228 }
229 }
230
231 // endregion
232
233 // region highlight
234
235 /**
236 * A highlighted range of text. This may be used to display handwriting gesture previews from
237 * the IME.
238 */
239 internal var highlight: Pair<TextHighlightType, TextRange>? = null
240 private set
241
242 /**
243 * Mark a range of text to be highlighted. This may be used to display handwriting gesture
244 * previews from the IME.
245 *
246 * An empty or reversed range is not allowed.
247 *
248 * @param type the highlight type
249 * @param start the inclusive start offset of the highlight
250 * @param end the exclusive end offset of the highlight
251 */
252 internal fun setHighlight(type: TextHighlightType, start: Int, end: Int) {
253 if (start >= end) {
254 throw IllegalArgumentException("Do not set reversed or empty range: $start > $end")
255 }
256 val clampedStart = start.coerceIn(0, length)
257 val clampedEnd = end.coerceIn(0, length)
258
259 highlight = Pair(type, TextRange(clampedStart, clampedEnd))
260 }
261
262 /** Clear the highlighted text range. */
263 internal fun clearHighlight() {
264 highlight = null
265 }
266
267 // endregion
268
269 // region editing
270
271 /**
272 * Replaces the text between [start] (inclusive) and [end] (exclusive) in this value with
273 * [text], and records the change in [changes].
274 *
275 * @param start The character offset of the first character to replace.
276 * @param end The character offset of the first character after the text to replace.
277 * @param text The text to replace the range `[start, end)` with.
278 * @see append
279 * @see insert
280 * @see delete
281 */
282 fun replace(start: Int, end: Int, text: CharSequence) {
283 replace(start, end, text, 0, text.length)
284 }
285
286 /**
287 * Replaces the text between [start] (inclusive) and [end] (exclusive) in this value with
288 * [text], and records the change in [changes].
289 *
290 * @param start The character offset of the first character to replace.
291 * @param end The character offset of the first character after the text to replace.
292 * @param text The text to replace the range `[start, end)` with.
293 * @param textStart The character offset of the first character in [text] to copy.
294 * @param textEnd The character offset after the last character in [text] to copy.
295 * @see append
296 * @see insert
297 * @see delete
298 */
299 internal fun replace(
300 start: Int,
301 end: Int,
302 text: CharSequence,
303 textStart: Int = 0,
304 textEnd: Int = text.length
305 ) {
306 requirePrecondition(start <= end) { "Expected start=$start <= end=$end" }
307 requirePrecondition(textStart <= textEnd) {
308 "Expected textStart=$textStart <= textEnd=$textEnd"
309 }
310 onTextWillChange(start, end, textEnd - textStart)
311 buffer.replace(start, end, text, textStart, textEnd)
312
313 commitComposition()
314 clearHighlight()
315 }
316
317 /**
318 * Similar to `replace(0, length, newText)` but only records a change if [newText] is actually
319 * different from the current buffer value.
320 */
321 internal fun setTextIfChanged(newText: CharSequence) {
322 findCommonPrefixAndSuffix(buffer, newText) { thisStart, thisEnd, newStart, newEnd ->
323 replace(thisStart, thisEnd, newText, newStart, newEnd)
324 }
325 }
326
327 // Doc inherited from Appendable.
328 // This append overload should be first so it ends up being the target of links to this method.
329 @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
330 override fun append(text: CharSequence?): Appendable = apply {
331 if (text != null) {
332 onTextWillChange(length, length, text.length)
333 buffer.replace(buffer.length, buffer.length, text)
334 }
335 }
336
337 // Doc inherited from Appendable.
338 @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
339 override fun append(text: CharSequence?, start: Int, end: Int): Appendable = apply {
340 if (text != null) {
341 onTextWillChange(length, length, end - start)
342 buffer.replace(buffer.length, buffer.length, text.subSequence(start, end))
343 }
344 }
345
346 // Doc inherited from Appendable.
347 @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
348 override fun append(char: Char): Appendable = apply {
349 onTextWillChange(length, length, 1)
350 buffer.replace(buffer.length, buffer.length, char.toString())
351 }
352
353 /**
354 * Called just before the text contents are about to change.
355 *
356 * @param replaceStart The first offset to be replaced (inclusive).
357 * @param replaceEnd The last offset to be replaced (exclusive).
358 * @param newLength The length of the replacement.
359 */
360 private fun onTextWillChange(replaceStart: Int, replaceEnd: Int, newLength: Int) {
361 changeTracker.trackChange(replaceStart, replaceEnd, newLength)
362 offsetMappingCalculator?.recordEditOperation(replaceStart, replaceEnd, newLength)
363 // On Android, IME calls are usually followed with an explicit change to selection.
364 // Therefore it might seem unnecessary to adjust the selection here. However, this sort of
365 // behavior is not expected for edits that are coming from the developer programmatically
366 // or desktop APIs. So, we make sure that the selection is placed at a reasonable place
367 // after any kind of edit.
368 selectionInChars = adjustTextRange(selection, replaceStart, replaceEnd, newLength)
369 }
370
371 // endregion
372
373 /** Returns the [Char] at [index] in this buffer. */
374 fun charAt(index: Int): Char = buffer[index]
375
376 override fun toString(): String = buffer.toString()
377
378 /**
379 * Returns a [CharSequence] backed by this buffer. Any subsequent changes to this buffer will be
380 * visible in the returned sequence as well.
381 */
382 fun asCharSequence(): CharSequence = buffer
383
384 private fun clearChangeList() {
385 changeTracker.clearChanges()
386 }
387
388 /**
389 * Revert all changes made to this value since it was created.
390 *
391 * After calling this method, this object will be in the same state it was when it was initially
392 * created, and [changes] will be empty.
393 */
394 fun revertAllChanges() {
395 replace(0, length, originalValue.toString())
396 selection = originalValue.selection
397 clearChangeList()
398 }
399
400 /**
401 * Places the cursor before the character at the given index.
402 *
403 * If [index] is inside a surrogate pair or other invalid run, the cursor will be placed at the
404 * nearest earlier index.
405 *
406 * To place the cursor at the beginning of the field, pass index 0. To place the cursor at the
407 * end of the field, after the last character, pass index [TextFieldBuffer.length] or call
408 * [placeCursorAtEnd].
409 *
410 * @param index Character index to place cursor before, should be in range 0 to
411 * [TextFieldBuffer.length], inclusive.
412 * @see placeCursorAfterCharAt
413 */
414 fun placeCursorBeforeCharAt(index: Int) {
415 requireValidIndex(index, startExclusive = true, endExclusive = false)
416 // skip further validation
417 selectionInChars = TextRange(index)
418 }
419
420 /**
421 * Places the cursor after the character at the given index.
422 *
423 * If [index] is inside a surrogate pair or other invalid run, the cursor will be placed at the
424 * nearest later index.
425 *
426 * To place the cursor at the end of the field, after the last character, pass index
427 * [TextFieldBuffer.length] or call [placeCursorAtEnd].
428 *
429 * @param index Character index to place cursor after, should be in range 0 (inclusive) to
430 * [TextFieldBuffer.length] (exclusive).
431 * @see placeCursorBeforeCharAt
432 */
433 fun placeCursorAfterCharAt(index: Int) {
434 requireValidIndex(index, startExclusive = false, endExclusive = true)
435 // skip further validation
436 selectionInChars = TextRange((index + 1).coerceAtMost(length))
437 }
438
439 /**
440 * Returns an immutable [TextFieldCharSequence] that has the same contents of this buffer.
441 *
442 * @param selection The selection for the returned [TextFieldCharSequence]. Default value is
443 * this buffer's selection. Passing a different value in here _only_ affects the return value,
444 * it does not change the current selection in the buffer.
445 * @param composition The composition range for the returned [TextFieldCharSequence]. Default
446 * value is this buffer's current composition.
447 */
448 internal fun toTextFieldCharSequence(
449 selection: TextRange = this.selection,
450 composition: TextRange? = this.composition,
451 composingAnnotations: List<PlacedAnnotation>? =
452 this.composingAnnotations?.asMutableList()?.takeIf { it.isNotEmpty() },
453 ): TextFieldCharSequence =
454 TextFieldCharSequence(
455 text = buffer.toString(),
456 selection = selection,
457 composition = composition,
458 composingAnnotations = composingAnnotations
459 )
460
461 private fun requireValidIndex(index: Int, startExclusive: Boolean, endExclusive: Boolean) {
462 val start = if (startExclusive) 0 else -1
463 val end = if (endExclusive) length else length + 1
464
465 requirePrecondition(index in start until end) { "Expected $index to be in [$start, $end)" }
466 }
467
468 private fun requireValidRange(range: TextRange) {
469 val validRange = TextRange(0, length)
470 requirePrecondition(range in validRange) { "Expected $range to be in $validRange" }
471 }
472
473 /**
474 * The ordered list of non-overlapping and discontinuous changes performed on a
475 * [TextFieldBuffer] during the current [edit][TextFieldState.edit] or
476 * [filter][InputTransformation.transformInput] operation. Changes are listed in the order they
477 * appear in the text, not the order in which they were made. Overlapping changes are
478 * represented as a single change.
479 */
480 interface ChangeList {
481 /** The number of changes that have been performed. */
482 val changeCount: Int
483
484 /**
485 * Returns the range in the [TextFieldBuffer] that was changed.
486 *
487 * @throws IndexOutOfBoundsException If [changeIndex] is not in [0, [changeCount]).
488 */
489 fun getRange(changeIndex: Int): TextRange
490
491 /**
492 * Returns the range in the original text that was replaced.
493 *
494 * @throws IndexOutOfBoundsException If [changeIndex] is not in [0, [changeCount]).
495 */
496 fun getOriginalRange(changeIndex: Int): TextRange
497 }
498 }
499
500 /**
501 * Given [originalRange], calculates its new placement in the buffer after a region starting from
502 * [replaceStart] (inclusive) ending at [replaceEnd] (exclusive) is deleted and [insertedTextLength]
503 * number of characters are inserted at [replaceStart]. The rules of the adjustment are as follows;
504 * - '||'; denotes the [originalRange]
505 * - '\/'; denotes the [replaceStart], [replaceEnd]
506 *
507 * If the [originalRange]
508 * - is before the replaced region, it remains in the same place.
509 * - abcd|efg|hijk\lmno/pqrs => abcd|efg|hijkxyzpqrs
510 * - TextRange(4, 7) => TextRange(4, 7)
511 * - is after the replaced region, it is moved by the difference in length after replacement,
512 * essentially corresponding to the same part of the text.
513 * - abcd\efg/hijk|lmno|pqrs => abcdxyzxyzxyzhijk|lmno|pqrs
514 * - TextRange(11, 15) => TextRange(17, 21)
515 * - fully wraps the replaced region, only the end is adjusted.
516 * - ab|cd\efg/hijklmno|pqrs => ab|cdxyzxyzxyzhijklmno|pqrs
517 * - TextRange(2, 15) => TextRange(2, 21)
518 * - is inside the replaced region, range is collapsed and moved to the end of the replaced region.
519 * - ab\cd|efg|hijklmno/pqrs => abxyzxyz|pqrs
520 * - TextRange(4, 7) => TextRange(8, 8)
521 * - collides with the replaced region at the start or at the end, it is adjusted so that the
522 * colliding range is not included anymore.
523 * - abcd|efg\hijk|lm/nopqrs => abcd|efg|xyzxyznopqrs
524 * - TextRange(4, 11) => TextRange(4, 7)
525 */
adjustTextRangenull526 internal fun adjustTextRange(
527 originalRange: TextRange,
528 replaceStart: Int,
529 replaceEnd: Int,
530 insertedTextLength: Int
531 ): TextRange {
532 var selStart = originalRange.min
533 var selEnd = originalRange.max
534
535 if (selEnd < replaceStart) {
536 // The entire originalRange is before the insertion point – we don't have to adjust
537 // the mark at all, so skip the math.
538 return originalRange
539 }
540
541 if (selStart <= replaceStart && replaceEnd <= selEnd) {
542 // The insertion is entirely inside the originalRange, move the end only.
543 val diff = insertedTextLength - (replaceEnd - replaceStart)
544 // Preserve "cursorness".
545 if (selStart == selEnd) {
546 selStart += diff
547 }
548 selEnd += diff
549 } else if (selStart > replaceStart && selEnd < replaceEnd) {
550 // originalRange is entirely inside replacement, move it to the end.
551 selStart = replaceStart + insertedTextLength
552 selEnd = replaceStart + insertedTextLength
553 } else if (selStart >= replaceEnd) {
554 // The entire originalRange is after the insertion, so shift everything forward.
555 val diff = insertedTextLength - (replaceEnd - replaceStart)
556 selStart += diff
557 selEnd += diff
558 } else if (replaceStart < selStart) {
559 // Insertion is around start of originalRange, truncate start of originalRange.
560 selStart = replaceStart + insertedTextLength
561 selEnd += insertedTextLength - (replaceEnd - replaceStart)
562 } else {
563 // Insertion is around end of originalRange, truncate end of originalRange.
564 selEnd = replaceStart
565 }
566 // should not validate
567 return TextRange(selStart, selEnd)
568 }
569
570 /**
571 * Insert [text] at the given [index] in this value. Pass 0 to insert [text] at the beginning of
572 * this buffer, and pass [TextFieldBuffer.length] to insert [text] at the end of this buffer.
573 *
574 * This is equivalent to calling `replace(index, index, text)`.
575 *
576 * @param index The character offset at which to insert [text].
577 * @param text The text to insert.
578 * @see TextFieldBuffer.replace
579 * @see TextFieldBuffer.append
580 * @see TextFieldBuffer.delete
581 */
insertnull582 fun TextFieldBuffer.insert(index: Int, text: String) {
583 replace(index, index, text)
584 }
585
586 /**
587 * Delete the text between [start] (inclusive) and [end] (exclusive). Pass 0 as [start] and
588 * [TextFieldBuffer.length] as [end] to delete everything in this buffer.
589 *
590 * @param start The character offset of the first character to delete.
591 * @param end The character offset of the first character after the deleted range.
592 * @see TextFieldBuffer.replace
593 * @see TextFieldBuffer.append
594 * @see TextFieldBuffer.insert
595 */
TextFieldBuffernull596 fun TextFieldBuffer.delete(start: Int, end: Int) {
597 replace(start, end, "")
598 }
599
600 /** Places the cursor at the end of the text. */
placeCursorAtEndnull601 fun TextFieldBuffer.placeCursorAtEnd() {
602 placeCursorBeforeCharAt(length)
603 }
604
605 /** Places the selection around all the text. */
selectAllnull606 fun TextFieldBuffer.selectAll() {
607 selection = TextRange(0, length)
608 }
609
610 /**
611 * Iterates over all the changes in this [ChangeList].
612 *
613 * Changes are iterated by index, so any changes made by [block] after the current one will be
614 * visited by [block]. [block] should not make any new changes _before_ the current one or changes
615 * will be visited more than once. If you need to make changes, consider using
616 * [forEachChangeReversed].
617 *
618 * @sample androidx.compose.foundation.samples.BasicTextFieldChangeIterationSample
619 * @see forEachChangeReversed
620 */
621 @ExperimentalFoundationApi
forEachChangenull622 inline fun ChangeList.forEachChange(block: (range: TextRange, originalRange: TextRange) -> Unit) {
623 var i = 0
624 // Check the size every iteration in case more changes were performed.
625 while (i < changeCount) {
626 block(getRange(i), getOriginalRange(i))
627 i++
628 }
629 }
630
631 /**
632 * Iterates over all the changes in this [ChangeList] in reverse order.
633 *
634 * Changes are iterated by index, so [block] should not perform any new changes before the current
635 * one or changes may be skipped. [block] may make non-overlapping changes after the current one
636 * safely, such changes will not be visited.
637 *
638 * @sample androidx.compose.foundation.samples.BasicTextFieldChangeReverseIterationSample
639 * @see forEachChange
640 */
641 @ExperimentalFoundationApi
forEachChangeReversednull642 inline fun ChangeList.forEachChangeReversed(
643 block: (range: TextRange, originalRange: TextRange) -> Unit
644 ) {
645 var i = changeCount - 1
646 while (i >= 0) {
647 block(getRange(i), getOriginalRange(i))
648 i--
649 }
650 }
651
652 /**
653 * Finds the common prefix and suffix between [a] and [b] and then reports the ranges of each that
654 * excludes those. The values are reported via an (inline) callback instead of a return value to
655 * avoid having to allocate something to hold them. If the [CharSequence]s are identical, the
656 * callback is not invoked.
657 *
658 * E.g. given `a="abcde"` and `b="abbbdefe"`, the middle diff for `a` is `"ab|cd|e"` and for `b` is
659 * `ab|bbdef|e`, so reports `aMiddle=TextRange(2, 4)` and `bMiddle=TextRange(2, 7)`.
660 */
findCommonPrefixAndSuffixnull661 internal inline fun findCommonPrefixAndSuffix(
662 a: CharSequence,
663 b: CharSequence,
664 onFound: (aPrefixStart: Int, aSuffixStart: Int, bPrefixStart: Int, bSuffixStart: Int) -> Unit
665 ) {
666 var aStart = 0
667 var aEnd = a.length
668 var bStart = 0
669 var bEnd = b.length
670
671 // If either one is empty, the diff range is the entire non-empty one.
672 if (a.isNotEmpty() && b.isNotEmpty()) {
673 var prefixFound = false
674 var suffixFound = false
675
676 do {
677 if (!prefixFound) {
678 if (a[aStart] == b[bStart]) {
679 aStart += 1
680 bStart += 1
681 } else {
682 prefixFound = true
683 }
684 }
685 if (!suffixFound) {
686 if (a[aEnd - 1] == b[bEnd - 1]) {
687 aEnd -= 1
688 bEnd -= 1
689 } else {
690 suffixFound = true
691 }
692 }
693 } while (
694 // As soon as we've completely traversed one of the strings, if the other hasn't also
695 // finished being traversed then we've found the diff region.
696 aStart < aEnd &&
697 bStart < bEnd &&
698 // If we've found the end of the common prefix and the start of the common suffix
699 // we're
700 // done.
701 !(prefixFound && suffixFound)
702 )
703 }
704
705 if (aStart >= aEnd && bStart >= bEnd) {
706 return
707 }
708
709 onFound(aStart, aEnd, bStart, bEnd)
710 }
711
712 /**
713 * Normally [TextFieldBuffer] throws an [IllegalArgumentException] when an invalid selection change
714 * is attempted. However internally and especially for selection ranges coming from the IME we
715 * coerce the given numbers to a valid range to not crash. Also, IMEs sometimes send values like
716 * `Int.MAX_VALUE` to move selection to end.
717 */
setSelectionCoercednull718 internal fun TextFieldBuffer.setSelectionCoerced(start: Int, end: Int = start) {
719 selection = TextRange(start.coerceIn(0, length), end.coerceIn(0, length))
720 }
721