Describe the bug
A fatal crash occurs in BasicRichTextEditor when the user scrolls or flings through a LazyColumn containing rich text editor instances. The crash is an IllegalStateException thrown by Compose's ValidatingOffsetMapping, indicating that the OffsetMapping provided by the library returns an out-of-bounds index.
Two variants of the same root cause have been observed:
IllegalStateException: OffsetMapping.originalToTransformed returned invalid mapping:
168 -> 168 is not in range of transformed text [0, 167]
IllegalStateException: OffsetMapping.transformedToOriginal returned invalid mapping:
463 -> 463 is not in range of original text [0, 460]
In both cases the mapped index is exactly 1 beyond the valid range, suggesting an off-by-one error in the OffsetMapping implementation when the transformed text length differs from the original.
Stack Trace
Fatal Exception: java.lang.IllegalStateException:
OffsetMapping.originalToTransformed returned invalid mapping:
168 -> 168 is not in range of transformed text [0, 167]
at androidx.compose.foundation.internal.InlineClassHelperKt.throwIllegalStateException
at androidx.compose.foundation.text.ValidatingOffsetMappingKt.validateOriginalToTransformed
at androidx.compose.foundation.text.ValidatingOffsetMappingKt.throwIfNotValidTransform
at androidx.compose.foundation.text.ValidatingOffsetMappingKt.filterWithValidation
at androidx.compose.foundation.text.TextFieldScrollKt.defaultTextFieldScroll
at androidx.compose.foundation.text.CoreTextFieldKt.CoreTextField
at com.mohamedrejeb.richeditor.ui.BasicRichTextEditorKt.BasicRichTextEditor$lambda$9$0 (BasicRichTextEditor.kt:254)
at androidx.compose.foundation.lazy.LazyListMeasureKt.measureLazyList
at androidx.compose.foundation.lazy.LazyListState.onScroll$foundation
To Reproduce
- Place one or more
BasicRichTextEditor instances inside a LazyColumn
- Populate the editors with rich text content
- Scroll or fling through the list rapidly
- Crash occurs
Root Cause Analysis
The stack trace confirms the crash originates from a LazyColumn scroll/fling:
LazyListState.onScroll triggers a remeasure of lazy list items
- This causes recomposition of
BasicRichTextEditor inside a lazy item
- During recomposition,
OffsetMapping.originalToTransformed (or transformedToOriginal) returns an index that is 1 beyond the valid range of the transformed/original text
- Compose's
ValidatingOffsetMapping catches this and throws IllegalStateException
The likely cause is that the OffsetMapping implementation does not clamp the returned index when the transformed text length differs from the original (e.g. due to applied spans or annotations). A possible fix:
return offset.coerceIn(0, transformedText.length)
Expected Behavior
OffsetMapping.originalToTransformed and transformedToOriginal should always return indices within the valid bounds of the respective text, even when transformed and original text lengths differ.
Environment
- Library version: 1.0.0-rc14, although it never appeared in version 1.0.0-rc13
- CMP version: 1.10.3
- Android API level: Android 14
- Device/emulator: Multiple brands
Describe the bug
A fatal crash occurs in
BasicRichTextEditorwhen the user scrolls or flings through aLazyColumncontaining rich text editor instances. The crash is anIllegalStateExceptionthrown by Compose'sValidatingOffsetMapping, indicating that theOffsetMappingprovided by the library returns an out-of-bounds index.Two variants of the same root cause have been observed:
In both cases the mapped index is exactly 1 beyond the valid range, suggesting an off-by-one error in the
OffsetMappingimplementation when the transformed text length differs from the original.Stack Trace
To Reproduce
BasicRichTextEditorinstances inside aLazyColumnRoot Cause Analysis
The stack trace confirms the crash originates from a
LazyColumnscroll/fling:LazyListState.onScrolltriggers a remeasure of lazy list itemsBasicRichTextEditorinside a lazy itemOffsetMapping.originalToTransformed(ortransformedToOriginal) returns an index that is 1 beyond the valid range of the transformed/original textValidatingOffsetMappingcatches this and throwsIllegalStateExceptionThe likely cause is that the
OffsetMappingimplementation does not clamp the returned index when the transformed text length differs from the original (e.g. due to applied spans or annotations). A possible fix:Expected Behavior
OffsetMapping.originalToTransformedandtransformedToOriginalshould always return indices within the valid bounds of the respective text, even when transformed and original text lengths differ.Environment