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