Skip to content

Commit

Permalink
[Android] Ensure pills are deleted all at once (#676)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonnyandrew committed May 16, 2023
1 parent 0a7ab7a commit fd9de23
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ class EditorEditTextInputTests {
.check(matches(withText("Test")))
}

@Test
fun testBackspacePill() {
onView(withId(R.id.rich_text_edit_text))
.perform(EditorActions.setLinkDisplayHandler { _, _ -> LinkDisplay.Pill })
.perform(typeText("Hello @"))
.perform(EditorActions.setLinkSuggestion("alice", "link"))
.perform(pressKey(KeyEvent.KEYCODE_DEL)) // Delete the space added after the pill
.perform(pressKey(KeyEvent.KEYCODE_DEL)) // Delete the pill
.perform(pressKey(KeyEvent.KEYCODE_DEL)) // Delete the trailing space after "Hello"
.check(matches(withText("Hello")))
}

@Test
fun testHardwareKeyboardDelete() {
onView(withId(R.id.rich_text_edit_text))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import android.view.inputmethod.*
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import io.element.android.wysiwyg.internal.utils.TextRangeHelper
import io.element.android.wysiwyg.internal.viewmodel.EditorInputAction
import io.element.android.wysiwyg.internal.viewmodel.ReplaceTextResult
import io.element.android.wysiwyg.utils.EditorIndexMapper
Expand Down Expand Up @@ -251,15 +252,18 @@ internal class InterceptInputConnection(
if (beforeLength == 0 && afterLength == 0) return false
val start = Selection.getSelectionStart(editable)
val end = Selection.getSelectionEnd(editable)
val deleteFrom = (start-beforeLength).coerceAtLeast(0)
val deleteTo = end + afterLength

var handled = false
beginBatchEdit()
if (afterLength > 0) {
val (deleteFrom, deleteTo) =
TextRangeHelper.extendRangeToReplacementSpans(
editable, start = end, length = afterLength
)

val result = withProcessor {
val action = if (afterLength > 1) {
EditorInputAction.DeleteIn(end, deleteTo)
val action = if (deleteTo - deleteFrom > 1) {
EditorInputAction.DeleteIn(deleteFrom, deleteTo)
} else {
EditorInputAction.Delete
}
Expand All @@ -275,9 +279,14 @@ internal class InterceptInputConnection(
}

if (beforeLength > 0) {
val (deleteFrom, deleteTo) =
TextRangeHelper.extendRangeToReplacementSpans(
editable, start = start - beforeLength, length = beforeLength
)

val result = withProcessor {
if (beforeLength > 1) {
updateSelection(editable, deleteFrom, start)
if (deleteTo - deleteFrom > 1) {
updateSelection(editable, deleteFrom, deleteTo)
}
processInput(EditorInputAction.BackPress)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.element.android.wysiwyg.internal.utils

import android.text.Spanned
import android.text.style.ReplacementSpan
import kotlin.math.max
import kotlin.math.min

internal object TextRangeHelper {
/**
* Return a new range that covers the given range and extends it to cover
* any replacement spans at either end.
*
* The range is returned as a pair of integers where the first is less than the last
*/
fun extendRangeToReplacementSpans(
spanned: Spanned,
start: Int,
length: Int,
): Pair<Int, Int> {
require(length > 0)
val end = start + length
val spans = spanned.getSpans(start, end, ReplacementSpan::class.java)
val firstReplacementSpanStart = spans.minOfOrNull { spanned.getSpanStart(it) }
val lastReplacementSpanEnd = spans.maxOfOrNull { spanned.getSpanEnd(it) }
val newStart = min(start, firstReplacementSpanStart ?: end)
val newEnd = max(end, lastReplacementSpanEnd ?: end)
return newStart to newEnd
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package io.element.android.wysiwyg.internal.utils

import android.graphics.Canvas
import android.graphics.Paint
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.ReplacementSpan
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class TextRangeHelperTest {
@Test(expected = IllegalArgumentException::class)
fun `given negative length, when extend to cover ReplacementSpans, it throws`() {
val text = SpannableStringBuilder("0123456789")

TextRangeHelper.extendRangeToReplacementSpans(
text, 3, -1
)
}
@Test
fun `given plain text, when extend to cover ReplacementSpans, selection is not extended`() {
val text = SpannableStringBuilder("0123456789")

val newSelection = TextRangeHelper.extendRangeToReplacementSpans(
text, 3, 4
)

assertEquals(3 to 7, newSelection)
}

@Test
fun `given ReplacementSpan at end, when extend to cover ReplacementSpans, selection extended`() {
val text = SpannableStringBuilder("0123456789")
text.setSpan(MyReplacementSpan(), 6, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

val newSelection = TextRangeHelper.extendRangeToReplacementSpans(
text, 3, 4
)

assertEquals(3 to 10, newSelection)
}

@Test
fun `given ReplacementSpan at start, when extend to cover ReplacementSpans, selection extended`() {
val text = SpannableStringBuilder("0123456789")
text.setSpan(MyReplacementSpan(), 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

val newSelection = TextRangeHelper.extendRangeToReplacementSpans(
text, 3, 4
)

assertEquals(0 to 7, newSelection)
}

@Test
fun `given ReplacementSpan immediately before and after, when extend to cover ReplacementSpans, selection not extended`() {
val text = SpannableStringBuilder("0123456789")
text.setSpan(MyReplacementSpan(), 0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
text.setSpan(MyReplacementSpan(), 7, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

val newSelection = TextRangeHelper.extendRangeToReplacementSpans(
text, 3, 4
)

assertEquals(3 to 7, newSelection)
}

@Test
fun `given ReplacementSpan at start and end, when extend to cover ReplacementSpans, selection extended`() {
val text = SpannableStringBuilder("0123456789")
text.setSpan(MyReplacementSpan(), 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
text.setSpan(MyReplacementSpan(), 6, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

val newSelection = TextRangeHelper.extendRangeToReplacementSpans(
text, 3, 4
)

assertEquals(0 to 10, newSelection)
}
}

class MyReplacementSpan : ReplacementSpan() {
override fun getSize(
paint: Paint,
text: CharSequence?,
start: Int,
end: Int,
fm: Paint.FontMetricsInt?
): Int = 10

override fun draw(
canvas: Canvas,
text: CharSequence?,
start: Int,
end: Int,
x: Float,
top: Int,
y: Int,
bottom: Int,
paint: Paint
) {
}
}

0 comments on commit fd9de23

Please sign in to comment.