From 3da7db90bc6d5583ae755adfde8c0580e44b5f1f Mon Sep 17 00:00:00 2001 From: MohamedRejeb Date: Mon, 30 Sep 2024 12:07:18 +0100 Subject: [PATCH] Support String Operations on Rich Text while retaining styles --- docs/rich_text_state.md | 24 ++ .../api/android/richeditor-compose.api | 5 + .../api/desktop/richeditor-compose.api | 5 + .../api/richeditor-compose.klib.api | 5 + .../richeditor/model/RichTextState.kt | 306 +++++++++++------- .../model/RichTextStateTest.kt | 153 +++++++++ 6 files changed, 375 insertions(+), 123 deletions(-) diff --git a/docs/rich_text_state.md b/docs/rich_text_state.md index 3b422c72..6ae1295c 100644 --- a/docs/rich_text_state.md +++ b/docs/rich_text_state.md @@ -44,4 +44,28 @@ The editor's selection can be changed using the `RichTextState.selection` proper ```kotlin richTextState.selection = TextRange(0, 5) +``` + +### Performing string operations on rich text + +The `RichTextState` class provides a set of functions to perform string operations on the rich text while preserving the styles. + +```kotlin +// Insert text at custom posiotn. +richTextState.addTextAtIndex(5, "Hello") + +// Insert text after the current selection. +richTextState.addTextAfterSelection("Hello") + +// Remove text range. +richTextState.removeTextRange(TextRange(0, 5)) + +// Remove selected text. +richTextState.removeSelectedText() + +// Replace text range. +richTextState.replaceTextRange(TextRange(0, 5), "Hello") + +// Replace selected text. +richTextState.replaceSelectedText("Hello") ``` \ No newline at end of file diff --git a/richeditor-compose/api/android/richeditor-compose.api b/richeditor-compose/api/android/richeditor-compose.api index 278f1b7f..d22b06a1 100644 --- a/richeditor-compose/api/android/richeditor-compose.api +++ b/richeditor-compose/api/android/richeditor-compose.api @@ -134,6 +134,8 @@ public final class com/mohamedrejeb/richeditor/model/RichTextState { public final fun addRichSpan-FDrldGo (Lcom/mohamedrejeb/richeditor/model/RichSpanStyle;J)V public final fun addSpanStyle (Landroidx/compose/ui/text/SpanStyle;)V public final fun addSpanStyle-FDrldGo (Landroidx/compose/ui/text/SpanStyle;J)V + public final fun addTextAfterSelection (Ljava/lang/String;)V + public final fun addTextAtIndex (ILjava/lang/String;)V public final fun addUnorderedList ()V public final fun clear ()V public final fun clearRichSpans ()V @@ -167,10 +169,13 @@ public final class com/mohamedrejeb/richeditor/model/RichTextState { public final fun removeParagraphStyle (Landroidx/compose/ui/text/ParagraphStyle;)V public final fun removeRichSpan (Lcom/mohamedrejeb/richeditor/model/RichSpanStyle;)V public final fun removeRichSpan-FDrldGo (Lcom/mohamedrejeb/richeditor/model/RichSpanStyle;J)V + public final fun removeSelectedText ()V public final fun removeSpanStyle (Landroidx/compose/ui/text/SpanStyle;)V public final fun removeSpanStyle-FDrldGo (Landroidx/compose/ui/text/SpanStyle;J)V + public final fun removeTextRange-5zc-tL8 (J)V public final fun removeUnorderedList ()V public final fun replaceSelectedText (Ljava/lang/String;)V + public final fun replaceTextRange-72CqOWE (JLjava/lang/String;)V public final fun setConfig-kmsmbh4 (JLandroidx/compose/ui/text/style/TextDecoration;JJJI)V public static synthetic fun setConfig-kmsmbh4$default (Lcom/mohamedrejeb/richeditor/model/RichTextState;JLandroidx/compose/ui/text/style/TextDecoration;JJJIILjava/lang/Object;)V public final fun setHtml (Ljava/lang/String;)Lcom/mohamedrejeb/richeditor/model/RichTextState; diff --git a/richeditor-compose/api/desktop/richeditor-compose.api b/richeditor-compose/api/desktop/richeditor-compose.api index 9c5fd9cd..80975eaf 100644 --- a/richeditor-compose/api/desktop/richeditor-compose.api +++ b/richeditor-compose/api/desktop/richeditor-compose.api @@ -134,6 +134,8 @@ public final class com/mohamedrejeb/richeditor/model/RichTextState { public final fun addRichSpan-FDrldGo (Lcom/mohamedrejeb/richeditor/model/RichSpanStyle;J)V public final fun addSpanStyle (Landroidx/compose/ui/text/SpanStyle;)V public final fun addSpanStyle-FDrldGo (Landroidx/compose/ui/text/SpanStyle;J)V + public final fun addTextAfterSelection (Ljava/lang/String;)V + public final fun addTextAtIndex (ILjava/lang/String;)V public final fun addUnorderedList ()V public final fun clear ()V public final fun clearRichSpans ()V @@ -167,10 +169,13 @@ public final class com/mohamedrejeb/richeditor/model/RichTextState { public final fun removeParagraphStyle (Landroidx/compose/ui/text/ParagraphStyle;)V public final fun removeRichSpan (Lcom/mohamedrejeb/richeditor/model/RichSpanStyle;)V public final fun removeRichSpan-FDrldGo (Lcom/mohamedrejeb/richeditor/model/RichSpanStyle;J)V + public final fun removeSelectedText ()V public final fun removeSpanStyle (Landroidx/compose/ui/text/SpanStyle;)V public final fun removeSpanStyle-FDrldGo (Landroidx/compose/ui/text/SpanStyle;J)V + public final fun removeTextRange-5zc-tL8 (J)V public final fun removeUnorderedList ()V public final fun replaceSelectedText (Ljava/lang/String;)V + public final fun replaceTextRange-72CqOWE (JLjava/lang/String;)V public final fun setConfig-kmsmbh4 (JLandroidx/compose/ui/text/style/TextDecoration;JJJI)V public static synthetic fun setConfig-kmsmbh4$default (Lcom/mohamedrejeb/richeditor/model/RichTextState;JLandroidx/compose/ui/text/style/TextDecoration;JJJIILjava/lang/Object;)V public final fun setHtml (Ljava/lang/String;)Lcom/mohamedrejeb/richeditor/model/RichTextState; diff --git a/richeditor-compose/api/richeditor-compose.klib.api b/richeditor-compose/api/richeditor-compose.klib.api index b281513c..ebc35522 100644 --- a/richeditor-compose/api/richeditor-compose.klib.api +++ b/richeditor-compose/api/richeditor-compose.klib.api @@ -183,6 +183,8 @@ final class com.mohamedrejeb.richeditor.model/RichTextState { // com.mohamedreje final fun addRichSpan(com.mohamedrejeb.richeditor.model/RichSpanStyle, androidx.compose.ui.text/TextRange) // com.mohamedrejeb.richeditor.model/RichTextState.addRichSpan|addRichSpan(com.mohamedrejeb.richeditor.model.RichSpanStyle;androidx.compose.ui.text.TextRange){}[0] final fun addSpanStyle(androidx.compose.ui.text/SpanStyle) // com.mohamedrejeb.richeditor.model/RichTextState.addSpanStyle|addSpanStyle(androidx.compose.ui.text.SpanStyle){}[0] final fun addSpanStyle(androidx.compose.ui.text/SpanStyle, androidx.compose.ui.text/TextRange) // com.mohamedrejeb.richeditor.model/RichTextState.addSpanStyle|addSpanStyle(androidx.compose.ui.text.SpanStyle;androidx.compose.ui.text.TextRange){}[0] + final fun addTextAfterSelection(kotlin/String) // com.mohamedrejeb.richeditor.model/RichTextState.addTextAfterSelection|addTextAfterSelection(kotlin.String){}[0] + final fun addTextAtIndex(kotlin/Int, kotlin/String) // com.mohamedrejeb.richeditor.model/RichTextState.addTextAtIndex|addTextAtIndex(kotlin.Int;kotlin.String){}[0] final fun addUnorderedList() // com.mohamedrejeb.richeditor.model/RichTextState.addUnorderedList|addUnorderedList(){}[0] final fun clear() // com.mohamedrejeb.richeditor.model/RichTextState.clear|clear(){}[0] final fun clearRichSpans() // com.mohamedrejeb.richeditor.model/RichTextState.clearRichSpans|clearRichSpans(){}[0] @@ -202,10 +204,13 @@ final class com.mohamedrejeb.richeditor.model/RichTextState { // com.mohamedreje final fun removeParagraphStyle(androidx.compose.ui.text/ParagraphStyle) // com.mohamedrejeb.richeditor.model/RichTextState.removeParagraphStyle|removeParagraphStyle(androidx.compose.ui.text.ParagraphStyle){}[0] final fun removeRichSpan(com.mohamedrejeb.richeditor.model/RichSpanStyle) // com.mohamedrejeb.richeditor.model/RichTextState.removeRichSpan|removeRichSpan(com.mohamedrejeb.richeditor.model.RichSpanStyle){}[0] final fun removeRichSpan(com.mohamedrejeb.richeditor.model/RichSpanStyle, androidx.compose.ui.text/TextRange) // com.mohamedrejeb.richeditor.model/RichTextState.removeRichSpan|removeRichSpan(com.mohamedrejeb.richeditor.model.RichSpanStyle;androidx.compose.ui.text.TextRange){}[0] + final fun removeSelectedText() // com.mohamedrejeb.richeditor.model/RichTextState.removeSelectedText|removeSelectedText(){}[0] final fun removeSpanStyle(androidx.compose.ui.text/SpanStyle) // com.mohamedrejeb.richeditor.model/RichTextState.removeSpanStyle|removeSpanStyle(androidx.compose.ui.text.SpanStyle){}[0] final fun removeSpanStyle(androidx.compose.ui.text/SpanStyle, androidx.compose.ui.text/TextRange) // com.mohamedrejeb.richeditor.model/RichTextState.removeSpanStyle|removeSpanStyle(androidx.compose.ui.text.SpanStyle;androidx.compose.ui.text.TextRange){}[0] + final fun removeTextRange(androidx.compose.ui.text/TextRange) // com.mohamedrejeb.richeditor.model/RichTextState.removeTextRange|removeTextRange(androidx.compose.ui.text.TextRange){}[0] final fun removeUnorderedList() // com.mohamedrejeb.richeditor.model/RichTextState.removeUnorderedList|removeUnorderedList(){}[0] final fun replaceSelectedText(kotlin/String) // com.mohamedrejeb.richeditor.model/RichTextState.replaceSelectedText|replaceSelectedText(kotlin.String){}[0] + final fun replaceTextRange(androidx.compose.ui.text/TextRange, kotlin/String) // com.mohamedrejeb.richeditor.model/RichTextState.replaceTextRange|replaceTextRange(androidx.compose.ui.text.TextRange;kotlin.String){}[0] final fun setConfig(androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.text.style/TextDecoration? = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., kotlin/Int = ...) // com.mohamedrejeb.richeditor.model/RichTextState.setConfig|setConfig(androidx.compose.ui.graphics.Color;androidx.compose.ui.text.style.TextDecoration?;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;kotlin.Int){}[0] final fun setHtml(kotlin/String): com.mohamedrejeb.richeditor.model/RichTextState // com.mohamedrejeb.richeditor.model/RichTextState.setHtml|setHtml(kotlin.String){}[0] final fun setMarkdown(kotlin/String): com.mohamedrejeb.richeditor.model/RichTextState // com.mohamedrejeb.richeditor.model/RichTextState.setMarkdown|setMarkdown(kotlin.String){}[0] 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 e244ab8e..ed236252 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 @@ -15,7 +15,6 @@ import androidx.compose.ui.text.input.TransformedText import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.sp import androidx.compose.ui.util.fastFirstOrNull import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachIndexed @@ -208,6 +207,10 @@ public class RichTextState internal constructor( updateRichParagraphList(initialRichParagraphList) } + /** + * Public methods + */ + @Deprecated( message = "Use config instead", replaceWith = ReplaceWith("config"), @@ -242,6 +245,124 @@ public class RichTextState internal constructor( updateTextFieldValue(textFieldValue) } + // Text + + /** + * Removes the selected text from the current text input. + * + * This method removes the text specified by the `selection` from the current text input. + * + * @see removeTextRange + */ + public fun removeSelectedText(): Unit = + removeTextRange(selection) + + /**x + * Removes the specified text range from the current text. + * + * @param textRange the range of text to be removed + */ + public fun removeTextRange( + textRange: TextRange + ) { + require(textRange.min >= 0) { + "The start index must be non-negative." + } + + 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}." + } + + onTextFieldValueChange( + newTextFieldValue = textFieldValue.copy( + text = textFieldValue.text.removeRange( + startIndex = textRange.min, + endIndex = textRange.max, + ), + selection = TextRange(textRange.min), + ) + ) + } + + /** + * Replaces the currently selected text with the provided text. + * + * @param text The new text to be inserted + */ + public fun replaceSelectedText(text: String): Unit = + replaceTextRange(selection, text) + + /** + * Replaces the text in the specified range with the provided text. + * + * @param textRange The range of text to be replaced + * @param text The new text to be inserted + */ + public fun replaceTextRange( + textRange: TextRange, + text: String + ) { + require(textRange.min >= 0) { + "The start index must be non-negative." + } + + 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}." + } + + removeTextRange(textRange) + addTextAfterSelection(text = text) + } + + /** + * Adds the provided text to the text field at the current selection. + * + * @param text The text to be added + */ + public fun addTextAfterSelection(text: String): Unit = + addTextAtIndex( + index = selection.min, + text = text + ) + + /** + * Adds the provided text to the text field at the specified index. + * + * @param index The index at which the text should be added + * @param text The text to be added + */ + public fun addTextAtIndex( + index: Int, + text: String, + ) { + require(index >= 0) { + "The index must be non-negative." + } + + require(index <= textFieldValue.text.length) { + "The index must be within the text bounds. " + + "The text length is ${textFieldValue.text.length}, " + + "but the index is $index." + } + + val beforeText = textFieldValue.text.substring(0, index) + val afterText = textFieldValue.text.substring(selection.max) + val newText = "$beforeText$text$afterText" + + onTextFieldValueChange( + newTextFieldValue = textFieldValue.copy( + text = newText, + selection = TextRange(index + text.length), + ) + ) + } + + // SpanStyle + /** * Returns the [SpanStyle] of the text at the specified text range. * If the text range is collapsed, the style of the character preceding the text range is returned. @@ -308,28 +429,6 @@ public class RichTextState internal constructor( ?: RichParagraph.DefaultParagraphStyle } - /** - * Returns the [ParagraphType] of the text at the specified text range. - * If the text range is collapsed, the type of the paragraph containing the text range is returned. - * - * @param textRange the text range. - * @return the [ParagraphType] of the text at the specified text range. - */ - internal fun getParagraphType(textRange: TextRange): ParagraphType = - if (textRange.collapsed) { - val richParagraph = getRichParagraphByTextIndex(textIndex = textRange.min - 1) - - richParagraph - ?.type - ?: DefaultParagraph() - } else { - val richParagraphList = getRichParagraphListByTextRange(textRange) - - richParagraphList - .getCommonType() - ?: DefaultParagraph() - } - /** * Toggle the [SpanStyle] * If the passed span style doesn't exist in the [currentSpanStyle] it's going to be added. @@ -465,6 +564,8 @@ public class RichTextState internal constructor( removeSpanStyle(currentSpanStyle, textRange) } + // RichSpanStyle + /** * Add a link to the text field. * The link is going to be added after the current selection. @@ -585,47 +686,6 @@ public class RichTextState internal constructor( updateTextFieldValue(textFieldValue) } - private fun getSelectedLinkRichSpan(): RichSpan? { - val richSpan = getRichSpanByTextIndex(selection.min - 1) - - return getLinkRichSpan(richSpan) - } - - /** - * Replaces the currently selected text with the provided text. - * - * @param text The new text to be inserted - */ - public fun replaceSelectedText(text: String) { - removeSelectedText() - addTextAfterSelection(text = text) - } - - private fun addTextAfterSelection( - text: String - ) { - addText( - text = text, - index = selection.min - ) - } - - private fun addText( - text: String, - index: Int, - ) { - val beforeText = textFieldValue.text.substring(0, index) - val afterText = textFieldValue.text.substring(selection.max) - val newText = "$beforeText$text$afterText" - - onTextFieldValueChange( - newTextFieldValue = textFieldValue.copy( - text = newText, - selection = TextRange(index + text.length), - ) - ) - } - @Deprecated( message = "Use toggleCodeSpan instead", replaceWith = ReplaceWith("toggleCodeSpan()"), @@ -653,8 +713,6 @@ public class RichTextState internal constructor( public fun removeCodeSpan(): Unit = removeRichSpan(RichSpanStyle.Code()) - //RichSpanStyle - public fun toggleRichSpan(spanStyle: RichSpanStyle) { if (isRichSpan(spanStyle::class)) removeRichSpan(spanStyle) @@ -858,19 +916,6 @@ public class RichTextState internal constructor( } } - private fun addUnorderedList(paragraph: RichParagraph) { - if (paragraph.type is UnorderedList) return - - val newType = UnorderedList( - initialIndent = config.unorderedListIndent - ) - - updateParagraphType( - paragraph = paragraph, - newType = newType - ) - } - public fun removeUnorderedList() { val paragraphs = getRichParagraphListByTextRange(selection) @@ -879,12 +924,6 @@ public class RichTextState internal constructor( } } - private fun removeUnorderedList(paragraph: RichParagraph) { - if (paragraph.type !is UnorderedList) return - - resetParagraphType(paragraph = paragraph) - } - public fun toggleOrderedList() { val paragraphs = getRichParagraphListByTextRange(selection) if (paragraphs.isEmpty()) return @@ -906,6 +945,65 @@ public class RichTextState internal constructor( } } + public fun removeOrderedList() { + val paragraphs = getRichParagraphListByTextRange(selection) + + paragraphs.forEach { paragraph -> + removeOrderedList(paragraph) + } + } + + /** + * Private/Internal methods + */ + + /** + * Returns the [ParagraphType] of the text at the specified text range. + * If the text range is collapsed, the type of the paragraph containing the text range is returned. + * + * @param textRange the text range. + * @return the [ParagraphType] of the text at the specified text range. + */ + internal fun getParagraphType(textRange: TextRange): ParagraphType = + if (textRange.collapsed) { + val richParagraph = getRichParagraphByTextIndex(textIndex = textRange.min - 1) + + richParagraph + ?.type + ?: DefaultParagraph() + } else { + val richParagraphList = getRichParagraphListByTextRange(textRange) + + richParagraphList + .getCommonType() + ?: DefaultParagraph() + } + + private fun getSelectedLinkRichSpan(): RichSpan? { + val richSpan = getRichSpanByTextIndex(selection.min - 1) + + return getLinkRichSpan(richSpan) + } + + private fun addUnorderedList(paragraph: RichParagraph) { + if (paragraph.type is UnorderedList) return + + val newType = UnorderedList( + initialIndent = config.unorderedListIndent + ) + + updateParagraphType( + paragraph = paragraph, + newType = newType + ) + } + + private fun removeUnorderedList(paragraph: RichParagraph) { + if (paragraph.type !is UnorderedList) return + + resetParagraphType(paragraph = paragraph) + } + private fun addOrderedList(paragraph: RichParagraph) { if (paragraph.type is OrderedList) return val index = richParagraphList.indexOf(paragraph) @@ -938,14 +1036,6 @@ public class RichTextState internal constructor( ) } - public fun removeOrderedList() { - val paragraphs = getRichParagraphListByTextRange(selection) - - paragraphs.forEach { paragraph -> - removeOrderedList(paragraph) - } - } - private fun removeOrderedList(paragraph: RichParagraph) { if (paragraph.type !is OrderedList) return val index = richParagraphList.indexOf(paragraph) @@ -1686,36 +1776,6 @@ public class RichTextState internal constructor( } } - /** - * Removes the selected text from the current text input. - * - * This method removes the text specified by the `selection` from the current text input. - * - * @see removeTextRange - */ - private fun removeSelectedText() { - removeTextRange(selection) - } - - /**x - * Removes the specified text range from the current text. - * - * @param textRange the range of text to be removed - */ - private fun removeTextRange( - textRange: TextRange - ) { - onTextFieldValueChange( - newTextFieldValue = textFieldValue.copy( - text = textFieldValue.text.removeRange( - startIndex = selection.min, - endIndex = selection.max, - ), - selection = TextRange(textRange.min), - ) - ) - } - /** * Handles adding or removing the style in [toAddSpanStyle] and [toRemoveSpanStyle] from the selected text. */ diff --git a/richeditor-compose/src/commonTest/kotlin/com.mohamedrejeb.richeditor/model/RichTextStateTest.kt b/richeditor-compose/src/commonTest/kotlin/com.mohamedrejeb.richeditor/model/RichTextStateTest.kt index 96b27398..9cfe5202 100644 --- a/richeditor-compose/src/commonTest/kotlin/com.mohamedrejeb.richeditor/model/RichTextStateTest.kt +++ b/richeditor-compose/src/commonTest/kotlin/com.mohamedrejeb.richeditor/model/RichTextStateTest.kt @@ -919,4 +919,157 @@ class RichTextStateTest { assertEquals(TextRange(8), richTextState.selection) } + @Test + fun testRemoveTextRange() { + val richTextState = RichTextState( + initialRichParagraphList = listOf( + RichParagraph( + key = 1, + ).also { + it.children.add( + RichSpan( + text = "Hello", + paragraph = it, + ), + ) + } + ) + ) + + // Remove the text range + richTextState.removeTextRange(TextRange(0, 5)) + + assertEquals("", richTextState.toText()) + assertEquals(TextRange(0), richTextState.selection) + } + + @Test + fun testRemoveSelectedText() { + val richTextState = RichTextState( + initialRichParagraphList = listOf( + RichParagraph( + key = 1, + ).also { + it.children.add( + RichSpan( + text = "Hello", + paragraph = it, + ), + ) + } + ) + ) + + // Select the text + richTextState.selection = TextRange(0, 5) + + // Remove the selected text + richTextState.removeSelectedText() + + assertEquals("", richTextState.toText()) + assertEquals(TextRange(0), richTextState.selection) + } + + @Test + fun testAddTextAtIndex() { + val richTextState = RichTextState( + initialRichParagraphList = listOf( + RichParagraph( + key = 1, + ).also { + it.children.add( + RichSpan( + text = "Hello", + paragraph = it, + ), + ) + } + ) + ) + + // Add text at index + richTextState.addTextAtIndex(5, " World") + + assertEquals("Hello World", richTextState.toText()) + assertEquals(TextRange(11), richTextState.selection) + } + + @Test + fun testAddTextAfterSelection() { + val richTextState = RichTextState( + initialRichParagraphList = listOf( + RichParagraph( + key = 1, + ).also { + it.children.add( + RichSpan( + text = "Hello", + paragraph = it, + ), + ) + } + ) + ) + + // Select the text + richTextState.selection = TextRange(5) + + // Add text after selection + richTextState.addTextAfterSelection(" World") + + assertEquals("Hello World", richTextState.toText()) + assertEquals(TextRange(11), richTextState.selection) + } + + @Test + fun testReplaceTextRange() { + val richTextState = RichTextState( + initialRichParagraphList = listOf( + RichParagraph( + key = 1, + ).also { + it.children.add( + RichSpan( + text = "Hello", + paragraph = it, + ), + ) + } + ) + ) + + // Replace the text range + richTextState.replaceTextRange(TextRange(0, 5), "Hi") + + assertEquals("Hi", richTextState.toText()) + assertEquals(TextRange(2), richTextState.selection) + } + + @Test + fun testReplaceSelectedText() { + val richTextState = RichTextState( + initialRichParagraphList = listOf( + RichParagraph( + key = 1, + ).also { + it.children.add( + RichSpan( + text = "Hello", + paragraph = it, + ), + ) + } + ) + ) + + // Select the text + richTextState.selection = TextRange(0, 5) + + // Replace the selected text + richTextState.replaceSelectedText("Hi") + + assertEquals("Hi", richTextState.toText()) + assertEquals(TextRange(2), richTextState.selection) + } + } \ No newline at end of file