From ba3f4346138fa9ed252af7cb3724426aa7380923 Mon Sep 17 00:00:00 2001 From: abarrafo Date: Tue, 22 Oct 2024 14:47:31 +0200 Subject: [PATCH 1/2] * #401 Fix for replaceTextRange * defensive change for null check * Fix for indexOutOfBounds on setText() from custom historyState. --- .../richeditor/model/RichTextState.kt | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt index 4323f653..b5b963ec 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt @@ -304,20 +304,31 @@ public class RichTextState internal constructor( textRange: TextRange, text: String ) { - require(textRange.min >= 0) { - "The start index must be non-negative." - } + // Ensure indices are within bounds + val safeMin = textRange.min.coerceIn(0, textFieldValue.text.length) + val safeMax = textRange.max.coerceIn(0, textFieldValue.text.length) - require(textRange.max <= textFieldValue.text.length) { - "The end index must be within the text bounds. " + - "The text length is ${textFieldValue.text.length}, " + - "but the end index is ${textRange.max}." + // Optional: Log or handle cases where the range was adjusted + if (safeMin != textRange.min || safeMax != textRange.max) { + // Handle the adjustment, e.g., log a warning or take corrective action + println("Adjusted text range from [$textRange.min, ${textRange.max}] to [$safeMin, $safeMax]") } - removeTextRange(textRange) - addTextAfterSelection(text = text) + // Perform the replacement using safe indices + val beforeText = textFieldValue.text.substring(0, safeMin) + val afterText = textFieldValue.text.substring(safeMax) + val newText = beforeText + text + afterText + + // Update the text field value with the new text and updated selection + onTextFieldValueChange( + newTextFieldValue = textFieldValue.copy( + text = newText, + selection = TextRange(safeMin + text.length), + ) + ) } + /** * Adds the provided text to the text field at the current selection. * @@ -1741,20 +1752,29 @@ public class RichTextState internal constructor( (!config.preserveStyleOnEmptyLine || richSpan.paragraph.isEmpty()) && isSelectionAtNewRichSpan ) { - newParagraphFirstRichSpan.spanStyle = SpanStyle() - newParagraphFirstRichSpan.richSpanStyle = RichSpanStyle.Default + if (newParagraphFirstRichSpan != null) { + newParagraphFirstRichSpan.spanStyle = SpanStyle() + } + if (newParagraphFirstRichSpan != null) { + newParagraphFirstRichSpan.richSpanStyle = RichSpanStyle.Default + } } else if ( config.preserveStyleOnEmptyLine && isSelectionAtNewRichSpan ) { - newParagraphFirstRichSpan.spanStyle = currentSpanStyle - newParagraphFirstRichSpan.richSpanStyle = currentRichSpanStyle + if (newParagraphFirstRichSpan != null) { + newParagraphFirstRichSpan.spanStyle = currentSpanStyle + } + if (newParagraphFirstRichSpan != null) { + newParagraphFirstRichSpan.richSpanStyle = currentRichSpanStyle + } } } // Get the text before and after the slice index - val beforeText = tempTextFieldValue.text.substring(0, sliceIndex + 1) - val afterText = tempTextFieldValue.text.substring(sliceIndex + 1) + val safeSliceIndex = sliceIndex.coerceAtMost(tempTextFieldValue.text.length - 1) + val beforeText = tempTextFieldValue.text.substring(0, safeSliceIndex + 1) + val afterText = tempTextFieldValue.text.substring(safeSliceIndex + 1) // Update the text field value to include the new paragraph custom start text tempTextFieldValue = tempTextFieldValue.copy( From 7cd972bb297ae9355dda16cd06224411df1cfaa7 Mon Sep 17 00:00:00 2001 From: abarrafo Date: Tue, 22 Oct 2024 14:52:31 +0200 Subject: [PATCH 2/2] Update with better safe handleing --- .../richeditor/model/RichTextState.kt | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt index b5b963ec..9b4e095c 100644 --- a/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt +++ b/richeditor-compose/src/commonMain/kotlin/com/mohamedrejeb/richeditor/model/RichTextState.kt @@ -304,30 +304,36 @@ public class RichTextState internal constructor( textRange: TextRange, text: String ) { - // Ensure indices are within bounds - val safeMin = textRange.min.coerceIn(0, textFieldValue.text.length) - val safeMax = textRange.max.coerceIn(0, textFieldValue.text.length) + // Ensure that the start and end indices are within bounds + val start = textRange.min.coerceAtLeast(0) // Ensure the start is non-negative + val end = textRange.max.coerceAtMost(textFieldValue.text.length) // Ensure the end does not exceed text length - // Optional: Log or handle cases where the range was adjusted - if (safeMin != textRange.min || safeMax != textRange.max) { - // Handle the adjustment, e.g., log a warning or take corrective action - println("Adjusted text range from [$textRange.min, ${textRange.max}] to [$safeMin, $safeMax]") + require(start <= end) { + "The start index ($start) must be less than or equal to the end index ($end)." } - // Perform the replacement using safe indices - val beforeText = textFieldValue.text.substring(0, safeMin) - val afterText = textFieldValue.text.substring(safeMax) + // Perform the replacement with safe slicing + val beforeText = textFieldValue.text.safeSubstring(0, start) + val afterText = textFieldValue.text.safeSubstring(end) val newText = beforeText + text + afterText - // Update the text field value with the new text and updated selection onTextFieldValueChange( newTextFieldValue = textFieldValue.copy( text = newText, - selection = TextRange(safeMin + text.length), + selection = TextRange(start + text.length), // Set cursor to the end of the inserted text ) ) } + // A helper function to safely slice strings without throwing exceptions + private fun String.safeSubstring(startIndex: Int, endIndex: Int = this.length): String { + return if (startIndex in 0..this.length && endIndex in startIndex..this.length) { + this.substring(startIndex, endIndex) + } else { + "" // Return empty string if indices are out of bounds + } + } + /** * Adds the provided text to the text field at the current selection.